Files
spark-store-abyss/app.vue
momen be8c0d518f 添加商业版页面 (#4)
* update:添加商业版内容

* update:添加插画并删除多余子版块

* update:加入商业版多发行版信息

* update:增加多cpu厂家标签

* update:商业版主页描述更新

* update:商业版标签前移到下载后面

* update:更新商业版内容“联系客服”按钮点击后的触发逻辑。展示联系方式并调用系统邮箱

* update:“关于页面”添加加入我们按钮

* update:使用@nuxt/icon引入SVG图标

* fix:修复竖屏设备导航栏由于增加”商业版“标签导致“GXDE OS"标签出界问题

* update:删除残留vue文件

* update:加入成果案例到商业版板块

* fix:修复790.29px*780px时,导航栏标签文字换行的问题

* fix:竖屏画面导航栏高度改为h-93.5

* update:更新商业版svg配图

* Standardize nav-link class styling

Removed padding from nav-link classes for consistency.

* fix(nav): 延后导航单行排布断点至 lg

修复视口在 780px~840px 区间时,因导航项增加导致的元素溢出和交叠问题。目前在设备大于 1024px 时才会并排显示导航栏项。

* style(commercial): 彻底重塑商业版前端插画与界面,并适配深色模式

1. 重绘场景插画:重新设计了“案例”、“客服”、“定制化”、“架构”、“分发”等 5 张大型场景 SVG 插图,升级为悬浮玻璃态面板、阵列多核管控板等高阶空间 UI 组合拼贴风,大幅提升商业严肃感与前瞻视觉。
2. 新增专属标题徽章:新设计 5 张针对各主体的立体厚重感微型徽章图(`*-title.svg`),替换原有的字体图标,增强每个模块的大型企服属性。
3. 清除硬编码与深色适配:地毯式拔除了插图内部强制锁色的 `<style>:root` 标签,并移除了用于高光的 `"#ffffff"` 死白硬层,无缝融入系统的深浅色动态 `var(--s-bg)` 等变量,适配完美的 Dark Mode 下暗色面板沉浸效果。
4. 界面版式对齐:重塑首屏第一区块(Section 1)的模拟 UI 板面为科技面板,收束卡片间距;重组了留板表单弹窗以及导航元素,令其样式表现与总应用的主界线全面拉齐。

---------

Co-authored-by: jiwangyihao <jwyh@jwyihao.top>
2026-03-04 13:42:02 +08:00

579 lines
18 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 ScrollPanel from "primevue/scrollpanel";
import Button from "primevue/button";
import { useToast } from "primevue/usetoast";
const appConfig = useAppConfig();
const path = computed(() => {
return useRoute().path;
});
const sProgress = ref(1);
const scrollPanel = useTemplateRef<ComponentPublicInstance>("scrollPanel");
const header = useTemplateRef<HTMLElement>("header");
const sX = ref(0);
const sY = ref(0);
const sWidth = ref(0);
const sHeight = ref(0);
const mounted = ref(false);
provide("scrollPanel", scrollPanel);
const handleScrollOrResize = () => {
const navEl = document.querySelector("header nav");
const el = document.querySelector("header .active");
if (el && navEl) {
sX.value = el.getBoundingClientRect().x - navEl.getBoundingClientRect().x;
sY.value = el.getBoundingClientRect().y - navEl.getBoundingClientRect().y;
sWidth.value = el.getBoundingClientRect().width;
sHeight.value = el.getBoundingClientRect().height;
}
const scrollTop = (scrollPanel.value as unknown as { lastScrollTop: number })
.lastScrollTop;
const clientHeight = scrollPanel.value?.$el.clientHeight;
if (path.value === "/") {
sProgress.value =
1 -
range(0, 1, scrollTop / clientHeight) +
1 -
Math.abs(range(-1, 1, scrollTop / clientHeight - 6));
return;
}
sProgress.value = 1 - range(0, 1, scrollTop / clientHeight);
};
const handleHeaderFocus = () => {
mounted.value = false;
setTimeout(() => {
handleScrollOrResize();
nextTick(() => {
mounted.value = true;
});
}, 300);
};
onMounted(() => {
addEventListener("resize", handleScrollOrResize);
watchEffect(handleScrollOrResize);
nextTick(() => {
mounted.value = true;
});
watchEffect(() => {
if (path.value) {
scrollPanel.value?.$el.firstChild.firstChild.scrollTo({
top: 0,
behavior: "instant",
});
}
});
});
const qrWidth = ref(0);
onMounted(() => {
const qrEl = document.querySelector("footer h2+svg");
if (qrEl) {
qrEl.addEventListener("resize", () => {
qrWidth.value = qrEl.getBoundingClientRect().height;
});
qrWidth.value = qrEl.getBoundingClientRect().height;
}
});
const sDarkConfig = ref("auto");
const isDarkMode = ref(false);
const sDark = computed(() => {
return sDarkConfig.value === "auto"
? isDarkMode.value
? true
: false
: sDarkConfig.value === "dark"
? true
: false;
});
provide("sDark", sDark);
const enableTransitions = () =>
"startViewTransition" in document &&
window.matchMedia("(prefers-reduced-motion: no-preference)").matches;
const mouseX = ref(0);
const mouseY = ref(0);
const toggleDarkMode = async ({ clientX: x, clientY: y }: MouseEvent) => {
if (sDarkConfig.value === "auto") {
sDarkConfig.value = isDarkMode.value ? "light" : "dark";
} else if (sDarkConfig.value === "dark" && isDarkMode.value) {
sDarkConfig.value = "auto";
} else if (sDarkConfig.value === "light" && !isDarkMode.value) {
sDarkConfig.value = "auto";
} else {
sDarkConfig.value = isDarkMode.value ? "dark" : "light";
}
localStorage.setItem("darkMode", sDarkConfig.value);
mouseX.value = x;
mouseY.value = y;
};
onMounted(() => {
const schemeQuery = window.matchMedia("(prefers-color-scheme: dark)");
isDarkMode.value = schemeQuery.matches;
schemeQuery.addEventListener("change", (e) => {
isDarkMode.value = e.matches;
});
const savedDarkMode = localStorage.getItem("darkMode");
if (savedDarkMode) {
sDarkConfig.value = savedDarkMode;
}
watch(
sDark,
async (newValue, oldValue) => {
console.log("Dark mode changed:", newValue, oldValue);
const sDarkValue = newValue;
if (oldValue === undefined) {
return;
}
if (!enableTransitions()) {
document.documentElement.classList.toggle("s-dark", sDarkValue);
return;
}
const clipPath = [
`circle(0px at ${mouseX.value}px ${mouseY.value}px)`,
`circle(${Math.hypot(
Math.max(mouseX.value, innerWidth - mouseX.value),
Math.max(mouseY.value, innerHeight - mouseY.value),
)}px at ${mouseX.value}px ${mouseY.value}px)`,
];
await document.startViewTransition(async () => {
document.documentElement.classList.toggle("s-dark", sDarkValue);
await nextTick();
}).ready;
document.documentElement.animate(
{ clipPath: sDarkValue ? clipPath.reverse() : clipPath },
{
duration: 300,
easing: "ease-in",
pseudoElement: `::view-transition-${
sDarkValue ? "old" : "new"
}(root)`,
},
);
},
{ immediate: true },
);
});
// 数据获取
export interface Release {
assets: {
name: string;
browser_download_url: string;
}[];
tag_name: string;
created_at: string;
body: string;
}
const { data: latestRelease } = await useAsyncData(
"latestRelease",
async () => {
if (appConfig.latestRelease) {
return appConfig.latestRelease;
} else {
return await $fetch(
"https://gitee.com/api/v5/repos/spark-store-project/spark-store/releases/latest",
);
}
},
);
provide("latestRelease", latestRelease);
const dialog = ref(false);
const joinGroup = () => {
dialog.value = true;
window.open(
"https://qm.qq.com/cgi-bin/qm/qr?authKey=1Oz2h5JBFSxBsHM63bwY1x0OswtQ08SBKj%2Fw9CRTxZ9%2B8%2FRKNIEv35s%2FhjOdRqkj&k=e7gkRbeGAjTfGupzsWbSbI8mOJM3ZqHQ&noverify=0",
"_blank",
);
};
const toast = useToast();
const copyGroupNumber = async () => {
try {
await navigator.clipboard.writeText("424677882");
toast.add({
severity: "success",
summary: "复制成功",
detail: "QQ群号已复制到剪贴板",
life: 5000,
});
} catch {
toast.add({
severity: "error",
summary: "复制失败",
detail: "请手动复制QQ群号424677882",
life: 5000,
});
}
};
</script>
<template>
<ScrollPanel
ref="scrollPanel"
class="overflow-hidden"
style="width: 100vw; height: 100vh"
:pt="{
content: {
class: 'snap-y snap-mandatory scroll-smooth overflow-x-hidden!',
},
}"
>
<header
ref="header"
tabindex="0"
class="fixed w-full h-15 z-10 px-4 sm:px-8 lg:px-12 translate-y-[calc(var(--s-progress)*4*var(--spacing))] sm:translate-y-[calc(var(--s-progress)*8*var(--spacing))] lg:translate-y-[calc(var(--s-progress)*12*var(--spacing))] before:translate-x-[calc(var(--s-progress)*4*var(--spacing))] sm:before:translate-x-[calc(var(--s-progress)*8*var(--spacing))] lg:before:translate-x-[calc(var(--s-progress)*12*var(--spacing))] before:w-[calc(100%-var(--s-progress)*8*var(--spacing))] sm:before:w-[calc(100%-var(--s-progress)*16*var(--spacing))] lg:before:w-[calc(100%-var(--s-progress)*24*var(--spacing))] sm:h-auto focus-within:h-auto before:h-15 focus-within:before:h-93.5 sm:before:h-full focus-within:sm:before:h-full overflow-hidden focus-within:overflow-visible transition-discrete group"
:style="{ '--s-progress': sProgress }"
@click="if (!header?.matches(':focus-within')) header?.focus();"
@focus="handleHeaderFocus"
>
<nav
class="relative flex px-2.5 md:px-4 lg:px-8 items-center flex-col lg:flex-row before:opacity-0 group-focus-within:before:opacity-100 sm:before:opacity-100 before:origin-top before:scale-0 group-focus-within:before:scale-100 sm:before:scale-100 before:translate-x-[calc(50vw-14.5*var(--spacing))] before:translate-y-15 group-focus-within:before:translate-x-[calc(var(--s-x)*1px)] group-focus-within:before:translate-y-[calc(var(--s-y)*1px)] sm:before:translate-x-[calc(var(--s-x)*1px)] sm:before:translate-y-[calc(var(--s-y)*1px)]"
:class="{ mounted }"
:style="{
'--s-x': sX,
'--s-y': sY,
'--s-width': sWidth,
'--s-height': sHeight,
}"
>
<div
class="flex h-15 w-full sm:w-auto px-2 justify-between items-center"
>
<NuxtLink class="flex items-center">
<Icon
name="sp:spark"
class="text-4xl sm:mr-2 s-color-primary-color"
mode="svg"
/>
</NuxtLink>
<NuxtLink class="flex items-center">
<h1 class="font-(family-name:--s-title-font) text-surface-500">
SPARK
</h1>
</NuxtLink>
<div class="sm:hidden relative">
<span
class="pi pi-ellipsis-v text-surface-400 font-bold! text-xl! group-focus-within:opacity-0 group-focus-within:rotate-90 transition-all duration-300"
></span>
<span
class="absolute top-0 left-0 pi pi-times text-surface-400 font-bold! text-xl! opacity-0 -rotate-90 group-focus-within:opacity-100 group-focus-within:rotate-0 transition-all duration-300"
></span>
</div>
</div>
<div
class="flex grow pb-2.5 lg:pt-2.5 items-end sm:items-center flex-row-reverse sm:flex-row w-full sm:w-auto justify-between lg:justify-end opacity-0 group-focus-within:opacity-100 sm:opacity-100 origin-top scale-0 group-focus-within:scale-100 sm:scale-100 transition-transform duration-300"
>
<div
class="flex flex-col gap-1 mr-1.5 lg:mr-1 lg:gap-2 lg:mr-2 sm:flex-row items-end sm:items-center"
>
<NuxtLink to="/" class="nav-link" active-class="active">
首页
</NuxtLink>
<NuxtLink to="/download" class="nav-link" active-class="active">
下载
</NuxtLink>
<NuxtLink to="/commercial" class="nav-link" active-class="active">
商业版
</NuxtLink>
<a
href="https://bbs.spark-app.store/"
class="nav-link"
active-class="active"
>
社区
</a>
<a
href="https://wiki.spark-app.store/"
class="nav-link"
active-class="active"
>
帮助
</a>
<NuxtLink to="/about" class="nav-link" active-class="active">
关于
</NuxtLink>
<a href="https://gxde.top/" class="nav-link" active-class="active">
GXDE OS
</a>
</div>
<Button
:icon="`pi ${
sDarkConfig === 'auto'
? 'pi-bullseye'
: sDarkConfig === 'dark'
? 'pi-moon'
: 'pi-sun'
}`"
aria-label="Toggle Dark Mode"
size="small"
class="shrink-0 m-1.5 sm:m-0"
rounded
severity="secondary"
@click="toggleDarkMode"
/>
</div>
</nav>
</header>
<div>
<NuxtPage />
</div>
<footer
class="w-full flex justify-around bg-white snap-start overflow-hidden p-8 md:p-14 gap-4 dark:bg-surface-900 flex-col-reverse md:flex-row"
>
<div
class="flex md:flex-col justify-end items-center md:items-start gap-2 md:gap-6"
>
<Icon
name="sp:spark"
class="md:grow w-8 h-8 md:w-auto md:h-auto max-w-40 max-h-40"
mode="svg"
/>
<h2
class="font-(family-name:--s-title-font) text-base md:text-5xl text-surface-500 dark:text-surface-100"
>
SPARK
</h2>
<p class="grow text-end text-base md:text-2xl md:hidden">
© 2020-{{ new Date().getFullYear() }} 星火社区
</p>
</div>
<div class="flex flex-col gap-2 md:gap-4">
<h2 class="text-lg md:text-2xl font-bold dark:text-surface-100">
关于星火社区
</h2>
<p
class="text-sm md:text-base leading-loose text-surface-600 md:max-w-[25.8em] dark:text-surface-200"
>
星火社区是以丰富 Linux
应用生态为核心目标的开源协作平台汇聚年轻开发者团队核心项目星火应用商店通过整合分散资源提供海量软件下载及安装功能解决
Linux 用户获取应用难题社区以星火燎原为理念通过论坛Wiki
和形象 IP 等传播方式构建开放协作生态
</p>
</div>
<div class="flex-col items-end gap-4 lg:gap-8 hidden md:flex">
<div class="flex flex-col lg:flex-row grow items-end gap-2 lg:gap-6">
<h2 class="whitespace-nowrap text-right text-2xl font-bold">
加入星火应用商店<br />
用户交流群
</h2>
<Icon
name="s:qrcode-3"
class="s-color-[black] dark:s-color-[white]"
mode="svg"
:style="{
width: qrWidth + 'px',
height: '100%',
}"
/>
</div>
<p class="text-lg lg:text-2xl">
© 2020-{{ new Date().getFullYear() }} 星火社区
</p>
</div>
<button
class="md:hidden self-center flex items-center gap-2 px-6 py-2 text-lg font-bold text-primary-600 bg-primary-200 rounded-full hover:bg-primary-300 dark:text-primary-400 dark:bg-[rgba(from_var(--p-primary-500)r_g_b/0.3)] dark:hover:bg-[rgba(from_var(--p-primary-500)r_g_b/0.5)] cursor-pointer"
@click="joinGroup"
>
<Icon name="simple-icons:qq" class="text-2xl! font-bold!" />
点击这里加入用户交流群
</button>
<Dialog
v-model:visible="dialog"
modal
:show-header="false"
class="w-7/8 rounded-3xl!"
:style="{
'--p-dialog-border-color': 'transparent',
}"
content-class="flex flex-col items-center p-5! w-full h-full gap-4 overflow-hidden!"
:pt="{
mask: {
class:
'backdrop-blur-2xl s-bg-[rgba(255,255,255,0.4)] s-deco-surface-950 dark:s-bg-[rgba(0,0,0,0.4)] dark:s-deco-surface-100',
style: {
'--p-mask-background': 'var(--s-bg)',
'--p-dialog-color': 'var(--s-deco)',
},
},
}"
dismissable-mask
>
<p class="flex items-center justify-center gap-2 font-bold">
<Icon name="simple-icons:qq" class="text-2xl!" />
正在拉起手机 QQ
<i class="pi pi-spin pi-spinner font-bold!"></i>
</p>
<p class="text-sm text-surface-600">
如未响应可通过以下方式加入交流群
</p>
<Icon
name="s:qrcode-3"
class="s-color-primary-600 bg-primary-100 dark:s-color-primary-400 dark:bg-[rgba(from_var(--p-primary-500)r_g_b/0.3)] w-48 h-48 p-3 rounded-2xl"
mode="svg"
/>
<ol
class="text-sm flex flex-col gap-2 *:pl-6 *:relative *:before:content-[counter(qq)] *:before:counter-increment-[qq] *:before:absolute *:before:left-0 *:before:bg-primary-600 *:before:w-5 *:before:h-5 *:before:text-center *:before:leading-5 *:before:text-xs *:before:font-bold *:before:rounded-full *:before:text-white counter-reset-[qq]"
>
<li>
长按保存或截图上方群二维码启动
QQ点击右上角+-扫一扫-相册选中此二维码以识别申请加入群聊
</li>
<li>
搜索 QQ 群号<span
class="font-bold py-0.5 px-2 mx-1 bg-primary-100 text-primary-600 rounded-full dark:text-primary-400 dark:bg-[rgba(from_var(--p-primary-500)r_g_b/0.3)]"
>424677882</span
>
<i
class="pi pi-clone text-xs! bg-surface-50 hover:bg-surface-100 px-2 py-0.5 rounded-full font-bold! dark:bg-surface-800 dark:hover:bg-surface-700 dark:text-surface-300 cursor-pointer"
@click="copyGroupNumber"
></i>
</li>
</ol>
</Dialog>
<Toast />
</footer>
</ScrollPanel>
</template>
<style scoped lang="scss">
header {
transform: translateY();
transition: {
property: transform, overflow;
duration: 0.1s, 0s;
delay: 0s, 0.3s;
}
&::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
border-radius: calc(var(--s-progress) * var(--radius-2xl));
border-bottom-right-radius: calc(var(--s-progress) * var(--radius-2xl));
border-bottom-left-radius: calc(var(--s-progress) * var(--radius-2xl));
background-color: var(--p-surface-0);
backdrop-filter: blur(calc(4 * var(--spacing)));
z-index: -1;
transition: {
property: transform, width, border-radius, background-color, height;
duration: 0.1s, 0.1s, 0.1s, 0.3s, 0.3s;
}
}
&:focus-within::before,
&:hover::before {
background-color: rgba(from var(--p-surface-0) r g b / 0.8);
border-bottom-right-radius: var(--radius-2xl);
border-bottom-left-radius: var(--radius-2xl);
}
h1 {
font-size: 1.75em;
}
nav::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: calc(var(--s-width) * 1px);
height: calc(var(--s-height) * 1px);
background-color: var(--p-primary-200);
border-radius: calc(var(--spacing) * 4.75);
z-index: -1;
visibility: hidden;
}
.nav-link {
padding: calc(var(--spacing) * 2) calc(var(--spacing) * 4);
border-radius: calc(var(--spacing) * 4.75);
font-weight: bold;
color: var(--p-surface-600);
white-space: nowrap;
&.active {
color: var(--p-primary-600);
background-color: var(--p-primary-200);
}
}
nav.mounted {
&::before {
visibility: visible;
transition: {
property: width, transform, opacity, scale, translate;
duration: 0.3s;
}
}
.nav-link {
background-color: unset;
&:hover {
background-color: rgba(from var(--p-surface-400) r g b / 0.1);
}
}
}
}
.s-dark header {
&::before {
background-color: var(--p-surface-900);
}
h1 {
color: var(--p-surface-100);
}
nav::before {
background-color: rgb(from var(--p-primary-500) r g b / 0.3);
}
.nav-link {
color: var(--p-surface-300);
&.active {
color: var(--p-primary-400);
background-color: rgb(from var(--p-primary-500) r g b / 0.3);
}
}
nav.mounted {
.nav-link {
background-color: unset;
&:hover {
background-color: rgba(from var(--p-surface-400) r g b / 0.1);
}
}
}
}
</style>