|
|
@@ -1,98 +1,761 @@
|
|
|
<script setup lang="ts">
|
|
|
-import { onMounted, reactive, computed } from "vue";
|
|
|
+import { computed, onMounted, reactive, ref } from "vue";
|
|
|
+import { useRoute, useRouter } from "vue-router";
|
|
|
import { useUserStore } from "@/store/modules/user";
|
|
|
-import { printHistory, productList, workorderList } from "@/api/ps";
|
|
|
+import { factoryBoard, homeBlocks } from "@/api/ps";
|
|
|
+import { getToken } from "@/utils/auth";
|
|
|
import InstallLodopButton from "@/components/InstallLodopButton.vue";
|
|
|
+import { useSiteStore } from "@/store/modules/site";
|
|
|
|
|
|
defineOptions({
|
|
|
name: "Welcome"
|
|
|
});
|
|
|
|
|
|
+type HomeBlock = {
|
|
|
+ id?: number;
|
|
|
+ type: string;
|
|
|
+ name?: string;
|
|
|
+ kicker?: string;
|
|
|
+ title?: string;
|
|
|
+ subtitle?: string;
|
|
|
+ content?: Record<string, any>;
|
|
|
+};
|
|
|
+
|
|
|
+const router = useRouter();
|
|
|
+const route = useRoute();
|
|
|
const store = useUserStore();
|
|
|
+const isPublic = computed(() => route.path === "/");
|
|
|
+const loggedIn = computed(() => !!getToken()?.accessToken);
|
|
|
const factory = computed(() => store.factory || {});
|
|
|
-const counts = reactive({ product: 0, workorder: 0, print: 0 });
|
|
|
+const platform = reactive({
|
|
|
+ factory_count: 0,
|
|
|
+ print_today: 0,
|
|
|
+ print_total: 0,
|
|
|
+ template_count: 0
|
|
|
+});
|
|
|
+const mine = reactive({
|
|
|
+ score: 0,
|
|
|
+ product: 0,
|
|
|
+ workorder: 0,
|
|
|
+ print: 0
|
|
|
+});
|
|
|
+const site = useSiteStore();
|
|
|
+const blocks = ref<HomeBlock[]>([]);
|
|
|
+const brandName = computed(() => site.title);
|
|
|
+const firstStepsIndex = computed(() =>
|
|
|
+ blocks.value.findIndex(item => item.type === "steps")
|
|
|
+);
|
|
|
+const firstFeaturesIndex = computed(() =>
|
|
|
+ blocks.value.findIndex(item => item.type === "features")
|
|
|
+);
|
|
|
+const heroMetrics = computed(() => {
|
|
|
+ if (loggedIn.value) {
|
|
|
+ return [
|
|
|
+ { label: "本厂工单", value: formatNum(mine.workorder), tag: "本厂" },
|
|
|
+ { label: "产品工艺", value: formatNum(mine.product), tag: "档案" },
|
|
|
+ { label: "累计套打", value: formatNum(mine.print), tag: "记录" },
|
|
|
+ { label: "工厂积分", value: formatNum(mine.score), tag: "积分" }
|
|
|
+ ];
|
|
|
+ }
|
|
|
+ return [
|
|
|
+ { label: "入驻厂家", value: formatNum(platform.factory_count), tag: "实时" },
|
|
|
+ { label: "今日打印", value: formatNum(platform.print_today), tag: "今日" },
|
|
|
+ { label: "累计套打", value: formatNum(platform.print_total), tag: "累计" },
|
|
|
+ { label: "可用模板", value: formatNum(platform.template_count), tag: "模板" }
|
|
|
+ ];
|
|
|
+});
|
|
|
|
|
|
onMounted(async () => {
|
|
|
try {
|
|
|
- await store.loadFactory();
|
|
|
- const [p, w, j] = await Promise.all([
|
|
|
- productList({ limit: 1 }),
|
|
|
- workorderList({ limit: 1 }),
|
|
|
- printHistory({ limit: 1 })
|
|
|
- ]);
|
|
|
- counts.product = p.total || 0;
|
|
|
- counts.workorder = w.total || 0;
|
|
|
- counts.print = j.total || 0;
|
|
|
+ if (loggedIn.value) {
|
|
|
+ await store.loadFactory();
|
|
|
+ }
|
|
|
+ const [board, list] = await Promise.all([factoryBoard(), homeBlocks()]);
|
|
|
+ Object.assign(platform, board.platform || {});
|
|
|
+ Object.assign(mine, board.mine || {});
|
|
|
+ blocks.value = Array.isArray(list) ? list : [];
|
|
|
+ site.applyFromBlocks(blocks.value);
|
|
|
} catch (e) {
|
|
|
/* 具体错误已由请求拦截器提示 */
|
|
|
}
|
|
|
});
|
|
|
+
|
|
|
+function formatNum(n: number) {
|
|
|
+ const v = Number(n) || 0;
|
|
|
+ if (v >= 10000) {
|
|
|
+ return (v / 10000).toFixed(v % 10000 === 0 ? 0 : 1) + "万";
|
|
|
+ }
|
|
|
+ return String(v);
|
|
|
+}
|
|
|
+
|
|
|
+function go(path: string) {
|
|
|
+ if (!loggedIn.value && path !== "/login" && path !== "/register" && !path.startsWith("/register")) {
|
|
|
+ router.push("/login");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ router.push(path);
|
|
|
+}
|
|
|
+
|
|
|
+function scrollTo(id: string) {
|
|
|
+ document.getElementById(id)?.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
|
+}
|
|
|
+
|
|
|
+function goAction(path?: string) {
|
|
|
+ const target = String(path || "").trim();
|
|
|
+ if (!target) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (target.startsWith("#")) {
|
|
|
+ scrollTo(target.slice(1));
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ go(target);
|
|
|
+}
|
|
|
+
|
|
|
+function blockItems(block: HomeBlock) {
|
|
|
+ return Array.isArray(block?.content?.items) ? block.content.items : [];
|
|
|
+}
|
|
|
+
|
|
|
+function blockNotes(block: HomeBlock) {
|
|
|
+ return Array.isArray(block?.content?.notes) ? block.content.notes : [];
|
|
|
+}
|
|
|
+
|
|
|
+function displayStat(item: Record<string, any>) {
|
|
|
+ const raw = String(item?.value || "").trim();
|
|
|
+ if (raw) {
|
|
|
+ return raw;
|
|
|
+ }
|
|
|
+ const key = String(item?.key || "");
|
|
|
+ if (key && Object.prototype.hasOwnProperty.call(platform, key)) {
|
|
|
+ return formatNum((platform as Record<string, number>)[key]);
|
|
|
+ }
|
|
|
+ return "0";
|
|
|
+}
|
|
|
+
|
|
|
+function isAnchor(path?: string) {
|
|
|
+ return String(path || "").trim().startsWith("#");
|
|
|
+}
|
|
|
</script>
|
|
|
|
|
|
<template>
|
|
|
- <div class="cards">
|
|
|
- <el-row :gutter="16">
|
|
|
- <el-col :span="6">
|
|
|
- <div class="stat">
|
|
|
- <div class="num">{{ factory.score || 0 }}</div>
|
|
|
- <div class="label">工厂积分</div>
|
|
|
+ <div class="home" :class="{ embedded: !isPublic, public: isPublic }">
|
|
|
+ <header v-if="isPublic" class="pub-nav">
|
|
|
+ <div class="wrap pub-nav-inner">
|
|
|
+ <div class="pub-brand">{{ brandName }}</div>
|
|
|
+ <nav class="pub-links">
|
|
|
+ <button
|
|
|
+ v-if="firstStepsIndex >= 0"
|
|
|
+ type="button"
|
|
|
+ class="pub-link"
|
|
|
+ @click="scrollTo('home-flow')"
|
|
|
+ >
|
|
|
+ 怎么用
|
|
|
+ </button>
|
|
|
+ <button
|
|
|
+ v-if="firstFeaturesIndex >= 0"
|
|
|
+ type="button"
|
|
|
+ class="pub-link"
|
|
|
+ @click="scrollTo('home-features')"
|
|
|
+ >
|
|
|
+ 能做什么
|
|
|
+ </button>
|
|
|
+ </nav>
|
|
|
+ <div class="pub-actions">
|
|
|
+ <button type="button" class="btn-text" @click="go('/login')">登录</button>
|
|
|
+ <button type="button" class="btn-ghost" @click="go('/register')">注册工厂</button>
|
|
|
+ <button type="button" class="btn-solid" @click="go('/register?mode=join')">加入工厂</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </header>
|
|
|
+
|
|
|
+ <template v-for="(block, index) in blocks" :key="block.id || index">
|
|
|
+ <section v-if="block.type === 'hero'" class="hero">
|
|
|
+ <div class="wrap hero-copy">
|
|
|
+ <p v-if="block.content?.eyebrow" class="eyebrow">{{ block.content.eyebrow }}</p>
|
|
|
+ <h1>
|
|
|
+ <span class="brand">{{ block.content?.brand || block.title || brandName }}</span>
|
|
|
+ <span v-if="block.subtitle" class="hero-title">{{ block.subtitle }}</span>
|
|
|
+ </h1>
|
|
|
+ <div v-if="blockNotes(block).length" class="pills">
|
|
|
+ <span v-for="note in blockNotes(block)" :key="note" class="pill">{{ note }}</span>
|
|
|
+ </div>
|
|
|
+ <p v-if="block.content?.lead" class="lead">{{ block.content.lead }}</p>
|
|
|
+ <div class="hero-actions">
|
|
|
+ <button
|
|
|
+ v-if="block.content?.btn_primary_text"
|
|
|
+ type="button"
|
|
|
+ class="btn-solid btn-lg"
|
|
|
+ @click="goAction(block.content.btn_primary_path)"
|
|
|
+ >
|
|
|
+ {{ block.content.btn_primary_text }}
|
|
|
+ </button>
|
|
|
+ <button
|
|
|
+ v-if="block.content?.btn_secondary_text"
|
|
|
+ type="button"
|
|
|
+ class="btn-ghost btn-lg"
|
|
|
+ @click="goAction(block.content.btn_secondary_path)"
|
|
|
+ >
|
|
|
+ {{ block.content.btn_secondary_text }}
|
|
|
+ <span v-if="isAnchor(block.content.btn_secondary_path)" class="btn-arrow">↓</span>
|
|
|
+ </button>
|
|
|
+ <InstallLodopButton v-if="!isPublic" />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="wrap">
|
|
|
+ <div class="hero-stage">
|
|
|
+ <div class="stage-head">
|
|
|
+ <span class="stage-live" />
|
|
|
+ <span>{{ loggedIn ? factory.name || "本厂看板" : block.content?.panel_title || "今天的车间" }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="stage-table">
|
|
|
+ <div v-for="item in heroMetrics" :key="item.label" class="stage-row">
|
|
|
+ <span class="stage-name">{{ item.label }}</span>
|
|
|
+ <span class="stage-value">{{ item.value }}</span>
|
|
|
+ <span class="stage-tag">{{ item.tag }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <section v-else-if="block.type === 'stats'" class="stats-band">
|
|
|
+ <div class="wrap stats">
|
|
|
+ <div v-for="(item, itemIndex) in blockItems(block)" :key="item.label || itemIndex" class="stat">
|
|
|
+ <div class="stat-num">{{ displayStat(item) }}</div>
|
|
|
+ <div class="stat-label">{{ item.label }}</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <section
|
|
|
+ v-else-if="block.type === 'steps'"
|
|
|
+ class="block block-flow"
|
|
|
+ :id="index === firstStepsIndex ? 'home-flow' : `home-flow-${block.id || index}`"
|
|
|
+ >
|
|
|
+ <div class="wrap">
|
|
|
+ <div class="block-head">
|
|
|
+ <p v-if="block.kicker" class="kicker">{{ block.kicker }}</p>
|
|
|
+ <h2 v-if="block.title">{{ block.title }}</h2>
|
|
|
+ <p v-if="block.subtitle" class="block-desc">{{ block.subtitle }}</p>
|
|
|
+ </div>
|
|
|
+ <div class="steps">
|
|
|
+ <article v-for="(item, itemIndex) in blockItems(block)" :key="item.title || itemIndex" class="step">
|
|
|
+ <span class="step-no">{{ String(itemIndex + 1).padStart(2, "0") }}</span>
|
|
|
+ <h3>{{ item.title }}</h3>
|
|
|
+ <p>{{ item.desc }}</p>
|
|
|
+ </article>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
- </el-col>
|
|
|
- <el-col :span="6">
|
|
|
- <div class="stat">
|
|
|
- <div class="num">{{ counts.product }}</div>
|
|
|
- <div class="label">产品</div>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <section
|
|
|
+ v-else-if="block.type === 'features'"
|
|
|
+ class="block block-features"
|
|
|
+ :id="index === firstFeaturesIndex ? 'home-features' : undefined"
|
|
|
+ >
|
|
|
+ <div class="wrap">
|
|
|
+ <div class="block-head">
|
|
|
+ <p v-if="block.kicker" class="kicker">{{ block.kicker }}</p>
|
|
|
+ <h2 v-if="block.title">{{ block.title }}</h2>
|
|
|
+ <p v-if="block.subtitle" class="block-desc">{{ block.subtitle }}</p>
|
|
|
+ </div>
|
|
|
+ <div class="features">
|
|
|
+ <article
|
|
|
+ v-for="(item, itemIndex) in blockItems(block)"
|
|
|
+ :key="item.title || itemIndex"
|
|
|
+ class="feature"
|
|
|
+ @click="goAction(item.path)"
|
|
|
+ >
|
|
|
+ <span v-if="item.tag" class="feature-tag">{{ item.tag }}</span>
|
|
|
+ <h3>{{ item.title }}</h3>
|
|
|
+ <p>{{ item.desc }}</p>
|
|
|
+ </article>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
- </el-col>
|
|
|
- <el-col :span="6">
|
|
|
- <div class="stat">
|
|
|
- <div class="num">{{ counts.workorder }}</div>
|
|
|
- <div class="label">工单</div>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <section v-else-if="block.type === 'cta'" class="cta-band">
|
|
|
+ <div class="wrap cta">
|
|
|
+ <h2 v-if="block.title">{{ block.title }}</h2>
|
|
|
+ <p v-if="block.subtitle">{{ block.subtitle }}</p>
|
|
|
+ <div class="cta-actions">
|
|
|
+ <button
|
|
|
+ v-if="block.content?.btn_primary_text"
|
|
|
+ type="button"
|
|
|
+ class="btn-solid btn-lg"
|
|
|
+ @click="goAction(block.content.btn_primary_path)"
|
|
|
+ >
|
|
|
+ {{ block.content.btn_primary_text }}
|
|
|
+ </button>
|
|
|
+ <button
|
|
|
+ v-if="block.content?.btn_secondary_text"
|
|
|
+ type="button"
|
|
|
+ class="btn-ghost btn-lg"
|
|
|
+ @click="goAction(block.content.btn_secondary_path)"
|
|
|
+ >
|
|
|
+ {{ block.content.btn_secondary_text }}
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
- </el-col>
|
|
|
- <el-col :span="6">
|
|
|
- <div class="stat">
|
|
|
- <div class="num">{{ counts.print }}</div>
|
|
|
- <div class="label">打印任务</div>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <section v-else class="block block-custom">
|
|
|
+ <div class="wrap">
|
|
|
+ <div class="block-head">
|
|
|
+ <p v-if="block.kicker" class="kicker">{{ block.kicker }}</p>
|
|
|
+ <h2 v-if="block.title">{{ block.title }}</h2>
|
|
|
+ <p v-if="block.subtitle" class="block-desc">{{ block.subtitle }}</p>
|
|
|
+ </div>
|
|
|
+ <div v-if="block.content?.body" class="custom-body">{{ block.content.body }}</div>
|
|
|
+ <div v-if="block.content?.btn_text" class="custom-actions">
|
|
|
+ <button type="button" class="btn-solid" @click="goAction(block.content.btn_path)">
|
|
|
+ {{ block.content.btn_text }}
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
- </el-col>
|
|
|
- </el-row>
|
|
|
- <div class="page-card" style="margin-top: 16px">
|
|
|
- <h3>快捷操作</h3>
|
|
|
- <el-space wrap>
|
|
|
- <el-button type="primary" @click="$router.push('/workorders/edit')"
|
|
|
- >新建工单并套打</el-button
|
|
|
- >
|
|
|
- <el-button @click="$router.push('/products/edit')">新建产品</el-button>
|
|
|
- <el-button type="success" @click="$router.push('/templates')"
|
|
|
- >本厂模板</el-button
|
|
|
- >
|
|
|
- <InstallLodopButton />
|
|
|
- </el-space>
|
|
|
- <p class="tip">
|
|
|
- 先安装本机打印脚本(CLodop),再到工单列表点「套打」。工单信息会填进模板空白格。
|
|
|
- </p>
|
|
|
- </div>
|
|
|
+ </section>
|
|
|
+ </template>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<style scoped>
|
|
|
+.home {
|
|
|
+ color: #334155;
|
|
|
+ background: #fff;
|
|
|
+}
|
|
|
+.home.embedded {
|
|
|
+ margin: -24px;
|
|
|
+ min-height: calc(100vh - 120px);
|
|
|
+}
|
|
|
+.home.public {
|
|
|
+ min-height: 100vh;
|
|
|
+}
|
|
|
+.wrap {
|
|
|
+ width: min(1120px, calc(100% - 80px));
|
|
|
+ margin-left: auto;
|
|
|
+ margin-right: auto;
|
|
|
+}
|
|
|
+
|
|
|
+.pub-nav {
|
|
|
+ position: sticky;
|
|
|
+ top: 0;
|
|
|
+ z-index: 20;
|
|
|
+ background: rgba(255, 255, 255, 0.92);
|
|
|
+ backdrop-filter: blur(16px);
|
|
|
+ border-bottom: 1px solid rgba(15, 23, 42, 0.06);
|
|
|
+}
|
|
|
+.pub-nav-inner {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ gap: 24px;
|
|
|
+ min-height: 72px;
|
|
|
+}
|
|
|
+.pub-brand {
|
|
|
+ font-size: 18px;
|
|
|
+ font-weight: 700;
|
|
|
+ letter-spacing: 0.02em;
|
|
|
+ color: #0f172a;
|
|
|
+}
|
|
|
+.pub-links {
|
|
|
+ display: flex;
|
|
|
+ gap: 8px;
|
|
|
+ flex: 1;
|
|
|
+ justify-content: center;
|
|
|
+}
|
|
|
+.pub-link {
|
|
|
+ border: 0;
|
|
|
+ background: none;
|
|
|
+ padding: 8px 14px;
|
|
|
+ color: #64748b;
|
|
|
+ font-size: 14px;
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+.pub-link:hover {
|
|
|
+ color: #0f172a;
|
|
|
+}
|
|
|
+.pub-actions {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ align-items: center;
|
|
|
+ gap: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.btn-solid,
|
|
|
+.btn-ghost,
|
|
|
+.btn-text {
|
|
|
+ border-radius: 999px;
|
|
|
+ cursor: pointer;
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 600;
|
|
|
+ line-height: 1;
|
|
|
+}
|
|
|
+.btn-text {
|
|
|
+ border: 0;
|
|
|
+ background: none;
|
|
|
+ padding: 10px 8px;
|
|
|
+ color: #334155;
|
|
|
+}
|
|
|
+.btn-solid {
|
|
|
+ border: 0;
|
|
|
+ background: #4f6ef7;
|
|
|
+ color: #fff;
|
|
|
+ padding: 12px 20px;
|
|
|
+ box-shadow: 0 8px 20px rgba(79, 110, 247, 0.22);
|
|
|
+}
|
|
|
+.btn-solid:hover {
|
|
|
+ background: #3d5de8;
|
|
|
+}
|
|
|
+.btn-ghost {
|
|
|
+ border: 1px solid #e2e8f0;
|
|
|
+ background: #fff;
|
|
|
+ color: #0f172a;
|
|
|
+ padding: 11px 18px;
|
|
|
+}
|
|
|
+.btn-ghost:hover {
|
|
|
+ border-color: #cbd5e1;
|
|
|
+ background: #f8fafc;
|
|
|
+}
|
|
|
+.btn-lg {
|
|
|
+ padding: 16px 26px;
|
|
|
+ font-size: 15px;
|
|
|
+}
|
|
|
+.btn-arrow {
|
|
|
+ margin-left: 8px;
|
|
|
+ font-size: 13px;
|
|
|
+}
|
|
|
+
|
|
|
+.hero {
|
|
|
+ padding: 72px 0 24px;
|
|
|
+}
|
|
|
+.hero-copy {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ text-align: center;
|
|
|
+ padding-bottom: 56px;
|
|
|
+}
|
|
|
+.eyebrow {
|
|
|
+ margin: 0 0 20px;
|
|
|
+ color: #4f6ef7;
|
|
|
+ font-size: 13px;
|
|
|
+ font-weight: 700;
|
|
|
+ letter-spacing: 0.16em;
|
|
|
+}
|
|
|
+h1 {
|
|
|
+ margin: 0;
|
|
|
+ max-width: 16em;
|
|
|
+}
|
|
|
+.brand {
|
|
|
+ display: block;
|
|
|
+ color: #4f6ef7;
|
|
|
+ font-size: clamp(40px, 5.4vw, 64px);
|
|
|
+ font-weight: 760;
|
|
|
+ line-height: 1.12;
|
|
|
+ letter-spacing: -0.03em;
|
|
|
+}
|
|
|
+.hero-title {
|
|
|
+ display: block;
|
|
|
+ margin-top: 16px;
|
|
|
+ color: #0f172a;
|
|
|
+ font-size: clamp(28px, 3.6vw, 42px);
|
|
|
+ font-weight: 650;
|
|
|
+ line-height: 1.28;
|
|
|
+ letter-spacing: -0.03em;
|
|
|
+}
|
|
|
+.pills {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ justify-content: center;
|
|
|
+ gap: 10px;
|
|
|
+ margin-top: 28px;
|
|
|
+}
|
|
|
+.pill {
|
|
|
+ padding: 8px 14px;
|
|
|
+ border: 1px solid #e8ecf3;
|
|
|
+ border-radius: 999px;
|
|
|
+ background: #fff;
|
|
|
+ color: #475569;
|
|
|
+ font-size: 13px;
|
|
|
+}
|
|
|
+.lead {
|
|
|
+ max-width: 36em;
|
|
|
+ margin: 28px 0 0;
|
|
|
+ color: #64748b;
|
|
|
+ font-size: 16px;
|
|
|
+ line-height: 1.9;
|
|
|
+}
|
|
|
+.hero-actions {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ justify-content: center;
|
|
|
+ gap: 12px;
|
|
|
+ align-items: center;
|
|
|
+ margin-top: 36px;
|
|
|
+}
|
|
|
+
|
|
|
+.hero-stage {
|
|
|
+ overflow: hidden;
|
|
|
+ border: 1px solid #e8ecf3;
|
|
|
+ border-radius: 20px;
|
|
|
+ background: #fff;
|
|
|
+ box-shadow: 0 24px 60px rgba(15, 23, 42, 0.06);
|
|
|
+}
|
|
|
+.stage-head {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 10px;
|
|
|
+ padding: 22px 28px;
|
|
|
+ border-bottom: 1px solid #eef1f6;
|
|
|
+ color: #64748b;
|
|
|
+ font-size: 14px;
|
|
|
+}
|
|
|
+.stage-live {
|
|
|
+ width: 8px;
|
|
|
+ height: 8px;
|
|
|
+ border-radius: 50%;
|
|
|
+ background: #22c55e;
|
|
|
+ box-shadow: 0 0 0 4px rgba(34, 197, 94, 0.12);
|
|
|
+}
|
|
|
+.stage-table {
|
|
|
+ padding: 8px 12px 16px;
|
|
|
+}
|
|
|
+.stage-row {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: minmax(0, 1fr) auto auto;
|
|
|
+ gap: 16px;
|
|
|
+ align-items: center;
|
|
|
+ min-height: 64px;
|
|
|
+ padding: 8px 16px;
|
|
|
+ border-radius: 12px;
|
|
|
+}
|
|
|
+.stage-row + .stage-row {
|
|
|
+ border-top: 1px solid #f1f5f9;
|
|
|
+}
|
|
|
+.stage-name {
|
|
|
+ color: #0f172a;
|
|
|
+ font-size: 15px;
|
|
|
+ font-weight: 600;
|
|
|
+}
|
|
|
+.stage-value {
|
|
|
+ color: #0f172a;
|
|
|
+ font-size: 20px;
|
|
|
+ font-weight: 700;
|
|
|
+ letter-spacing: -0.03em;
|
|
|
+}
|
|
|
+.stage-tag {
|
|
|
+ min-width: 52px;
|
|
|
+ padding: 4px 10px;
|
|
|
+ border-radius: 999px;
|
|
|
+ background: #ecfdf3;
|
|
|
+ color: #15803d;
|
|
|
+ font-size: 12px;
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+
|
|
|
+.stats-band {
|
|
|
+ padding: 28px 0 12px;
|
|
|
+}
|
|
|
+.stats {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
|
|
+ gap: 8px;
|
|
|
+ padding: 12px 0 8px;
|
|
|
+}
|
|
|
.stat {
|
|
|
- background: var(--el-bg-color);
|
|
|
- border-radius: 10px;
|
|
|
- padding: 20px;
|
|
|
- box-shadow: 0 8px 24px rgb(29 43 58 / 6%);
|
|
|
+ padding: 28px 16px 20px;
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+.stat-num {
|
|
|
+ font-size: 32px;
|
|
|
+ font-weight: 720;
|
|
|
+ letter-spacing: -0.04em;
|
|
|
+ color: #0f172a;
|
|
|
+}
|
|
|
+.stat-label {
|
|
|
+ margin-top: 8px;
|
|
|
+ color: #94a3b8;
|
|
|
+ font-size: 13px;
|
|
|
+}
|
|
|
+
|
|
|
+.block {
|
|
|
+ padding: 96px 0;
|
|
|
+ scroll-margin-top: 88px;
|
|
|
+}
|
|
|
+.block-flow,
|
|
|
+.block-custom {
|
|
|
+ background: #f8fafc;
|
|
|
+}
|
|
|
+.block-features {
|
|
|
+ background: #fff;
|
|
|
+}
|
|
|
+.block-head {
|
|
|
+ max-width: 640px;
|
|
|
+ margin-bottom: 48px;
|
|
|
+}
|
|
|
+.kicker {
|
|
|
+ margin: 0 0 12px;
|
|
|
+ color: #4f6ef7;
|
|
|
+ font-size: 13px;
|
|
|
+ font-weight: 700;
|
|
|
+ letter-spacing: 0.12em;
|
|
|
+}
|
|
|
+h2 {
|
|
|
+ margin: 0;
|
|
|
+ font-size: clamp(28px, 3vw, 36px);
|
|
|
+ font-weight: 700;
|
|
|
+ letter-spacing: -0.03em;
|
|
|
+ color: #0f172a;
|
|
|
+}
|
|
|
+.block-desc {
|
|
|
+ margin: 16px 0 0;
|
|
|
+ color: #64748b;
|
|
|
+ font-size: 16px;
|
|
|
+ line-height: 1.85;
|
|
|
+}
|
|
|
+
|
|
|
+.steps,
|
|
|
+.features {
|
|
|
+ display: grid;
|
|
|
+ gap: 20px;
|
|
|
+}
|
|
|
+.steps {
|
|
|
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
|
|
+}
|
|
|
+.features {
|
|
|
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
|
|
+}
|
|
|
+.step,
|
|
|
+.feature {
|
|
|
+ padding: 32px 28px;
|
|
|
+ border: 1px solid #eef1f6;
|
|
|
+ border-radius: 24px;
|
|
|
+ background: #fff;
|
|
|
+}
|
|
|
+.feature {
|
|
|
+ cursor: pointer;
|
|
|
+ transition: transform 0.18s ease, box-shadow 0.18s ease;
|
|
|
}
|
|
|
-.num {
|
|
|
+.feature:hover {
|
|
|
+ transform: translateY(-4px);
|
|
|
+ box-shadow: 0 18px 40px rgba(15, 23, 42, 0.06);
|
|
|
+}
|
|
|
+.step-no {
|
|
|
+ display: block;
|
|
|
+ margin-bottom: 28px;
|
|
|
+ color: #cbd5e1;
|
|
|
font-size: 28px;
|
|
|
font-weight: 700;
|
|
|
+ letter-spacing: -0.04em;
|
|
|
+}
|
|
|
+.feature-tag {
|
|
|
+ display: inline-flex;
|
|
|
+ margin-bottom: 18px;
|
|
|
+ padding: 6px 12px;
|
|
|
+ border-radius: 999px;
|
|
|
+ background: #eef2ff;
|
|
|
+ color: #4f6ef7;
|
|
|
+ font-size: 12px;
|
|
|
+ font-weight: 700;
|
|
|
+}
|
|
|
+.step h3,
|
|
|
+.feature h3 {
|
|
|
+ margin: 0 0 12px;
|
|
|
+ font-size: 18px;
|
|
|
+ font-weight: 650;
|
|
|
+ color: #0f172a;
|
|
|
}
|
|
|
-.label {
|
|
|
- color: var(--el-text-color-secondary);
|
|
|
- margin-top: 6px;
|
|
|
+.step p,
|
|
|
+.feature p {
|
|
|
+ margin: 0;
|
|
|
+ color: #64748b;
|
|
|
+ font-size: 14px;
|
|
|
+ line-height: 1.85;
|
|
|
}
|
|
|
-.tip {
|
|
|
- color: var(--el-text-color-secondary);
|
|
|
+
|
|
|
+.custom-body {
|
|
|
+ max-width: 720px;
|
|
|
+ color: #64748b;
|
|
|
+ font-size: 16px;
|
|
|
+ line-height: 1.9;
|
|
|
+ white-space: pre-wrap;
|
|
|
+}
|
|
|
+.custom-actions {
|
|
|
+ margin-top: 28px;
|
|
|
+}
|
|
|
+
|
|
|
+.cta-band {
|
|
|
+ padding: 96px 0 112px;
|
|
|
+ background: #fff;
|
|
|
+}
|
|
|
+.cta {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ text-align: center;
|
|
|
+ gap: 0;
|
|
|
+}
|
|
|
+.cta h2 {
|
|
|
+ font-size: clamp(28px, 3vw, 40px);
|
|
|
+}
|
|
|
+.cta p {
|
|
|
+ max-width: 28em;
|
|
|
margin: 16px 0 0;
|
|
|
+ color: #64748b;
|
|
|
+ font-size: 16px;
|
|
|
+ line-height: 1.8;
|
|
|
+}
|
|
|
+.cta-actions {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ justify-content: center;
|
|
|
+ gap: 12px;
|
|
|
+ margin-top: 32px;
|
|
|
+}
|
|
|
+
|
|
|
+@media (max-width: 900px) {
|
|
|
+ .pub-links {
|
|
|
+ display: none;
|
|
|
+ }
|
|
|
+ .wrap {
|
|
|
+ width: min(1120px, calc(100% - 40px));
|
|
|
+ }
|
|
|
+ .hero {
|
|
|
+ padding-top: 48px;
|
|
|
+ }
|
|
|
+ .hero-copy {
|
|
|
+ padding-bottom: 36px;
|
|
|
+ }
|
|
|
+ .block,
|
|
|
+ .cta-band {
|
|
|
+ padding: 64px 0;
|
|
|
+ }
|
|
|
+ .pills {
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+@media (max-width: 768px) {
|
|
|
+ .home.embedded {
|
|
|
+ margin: -16px;
|
|
|
+ }
|
|
|
+ .pub-nav-inner {
|
|
|
+ min-height: 64px;
|
|
|
+ }
|
|
|
+ .hero-title {
|
|
|
+ font-size: 26px;
|
|
|
+ }
|
|
|
+ .stage-row {
|
|
|
+ grid-template-columns: minmax(0, 1fr) auto;
|
|
|
+ }
|
|
|
+ .stage-tag {
|
|
|
+ display: none;
|
|
|
+ }
|
|
|
+ .steps,
|
|
|
+ .features,
|
|
|
+ .stats {
|
|
|
+ grid-template-columns: 1fr;
|
|
|
+ }
|
|
|
}
|
|
|
</style>
|