🎉 创世提交

This commit is contained in:
柚子
2025-01-22 01:48:07 +08:00
commit 5d3481b9ea
92 changed files with 11705 additions and 0 deletions

View File

@@ -0,0 +1,108 @@
.app-detail {
position: relative;
}
.app-detail .bg-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 500px;
overflow: hidden;
}
.app-detail .bg {
width: 100%;
height: 100%;
object-fit: fill;
filter: blur(100px) brightness(1.2);
}
.app-detail .info-container {
display: flex;
flex-direction: column;
justify-content: start;
align-items: start;
position: relative;
padding: 40px;
z-index: 1;
}
.app-detail .bg-container:after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(
0deg,
hsl(var(--background)) 0%,
hsla(var(--background) / 0.6) 100%
);
}
.app-detail .app-icon {
width: 80px;
height: 80px;
border-radius: 16px;
}
.app-detail .app-name {
color: hsl(var(--foreground));
}
.app-detail .app-title-container {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
gap: 20px;
margin-bottom: 10px;
width: 100%;
}
.app-detail .divider {
height: 18px;
background-color: hsl(var(--border));
width: 1px;
}
.app-detail .social-container {
display: flex;
flex-direction: row;
justify-content: start;
align-items: center;
gap: 10px;
}
.app-detail .install-btn {
width: 200px;
height: 50px;
}
.app-detail .description {
display: block;
max-height: 4.5em;
line-height: 1.5em;
overflow: hidden;
position: relative;
transition: max-height 0.4s ease-in-out;
}
.app-detail .trunk::before {
content: '';
float: right;
width: 0px;
height: 100%;
margin-bottom: -20px;
}
.app-detail .read-more-btn {
float: right;
clear: both;
font-size: 14px;
height: 20px;
width: 40px;
color: hsl(var(--primary));
}

View File

@@ -0,0 +1,152 @@
import { Component } from 'solid-js';
import { useParams } from '@solidjs/router';
import { useAppDetailStore } from './store';
import './AppDetail.css';
import { Skeleton } from '@/components/ui/skeleton';
import { Button } from '@/components/ui/button';
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
import ScreenshotCarousel from '@/components/ScreenshotCarousel';
import { generateShareLinks, copy } from '@/lib/share';
import { showToast } from "@/components/ui/toast"
const AppDetail: Component = () => {
const params = useParams();
const { app, loading } = useAppDetailStore(params.category, params.pkgname);
return (
<div class="w-full h-full">
{loading() ? (
<div class="app-detail">
<div class="info-container">
<div class="app-title-container">
<div class="flex-row-start gap-20px">
<div class="app-icon bg-muted flex items-center justify-center overflow-hidden">
<Skeleton width={64} height={64} radius={8} />
</div>
<div class="py-2" />
<Skeleton height={32} width={200} />
</div>
<div class="flex flex-col gap-2">
<Skeleton height={40} width={96} radius={6} />
<Skeleton height={40} width={96} radius={6} />
</div>
</div>
<div class="mt-2">
<div class="py-2">
<Skeleton height={24} width={80} class="mb-2" />
<Skeleton height={16} width={100} class="mt-2" />
</div>
<div class="py-2">
<Skeleton height={24} width={80} class="mb-2" />
<div class="social-container text-sm text-muted-foreground">
<Skeleton height={16} width={120} />
<div class="divider" />
<Skeleton height={16} width={120} />
<div class="divider" />
<Skeleton height={16} width={120} />
</div>
</div>
</div>
</div>
</div>
) : (
<div class="app-detail">
<div class="bg-container">
<div
class="bg"
style={{
'background-image': `url(${app()?.Icon})`,
'background-size': 'cover',
'background-position': 'center',
}}
/>
</div>
<div class="info-container">
<div class="app-title-container">
<div class="flex-row-start gap-20px">
<div class="app-icon bg-muted flex items-center justify-center overflow-hidden">
{app()?.Icon ? (
<img src={app()?.Icon} alt={app()?.Name} class="w-full h-full object-cover" />
) : (
<div class="text-2xl font-bold text-muted-foreground">
{app()?.Name?.charAt(0)}
</div>
)}
</div>
<div class="py-2" />
<h2 class="app-name text-2xl font-bold pt-2">{app()?.Name}</h2>
</div>
<div class="flex flex-col gap-2">
<Button size="lg"></Button>
<DropdownMenu>
<DropdownMenuTrigger>
<Button variant="outline" size="lg"></Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuLabel></DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem onSelect={async () => {
try {
const links = await generateShareLinks(params.category, params.pkgname);
await copy(links.spkLink);
showToast({ description: 'SPK链接已复制到剪贴板', variant: "success" });
} catch (error) {
showToast({ description: '复制SPK链接失败', variant: "error" });
}
}}>SPK链接</DropdownMenuItem>
<DropdownMenuItem onSelect={async () => {
try {
const links = await generateShareLinks(params.category, params.pkgname);
await copy(links.shareLink);
showToast({ description: '分享链接已复制到剪贴板', variant: "success" });
} catch (error) {
showToast({ description: '复制分享链接失败', variant: "error" });
}
}}></DropdownMenuItem>
<DropdownMenuItem onSelect={async () => {
try {
const links = await generateShareLinks(params.category, params.pkgname);
await copy(links.shareIframe);
showToast({ description: '嵌入代码已复制到剪贴板', variant: "success" });
} catch (error) {
showToast({ description: '复制嵌入代码失败', variant: "error" });
}
}}></DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
<div class="mt-2 w-full">
<div class="py-2">
<h3 class="text-lg font-semibold"></h3>
<p class="description text-muted-foreground">{app()?.More}</p>
</div>
<div class="py-2">
<h3 class="text-lg font-semibold"></h3>
<div class="social-container text-sm text-muted-foreground">
<p>: {app()?.Size}</p>
<div class="divider" />
<p>: {app()?.Category}</p>
<div class="divider" />
<p>: {app()?.Update}</p>
</div>
</div>
<div class="py-2 w-full">
<h3 class="text-lg font-semibold"></h3>
<ScreenshotCarousel
screenshots={app()?.Screenshots?.map(url => ({
url,
title: app()?.Name || ''
}))}
loading={loading()}
/>
</div>
</div>
</div>
</div>
)}
</div>
);
};
export default AppDetail;

View File

@@ -0,0 +1,35 @@
import { getAppInfo } from '@/lib/api/app';
import { getImgServerUrl } from '@/lib/api/server';
import { AppDetail } from '@/types/app';
import { createResource } from 'solid-js';
const fetchAppDetail = async (category: string, pkgname: string): Promise<AppDetail> => {
try {
const appInfo = await getAppInfo(category, pkgname);
// 生成5张截图的链接
const screenshots = Array.from({ length: 5 }, (_, index) =>
`${getImgServerUrl()}/${appInfo.Category}/${appInfo.Pkgname}/screen_${index}.png`
);
return {
...appInfo,
Screenshots: screenshots
};
} catch (error) {
console.error(`获取应用详情失败: ${error}`);
throw error;
}
};
export const useAppDetailStore = (category: string, pkgname: string) => {
const [app, { refetch }] = createResource<AppDetail>(() => fetchAppDetail(category, pkgname));
const loading = () => app.loading;
return {
app,
loading,
refetch,
};
};

View File

@@ -0,0 +1,38 @@
import { Component, createEffect, createSignal } from 'solid-js';
import { useParams } from '@solidjs/router';
import { fetchCategoryApps, useCategoriesStore } from './store';
import AppList from '@/components/AppList';
const Categories: Component = () => {
const params = useParams();
const [categoryApps, setCategoryApps] = createSignal<any[]>([]);
const [loadingApps, setLoadingApps] = createSignal(true);
const { categories } = useCategoriesStore();
createEffect(async () => {
if (params.id) {
setLoadingApps(true);
try {
const apps = await fetchCategoryApps(params.id);
setCategoryApps(apps);
} finally {
setLoadingApps(false);
}
}
});
return (
<div class="p-6 w-full h-full">
<h1 class="text-2xl font-bold mb-6">
{categories()?.find((category) => category.id === params.id)?.name_zh_cn}
</h1>
<AppList
apps={categoryApps()}
loading={loadingApps()}
category={params.id}
/>
</div>
);
};
export default Categories;

View File

@@ -0,0 +1,23 @@
import { createResource } from 'solid-js';
import { getAllCategories, getCategoryApps } from '@/lib/api/category';
import type { Category } from '@/types/category';
import { AppItem } from '@/types/app';
const fetchCategories = async (): Promise<Category[]> => {
return await getAllCategories();
};
export const fetchCategoryApps = async (categoryId: string): Promise<AppItem[]> => {
return await getCategoryApps(categoryId);
};
export const useCategoriesStore = () => {
const [categories, { refetch }] = createResource<Category[]>(fetchCategories);
const loading = () => categories.loading;
return {
categories,
loading,
refetch,
};
};

View File

@@ -0,0 +1,18 @@
import { Component } from 'solid-js';
import { useHomeStore } from './store';
import AppList from '@/components/AppList';
import HomeCarousel from '@/components/HomeCarousel';
const Home: Component = () => {
const { apps, loading, slides } = useHomeStore();
return (
<div class="p-6 w-full h-full">
<HomeCarousel slides={slides() ?? []} loading={loading()} />
<h1 class="text-2xl font-bold mb-6">使 Spark Store</h1>
<AppList apps={apps() ?? []} loading={loading()} />
</div>
);
};
export default Home;

View File

@@ -0,0 +1,72 @@
import { AppItem } from '@/types/app';
import { HomeLink } from '@/types/home';
import { createResource } from 'solid-js';
import { getHomeLinks } from '@/lib/api/home';
const fetchApps = async (): Promise<AppItem[]> => {
// 模拟从后端获取数据的延迟
await new Promise(resolve => setTimeout(resolve, 1000));
return [
{
pkgname: '1',
name: 'Visual Studio Code',
more: '轻量级但功能强大的代码编辑器',
category: 'development',
icon: '/icons/vscode.png',
update: ''
},
{
pkgname: '2',
name: 'Firefox',
more: '注重隐私的开源浏览器',
category: 'development',
icon: '/icons/firefox.png',
update: ''
},
{
pkgname: '3',
name: 'GIMP',
more: '功能丰富的图像编辑软件',
category: 'development',
icon: '/icons/gimp.png',
update: ''
},
{
pkgname: '4',
name: 'Notion',
more: '功能强大的笔记软件',
category: 'development',
icon: '/icons/notion.png',
update: ''
},
{
pkgname: '5',
name: 'Slack',
more: '团队沟通和协作工具',
category: 'development',
icon: '/icons/slack.png',
update: ''
},
];
};
const getCarouselSlides = async (): Promise<HomeLink[]> => {
return await getHomeLinks();
};
export const useHomeStore = () => {
const [apps, { refetch: refetchApps }] = createResource<AppItem[]>(fetchApps);
const [slides, { refetch: refetchSlides }] = createResource<HomeLink[]>(getCarouselSlides);
const loading = () => apps.loading || slides.loading;
return {
apps,
slides,
loading,
refetch: () => {
refetchApps();
refetchSlides();
}
};
};

View File

@@ -0,0 +1,46 @@
import { Component, createEffect, createSignal } from 'solid-js';
import { useSearchParams } from '@solidjs/router';
import { searchAllApps } from '@/lib/api/app';
import { AppItem } from '@/types/app';
import AppList from '@/components/AppList';
const Search: Component = () => {
const [searchParams] = useSearchParams();
const [searchResults, setSearchResults] = createSignal<AppItem[]>([]);
const [loading, setLoading] = createSignal(true);
const [error, setError] = createSignal<string | null>(null);
createEffect(async () => {
const query = searchParams.q;
if (query && typeof query === 'string') {
setLoading(true);
setError(null);
try {
const results = await searchAllApps(query);
setSearchResults(results);
} catch (err) {
setError(err instanceof Error ? err.message : '搜索失败');
} finally {
setLoading(false);
}
}
});
return (
<div class="p-6 w-full h-full">
<h1 class="text-2xl font-bold mb-6">
: {searchParams.q}
</h1>
{error() ? (
<div class="text-red-500">{error()}</div>
) : (
<AppList
apps={searchResults()}
loading={loading()}
/>
)}
</div>
);
};
export default Search;