增加website

This commit is contained in:
2026-09-14 22:24:55 +08:00
parent bb8a6936e4
commit 7e6a3e969f
57 changed files with 10568 additions and 1 deletions
+7
View File
@@ -0,0 +1,7 @@
<script setup lang="ts">
// 站点根组件:仅负责渲染路由出口
</script>
<template>
<RouterView />
</template>
+120
View File
@@ -0,0 +1,120 @@
import type {
ContentItem,
ListQuery,
ListResult,
SectionKey,
SinglePage,
} from "@/types/content";
import { products } from "@/data/products";
import { solutions } from "@/data/solutions";
import { cases } from "@/data/cases";
import { news } from "@/data/news";
import { events } from "@/data/events";
import { singlePages } from "@/data/pages";
/**
* 内容数据访问层
* 目前读取本地 mock 数据,接入后端 CMS 后只需把下面几个方法
* 换成 request.get("/cms/...") 即可,调用方无需改动。
*/
const registry: Record<SectionKey, ContentItem[]> = {
news,
products,
solutions,
cases,
events,
};
/** 模拟一次异步请求,保证调用方始终按异步处理 */
const tick = () => new Promise<void>((resolve) => setTimeout(resolve, 0));
/** 列表 / 分类 / 搜索(带分页) */
export async function fetchList(query: ListQuery): Promise<ListResult> {
await tick();
const { section, category, keyword, page = 1, pageSize = 6 } = query;
let list = [...(registry[section] ?? [])];
if (category) {
list = list.filter((item) => item.category === category);
}
const kw = keyword?.trim().toLowerCase();
if (kw) {
list = list.filter((item) =>
[item.title, item.subtitle, item.summary, ...(item.tags ?? [])]
.join(" ")
.toLowerCase()
.includes(kw)
);
}
list.sort((a, b) => (b.date ?? "").localeCompare(a.date ?? ""));
const total = list.length;
const totalPages = Math.max(1, Math.ceil(total / pageSize));
const safePage = Math.min(Math.max(1, page), totalPages);
const start = (safePage - 1) * pageSize;
return {
list: list.slice(start, start + pageSize),
total,
page: safePage,
pageSize,
totalPages,
};
}
/** 详情 */
export async function fetchDetail(
section: SectionKey,
slug: string
): Promise<ContentItem | undefined> {
await tick();
return (registry[section] ?? []).find((item) => item.slug === slug);
}
/** 相关推荐:同分类优先,不足时用同栏目其它内容补齐 */
export async function fetchRelated(
section: SectionKey,
slug: string,
limit = 3
): Promise<ContentItem[]> {
await tick();
const all = registry[section] ?? [];
const current = all.find((item) => item.slug === slug);
if (!current) return all.slice(0, limit);
const sameCategory = all.filter(
(item) => item.slug !== slug && item.category === current.category
);
const others = all.filter(
(item) => item.slug !== slug && item.category !== current.category
);
return [...sameCategory, ...others].slice(0, limit);
}
/** 统计各分类下的内容数量,用于筛选条上的角标 */
export async function fetchCategoryCounts(
section: SectionKey
): Promise<Record<string, number>> {
await tick();
const counts: Record<string, number> = {};
(registry[section] ?? []).forEach((item) => {
counts[item.category] = (counts[item.category] ?? 0) + 1;
});
counts.__all__ = (registry[section] ?? []).length;
return counts;
}
/** 单页:key 为去掉前导斜杠的路径,例如 about、support/docs */
export async function fetchSinglePage(
key: string
): Promise<SinglePage | undefined> {
await tick();
return singlePages[key];
}
+85
View File
@@ -0,0 +1,85 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1920 1080" preserveAspectRatio="xMidYMid slice" role="img" aria-label="云泽科技数据智能主视觉">
<defs>
<linearGradient id="heroBg" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#0b1430" />
<stop offset="0.55" stop-color="#122048" />
<stop offset="1" stop-color="#172c6d" />
</linearGradient>
<radialGradient id="heroGlowBlue" cx="0.5" cy="0.5" r="0.5">
<stop offset="0" stop-color="#4b83ff" stop-opacity="0.58" />
<stop offset="1" stop-color="#4b83ff" stop-opacity="0" />
</radialGradient>
<radialGradient id="heroGlowTeal" cx="0.5" cy="0.5" r="0.5">
<stop offset="0" stop-color="#22c2a0" stop-opacity="0.42" />
<stop offset="1" stop-color="#22c2a0" stop-opacity="0" />
</radialGradient>
<linearGradient id="heroArea" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#6f9dff" stop-opacity="0.4" />
<stop offset="1" stop-color="#6f9dff" stop-opacity="0" />
</linearGradient>
<pattern id="heroGrid" width="80" height="80" patternUnits="userSpaceOnUse">
<path d="M80 0H0V80" fill="none" stroke="#ffffff" stroke-opacity="0.06" stroke-width="1" />
</pattern>
</defs>
<rect width="1920" height="1080" fill="url(#heroBg)" />
<rect width="1920" height="1080" fill="url(#heroGrid)" />
<ellipse cx="1480" cy="300" rx="660" ry="540" fill="url(#heroGlowBlue)" />
<ellipse cx="1810" cy="960" rx="520" ry="420" fill="url(#heroGlowTeal)" />
<g fill="none" stroke="#ffffff" stroke-opacity="0.13" stroke-width="1.5">
<circle cx="1480" cy="300" r="170" />
<circle cx="1480" cy="300" r="250" />
<circle cx="1480" cy="300" r="330" />
<circle cx="1480" cy="300" r="410" />
</g>
<g fill="#4b83ff" fill-opacity="0.22">
<rect x="1090" y="896" width="46" height="124" rx="9" />
<rect x="1172" y="838" width="46" height="182" rx="9" />
<rect x="1254" y="874" width="46" height="146" rx="9" />
<rect x="1336" y="786" width="46" height="234" rx="9" />
<rect x="1418" y="712" width="46" height="308" rx="9" />
<rect x="1500" y="640" width="46" height="380" rx="9" />
</g>
<path
d="M1060 872 L1210 742 L1338 806 L1472 628 L1612 686 L1762 462 L1920 512 L1920 1080 L1060 1080 Z"
fill="url(#heroArea)"
/>
<path
d="M1060 872 L1210 742 L1338 806 L1472 628 L1612 686 L1762 462 L1920 512"
fill="none"
stroke="#9dc0ff"
stroke-opacity="0.7"
stroke-width="3.5"
stroke-linejoin="round"
stroke-linecap="round"
/>
<g fill="#ffffff" fill-opacity="0.35">
<circle cx="1060" cy="872" r="6" />
<circle cx="1210" cy="742" r="6" />
<circle cx="1338" cy="806" r="6" />
<circle cx="1472" cy="628" r="6" />
<circle cx="1612" cy="686" r="6" />
<circle cx="1762" cy="462" r="6" />
<circle cx="1920" cy="512" r="6" />
</g>
<g fill="#ffffff" fill-opacity="0.08">
<circle cx="1120" cy="180" r="4" />
<circle cx="1180" cy="180" r="4" />
<circle cx="1240" cy="180" r="4" />
<circle cx="1300" cy="180" r="4" />
<circle cx="1120" cy="240" r="4" />
<circle cx="1180" cy="240" r="4" />
<circle cx="1240" cy="240" r="4" />
<circle cx="1300" cy="240" r="4" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

+71
View File
@@ -0,0 +1,71 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000" preserveAspectRatio="xMidYMid slice" role="img" aria-label="云泽科技活动封面">
<defs>
<linearGradient id="newsBg" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#0d1836" />
<stop offset="0.6" stop-color="#152a63" />
<stop offset="1" stop-color="#243f96" />
</linearGradient>
<radialGradient id="newsGlow" cx="0.5" cy="0.5" r="0.5">
<stop offset="0" stop-color="#5e91ff" stop-opacity="0.6" />
<stop offset="1" stop-color="#5e91ff" stop-opacity="0" />
</radialGradient>
<radialGradient id="newsGlowTeal" cx="0.5" cy="0.5" r="0.5">
<stop offset="0" stop-color="#22c2a0" stop-opacity="0.45" />
<stop offset="1" stop-color="#22c2a0" stop-opacity="0" />
</radialGradient>
<linearGradient id="newsArea" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#8fb4ff" stop-opacity="0.42" />
<stop offset="1" stop-color="#8fb4ff" stop-opacity="0" />
</linearGradient>
<pattern id="newsGrid" width="62" height="62" patternUnits="userSpaceOnUse">
<path d="M62 0H0V62" fill="none" stroke="#ffffff" stroke-opacity="0.07" stroke-width="1" />
</pattern>
</defs>
<rect width="1000" height="1000" fill="url(#newsBg)" />
<rect width="1000" height="1000" fill="url(#newsGrid)" />
<ellipse cx="780" cy="240" rx="440" ry="400" fill="url(#newsGlow)" />
<ellipse cx="220" cy="880" rx="400" ry="340" fill="url(#newsGlowTeal)" />
<g fill="none" stroke="#ffffff" stroke-opacity="0.14" stroke-width="1.5">
<circle cx="780" cy="240" r="120" />
<circle cx="780" cy="240" r="190" />
<circle cx="780" cy="240" r="260" />
</g>
<g fill="#7aa6ff" fill-opacity="0.24">
<rect x="150" y="700" width="40" height="120" rx="8" />
<rect x="222" y="640" width="40" height="180" rx="8" />
<rect x="294" y="676" width="40" height="144" rx="8" />
<rect x="366" y="586" width="40" height="234" rx="8" />
<rect x="438" y="520" width="40" height="300" rx="8" />
</g>
<path
d="M120 760 L280 640 L410 706 L560 520 L700 580 L880 386 L1000 430 L1000 1000 L120 1000 Z"
fill="url(#newsArea)"
/>
<path
d="M120 760 L280 640 L410 706 L560 520 L700 580 L880 386 L1000 430"
fill="none"
stroke="#a9c7ff"
stroke-opacity="0.72"
stroke-width="4"
stroke-linejoin="round"
stroke-linecap="round"
/>
<g fill="#ffffff" fill-opacity="0.38">
<circle cx="120" cy="760" r="7" />
<circle cx="280" cy="640" r="7" />
<circle cx="410" cy="706" r="7" />
<circle cx="560" cy="520" r="7" />
<circle cx="700" cy="580" r="7" />
<circle cx="880" cy="386" r="7" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

+191
View File
@@ -0,0 +1,191 @@
<script setup lang="ts">
/**
* 轻量图标组件:统一描边风格,颜色继承 currentColor
* 用法:<AppIcon name="calendar" :size="22" />
*/
withDefaults(
defineProps<{
name: string;
size?: number | string;
strokeWidth?: number;
}>(),
{
size: 24,
strokeWidth: 1.6,
}
);
</script>
<template>
<svg
class="app-icon"
:width="size"
:height="size"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
:stroke-width="strokeWidth"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
focusable="false"
>
<template v-if="name === 'monitor'">
<rect x="2.5" y="4" width="19" height="13" rx="2.5" />
<path d="M8 20h8M12 17v3" />
</template>
<template v-else-if="name === 'clipboard'">
<path d="M9 4.5h6V7H9z" />
<path
d="M15 5.5h1.5A1.5 1.5 0 0 1 18 7v12a1.5 1.5 0 0 1-1.5 1.5h-9A1.5 1.5 0 0 1 6 19V7a1.5 1.5 0 0 1 1.5-1.5H9"
/>
<path d="m9.5 13 2 2 3.5-4" />
</template>
<template v-else-if="name === 'cap'">
<path d="M2.5 8.6 12 4.4l9.5 4.2-9.5 4.2z" />
<path d="M6.6 10.6V15c0 1.4 2.4 2.5 5.4 2.5s5.4-1.1 5.4-2.5v-4.4" />
<path d="M21.5 8.6V14" />
</template>
<template v-else-if="name === 'book'">
<path d="M4 5.6A1.6 1.6 0 0 1 5.6 4H10a2 2 0 0 1 2 2v12.4a1.8 1.8 0 0 0-1.8-1.8H4z" />
<path d="M20 5.6A1.6 1.6 0 0 0 18.4 4H14a2 2 0 0 0-2 2v12.4a1.8 1.8 0 0 1 1.8-1.8H20z" />
</template>
<template v-else-if="name === 'ribbon'">
<circle cx="12" cy="9.6" r="5.6" />
<path d="m8.6 14.4-1 6.1 4.4-2.4 4.4 2.4-1-6.1" />
</template>
<template v-else-if="name === 'phone'">
<path
d="M6.6 3.5h2.6l1.5 3.7-1.9 1.3a12.4 12.4 0 0 0 5.7 5.7l1.3-1.9 3.7 1.5v2.6a2 2 0 0 1-2.2 2C10.7 17.7 6.3 13.3 4.6 5.7a2 2 0 0 1 2-2.2Z"
/>
</template>
<template v-else-if="name === 'calendar'">
<rect x="3.5" y="5" width="17" height="15.5" rx="2.6" />
<path d="M3.5 10h17M8 3.5v3M16 3.5v3" />
<path d="M8 14h3v3H8z" />
</template>
<template v-else-if="name === 'mail'">
<rect x="3" y="5.5" width="18" height="13" rx="2.6" />
<path d="m4.5 8.2 7.5 5 7.5-5" />
</template>
<template v-else-if="name === 'location'">
<path d="M12 21s7-5.4 7-11a7 7 0 1 0-14 0c0 5.6 7 11 7 11Z" />
<circle cx="12" cy="10" r="2.6" />
</template>
<template v-else-if="name === 'chevron-left'">
<path d="m14.5 6-6 6 6 6" />
</template>
<template v-else-if="name === 'chevron-right'">
<path d="m9.5 6 6 6-6 6" />
</template>
<template v-else-if="name === 'arrow-left'">
<path d="M19 12H5m0 0 6-6m-6 6 6 6" />
</template>
<template v-else-if="name === 'arrow-right'">
<path d="M5 12h14m0 0-6-6m6 6-6 6" />
</template>
<template v-else-if="name === 'arrow-up-right'">
<path d="M7 17 17 7M9 7h8v8" />
</template>
<template v-else-if="name === 'pause'">
<path d="M9.5 7.5v9M14.5 7.5v9" stroke-width="2" />
</template>
<template v-else-if="name === 'play'">
<path d="M9 7.2 16.5 12 9 16.8z" fill="currentColor" stroke="none" />
</template>
<template v-else-if="name === 'facebook'">
<path
d="M13.8 20.5v-6.7h2.2l.4-2.9h-2.6V9.1c0-.8.3-1.2 1.2-1.2h1.5V5.3c-.3 0-1.2-.1-2.2-.1-2.2 0-3.6 1.3-3.6 3.7v2h-2.2v2.9h2.2v6.7z"
fill="currentColor"
stroke="none"
/>
</template>
<template v-else-if="name === 'plug'">
<path d="M9 3.5v4M15 3.5v4" />
<path d="M6.5 7.5h11V11a5.5 5.5 0 0 1-11 0z" />
<path d="M12 16.5v4" />
</template>
<template v-else-if="name === 'shield'">
<path d="M12 3.2 5 6v6c0 4.2 2.9 7.2 7 8.8 4.1-1.6 7-4.6 7-8.8V6z" />
<path d="m9.2 11.8 2 2 3.6-4" />
</template>
<template v-else-if="name === 'code'">
<path d="m8.5 8.5-4 3.5 4 3.5M15.5 8.5l4 3.5-4 3.5" />
<path d="m13.6 5.5-3.2 13" />
</template>
<template v-else-if="name === 'chart'">
<path d="M4 4v16h16" />
<path d="M8.5 16.5v-4M12.5 16.5v-8M16.5 16.5v-6" />
</template>
<template v-else-if="name === 'clock'">
<circle cx="12" cy="12" r="8.2" />
<path d="M12 7.6V12l3 1.9" />
</template>
<template v-else-if="name === 'file'">
<path d="M13.6 3.5H7.2A1.7 1.7 0 0 0 5.5 5.2v13.6a1.7 1.7 0 0 0 1.7 1.7h9.6a1.7 1.7 0 0 0 1.7-1.7V8.4z" />
<path d="M13.6 3.5v4.9h4.9" />
<path d="M9 13.2h6M9 16.4h4" />
</template>
<template v-else-if="name === 'target'">
<circle cx="12" cy="12" r="8.2" />
<circle cx="12" cy="12" r="4.2" />
<circle cx="12" cy="12" r="1.1" fill="currentColor" stroke="none" />
</template>
<template v-else-if="name === 'users'">
<circle cx="9.2" cy="8.6" r="3.6" />
<path d="M3.6 19.8c0-3 2.5-5.3 5.6-5.3s5.6 2.3 5.6 5.3" />
<path d="M16.2 5.3a3.6 3.6 0 0 1 0 6.6M17.8 14.8c1.8.7 2.8 2.4 2.8 5" />
</template>
<template v-else-if="name === 'headset'">
<path d="M4.6 14.2V12a7.4 7.4 0 0 1 14.8 0v2.2" />
<rect x="2.6" y="13.4" width="4.2" height="6.6" rx="1.9" />
<rect x="17.2" y="13.4" width="4.2" height="6.6" rx="1.9" />
</template>
<template v-else-if="name === 'spark'">
<path d="m11 3.5 1.8 4.9 4.9 1.8-4.9 1.8L11 16.9l-1.8-4.9L4.3 10.2l4.9-1.8z" />
<path d="m18 3.2.7 1.8 1.8.7-1.8.7-.7 1.8-.7-1.8-1.8-.7 1.8-.7z" />
</template>
<template v-else-if="name === 'check'">
<path d="m5 12.6 4.6 4.6L19 7.8" />
</template>
<template v-else-if="name === 'search'">
<circle cx="11" cy="11" r="6.5" />
<path d="m15.8 15.8 4.7 4.7" />
</template>
</svg>
</template>
<style scoped lang="less">
.app-icon {
display: block;
flex-shrink: 0;
}
</style>
+593
View File
@@ -0,0 +1,593 @@
<script setup lang="ts">
import type { Block } from "@/types/content";
import AppIcon from "./AppIcon.vue";
defineProps<{ blocks: Block[] }>();
</script>
<template>
<div class="blocks">
<template v-for="(block, index) in blocks" :key="index">
<!-- 小标题 -->
<header v-if="block.type === 'heading'" class="blk-heading">
<h2>{{ block.text }}</h2>
<p v-if="block.sub">{{ block.sub }}</p>
</header>
<!-- 段落 -->
<p v-else-if="block.type === 'paragraph'" class="blk-paragraph">
{{ block.text }}
</p>
<!-- 引用 -->
<blockquote v-else-if="block.type === 'quote'" class="blk-quote">
<span class="blk-quote__mark" aria-hidden="true">&ldquo;</span>
<p>{{ block.text }}</p>
<cite v-if="block.author">{{ block.author }}</cite>
</blockquote>
<!-- 要点列表 -->
<ul v-else-if="block.type === 'list'" class="blk-list">
<li v-for="item in block.items" :key="item">
<AppIcon name="check" :size="15" :stroke-width="2.2" />
<span>{{ item }}</span>
</li>
</ul>
<!-- 能力卡片 -->
<ul v-else-if="block.type === 'features'" class="blk-features">
<li v-for="item in block.items" :key="item.title">
<span class="blk-features__icon">
<AppIcon :name="item.icon" :size="22" :stroke-width="1.5" />
</span>
<strong>{{ item.title }}</strong>
<p>{{ item.desc }}</p>
</li>
</ul>
<!-- 数据指标 -->
<ul v-else-if="block.type === 'stats'" class="blk-stats">
<li v-for="item in block.items" :key="item.label">
<strong>{{ item.value }}</strong>
<span>{{ item.label }}</span>
</li>
</ul>
<!-- 步骤 -->
<ol v-else-if="block.type === 'steps'" class="blk-steps">
<li v-for="(item, i) in block.items" :key="item.title">
<span class="blk-steps__index">{{ String(i + 1).padStart(2, "0") }}</span>
<div>
<strong>{{ item.title }}</strong>
<p>{{ item.desc }}</p>
</div>
</li>
</ol>
<!-- 发展历程 -->
<ul v-else-if="block.type === 'timeline'" class="blk-timeline">
<li v-for="item in block.items" :key="item.year">
<span class="blk-timeline__year">{{ item.year }}</span>
<span class="blk-timeline__node" aria-hidden="true"></span>
<p>{{ item.text }}</p>
</li>
</ul>
<!-- 联系信息 -->
<ul v-else-if="block.type === 'contact'" class="blk-contact">
<li v-for="item in block.items" :key="item.label">
<span class="blk-contact__icon">
<AppIcon :name="item.icon" :size="20" :stroke-width="1.6" />
</span>
<div>
<em>{{ item.label }}</em>
<strong>{{ item.value }}</strong>
</div>
</li>
</ul>
<!-- 快捷入口 -->
<ul v-else-if="block.type === 'links'" class="blk-links">
<li v-for="item in block.items" :key="item.path + item.title">
<RouterLink :to="item.path">
<span class="blk-links__icon">
<AppIcon :name="item.icon" :size="20" :stroke-width="1.6" />
</span>
<div>
<strong>{{ item.title }}</strong>
<em>{{ item.desc }}</em>
</div>
<AppIcon name="arrow-up-right" :size="16" />
</RouterLink>
</li>
</ul>
<!-- 表格 -->
<div v-else-if="block.type === 'table'" class="blk-table">
<table>
<thead>
<tr>
<th v-for="head in block.head" :key="head">{{ head }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, i) in block.rows" :key="i">
<td v-for="(cell, j) in row" :key="j">{{ cell }}</td>
</tr>
</tbody>
</table>
</div>
</template>
</div>
</template>
<style scoped lang="less">
.blocks {
display: flex;
flex-direction: column;
gap: 30px;
}
/* ---------------- 小标题 ---------------- */
.blk-heading {
margin-top: 18px;
padding-left: 16px;
border-left: 3px solid @blue-600;
h2 {
font-size: 22px;
font-weight: 700;
line-height: 1.5;
color: @ink-900;
}
p {
margin-top: 8px;
font-size: 13.5px;
color: @ink-500;
}
}
/* ---------------- 段落 ---------------- */
.blk-paragraph {
font-size: 15.5px;
line-height: 2.05;
color: @ink-600;
}
/* ---------------- 引用 ---------------- */
.blk-quote {
padding: 32px 36px 30px;
border-radius: 12px;
background: linear-gradient(140deg, #f5f8ff 0%, #eef3fd 100%);
border-left: 3px solid @blue-600;
&__mark {
display: block;
margin-bottom: 10px;
font-family: Georgia, "Times New Roman", serif;
font-size: 54px;
font-weight: 900;
line-height: 0.6;
color: @blue-600;
}
p {
font-size: 17px;
font-weight: 500;
line-height: 1.95;
color: @ink-800;
}
cite {
display: block;
margin-top: 14px;
font-style: normal;
font-size: 13px;
color: @ink-500;
}
}
/* ---------------- 要点列表 ---------------- */
.blk-list {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px 32px;
li {
display: flex;
align-items: flex-start;
gap: 10px;
font-size: 14.5px;
line-height: 1.85;
color: @ink-600;
svg {
margin-top: 5px;
color: @green-400;
}
}
}
/* ---------------- 能力卡片 ---------------- */
.blk-features {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 18px;
li {
padding: 26px 26px 24px;
border: 1px solid @line;
border-radius: 12px;
background: @white;
transition: border-color 0.3s ease, box-shadow 0.3s ease;
&:hover {
border-color: rgba(47, 107, 243, 0.4);
box-shadow: 0 16px 36px rgba(20, 35, 82, 0.08);
}
}
&__icon {
display: inline-flex;
color: @navy-700;
}
strong {
display: block;
margin-top: 18px;
font-size: 16px;
font-weight: 700;
color: @ink-900;
}
p {
margin-top: 8px;
font-size: 13.5px;
line-height: 1.85;
color: @ink-500;
}
}
/* ---------------- 数据指标 ---------------- */
.blk-stats {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 1px;
border-radius: 12px;
overflow: hidden;
background: @line;
li {
display: flex;
flex-direction: column;
gap: 8px;
padding: 30px 24px;
background: @bg-soft;
}
strong {
font-size: 30px;
font-weight: 700;
line-height: 1.2;
color: @navy-800;
letter-spacing: -0.01em;
}
span {
font-size: 13px;
line-height: 1.7;
color: @ink-500;
}
}
/* ---------------- 步骤 ---------------- */
.blk-steps {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 18px;
li {
display: flex;
align-items: flex-start;
gap: 16px;
padding: 24px 26px;
border-radius: 12px;
background: @bg-soft;
}
&__index {
flex-shrink: 0;
font-size: 20px;
font-weight: 900;
line-height: 1.3;
color: rgba(47, 107, 243, 0.45);
letter-spacing: 0.02em;
}
strong {
display: block;
font-size: 15.5px;
font-weight: 700;
color: @ink-900;
}
p {
margin-top: 7px;
font-size: 13.5px;
line-height: 1.85;
color: @ink-500;
}
}
/* ---------------- 发展历程 ---------------- */
.blk-timeline {
display: flex;
flex-direction: column;
li {
position: relative;
display: grid;
grid-template-columns: 92px 24px minmax(0, 1fr);
align-items: center;
gap: 0 4px;
padding: 15px 0;
& + li {
border-top: 1px dashed @line;
}
}
&__year {
font-size: 17px;
font-weight: 700;
color: @navy-800;
letter-spacing: 0.02em;
}
&__node {
position: relative;
justify-self: center;
width: 11px;
height: 11px;
border-radius: 50%;
border: 3px solid @blue-600;
background: @white;
&::before,
&::after {
content: "";
position: absolute;
left: 50%;
width: 1px;
height: 24px;
background: @line;
transform: translateX(-50%);
}
&::before {
bottom: 100%;
}
&::after {
top: 100%;
}
}
li:first-child &__node::before,
li:last-child &__node::after {
display: none;
}
p {
font-size: 14.5px;
line-height: 1.85;
color: @ink-600;
}
}
/* ---------------- 联系信息 ---------------- */
.blk-contact {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
li {
display: flex;
align-items: center;
gap: 16px;
padding: 24px 26px;
border: 1px solid @line;
border-radius: 12px;
}
&__icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 46px;
height: 46px;
flex-shrink: 0;
border-radius: 12px;
background: @bg-soft;
color: @navy-700;
}
em {
display: block;
font-style: normal;
font-size: 12.5px;
color: @ink-400;
letter-spacing: 0.04em;
}
strong {
display: block;
margin-top: 5px;
font-size: 15px;
font-weight: 600;
line-height: 1.6;
color: @ink-900;
word-break: break-all;
}
}
/* ---------------- 快捷入口 ---------------- */
.blk-links {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
a {
display: flex;
align-items: center;
gap: 16px;
height: 100%;
padding: 24px 26px;
border: 1px solid @line;
border-radius: 12px;
transition: all 0.3s ease;
> svg {
margin-left: auto;
color: @ink-300;
transition: color 0.3s ease, transform 0.3s ease;
}
&:hover {
border-color: rgba(47, 107, 243, 0.45);
box-shadow: 0 16px 36px rgba(20, 35, 82, 0.09);
> svg {
color: @blue-600;
transform: translate(2px, -2px);
}
.blk-links__icon {
background: @navy-800;
color: @white;
}
}
}
&__icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 46px;
height: 46px;
flex-shrink: 0;
border-radius: 12px;
background: @bg-soft;
color: @navy-700;
transition: all 0.3s ease;
}
strong {
display: block;
font-size: 15.5px;
font-weight: 700;
color: @ink-900;
}
em {
display: block;
margin-top: 6px;
font-style: normal;
font-size: 13px;
line-height: 1.75;
color: @ink-500;
}
}
/* ---------------- 表格 ---------------- */
.blk-table {
overflow-x: auto;
border: 1px solid @line;
border-radius: 12px;
table {
width: 100%;
min-width: 560px;
border-collapse: collapse;
font-size: 14px;
}
th,
td {
padding: 16px 22px;
text-align: left;
border-bottom: 1px solid @line;
}
th {
background: @bg-soft;
font-weight: 600;
font-size: 13px;
color: @ink-600;
letter-spacing: 0.03em;
white-space: nowrap;
}
td {
color: @ink-700;
line-height: 1.75;
}
tbody tr:last-child td {
border-bottom: 0;
}
tbody tr:hover td {
background: #fafbfe;
}
}
/* ---------------- 响应式 ---------------- */
@media (max-width: 768px) {
.blocks {
gap: 24px;
}
.blk-list,
.blk-features,
.blk-steps,
.blk-contact,
.blk-links {
grid-template-columns: minmax(0, 1fr);
}
.blk-stats {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.blk-heading h2 {
font-size: 19px;
}
.blk-paragraph {
font-size: 14.5px;
line-height: 1.95;
}
.blk-quote {
padding: 24px 22px 22px;
p {
font-size: 15.5px;
}
}
.blk-timeline li {
grid-template-columns: 66px 20px minmax(0, 1fr);
}
.blk-timeline__year {
font-size: 15px;
}
.blk-stats strong {
font-size: 24px;
}
}
</style>
+284
View File
@@ -0,0 +1,284 @@
<script setup lang="ts">
import { computed } from "vue";
import type { ContentItem } from "@/types/content";
import { categoryName, sections } from "@/config/sections";
import AppIcon from "./AppIcon.vue";
const props = withDefaults(
defineProps<{
item: ContentItem;
/** list = 横向大卡(新闻/活动);grid = 竖向卡片(产品/方案/案例) */
layout?: "list" | "grid";
}>(),
{ layout: "grid" }
);
const to = computed(
() => `${sections[props.item.section].detailPath}/${props.item.slug}`
);
const categoryLabel = computed(() =>
categoryName(props.item.section, props.item.category)
);
/** 封面上的装饰文字:取标题首字,中文视觉上很像一个字标 */
const glyph = computed(() => props.item.title.slice(0, 1));
</script>
<template>
<RouterLink :to="to" class="content-card" :class="`content-card--${layout}`">
<div class="content-card__cover" :class="`tone-${item.tone}`">
<span class="content-card__deco" aria-hidden="true"></span>
<span class="content-card__glyph" aria-hidden="true">{{ glyph }}</span>
<span class="content-card__badge">{{ categoryLabel }}</span>
</div>
<div class="content-card__body">
<p v-if="item.subtitle" class="content-card__eyebrow">
{{ item.subtitle }}
</p>
<h3 class="content-card__title">{{ item.title }}</h3>
<p class="content-card__summary">{{ item.summary }}</p>
<div class="content-card__foot">
<span v-if="item.date" class="content-card__date">
<AppIcon name="clock" :size="14" />
{{ item.date }}
</span>
<span v-else class="content-card__tags">
<em v-for="tag in (item.tags ?? []).slice(0, 2)" :key="tag">
{{ tag }}
</em>
</span>
<span class="content-card__more">
查看详情
<AppIcon name="arrow-right" :size="14" />
</span>
</div>
</div>
</RouterLink>
</template>
<style scoped lang="less">
.content-card {
display: flex;
border: 1px solid @line;
border-radius: 12px;
background: @white;
overflow: hidden;
transition: transform 0.34s cubic-bezier(0.22, 0.61, 0.36, 1),
border-color 0.34s ease, box-shadow 0.34s ease;
&:hover {
transform: translateY(-6px);
border-color: rgba(47, 107, 243, 0.4);
box-shadow: 0 24px 52px rgba(20, 35, 82, 0.12);
.content-card__glyph {
transform: translateY(-6px) scale(1.06);
opacity: 0.32;
}
.content-card__more {
color: @blue-600;
}
}
/* ---------------- 封面 ---------------- */
&__cover {
position: relative;
overflow: hidden;
flex-shrink: 0;
color: @white;
}
&__deco {
position: absolute;
right: -34px;
bottom: -34px;
width: 140px;
height: 140px;
border-radius: 50%;
border: 1px solid rgba(255, 255, 255, 0.18);
&::after {
content: "";
position: absolute;
inset: 26px;
border-radius: 50%;
border: 1px solid rgba(255, 255, 255, 0.14);
}
}
&__glyph {
position: absolute;
right: 6px;
bottom: -22px;
font-size: 122px;
font-weight: 900;
line-height: 1;
color: rgba(255, 255, 255, 0.16);
letter-spacing: -0.02em;
transition: transform 0.5s cubic-bezier(0.22, 0.61, 0.36, 1),
opacity 0.4s ease;
}
&__badge {
position: absolute;
left: 22px;
top: 22px;
padding: 5px 13px;
border-radius: 100px;
background: rgba(255, 255, 255, 0.16);
backdrop-filter: blur(6px);
font-size: 11.5px;
font-weight: 500;
letter-spacing: 0.06em;
}
/* ---------------- 正文 ---------------- */
&__body {
display: flex;
flex-direction: column;
flex: 1;
min-width: 0;
padding: 28px 30px 26px;
}
&__eyebrow {
margin-bottom: 8px;
font-size: 12.5px;
letter-spacing: 0.06em;
color: @blue-600;
}
&__title {
font-size: 19px;
font-weight: 700;
line-height: 1.55;
color: @ink-900;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
transition: color 0.28s ease;
}
&__summary {
margin-top: 12px;
font-size: 13.5px;
line-height: 1.85;
color: @ink-500;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
&__foot {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-top: auto;
padding-top: 22px;
}
&__date {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 12.5px;
color: @ink-400;
}
&__tags {
display: inline-flex;
gap: 6px;
em {
padding: 3px 10px;
border-radius: 100px;
background: @bg-soft;
font-style: normal;
font-size: 11.5px;
color: @ink-500;
}
}
&__more {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 13px;
font-weight: 500;
color: @ink-500;
white-space: nowrap;
transition: color 0.28s ease;
}
/* ---------------- 竖向卡片 ---------------- */
&--grid {
flex-direction: column;
.content-card__cover {
height: 176px;
}
}
/* ---------------- 横向大卡 ---------------- */
&--list {
align-items: stretch;
.content-card__cover {
width: 300px;
min-height: 200px;
}
.content-card__glyph {
font-size: 150px;
bottom: -30px;
}
.content-card__title {
font-size: 21px;
-webkit-line-clamp: 2;
}
.content-card__summary {
font-size: 14px;
-webkit-line-clamp: 3;
}
}
}
/* ---------------- 响应式 ---------------- */
@media (max-width: 992px) {
.content-card--list {
.content-card__cover {
width: 230px;
min-height: 176px;
}
.content-card__glyph {
font-size: 118px;
}
}
}
@media (max-width: 640px) {
.content-card--list {
flex-direction: column;
.content-card__cover {
width: 100%;
min-height: 148px;
}
}
.content-card__body {
padding: 22px 22px 20px;
}
}
</style>
+181
View File
@@ -0,0 +1,181 @@
<script setup lang="ts">
import { computed } from "vue";
import type { Category } from "@/types/content";
import AppIcon from "./AppIcon.vue";
const props = defineProps<{
categories: Category[];
/** 当前分类 slug,空串表示全部 */
active: string;
counts: Record<string, number>;
total: number;
searchable?: boolean;
keyword?: string;
}>();
const emit = defineEmits<{
(e: "update:active", value: string): void;
(e: "update:keyword", value: string): void;
}>();
/** 当前选中分类的描述 */
const activeDesc = computed(() => {
if (!props.active) return "";
return props.categories.find((item) => item.slug === props.active)?.desc ?? "";
});
</script>
<template>
<div class="filter-bar">
<div class="filter-bar__row">
<ul class="filter-bar__tabs">
<li>
<button
type="button"
class="filter-tab"
:class="{ 'is-active': !active }"
@click="emit('update:active', '')"
>
全部
<em>{{ total }}</em>
</button>
</li>
<li v-for="item in categories" :key="item.slug">
<button
type="button"
class="filter-tab"
:class="{ 'is-active': active === item.slug }"
@click="emit('update:active', item.slug)"
>
{{ item.name }}
<em>{{ counts[item.slug] ?? 0 }}</em>
</button>
</li>
</ul>
<label v-if="searchable" class="filter-search">
<AppIcon name="search" :size="16" />
<input
:value="keyword"
type="search"
placeholder="搜索标题或关键词"
@input="
emit('update:keyword', ($event.target as HTMLInputElement).value)
"
/>
</label>
</div>
<p v-if="activeDesc" class="filter-bar__desc">{{ activeDesc }}</p>
</div>
</template>
<style scoped lang="less">
.filter-bar {
display: flex;
flex-direction: column;
gap: 18px;
padding-bottom: 26px;
border-bottom: 1px solid @line;
&__row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 28px;
flex-wrap: wrap;
}
&__tabs {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
&__desc {
font-size: 13px;
line-height: 1.8;
color: @ink-500;
}
}
.filter-tab {
display: inline-flex;
align-items: center;
gap: 7px;
padding: 9px 18px;
border-radius: 100px;
border: 1px solid @line;
font-size: 14px;
color: @ink-600;
transition: all 0.26s ease;
em {
font-style: normal;
font-size: 11.5px;
color: @ink-400;
transition: color 0.26s ease;
}
&:hover {
border-color: rgba(47, 107, 243, 0.45);
color: @navy-800;
}
&.is-active {
background: @navy-800;
border-color: @navy-800;
color: @white;
em {
color: rgba(255, 255, 255, 0.6);
}
}
}
.filter-search {
display: inline-flex;
align-items: center;
gap: 9px;
width: 280px;
max-width: 100%;
padding: 0 16px;
height: 42px;
border-radius: 100px;
border: 1px solid @line;
background: @bg-soft;
color: @ink-400;
transition: all 0.26s ease;
&:focus-within {
background: @white;
border-color: rgba(47, 107, 243, 0.5);
color: @blue-600;
}
input {
flex: 1;
min-width: 0;
border: 0;
outline: none;
background: transparent;
font-size: 13.5px;
color: @ink-800;
&::placeholder {
color: @ink-400;
}
&::-webkit-search-cancel-button {
cursor: pointer;
}
}
}
@media (max-width: 640px) {
.filter-search {
width: 100%;
}
}
</style>
+157
View File
@@ -0,0 +1,157 @@
<script setup lang="ts">
import cover from "@/assets/images/hero-cover.svg";
defineProps<{
title: string;
subtitle?: string;
/** 面包屑,不含首页 */
crumbs: string[];
}>();
</script>
<template>
<section class="page-hero">
<div class="page-hero__media">
<img :src="cover" alt="" aria-hidden="true" />
</div>
<div class="page-hero__veil"></div>
<div class="container page-hero__inner">
<nav class="page-hero__crumbs" aria-label="面包屑">
<RouterLink to="/">首页</RouterLink>
<template v-for="(item, index) in crumbs" :key="item">
<i></i>
<span :class="{ 'is-current': index === crumbs.length - 1 }">
{{ item }}
</span>
</template>
</nav>
<h1 class="page-hero__title">{{ title }}</h1>
<p v-if="subtitle" class="page-hero__subtitle">{{ subtitle }}</p>
</div>
</section>
</template>
<style scoped lang="less">
.page-hero {
position: relative;
overflow: hidden;
background: @navy-900;
color: @white;
&__media {
position: absolute;
inset: 0;
img {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center 35%;
}
}
&__veil {
position: absolute;
inset: 0;
background: linear-gradient(
100deg,
rgba(6, 12, 32, 0.94) 0%,
rgba(8, 16, 40, 0.86) 45%,
rgba(10, 18, 44, 0.68) 100%
);
}
&__inner {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
justify-content: flex-end;
min-height: 322px;
padding-top: 72px;
padding-bottom: 52px;
}
&__crumbs {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 20px;
a,
span {
font-size: 12.5px;
letter-spacing: 0.06em;
color: rgba(255, 255, 255, 0.5);
transition: color 0.2s ease;
}
a:hover {
color: @white;
}
span.is-current {
color: rgba(255, 255, 255, 0.9);
}
i {
width: 5px;
height: 5px;
border-top: 1px solid rgba(255, 255, 255, 0.4);
border-right: 1px solid rgba(255, 255, 255, 0.4);
transform: rotate(45deg);
}
}
&__title {
font-size: 44px;
font-weight: 900;
line-height: 1.2;
letter-spacing: 0.02em;
}
&__subtitle {
max-width: 760px;
margin-top: 16px;
font-size: 15.5px;
line-height: 1.9;
font-weight: 300;
color: rgba(255, 255, 255, 0.66);
}
}
@media (max-width: 992px) {
.page-hero {
&__inner {
min-height: 260px;
padding-top: 54px;
padding-bottom: 40px;
}
&__title {
font-size: 34px;
}
}
}
@media (max-width: 640px) {
.page-hero {
&__inner {
min-height: 210px;
padding-top: 40px;
padding-bottom: 32px;
}
&__title {
font-size: 27px;
}
&__subtitle {
margin-top: 12px;
font-size: 14px;
}
}
}
</style>
+163
View File
@@ -0,0 +1,163 @@
<script setup lang="ts">
import { computed } from "vue";
import AppIcon from "./AppIcon.vue";
const props = defineProps<{
page: number;
totalPages: number;
total: number;
unit: string;
}>();
const emit = defineEmits<{ (e: "change", page: number): void }>();
/** 最多显示 7 个页码,超出时用省略号 */
const pages = computed<(number | string)[]>(() => {
const { page, totalPages } = props;
if (totalPages <= 7) {
return Array.from({ length: totalPages }, (_, i) => i + 1);
}
const result: (number | string)[] = [1];
const start = Math.max(2, page - 1);
const end = Math.min(totalPages - 1, page + 1);
if (start > 2) result.push("...");
for (let i = start; i <= end; i += 1) result.push(i);
if (end < totalPages - 1) result.push("...");
result.push(totalPages);
return result;
});
const go = (target: number) => {
if (target < 1 || target > props.totalPages || target === props.page) return;
emit("change", target);
};
</script>
<template>
<div v-if="totalPages > 1" class="pager">
<p class="pager__total">
共 <strong>{{ total }}</strong> {{ unit }}内容,第
<strong>{{ page }}</strong> / {{ totalPages }} 页
</p>
<nav class="pager__nav">
<button
type="button"
class="pager__arrow"
:disabled="page <= 1"
aria-label="上一页"
@click="go(page - 1)"
>
<AppIcon name="chevron-left" :size="16" />
</button>
<template v-for="(item, index) in pages" :key="`${item}-${index}`">
<span v-if="item === '...'" class="pager__gap">···</span>
<button
v-else
type="button"
class="pager__item"
:class="{ 'is-active': item === page }"
@click="go(item as number)"
>
{{ item }}
</button>
</template>
<button
type="button"
class="pager__arrow"
:disabled="page >= totalPages"
aria-label="下一页"
@click="go(page + 1)"
>
<AppIcon name="chevron-right" :size="16" />
</button>
</nav>
</div>
</template>
<style scoped lang="less">
.pager {
display: flex;
align-items: center;
justify-content: space-between;
gap: 24px;
flex-wrap: wrap;
margin-top: 52px;
&__total {
font-size: 13px;
color: @ink-500;
strong {
font-weight: 700;
color: @ink-800;
}
}
&__nav {
display: flex;
align-items: center;
gap: 6px;
}
&__item,
&__arrow {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 40px;
height: 40px;
padding: 0 10px;
border-radius: 10px;
border: 1px solid @line;
font-size: 14px;
color: @ink-600;
transition: all 0.24s ease;
&:hover:not(:disabled) {
border-color: @navy-800;
color: @navy-800;
}
&.is-active {
background: @navy-800;
border-color: @navy-800;
color: @white;
}
&:disabled {
opacity: 0.4;
cursor: not-allowed;
}
}
&__gap {
padding: 0 4px;
color: @ink-400;
letter-spacing: 0.1em;
}
}
@media (max-width: 640px) {
.pager {
justify-content: center;
&__total {
order: 2;
width: 100%;
text-align: center;
}
&__item,
&__arrow {
min-width: 36px;
height: 36px;
}
}
}
</style>
+123
View File
@@ -0,0 +1,123 @@
import type { Category, SectionKey } from "@/types/content";
export interface SectionConfig {
key: SectionKey;
title: string;
subtitle: string;
/** 列表页路径 */
basePath: string;
/** 详情页路径前缀 */
detailPath: string;
/** 分类页路径前缀 */
categoryPath: string;
/** 计数单位 */
unit: string;
/** 列表布局:list = 横向大卡;grid = 竖向卡片网格 */
layout: "list" | "grid";
/** 是否提供关键字搜索 */
searchable: boolean;
categories: Category[];
}
export const sections: Record<SectionKey, SectionConfig> = {
news: {
key: "news",
title: "新闻中心",
subtitle: "了解云泽科技的产品动态、公司公告与行业活动。",
basePath: "/news",
detailPath: "/news/detail",
categoryPath: "/news/category",
unit: "条",
layout: "list",
searchable: true,
categories: [
{ slug: "company", name: "公司公告", desc: "融资、认证、人事与经营相关的官方信息。" },
{ slug: "product", name: "产品发布", desc: "版本更新、新功能上线与产品路线说明。" },
{ slug: "event", name: "市场活动", desc: "大会、公开课、巡展等线上线下活动。" },
{ slug: "case", name: "客户案例", desc: "各行业客户的落地实践与项目成果。" },
],
},
products: {
key: "products",
title: "产品中心",
subtitle: "从云原生底座到数据智能应用,构建完整的产品矩阵。",
basePath: "/products",
detailPath: "/products/detail",
categoryPath: "/products/category",
unit: "款",
layout: "grid",
searchable: true,
categories: [
{ slug: "platform", name: "平台产品", desc: "支撑业务运行的底层技术平台。" },
{ slug: "data", name: "数据产品", desc: "数据采集、治理与分析类产品。" },
{ slug: "app", name: "应用产品", desc: "面向具体业务场景的智能应用。" },
],
},
solutions: {
key: "solutions",
title: "解决方案",
subtitle: "面向重点行业的场景化方案,从诊断到落地全程陪跑。",
basePath: "/solutions",
detailPath: "/solutions/detail",
categoryPath: "/solutions/category",
unit: "套",
layout: "grid",
searchable: true,
categories: [
{ slug: "manufacturing", name: "制造业", desc: "离散与流程制造的生产数字化。" },
{ slug: "retail", name: "零售消费", desc: "会员、商品与门店的数据运营。" },
{ slug: "energy", name: "能源电力", desc: "设备状态监测与预测性维护。" },
{ slug: "finance", name: "金融", desc: "数据合规、风控与实时报表。" },
],
},
cases: {
key: "cases",
title: "客户案例",
subtitle: "超过 1000 家企业客户,在这些场景里跑通了数字化。",
basePath: "/cases",
detailPath: "/cases/detail",
categoryPath: "/cases/category",
unit: "个",
layout: "grid",
searchable: true,
categories: [
{ slug: "manufacturing", name: "制造业", desc: "" },
{ slug: "retail", name: "零售消费", desc: "" },
{ slug: "energy", name: "能源电力", desc: "" },
{ slug: "finance", name: "金融", desc: "" },
],
},
events: {
key: "events",
title: "市场活动",
subtitle: "大会、公开课与合作巡展,全年活动一站查看。",
basePath: "/events",
detailPath: "/events/detail",
categoryPath: "/events/category",
unit: "场",
layout: "list",
searchable: false,
categories: [
{ slug: "conference", name: "行业大会", desc: "" },
{ slug: "online", name: "线上直播", desc: "" },
{ slug: "offline", name: "线下活动", desc: "" },
],
},
};
/** 从路由路径推断栏目:/news/detail/xxx -> news */
export function sectionFromPath(path: string): SectionConfig | undefined {
const first = path.split("/").filter(Boolean)[0];
return first && first in sections ? sections[first as SectionKey] : undefined;
}
/** 查询某个分类的展示名 */
export function categoryName(
section: SectionKey,
slug: string | undefined
): string {
if (!slug) return "全部";
return (
sections[section].categories.find((item) => item.slug === slug)?.name ?? "全部"
);
}
+73
View File
@@ -0,0 +1,73 @@
/**
* 站点级公共配置:品牌、导航、页脚
* 后续接入后端 CMS 时,只需替换此处的数据来源即可。
*/
export interface NavItem {
label: string;
path: string;
}
export interface FooterColumn {
title: string;
links: NavItem[];
}
export const siteInfo = {
brandMark: "YZ",
brandName: "云泽科技",
brandSub: "YUNZER TECH",
fullName: "云泽科技有限公司",
slogan: "欢迎访问云泽科技官方网站",
phone: "400-000-0000",
email: "contact@yunzer.com",
address: "北京市海淀区中关村软件园 25 号楼",
postalCode: "100193",
copyright:
"COPYRIGHT (C) 2026 YUNZER TECHNOLOGY CO., LTD. ALL RIGHTS RESERVED.",
};
/** 顶部工具条链接 */
export const utilityLinks: NavItem[] = [
{ label: "加入我们", path: "/careers" },
{ label: "客户支持", path: "/support" },
{ label: "合作伙伴", path: "/partners" },
];
/** 主导航 */
export const navItems: NavItem[] = [
{ label: "关于我们", path: "/about" },
{ label: "产品中心", path: "/products" },
{ label: "解决方案", path: "/solutions" },
{ label: "客户案例", path: "/cases" },
{ label: "新闻中心", path: "/news" },
];
/** 页脚链接分组 */
export const footerColumns: FooterColumn[] = [
{
title: "产品与方案",
links: [
{ label: "云原生平台", path: "/products/detail/cloud-native" },
{ label: "数据中台", path: "/products/detail/data-platform" },
{ label: "智能分析引擎", path: "/products/detail/analytics" },
{ label: "行业解决方案", path: "/solutions" },
],
},
{
title: "服务与支持",
links: [
{ label: "产品文档", path: "/support/docs" },
{ label: "开发者中心", path: "/support/developer" },
{ label: "申请试用", path: "/support/trial" },
{ label: "服务保障", path: "/support/sla" },
],
},
];
/** 页脚底部合规链接 */
export const policyLinks: NavItem[] = [
{ label: "隐私政策", path: "/policy/privacy" },
{ label: "使用条款", path: "/policy/terms" },
{ label: "网站地图", path: "/policy/sitemap" },
];
+241
View File
@@ -0,0 +1,241 @@
import type { ContentItem } from "@/types/content";
export const cases: ContentItem[] = [
{
slug: "manufacturing-group",
section: "cases",
category: "manufacturing",
title: "某大型制造集团",
subtitle: "生产数据一体化改造",
summary:
"覆盖 6 个厂区、1200 余台设备,设备数据采集覆盖率从 41% 提升至 96%。",
tone: "navy",
tags: ["制造业", "设备联网", "生产可视化"],
featured: true,
blocks: [
{
type: "paragraph",
text: "客户是国内领先的精密制造集团,旗下 6 个厂区、1200 余台设备分属 20 多个品牌。此前生产数据依赖人工抄录,日报在次日中午才能汇总完成,异常发现普遍滞后半天以上。",
},
{ type: "heading", text: "业务挑战" },
{
type: "list",
items: [
"设备协议不统一,20 多个品牌难以用同一套方案采集",
"生产日报依赖人工,数据滞后且口径不一致",
"异常停机平均 4 小时才被发现,损失难以估量",
"各厂区系统独立建设,集团层面看不到整体产能",
],
},
{ type: "heading", text: "解决方案" },
{
type: "features",
items: [
{ icon: "plug", title: "边缘统一采集", desc: "按产线部署边缘网关,屏蔽协议差异,本地缓存保证不断数。" },
{ icon: "monitor", title: "集团产能看板", desc: "6 个厂区共用一套指标口径,产能与在制品实时上墙。" },
{ icon: "shield", title: "异常自动预警", desc: "停机、节拍偏离与质量异常自动推送至班组与设备科。" },
],
},
{ type: "heading", text: "项目成果", sub: "上线 6 个月后的实测数据" },
{
type: "stats",
items: [
{ value: "96%", label: "设备数据采集覆盖率" },
{ value: "-60%", label: "生产异常响应时长" },
{ value: "+18%", label: "设备综合效率(OEE)" },
{ value: "4 小时", label: "日报缩短为实时看板" },
],
},
{
type: "quote",
text: "以前我们每天上午都在开会核对昨天的产量,现在数据是实时的,会议变成了讨论怎么改进。",
author: "信息中心主任 · 赵敏",
},
],
},
{
slug: "retail-chain",
section: "cases",
category: "retail",
title: "某全国连锁零售",
subtitle: "会员数据中台建设",
summary:
"打通 3 个线上渠道与 800 家门店的会员数据,活动 ROI 提升 24%。",
tone: "teal",
tags: ["零售", "会员运营", "OneID"],
blocks: [
{
type: "paragraph",
text: "客户在全国拥有 800 余家门店,同时经营小程序、电商旗舰店与社群渠道。会员数据分散在四个系统,同一顾客在不同渠道被识别为不同的人,营销活动只能在单渠道内做。",
},
{ type: "heading", text: "业务挑战" },
{
type: "list",
items: [
"跨渠道会员无法识别为同一人,权益与积分无法合并",
"标签体系缺失,活动只能面向全量人群投放",
"门店选品依赖经验,滞销与缺货同时存在",
],
},
{
type: "features",
items: [
{ icon: "users", title: "OneID 归一", desc: "以手机号为主键合并四个渠道的身份数据。" },
{ icon: "target", title: "标签与人群包", desc: "沉淀 300 余个标签,人群包一键推送至短信与企微。" },
{ icon: "chart", title: "门店经营分析", desc: "按门店、时段拆解客流与转化,支撑排班与选品调整。" },
],
},
{
type: "stats",
items: [
{ value: "+18%", label: "会员复购率" },
{ value: "+24%", label: "活动 ROI" },
{ value: "-30%", label: "滞销库存占比" },
],
},
{
type: "quote",
text: "现在我们做一场活动,先选人群再定商品,而不是先把商品发下去再想卖给谁。",
author: "数字化总监 · 刘鹏",
},
],
},
{
slug: "energy-group",
section: "cases",
category: "energy",
title: "某能源集团",
subtitle: "设备预测性维护平台",
summary:
"为 380 台关键机组建立健康度模型,非计划停机时长下降 32%。",
tone: "indigo",
tags: ["能源", "预测性维护", "状态监测"],
blocks: [
{
type: "paragraph",
text: "客户的 380 台关键机组分布在全国 12 个基地,此前采用固定周期检修。突发故障带来的非计划停机,每年造成的直接损失超过千万元。",
},
{
type: "features",
items: [
{ icon: "monitor", title: "高频状态采集", desc: "振动、温度、电流等测点毫秒级采集,边缘侧初筛异常。" },
{ icon: "chart", title: "健康度模型", desc: "机理与数据双模型输出健康度评分与剩余寿命预估。" },
{ icon: "clipboard", title: "检修工单闭环", desc: "预警自动派单,处理结果回流用于模型迭代。" },
],
},
{
type: "stats",
items: [
{ value: "-32%", label: "非计划停机时长" },
{ value: "-21%", label: "年度检修成本" },
{ value: "92%", label: "故障预警准确率" },
],
},
{
type: "quote",
text: "过去是坏了才修,现在是系统告诉我们哪台该修了,检修计划终于能提前半个月排。",
author: "设备管理部负责人 · 陈建国",
},
],
},
{
slug: "bank-risk",
section: "cases",
category: "finance",
title: "某股份制银行",
subtitle: "数据合规与实时风控",
summary:
"经营日报从 T+1 提升到分钟级,监管报送人工核对工作量下降 90%。",
tone: "blue",
tags: ["金融", "数据合规", "实时风控"],
blocks: [
{
type: "paragraph",
text: "客户同时承担监管报送、内部风控与经营分析三类数据需求。此前三套体系各自建设,口径不一致,审计时需要投入大量人力整理台账。",
},
{
type: "features",
items: [
{ icon: "shield", title: "数据血缘", desc: "任一指标可追溯至源系统字段,审计留痕自动生成。" },
{ icon: "clock", title: "实时指标", desc: "核心经营指标分钟级更新,支撑日内经营决策。" },
{ icon: "target", title: "风险双引擎", desc: "规则引擎与模型引擎并行,覆盖名单、阈值与图谱识别。" },
],
},
{
type: "table",
head: ["指标", "改造前", "改造后"],
rows: [
["经营日报时效", "T+1 上午", "当日分钟级"],
["监管报送核对", "5 人日 / 期", "0.5 人日 / 期"],
["风险识别方式", "名单规则为主", "规则 + 模型 + 图谱"],
],
},
],
},
{
slug: "logistics-company",
section: "cases",
category: "retail",
title: "某头部物流企业",
subtitle: "智能调度与在途可视化",
summary:
"每天 1.2 万单的动态排线,单均运输成本下降 14%,时效达成率 98.6%。",
tone: "teal",
tags: ["物流", "路径优化", "时效履约"],
blocks: [
{
type: "paragraph",
text: "客户日均处理 1.2 万张运单,调度依赖资深调度员的经验。旺季运力紧张时,排线质量波动明显,时效投诉随之上升。",
},
{
type: "features",
items: [
{ icon: "target", title: "分钟级排线", desc: "综合时效、成本与装载率,分钟级输出可用方案。" },
{ icon: "monitor", title: "在途可视化", desc: "位置与温控数据实时回传,偏离路线自动预警。" },
{ icon: "clipboard", title: "人机协同", desc: "调度员可微调方案,调整结果回流持续优化模型。" },
],
},
{
type: "stats",
items: [
{ value: "-14%", label: "单均运输成本" },
{ value: "+9%", label: "车辆装载率" },
{ value: "98.6%", label: "承诺时效达成率" },
],
},
],
},
{
slug: "gov-platform",
section: "cases",
category: "finance",
title: "某市级政务平台",
subtitle: "数据共享与一网通办",
summary:
"完成 42 个部门的数据编目,一件事办理材料精简 60%。",
tone: "navy",
tags: ["政务", "数据共享", "一网通办"],
blocks: [
{
type: "paragraph",
text: "该市在推进一网通办过程中遇到数据壁垒:部门之间不清楚彼此有哪些数据,跨部门办事仍需群众重复提交材料。",
},
{
type: "features",
items: [
{ icon: "file", title: "统一数据目录", desc: "完成 42 个部门、1800 余项数据的编目与责任登记。" },
{ icon: "plug", title: "共享交换", desc: "库表、文件、接口三种方式按需调用,全程留痕。" },
{ icon: "users", title: "一件事办理", desc: "以事项为主线串联多部门,材料一次提交多次复用。" },
],
},
{
type: "stats",
items: [
{ value: "42 个", label: "已编目部门" },
{ value: "-60%", label: "办事提交材料" },
{ value: "+55%", label: "一次办结率" },
],
},
],
},
];
+154
View File
@@ -0,0 +1,154 @@
import type { ContentItem } from "@/types/content";
export const events: ContentItem[] = [
{
slug: "data-platform-v3-trial",
section: "events",
category: "online",
title: "数据中台 V3.0 全量开放试用",
subtitle: "线上 · 9 月 10 日 - 9 月 30 日",
summary:
"面向全部客户开放试用,体验流批一体内核、统一指标中心与数据质量中心。",
tone: "blue",
date: "09.10 ~ 09.30",
tags: ["线上", "免费试用"],
featured: true,
blocks: [
{
type: "paragraph",
text: "数据中台 V3.0 全量开放试用。试用账号开通后可直接体验流批一体内核、统一指标中心与数据质量中心三项核心能力,我们同时提供 20 个行业示例数据集供直接使用。",
},
{ type: "heading", text: "活动亮点" },
{
type: "list",
items: [
"开箱即用的行业示例数据集,无需自备数据即可验证",
"完整功能开放,无功能裁剪与调用次数限制",
"试用期内配备专属技术支持,工作日 2 小时内响应",
"提交试用反馈可获得 V3.0 官方实施手册",
],
},
{
type: "table",
head: ["项目", "说明"],
rows: [
["活动时间", "2026.09.10 09:00 ~ 2026.09.30 18:00"],
["参与形式", "线上,随时开始"],
["参与对象", "企业客户与合作伙伴"],
["报名方式", "提交试用申请,1 个工作日内开通账号"],
],
},
],
},
{
slug: "summit-2026",
section: "events",
category: "conference",
title: "2026 云泽数智大会 · 上海站",
subtitle: "线下 · 10 月 18 日 - 10 月 19 日",
summary:
"一个主论坛 + 四个行业分论坛 + 800㎡ 产品体验区,30 余位客户现场分享。",
tone: "navy",
date: "10.18 ~ 10.19",
tags: ["大会", "上海"],
blocks: [
{
type: "paragraph",
text: "本届大会以「数智驱动 产业升级」为主题,设主论坛一个、行业分论坛四个,并在现场设置 800 平方米产品体验区。",
},
{ type: "heading", text: "日程概览" },
{
type: "steps",
items: [
{ title: "10.18 上午 · 主论坛", desc: "年度产品路线图发布,三项重大能力升级同步揭晓。" },
{ title: "10.18 下午 · 分论坛", desc: "制造业与零售消费两个行业分论坛平行开讲。" },
{ title: "10.19 上午 · 分论坛", desc: "能源电力与金融两个行业分论坛平行开讲。" },
{ title: "10.19 下午 · 体验区", desc: "产品实操体验与一对一方案咨询,需提前预约时段。" },
],
},
{
type: "table",
head: ["信息", "内容"],
rows: [
["举办时间", "2026.10.18 ~ 2026.10.19"],
["举办地点", "上海 · 国家会展中心 3 号馆"],
["参会费用", "免费,需审核通过"],
["报名方式", "官网在线报名,名额限 1200 人"],
],
},
],
},
{
slug: "partner-city-tour",
section: "events",
category: "offline",
title: "全国合作伙伴城市巡展",
subtitle: "线下 · 11 月 5 日 - 11 月 28 日",
summary:
"覆盖 12 座城市,面向合作伙伴开放产品路线、政策解读与联合方案研讨。",
tone: "indigo",
date: "11.05 ~ 11.28",
tags: ["合作伙伴", "巡展"],
blocks: [
{
type: "paragraph",
text: "本次巡展覆盖华东、华南、西南共 12 座城市,每站半天,内容包含产品路线解读、渠道政策说明与联合方案研讨。",
},
{
type: "list",
items: [
"产品路线:未来一年的产品规划与能力开放节奏",
"渠道政策:伙伴分级标准、返点规则与项目保护机制",
"联合方案:围绕区域重点行业打磨可复制的方案包",
"现场答疑:原厂售前与交付团队面对面沟通",
],
},
{
type: "table",
head: ["日期", "城市", "形式"],
rows: [
["11.05", "杭州", "线下闭门会"],
["11.12", "广州", "线下闭门会"],
["11.19", "成都", "线下闭门会"],
["11.28", "线上", "全国同步直播"],
],
},
],
},
{
slug: "data-platform-open-class-sept",
section: "events",
category: "online",
title: "数据中台实战公开课 · 第二期",
subtitle: "线上直播 · 9 月 25 日 20:00",
summary:
"主题:实时计算任务如何做到稳定可控。由云泽产品总监主讲,含 20 分钟答疑。",
tone: "teal",
date: "09.25",
tags: ["公开课", "直播"],
blocks: [
{
type: "paragraph",
text: "第二期公开课聚焦实时计算任务的稳定性问题。我们将结合三个真实故障案例,拆解任务延迟、数据倾斜与状态膨胀三类典型问题的排查与治理方法。",
},
{ type: "heading", text: "课程大纲" },
{
type: "list",
items: [
"实时任务的稳定性到底由哪些因素决定",
"任务延迟的三种常见成因与定位路径",
"数据倾斜与状态膨胀的工程化治理手段",
"上线前的检查清单与线上监控指标建议",
],
},
{
type: "contact",
items: [
{ icon: "clock", label: "直播时间", value: "2026 年 9 月 25 日 20:00 - 21:30" },
{ icon: "monitor", label: "直播平台", value: "官网直播间 / 视频号同步" },
{ icon: "phone", label: "咨询电话", value: "400-000-0000" },
],
},
],
},
];
+308
View File
@@ -0,0 +1,308 @@
import type { ContentItem } from "@/types/content";
export const news: ContentItem[] = [
{
slug: "series-b-financing",
section: "news",
category: "company",
title: "云泽科技完成新一轮战略融资,加速数据智能产品研发",
subtitle: "公司公告",
summary:
"本轮融资将主要用于数据智能平台的产品迭代、行业解决方案深化以及海外市场拓展。",
tone: "navy",
date: "2025.08.20",
author: "品牌公关部",
tags: ["融资", "公司动态"],
featured: true,
blocks: [
{
type: "paragraph",
text: "云泽科技今日宣布完成新一轮战略融资,由产业资本与老股东共同参与。本轮融资将主要用于三个方向:数据智能平台的产品迭代、重点行业解决方案的深化,以及海外市场的初步布局。",
},
{
type: "paragraph",
text: "公司创始人表示,过去两年客户需求已经从「要不要做数字化」转变为「怎么做得更快更稳」,这要求厂商不只是提供工具,还要能陪着客户把数据真正用起来。",
},
{ type: "heading", text: "资金用途" },
{
type: "list",
items: [
"数据中台与智能分析产品的持续研发投入",
"制造业、零售、能源三个重点行业的方案沉淀",
"东南亚与中东市场的团队组建与本地化适配",
"客户成功体系建设,提升交付后的长期价值",
],
},
{
type: "quote",
text: "我们不追求把产品线铺得最宽,而是希望在自己擅长的三个行业里做到客户愿意推荐给同行。",
author: "云泽科技 创始人兼 CEO",
},
],
},
{
slug: "data-platform-v3",
section: "news",
category: "product",
title: "云泽数据中台 V3.0 正式发布:全链路实时计算能力升级",
subtitle: "产品发布",
summary:
"新版本在实时计算、指标管理与数据资产治理三个方向进行了系统性重构。",
tone: "blue",
date: "2025.08.20",
author: "产品中心",
tags: ["数据中台", "版本发布"],
featured: true,
blocks: [
{
type: "paragraph",
text: "云泽数据中台 V3.0 今日正式发布。本次版本重构了实时计算内核,将流式与批量任务的开发、调度与运维统一到一个引擎中,端到端延迟可稳定控制在秒级。",
},
{ type: "heading", text: "主要更新" },
{
type: "features",
items: [
{ icon: "clock", title: "流批一体内核", desc: "同一套 SQL 同时支持流式与批量任务,减少重复开发。" },
{ icon: "chart", title: "统一指标中心", desc: "指标定义、计算逻辑与权限集中维护,一处修改全局生效。" },
{ icon: "shield", title: "数据质量中心", desc: "内置 30 余种质量规则,异常自动阻断下游任务。" },
{ icon: "plug", title: "数据源扩展", desc: "新增支持 12 种数据源,累计覆盖 40 余种主流系统。" },
],
},
{
type: "paragraph",
text: "已有客户可在控制台一键升级,V2.x 版本的作业定义保持兼容。升级前建议先在测试环境验证自定义插件。",
},
],
},
{
slug: "summit-2026-shanghai",
section: "news",
category: "event",
title: "2026 云泽数智大会将于 10 月在上海举办,报名通道开启",
subtitle: "市场活动",
summary:
"本届大会以「数智驱动 产业升级」为主题,将邀请 30 余位行业客户分享落地实践。",
tone: "indigo",
date: "2025.08.18",
author: "市场部",
tags: ["大会", "报名"],
blocks: [
{
type: "paragraph",
text: "2026 云泽数智大会将于 10 月 18 日至 19 日在上海国家会展中心举办。大会设一个主论坛与四个行业分论坛,现场另设 800 平方米的产品体验区。",
},
{ type: "heading", text: "大会亮点" },
{
type: "list",
items: [
"主论坛发布年度产品路线图与三项重大能力升级",
"制造业、零售、能源、金融四个行业分论坛同步开讲",
"30 余位客户现场分享项目踩坑经验与真实收益数据",
"产品体验区提供数据中台与边缘网关的现场实操",
],
},
{
type: "table",
head: ["时间", "议程", "地点"],
rows: [
["10.18 09:30", "开幕式与主论坛", "主会场"],
["10.18 14:00", "制造业 / 零售分论坛", "平行会场 A / B"],
["10.19 09:30", "能源 / 金融分论坛", "平行会场 A / B"],
["10.19 14:00", "产品体验与一对一咨询", "体验区"],
],
},
],
},
{
slug: "manufacturing-case-live",
section: "news",
category: "case",
title: "云泽科技助力某大型制造企业完成生产数据一体化改造",
subtitle: "客户案例",
summary:
"项目上线后,客户设备数据采集覆盖率提升至 96%,生产异常响应时长缩短 60%。",
tone: "teal",
date: "2025.08.15",
author: "客户成功部",
tags: ["制造业", "项目验收"],
blocks: [
{
type: "paragraph",
text: "历时 6 个月,云泽科技为某大型精密制造集团完成 6 个厂区、1200 余台设备的数据一体化改造,项目已于近日通过终验。",
},
{
type: "stats",
items: [
{ value: "96%", label: "设备数据采集覆盖率" },
{ value: "-60%", label: "生产异常响应时长" },
{ value: "+18%", label: "设备综合效率(OEE)" },
],
},
{
type: "paragraph",
text: "项目采用边缘网关统一采集、中台统一建模的两层架构,集团层面首次实现了跨厂区的产能对比与瓶颈识别。",
},
],
},
{
slug: "iso27001-certified",
section: "news",
category: "company",
title: "云泽科技通过 ISO 27001 信息安全管理体系认证",
subtitle: "公司公告",
summary:
"认证范围覆盖产品研发、项目实施与云端运维全流程,进一步满足金融与政务客户的合规要求。",
tone: "navy",
date: "2025.08.08",
author: "安全合规部",
tags: ["认证", "信息安全"],
blocks: [
{
type: "paragraph",
text: "云泽科技近日正式通过 ISO/IEC 27001 信息安全管理体系认证,认证范围覆盖产品研发、项目实施与云端运维全流程。",
},
{
type: "paragraph",
text: "配合本次认证,公司在数据分类分级、访问控制、供应商管理与应急响应四个方面完善了内部制度,并完成了全员安全意识培训。",
},
{
type: "list",
items: [
"建立数据分类分级标准与对应的访问控制策略",
"研发与生产环境网络隔离,变更须双人复核",
"与核心供应商签署数据处理协议并定期审计",
"制定安全事件响应预案,每半年组织一次演练",
],
},
],
},
{
slug: "analytics-3-5",
section: "news",
category: "product",
title: "智能分析引擎 3.5 版本上线:新增自然语言取数能力",
subtitle: "产品发布",
summary:
"业务人员可直接用一句话描述分析需求,系统自动生成查询并推荐可视化方式。",
tone: "blue",
date: "2025.07.30",
author: "产品中心",
tags: ["版本发布", "智能分析"],
blocks: [
{
type: "paragraph",
text: "智能分析引擎 3.5 版本新增自然语言取数能力。业务人员只需用日常语言描述问题,系统即会结合指标中心生成查询语句,并推荐合适的图表形式。",
},
{ type: "heading", text: "本次更新" },
{
type: "list",
items: [
"自然语言取数:支持中文口语化提问与多轮追问",
"图表智能推荐:根据字段类型与分析意图自动选择图表",
"看板嵌入能力增强:支持免登录 Token 嵌入既有业务系统",
"移动端适配优化:复杂表格在小屏下自动切换为卡片视图",
],
},
{
type: "paragraph",
text: "自然语言取数默认开启审计日志,所有生成的查询语句均可回溯,便于数据团队掌握使用情况。",
},
],
},
{
slug: "partner-program-2026",
section: "news",
category: "company",
title: "2026 年全国合作伙伴招募计划正式启动",
subtitle: "公司公告",
summary:
"面向华东、华南、西南三个区域招募解决方案与实施交付伙伴,提供产品培训与项目支持。",
tone: "indigo",
date: "2025.07.22",
author: "渠道合作部",
tags: ["合作伙伴", "招募"],
blocks: [
{
type: "paragraph",
text: "云泽科技 2026 年全国合作伙伴招募计划正式启动,本轮重点面向华东、华南与西南三个区域,招募解决方案伙伴与实施交付伙伴。",
},
{ type: "heading", text: "伙伴权益" },
{
type: "features",
items: [
{ icon: "book", title: "产品培训", desc: "每季度一次线下集训,配套线上课程与认证考试。" },
{ icon: "headset", title: "售前支持", desc: "重点项目提供原厂售前与方案架构师陪访。" },
{ icon: "shield", title: "交付保障", desc: "提供标准交付方法论与项目管理系统支持。" },
],
},
{
type: "paragraph",
text: "有意向的伙伴可通过官网合作伙伴页面提交申请,我们会在 5 个工作日内完成初步沟通。",
},
],
},
{
slug: "retail-case-acceptance",
section: "news",
category: "case",
title: "某全国连锁零售企业会员数据中台项目顺利验收",
subtitle: "客户案例",
summary:
"打通 3 个线上渠道与 800 家门店的会员数据,活动 ROI 提升 24%。",
tone: "teal",
date: "2025.07.15",
author: "客户成功部",
tags: ["零售", "项目验收"],
blocks: [
{
type: "paragraph",
text: "该项目覆盖客户旗下 800 余家门店与小程序、电商旗舰店、社群三个线上渠道,完成了会员身份的跨渠道归一与标签体系建设。",
},
{
type: "stats",
items: [
{ value: "+18%", label: "会员复购率" },
{ value: "+24%", label: "活动 ROI" },
{ value: "-30%", label: "滞销库存占比" },
],
},
{
type: "quote",
text: "现在我们做一场活动,先选人群再定商品,而不是先把商品发下去再想卖给谁。",
author: "客户数字化总监",
},
],
},
{
slug: "data-platform-open-class",
section: "news",
category: "event",
title: "数据中台实战公开课开播,首期聚焦指标管理",
subtitle: "市场活动",
summary:
"由云泽产品团队主讲的系列公开课,每期围绕一个真实项目难题展开拆解。",
tone: "blue",
date: "2025.07.08",
author: "市场部",
tags: ["公开课", "直播"],
blocks: [
{
type: "paragraph",
text: "数据中台实战公开课正式开播,首期主题为「指标口径不统一,到底该从哪里下手」。课程以真实项目为背景,拆解指标混乱的常见成因与治理路径。",
},
{
type: "list",
items: [
"第一期:指标口径不统一的成因与治理路径",
"第二期:实时计算任务如何做到稳定可控",
"第三期:数据质量规则应该由谁来定",
"第四期:数据资产目录的落地与维护",
],
},
{
type: "paragraph",
text: "课程在官网与视频号同步直播,回放将在开播后三个工作日内上线。",
},
],
},
];
+564
View File
@@ -0,0 +1,564 @@
import type { SinglePage } from "@/types/content";
/**
* 独立单页内容
* key = 去掉前导斜杠的路由路径,例如 about、support/docs、policy/privacy
*/
export const singlePages: Record<string, SinglePage> = {
about: {
title: "关于云泽",
subtitle: "用可靠的产品与长期的陪伴,帮企业把数据真正用起来。",
crumbs: ["关于云泽"],
tone: "navy",
blocks: [
{
type: "paragraph",
text: "云泽科技成立于 2014 年,专注企业级云计算与数据智能领域。我们提供云原生平台、数据中台与智能分析产品,并为制造业、零售消费、能源电力与金融行业提供场景化解决方案。十二年里,我们服务了超过 1000 家企业客户,其中 300 余家为上市公司或集团型企业。",
},
{
type: "stats",
items: [
{ value: "2014", label: "成立年份" },
{ value: "600+", label: "员工规模" },
{ value: "62%", label: "研发人员占比" },
{ value: "1000+", label: "服务企业客户" },
],
},
{ type: "heading", text: "发展历程", sub: "从一支 8 人团队到全国 6 个交付中心" },
{
type: "timeline",
items: [
{ year: "2014", text: "公司成立,从企业级中间件与容器化咨询起步。" },
{ year: "2017", text: "发布自研云原生平台,服务首批 50 家制造业客户。" },
{ year: "2019", text: "数据中台 1.0 发布,进入零售与能源行业。" },
{ year: "2021", text: "完成 B 轮融资,成立上海、深圳、成都交付中心。" },
{ year: "2023", text: "智能分析引擎发布,累计服务客户突破 800 家。" },
{ year: "2026", text: "数据中台 V3.0 发布,启动海外市场布局。" },
],
},
{ type: "heading", text: "我们相信什么", sub: "这三条决定了我们怎么做产品、怎么做项目" },
{
type: "features",
items: [
{ icon: "target", title: "先解决业务问题", desc: "不为了上系统而上系统,先弄清楚客户到底想改善哪个指标。" },
{ icon: "shield", title: "交付即运维起点", desc: "项目终验不是结束,我们要求交付团队对上线后半年的稳定负责。" },
{ icon: "users", title: "把能力留给客户", desc: "培训与文档与产品同等重要,客户能自主运转才算真正交付。" },
],
},
{ type: "heading", text: "资质与荣誉" },
{
type: "list",
items: [
"国家高新技术企业",
"ISO/IEC 27001 信息安全管理体系认证",
"CMMI 5 级软件成熟度认证",
"累计取得软件著作权 120 余项、发明专利 26 项",
"连续三年入选某行业研究机构「数据中台代表厂商」",
],
},
],
},
careers: {
title: "加入我们",
subtitle: "和一群愿意把事情做扎实的人,一起做点有难度的事。",
crumbs: ["加入我们"],
tone: "blue",
blocks: [
{
type: "paragraph",
text: "我们是一家技术驱动型公司,研发人员占比 62%。这里的项目大多有真实的业务难题,客户会在验收现场告诉你哪些地方还不好用——这既是压力,也是成长最快的地方。",
},
{ type: "heading", text: "为什么选择云泽" },
{
type: "features",
items: [
{ icon: "target", title: "做有难度的项目", desc: "面向集团级客户,数据规模与业务复杂度都是真实的。" },
{ icon: "users", title: "扁平的技术氛围", desc: "技术方案在评审会上讨论,不按职级定对错。" },
{ icon: "clock", title: "尊重生活节奏", desc: "项目冲刺有调休,非必要不加班。" },
{ icon: "book", title: "持续学习投入", desc: "每年人均 8000 元学习预算,内部技术分享每周一次。" },
],
},
{ type: "heading", text: "热招岗位", sub: "以下岗位长期开放,欢迎自荐或推荐" },
{
type: "table",
head: ["岗位", "方向", "经验要求", "工作地点"],
rows: [
["后端研发工程师", "数据中台", "3 年以上", "北京 / 成都"],
["大数据开发工程师", "实时计算", "3 年以上", "北京 / 上海"],
["前端研发工程师", "智能分析", "2 年以上", "北京"],
["解决方案架构师", "制造业方向", "5 年以上", "上海 / 深圳"],
["实施交付顾问", "零售方向", "3 年以上", "广州 / 成都"],
["客户成功经理", "重点客户", "3 年以上", "北京 / 上海"],
],
},
{ type: "heading", text: "福利待遇" },
{
type: "list",
items: [
"六险一金,补充商业医疗保险覆盖配偶与子女",
"年度体检、带薪年假 10 天起、司龄假",
"季度绩效奖金 + 项目奖金,核心岗位含股权激励",
"弹性工作制,每周可远程办公 1 天",
],
},
{
type: "links",
items: [
{ icon: "mail", title: "投递简历", desc: "发送至 hr@yunzer.com,标题注明岗位与城市", path: "/contact" },
{ icon: "users", title: "员工内推", desc: "内推成功可获得推荐奖金,详情咨询 HR", path: "/contact" },
],
},
],
},
partners: {
title: "合作伙伴",
subtitle: "与区域伙伴一起,把方案落到离客户最近的地方。",
crumbs: ["合作伙伴"],
tone: "indigo",
blocks: [
{
type: "paragraph",
text: "我们采用「原厂产品 + 伙伴交付」的模式服务全国客户。原厂负责产品研发、售前支持与交付质量把关,伙伴负责区域客户经营与项目实施,双方在项目中共享收益。",
},
{ type: "heading", text: "合作类型" },
{
type: "features",
items: [
{ icon: "target", title: "解决方案伙伴", desc: "在特定行业具备方案能力,可与原厂联合打磨行业方案包。" },
{ icon: "headset", title: "实施交付伙伴", desc: "具备项目实施与运维能力,承接区域项目的落地交付。" },
{ icon: "plug", title: "产品集成伙伴", desc: "产品可与云泽平台完成集成认证,进入联合解决方案目录。" },
{ icon: "book", title: "培训认证伙伴", desc: "面向区域客户提供产品培训与认证服务。" },
],
},
{ type: "heading", text: "伙伴权益" },
{
type: "list",
items: [
"区域项目保护,避免伙伴之间互相压价",
"重点项目原厂售前与架构师陪访支持",
"季度线下集训 + 线上课程 + 认证考试",
"标准交付方法论、项目管理系统与验收模板",
],
},
{ type: "heading", text: "合作流程" },
{
type: "steps",
items: [
{ title: "提交申请", desc: "在线提交公司资质、行业经验与团队规模信息。" },
{ title: "初步沟通", desc: "5 个工作日内完成首次沟通,明确合作类型。" },
{ title: "能力评估", desc: "评估方案或交付能力,必要时安排技术面谈。" },
{ title: "签约授权", desc: "签署合作协议并完成伙伴分级与授权。" },
{ title: "培训赋能", desc: "参加集训并通过认证,进入项目推荐池。" },
],
},
{
type: "links",
items: [
{ icon: "mail", title: "提交合作申请", desc: "发送公司简介与意向合作类型至 partner@yunzer.com", path: "/contact" },
{ icon: "file", title: "查阅伙伴政策", desc: "分级标准、返点规则与项目保护机制说明", path: "/support/docs" },
],
},
],
},
contact: {
title: "联系我们",
subtitle: "无论是产品咨询、方案交流还是合作洽谈,我们都会在 1 个工作日内回复。",
crumbs: ["联系我们"],
tone: "blue",
blocks: [
{
type: "contact",
items: [
{ icon: "phone", label: "服务热线", value: "400-000-0000(工作日 9:00 - 18:00)" },
{ icon: "mail", label: "商务合作", value: "contact@yunzer.com" },
{ icon: "mail", label: "渠道合作", value: "partner@yunzer.com" },
{ icon: "location", label: "总部地址", value: "北京市海淀区中关村软件园 25 号楼" },
],
},
{ type: "heading", text: "分支机构", sub: "就近联系我们的区域团队" },
{
type: "table",
head: ["城市", "地址", "联系电话"],
rows: [
["北京(总部)", "海淀区中关村软件园 25 号楼", "010-0000-0000"],
["上海", "浦东新区张江高科技园区 8 号楼", "021-0000-0000"],
["深圳", "南山区科技园南区 12 栋", "0755-0000-0000"],
["成都", "高新区天府大道中段 966 号", "028-0000-0000"],
],
},
{ type: "heading", text: "按需求选择入口" },
{
type: "links",
items: [
{ icon: "monitor", title: "预约产品演示", desc: "1 对 1 演示与答疑,通常 30 分钟", path: "/support/demo" },
{ icon: "clipboard", title: "申请免费试用", desc: "1 个工作日内开通试用账号", path: "/support/trial" },
{ icon: "book", title: "查阅产品文档", desc: "快速上手、接口说明与常见问题", path: "/support/docs" },
{ icon: "headset", title: "客户支持中心", desc: "已签约客户的服务入口与工单通道", path: "/support" },
],
},
],
},
support: {
title: "客户支持中心",
subtitle: "从上线到日常运维,遇到问题时有明确的路径可以走。",
crumbs: ["客户支持"],
tone: "navy",
blocks: [
{
type: "paragraph",
text: "我们把支持体系分成「自助」与「人工」两层:常规问题通过文档与开发者中心自助解决,复杂问题通过工单进入原厂技术支持,并按合同约定的响应时效处理。",
},
{ type: "heading", text: "自助服务" },
{
type: "links",
items: [
{ icon: "book", title: "产品文档", desc: "安装部署、功能说明与最佳实践", path: "/support/docs" },
{ icon: "code", title: "开发者中心", desc: "SDK、API 文档与示例代码", path: "/support/developer" },
{ icon: "clipboard", title: "申请试用", desc: "开通试用账号,含示例数据集", path: "/support/trial" },
{ icon: "monitor", title: "预约演示", desc: "由售前顾问进行 1 对 1 演示", path: "/support/demo" },
],
},
{ type: "heading", text: "支持体系" },
{
type: "features",
items: [
{ icon: "headset", title: "专属支持群", desc: "每个项目建立专属群,原厂研发与交付人员都在群里。" },
{ icon: "clock", title: "分级响应", desc: "按问题等级约定响应与解决时效,超时自动升级。" },
{ icon: "shield", title: "版本与补丁", desc: "重大缺陷提供补丁包,安全漏洞 24 小时内响应。" },
{ icon: "file", title: "季度巡检", desc: "每季度输出一次系统健康报告与优化建议。" },
],
},
{ type: "heading", text: "服务承诺", sub: "详见服务保障条款" },
{
type: "stats",
items: [
{ value: "2 小时", label: "紧急问题响应" },
{ value: "8 小时", label: "一般问题响应" },
{ value: "99.9%", label: "标准版服务可用性" },
{ value: "24 小时", label: "安全漏洞响应" },
],
},
{ type: "paragraph", text: "完整的服务等级与赔付条款请查阅服务保障页面。" },
{
type: "links",
items: [
{ icon: "shield", title: "服务保障(SLA)", desc: "分级响应时效、可用性承诺与赔付规则", path: "/support/sla" },
],
},
],
},
"support/docs": {
title: "产品文档",
subtitle: "安装部署、功能说明、最佳实践与常见问题。",
crumbs: ["客户支持", "产品文档"],
tone: "blue",
blocks: [
{
type: "paragraph",
text: "产品文档覆盖部署、配置、开发与运维四个阶段,所有文档随版本同步更新。建议在升级前先查阅对应版本的变更说明与兼容性提示。",
},
{ type: "heading", text: "文档目录" },
{
type: "features",
items: [
{ icon: "monitor", title: "快速开始", desc: "环境要求、安装部署与初始化配置,30 分钟完成首次启动。" },
{ icon: "file", title: "功能说明", desc: "按模块说明功能入口、参数含义与使用限制。" },
{ icon: "book", title: "最佳实践", desc: "来自真实项目的建模规范、命名约定与调优建议。" },
{ icon: "shield", title: "运维手册", desc: "监控指标、备份恢复、扩容缩容与故障处理流程。" },
],
},
{ type: "heading", text: "按产品查阅" },
{
type: "table",
head: ["产品", "当前版本", "文档入口"],
rows: [
["云原生平台", "v5.2", "部署指南 / 运维手册"],
["数据中台", "v3.0", "快速开始 / 指标管理 / 数据质量"],
["智能分析引擎", "v3.5", "建模指南 / 看板设计 / 移动端"],
["边缘物联网关", "v2.4", "协议清单 / 边缘推理 / 远程升级"],
],
},
{ type: "paragraph", text: "找不到需要的文档?可以在客户支持中心提交工单,我们会补充对应的说明。" },
],
},
"support/developer": {
title: "开发者中心",
subtitle: "SDK、开放 API、示例代码与集成认证。",
crumbs: ["客户支持", "开发者中心"],
tone: "indigo",
blocks: [
{
type: "paragraph",
text: "开发者中心面向需要把云泽产品集成进既有系统的团队。平台提供 REST API、多语言 SDK 与 Webhook 回调,全部接口遵循统一的鉴权与错误码规范。",
},
{
type: "features",
items: [
{ icon: "code", title: "开放 API", desc: "覆盖数据接入、任务调度、指标查询与权限管理。" },
{ icon: "plug", title: "多语言 SDK", desc: "提供 Java、Go、Python、TypeScript 四种语言 SDK。" },
{ icon: "monitor", title: "沙箱环境", desc: "独立沙箱与示例数据集,不影响生产环境。" },
{ icon: "shield", title: "集成认证", desc: "通过集成测试后可进入联合解决方案目录。" },
],
},
{ type: "heading", text: "接入流程" },
{
type: "steps",
items: [
{ title: "创建应用", desc: "在控制台创建应用并获取 Client ID 与密钥。" },
{ title: "沙箱联调", desc: "在沙箱环境完成接口联调,验证鉴权与错误处理。" },
{ title: "提交审核", desc: "提交集成说明与技术联系人信息。" },
{ title: "正式上线", desc: "审核通过后切换生产环境,获得集成认证标识。" },
],
},
{ type: "paragraph", text: "接口限流默认按应用维度控制,如需提升配额请联系客户成功经理。" },
],
},
"support/trial": {
title: "申请免费试用",
subtitle: "1 个工作日内开通账号,含行业示例数据集,无需自备数据。",
crumbs: ["客户支持", "申请试用"],
tone: "teal",
blocks: [
{
type: "paragraph",
text: "试用账号包含产品全部功能,不设调用次数限制。我们同时提供 20 个行业示例数据集,可以直接用于验证建模、指标与分析效果。",
},
{ type: "heading", text: "试用流程" },
{
type: "steps",
items: [
{ title: "提交申请", desc: "填写企业与联系人信息,说明希望验证的场景。" },
{ title: "账号开通", desc: "1 个工作日内收到账号与初始密码,含入门指引。" },
{ title: "自助验证", desc: "使用示例数据或自备数据完成场景验证,可随时咨询。" },
{ title: "评估与选型", desc: "试用结束后我们提供选型建议与部署方案。" },
],
},
{ type: "heading", text: "试用版与正式版差异" },
{
type: "table",
head: ["项目", "试用版", "正式版"],
rows: [
["功能范围", "全部功能", "全部功能"],
["使用期限", "30 天", "按合同约定"],
["部署方式", "云端共享环境", "私有化 / 专有云 / 公有云"],
["技术支持", "工作日 2 小时内", "按 SLA 分级响应"],
],
},
{
type: "links",
items: [
{ icon: "clipboard", title: "提交试用申请", desc: "填写信息后 1 个工作日内开通", path: "/contact" },
{ icon: "monitor", title: "先预约一次演示", desc: "由售前顾问讲解并演示关键能力", path: "/support/demo" },
],
},
],
},
"support/sla": {
title: "服务保障",
subtitle: "分级响应时效、可用性承诺与赔付规则说明。",
crumbs: ["客户支持", "服务保障"],
tone: "navy",
blocks: [
{
type: "paragraph",
text: "本页说明标准版服务等级协议(SLA)的主要内容。具体条款以双方签署的合同为准,企业版客户可在合同中约定更高等级的服务承诺。",
},
{ type: "heading", text: "问题分级与响应时效" },
{
type: "table",
head: ["等级", "定义", "响应时效", "解决时效"],
rows: [
["P1 紧急", "生产环境不可用或核心数据丢失风险", "30 分钟", "4 小时"],
["P2 严重", "核心功能不可用,业务受阻但有替代方案", "2 小时", "1 个工作日"],
["P3 一般", "非核心功能异常或使用体验问题", "8 小时", "5 个工作日"],
["P4 咨询", "使用咨询、配置指导与需求建议", "1 个工作日", "协商确定"],
],
},
{ type: "heading", text: "可用性承诺" },
{
type: "stats",
items: [
{ value: "99.9%", label: "标准版月可用性" },
{ value: "99.95%", label: "企业版月可用性" },
{ value: "24 小时", label: "安全漏洞响应" },
],
},
{ type: "heading", text: "赔付规则" },
{
type: "list",
items: [
"月度可用性低于承诺值时,按当季服务费的一定比例进行抵扣",
"P1 级问题响应超时,每次按合同约定金额赔付",
"因计划内维护导致的不可用不计入可用性统计,维护需提前 3 个工作日通知",
"不可抗力、客户自身环境变更或第三方系统故障不纳入赔付范围",
],
},
{ type: "paragraph", text: "如需了解完整条款或申请更高的服务等级,请联系您的客户成功经理。" },
],
},
"support/demo": {
title: "预约产品演示",
subtitle: "由售前顾问进行 1 对 1 演示,围绕你的业务场景展开。",
crumbs: ["客户支持", "预约演示"],
tone: "blue",
blocks: [
{
type: "paragraph",
text: "演示通常安排在 30 至 45 分钟,我们会在开始前先了解你的业务场景与关注点,演示内容围绕这些场景展开,而不是照着产品功能清单念一遍。",
},
{ type: "heading", text: "演示流程" },
{
type: "steps",
items: [
{ title: "提交预约", desc: "填写企业信息、业务场景与期望的演示时间。" },
{ title: "需求确认", desc: "售前顾问在 1 个工作日内电话沟通,明确关注重点。" },
{ title: "在线演示", desc: "线上会议进行 30 - 45 分钟演示与答疑。" },
{ title: "方案建议", desc: "会后 2 个工作日内提供针对性的方案与选型建议。" },
],
},
{ type: "heading", text: "预约方式" },
{
type: "contact",
items: [
{ icon: "phone", label: "预约热线", value: "400-000-0000(工作日 9:00 - 18:00)" },
{ icon: "mail", label: "预约邮箱", value: "demo@yunzer.com" },
{ icon: "clock", label: "可预约时段", value: "工作日 10:00 - 17:00" },
],
},
],
},
"policy/privacy": {
title: "隐私政策",
subtitle: "我们如何收集、使用和保护你的个人信息。",
crumbs: ["隐私政策"],
tone: "navy",
blocks: [
{ type: "paragraph", text: "生效日期:2026 年 1 月 1 日。本政策适用于云泽科技官方网站及我们提供的相关服务。" },
{ type: "heading", text: "一、我们收集的信息" },
{
type: "list",
items: [
"你主动提交的信息:姓名、公司名称、联系电话、邮箱等表单信息",
"自动收集的信息:IP 地址、浏览器类型、访问时间与访问页面",
"为你提供服务过程中产生的信息:账号信息、操作日志与工单记录",
],
},
{ type: "heading", text: "二、信息的使用" },
{
type: "paragraph",
text: "我们将收集到的信息用于:响应你的咨询与需求、向你发送你已同意的产品与活动信息、改进网站与产品体验、以及履行法律法规要求的义务。我们不会将你的个人信息用于与上述目的无关的用途。",
},
{ type: "heading", text: "三、信息的共享" },
{
type: "list",
items: [
"除法律法规要求或你明确授权外,我们不会向第三方共享你的个人信息",
"为完成服务而必须委托的供应商,我们会与其签署数据处理协议并限制其使用范围",
"涉及合并、分立等情形的,我们会要求承继方继续受本政策约束",
],
},
{ type: "heading", text: "四、信息的保护" },
{
type: "paragraph",
text: "我们通过了 ISO/IEC 27001 信息安全管理体系认证,采取数据分类分级、访问控制、传输与存储加密、操作审计等措施保护你的信息。",
},
{ type: "heading", text: "五、你的权利" },
{
type: "paragraph",
text: "你有权查询、更正、删除你提交的个人信息,或撤回此前的授权同意。如需行使上述权利,请通过 contact@yunzer.com 与我们联系,我们将在 15 个工作日内处理。",
},
],
},
"policy/terms": {
title: "使用条款",
subtitle: "访问和使用本网站前,请仔细阅读以下条款。",
crumbs: ["使用条款"],
tone: "navy",
blocks: [
{ type: "paragraph", text: "欢迎访问云泽科技官方网站。你访问或使用本网站,即表示你已阅读、理解并同意接受本条款的约束。" },
{ type: "heading", text: "一、网站内容" },
{
type: "paragraph",
text: "本网站上的文字、图片、图表、界面设计与程序代码等内容的著作权及相关权利归云泽科技或相应权利人所有。未经书面许可,任何人不得复制、修改、传播或用于商业用途。",
},
{ type: "heading", text: "二、使用规范" },
{
type: "list",
items: [
"不得利用本网站从事任何违反法律法规的活动",
"不得通过自动化程序批量抓取本站内容",
"不得尝试入侵、干扰或破坏本网站及其相关服务的正常运行",
"不得冒用云泽科技名义发布信息或从事商业活动",
],
},
{ type: "heading", text: "三、免责声明" },
{
type: "paragraph",
text: "本网站提供的信息仅供参考,不构成任何形式的承诺或保证。产品功能与规格以双方签署的合同及产品文档为准。我们不对因使用本站信息而产生的直接或间接损失承担责任。",
},
{ type: "heading", text: "四、条款变更" },
{
type: "paragraph",
text: "我们可能根据业务或法律要求调整本条款,调整后的内容将在本页面公布并注明生效日期。继续使用本网站即视为接受调整后的条款。",
},
{ type: "heading", text: "五、适用法律" },
{
type: "paragraph",
text: "本条款的订立、效力、解释与争议解决均适用中华人民共和国法律。因本条款产生的争议,双方应友好协商解决;协商不成的,提交北京市海淀区人民法院管辖。",
},
],
},
"policy/sitemap": {
title: "网站地图",
subtitle: "全站栏目与页面索引。",
crumbs: ["网站地图"],
tone: "blue",
blocks: [
{ type: "heading", text: "主要内容" },
{
type: "links",
items: [
{ icon: "monitor", title: "新闻中心", desc: "公司公告、产品发布、市场活动与客户案例", path: "/news" },
{ icon: "plug", title: "产品中心", desc: "云原生平台、数据中台、智能分析等产品", path: "/products" },
{ icon: "target", title: "解决方案", desc: "制造业、零售、能源与金融行业的场景方案", path: "/solutions" },
{ icon: "ribbon", title: "客户案例", desc: "各行业客户的落地实践与项目成果", path: "/cases" },
{ icon: "calendar", title: "市场活动", desc: "大会、公开课与合作伙伴巡展安排", path: "/events" },
],
},
{ type: "heading", text: "关于与合作" },
{
type: "links",
items: [
{ icon: "users", title: "关于云泽", desc: "公司简介、发展历程与资质荣誉", path: "/about" },
{ icon: "book", title: "加入我们", desc: "热招岗位、福利待遇与投递方式", path: "/careers" },
{ icon: "plug", title: "合作伙伴", desc: "合作类型、伙伴权益与申请流程", path: "/partners" },
{ icon: "mail", title: "联系我们", desc: "总部与分支机构联系方式", path: "/contact" },
],
},
{ type: "heading", text: "服务与支持" },
{
type: "links",
items: [
{ icon: "headset", title: "客户支持中心", desc: "自助服务与人工支持入口", path: "/support" },
{ icon: "file", title: "产品文档", desc: "部署、功能说明与最佳实践", path: "/support/docs" },
{ icon: "code", title: "开发者中心", desc: "SDK、API 与集成认证", path: "/support/developer" },
{ icon: "clipboard", title: "申请试用", desc: "开通试用账号与示例数据集", path: "/support/trial" },
{ icon: "monitor", title: "预约演示", desc: "1 对 1 产品演示与答疑", path: "/support/demo" },
{ icon: "shield", title: "服务保障", desc: "分级响应时效与可用性承诺", path: "/support/sla" },
],
},
],
},
};
+238
View File
@@ -0,0 +1,238 @@
import type { ContentItem } from "@/types/content";
export const products: ContentItem[] = [
{
slug: "cloud-native",
section: "products",
category: "platform",
title: "云原生平台",
subtitle: "容器化与微服务治理",
summary:
"一站式容器编排、服务治理与 DevOps 流水线,帮助研发团队把交付周期从周压缩到天。",
tone: "navy",
tags: ["Kubernetes", "Service Mesh", "DevOps"],
featured: true,
blocks: [
{
type: "paragraph",
text: "云泽云原生平台面向中大型企业的多集群运维场景,统一纳管容器编排、服务网格、镜像仓库与 CI/CD 流水线。平台屏蔽底层复杂度,让业务团队专注于应用本身,运维团队则通过统一控制台完成资源交付、灰度发布与故障定位。",
},
{
type: "features",
items: [
{ icon: "plug", title: "多集群统一纳管", desc: "跨公有云与自建机房统一编排,支持一键扩缩容与集群漂移。" },
{ icon: "shield", title: "服务网格治理", desc: "内置灰度、熔断、限流与全链路追踪,无需改造业务代码。" },
{ icon: "code", title: "流水线即代码", desc: "以 YAML 描述构建、测试、部署全流程,天然适配 GitOps。" },
{ icon: "chart", title: "可观测性", desc: "指标、日志、链路三合一,问题定位从小时级降到分钟级。" },
],
},
{ type: "heading", text: "适用场景", sub: "以下场景中收益最为明显" },
{
type: "list",
items: [
"业务系统正在从单体向微服务拆分,缺少统一治理手段",
"多环境、多集群并存,发布流程依赖人工、容易出错",
"需要满足等保与信创要求,要求资源可审计、可追溯",
"研发团队规模扩张,希望以平台化方式承载交付效率",
],
},
{
type: "table",
head: ["对比项", "传统模式", "使用云泽云原生平台"],
rows: [
["环境交付", "3 ~ 5 个工作日", "10 分钟以内"],
["发布方式", "停机发布", "灰度发布并可一键回滚"],
["资源利用率", "约 20%", "提升至 55% 以上"],
["故障定位", "依赖人工排查", "全链路追踪自动定位"],
],
},
],
},
{
slug: "data-platform",
section: "products",
category: "data",
title: "数据中台",
subtitle: "采集 · 治理 · 资产化",
summary:
"从数据采集、治理到资产化与实时计算的一体化平台,让指标口径统一、数据随取随用。",
tone: "blue",
tags: ["实时计算", "数据治理", "指标管理"],
featured: true,
blocks: [
{
type: "paragraph",
text: "数据中台把散落在各业务系统中的数据统一采集入湖,经过清洗、建模与指标定义后,形成可复用的数据资产。业务人员不再需要向多个部门要数,口径不一致的问题也从机制上被消除。V3.0 版本重构了实时计算内核,任务延迟可稳定控制在秒级。",
},
{
type: "features",
items: [
{ icon: "plug", title: "多源数据接入", desc: "支持数据库、消息队列、文件与 API 共 40 余种数据源。" },
{ icon: "shield", title: "数据质量监控", desc: "内置完整性、一致性、及时性规则,异常自动告警并阻断下游。" },
{ icon: "chart", title: "统一指标管理", desc: "指标定义、计算逻辑与权限统一维护,一处修改全局生效。" },
{ icon: "clock", title: "实时计算内核", desc: "流批一体,端到端延迟稳定在秒级,支撑实时决策场景。" },
],
},
{ type: "heading", text: "核心指标", sub: "基于已交付项目的统计数据" },
{
type: "stats",
items: [
{ value: "40+", label: "支持的数据源类型" },
{ value: "秒级", label: "端到端处理延迟" },
{ value: "96%", label: "平均数据采集覆盖率" },
{ value: "60%", label: "问题响应时长下降" },
],
},
{
type: "table",
head: ["版本", "核心能力", "适用规模"],
rows: [
["标准版", "离线数仓、指标管理", "10 个业务系统以内"],
["专业版", "流批一体、数据质量", "50 个业务系统以内"],
["企业版", "多租户、跨域数据交换", "集团级多法人架构"],
],
},
],
},
{
slug: "analytics",
section: "products",
category: "data",
title: "智能分析引擎",
subtitle: "自助式 BI 与智能洞察",
summary:
"面向业务人员的自助分析工具,支持拖拽建模与自然语言取数,让报表需求不再排队。",
tone: "teal",
tags: ["BI", "自助分析", "自然语言取数"],
blocks: [
{
type: "paragraph",
text: "智能分析引擎把数据准备、可视化与智能洞察整合在一个工作台里。业务人员通过拖拽即可完成多维分析,借助自然语言取数能力,只需要用一句话描述问题,系统就会自动生成查询并给出图表建议。",
},
{
type: "features",
items: [
{ icon: "chart", title: "拖拽式分析", desc: "无需 SQL 即可完成钻取、联动与同环比对比。" },
{ icon: "spark", title: "自然语言取数", desc: "用日常语言提问,自动生成查询语句与可视化结果。" },
{ icon: "target", title: "智能异常检测", desc: "自动识别指标突增突降并给出可能的影响因子。" },
{ icon: "users", title: "权限与分享", desc: "行级权限控制,看板可一键分享给指定成员或群组。" },
],
},
{ type: "heading", text: "为什么业务团队喜欢它" },
{
type: "list",
items: [
"报表需求从「提工单等排期」变为「自己动手五分钟出图」",
"指标口径与数据中台一致,避免同一张报表出现两个数字",
"移动端与钉钉、企业微信打通,管理层可随时查看",
"支持将看板嵌入既有业务系统,无需切换工作台",
],
},
],
},
{
slug: "iot-gateway",
section: "products",
category: "platform",
title: "边缘物联网关",
subtitle: "设备接入与边缘计算",
summary:
"支持 200 余种工业协议,在边缘侧完成数据采集、清洗与轻量推理,弱网环境依然稳定。",
tone: "indigo",
tags: ["工业协议", "边缘计算", "设备接入"],
blocks: [
{
type: "paragraph",
text: "边缘物联网关部署在靠近设备的一侧,负责协议解析、数据缓存与边缘推理。即使与云端断链,网关仍可独立运行并在恢复后自动补传,保证数据不丢、生产不停。",
},
{
type: "features",
items: [
{ icon: "plug", title: "协议广泛", desc: "内置 Modbus、OPC UA、Profinet 等 200 余种工业协议。" },
{ icon: "clock", title: "断网续传", desc: "本地可缓存 30 天数据,链路恢复后自动补传。" },
{ icon: "shield", title: "边缘推理", desc: "在本地运行轻量模型,满足低时延与数据不出厂的要求。" },
{ icon: "monitor", title: "远程运维", desc: "支持批量下发配置、远程升级与运行状态监控。" },
],
},
{
type: "table",
head: ["规格", "标准型", "增强型"],
rows: [
["接入点数", "2000 点", "10000 点"],
["本地存储", "64 GB", "512 GB"],
["工作温度", "-10℃ ~ 60℃", "-30℃ ~ 70℃"],
["典型部署", "车间级", "厂区级"],
],
},
],
},
{
slug: "lowcode",
section: "products",
category: "app",
title: "低代码应用平台",
subtitle: "拖拽搭建业务应用",
summary:
"表单、流程、报表一体化搭建,业务人员一周内即可上线一套内部管理系统。",
tone: "blue",
tags: ["低代码", "表单流程", "集成"],
blocks: [
{
type: "paragraph",
text: "低代码应用平台把常见的审批、台账、巡检等场景沉淀为可复用模板。业务人员在可视化设计器中拖拽即可完成表单与流程配置,需要个性化逻辑时再通过脚本扩展,兼顾效率与灵活度。",
},
{
type: "features",
items: [
{ icon: "file", title: "可视化设计器", desc: "表单、列表、详情页拖拽生成,支持多端自适应。" },
{ icon: "plug", title: "流程引擎", desc: "支持串行、并行、会签与条件分支,审批节点可视化配置。" },
{ icon: "code", title: "脚本扩展", desc: "需要复杂逻辑时可插入脚本,不牺牲灵活度。" },
{ icon: "shield", title: "权限体系", desc: "按组织、角色、数据范围三级控制,天然适配集团架构。" },
],
},
{
type: "list",
items: [
"内部审批流:请假、报销、采购、用章",
"业务台账:设备巡检、客户回访、安全检查",
"数据填报:周报、月报、专项统计",
"轻量门户:面向员工的一站式工作入口",
],
},
],
},
{
slug: "ai-copilot",
section: "products",
category: "app",
title: "智能运营助手",
subtitle: "基于大模型的业务助手",
summary:
"把企业知识与业务数据接入大模型,为客服、运营与销售提供随问随答的智能助手。",
tone: "teal",
tags: ["大模型", "知识库", "智能客服"],
blocks: [
{
type: "paragraph",
text: "智能运营助手在私有化环境中接入企业知识库与业务数据,回答时附带来源出处,避免模型幻觉带来的风险。企业可以按部门配置不同的知识范围与数据权限,做到「答得准、管得住」。",
},
{
type: "features",
items: [
{ icon: "book", title: "企业知识库", desc: "支持文档、工单、网页与数据库多类知识来源接入。" },
{ icon: "target", title: "答案可溯源", desc: "每条回答附带引用出处,便于核对与追责。" },
{ icon: "shield", title: "私有化部署", desc: "支持本地化部署,数据不出企业内网。" },
{ icon: "headset", title: "多渠道接入", desc: "可嵌入官网、企业微信、钉钉与工单系统。" },
],
},
{
type: "stats",
items: [
{ value: "72%", label: "常见问题自动解决率" },
{ value: "45%", label: "客服首次响应耗时下降" },
{ value: "3 周", label: "平均上线周期" },
],
},
],
},
];
+235
View File
@@ -0,0 +1,235 @@
import type { ContentItem } from "@/types/content";
export const solutions: ContentItem[] = [
{
slug: "smart-factory",
section: "solutions",
category: "manufacturing",
title: "智能制造数字化工厂",
subtitle: "从设备联网到生产协同",
summary:
"打通设备层、执行层与管理层,让生产进度、设备状态与质量数据在同一套体系里流转。",
tone: "navy",
tags: ["设备联网", "MES 集成", "质量追溯"],
featured: true,
blocks: [
{
type: "paragraph",
text: "多数制造企业的数字化难点不在于缺少系统,而在于设备、MES、ERP 各自为政,数据无法贯通。我们以边缘网关统一采集设备数据,在中台完成建模与指标定义,再向计划、质量与设备管理三个方向输出能力。",
},
{ type: "heading", text: "常见挑战" },
{
type: "list",
items: [
"设备品牌与协议繁杂,数据采集依赖人工抄表",
"生产进度靠电话与微信群同步,异常发现滞后",
"质量数据分散在纸质记录与 Excel 中,难以追溯",
"系统上线后缺乏运维能力,三个月后又回到原状",
],
},
{ type: "heading", text: "方案构成", sub: "四个模块按需组合,可分阶段实施" },
{
type: "features",
items: [
{ icon: "plug", title: "设备数据采集", desc: "边缘网关接入 200 余种工业协议,覆盖主流机床与产线。" },
{ icon: "monitor", title: "生产可视化", desc: "工单进度、节拍、在制品数量实时上墙,异常自动预警。" },
{ icon: "shield", title: "质量追溯", desc: "以批次为主线关联工艺参数与检验结果,问题可回溯到工序。" },
{ icon: "chart", title: "设备健康管理", desc: "基于振动与温度趋势做早期故障预警,降低非计划停机。" },
],
},
{ type: "heading", text: "实施路径" },
{
type: "steps",
items: [
{ title: "现状诊断", desc: "2 周内完成产线走访、系统盘点与数据可用性评估。" },
{ title: "试点产线", desc: "选择 1 条代表性产线,8 周内完成采集与可视化上线。" },
{ title: "指标建模", desc: "与业务共同定义 OEE、良率等核心指标的计算口径。" },
{ title: "全面推广", desc: "按车间分批推广,同步移交运维文档与培训。" },
],
},
{
type: "stats",
items: [
{ value: "96%", label: "设备数据采集覆盖率" },
{ value: "60%", label: "生产异常响应时长下降" },
{ value: "18%", label: "设备综合效率提升" },
],
},
],
},
{
slug: "retail-ops",
section: "solutions",
category: "retail",
title: "零售全域数据运营",
subtitle: "会员 · 商品 · 门店",
summary:
"把线上与线下会员数据统一到一套标签体系,让活动投放从「拍脑袋」变成「按人群」。",
tone: "teal",
tags: ["会员运营", "标签体系", "门店分析"],
blocks: [
{
type: "paragraph",
text: "零售企业的数据通常分散在 POS、小程序、电商平台与会员系统中。我们以手机号为主键完成 OneID 归一,建立统一标签体系,再把人群能力开放给营销与商品团队使用。",
},
{ type: "heading", text: "常见挑战" },
{
type: "list",
items: [
"同一会员在多个渠道有多个身份,无法识别为同一个人",
"活动效果无法归因,预算分配缺少依据",
"门店选品依赖店长经验,滞销与缺货同时存在",
"会员数据分散在服务商手里,企业无法自主运营",
],
},
{
type: "features",
items: [
{ icon: "users", title: "OneID 归一", desc: "跨渠道身份合并,形成唯一的会员视图。" },
{ icon: "target", title: "标签与人群", desc: "300+ 预设标签,支持自定义组合并一键推送到营销通道。" },
{ icon: "chart", title: "门店经营分析", desc: "客流、转化、连带率按门店与时段拆解,指导排班与选品。" },
{ icon: "spark", title: "效果归因", desc: "打通投放与成交数据,量化每场活动的增量收益。" },
],
},
{
type: "stats",
items: [
{ value: "+18%", label: "会员复购率提升" },
{ value: "+24%", label: "活动 ROI 提升" },
{ value: "-30%", label: "滞销库存占比下降" },
],
},
],
},
{
slug: "energy-maintenance",
section: "solutions",
category: "energy",
title: "能源设备预测性维护",
subtitle: "从定期检修到按需检修",
summary:
"基于设备运行数据建立健康度模型,在故障发生前给出检修建议,减少非计划停机。",
tone: "indigo",
tags: ["预测性维护", "设备健康度", "状态监测"],
blocks: [
{
type: "paragraph",
text: "风电场、变电站与大型机组的检修长期依赖固定周期,既存在过度检修,也存在突发故障停机。我们通过高频采集振动、温度与电流数据,结合机理模型与机器学习,输出设备健康度评分与剩余可用寿命预估。",
},
{
type: "features",
items: [
{ icon: "monitor", title: "状态实时监测", desc: "毫秒级采集关键测点,支持边缘侧异常初筛。" },
{ icon: "chart", title: "健康度建模", desc: "机理模型与数据模型结合,输出 0 ~ 100 健康度评分。" },
{ icon: "clock", title: "剩余寿命预估", desc: "预测关键部件剩余可用时间,辅助备件与检修排程。" },
{ icon: "shield", title: "检修闭环", desc: "预警自动生成检修工单,处理结果回流优化模型。" },
],
},
{ type: "heading", text: "典型收益" },
{
type: "stats",
items: [
{ value: "-32%", label: "非计划停机时长" },
{ value: "-21%", label: "年度检修成本" },
{ value: "92%", label: "故障预警准确率" },
],
},
],
},
{
slug: "finance-risk",
section: "solutions",
category: "finance",
title: "金融数据合规与风控",
subtitle: "报表时效与风险识别",
summary:
"在满足监管报送与数据合规要求的前提下,把报表时效从 T+1 提升到分钟级。",
tone: "blue",
tags: ["监管报送", "数据合规", "实时风控"],
blocks: [
{
type: "paragraph",
text: "金融机构同时面对监管报送、内部风控与业务分析三类数据需求,口径与时效要求各不相同。我们以统一数据底座承载三类场景,通过数据血缘与权限体系满足合规审计要求。",
},
{
type: "features",
items: [
{ icon: "shield", title: "数据血缘与审计", desc: "全链路血缘可视化,任一指标均可追溯到源系统字段。" },
{ icon: "clock", title: "实时指标计算", desc: "核心经营指标分钟级更新,支撑日内决策。" },
{ icon: "target", title: "风险规则引擎", desc: "规则与模型双引擎,支持名单、阈值与图谱三类识别。" },
{ icon: "file", title: "报送自动化", desc: "监管报表自动生成与校验,减少人工核对工作量。" },
],
},
{
type: "table",
head: ["场景", "改造前", "改造后"],
rows: [
["经营日报", "T+1 上午出具", "当日分钟级更新"],
["监管报送", "人工核对 5 人日", "自动生成 0.5 人日"],
["风险识别", "名单规则为主", "规则 + 模型 + 图谱"],
["合规审计", "人工整理台账", "血缘自动留痕"],
],
},
],
},
{
slug: "logistics",
section: "solutions",
category: "retail",
title: "智慧物流调度优化",
subtitle: "运力、路径与时效",
summary:
"结合订单结构、运力与路况做动态排线,在保证时效的前提下降低空驶率。",
tone: "teal",
tags: ["路径优化", "运力调度", "时效履约"],
blocks: [
{
type: "paragraph",
text: "物流成本中占比最高的是运输环节。我们以订单、车辆、路况与时效承诺为输入,通过启发式算法在分钟级给出排线与配载方案,并支持调度员人工微调后回训练模型。",
},
{
type: "features",
items: [
{ icon: "target", title: "动态排线", desc: "综合考虑时效、成本与装载率,分钟级输出方案。" },
{ icon: "monitor", title: "在途可视化", desc: "车辆位置与温控数据实时回传,异常自动预警。" },
{ icon: "chart", title: "成本分析", desc: "按线路、车型、客户维度拆解成本构成。" },
{ icon: "clipboard", title: "调度协同", desc: "调度员可在方案基础上调整,调整结果回流优化模型。" },
],
},
{
type: "stats",
items: [
{ value: "-14%", label: "单均运输成本" },
{ value: "+9%", label: "车辆装载率提升" },
{ value: "98.6%", label: "承诺时效达成率" },
],
},
],
},
{
slug: "gov-data",
section: "solutions",
category: "finance",
title: "政务数据一体化",
subtitle: "共享交换与一网通办",
summary:
"打通部门间的数据壁垒,支撑一网通办、一件事一次办等跨部门协同场景。",
tone: "navy",
tags: ["数据共享", "一网通办", "目录管理"],
blocks: [
{
type: "paragraph",
text: "政务数据的难点在于目录不清、责任不明。我们以数据目录为抓手,先理清「有什么、在哪里、谁负责」,再通过共享交换平台提供按需调用能力,让跨部门办事流程真正跑起来。",
},
{
type: "features",
items: [
{ icon: "file", title: "数据目录", desc: "统一编目与责任登记,解决数据找不到、认不清的问题。" },
{ icon: "plug", title: "共享交换", desc: "支持库表、文件、接口三种交换方式,全程留痕可审计。" },
{ icon: "shield", title: "安全管控", desc: "按事项授权,最小必要原则,敏感字段自动脱敏。" },
{ icon: "users", title: "协同办事", desc: "以一件事为主线串联多个部门,材料一次提交多次复用。" },
],
},
],
},
];
+26
View File
@@ -0,0 +1,26 @@
<script setup lang="ts">
import SiteHeader from "./SiteHeader.vue";
import SiteFooter from "./SiteFooter.vue";
</script>
<template>
<div class="site">
<SiteHeader />
<main class="site__main">
<RouterView />
</main>
<SiteFooter />
</div>
</template>
<style scoped lang="less">
.site {
display: flex;
flex-direction: column;
min-height: 100vh;
&__main {
flex: 1;
}
}
</style>
+397
View File
@@ -0,0 +1,397 @@
<script setup lang="ts">
import { footerColumns, policyLinks, siteInfo } from "@/config/site";
</script>
<template>
<footer class="site-footer">
<div class="container site-footer__top">
<!-- 品牌与联系方式 -->
<div class="footer-brand">
<RouterLink to="/" class="footer-brand__logo">
<span class="footer-brand__mark">{{ siteInfo.brandMark }}</span>
<span class="footer-brand__name">{{ siteInfo.fullName }}</span>
</RouterLink>
<ul class="footer-info">
<li>
<span>地址</span>{{ siteInfo.address }}(邮编
{{ siteInfo.postalCode }})
</li>
<li>
<span>备案</span>京ICP备 00000000 号-1 | 京公网安备
11010800000000 号
</li>
<li>
<span>联系</span>服务热线:{{ siteInfo.phone }} · 商务合作:{{
siteInfo.email
}}
</li>
</ul>
</div>
<!-- 链接分组 -->
<div class="footer-links">
<div
v-for="column in footerColumns"
:key="column.title"
class="footer-links__column"
>
<h3>{{ column.title }}</h3>
<ul>
<li v-for="link in column.links" :key="link.path">
<RouterLink :to="link.path">{{ link.label }}</RouterLink>
</li>
</ul>
</div>
</div>
</div>
<div class="site-footer__bottom">
<div class="container site-footer__bottom-inner">
<p class="copyright">{{ siteInfo.copyright }}</p>
<div class="footer-end">
<ul class="policy-links">
<li v-for="link in policyLinks" :key="link.path">
<RouterLink :to="link.path">{{ link.label }}</RouterLink>
</li>
</ul>
<ul class="social-links">
<li>
<a href="#" aria-label="微博" title="微博">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path
d="M10.5 20c-4 0-7.5-2-7.5-4.6 0-1.6 1-3.4 2.7-5C7.6 9 9.7 8 11.4 8c.8 0 1.4.2 1.7.6.4.5.2 1.2-.1 1.9 0 0 .9-.4 1.5-.4 1 0 1.4.6 1.4 1.4 0 .5-.2 1-.6 1.5 1.3.4 2.2 1.3 2.2 2.4C17.5 17.6 14.6 20 10.5 20Z"
fill="none"
stroke="currentColor"
stroke-width="1.6"
/>
</svg>
</a>
</li>
<li>
<a href="#" aria-label="微信" title="微信">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path
d="M9 4.5c-3.6 0-6.5 2.4-6.5 5.4 0 1.7.9 3.2 2.4 4.2l-.6 2 2.3-1.2c.7.2 1.5.3 2.4.3h.5"
fill="none"
stroke="currentColor"
stroke-width="1.6"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M21.5 15.2c0-2.6-2.5-4.7-5.6-4.7s-5.6 2.1-5.6 4.7 2.5 4.7 5.6 4.7c.7 0 1.4-.1 2-.3l2 1-.5-1.7c1.3-.9 2.1-2.2 2.1-3.7Z"
fill="none"
stroke="currentColor"
stroke-width="1.6"
stroke-linejoin="round"
/>
</svg>
</a>
</li>
<li>
<a href="#" aria-label="哔哩哔哩" title="哔哩哔哩">
<svg viewBox="0 0 24 24" aria-hidden="true">
<rect
x="3"
y="7"
width="18"
height="13"
rx="3.5"
fill="none"
stroke="currentColor"
stroke-width="1.6"
/>
<path
d="M7.5 4.5 9.5 7M16.5 4.5 14.5 7M8.5 12v3M15.5 12v3"
fill="none"
stroke="currentColor"
stroke-width="1.6"
stroke-linecap="round"
/>
</svg>
</a>
</li>
<li>
<a href="#" aria-label="抖音" title="抖音">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path
d="M14 3.5v10.2a3.6 3.6 0 1 1-3.6-3.6c.3 0 .6 0 .9.1"
fill="none"
stroke="currentColor"
stroke-width="1.6"
stroke-linecap="round"
/>
<path
d="M14 3.5c.5 2.3 2.1 3.8 4.4 4"
fill="none"
stroke="currentColor"
stroke-width="1.6"
stroke-linecap="round"
/>
</svg>
</a>
</li>
<li>
<a href="#" aria-label="邮箱" title="邮箱">
<svg viewBox="0 0 24 24" aria-hidden="true">
<rect
x="3"
y="5.5"
width="18"
height="13"
rx="2.5"
fill="none"
stroke="currentColor"
stroke-width="1.6"
/>
<path
d="m4.5 8 7.5 5 7.5-5"
fill="none"
stroke="currentColor"
stroke-width="1.6"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</a>
</li>
</ul>
</div>
</div>
</div>
</footer>
</template>
<style scoped lang="less">
.site-footer {
background: @navy-900;
color: rgba(255, 255, 255, 0.62);
&__top {
display: flex;
justify-content: space-between;
gap: 60px;
padding-top: 62px;
padding-bottom: 52px;
}
&__bottom {
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
&__bottom-inner {
display: flex;
align-items: center;
justify-content: space-between;
gap: 24px;
min-height: 74px;
padding-top: 12px;
padding-bottom: 12px;
}
}
/* ---------- 左侧品牌 ---------- */
.footer-brand {
max-width: 720px;
&__logo {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 26px;
}
&__mark {
display: flex;
align-items: center;
justify-content: center;
width: 38px;
height: 38px;
border-radius: 10px;
background: rgba(255, 255, 255, 0.12);
color: @white;
font-size: 14px;
font-weight: 700;
}
&__name {
font-size: 19px;
font-weight: 700;
color: @white;
letter-spacing: 0.02em;
}
}
.footer-info {
li {
position: relative;
padding-left: 52px;
font-size: 13.5px;
line-height: 2;
color: rgba(255, 255, 255, 0.55);
span {
position: absolute;
left: 0;
top: 0;
color: rgba(255, 255, 255, 0.8);
font-weight: 500;
}
}
}
/* ---------- 右侧链接 ---------- */
.footer-links {
display: flex;
gap: 88px;
flex-shrink: 0;
&__column {
h3 {
margin-bottom: 16px;
font-size: 14px;
font-weight: 700;
color: @white;
letter-spacing: 0.06em;
}
li + li {
margin-top: 9px;
}
a {
font-size: 13.5px;
color: rgba(255, 255, 255, 0.55);
transition: color 0.2s ease;
&:hover {
color: @white;
}
}
}
}
/* ---------- 底部 ---------- */
.copyright {
font-size: 12px;
letter-spacing: 0.04em;
color: rgba(255, 255, 255, 0.4);
}
.footer-end {
display: flex;
align-items: center;
gap: 34px;
}
.policy-links {
display: flex;
align-items: center;
li {
display: flex;
align-items: center;
& + li::before {
content: "";
width: 1px;
height: 11px;
margin: 0 14px;
background: rgba(255, 255, 255, 0.2);
}
}
a {
font-size: 13px;
color: rgba(255, 255, 255, 0.6);
&:hover {
color: @white;
}
}
}
.social-links {
display: flex;
align-items: center;
gap: 10px;
a {
display: flex;
align-items: center;
justify-content: center;
width: 34px;
height: 34px;
border-radius: 50%;
border: 1px solid rgba(255, 255, 255, 0.16);
color: rgba(255, 255, 255, 0.7);
transition: all 0.25s ease;
&:hover {
background: rgba(255, 255, 255, 0.14);
border-color: transparent;
color: @white;
}
}
svg {
width: 18px;
height: 18px;
}
}
/* ---------- 响应式 ---------- */
@media (max-width: 1200px) {
.footer-links {
gap: 56px;
}
}
@media (max-width: 992px) {
.site-footer__top {
flex-direction: column;
gap: 44px;
padding-top: 48px;
padding-bottom: 40px;
}
.footer-links {
gap: 48px;
}
}
@media (max-width: 768px) {
.footer-info li {
padding-left: 0;
line-height: 1.9;
margin-bottom: 8px;
span {
position: static;
margin-right: 8px;
}
}
.footer-links {
flex-direction: column;
gap: 32px;
}
.site-footer__bottom-inner {
flex-direction: column;
align-items: flex-start;
gap: 16px;
padding-top: 20px;
padding-bottom: 24px;
}
.footer-end {
flex-direction: column;
align-items: flex-start;
gap: 16px;
}
}
</style>
+373
View File
@@ -0,0 +1,373 @@
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref, watch } from "vue";
import { useRoute } from "vue-router";
import { navItems, siteInfo, utilityLinks } from "@/config/site";
const route = useRoute();
const stuck = ref(false);
const menuOpen = ref(false);
const onScroll = () => {
stuck.value = window.scrollY > 8;
};
onMounted(() => {
onScroll();
window.addEventListener("scroll", onScroll, { passive: true });
});
onBeforeUnmount(() => {
window.removeEventListener("scroll", onScroll);
});
// 路由变化时收起移动端菜单
watch(
() => route.fullPath,
() => {
menuOpen.value = false;
}
);
</script>
<template>
<header class="site-header" :class="{ 'is-stuck': stuck }">
<!-- 顶部工具条 -->
<div class="header-util">
<div class="container header-util__inner">
<p class="header-util__slogan">{{ siteInfo.slogan }}</p>
<ul class="util-links">
<li v-for="item in utilityLinks" :key="item.path">
<RouterLink :to="item.path">{{ item.label }}</RouterLink>
</li>
</ul>
</div>
</div>
<!-- 主导航 -->
<div class="header-main">
<div class="container header-main__inner">
<RouterLink to="/" class="brand" aria-label="返回首页">
<span class="brand__mark">{{ siteInfo.brandMark }}</span>
<span class="brand__text">
<strong>{{ siteInfo.brandName }}</strong>
<em>{{ siteInfo.brandSub }}</em>
</span>
</RouterLink>
<nav class="main-nav">
<RouterLink
v-for="item in navItems"
:key="item.path"
:to="item.path"
class="main-nav__link"
>
{{ item.label }}
</RouterLink>
</nav>
<button
class="menu-toggle"
type="button"
:class="{ 'is-open': menuOpen }"
:aria-expanded="menuOpen"
aria-label="展开导航菜单"
@click="menuOpen = !menuOpen"
>
<span></span>
<span></span>
<span></span>
</button>
</div>
</div>
<!-- 移动端下拉菜单 -->
<transition name="drawer">
<nav v-show="menuOpen" class="mobile-nav">
<div class="container">
<RouterLink
v-for="item in navItems"
:key="item.path"
:to="item.path"
class="mobile-nav__link"
>
{{ item.label }}
</RouterLink>
<div class="mobile-nav__util">
<RouterLink
v-for="item in utilityLinks"
:key="item.path"
:to="item.path"
>
{{ item.label }}
</RouterLink>
</div>
</div>
</nav>
</transition>
</header>
</template>
<style scoped lang="less">
.site-header {
position: sticky;
top: 0;
z-index: 100;
background: @white;
transition: box-shadow 0.3s ease;
&.is-stuck {
box-shadow: 0 8px 30px rgba(11, 20, 48, 0.09);
}
}
/* ---------- 顶部工具条 ---------- */
.header-util {
background: #f8fafd;
border-bottom: 1px solid @line;
&__inner {
display: flex;
align-items: center;
justify-content: space-between;
height: 44px;
}
&__slogan {
font-size: 13px;
color: @ink-500;
letter-spacing: 0.02em;
}
}
.util-links {
display: flex;
align-items: center;
li {
display: flex;
align-items: center;
& + li::before {
content: "";
width: 1px;
height: 11px;
margin: 0 16px;
background: @ink-300;
}
}
a {
font-size: 13px;
color: @ink-600;
transition: color 0.2s ease;
&:hover {
color: @blue-600;
}
}
}
/* ---------- 主导航 ---------- */
.header-main {
&__inner {
display: flex;
align-items: center;
justify-content: space-between;
height: 88px;
}
}
.brand {
display: flex;
align-items: center;
gap: 12px;
flex-shrink: 0;
&__mark {
display: flex;
align-items: center;
justify-content: center;
width: 42px;
height: 42px;
border-radius: 11px;
background: linear-gradient(140deg, @navy-700, @navy-900);
color: @white;
font-size: 16px;
font-weight: 700;
letter-spacing: 0.02em;
}
&__text {
display: flex;
flex-direction: column;
line-height: 1.15;
strong {
font-size: 20px;
font-weight: 700;
color: @ink-900;
letter-spacing: 0.01em;
}
em {
font-style: normal;
font-size: 10px;
font-weight: 500;
letter-spacing: 0.34em;
color: @ink-400;
text-transform: uppercase;
}
}
}
.main-nav {
display: flex;
align-items: center;
margin-left: auto;
&__link {
position: relative;
padding: 0 26px;
font-size: 17px;
font-weight: 500;
color: @ink-800;
line-height: 88px;
letter-spacing: 0.02em;
transition: color 0.22s ease;
&::after {
content: "";
position: absolute;
left: 26px;
right: 26px;
bottom: 24px;
height: 2px;
background: @blue-600;
transform: scaleX(0);
transform-origin: left center;
transition: transform 0.28s cubic-bezier(0.22, 0.61, 0.36, 1);
}
&:hover,
&.router-link-active {
color: @blue-600;
&::after {
transform: scaleX(1);
}
}
}
}
.menu-toggle {
display: none;
width: 30px;
height: 30px;
margin-left: 22px;
flex-direction: column;
justify-content: center;
gap: 6px;
span {
display: block;
height: 2px;
width: 100%;
background: @ink-800;
border-radius: 2px;
transition: transform 0.3s ease, opacity 0.2s ease;
}
&.is-open {
span:nth-child(1) {
transform: translateY(8px) rotate(45deg);
}
span:nth-child(2) {
opacity: 0;
}
span:nth-child(3) {
transform: translateY(-8px) rotate(-45deg);
}
}
}
/* ---------- 移动端菜单 ---------- */
.mobile-nav {
display: none;
border-top: 1px solid @line;
background: @white;
padding: 12px 0 20px;
&__link {
display: block;
padding: 15px 0;
font-size: 16px;
font-weight: 500;
color: @ink-800;
border-bottom: 1px solid @line;
}
&__util {
display: flex;
flex-wrap: wrap;
gap: 8px 20px;
padding-top: 16px;
a {
font-size: 13px;
color: @ink-500;
}
}
}
.drawer-enter-active,
.drawer-leave-active {
transition: opacity 0.25s ease, transform 0.25s ease;
}
.drawer-enter-from,
.drawer-leave-to {
opacity: 0;
transform: translateY(-10px);
}
/* ---------- 响应式 ---------- */
@media (max-width: 1200px) {
.main-nav__link {
padding: 0 16px;
font-size: 16px;
&::after {
left: 16px;
right: 16px;
}
}
}
@media (max-width: 992px) {
.header-util__slogan {
display: none;
}
.header-util__inner {
justify-content: flex-end;
height: 38px;
}
.header-main__inner {
height: 70px;
}
.main-nav {
display: none;
}
.menu-toggle {
display: flex;
}
.mobile-nav {
display: block;
}
}
</style>
+9
View File
@@ -0,0 +1,9 @@
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router"; // 配置路由
import store from "./store"; // 配置vuex
import "./styles/index.less";
const app = createApp(App);
app.use(router).use(store).mount("#app");
+201
View File
@@ -0,0 +1,201 @@
//index.ts
import { createRouter, createWebHashHistory, type RouteRecordRaw } from "vue-router";
const DefaultLayout = () => import("../layout/DefaultLayout.vue");
const HomeView = () => import("../views/home/index.vue");
const ListView = () => import("../views/list.vue");
const DetailView = () => import("../views/detail.vue");
const SingleView = () => import("../views/single.vue");
const NotFoundView = () => import("../views/not-found.vue");
/**
* 路由约定:
* - 列表页 /news、/products、/solutions、/cases、/events
* - 分类页 /news/category/:slug (其余栏目同理)
* - 详情页 /news/detail/:slug (其余栏目同理)
* - 单页 /about、/careers、/contact、/support、/support/:slug、/policy/:slug
* 列表页与详情页共用同一个组件,通过路由路径推导所属栏目。
*/
const routes: RouteRecordRaw[] = [
{
path: "/",
component: DefaultLayout,
children: [
{
path: "",
name: "Home",
component: HomeView,
meta: { title: "官方网站" },
},
// ---------------- 新闻中心 ----------------
{
path: "news",
name: "News",
component: ListView,
meta: { title: "新闻中心" },
},
{
path: "news/category/:slug",
name: "NewsCategory",
component: ListView,
meta: { title: "新闻中心" },
},
{
path: "news/detail/:slug",
name: "NewsDetail",
component: DetailView,
meta: { title: "新闻详情" },
},
// ---------------- 产品中心 ----------------
{
path: "products",
name: "Products",
component: ListView,
meta: { title: "产品中心" },
},
{
path: "products/category/:slug",
name: "ProductCategory",
component: ListView,
meta: { title: "产品中心" },
},
{
path: "products/detail/:slug",
name: "ProductDetail",
component: DetailView,
meta: { title: "产品详情" },
},
// ---------------- 解决方案 ----------------
{
path: "solutions",
name: "Solutions",
component: ListView,
meta: { title: "解决方案" },
},
{
path: "solutions/category/:slug",
name: "SolutionCategory",
component: ListView,
meta: { title: "解决方案" },
},
{
path: "solutions/detail/:slug",
name: "SolutionDetail",
component: DetailView,
meta: { title: "方案详情" },
},
// ---------------- 客户案例 ----------------
{
path: "cases",
name: "Cases",
component: ListView,
meta: { title: "客户案例" },
},
{
path: "cases/category/:slug",
name: "CaseCategory",
component: ListView,
meta: { title: "客户案例" },
},
{
path: "cases/detail/:slug",
name: "CaseDetail",
component: DetailView,
meta: { title: "案例详情" },
},
// ---------------- 市场活动 ----------------
{
path: "events",
name: "Events",
component: ListView,
meta: { title: "市场活动" },
},
{
path: "events/category/:slug",
name: "EventCategory",
component: ListView,
meta: { title: "市场活动" },
},
{
path: "events/detail/:slug",
name: "EventDetail",
component: DetailView,
meta: { title: "活动详情" },
},
// ---------------- 独立单页 ----------------
{
path: "about",
name: "About",
component: SingleView,
meta: { title: "关于云泽" },
},
{
path: "careers",
name: "Careers",
component: SingleView,
meta: { title: "加入我们" },
},
{
path: "partners",
name: "Partners",
component: SingleView,
meta: { title: "合作伙伴" },
},
{
path: "contact",
name: "Contact",
component: SingleView,
meta: { title: "联系我们" },
},
{
path: "support",
name: "Support",
component: SingleView,
meta: { title: "客户支持中心" },
},
{
path: "support/:slug",
name: "SupportDetail",
component: SingleView,
meta: { title: "客户支持" },
},
{
path: "policy/:slug",
name: "Policy",
component: SingleView,
meta: { title: "政策条款" },
},
// ---------------- 兜底 ----------------
{
path: ":pathMatch(.*)*",
name: "NotFound",
component: NotFoundView,
meta: { title: "页面不存在" },
},
],
},
];
const router = createRouter({
history: createWebHashHistory(), // 哈希模式
routes,
scrollBehavior(_to, _from, savedPosition) {
if (savedPosition) return savedPosition;
return { top: 0 };
},
});
/** 根据路由 meta 同步浏览器标题 */
router.afterEach((to) => {
const title = (to.meta.title as string | undefined) ?? "";
document.title = title ? `${title} | 云泽科技` : "云泽科技 | 官方网站";
});
export default router;
+3
View File
@@ -0,0 +1,3 @@
//actions.ts
const actions= {};
export default actions;
+12
View File
@@ -0,0 +1,12 @@
//index.ts
import { createStore } from "vuex";
import state from "./state";
import mutations from "./mutations";
import actions from "./actions";
const store = createStore({
state,
mutations,
actions,
modules: {},
});
export default store;
+3
View File
@@ -0,0 +1,3 @@
//mutations.ts
const mutations= {};
export default mutations;
+3
View File
@@ -0,0 +1,3 @@
//state.ts
const state= {};
export default state;
+148
View File
@@ -0,0 +1,148 @@
*,
*::before,
*::after {
box-sizing: border-box;
}
html,
body {
margin: 0;
padding: 0;
}
html {
scroll-behavior: smooth;
}
body {
font-family: "Noto Sans SC", -apple-system, BlinkMacSystemFont, "Segoe UI",
"PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
font-size: 16px;
line-height: 1.6;
color: @ink-700;
background: @white;
overflow-x: hidden;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
h1,
h2,
h3,
h4,
h5,
p,
figure {
margin: 0;
font-weight: inherit;
}
ul,
ol {
margin: 0;
padding: 0;
list-style: none;
}
a {
color: inherit;
text-decoration: none;
}
img {
display: block;
max-width: 100%;
}
button {
padding: 0;
border: 0;
background: none;
font: inherit;
color: inherit;
cursor: pointer;
}
input,
textarea {
font: inherit;
}
.container {
width: 100%;
max-width: @page-width;
margin: 0 auto;
padding: 0 @page-gutter;
}
// 通用:横向滚动时隐藏滚动条
.no-scrollbar {
scrollbar-width: none;
-ms-overflow-style: none;
&::-webkit-scrollbar {
display: none;
}
}
// 通用圆形箭头按钮
.circle-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 44px;
height: 44px;
border-radius: 50%;
border: 1px solid @ink-300;
color: @ink-700;
background: @white;
transition: all 0.25s ease;
svg {
width: 16px;
height: 16px;
}
&:hover:not(:disabled) {
background: @navy-800;
border-color: @navy-800;
color: @white;
}
&:disabled {
opacity: 0.35;
cursor: not-allowed;
}
}
// 通用:封面配色(暂用渐变替代实拍图,后续可换成 img)
.tone-navy {
background: linear-gradient(140deg, #1b2f6e 0%, #0b1430 100%);
}
.tone-blue {
background: linear-gradient(140deg, #3f7bff 0%, #2450d8 100%);
}
.tone-teal {
background: linear-gradient(140deg, #22c2a0 0%, #17a88f 100%);
}
.tone-indigo {
background: linear-gradient(140deg, #5b5ee8 0%, #3a3dbf 100%);
}
// 通用:淡入上移动画
.fade-up {
animation: fadeUp 0.7s cubic-bezier(0.22, 0.61, 0.36, 1) both;
}
@keyframes fadeUp {
from {
opacity: 0;
transform: translateY(28px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
+2
View File
@@ -0,0 +1,2 @@
@import "./variables.less";
@import "./base.less";
+27
View File
@@ -0,0 +1,27 @@
// ============ 品牌色 ============
@navy-900: #0b1430; // 页脚 / 最深
@navy-800: #142352; // 主色:深蓝区块
@navy-700: #1b2f6e;
@navy-600: #24418f;
@blue-600: #2f6bf3; // 强调蓝
@blue-500: #4b83ff;
@teal-600: #17a88f; // 右侧卡片
@teal-500: #22c2a0;
@green-400: #2ec89b; // 小竖条
@indigo-500: #4a4ddb;
// ============ 中性色 ============
@ink-900: #14161c;
@ink-800: #232733;
@ink-700: #3c4252;
@ink-600: #5b6376;
@ink-500: #838c9f;
@ink-400: #a9b1c1;
@ink-300: #ccd2dd;
@line: #e8ebf2;
@bg-soft: #f4f6fb;
@white: #ffffff;
// ============ 布局 ============
@page-width: 1680px; // 内容最大宽度
@page-gutter: 40px; // 左右安全边距
+110
View File
@@ -0,0 +1,110 @@
/**
* 内容模型:列表页 / 详情页 / 单页共用
* 与后端 CMS(文章、产品、解决方案)字段保持一一对应的思路,
* 后续接入接口时只需替换 data 层的数据来源。
*/
/** 栏目标识 */
export type SectionKey = "news" | "products" | "solutions" | "cases" | "events";
/** 分类 */
export interface Category {
slug: string;
name: string;
desc?: string;
}
/** 封面配色(暂用渐变替代实拍图) */
export type CoverTone = "navy" | "blue" | "teal" | "indigo";
export interface Feature {
icon: string;
title: string;
desc: string;
}
export interface Stat {
value: string;
label: string;
}
export interface Step {
title: string;
desc: string;
}
export interface TimelineNode {
year: string;
text: string;
}
export interface ContactRow {
icon: string;
label: string;
value: string;
}
export interface QuickLink {
icon: string;
title: string;
desc: string;
path: string;
}
/** 正文区块:详情页与单页共用同一套渲染器 */
export type Block =
| { type: "heading"; text: string; sub?: string }
| { type: "paragraph"; text: string }
| { type: "quote"; text: string; author?: string }
| { type: "list"; items: string[] }
| { type: "features"; items: Feature[] }
| { type: "stats"; items: Stat[] }
| { type: "steps"; items: Step[] }
| { type: "timeline"; items: TimelineNode[] }
| { type: "contact"; items: ContactRow[] }
| { type: "links"; items: QuickLink[] }
| { type: "table"; head: string[]; rows: string[][] };
/** 一条内容(新闻 / 产品 / 方案 / 案例 / 活动) */
export interface ContentItem {
slug: string;
section: SectionKey;
/** 分类 slug */
category: string;
title: string;
subtitle: string;
summary: string;
tone: CoverTone;
date?: string;
author?: string;
tags?: string[];
featured?: boolean;
blocks: Block[];
}
/** 独立单页 */
export interface SinglePage {
title: string;
subtitle: string;
/** 面包屑(不含"首页") */
crumbs: string[];
tone: CoverTone;
blocks: Block[];
}
/** 列表查询参数 */
export interface ListQuery {
section: SectionKey;
category?: string;
keyword?: string;
page?: number;
pageSize?: number;
}
export interface ListResult {
list: ContentItem[];
total: number;
page: number;
pageSize: number;
totalPages: number;
}
+8
View File
@@ -0,0 +1,8 @@
// 补充第三方库缺失的类型声明
declare module "vuex";
declare module "*.vue" {
import type { DefineComponent } from "vue";
const component: DefineComponent<{}, {}, any>;
export default component;
}
+540
View File
@@ -0,0 +1,540 @@
<script setup lang="ts">
import { computed, ref, watch } from "vue";
import { useRoute } from "vue-router";
import type { ContentItem } from "@/types/content";
import { categoryName, sections } from "@/config/sections";
import { fetchDetail, fetchRelated } from "@/api/content";
import PageHero from "@/components/PageHero.vue";
import BlockRenderer from "@/components/BlockRenderer.vue";
import ContentCard from "@/components/ContentCard.vue";
import AppIcon from "@/components/AppIcon.vue";
/**
* 通用详情页
* 服务 /news/detail/:slug、/products/detail/:slug、/cases/detail/:slug 等,
* 正文统一交给 BlockRenderer 渲染。
*/
const route = useRoute();
const section = computed(
() =>
sections[
(route.path.split("/").filter(Boolean)[0] ?? "news") as keyof typeof sections
] ?? sections.news
);
const slug = computed(() => String(route.params.slug ?? ""));
const item = ref<ContentItem | null>(null);
const related = ref<ContentItem[]>([]);
const loading = ref(true);
const crumbs = computed(() =>
item.value
? [section.value.title, categoryName(section.value.key, item.value.category)]
: [section.value.title]
);
/**
* 列表页路径,用于「返回列表」与面包屑跳转。
* 带分类时回到该分类的列表页。
*/
const backPath = computed(() =>
item.value
? `${section.value.categoryPath}/${item.value.category}`
: section.value.basePath
);
async function load() {
loading.value = true;
const current = section.value;
const [detail, recommend] = await Promise.all([
fetchDetail(current.key, slug.value),
fetchRelated(current.key, slug.value, 3),
]);
item.value = detail ?? null;
related.value = recommend;
loading.value = false;
// 详情页标题跟随内容变化
document.title = detail
? `${detail.title} | 云泽科技`
: `${current.title} | 云泽科技`;
}
watch(() => `${section.value.key}|${slug.value}`, load, { immediate: true });
</script>
<template>
<div class="detail-page">
<!-- 详情头图 -->
<PageHero
v-if="item"
:title="item.title"
:subtitle="item.summary"
:crumbs="crumbs"
/>
<PageHero v-else :title="section.title" :crumbs="[section.title]" />
<div class="container detail-page__body">
<!-- 加载中 -->
<div v-if="loading" class="detail-skeleton">
<span></span>
<span></span>
<span></span>
</div>
<!-- 内容不存在 -->
<div v-else-if="!item" class="detail-missing">
<span class="detail-missing__icon">
<AppIcon name="file" :size="26" />
</span>
<h2>内容不存在或已下线</h2>
<p>该{{ section.title }}内容可能已被删除,或者链接地址有误。</p>
<RouterLink :to="section.basePath" class="detail-missing__btn">
返回{{ section.title }}
</RouterLink>
</div>
<template v-else>
<div class="detail-page__grid">
<!-- 正文 -->
<article class="detail-article">
<RouterLink :to="backPath" class="detail-article__back">
<AppIcon name="arrow-left" :size="15" />
返回列表
</RouterLink>
<BlockRenderer :blocks="item.blocks" />
<footer class="detail-article__foot">
<span v-if="item.author">供稿:{{ item.author }}</span>
<RouterLink :to="backPath" class="detail-article__more">
查看更多{{ section.title }}内容
<AppIcon name="arrow-right" :size="15" />
</RouterLink>
</footer>
</article>
<!-- 侧栏 -->
<aside class="detail-aside">
<div class="aside-card">
<h3 class="aside-card__title">内容信息</h3>
<ul class="aside-meta">
<li>
<span>栏目</span>
<RouterLink :to="section.basePath">
{{ section.title }}
</RouterLink>
</li>
<li>
<span>分类</span>
<RouterLink :to="backPath">
{{ categoryName(section.key, item.category) }}
</RouterLink>
</li>
<li v-if="item.date">
<span>日期</span>
<em>{{ item.date }}</em>
</li>
<li v-if="item.author">
<span>供稿</span>
<em>{{ item.author }}</em>
</li>
</ul>
<ul v-if="item.tags?.length" class="aside-tags">
<li v-for="tag in item.tags" :key="tag">{{ tag }}</li>
</ul>
</div>
<div class="aside-card aside-card--cta">
<h3>想进一步了解?</h3>
<p>留下需求,我们会在 1 个工作日内与你联系。</p>
<ul class="aside-cta">
<li>
<RouterLink to="/support/demo">
<AppIcon name="monitor" :size="18" />
预约产品演示
<AppIcon name="arrow-right" :size="15" />
</RouterLink>
</li>
<li>
<RouterLink to="/support/trial">
<AppIcon name="clipboard" :size="18" />
申请免费试用
<AppIcon name="arrow-right" :size="15" />
</RouterLink>
</li>
<li>
<RouterLink to="/contact">
<AppIcon name="phone" :size="18" />
联系销售顾问
<AppIcon name="arrow-right" :size="15" />
</RouterLink>
</li>
</ul>
</div>
</aside>
</div>
<!-- 相关推荐 -->
<section v-if="related.length" class="detail-related">
<header class="detail-related__head">
<h2>相关推荐</h2>
<RouterLink :to="section.basePath">
查看全部
<AppIcon name="arrow-right" :size="15" />
</RouterLink>
</header>
<ul class="detail-related__grid">
<li v-for="row in related" :key="row.slug">
<ContentCard :item="row" layout="grid" />
</li>
</ul>
</section>
</template>
</div>
</div>
</template>
<style scoped lang="less">
.detail-page {
&__body {
padding-top: 64px;
padding-bottom: 110px;
}
&__grid {
display: grid;
grid-template-columns: minmax(0, 1fr) 340px;
gap: 64px;
align-items: start;
}
}
/* ---------------- 正文 ---------------- */
.detail-article {
min-width: 0;
&__back {
display: inline-flex;
align-items: center;
gap: 7px;
margin-bottom: 34px;
font-size: 13.5px;
color: @ink-500;
transition: color 0.25s ease;
&:hover {
color: @blue-600;
}
}
&__foot {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20px;
flex-wrap: wrap;
margin-top: 52px;
padding-top: 28px;
border-top: 1px solid @line;
font-size: 13px;
color: @ink-400;
}
&__more {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13.5px;
font-weight: 500;
color: @navy-800;
transition: color 0.25s ease;
&:hover {
color: @blue-600;
}
}
}
/* ---------------- 侧栏 ---------------- */
.detail-aside {
position: sticky;
top: 120px;
display: flex;
flex-direction: column;
gap: 20px;
}
.aside-card {
padding: 28px 26px;
border: 1px solid @line;
border-radius: 12px;
&__title {
padding-bottom: 16px;
border-bottom: 1px solid @line;
font-size: 15px;
font-weight: 700;
color: @ink-900;
}
&--cta {
background: linear-gradient(155deg, @navy-800 0%, @navy-900 100%);
border-color: transparent;
color: @white;
h3 {
font-size: 17px;
font-weight: 700;
}
p {
margin-top: 10px;
font-size: 13px;
line-height: 1.8;
color: rgba(255, 255, 255, 0.6);
}
}
}
.aside-meta {
padding: 18px 0 4px;
li {
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
padding: 9px 0;
font-size: 13.5px;
span {
color: @ink-400;
}
a {
color: @ink-800;
font-weight: 500;
&:hover {
color: @blue-600;
}
}
em {
font-style: normal;
color: @ink-800;
font-weight: 500;
}
}
}
.aside-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
padding-top: 16px;
border-top: 1px solid @line;
li {
padding: 4px 12px;
border-radius: 100px;
background: @bg-soft;
font-size: 12px;
color: @ink-500;
}
}
.aside-cta {
display: flex;
flex-direction: column;
gap: 10px;
margin-top: 24px;
a {
display: flex;
align-items: center;
gap: 12px;
padding: 13px 18px;
border-radius: 10px;
background: rgba(255, 255, 255, 0.1);
font-size: 13.5px;
color: @white;
transition: background 0.26s ease;
svg:last-child {
margin-left: auto;
opacity: 0.6;
}
&:hover {
background: rgba(255, 255, 255, 0.2);
}
}
}
/* ---------------- 相关推荐 ---------------- */
.detail-related {
margin-top: 96px;
&__head {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 20px;
margin-bottom: 30px;
h2 {
font-size: 24px;
font-weight: 700;
color: @ink-900;
}
a {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 13.5px;
color: @ink-500;
&:hover {
color: @blue-600;
}
}
}
&__grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 26px;
}
}
/* ---------------- 缺失状态 ---------------- */
.detail-missing {
display: flex;
flex-direction: column;
align-items: center;
padding: 90px 0;
text-align: center;
&__icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 62px;
height: 62px;
margin-bottom: 22px;
border-radius: 50%;
background: @bg-soft;
color: @ink-400;
}
h2 {
font-size: 21px;
font-weight: 700;
color: @ink-900;
}
p {
margin-top: 10px;
font-size: 14px;
color: @ink-500;
}
&__btn {
margin-top: 26px;
padding: 12px 28px;
border-radius: 100px;
background: @navy-800;
font-size: 14px;
color: @white;
&:hover {
background: @blue-600;
}
}
}
/* ---------------- 骨架屏 ---------------- */
.detail-skeleton {
display: flex;
flex-direction: column;
gap: 18px;
span {
height: 22px;
border-radius: 6px;
background: linear-gradient(100deg, #f2f5fa 0%, #f8fafd 42%, #f2f5fa 84%);
background-size: 200% 100%;
animation: detailSkeleton 1.4s ease-in-out infinite;
&:nth-child(1) {
height: 34px;
width: 46%;
}
&:nth-child(2) {
width: 100%;
}
&:nth-child(3) {
width: 78%;
}
}
}
@keyframes detailSkeleton {
0% {
background-position: 120% 0;
}
100% {
background-position: -20% 0;
}
}
/* ---------------- 响应式 ---------------- */
@media (max-width: 1200px) {
.detail-page__grid {
grid-template-columns: minmax(0, 1fr) 300px;
gap: 44px;
}
}
@media (max-width: 992px) {
.detail-page {
&__body {
padding-top: 48px;
padding-bottom: 80px;
}
&__grid {
grid-template-columns: minmax(0, 1fr);
gap: 48px;
}
}
.detail-aside {
position: static;
}
.detail-related {
margin-top: 64px;
&__grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
}
@media (max-width: 640px) {
.detail-related__grid {
grid-template-columns: minmax(0, 1fr);
}
}
</style>
@@ -0,0 +1,302 @@
<script setup lang="ts">
import heroImage from "@/assets/images/hero-cover.svg";
const crumbs = ["首页", "关于云泽", "企业简介"];
</script>
<template>
<section class="hero">
<div class="hero__media">
<img :src="heroImage" alt="云泽科技团队协作场景" />
</div>
<div class="hero__veil"></div>
<div class="container hero__inner">
<h1 class="hero__title">云泽科技</h1>
<p class="hero__desc">
以云计算与数据智能为核心,为企业提供从咨询规划、产品研发到落地运维的<br />
一站式数字化解决方案,让技术真正服务于业务增长。
</p>
<div class="hero__rule"></div>
<div class="hero__meta">
<div class="hero__stat">
<strong>12 年</strong>
<span class="hero__stat-label">行业深耕</span>
<em>服务 1000+ 企业客户</em>
</div>
<div class="hero__divider"></div>
<div class="hero__stat hero__stat--contact">
<span class="hero__stat-icon" aria-hidden="true">
<svg viewBox="0 0 24 24">
<path
d="M6.6 3.5h2.6l1.5 3.7-1.9 1.3a12.4 12.4 0 0 0 5.7 5.7l1.3-1.9 3.7 1.5v2.6a2 2 0 0 1-2.2 2C10.7 17.7 6.3 13.3 4.6 5.7a2 2 0 0 1 2-2.2Z"
fill="none"
stroke="currentColor"
stroke-width="1.6"
stroke-linejoin="round"
/>
</svg>
</span>
<strong>400-000-0000</strong>
<span class="hero__stat-label">售前咨询专线</span>
<em>工作日 9:00 - 18:00</em>
</div>
</div>
</div>
</section>
</template>
<style scoped lang="less">
.hero {
position: relative;
overflow: hidden;
background: @navy-900;
color: @white;
&__media {
position: absolute;
inset: 0;
img {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center 30%;
}
}
&__veil {
position: absolute;
inset: 0;
background: linear-gradient(
100deg,
rgba(6, 12, 32, 0.93) 0%,
rgba(8, 16, 40, 0.82) 32%,
rgba(10, 18, 44, 0.5) 62%,
rgba(10, 18, 44, 0.32) 100%
),
linear-gradient(180deg, rgba(6, 12, 32, 0.5) 0%, rgba(6, 12, 32, 0) 40%);
}
&__inner {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
justify-content: center;
min-height: 620px;
padding-top: 120px;
padding-bottom: 110px;
}
/* ---------- 面包屑 ---------- */
&__crumbs {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 26px;
span {
font-size: 12.5px;
letter-spacing: 0.06em;
color: rgba(255, 255, 255, 0.5);
&.is-current {
color: rgba(255, 255, 255, 0.85);
}
}
i {
width: 5px;
height: 5px;
border-top: 1px solid rgba(255, 255, 255, 0.45);
border-right: 1px solid rgba(255, 255, 255, 0.45);
transform: rotate(45deg);
}
}
/* ---------- 主标题 ---------- */
&__title {
font-size: 74px;
font-weight: 900;
line-height: 1.1;
letter-spacing: 0.02em;
color: @white;
text-shadow: 0 8px 40px rgba(0, 0, 0, 0.35);
}
&__desc {
margin-top: 26px;
font-size: 17px;
line-height: 1.95;
font-weight: 300;
color: rgba(255, 255, 255, 0.74);
}
&__rule {
width: 560px;
max-width: 100%;
height: 1px;
margin: 46px 0 30px;
background: rgba(255, 255, 255, 0.38);
}
/* ---------- 数据条 ---------- */
&__meta {
display: flex;
align-items: stretch;
gap: 0;
}
&__stat {
display: flex;
flex-direction: column;
min-width: 260px;
padding: 4px 0;
strong {
font-size: 34px;
font-weight: 700;
line-height: 1.2;
color: @white;
letter-spacing: 0.01em;
}
&-label {
margin-top: 4px;
font-size: 14px;
font-weight: 500;
color: rgba(255, 255, 255, 0.88);
}
em {
margin-top: 8px;
font-style: normal;
font-size: 12.5px;
color: rgba(255, 255, 255, 0.45);
letter-spacing: 0.02em;
}
&--contact {
flex-direction: row;
align-items: center;
flex-wrap: wrap;
gap: 0 16px;
strong {
font-size: 28px;
letter-spacing: 0.02em;
}
.hero__stat-label {
margin-top: 0;
}
em {
flex-basis: 100%;
margin-top: 6px;
padding-left: 0;
}
}
&-icon {
display: flex;
align-items: center;
justify-content: center;
width: 52px;
height: 52px;
border-radius: 50%;
border: 1px solid rgba(255, 255, 255, 0.3);
color: @white;
flex-shrink: 0;
svg {
width: 24px;
height: 24px;
}
}
}
&__divider {
width: 1px;
margin: 0 56px;
background: rgba(255, 255, 255, 0.22);
}
}
/* ---------- 响应式 ---------- */
@media (max-width: 1200px) {
.hero {
&__title {
font-size: 58px;
}
&__inner {
min-height: 520px;
padding-top: 90px;
padding-bottom: 80px;
}
&__divider {
margin: 0 36px;
}
&__stat {
min-width: 200px;
}
}
}
@media (max-width: 768px) {
.hero {
&__inner {
min-height: auto;
padding-top: 64px;
padding-bottom: 60px;
}
&__title {
font-size: 38px;
}
&__desc {
margin-top: 18px;
font-size: 15px;
line-height: 1.85;
}
&__rule {
margin: 32px 0 24px;
}
&__meta {
flex-direction: column;
gap: 24px;
}
&__divider {
width: 100%;
height: 1px;
margin: 0;
}
&__stat {
min-width: 0;
strong {
font-size: 26px;
}
&--contact strong {
font-size: 22px;
}
}
}
}
</style>
@@ -0,0 +1,519 @@
<script setup lang="ts">
import { ref } from "vue";
import AppIcon from "@/components/AppIcon.vue";
import newsImage from "@/assets/images/news-cover.svg";
interface MediaCard {
type: "media";
source: string;
icon: string;
image: string;
caption: string;
}
interface ArticleCard {
type: "article";
text: string;
author: string;
}
interface QuoteCard {
type: "quote";
text: string;
author: string;
}
type NewsCard = MediaCard | ArticleCard | QuoteCard;
const slides: NewsCard[][] = [
[
{
type: "media",
source: "WECHAT",
icon: "mail",
image: newsImage,
caption: "2026 云泽数智大会现场",
},
{
type: "article",
text: "从立项到上线只用了三个月,云泽的团队几乎是把我们的业务当成自己的业务在做。",
author: "王磊 · 某制造集团 CIO",
},
{
type: "quote",
text: "数据真正跑通之后,产线排产效率提升了将近 20%,这是项目立项时我们不敢想的结果。",
author: "陈静 · 运营负责人",
},
],
[
{
type: "media",
source: "BILIBILI",
icon: "monitor",
image: newsImage,
caption: "数据中台实战公开课",
},
{
type: "article",
text: "我们最看重的是产品之外的那套方法论,它让数字化不再只是买一套系统,而是一次组织能力的升级。",
author: "刘鹏 · 某零售集团 数字化总监",
},
{
type: "quote",
text: "客户的问题永远比文档里的场景更复杂,所以我们坚持每个项目都到现场待上一周。",
author: "孙浩 · 解决方案架构师",
},
],
[
{
type: "media",
source: "FACEBOOK",
icon: "facebook",
image: newsImage,
caption: "合作伙伴城市巡展记录",
},
{
type: "article",
text: "合作三年,云泽是我们唯一一个没有换过的基础软件供应商。",
author: "赵敏 · 某能源集团 信息中心主任",
},
{
type: "quote",
text: "从入职到现在我参与了六个行业的项目,这里的成长速度比我想象中快得多。",
author: "张宇 · 研发工程师",
},
],
];
const slide = ref(0);
const step = (delta: number) => {
slide.value = (slide.value + delta + slides.length) % slides.length;
};
</script>
<template>
<section class="news">
<div class="container news__inner">
<div class="news__viewport">
<div
class="news__track"
:style="{ transform: `translateX(-${slide * 100}%)` }"
>
<div v-for="(group, index) in slides" :key="index" class="news__slide">
<template v-for="(card, i) in group" :key="`${index}-${i}`">
<!-- 社交动态卡片 -->
<article v-if="card.type === 'media'" class="news-card news-card--media">
<img :src="card.image" :alt="card.caption" />
<span class="news-card__veil"></span>
<span class="news-card__source">
<AppIcon :name="card.icon" :size="14" />
{{ card.source }}
</span>
<p class="news-card__caption">{{ card.caption }}</p>
</article>
<!-- 图文引用卡片 -->
<article
v-else-if="card.type === 'article'"
class="news-card news-card--article"
>
<span class="news-card__mark">&ldquo;</span>
<p class="news-card__text">{{ card.text }}</p>
<div class="news-card__foot">
<span class="news-card__author">{{ card.author }}</span>
<RouterLink to="/news" class="news-card__pill">
查看详情
<AppIcon name="arrow-right" :size="15" />
</RouterLink>
</div>
</article>
<!-- 引言卡片 -->
<article v-else class="news-card news-card--quote">
<span class="news-card__mark">&ldquo;</span>
<p class="news-card__text">{{ card.text }}</p>
<div class="news-card__foot">
<span class="news-card__author">{{ card.author }}</span>
<RouterLink
to="/news"
class="news-card__round"
aria-label="查看详情"
>
<AppIcon name="arrow-up-right" :size="18" />
</RouterLink>
</div>
</article>
</template>
</div>
</div>
</div>
</div>
<!-- 轮播控制行:任何宽度下都不会被裁切 -->
<div class="container news__controls">
<button
class="news__nav"
type="button"
aria-label="上一组动态"
@click="step(-1)"
>
<AppIcon name="chevron-left" :size="18" />
</button>
<div class="news__dots">
<button
v-for="(_, index) in slides"
:key="index"
type="button"
class="news__dot"
:class="{ 'is-active': index === slide }"
:aria-label="`第 ${index + 1} 组`"
@click="slide = index"
></button>
</div>
<button
class="news__nav"
type="button"
aria-label="下一组动态"
@click="step(1)"
>
<AppIcon name="chevron-right" :size="18" />
</button>
</div>
</section>
</template>
<style scoped lang="less">
.news {
padding: 100px 0 96px;
background: @white;
&__inner {
position: relative;
}
&__viewport {
overflow: hidden;
border-radius: 12px;
}
&__track {
display: flex;
transition: transform 0.55s cubic-bezier(0.22, 0.61, 0.36, 1);
}
&__slide {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 24px;
flex: 0 0 100%;
min-width: 0;
}
/* 控制行:上一组 / 指示点 / 下一组,居中排列,不依赖容器外空间 */
&__controls {
display: flex;
align-items: center;
justify-content: center;
gap: 18px;
margin-top: 34px;
}
&__nav {
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
width: 44px;
height: 44px;
border-radius: 50%;
border: 1px solid @line;
background: @white;
color: @ink-600;
transition: all 0.26s ease;
&:hover {
background: @navy-800;
border-color: @navy-800;
color: @white;
}
}
&__dots {
display: flex;
align-items: center;
gap: 10px;
}
&__dot {
width: 9px;
height: 9px;
border-radius: 50%;
background: @ink-300;
transition: all 0.3s ease;
&.is-active {
width: 26px;
border-radius: 100px;
background: @navy-800;
}
}
}
/* ---------------- 卡片通用 ---------------- */
.news-card {
position: relative;
display: flex;
flex-direction: column;
min-height: 400px;
padding: 38px 36px 34px;
border-radius: 12px;
overflow: hidden;
&__text {
font-size: 16.5px;
font-weight: 500;
line-height: 1.95;
letter-spacing: 0.01em;
}
&__mark {
display: block;
font-size: 76px;
font-weight: 900;
line-height: 0.7;
margin-bottom: 18px;
font-family: Georgia, "Times New Roman", serif;
}
&__foot {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-top: auto;
padding-top: 28px;
}
&__author {
font-size: 13px;
letter-spacing: 0.02em;
}
/* ---------- 社交动态 ---------- */
&--media {
justify-content: space-between;
background: @navy-900;
img {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.7s cubic-bezier(0.22, 0.61, 0.36, 1);
}
&:hover img {
transform: scale(1.06);
}
}
&__veil {
position: absolute;
inset: 0;
background: linear-gradient(
175deg,
rgba(6, 12, 32, 0.82) 0%,
rgba(6, 12, 32, 0.34) 42%,
rgba(6, 12, 32, 0.86) 100%
);
}
&__source {
position: relative;
z-index: 1;
display: inline-flex;
align-items: center;
gap: 8px;
align-self: flex-start;
padding: 7px 14px;
border-radius: 100px;
background: rgba(255, 255, 255, 0.14);
backdrop-filter: blur(6px);
font-size: 11.5px;
font-weight: 500;
letter-spacing: 0.16em;
color: @white;
}
&__caption {
position: relative;
z-index: 1;
font-size: 17px;
font-weight: 700;
line-height: 1.6;
color: @white;
}
/* ---------- 图文引用 ---------- */
&--article {
background: @white;
border: 1px solid @line;
box-shadow: 0 18px 44px rgba(20, 35, 82, 0.07);
.news-card__mark {
color: @blue-600;
}
.news-card__text {
color: @ink-800;
}
.news-card__author {
color: @ink-500;
}
}
&__pill {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 10px 20px;
border: 1px solid @ink-300;
border-radius: 100px;
font-size: 13px;
color: @ink-700;
transition: all 0.28s ease;
&:hover {
background: @navy-800;
border-color: @navy-800;
color: @white;
}
}
/* ---------- 引言卡片 ---------- */
&--quote {
background: linear-gradient(160deg, @teal-500 0%, @teal-600 100%);
color: @white;
box-shadow: 0 22px 48px rgba(23, 168, 143, 0.24);
.news-card__mark {
color: rgba(255, 255, 255, 0.45);
}
.news-card__text {
color: rgba(255, 255, 255, 0.95);
}
.news-card__author {
color: rgba(255, 255, 255, 0.72);
}
}
&__round {
display: inline-flex;
align-items: center;
justify-content: center;
width: 44px;
height: 44px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.16);
color: @white;
transition: all 0.28s ease;
&:hover {
background: @white;
color: @teal-600;
transform: translate(3px, -3px);
}
}
}
/* ---------------- 响应式 ---------------- */
@media (max-width: 1200px) {
.news {
padding: 84px 0 80px;
}
.news-card {
min-height: 340px;
padding: 30px 28px 28px;
&__text {
font-size: 15px;
}
&__mark {
font-size: 62px;
}
}
}
/* 两列:隐藏最后一张,避免第二行只留孤卡 */
@media (max-width: 992px) {
.news {
padding: 72px 0 68px;
&__slide {
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 20px;
}
}
.news-card:last-child {
display: none;
}
}
/* 单列:卡片高度交给内容决定,不再写死 min-height */
@media (max-width: 640px) {
.news {
padding: 56px 0 60px;
&__slide {
grid-template-columns: minmax(0, 1fr);
gap: 16px;
}
&__controls {
gap: 14px;
margin-top: 26px;
}
&__nav {
width: 40px;
height: 40px;
}
}
.news-card {
min-height: 0;
padding: 24px 22px 22px;
&__text {
font-size: 14.5px;
line-height: 1.9;
}
&__mark {
margin-bottom: 12px;
font-size: 52px;
}
&__foot {
padding-top: 20px;
}
&--media {
min-height: 220px;
}
}
.news-card:last-child {
display: none;
}
}
</style>
@@ -0,0 +1,637 @@
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref } from "vue";
import type { ContentItem } from "@/types/content";
import { categoryName } from "@/config/sections";
import { fetchList } from "@/api/content";
interface Popup {
tags: string[];
title: string;
rows: [string, string][];
}
/**
* 首页取新闻中心最新 4 条,
* 卡片文案与详情页共用同一数据源,点击标题/卡片可直接进入详情。
*/
const notices = ref<ContentItem[]>([]);
const NOTICE_LIMIT = 4;
const popups: Popup[] = [
{
tags: ["市场活动", "线上直播"],
title: "数据中台实战公开课",
rows: [
["主办单位", "云泽科技"],
["主 讲 人", "产品总监 · 李明"],
["直播时间", "9 月 25 日 20:00"],
["参与方式", "扫码预约直播"],
],
},
{
tags: ["产品发布", "新版本"],
title: "云泽数据中台 V3.0 发布",
rows: [
["发布时间", "9 月 10 日"],
["版本亮点", "实时计算 · 指标管理"],
["试用方式", "官网申请免费试用"],
["咨询电话", "400-000-0000"],
],
},
{
tags: ["展会日程", "线下活动"],
title: "2026 云泽数智大会",
rows: [
["举办城市", "上海"],
["举办时间", "10 月 18 日 09:30"],
["举办地点", "国家会展中心 3 号馆"],
["报名方式", "官网在线报名"],
],
},
];
/* ---------------- 公告轮播 ---------------- */
const pageSize = 2;
const page = ref(0);
const progress = ref(0);
const playing = ref(true);
const pageCount = computed(() =>
Math.max(1, Math.ceil(notices.value.length / pageSize))
);
const currentNotices = computed(() =>
notices.value.slice(page.value * pageSize, page.value * pageSize + pageSize)
);
let timer: number | undefined;
const stopTimer = () => {
if (timer) {
window.clearInterval(timer);
timer = undefined;
}
};
const startTimer = () => {
stopTimer();
if (!playing.value) return;
timer = window.setInterval(() => {
progress.value += 1;
if (progress.value >= 100) {
goTo(page.value + 1);
}
}, 70);
};
const goTo = (next: number) => {
const count = pageCount.value;
page.value = (next + count) % count;
progress.value = 0;
};
const togglePlay = () => {
playing.value = !playing.value;
if (playing.value) {
startTimer();
} else {
stopTimer();
}
};
/* ---------------- 弹窗公告轮播 ---------------- */
const popupIndex = ref(0);
const currentPopup = computed(() => popups[popupIndex.value]);
const stepPopup = (delta: number) => {
popupIndex.value = (popupIndex.value + delta + popups.length) % popups.length;
};
onMounted(async () => {
const result = await fetchList({
section: "news",
page: 1,
pageSize: NOTICE_LIMIT,
});
notices.value = result.list;
startTimer();
});
onBeforeUnmount(stopTimer);
</script>
<template>
<section class="notice">
<div class="container notice__grid">
<!-- 通知公告 -->
<div class="notice__main">
<header class="notice__head">
<h2>新闻中心</h2>
<RouterLink to="/news" class="notice__more">
查看更多
<svg viewBox="0 0 24 24" aria-hidden="true">
<path
d="m9 6 6 6-6 6"
fill="none"
stroke="currentColor"
stroke-width="1.8"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</RouterLink>
</header>
<div class="notice__cards">
<RouterLink
v-for="item in currentNotices"
:key="item.slug"
:to="`/news/detail/${item.slug}`"
class="notice-card"
>
<span class="notice-card__tag">
{{ categoryName("news", item.category) }}
</span>
<h3 class="notice-card__title">{{ item.title }}</h3>
<p class="notice-card__summary">{{ item.summary }}</p>
<time class="notice-card__date">{{ item.date }}</time>
</RouterLink>
</div>
<div class="notice__controls">
<button
class="notice__arrow"
type="button"
aria-label="上一条"
@click="goTo(page - 1)"
>
<svg viewBox="0 0 24 24" aria-hidden="true">
<path
d="m14 6-6 6 6 6"
fill="none"
stroke="currentColor"
stroke-width="1.8"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</button>
<button
class="notice__arrow"
type="button"
aria-label="下一条"
@click="goTo(page + 1)"
>
<svg viewBox="0 0 24 24" aria-hidden="true">
<path
d="m10 6 6 6-6 6"
fill="none"
stroke="currentColor"
stroke-width="1.8"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</button>
<div class="notice__progress">
<span :style="{ width: `${progress}%` }"></span>
</div>
<button
class="notice__play"
type="button"
:aria-label="playing ? '暂停轮播' : '开始轮播'"
@click="togglePlay"
>
<svg v-if="playing" viewBox="0 0 24 24" aria-hidden="true">
<path
d="M9.5 7.5v9M14.5 7.5v9"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
/>
</svg>
<svg v-else viewBox="0 0 24 24" aria-hidden="true">
<path d="M9 7.5 16 12l-7 4.5z" fill="currentColor" />
</svg>
</button>
</div>
</div>
<!-- 弹窗公告 -->
<aside class="popup">
<header class="popup__head">
<h2>弹窗公告</h2>
<div class="popup__tools">
<button type="button" aria-label="上一条弹窗" @click="stepPopup(-1)">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path
d="m14 6-6 6 6 6"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</button>
<span>{{ popupIndex + 1 }} / {{ popups.length }}</span>
<button type="button" aria-label="下一条弹窗" @click="stepPopup(1)">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path
d="m10 6 6 6-6 6"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</button>
</div>
</header>
<transition name="popup-fade" mode="out-in">
<div :key="popupIndex" class="popup__card">
<ul class="popup__tags">
<li v-for="tag in currentPopup.tags" :key="tag">{{ tag }}</li>
</ul>
<h3>{{ currentPopup.title }}</h3>
<ul class="popup__rows">
<li v-for="[label, value] in currentPopup.rows" :key="label">
<span>{{ label }}</span>
<em>{{ value }}</em>
</li>
</ul>
</div>
</transition>
</aside>
</div>
</section>
</template>
<style scoped lang="less">
.notice {
position: relative;
z-index: 2;
background: @navy-800;
padding: 74px 0 96px;
height: 650px;
&__grid {
display: grid;
grid-template-columns: minmax(0, 1fr) 470px;
gap: 70px;
}
&__head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 30px;
h2 {
position: relative;
padding-bottom: 14px;
font-size: 30px;
font-weight: 700;
letter-spacing: 0.01em;
color: @white;
&::after {
content: "";
position: absolute;
left: 0;
bottom: 0;
width: 46px;
height: 3px;
background: @blue-500;
}
}
}
&__more {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 13px;
color: rgba(255, 255, 255, 0.6);
transition: color 0.2s ease;
svg {
width: 15px;
height: 15px;
}
&:hover {
color: @white;
}
}
&__cards {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 24px;
}
&__controls {
display: flex;
align-items: center;
gap: 14px;
margin-top: 34px;
}
&__arrow,
&__play {
display: inline-flex;
align-items: center;
justify-content: center;
width: 44px;
height: 44px;
border-radius: 50%;
border: 1px solid rgba(255, 255, 255, 0.28);
color: @white;
background: transparent;
flex-shrink: 0;
transition: all 0.25s ease;
svg {
width: 17px;
height: 17px;
}
&:hover {
background: @white;
border-color: @white;
color: @navy-800;
}
}
&__play {
background: @white;
border-color: @white;
color: @navy-800;
margin-left: 4px;
}
&__progress {
position: relative;
flex: 1;
height: 2px;
margin-left: 10px;
background: rgba(255, 255, 255, 0.2);
overflow: hidden;
span {
display: block;
height: 100%;
background: @white;
transition: width 0.08s linear;
}
}
}
/* ---------------- 公告卡片 ---------------- */
.notice-card {
display: flex;
flex-direction: column;
min-height: 236px;
padding: 30px 30px 26px;
border-radius: 8px;
background: @white;
box-shadow: 0 18px 44px rgba(4, 10, 30, 0.28);
transition: transform 0.3s cubic-bezier(0.22, 0.61, 0.36, 1),
box-shadow 0.3s ease;
&:hover {
transform: translateY(-6px);
box-shadow: 0 26px 56px rgba(4, 10, 30, 0.36);
/* 整卡可点,标题给出明确的点击反馈 */
.notice-card__title {
color: @blue-600;
text-decoration: underline;
text-decoration-thickness: 2px;
text-underline-offset: 5px;
}
}
&__tag {
align-self: flex-start;
margin-bottom: 14px;
padding: 3px 11px;
border-radius: 100px;
background: rgba(47, 107, 243, 0.1);
color: @blue-600;
font-size: 12px;
font-weight: 500;
letter-spacing: 0.04em;
}
&__title {
font-size: 18px;
font-weight: 700;
line-height: 1.55;
color: @ink-900;
transition: color 0.26s ease;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
&__summary {
margin-top: 12px;
font-size: 13.5px;
line-height: 1.8;
color: @ink-500;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
&__date {
margin-top: auto;
padding-top: 18px;
font-size: 12.5px;
font-weight: 500;
color: @blue-600;
letter-spacing: 0.04em;
}
}
/* ---------------- 弹窗公告 ---------------- */
.popup {
display: flex;
flex-direction: column;
&__head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 30px;
h2 {
position: relative;
padding-bottom: 14px;
font-size: 26px;
font-weight: 700;
color: @white;
&::after {
content: "";
position: absolute;
left: 0;
bottom: 0;
width: 40px;
height: 3px;
background: @blue-500;
}
}
}
&__tools {
display: flex;
align-items: center;
gap: 14px;
font-size: 12.5px;
letter-spacing: 0.06em;
color: rgba(255, 255, 255, 0.6);
button {
display: inline-flex;
color: rgba(255, 255, 255, 0.6);
transition: color 0.2s ease;
svg {
width: 15px;
height: 15px;
}
&:hover {
color: @white;
}
}
}
&__card {
flex: 1;
padding: 30px 32px 34px;
border-radius: 8px;
border: 1px solid rgba(118, 156, 255, 0.45);
background: linear-gradient(
155deg,
rgba(75, 131, 255, 0.26) 0%,
rgba(36, 65, 143, 0.55) 100%
);
box-shadow: 0 20px 46px rgba(4, 10, 30, 0.3);
h3 {
margin-bottom: 22px;
font-size: 23px;
font-weight: 700;
line-height: 1.45;
color: @white;
}
}
&__tags {
display: flex;
gap: 8px;
margin-bottom: 16px;
li {
padding: 3px 11px;
border-radius: 100px;
background: rgba(255, 255, 255, 0.16);
font-size: 12px;
color: rgba(255, 255, 255, 0.9);
}
}
&__rows {
li {
display: flex;
gap: 16px;
padding: 7px 0;
font-size: 13px;
border-bottom: 1px dashed rgba(255, 255, 255, 0.14);
&:last-child {
border-bottom: 0;
}
span {
flex-shrink: 0;
width: 72px;
color: rgba(255, 255, 255, 0.55);
}
em {
font-style: normal;
color: rgba(255, 255, 255, 0.92);
}
}
}
}
.popup-fade-enter-active,
.popup-fade-leave-active {
transition: opacity 0.28s ease, transform 0.28s ease;
}
.popup-fade-enter-from {
opacity: 0;
transform: translateY(12px);
}
.popup-fade-leave-to {
opacity: 0;
transform: translateY(-10px);
}
/* ---------------- 响应式 ---------------- */
@media (max-width: 1200px) {
.notice {
padding: 60px 0 76px;
&__grid {
grid-template-columns: minmax(0, 1fr) 380px;
gap: 44px;
}
}
}
@media (max-width: 992px) {
.notice {
&__grid {
grid-template-columns: minmax(0, 1fr);
gap: 52px;
}
}
}
@media (max-width: 768px) {
.notice {
padding: 48px 0 60px;
&__head h2 {
font-size: 22px;
}
&__cards {
grid-template-columns: minmax(0, 1fr);
}
}
.notice-card {
min-height: 0;
}
.popup__card h3 {
font-size: 20px;
}
}
</style>
@@ -0,0 +1,268 @@
<script setup lang="ts">
import AppIcon from "@/components/AppIcon.vue";
const services = [
{
icon: "monitor",
title: "预约在线演示",
desc: "1 对 1 产品演示与答疑",
path: "/support/demo",
},
{
icon: "clipboard",
title: "申请免费试用",
desc: "快速开通试用账号",
path: "/support/trial",
},
{
icon: "cap",
title: "产品文档",
desc: "快速上手与接口说明",
path: "/support/docs",
},
{
icon: "book",
title: "开发者中心",
desc: "SDK · API · 最佳实践",
path: "/support/developer",
},
{
icon: "ribbon",
title: "客户案例",
desc: "行业落地实践合集",
path: "/cases",
},
{
icon: "phone",
title: "联系销售",
desc: "获取专属解决方案",
path: "/contact",
},
];
</script>
<template>
<section class="quick">
<div class="container quick__inner">
<div class="quick__intro">
<h2 class="quick__title">
<span>QUICK</span>
<span>SERVICE</span>
</h2>
<span class="quick__bar"></span>
<h3 class="quick__subtitle">快捷入口</h3>
<p class="quick__desc">
按访问量整理的高频服务入口,帮助客户与合作伙伴<br />
快速找到所需的产品资料与服务支持。
</p>
</div>
<ul class="quick__grid">
<li v-for="item in services" :key="item.title">
<RouterLink :to="item.path" class="service-card">
<span class="service-card__icon">
<AppIcon :name="item.icon" :size="28" :stroke-width="1.5" />
</span>
<strong class="service-card__title">{{ item.title }}</strong>
<em class="service-card__desc">{{ item.desc }}</em>
<span class="service-card__arrow">
<AppIcon name="arrow-right" :size="16" />
</span>
</RouterLink>
</li>
</ul>
</div>
</section>
</template>
<style scoped lang="less">
.quick {
position: relative;
z-index: 3;
margin-top: -140px;
padding: 104px 0 118px;
background: @white;
border-top-left-radius: 200px 150px;
&__inner {
display: grid;
grid-template-columns: 400px minmax(0, 1fr);
gap: 90px;
align-items: start;
}
&__title {
display: flex;
flex-direction: column;
font-size: 62px;
font-weight: 900;
line-height: 1.02;
letter-spacing: -0.01em;
color: @ink-900;
}
&__bar {
display: block;
width: 5px;
height: 54px;
margin: 34px 0 22px;
background: @green-400;
border-radius: 3px;
}
&__subtitle {
font-size: 30px;
font-weight: 700;
letter-spacing: 0.06em;
color: @ink-900;
}
&__desc {
margin-top: 14px;
font-size: 14px;
line-height: 1.9;
color: @ink-500;
}
&__grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 22px;
}
}
.service-card {
position: relative;
display: flex;
flex-direction: column;
height: 100%;
min-height: 186px;
padding: 30px 28px 28px;
border: 1px solid @line;
border-radius: 10px;
background: @white;
transition: transform 0.32s cubic-bezier(0.22, 0.61, 0.36, 1),
border-color 0.32s ease, box-shadow 0.32s ease;
&__icon {
display: inline-flex;
color: @navy-700;
transition: color 0.3s ease;
}
&__title {
margin-top: 26px;
font-size: 17px;
font-weight: 700;
color: @ink-900;
letter-spacing: 0.01em;
}
&__desc {
margin-top: 7px;
font-style: normal;
font-size: 12.5px;
line-height: 1.7;
color: @ink-500;
}
&__arrow {
position: absolute;
right: 24px;
bottom: 24px;
display: inline-flex;
color: @ink-300;
opacity: 0;
transform: translateX(-6px);
transition: opacity 0.3s ease, transform 0.3s ease, color 0.3s ease;
}
&:hover {
transform: translateY(-8px);
border-color: rgba(47, 107, 243, 0.45);
box-shadow: 0 22px 48px rgba(20, 35, 82, 0.12);
.service-card__icon {
color: @blue-600;
}
.service-card__arrow {
opacity: 1;
transform: translateX(0);
color: @blue-600;
}
}
}
/* ---------------- 响应式 ---------------- */
@media (max-width: 1300px) {
.quick {
&__inner {
grid-template-columns: 320px minmax(0, 1fr);
gap: 56px;
}
&__title {
font-size: 50px;
}
&__subtitle {
font-size: 26px;
}
&__grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 16px;
}
}
.service-card {
min-height: 168px;
padding: 24px 22px;
}
}
@media (max-width: 992px) {
.quick {
margin-top: -48px;
padding: 68px 0 76px;
border-top-left-radius: 150px 70px;
&__inner {
grid-template-columns: minmax(0, 1fr);
gap: 44px;
}
&__grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
}
@media (max-width: 600px) {
.quick {
border-top-left-radius: 90px 44px;
&__title {
font-size: 38px;
}
&__bar {
height: 40px;
margin: 22px 0 16px;
}
&__subtitle {
font-size: 21px;
}
&__grid {
grid-template-columns: minmax(0, 1fr);
}
}
.service-card {
min-height: 0;
}
}
</style>
@@ -0,0 +1,436 @@
<script setup lang="ts">
import AppIcon from "@/components/AppIcon.vue";
interface ScheduleItem {
range: string;
period: string;
name: string;
desc: string;
}
const schedules: ScheduleItem[] = [
{
range: "09.10 ~ 09.30",
period: "2026 年 9 月",
name: "数据中台 V3.0 全量开放试用",
desc: "面向全部客户开放试用,体验实时计算与指标管理能力。",
},
{
range: "10.18 ~ 10.19",
period: "2026 年 10 月",
name: "云泽数智大会 · 上海站",
desc: "30 余位行业客户分享落地实践,现场设产品体验区。",
},
{
range: "11.05 ~ 11.28",
period: "2026 年 11 月",
name: "全国合作伙伴城市巡展",
desc: "覆盖 12 座城市,面向合作伙伴开放产品与政策解读。",
},
];
</script>
<template>
<section class="schedule">
<div class="container schedule__inner">
<!-- 活动时间轴 -->
<div class="schedule__panel">
<div class="schedule__head">
<h2>近期活动与发布节奏</h2>
<p>2026 年 · 活动日历</p>
</div>
<ul class="timeline">
<li
v-for="(item, index) in schedules"
:key="item.range"
class="timeline__item"
:class="{ 'is-next': index === 0 }"
>
<h3 class="timeline__range">{{ item.range }}</h3>
<!-- 节点轨道:横线由 CSS 绘制,节点天然居中 -->
<span class="timeline__rail" aria-hidden="true">
<i class="timeline__node"></i>
<i class="timeline__axis"></i>
</span>
<p class="timeline__period">{{ item.period }}</p>
<p class="timeline__name">{{ item.name }}</p>
<p class="timeline__desc">{{ item.desc }}</p>
</li>
</ul>
</div>
<!-- 活动入口 -->
<RouterLink to="/events" class="schedule-card">
<span class="schedule-card__icon">
<AppIcon name="calendar" :size="28" :stroke-width="1.5" />
</span>
<div class="schedule-card__text">
<strong>市场活动</strong>
<em>参展、发布会与公开课,全年安排一站查看。</em>
</div>
<span class="schedule-card__more">
查看全部活动
<AppIcon name="arrow-right" :size="16" />
</span>
</RouterLink>
</div>
</section>
</template>
<style scoped lang="less">
@timeline-gap: 46px;
.schedule {
position: relative;
padding: 92px 0 100px;
background: linear-gradient(180deg, #f7f9fd 0%, #edf1fa 100%);
overflow: hidden;
&::before {
content: "";
position: absolute;
top: -140px;
right: -120px;
width: 520px;
height: 520px;
border-radius: 50%;
background: radial-gradient(
circle,
rgba(47, 107, 243, 0.09) 0%,
rgba(47, 107, 243, 0) 68%
);
pointer-events: none;
}
&__inner {
position: relative;
display: grid;
grid-template-columns: minmax(0, 1fr) 360px;
gap: 26px;
align-items: stretch;
}
&__panel {
padding: 40px 46px 46px;
border-radius: 14px;
background: @white;
box-shadow: 0 20px 50px rgba(20, 35, 82, 0.07);
}
&__head {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 20px;
margin-bottom: 36px;
padding-bottom: 20px;
border-bottom: 1px solid @line;
h2 {
font-size: 22px;
font-weight: 700;
letter-spacing: 0.01em;
color: @ink-900;
}
p {
font-size: 13px;
letter-spacing: 0.06em;
color: @ink-400;
white-space: nowrap;
}
}
}
/* ---------------- 时间轴 ---------------- */
.timeline {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: @timeline-gap;
&__item {
display: flex;
flex-direction: column;
min-width: 0;
}
&__range {
font-size: 25px;
font-weight: 700;
line-height: 36px;
letter-spacing: 0.01em;
color: @ink-900;
white-space: nowrap;
}
/* 节点轨道:自身即 flex 容器,节点与横线必然在同一中线 */
&__rail {
display: flex;
align-items: center;
height: 26px;
margin: 14px 0 12px;
}
&__node {
flex-shrink: 0;
width: 15px;
height: 15px;
border-radius: 50%;
background: @white;
border: 4px solid @ink-300;
box-shadow: 0 0 0 4px rgba(20, 35, 82, 0.05);
transition: border-color 0.3s ease, box-shadow 0.3s ease;
}
/* 把横线延伸进列间距,让三个节点真正连成一条轴 */
&__axis {
flex: 1;
height: 1px;
margin: 0 calc(-1 * @timeline-gap) 0 0;
background: linear-gradient(
90deg,
@line 0%,
@line 70%,
rgba(232, 235, 242, 0) 100%
);
}
&__item:last-child &__axis {
display: none;
}
&__item.is-next &__node {
border-color: @blue-600;
box-shadow: 0 0 0 5px rgba(47, 107, 243, 0.14);
}
&__period {
font-size: 12.5px;
letter-spacing: 0.06em;
color: @ink-400;
}
&__name {
margin-top: 8px;
font-size: 16px;
font-weight: 700;
line-height: 1.55;
color: @ink-800;
transition: color 0.28s ease;
}
&__desc {
margin-top: 10px;
font-size: 13px;
line-height: 1.85;
color: @ink-500;
}
&__item:hover &__node {
border-color: @blue-600;
box-shadow: 0 0 0 7px rgba(47, 107, 243, 0.12);
}
&__item:hover &__name {
color: @blue-600;
}
}
/* ---------------- 活动入口卡 ---------------- */
.schedule-card {
position: relative;
display: flex;
flex-direction: column;
gap: 26px;
min-height: 240px;
padding: 38px 34px 34px;
border-radius: 14px;
background: linear-gradient(155deg, #3f7bff 0%, #2450d8 100%);
box-shadow: 0 26px 54px rgba(36, 80, 216, 0.26);
color: @white;
overflow: hidden;
transition: transform 0.34s cubic-bezier(0.22, 0.61, 0.36, 1),
box-shadow 0.34s ease;
&::after {
content: "";
position: absolute;
right: -70px;
top: -70px;
width: 220px;
height: 220px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
}
&__icon {
position: relative;
z-index: 1;
display: inline-flex;
align-items: center;
justify-content: center;
width: 54px;
height: 54px;
border-radius: 14px;
background: rgba(255, 255, 255, 0.16);
}
&__text {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
strong {
font-size: 25px;
font-weight: 700;
letter-spacing: 0.04em;
}
em {
margin-top: 10px;
font-style: normal;
font-size: 13px;
line-height: 1.8;
color: rgba(255, 255, 255, 0.72);
}
}
&__more {
position: relative;
z-index: 1;
margin-top: auto;
display: inline-flex;
align-items: center;
justify-content: space-between;
gap: 10px;
align-self: stretch;
padding: 12px 20px;
border-radius: 100px;
background: rgba(255, 255, 255, 0.14);
font-size: 13.5px;
font-weight: 500;
transition: background 0.3s ease, color 0.3s ease;
}
&:hover {
transform: translateY(-8px);
box-shadow: 0 34px 66px rgba(36, 80, 216, 0.34);
.schedule-card__more {
background: @white;
color: @blue-600;
}
}
}
/* ---------------- 响应式 ---------------- */
@media (max-width: 1200px) {
.schedule {
padding: 74px 0 82px;
&__inner {
grid-template-columns: minmax(0, 1fr) 310px;
gap: 22px;
}
&__panel {
padding: 32px 32px 38px;
}
}
.timeline {
gap: 34px;
&__range {
font-size: 21px;
line-height: 32px;
}
&__axis {
margin-right: -34px;
}
}
}
@media (max-width: 992px) {
.schedule {
&__inner {
grid-template-columns: minmax(0, 1fr);
}
}
.schedule-card {
min-height: 210px;
}
}
@media (max-width: 768px) {
.schedule {
padding: 56px 0 64px;
&__panel {
padding: 26px 22px 30px;
}
&__head {
flex-direction: column;
align-items: flex-start;
gap: 8px;
margin-bottom: 8px;
h2 {
font-size: 19px;
}
}
}
.timeline {
grid-template-columns: minmax(0, 1fr);
gap: 0;
&__item {
position: relative;
padding: 26px 0 26px 28px;
& + & {
border-top: 1px solid @line;
}
}
/* 移动端改为左侧竖向节点 */
&__rail {
position: absolute;
left: 0;
top: 33px;
width: 14px;
height: 14px;
margin: 0;
}
&__axis {
display: none !important;
}
&__node {
width: 12px;
height: 12px;
border-width: 3px;
box-shadow: none;
}
&__range {
font-size: 19px;
line-height: 28px;
}
&__desc {
margin-top: 8px;
font-size: 12.5px;
}
}
}
</style>
+23
View File
@@ -0,0 +1,23 @@
<script setup lang="ts">
import HeroBanner from "./components/HeroBanner.vue";
import NoticeSection from "./components/NoticeSection.vue";
import QuickService from "./components/QuickService.vue";
import ScheduleSection from "./components/ScheduleSection.vue";
import NewsSection from "./components/NewsSection.vue";
</script>
<template>
<div class="home">
<HeroBanner />
<NoticeSection />
<QuickService />
<ScheduleSection />
<NewsSection />
</div>
</template>
<style scoped lang="less">
.home {
overflow-x: hidden;
}
</style>
+297
View File
@@ -0,0 +1,297 @@
<script setup lang="ts">
import { computed, ref, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
import type { ContentItem } from "@/types/content";
import { categoryName, sections } from "@/config/sections";
import { fetchCategoryCounts, fetchList } from "@/api/content";
import PageHero from "@/components/PageHero.vue";
import FilterBar from "@/components/FilterBar.vue";
import ContentCard from "@/components/ContentCard.vue";
import PagerBar from "@/components/PagerBar.vue";
import AppIcon from "@/components/AppIcon.vue";
/**
* 通用列表 / 分类页
* 同一组件服务 /news、/news/category/:slug、/products 等所有栏目,
* 栏目与分类通过路由路径推导,组件实例复用时也能正确响应。
*/
const route = useRoute();
const router = useRouter();
const section = computed(
() =>
sections[
(route.path.split("/").filter(Boolean)[0] ?? "news") as keyof typeof sections
] ?? sections.news
);
const activeCategory = computed(() => (route.params.slug as string) ?? "");
const keyword = ref("");
const page = ref(1);
const items = ref<ContentItem[]>([]);
const counts = ref<Record<string, number>>({});
const total = ref(0);
const totalPages = ref(1);
const loading = ref(true);
/** 瀑布式列表每页 6 条,卡片网格每页 9 条 */
const pageSize = computed(() => (section.value.layout === "grid" ? 9 : 6));
const heroTitle = computed(() =>
activeCategory.value
? categoryName(section.value.key, activeCategory.value)
: section.value.title
);
const heroSubtitle = computed(() => {
if (!activeCategory.value) return section.value.subtitle;
return (
section.value.categories.find((item) => item.slug === activeCategory.value)
?.desc || section.value.subtitle
);
});
const crumbs = computed(() =>
activeCategory.value
? [section.value.title, categoryName(section.value.key, activeCategory.value)]
: [section.value.title]
);
const empty = computed(() => !loading.value && items.value.length === 0);
async function load() {
loading.value = true;
const current = section.value;
const [result, categoryCounts] = await Promise.all([
fetchList({
section: current.key,
category: activeCategory.value || undefined,
keyword: keyword.value,
page: page.value,
pageSize: pageSize.value,
}),
fetchCategoryCounts(current.key),
]);
items.value = result.list;
total.value = result.total;
totalPages.value = result.totalPages;
counts.value = categoryCounts;
loading.value = false;
}
/** 条件变化 => 回到第一页并重新加载 */
watch(
() => `${section.value.key}|${activeCategory.value}|${keyword.value}|${page.value}`,
load,
{ immediate: true }
);
function handleCategoryChange(slug: string) {
page.value = 1;
const current = section.value;
router.push(slug ? `${current.categoryPath}/${slug}` : current.basePath);
}
function handleKeywordChange(value: string) {
keyword.value = value;
page.value = 1;
}
</script>
<template>
<div class="list-page">
<PageHero :title="heroTitle" :subtitle="heroSubtitle" :crumbs="crumbs" />
<div class="container list-page__body">
<FilterBar
:categories="section.categories"
:active="activeCategory"
:counts="counts"
:total="counts.__all__ ?? 0"
:searchable="section.searchable"
:keyword="keyword"
@update:active="handleCategoryChange"
@update:keyword="handleKeywordChange"
/>
<!-- 加载骨架 -->
<ul v-if="loading" class="content-grid" :class="`is-${section.layout}`">
<li v-for="i in pageSize" :key="i" class="skeleton" :class="`is-${section.layout}`">
<span></span>
</li>
</ul>
<!-- 空状态 -->
<div v-else-if="empty" class="empty">
<span class="empty__icon">
<AppIcon name="search" :size="26" />
</span>
<h3>没有找到相关内容</h3>
<p>试试更换关键词,或切换到其它分类查看。</p>
<button
v-if="keyword || activeCategory"
type="button"
class="empty__btn"
@click="
handleKeywordChange('');
handleCategoryChange('');
"
>
重置筛选条件
</button>
</div>
<!-- 内容列表 -->
<template v-else>
<ul class="content-grid" :class="`is-${section.layout}`">
<li v-for="item in items" :key="item.slug">
<ContentCard :item="item" :layout="section.layout" />
</li>
</ul>
<PagerBar
:page="page"
:total-pages="totalPages"
:total="total"
:unit="section.unit"
@change="page = $event"
/>
</template>
</div>
</div>
</template>
<style scoped lang="less">
.list-page {
&__body {
padding-top: 56px;
padding-bottom: 110px;
}
}
.content-grid {
margin-top: 40px;
&.is-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 26px;
}
&.is-list {
display: flex;
flex-direction: column;
gap: 22px;
}
}
/* ---------------- 骨架屏 ---------------- */
.skeleton {
overflow: hidden;
border: 1px solid @line;
border-radius: 12px;
background: @white;
span {
display: block;
background: linear-gradient(
100deg,
#f2f5fa 0%,
#f8fafd 42%,
#f2f5fa 84%
);
background-size: 200% 100%;
animation: skeleton 1.4s ease-in-out infinite;
}
&.is-grid span {
height: 300px;
}
&.is-list span {
height: 200px;
}
}
@keyframes skeleton {
0% {
background-position: 120% 0;
}
100% {
background-position: -20% 0;
}
}
/* ---------------- 空状态 ---------------- */
.empty {
display: flex;
flex-direction: column;
align-items: center;
padding: 90px 0 100px;
text-align: center;
&__icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 62px;
height: 62px;
margin-bottom: 22px;
border-radius: 50%;
background: @bg-soft;
color: @ink-400;
}
h3 {
font-size: 19px;
font-weight: 700;
color: @ink-900;
}
p {
margin-top: 10px;
font-size: 14px;
color: @ink-500;
}
&__btn {
margin-top: 26px;
padding: 11px 26px;
border-radius: 100px;
border: 1px solid @ink-300;
font-size: 14px;
color: @ink-700;
transition: all 0.26s ease;
&:hover {
background: @navy-800;
border-color: @navy-800;
color: @white;
}
}
}
/* ---------------- 响应式 ---------------- */
@media (max-width: 1200px) {
.content-grid.is-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 22px;
}
}
@media (max-width: 640px) {
.list-page__body {
padding-top: 40px;
padding-bottom: 72px;
}
.content-grid.is-grid {
grid-template-columns: minmax(0, 1fr);
gap: 18px;
}
}
</style>
+174
View File
@@ -0,0 +1,174 @@
<script setup lang="ts">
import { useRoute, useRouter } from "vue-router";
import { navItems } from "@/config/site";
import AppIcon from "@/components/AppIcon.vue";
const route = useRoute();
const router = useRouter();
</script>
<template>
<section class="not-found">
<div class="container not-found__inner">
<span class="not-found__code">404</span>
<h1>没有找到这个页面</h1>
<p>
你访问的地址 <code>{{ route.fullPath }}</code> 不存在,<br />
可能是链接已失效,或者地址输入有误。
</p>
<div class="not-found__actions">
<RouterLink to="/" class="not-found__btn not-found__btn--primary">
<AppIcon name="arrow-left" :size="16" />
返回首页
</RouterLink>
<button type="button" class="not-found__btn" @click="router.back()">
回到上一页
</button>
</div>
<nav class="not-found__nav">
<span>你可能想找:</span>
<RouterLink v-for="item in navItems" :key="item.path" :to="item.path">
{{ item.label }}
</RouterLink>
</nav>
</div>
</section>
</template>
<style scoped lang="less">
.not-found {
display: flex;
align-items: center;
min-height: 64vh;
padding: 90px 0;
background: linear-gradient(180deg, #f7f9fd 0%, #edf1fa 100%);
&__inner {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
}
&__code {
font-size: 96px;
font-weight: 900;
line-height: 1;
letter-spacing: 0.02em;
color: rgba(27, 47, 110, 0.12);
}
h1 {
margin-top: -18px;
font-size: 34px;
font-weight: 700;
color: @ink-900;
}
p {
margin-top: 18px;
font-size: 15px;
line-height: 1.95;
color: @ink-500;
code {
padding: 2px 8px;
border-radius: 5px;
background: @white;
border: 1px solid @line;
font-size: 13.5px;
color: @ink-700;
}
}
&__actions {
display: flex;
gap: 14px;
margin-top: 38px;
}
&__btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 13px 30px;
border-radius: 100px;
border: 1px solid @ink-300;
background: @white;
font-size: 14.5px;
font-weight: 500;
color: @ink-700;
transition: all 0.28s ease;
&:hover {
border-color: @navy-800;
color: @navy-800;
}
&--primary {
border-color: @navy-800;
background: @navy-800;
color: @white;
&:hover {
background: @blue-600;
border-color: @blue-600;
color: @white;
}
}
}
&__nav {
display: flex;
align-items: center;
flex-wrap: wrap;
justify-content: center;
gap: 10px 22px;
margin-top: 52px;
padding-top: 30px;
border-top: 1px solid @line;
width: 100%;
max-width: 760px;
span {
font-size: 13px;
color: @ink-400;
}
a {
font-size: 13.5px;
color: @ink-600;
&:hover {
color: @blue-600;
}
}
}
}
@media (max-width: 640px) {
.not-found {
padding: 60px 0;
&__code {
font-size: 68px;
}
h1 {
margin-top: -12px;
font-size: 25px;
}
&__actions {
flex-direction: column;
align-self: stretch;
.not-found__btn {
justify-content: center;
}
}
}
}
</style>
+292
View File
@@ -0,0 +1,292 @@
<script setup lang="ts">
import { computed, ref, watch } from "vue";
import { useRoute } from "vue-router";
import type { SinglePage } from "@/types/content";
import { fetchSinglePage } from "@/api/content";
import PageHero from "@/components/PageHero.vue";
import BlockRenderer from "@/components/BlockRenderer.vue";
import AppIcon from "@/components/AppIcon.vue";
/**
* 通用单页
* 服务 /about、/careers、/partners、/contact、/support、/support/:slug、/policy/:slug,
* 页面 key 直接由路由路径推导(去掉前后斜杠)。
*/
const route = useRoute();
const key = computed(() => route.path.replace(/^\/+|\/+$/g, ""));
const page = ref<SinglePage | null>(null);
const loading = ref(true);
/** 政策类页面不需要底部转化入口 */
const showCta = computed(() => !key.value.startsWith("policy"));
watch(
key,
async () => {
loading.value = true;
page.value = (await fetchSinglePage(key.value)) ?? null;
loading.value = false;
// 单页标题跟随内容变化
document.title = page.value
? `${page.value.title} | 云泽科技`
: "云泽科技 | 官方网站";
},
{ immediate: true }
);
</script>
<template>
<div class="single-page">
<PageHero
:title="page?.title ?? '页面加载中'"
:subtitle="page?.subtitle"
:crumbs="page?.crumbs ?? []"
/>
<div class="container single-page__body">
<div v-if="loading" class="single-skeleton">
<span></span>
<span></span>
<span></span>
<span></span>
</div>
<div v-else-if="!page" class="single-missing">
<span class="single-missing__icon">
<AppIcon name="file" :size="26" />
</span>
<h2>页面还没准备好</h2>
<p>该页面暂未上线,或者链接地址有误。</p>
<RouterLink to="/" class="single-missing__btn">返回首页</RouterLink>
</div>
<template v-else>
<div class="single-page__inner">
<BlockRenderer :blocks="page.blocks" />
</div>
<!-- 底部转化入口 -->
<section v-if="showCta" class="single-cta">
<div class="single-cta__text">
<h2>还没找到想要的信息?</h2>
<p>把需求告诉我们,售前顾问会在 1 个工作日内与你联系。</p>
</div>
<div class="single-cta__actions">
<RouterLink to="/support/demo" class="btn btn--primary">
预约产品演示
<AppIcon name="arrow-right" :size="16" />
</RouterLink>
<RouterLink to="/contact" class="btn btn--ghost">
联系销售顾问
</RouterLink>
</div>
</section>
</template>
</div>
</div>
</template>
<style scoped lang="less">
.single-page {
&__body {
padding-top: 64px;
padding-bottom: 110px;
}
&__inner {
max-width: 1080px;
margin: 0 auto;
}
}
/* ---------------- 底部转化 ---------------- */
.single-cta {
display: flex;
align-items: center;
justify-content: space-between;
gap: 40px;
flex-wrap: wrap;
margin-top: 80px;
padding: 46px 52px;
border-radius: 14px;
background: linear-gradient(150deg, @navy-800 0%, @navy-900 100%);
color: @white;
&__text {
max-width: 560px;
h2 {
font-size: 24px;
font-weight: 700;
}
p {
margin-top: 12px;
font-size: 14px;
line-height: 1.85;
color: rgba(255, 255, 255, 0.62);
}
}
&__actions {
display: flex;
gap: 14px;
flex-wrap: wrap;
}
}
.btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 14px 30px;
border-radius: 100px;
font-size: 14.5px;
font-weight: 500;
transition: all 0.28s ease;
&--primary {
background: @white;
color: @navy-800;
&:hover {
background: @blue-600;
color: @white;
}
}
&--ghost {
border: 1px solid rgba(255, 255, 255, 0.3);
color: @white;
&:hover {
border-color: @white;
background: rgba(255, 255, 255, 0.12);
}
}
}
/* ---------------- 缺失状态 ---------------- */
.single-missing {
display: flex;
flex-direction: column;
align-items: center;
padding: 90px 0;
text-align: center;
&__icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 62px;
height: 62px;
margin-bottom: 22px;
border-radius: 50%;
background: @bg-soft;
color: @ink-400;
}
h2 {
font-size: 21px;
font-weight: 700;
color: @ink-900;
}
p {
margin-top: 10px;
font-size: 14px;
color: @ink-500;
}
&__btn {
margin-top: 26px;
padding: 12px 28px;
border-radius: 100px;
background: @navy-800;
font-size: 14px;
color: @white;
&:hover {
background: @blue-600;
}
}
}
/* ---------------- 骨架屏 ---------------- */
.single-skeleton {
display: flex;
flex-direction: column;
gap: 18px;
max-width: 1080px;
margin: 0 auto;
span {
height: 22px;
border-radius: 6px;
background: linear-gradient(100deg, #f2f5fa 0%, #f8fafd 42%, #f2f5fa 84%);
background-size: 200% 100%;
animation: singleSkeleton 1.4s ease-in-out infinite;
&:nth-child(1) {
height: 34px;
width: 40%;
}
&:nth-child(2) {
width: 100%;
}
&:nth-child(3) {
width: 92%;
}
&:nth-child(4) {
width: 74%;
}
}
}
@keyframes singleSkeleton {
0% {
background-position: 120% 0;
}
100% {
background-position: -20% 0;
}
}
/* ---------------- 响应式 ---------------- */
@media (max-width: 992px) {
.single-page__body {
padding-top: 48px;
padding-bottom: 80px;
}
.single-cta {
margin-top: 60px;
padding: 34px 30px;
&__text h2 {
font-size: 20px;
}
}
}
@media (max-width: 640px) {
.single-page__body {
padding-bottom: 64px;
}
.single-cta__actions {
width: 100%;
.btn {
flex: 1;
justify-content: center;
}
}
}
</style>