首页应用列表完成

This commit is contained in:
柚子
2025-01-22 20:10:03 +08:00
parent e0933216d4
commit b2f458f3b8
11 changed files with 241 additions and 64 deletions

View File

@@ -1,13 +1,13 @@
use crate::handlers::server::get_img_server_url;
use crate::models::home::HomeLink;
use crate::utils::UA;
use crate::models::home::{HomeLink, HomeList, HomeListApp};
use crate::utils::{format_icon_url, UA};
use super::server::get_json_server_url;
#[tauri::command]
pub async fn get_home_links() -> Result<Vec<HomeLink>, String> {
let json_server_url = get_json_server_url();
let img_server_url = get_img_server_url();
let url = format!("{}home/homelinks.json", json_server_url);
let url = format!("{}/home/homelinks.json", json_server_url);
let client = reqwest::Client::new();
let response = client
@@ -33,4 +33,56 @@ pub async fn get_home_links() -> Result<Vec<HomeLink>, String> {
}
Ok(links)
}
#[tauri::command]
pub async fn get_home_list_apps(json_url: String) -> Result<Vec<HomeListApp>, String> {
let json_server_url = get_json_server_url();
let url = format!("{}{}", json_server_url, json_url);
let client = reqwest::Client::new();
let response = client
.get(&url)
.header("User-Agent", UA)
.send()
.await
.map_err(|e| e.to_string())?;
let response_text = response
.text()
.await
.map_err(|e| e.to_string())?;
let mut apps: Vec<HomeListApp> = serde_json::from_str(&response_text)
.map_err(|e| e.to_string())?;
for app in &mut apps {
app.icon = Some(format_icon_url(&app.category, &app.pkgname));
}
Ok(apps)
}
#[tauri::command]
pub async fn get_home_lists() -> Result<Vec<HomeList>, String> {
let json_server_url = get_json_server_url();
let url = format!("{}/home/homelist.json", json_server_url);
let client = reqwest::Client::new();
let response = client
.get(&url)
.header("User-Agent", UA)
.send()
.await
.map_err(|e| e.to_string())?;
let response_text = response
.text()
.await
.map_err(|e| e.to_string())?;
let lists: Vec<HomeList> = serde_json::from_str(&response_text)
.map_err(|e| e.to_string())?;
Ok(lists)
}

View File

@@ -17,6 +17,8 @@ pub fn run() {
handlers::app::get_app_info,
handlers::app::search_all_apps,
handlers::home::get_home_links,
handlers::home::get_home_lists,
handlers::home::get_home_list_apps,
utils::get_user_agent,
])
.run(tauri::generate_context!())

View File

@@ -1,5 +1,19 @@
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct HomeListApp {
#[serde(rename = "Name")]
pub name: String,
#[serde(rename = "Pkgname")]
pub pkgname: String,
#[serde(rename = "Category")]
pub category: String,
#[serde(rename = "More")]
pub more: String,
#[serde(rename = "Icon")]
pub icon: Option<String>,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct HomeLink {
pub name: String,
@@ -9,4 +23,13 @@ pub struct HomeLink {
#[serde(rename = "type")]
pub link_type: String,
pub url: String,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct HomeList {
pub name: String,
#[serde(rename = "type")]
pub list_type: String,
#[serde(rename = "jsonUrl")]
pub json_url: String,
}

View File

@@ -15,7 +15,7 @@ const AppCard: Component<AppCardProps> = (props) => {
href={`/app/${props.category}/${props.pkgname}`}
class="block p-4 rounded-lg border border-border bg-card hover:border-primary/50 transition-colors"
>
<div class="flex items-start gap-3">
<div class="flex items-center gap-3 h-full">
<div class="w-12 h-12 rounded-lg bg-muted flex items-center justify-center overflow-hidden">
{props.icon ? (
<img src={props.icon} alt={props.name} class="w-full h-full object-cover" />

View File

@@ -6,6 +6,7 @@ import { HomeLink } from '@/types/home';
interface HomeCarouselProps {
slides?: HomeLink[];
loading?: boolean;
class?: string;
}
const HomeCarousel: Component<HomeCarouselProps> = (props) => {
@@ -42,6 +43,7 @@ const HomeCarousel: Component<HomeCarouselProps> = (props) => {
return (
<BaseCarousel
class={props.class}
items={props.slides}
loading={props.loading}
renderItem={renderItem}

View File

@@ -0,0 +1,63 @@
import { Component, For } from 'solid-js';
import { HomeListApp } from '@/types/home';
import AppCard from '../AppCard';
import { Skeleton } from "@/components/ui/skeleton";
interface HomeListAppsProps {
title: string;
apps: HomeListApp[];
loading: boolean;
category?: string;
}
const HomeListApps: Component<HomeListAppsProps> = (props) => {
return (
<div class="mb-2 mt-2">
<div class="mb-2">
{props.loading ? (
<Skeleton height={30} width={120} />
) : (
<h2 class="text-2xl font-bold">{props.title}</h2>
)}
</div>
<div class="grid auto-rows-auto grid-cols-[repeat(auto-fit,minmax(200px,1fr))] gap-4 pb-6">
{props.loading ? (
<For each={Array(8).fill(0)}>
{() => (
<div class="p-4 rounded-lg border border-border/40 bg-card hover:border-primary/50 transition-colors">
<div class="flex items-start gap-3">
<div class="w-12 h-12 rounded-lg bg-muted flex items-center justify-center overflow-hidden">
<Skeleton class="w-full h-full" />
</div>
<div class="flex-1 min-w-0">
<div class="flex items-center justify-between gap-2">
<Skeleton class="h-5 w-24" />
</div>
<div class="mt-1">
<Skeleton class="h-4 w-full mt-2" />
<Skeleton class="h-4 w-3/4 mt-2" />
</div>
</div>
</div>
</div>
)}
</For>
) : (
<For each={props.apps}>
{(app) => (
<AppCard
category={props.category || app.Category || ''}
pkgname={app.Pkgname}
name={app.Name}
description={app.More}
icon={app.Icon}
/>
)}
</For>
)}
</div>
</div>
);
};
export default HomeListApps;

View File

@@ -14,11 +14,12 @@ interface BaseCarouselProps {
renderItem: (item: any) => JSX.Element;
renderSkeleton: () => JSX.Element;
items?: any[];
class?: string;
}
const BaseCarousel: Component<BaseCarouselProps> = (props) => {
return (
<div class="relative w-full group">
<div class={"relative w-full group " + props.class} >
<Carousel
opts={{
align: 'start',

View File

@@ -1,16 +1,35 @@
import { Component } from 'solid-js';
import { Component, For } from 'solid-js';
import { useHomeStore } from './store';
import AppList from '@/components/AppList';
import HomeCarousel from '@/components/HomeCarousel';
import HomeListApps from '@/components/HomeListApps';
const Home: Component = () => {
const { apps, loading, slides } = useHomeStore();
const { lists, 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()} />
<HomeCarousel slides={slides() ?? []} loading={loading()} class='pb-2'/>
{loading() ? (
<For each={Array(2).fill(0)}>
{() => (
<HomeListApps
title=""
apps={[]}
loading={true}
/>
)}
</For>
) : (
<For each={lists() ?? []}>
{(list) => (
<HomeListApps
title={list.title}
apps={list.apps}
loading={loading()}
/>
)}
</For>
)}
</div>
);
};

View File

@@ -1,54 +1,33 @@
import { AppItem } from '@/types/app';
import { HomeLink } from '@/types/home';
import { createResource } from 'solid-js';
import { getHomeLinks } from '@/lib/api/home';
import { getHomeLinks, getHomeLists, getListApps } 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 fetchLists = async () => {
try {
// 获取首页列表
const homeLists = await getHomeLists();
// 如果没有列表数据,返回空数组
if (!homeLists || homeLists.length === 0) {
return [];
}
// 获取所有列表的应用数据
const listsWithApps = await Promise.all(
homeLists.map(async (list) => {
const apps = await getListApps(list.jsonUrl);
return {
title: list.name,
apps
};
})
);
return listsWithApps;
} catch (error) {
console.error('获取应用列表失败:', error);
return [];
}
};
const getCarouselSlides = async (): Promise<HomeLink[]> => {
@@ -56,16 +35,16 @@ const getCarouselSlides = async (): Promise<HomeLink[]> => {
};
export const useHomeStore = () => {
const [apps, { refetch: refetchApps }] = createResource<AppItem[]>(fetchApps);
const [lists, { refetch: refetchLists }] = createResource(fetchLists);
const [slides, { refetch: refetchSlides }] = createResource<HomeLink[]>(getCarouselSlides);
const loading = () => apps.loading || slides.loading;
const loading = () => lists.loading || slides.loading;
return {
apps,
lists,
slides,
loading,
refetch: () => {
refetchApps();
refetchLists();
refetchSlides();
}
};

View File

@@ -1,6 +1,6 @@
import { invoke } from "@tauri-apps/api/core";
import { retryOperation } from "../utils";
import { HomeLink, HomeLinkResponse } from "@/types/home";
import { HomeLink, HomeLinkResponse, HomeList, HomeListApp } from "@/types/home";
export const getHomeLinks = async (): Promise<HomeLink[]> => {
try {
@@ -18,4 +18,26 @@ export const getHomeLinks = async (): Promise<HomeLink[]> => {
console.error("获取主页链接列表失败:", error);
throw new Error("获取主页链接列表失败");
}
};
export const getHomeLists = async (): Promise<HomeList[]> => {
try {
return await retryOperation(async () => {
return await invoke<HomeList[]>("get_home_lists");
});
} catch (error) {
console.error("获取首页列表失败:", error);
throw new Error("获取首页列表失败");
}
};
export const getListApps = async (jsonUrl: string): Promise<HomeListApp[]> => {
try {
return await retryOperation(async () => {
return await invoke<HomeListApp[]>("get_home_list_apps", { jsonUrl });
});
} catch (error) {
console.error("获取列表应用数据失败:", error);
throw new Error("获取列表应用数据失败");
}
};

View File

@@ -13,3 +13,17 @@ export interface HomeLinkResponse {
type: string;
url: string;
}
export interface HomeList {
name: string;
more: string;
jsonUrl: string;
}
export interface HomeListApp {
Name: string;
Pkgname: string;
Category: string;
More: string;
Icon: string;
}