✨ 首页应用列表完成
This commit is contained in:
@@ -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)
|
||||||
}
|
}
|
||||||
@@ -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!())
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
63
src/components/HomeListApps/index.tsx
Normal file
63
src/components/HomeListApps/index.tsx
Normal 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;
|
||||||
@@ -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',
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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("获取列表应用数据失败");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user