🎉 创世提交
This commit is contained in:
108
src/features/app-detail/AppDetail.css
Normal file
108
src/features/app-detail/AppDetail.css
Normal 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));
|
||||
}
|
||||
152
src/features/app-detail/AppDetail.tsx
Normal file
152
src/features/app-detail/AppDetail.tsx
Normal 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;
|
||||
35
src/features/app-detail/store.ts
Normal file
35
src/features/app-detail/store.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
38
src/features/categories/Categories.tsx
Normal file
38
src/features/categories/Categories.tsx
Normal 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;
|
||||
23
src/features/categories/store.ts
Normal file
23
src/features/categories/store.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
18
src/features/home/Home.tsx
Normal file
18
src/features/home/Home.tsx
Normal 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;
|
||||
72
src/features/home/store.ts
Normal file
72
src/features/home/store.ts
Normal 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();
|
||||
}
|
||||
};
|
||||
};
|
||||
46
src/features/search/Search.tsx
Normal file
46
src/features/search/Search.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user