Files
website/.vitepress/theme/components/ScreenshotPreview.vue
T
Dai0814 ccb3f5e39a
mirror-to-gitee / mirror (push) Successful in 4s
feat(portal): 首页补齐产品功能、核心能力、界面预览、官方文档四个区块
用户反馈首页缺产品功能、核心能力与界面预览,且没有官方文档入口。

新增区块
- 产品功能:三条产品线(iotLCM / 秀杰 DTU / XiuAdmin),
  每条列 6 项能力,卡片底部直达详情页
- 核心能力:8 张带标签的卡片,讲工程层面的硬指标
  (部署、接入、开放、治理、韧性、交付、数据、维护),
  与"产品功能"区分开:前者是做什么,后者是凭什么能用住
- 界面预览:ScreenshotPreview 组件,Web 管理端 / 小程序端两组标签切换,
  首屏大图 + 缩略图条,带浏览器窗口装饰
- 官方文档:6 个入口卡片(文档中心、Web 手册、小程序手册、部署手册、
  OpenAPI 合同、门户部署说明),外链自动加 target=_blank

界面预览的占位机制
- previewShots 里 file 留空时渲染带图注的占位框,不报错、不影响构建
- 补图只改 file 字段,组件无需改动
- 新增 pages/public/screenshots/README.md 写清命名、尺寸、脱敏要求

导航
- 增加产品功能、核心能力、界面预览、官方文档四个锚点项
- 窄屏(≤860px)隐藏中间三个锚点项,避免挤压

验证
- 构建 13 页,208 条引用 0 失效
- 浏览器实测:7 个导航项、5 个 section 锚点齐全
- 移动端 390px 无横向溢出
- 文档站 5 个外链地址均实测 200 可达
2026-11-04 14:27:48 +08:00

227 lines
5.0 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { ref, computed } from 'vue'
import { previewShots } from '../../../data/content'
const active = ref<'web' | 'app'>('web')
const shots = computed(() => previewShots[active.value] ?? [])
const main = computed(() => shots.value[0])
const rest = computed(() => shots.value.slice(1))
const current = ref(0)
const mainShot = computed(() => shots.value[current.value] ?? main.value)
function select(i: number) {
current.value = i
}
function switchGroup(g: 'web' | 'app') {
active.value = g
current.value = 0
}
</script>
<template>
<div class="pv">
<div class="p-tabs pv-tabs" role="tablist">
<button
class="p-tab"
:class="{ 'is-active': active === 'web' }"
role="tab"
:aria-selected="active === 'web'"
@click="switchGroup('web')"
>
Web 管理端
</button>
<button
class="p-tab"
:class="{ 'is-active': active === 'app' }"
role="tab"
:aria-selected="active === 'app'"
@click="switchGroup('app')"
>
小程序 / H5 用户端
</button>
</div>
<!-- 主图 -->
<figure class="pv-main" :data-group="active">
<div class="pv-frame">
<span class="pv-bar" aria-hidden="true">
<i></i><i></i><i></i>
</span>
<img
v-if="mainShot && mainShot.file"
:src="`/screenshots/${mainShot.file}`"
:alt="mainShot.caption"
loading="lazy"
/>
<div v-else class="pv-empty">
<svg width="34" height="34" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<rect x="3" y="4" width="18" height="14" rx="2" stroke="currentColor" stroke-width="1.5" />
<path d="M3 9h18" stroke="currentColor" stroke-width="1.5" />
<circle cx="6.5" cy="6.5" r="0.9" fill="currentColor" />
<path d="M8 17l3.2-3.6 2.3 2.2 1.8-1.6L19 17" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
</svg>
<span class="pv-empty-text">截图待补</span>
<span v-if="mainShot" class="pv-empty-caption">{{ mainShot.caption }}</span>
</div>
</div>
<figcaption v-if="mainShot" class="pv-caption">{{ mainShot.caption }}</figcaption>
</figure>
<!-- 缩略图条 -->
<div v-if="rest.length" class="pv-thumbs">
<button
v-for="(s, i) in shots"
:key="s.caption"
class="pv-thumb"
:class="{ 'is-active': i === current }"
:title="s.caption"
@click="select(i)"
>
<img v-if="s.file" :src="`/screenshots/${s.file}`" :alt="s.caption" loading="lazy" />
<span v-else class="pv-thumb-ph">{{ String(i + 1).padStart(2, '0') }}</span>
</button>
</div>
<p class="pv-note">
截图按端分组,点击缩略图切换主图。当前为占位状态,补齐截图后自动替换。
</p>
</div>
</template>
<style scoped>
.pv-tabs {
margin-bottom: 28px;
}
.pv-main {
margin: 0;
}
.pv-frame {
position: relative;
border: 1px solid var(--p-line-strong);
border-radius: var(--p-radius);
background: var(--p-surface-alt);
overflow: hidden;
box-shadow: var(--p-shadow-hover);
}
.pv-bar {
display: flex;
align-items: center;
gap: 6px;
height: 34px;
padding: 0 14px;
background: var(--p-surface);
border-bottom: 1px solid var(--p-line);
}
.pv-bar i {
width: 9px;
height: 9px;
border-radius: 50%;
background: var(--p-line-strong);
}
.pv-frame img {
display: block;
width: 100%;
height: auto;
}
.pv-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10px;
min-height: 380px;
color: var(--p-ink-3);
}
.pv-main[data-group='app'] .pv-empty {
min-height: 420px;
}
.pv-empty-text {
font-size: 13px;
font-weight: 500;
}
.pv-empty-caption {
font-size: 13px;
color: var(--p-ink-2);
}
.pv-caption {
margin: 14px 0 0;
text-align: center;
font-size: 13px;
color: var(--p-ink-3);
}
.pv-thumbs {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 10px;
margin-top: 22px;
}
.pv-thumb {
display: flex;
align-items: center;
justify-content: center;
width: 76px;
height: 52px;
padding: 0;
overflow: hidden;
background: var(--p-surface-alt);
border: 1px solid var(--p-line-strong);
border-radius: 8px;
cursor: pointer;
transition: border-color 0.15s, transform 0.15s;
}
.pv-thumb:hover {
transform: translateY(-1px);
border-color: var(--p-ink-3);
}
.pv-thumb.is-active {
border-color: var(--p-accent);
box-shadow: 0 0 0 2px var(--p-accent-soft);
}
.pv-thumb img {
width: 100%;
height: 100%;
object-fit: cover;
}
.pv-thumb-ph {
font-family: var(--vp-font-family-mono);
font-size: 12px;
color: var(--p-ink-3);
}
.pv-note {
margin: 20px 0 0;
text-align: center;
font-size: 12.5px;
color: var(--p-ink-3);
}
@media (max-width: 720px) {
.pv-empty {
min-height: 220px;
}
.pv-thumb {
width: 62px;
height: 44px;
}
}
</style>