首页应用列表完成

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::handlers::server::get_img_server_url;
use crate::models::home::HomeLink; use crate::models::home::{HomeLink, HomeList, HomeListApp};
use crate::utils::UA; use crate::utils::{format_icon_url, UA};
use super::server::get_json_server_url; use super::server::get_json_server_url;
#[tauri::command] #[tauri::command]
pub async fn get_home_links() -> Result<Vec<HomeLink>, String> { pub async fn get_home_links() -> Result<Vec<HomeLink>, String> {
let json_server_url = get_json_server_url(); let json_server_url = get_json_server_url();
let img_server_url = get_img_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 client = reqwest::Client::new();
let response = client let response = client
@@ -33,4 +33,56 @@ pub async fn get_home_links() -> Result<Vec<HomeLink>, String> {
} }
Ok(links) 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::get_app_info,
handlers::app::search_all_apps, handlers::app::search_all_apps,
handlers::home::get_home_links, handlers::home::get_home_links,
handlers::home::get_home_lists,
handlers::home::get_home_list_apps,
utils::get_user_agent, utils::get_user_agent,
]) ])
.run(tauri::generate_context!()) .run(tauri::generate_context!())

View File

@@ -1,5 +1,19 @@
use serde::{Deserialize, Serialize}; 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)] #[derive(Serialize, Deserialize, Clone)]
pub struct HomeLink { pub struct HomeLink {
pub name: String, pub name: String,
@@ -9,4 +23,13 @@ pub struct HomeLink {
#[serde(rename = "type")] #[serde(rename = "type")]
pub link_type: String, pub link_type: String,
pub url: 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}`} href={`/app/${props.category}/${props.pkgname}`}
class="block p-4 rounded-lg border border-border bg-card hover:border-primary/50 transition-colors" 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"> <div class="w-12 h-12 rounded-lg bg-muted flex items-center justify-center overflow-hidden">
{props.icon ? ( {props.icon ? (
<img src={props.icon} alt={props.name} class="w-full h-full object-cover" /> <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 { interface HomeCarouselProps {
slides?: HomeLink[]; slides?: HomeLink[];
loading?: boolean; loading?: boolean;
class?: string;
} }
const HomeCarousel: Component<HomeCarouselProps> = (props) => { const HomeCarousel: Component<HomeCarouselProps> = (props) => {
@@ -42,6 +43,7 @@ const HomeCarousel: Component<HomeCarouselProps> = (props) => {
return ( return (
<BaseCarousel <BaseCarousel
class={props.class}
items={props.slides} items={props.slides}
loading={props.loading} loading={props.loading}
renderItem={renderItem} 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; renderItem: (item: any) => JSX.Element;
renderSkeleton: () => JSX.Element; renderSkeleton: () => JSX.Element;
items?: any[]; items?: any[];
class?: string;
} }
const BaseCarousel: Component<BaseCarouselProps> = (props) => { const BaseCarousel: Component<BaseCarouselProps> = (props) => {
return ( return (
<div class="relative w-full group"> <div class={"relative w-full group " + props.class} >
<Carousel <Carousel
opts={{ opts={{
align: 'start', align: 'start',

View File

@@ -1,16 +1,35 @@
import { Component } from 'solid-js'; import { Component, For } from 'solid-js';
import { useHomeStore } from './store'; import { useHomeStore } from './store';
import AppList from '@/components/AppList';
import HomeCarousel from '@/components/HomeCarousel'; import HomeCarousel from '@/components/HomeCarousel';
import HomeListApps from '@/components/HomeListApps';
const Home: Component = () => { const Home: Component = () => {
const { apps, loading, slides } = useHomeStore(); const { lists, loading, slides } = useHomeStore();
return ( return (
<div class="p-6 w-full h-full"> <div class="p-6 w-full h-full">
<HomeCarousel slides={slides() ?? []} loading={loading()} /> <HomeCarousel slides={slides() ?? []} loading={loading()} class='pb-2'/>
<h1 class="text-2xl font-bold mb-6">使 Spark Store</h1> {loading() ? (
<AppList apps={apps() ?? []} loading={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> </div>
); );
}; };

View File

@@ -1,54 +1,33 @@
import { AppItem } from '@/types/app';
import { HomeLink } from '@/types/home'; import { HomeLink } from '@/types/home';
import { createResource } from 'solid-js'; import { createResource } from 'solid-js';
import { getHomeLinks } from '@/lib/api/home'; import { getHomeLinks, getHomeLists, getListApps } from '@/lib/api/home';
const fetchApps = async (): Promise<AppItem[]> => { const fetchLists = async () => {
// 模拟从后端获取数据的延迟 try {
await new Promise(resolve => setTimeout(resolve, 1000)); // 获取首页列表
const homeLists = await getHomeLists();
return [
{ // 如果没有列表数据,返回空数组
pkgname: '1', if (!homeLists || homeLists.length === 0) {
name: 'Visual Studio Code', return [];
more: '轻量级但功能强大的代码编辑器', }
category: 'development',
icon: '/icons/vscode.png', // 获取所有列表的应用数据
update: '' const listsWithApps = await Promise.all(
}, homeLists.map(async (list) => {
{ const apps = await getListApps(list.jsonUrl);
pkgname: '2', return {
name: 'Firefox', title: list.name,
more: '注重隐私的开源浏览器', apps
category: 'development', };
icon: '/icons/firefox.png', })
update: '' );
},
{ return listsWithApps;
pkgname: '3', } catch (error) {
name: 'GIMP', console.error('获取应用列表失败:', error);
more: '功能丰富的图像编辑软件', return [];
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[]> => { const getCarouselSlides = async (): Promise<HomeLink[]> => {
@@ -56,16 +35,16 @@ const getCarouselSlides = async (): Promise<HomeLink[]> => {
}; };
export const useHomeStore = () => { export const useHomeStore = () => {
const [apps, { refetch: refetchApps }] = createResource<AppItem[]>(fetchApps); const [lists, { refetch: refetchLists }] = createResource(fetchLists);
const [slides, { refetch: refetchSlides }] = createResource<HomeLink[]>(getCarouselSlides); const [slides, { refetch: refetchSlides }] = createResource<HomeLink[]>(getCarouselSlides);
const loading = () => apps.loading || slides.loading; const loading = () => lists.loading || slides.loading;
return { return {
apps, lists,
slides, slides,
loading, loading,
refetch: () => { refetch: () => {
refetchApps(); refetchLists();
refetchSlides(); refetchSlides();
} }
}; };

View File

@@ -1,6 +1,6 @@
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
import { retryOperation } from "../utils"; import { retryOperation } from "../utils";
import { HomeLink, HomeLinkResponse } from "@/types/home"; import { HomeLink, HomeLinkResponse, HomeList, HomeListApp } from "@/types/home";
export const getHomeLinks = async (): Promise<HomeLink[]> => { export const getHomeLinks = async (): Promise<HomeLink[]> => {
try { try {
@@ -18,4 +18,26 @@ export const getHomeLinks = async (): Promise<HomeLink[]> => {
console.error("获取主页链接列表失败:", error); console.error("获取主页链接列表失败:", error);
throw new 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; type: string;
url: 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;
}