fix: 服装搭配
This commit is contained in:
2095
RUST_AWESOME.md
Normal file
2095
RUST_AWESOME.md
Normal file
File diff suppressed because it is too large
Load Diff
462
TAURI_AWESOME.md
Normal file
462
TAURI_AWESOME.md
Normal file
@@ -0,0 +1,462 @@
|
||||
<!--lint disable awesome-heading awesome-toc awesome-github double-link -->
|
||||
|
||||
<div align="center">
|
||||
<h1>Awesome Tauri</h1>
|
||||
|
||||
A curated collection of the best stuff from the Tauri ecosystem and community.
|
||||
|
||||
<br />
|
||||
|
||||
[](https://awesome.re)
|
||||
|
||||
</div>
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Getting Started](#getting-started)
|
||||
- [Guides & Tutorials](#guides--tutorials)
|
||||
- [Templates](#templates)
|
||||
- [Development](#development)
|
||||
- [Plugins](#plugins)
|
||||
- [Integrations](#integrations)
|
||||
- [Articles](#articles)
|
||||
- [Applications](#applications)
|
||||
- [Audio & Video](#audio--video)
|
||||
- [ChatGPT clients](#chatgpt-clients)
|
||||
- [Data](#data)
|
||||
- [Developer tools](#developer-tools)
|
||||
- [Ebook readers](#ebook-readers)
|
||||
- [Email & Feeds](#email--feeds)
|
||||
- [File management](#file-management)
|
||||
- [Finance](#finance)
|
||||
- [Gaming](#gaming)
|
||||
- [Information](#information)
|
||||
- [Learning](#learning)
|
||||
- [Networking](#networking)
|
||||
- [Office & Writing](#office--writing)
|
||||
- [Productivity](#productivity)
|
||||
- [Search](#search)
|
||||
- [Security](#security)
|
||||
- [Social media](#social-media)
|
||||
- [Utilities](#utilities)
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Guides & Tutorials
|
||||
|
||||
- [Introduction](https://v2.tauri.app/start/) ![officially maintained] - Official introduction to Tauri.
|
||||
- [Getting Started](https://v2.tauri.app/start/prerequisites/) ![officially maintained] - Official getting started with Tauri docs.
|
||||
- [create-tauri-app](https://github.com/tauri-apps/create-tauri-app) ![officially maintained] - Rapidly scaffold your Tauri app.
|
||||
- [Auto-Updates with Tauri v2](https://docs.crabnebula.dev/guides/auto-updates-tauri) - Setup auto-updates with Tauri and CrabNebula Cloud.
|
||||
- [Create Tauri App with React](https://www.youtube.com/watch?v=zawhqLA7N9Y&ab_channel=chrisbiscardi) ![youtube] - Chris Biscardi shows how easy it is to wire up a Rust crate with a JS module and communicate between them.
|
||||
- [Publish to Apple's App Store](https://thinkgo.io/post/2023/02/publish_tauri_to_apples_app_store/) - Details all the steps needed to publish your Mac app to the app store. Includes a sample bash script.
|
||||
- [Tauri & ReactJS - Creating Modern Desktop Apps](https://youtube.com/playlist?list=PLmWYh0f8jKSjt9VC5sq2T3mFETasG2p2L) ![youtube] - Creating a modern desktop application with Tauri.
|
||||
|
||||
### Templates
|
||||
|
||||
- [angular-tauri](https://github.com/maximegris/angular-tauri) - Angular with Typescript, SASS, and Hot Reload.
|
||||
- [nuxtor](https://github.com/NicolaSpadari/nuxtor) - Nuxt 3 + Tauri 2 + UnoCSS, a starter template for building desktop apps.
|
||||
- [rust-full-stack-with-authentication-template](https://github.com/sollambert/rust-full-stack-with-auth-template) - Yew, Tailwind CSS, Tauri, Axum, Sqlx - Starter template for full stack applications with built-in authentication.
|
||||
- [tauri-angular-template](https://github.com/charlesxsh/tauri-angular-boilerplate) - Angular template
|
||||
- [tauri-astro-template](https://github.com/HuakunShen/tauri-astro-template) - Astro template
|
||||
- [tauri-bishop-template](https://github.com/RoseBlume/Bishop-Tauri-Template) - Minimized vanilla template designed for highschool students.
|
||||
- [tauri-clojurescript-template](https://github.com/rome-user/tauri-clojurescript-template) - Minimal ClojureScript template with Shadow CLJS and React.
|
||||
- [tauri-deno-starter](https://github.com/marc2332/tauri-deno-starter) - React template using esbuild with Deno.
|
||||
- [tauri-leptos-template](https://gitlab.com/cristofa/tauri-leptos-template) - Leptos template
|
||||
- [tauri-nextjs-template](https://github.com/kvnxiao/tauri-nextjs-template) - Next.js (SSG) template, with TailwindCSS, opinionated linting, and GitHub Actions preconfigured.
|
||||
- [tauri-nuxt-template](https://github.com/HuakunShen/tauri-nuxt-template) - Nuxt3 template.
|
||||
- [tauri-preact-rsbuild-template](https://github.com/Alfredoes234/tauri-preact-rsbuild-template) - Preact template that uses rsbuild, rather than vite.
|
||||
- [tauri-react-mantine-vite-template](https://github.com/elibroftw/modern-desktop-app-template) - React Mantine template featuring custom titlebar for Windows, auto publish action, auto update, and more.
|
||||
- [tauri-react-parcel-template](https://github.com/henrhie/tauri-react-parcel-template) - React template with Parcel as build tool, TypeScript and hot module replacement.
|
||||
- [tauri-rescript-template](https://github.com/JonasKruckenberg/tauri-rescript-template) - Tauri, ReScript, and React template.
|
||||
- [tauri-solid-ts-tailwind-vite-template](https://github.com/AR10Dev/tauri-solid-ts-tailwind-vite) - SolidJS Template preconfigured to use Vite, TypeScript, Tailwind CSS, ESLint and Prettier.
|
||||
- [tauri-svelte-template](https://github.com/probablykasper/tauri-svelte-template) - Svelte template with cross-platform GitHub action builds, Vite, TypeScript, Svelte Preprocess, hot module replacement, ESLint and Prettier.
|
||||
- [tauri-sveltekit-template](https://github.com/deid84/tauri-sveltekit-admin-template) - SvelteKit Admin template with cross-platform GitHub action builds, Vite, TypeScript, Svelte Preprocess, hot module replacement, ESLint and Prettier.
|
||||
- [tauri-sycamore-template](https://github.com/JonasKruckenberg/tauri-sycamore-template) - Tauri and Sycamore template.
|
||||
- [tauri-vue-template](https://github.com/Uninen/tauri-vue-template) - Vue template with TypeScript, Vite + HMR, Vitest, Tailwind CSS, ESLint, and GitHub Actions.
|
||||
- [tauri-vue-template-2](https://github.com/skymen/tauri-vue-template) - Another vue template with Javascript, Vite, Pinia, Vue Router and Github Actions.
|
||||
- [tauri-yew-example](https://bitbucket.org/ftegtmeyer/tauri-yew-stopwatch/) - Simple stopwatch with Yew using commands and Tauri events.
|
||||
- [tauronic](https://github.com/rgilsimoes/Tauronic/) - Tauri template for hybrid Apps using Ionic components in React flavour.
|
||||
|
||||
## Development
|
||||
|
||||
### Plugins
|
||||
|
||||
- [Official Plugins](https://github.com/tauri-apps/plugins-workspace) ![officially maintained] - This repository contains all the plugins maintained by the Tauri team. This includes plugins for NFC, logging, notifications, and more.
|
||||
- [window-vibrancy](https://github.com/tauri-apps/window-vibrancy) ![officially maintained] - Make your windows vibrant (v1 only - added to Tauri in v2).
|
||||
- [window-shadows](https://github.com/tauri-apps/window-shadows) ![officially maintained] - Add native shadows to your windows in Tauri (v1 only - added to Tauri in v2).
|
||||
- [tauri-plugin-auth](https://github.com/inKibra/tauri-plugins/tree/main/packages/tauri-plugin-auth) - Auth plugin for iOS that uses ASWebAuthenticationSession for authentication, which allows keychain access
|
||||
- [tauri-plugin-blec](https://github.com/MnlPhlp/tauri-plugin-blec) - Cross platform Bluetooth Low Energy client based on `btleplug`.
|
||||
- [tauri-plugin-cache](https://github.com/Taiizor/tauri-plugin-cache) - Advanced disk caching solution with memory layer, TTL management, compression support, and cross-platform compatibility for desktop and mobile.
|
||||
- [tauri-plugin-drpc](https://github.com/smokingplaya/tauri-plugin-drpc) - Discord RPC support
|
||||
- [tauri-plugin-keep-screen-on](https://gitlab.com/cristofa/tauri-plugin-keep-screen-on) - Disable screen timeout on Android and iOS.
|
||||
- [tauri-plugin-graphql](https://github.com/JonasKruckenberg/tauri-plugin-graphql) - Type-safe IPC for Tauri using GraphQL.
|
||||
- [sentry-tauri](https://github.com/timfish/sentry-tauri) - Capture JavaScript errors, Rust panics and native crash minidumps to Sentry.
|
||||
- [tauri-plugin-aptabase](https://github.com/aptabase/tauri-plugin-aptabase) - Privacy-first and minimalist analytics for desktop and mobile apps.
|
||||
- [tauri-plugin-clipboard](https://github.com/CrossCopy/tauri-plugin-clipboard) - Clipboard plugin for reading/writing clipboard text/image/html/rtf/files, and monitoring clipboard update.
|
||||
- [taurpc](https://github.com/MatsDK/TauRPC) - Typesafe IPC wrapper for Tauri commands and events.
|
||||
- [tauri-plugin-context-menu](https://github.com/c2r0b/tauri-plugin-context-menu) - Native context menu.
|
||||
- [tauri-plugin-desktop-underlay](https://github.com/Charlie-XIAO/tauri-plugin-desktop-underlay) - Attach a window to desktop, below icons and above wallpaper.
|
||||
- [tauri-plugin-fs-pro](https://github.com/ayangweb/tauri-plugin-fs-pro) - Extended with additional methods for files and directories.
|
||||
- [tauri-plugin-iap](https://github.com/inKibra/tauri-plugins/tree/main/packages/tauri-plugin-iap) - In-app-purchase plugin for iOS that allows fetching, purchasing, and restoring of products.
|
||||
- [tauri-plugin-macos-permissions](https://github.com/ayangweb/tauri-plugin-macos-permissions) - Support for checking and requesting macOS system permissions.
|
||||
- [tauri-pluing-ota](https://github.com/inKibra/tauri-plugins/tree/main/packages/tauri-plugin-ota) - OTA plugin for applications that just want to continuously deliever new JavaScript code based on a manfiest.
|
||||
- [tauri-nspopover-plugin](https://github.com/freethinkel/tauri-nspopover-plugin) - Native NSPopover view for use in the status bar in macOS.
|
||||
- [tauri-plugin-network](https://github.com/HuakunShen/tauri-plugin-network) - Tools for reading network information and scanning network.
|
||||
- [tauri-plugin-pinia](https://github.com/ferreira-tb/tauri-store/tree/main/packages/plugin-pinia) - Persistent Pinia stores for Vue.
|
||||
- [tauri-plugin-prevent-default](https://github.com/ferreira-tb/tauri-plugin-prevent-default) - Disable default browser shortcuts.
|
||||
- [tauri-plugin-python](https://github.com/marcomq/tauri-plugin-python/) - Use python in your backend.
|
||||
- [tauri-plugin-screenshots](https://github.com/ayangweb/tauri-plugin-screenshots) - Get screenshots of windows and monitors.
|
||||
- [tauri-plugin-serialport](https://github.com/deid84/tauri-plugin-serialport) - Cross-compatible serialport communication tool.
|
||||
- [tauri-plugin-serialplugin](https://github.com/s00d/tauri-plugin-serialplugin) - Cross-compatible serialport communication tool for tauri 2.
|
||||
- [tauri-plugin-sharesheet](https://github.com/buildyourwebapp/tauri-plugin-sharesheet) - Share content to other apps via the Android Sharesheet or iOS Share Pane.
|
||||
- [tauri-plugin-svelte](https://github.com/ferreira-tb/tauri-store/tree/main/packages/plugin-svelte) - Persistent Svelte stores.
|
||||
- [tauri-plugin-system-info](https://github.com/HuakunShen/tauri-plugin-system-info) - Detailed system information.
|
||||
- [tauri-plugin-theme](https://github.com/wyhaya/tauri-plugin-theme) - Dynamically change Tauri App theme.
|
||||
- [tauri-awesome-rpc](https://github.com/ahkohd/tauri-awesome-rpc) - Custom invoke system that leverages WebSocket.
|
||||
- [tauri-nspanel](https://github.com/ahkohd/tauri-nspanel) - Convert a window to panel.
|
||||
- [tauri-plugin-nosleep](https://github.com/pevers/tauri-plugin-nosleep/) - Block the power save functionality in the OS.
|
||||
- [tauri-plugin-udp](https://github.com/kuyoonjo/tauri-plugin-udp) - UDP socket support.
|
||||
- [tauri-plugin-tcp](https://github.com/kuyoonjo/tauri-plugin-tcp) - TCP socket support.
|
||||
- [tauri-plugin-mqtt](https://github.com/kuyoonjo/tauri-plugin-mqtt) - MQTT client support.
|
||||
- [tauri-plugin-view](https://github.com/ecmel/tauri-plugin-view) - View and share files on mobile.
|
||||
|
||||
### Integrations
|
||||
|
||||
- [Astrodon](https://github.com/astrodon/astrodon) - Make Tauri desktop apps with Deno.
|
||||
- [Deno in Tauri](https://github.com/typed-sigterm/deno-in-tauri) - Run JS/TS code with Deno Core Engine, in Tauri apps.
|
||||
- [kkrpc](https://github.com/kunkunsh/kkrpc) - Seamless RPC communication between a Tauri app and node/deno/bun processes, just like Electron.
|
||||
- [Tauri Specta](https://github.com/oscartbeaumont/tauri-specta) - Completely typesafe Tauri commands.
|
||||
- [axios-tauri-adapter](https://git.kaki87.net/KaKi87/axios-tauri-adapter) - `axios` adapter for the `@tauri-apps/api/http` module.
|
||||
- [axios-tauri-api-adapter](https://github.com/persiliao/axios-tauri-api-adapter) - Makes it easy to use Axios in Tauri, `axios` adapter for the `@tauri-apps/api/http` module.
|
||||
- [ngx-tauri](https://codeberg.org/crapsilon/ngx-tauri) - Small lib to wrap around functions from tauri modules, to integrate easier with Angular.
|
||||
- [svelte-tauri-filedrop](https://github.com/probablykasper/svelte-tauri-filedrop) - File drop handling component for Svelte.
|
||||
- [tauri-macos-menubar-app-example](https://github.com/ahkohd/tauri-macos-menubar-app-example) - Example macOS Menubar app project.
|
||||
- [tauri-macos-spotlight-example](https://github.com/ahkohd/tauri-macos-spotlight-example) - Example macOS Spotlight app project.
|
||||
- [tauri-update-cloudflare](https://github.com/KilleenCode/tauri-update-cloudflare) - One-click deploy a Tauri Update Server to Cloudflare.
|
||||
- [tauri-update-server](https://git.kaki87.net/KaKi87/tauri-update-server) - Automatically interface the Tauri updater with git repository releases.
|
||||
- [vite-plugin-tauri](https://github.com/amrbashir/vite-plugin-tauri) - Integrate Tauri in a Vite project to build cross-platform apps.
|
||||
|
||||
### Articles
|
||||
|
||||
- [Getting Started Using Tauri Mobile](https://medium.com/p/6f90de5b098) ![paid] - Ed Rutherford outlines how to create a mobile app with Tauri.
|
||||
- [How to use local SQLite database with Tauri and Rust](https://blog.moonguard.dev/how-to-use-local-sqlite-database-with-tauri) - Guide to setup and use SQLite database with Tauri and Rust.
|
||||
- [Managing State in Desktop Applications with Rust and Tauri](https://blog.moonguard.dev/manage-state-with-tauri) - How to share and manage any kind of state globally in Tauri apps.
|
||||
- [Setting up Actix Web in a Tauri App](https://blog.moonguard.dev/setting-up-actix-in-tauri) - How to setup a HTTP server with Tauri and Actix Web.
|
||||
- [Tauri's async process](https://rfdonnelly.github.io/posts/tauri-async-rust-process/) - Rob Donnelly dives deep into Async with Tauri.
|
||||
|
||||
## Applications
|
||||
|
||||
### Audio & Video
|
||||
|
||||
- [Ascapes Mixer](https://github.com/ilyaly/ascapes-mixer) - Audio mixer with three dedicated players for music, ambience and SFX for TTRPG sessions.
|
||||
- [Cap](https://github.com/CapSoftware/cap) - The open-source Loom alternative. Beautiful, shareable screen recordings.
|
||||
- [Cardo](https://github.com/n0vella/cardo) - Podcast player with integrated search and management of subscriptions.
|
||||
- [Compresso](https://github.com/codeforreal1/compressO) - Cross-platform video compression app powered by FFmpeg.
|
||||
- [Curses](https://github.com/mmpneo/curses) - Speech-to-Text and Text-to-Speech captions for OBS, VRChat, Twitch chat and more.
|
||||
- [Douyin Downloader](https://github.com/lzdyes/douyin-downloader) - Cross-platform douyin video downloader.
|
||||
- [Feiyu Player](https://github.com/idootop/feiyu-player) - Cross-platform online video player where beauty meets functionality.
|
||||
- [Hypetrigger](https://hypetrigger.io/) ![closed source] - Detect highlight clips in video with FFMPEG + Tensorflow on the GPU.
|
||||
- [Hyprnote](https://github.com/fastrepl/hyprnote) - AI notepad for meetings. Local-first and extensible.
|
||||
- [Jellyfin Vue](https://github.com/jellyfin/jellyfin-vue) - GUI client for a Jellyfin server based on Vue.js and Tauri.
|
||||
- [Lofi Engine](https://github.com/meel-hd/lofi-engine) - Generate Lo-Fi music on the go and locally.
|
||||
- [mediarepo](https://github.com/Trivernis/mediarepo) - Tag-based media management application.
|
||||
- [Mr Tagger](https://github.com/probablykasper/mr-tagger) - Music file tagging app.
|
||||
- [Musicat](https://github.com/basharovV/musicat) - Sleek desktop music player and tagger for offline music.
|
||||
- [screenpipe](https://github.com/louis030195/screen-pipe) - Build AI apps based on all your screens & mics context.
|
||||
- [ToneTempo](https://tonetempo.com) ![closed source] ![paid] - Workout and run with AutoMixed music and an AI fitness coach.
|
||||
- [Watson.ai](https://github.com/LatentDream/watson.ai) - Easily record and extract the most important information from your meetings.
|
||||
- [XGetter](https://github.com/xgetter-team/xgetter) ![closed source]- Cross-platform GUI to download videos and audio from Youtube, Facebook, X(Twitter), Instagram, Tiktok and more.
|
||||
- [yt-dlp GUI](https://github.com/gaeljacquin/yt-dlp-gui) - Cross-platform GUI client for the `yt-dlp` command-line audio/video downloader.
|
||||
|
||||
### ChatGPT clients
|
||||
|
||||
- [ChatGPT](https://github.com/lencx/ChatGPT) - Cross-platform ChatGPT desktop application.
|
||||
- [ChatGPT-Desktop](https://github.com/Synaptrix/ChatGPT-Desktop) - Cross-platform productivity ChatGPT assistant launcher.
|
||||
- [Kaas](https://github.com/0xfrankz/Kaas) - Cross-platform desktop LLM client for OpenAI ChatGPT, Anthropic Claude, Microsoft Azure and more, with a focus on privacy and security.
|
||||
- [Orion](https://github.com/taecontrol/orion) - Cross-platform app that lets you create multiple AI assistants with specific goals powered with ChatGPT.
|
||||
- [QuickGPT](https://github.com/dubisdev/quickgpt) - Lightweight AI assistant for Windows.
|
||||
- [Yack](https://github.com/rajatkulkarni95/yack) - Spotlight like app for interfacing with GPT APIs.
|
||||
|
||||
### Data
|
||||
|
||||
- [Annimate](https://github.com/matthias-stemmler/annimate) - Convenient export of query results from the ANNIS system for linguistic corpora.
|
||||
- [BS Redis Desktop Client](https://github.com/fuyoo/bs-redis-desktop-client) - The Best Surprise Redis Desktop Client.
|
||||
- [Dataflare](https://dataflare.app) ![closed source] ![paid] - Simple and elegant database manager.
|
||||
- [DocKit](https://github.com/geek-fun/dockit) - GUI client for NoSQL databases such as elasticsearch, OpenSearch, etc.
|
||||
- [Duckling](https://github.com/l1xnan/duckling) - Lightweight and fast viewer for csv/parquet files and databases such as DuckDB, SQLite, PostgreSQL, MySQL, Clickhouse, etc.
|
||||
- [Elasticvue](https://elasticvue.com/) - Free and open-source Elasticsearch GUI
|
||||
- [Noir](https://noirdb.dev) - Keyboard-driven database management client.
|
||||
- [pgMagic🪄](https://pgmagic.app/?ref=awesometauri) ![closed source] ![paid] - GUI client to talk to Postgres in SQL or with natural language.
|
||||
- [qsv pro](https://qsvpro.dathere.com) ![closed source] ![paid] - Explore spreadsheet data including CSV in interactive data tables with generated metadata and a node editor based on the `qsv` CLI.
|
||||
- [Rclone UI](https://rcloneui.com) - The cross-platform desktop GUI for **`rclone`** & S3.
|
||||
- [SmoothCSV](https://smoothcsv.com/) ![closed source] - Powerful and intuitive tool for editing CSV files with spreadsheet-like interface.
|
||||
|
||||
### Developer tools
|
||||
|
||||
- [AHQ Store](https://github.com/ahqsoftwares/tauri-ahq-store) - Publish, Update and Install apps to the Windows-specific AHQ Store.
|
||||
- [AppCenter Companion](https://github.com/zenoxs/tauri-appcenter-companion) - Regroup, build and track your `VS App Center` apps.
|
||||
- [AppHub](https://github.com/francesco-gaglione/AppHub) - Streamlines .appImage package installation, management, and uninstallation through an intuitive Linux desktop interface.
|
||||
- [Aptakube](https://aptakube.com/) ![closed source] - Multi-cluster Kubernetes UI.
|
||||
- [Brew Services Manage](https://github.com/persiliao/brew-services-manage)![closed source] macOS Menu Bar application for managing Homebrew services.
|
||||
- [claws](https://clawsapp.com/) ![closed source] - Visual interface for the AWS CLI.
|
||||
- [CrabNebula DevTools](https://crabnebula.dev/devtools) - Visual tool for understanding your app. Optimize the development process with easy debugging and profiling.
|
||||
- [CrabNebula DevTools Premium](https://crabnebula.dev/devtools) ![closed source] ![paid] - Optimize the development process with easy debugging and profiling. Debug the Rust portion of your app with the same comfort as JavaScript!
|
||||
- [DevBox](https://www.dev-box.app/) ![closed source] - Many useful tools for developers, like generators, viewers, converters, etc.
|
||||
- [DevClean](https://github.com/HuakunShen/devclean) - Clean up development environment with ease.
|
||||
- [DevTools-X](https://github.com/fosslife/devtools-x) - Collection of 30+ cross platform development utilities.
|
||||
- [Dropcode](https://github.com/egoist/dropcode) - Simple and lightweight code snippet manager.
|
||||
- [Echoo](https://github.com/zsmatrix62/echoo-app) - Offline/Online utilities for developers on MacOS & Windows.
|
||||
- [GitButler](https://gitbutler.com) - GitButler is a new Source Code Management system.
|
||||
- [GitLight](https://github.com/colinlienard/gitlight) - GitHub & GitLab notifications on your desktop.
|
||||
- [JET Pilot](https://www.jet-pilot.app) - Kubernetes desktop client that focuses on less clutter, speed and good looks.
|
||||
- [Hoppscotch](https://hoppscotch.com/download) ![closed source] - Trusted by millions of developers to build, test and share APIs.
|
||||
- [Keadex Mina](https://github.com/keadex/keadex) - Open Source, serverless IDE to easily code and organize at a scale C4 model diagrams.
|
||||
- [KFtray](https://github.com/hcavarsan/kftray) - A tray application that manages port forwarding in Kubernetes.
|
||||
- [PraccJS](https://github.com/alyalin/PraccJS) - Lets you practice JavaScript with real-time code execution.
|
||||
- [nda](https://github.com/kuyoonjo/nda) - Network Debug Assistant - UDP, TCP, Websocket, SocketIO, MQTT
|
||||
- [Ngroker](https://ngroker.com) ![closed source] ![paid] - 🆖ngrok gui client.
|
||||
- [Soda](https://github.com/Web3-Builders-Alliance/soda) - Generate source code from an IDL.
|
||||
- [Pake](https://github.com/tw93/Pake) - Turn any webpage into a desktop app with Rust with ease.
|
||||
- [Rivet](https://github.com/Ironclad/rivet) - Visual programming environment for creating AI features and agents.
|
||||
- [TableX](https://tablex-tan.vercel.app/) - Table viewer for modern developers
|
||||
- [Tauri Mobile Test](https://github.com/dedSyn4ps3/tauri-mobile-test) - Create and build cross-platform mobile applications.
|
||||
- [Testfully](https://testfully.io/) ![closed source] ![paid] - Offline API Client & Testing tool.
|
||||
- [verbcode](https://github.com/Verbcode/verbcode-release) ![closed source] - Simplify your localization journey.
|
||||
- [Worktree Status](https://github.com/sandercox/worktree-status/) - Get git repo status in your macOS MenuBar or Windows notification area.
|
||||
- [Yaak](https://yaak.app) - Organize and execute REST, GraphQL, and gRPC requests.
|
||||
|
||||
### Ebook readers
|
||||
|
||||
- [Alexandria](https://github.com/btpf/Alexandria) - Minimalistic cross-platform eBook reader.
|
||||
- [Jane Reader](https://janereader.com) ![closed source] - Modern and distraction-free epub reader.
|
||||
- [Readest](https://github.com/chrox/readest) - Modern and feature-rich ebook reader designed for avid readers.
|
||||
- [Cloak](https://github.com/Xav1erSue/cloak) - A see-through mini reader for slacking off at work.
|
||||
|
||||
### Email & Feeds
|
||||
|
||||
- [Alduin](https://alduin.stouder.io/) - Alduin is a free and open source RSS, Atom and JSON feed reader that allows you to keep track of your favorite websites.
|
||||
- [Aleph](https://github.com/chezhe/aleph) - Aleph is an RSS reader & podcast client.
|
||||
- [BULKUS](https://github.com/KM8Oz/BULKUS) - Email validation software.
|
||||
- [Lettura](https://github.com/zhanglun/lettura) - Open-source feed reader for macOS.
|
||||
- [mdsilo Desktop](https://github.com/mdSilo/mdSilo-app) - Feed reader and knowledge base.
|
||||
|
||||
### File management
|
||||
|
||||
- [CzkawkaTauri](https://github.com/shixinhuang99/czkawka-tauri) - Multi functional app to find duplicates, empty folders, similar images etc.
|
||||
- [enassi](https://github.com/enassi/enassi) - Encryption assistant that encrypts and stores your notes and files.
|
||||
- [EzUp](https://github.com/HuakunShen/ezup) - File and Image uploader. Designed for blog writing and note taking.
|
||||
- [Orange](https://github.com/naaive/orange) - Cross-platform file search engine that can quickly locate files or folders based on keywords.
|
||||
- [Payload](https://payload.app/) ![closed source] - Drag & drop file transfers over local networks and online.
|
||||
- [Spacedrive](https://github.com/spacedriveapp/spacedrive) - A file explorer from the future.
|
||||
- [SquirrelDisk](https://github.com/adileo/squirreldisk) - Beautiful cross-platform disk usage analysis tool.
|
||||
- [Time Machine Inspector](https://github.com/probablykasper/time-machine-inspector) - Find out what's taking up your Time Machine backup space.
|
||||
- [Xplorer](https://github.com/kimlimjustin/xplorer) - Customizable, modern and cross-platform File Explorer.
|
||||
|
||||
### Finance
|
||||
|
||||
- [Compotes](https://github.com/Orbitale/Compotes) - Local bank account operations storage to vizualize them as graphs and customize them with rules and tags for better filtering.
|
||||
- [CryptoBal](https://github.com/Rabbit-Company/CryptoBal-Desktop) - Desktop application for monitoring your crypto assets.
|
||||
- [Ghorbu Wallet](https://github.com/matthias-wright/ghorbu-wallet) - Cross-platform desktop HD wallet for Bitcoin.
|
||||
- [Mahalli](https://github.com/AbdelilahOu/Mahalli-tauri) - Local first inventory and invoicing management app.
|
||||
- [nym-wallet](https://github.com/nymtech/nym/tree/develop/nym-wallet) - The Nym desktop wallet enables you to use the Nym network and take advantage of its key capabilities.
|
||||
- [Upcount](https://www.upcount.app/) ![v2] - Free invoicing and time tracking application for freelancers and small businesses.
|
||||
- [UsTaxes](https://github.com/ustaxes/ustaxes) - Free, private, open-source US tax filings.
|
||||
- [Wealthfolio](https://wealthfolio.app) - Simple, open-source desktop portfolio tracker that keeps your financial data safe on your computer.
|
||||
|
||||
### Gaming
|
||||
|
||||
- [9Launcher](https://github.com/wearrrrr/9Launcher) - Modern Cross-platform launcher for Touhou Project Games.
|
||||
- [BestCraft](https://github.com/Tnze/ffxiv-best-craft) - Crafting simulator with solver algorithms for Final Fantasy XIV(FF14).
|
||||
- [BetterFleet](https://github.com/zelytra/BetterFleet) - Help players of Sea of Thieves create an alliance server.
|
||||
- [clear](https://clear.adithya.zip) - Clean and minimalist video game library manager and launcher.
|
||||
- [CubeShuffle](https://github.com/philipborg/CubeShuffle) - Card game shuffling utility.
|
||||
- [En Croissant](https://github.com/franciscoBSalgueiro/en-croissant) - Chess database and game analysis app.
|
||||
- [FishLauncher](https://github.com/fishfight/FishLauncher) - Cross-platform launcher for `Fish Fight`.
|
||||
- [Gale](https://github.com/Kesomannen/gale) - Mod manager for many games on `Thunderstore`.
|
||||
- [Modrinth App](https://github.com/modrinth/code/blob/main/apps/app) - Cross-platform launcher for `Minecraft` with mod management.
|
||||
- [OpenGOAL](https://github.com/open-goal/launcher) - Cross-platform installer, mod-manager and launcher for `OpenGOAL`; the reverse engineered PC ports of the Jak and Daxter series.
|
||||
- [Outer Wilds Mod Manager](https://github.com/ow-mods/ow-mod-man) - Cross-platform mod manager for `Outer Wilds`.
|
||||
- [OyasumiVR](https://github.com/Raphiiko/OyasumiVR) - Software that helps you sleep in virtual reality, for use with SteamVR, VRChat, and more.
|
||||
- [Rai Pal](https://github.com/raicuparta/rai-pal) - Manager for universal mods such as `UEVR` and `UUVR`.
|
||||
- [Resolute](https://github.com/Gawdl3y/Resolute) - User-friendly, cross-platform mod manager for the game Resonite.
|
||||
- [Retrom](https://github.com/JMBeresford/retrom) - Private cloud game library distribution server + frontend/launcher.
|
||||
- [Samira](https://github.com/jsnli/Samira) - Steam achievement manager for Linux.
|
||||
- [Steam Art Manager](https://github.com/Tormak9970/Steam-Art-Manager) - Tool for customizing the art of your Steam games.
|
||||
- [Tauri Chess](https://github.com/jamessizeland/tauri-chess) - Implementation of Chess, logic in Rust and visualization in React.
|
||||
- [Teyvat Guide](https://github.com/BTMuli/TeyvatGuide) - Game Tool for Genshin Impact player.
|
||||
- [Quadrant](https://github.com/mrquantumoff/quadrant/) - Tool for managing Minecraft mods and modpacks with the ability to use Modrinth and CurseForge.
|
||||
|
||||
### Information
|
||||
|
||||
- [Cores](https://github.com/Levminer/cores) ![paid] - Modern hardware monitor with remote monitoring.
|
||||
- [Seismic](https://github.com/breadthe/seismic) - Taskbar app for USGS earthquake tracking.
|
||||
- [Stockman](https://github.com/awkj/stockman) - Display stock info on mac menubar.
|
||||
- [Watchcoin](https://github.com/lifecoder1988/tauri-watch-coin) - Display cypto price on OS menubar without a window.
|
||||
|
||||
### Learning
|
||||
|
||||
- [Japanese](https://github.com/meel-hd/japanese) - Learn Japanese Hiragana and Katakana. Memorize, write, pronounce, and test your knowledge.
|
||||
- [Manjaro Starter](https://github.com/oguzkaganeren/manjaro-starter) - Documentation and support app for new Manjaro users.
|
||||
- [Piano Trainer](https://github.com/ZaneH/piano-trainer) - Practice piano chords, scales, and more using your MIDI keyboard.
|
||||
- [Solars](https://github.com/hiltontj/solars) - Visualize the planets of our solar system.
|
||||
- [Syre](https://github.com/syre-data/syre) - Scientific data assistant.
|
||||
- [Rosary](https://github.com/Roseblume/Rosary) - Study Christianity.
|
||||
|
||||
### Networking
|
||||
|
||||
- [Clash Verge Rev](https://github.com/clash-verge-rev/clash-verge-rev) - Continuation of Clash Verge, a rule-based proxy.
|
||||
- [CyberAPI](https://github.com/vicanso/cyberapi) - API tool client for developer.
|
||||
- [Jexpe](https://github.com/jexpe-apps/jexpe) - Cross-platform, open source SSH and SFTP client that makes connecting to your remote servers easy.
|
||||
- [Mail-Dev](https://github.com/samirdjelal/mail-dev) - Cross-platform, local SMTP server for email testing/debugging.
|
||||
- [mDNS-Browser](https://github.com/hrzlgnm/mdns-browser) - Cross-platform mDNS browser app for discovering network services using mDNS.
|
||||
- [Nhex](https://github.com/nhexirc/nhex) - Next-generation IRC client inspired by HexChat.
|
||||
- [RustDesk](https://github.com/rustdesk/rustdesk-server) - Self-hosted server for RustDesk, an open source remote desktop.
|
||||
- [RustDuck](https://github.com/thewh1teagle/RustDuck) - Cross platform dynamic DNS updater for duckdns.org.
|
||||
- [T-Shell](https://github.com/TheBlindM/T-Shell) - An open-source SSH, SFTP intelligent command line terminal application.
|
||||
- [TunnlTo](https://github.com/TunnlTo/desktop-app) - Windows WireGuard VPN client built for split tunneling.
|
||||
- [UpVPN](https://github.com/upvpn/upvpn-app) - WireGuard VPN client for Linux, macOS, and Windows.
|
||||
- [Watcher](https://github.com/windht/watcher) - API manager built for a easier use to manage and collaborate.
|
||||
- [Wirefish](https://github.com/stefanodevenuto/wirefish) - Cross-platform packet sniffer and analyzer.
|
||||
|
||||
### Office & Writing
|
||||
|
||||
- [fylepad](https://github.com/imrofayel/fylepad/) - Notepad with powerful rich-text editing, built with Vue & Tauri.
|
||||
- [Bidirectional](https://github.com/samirdjelal/bidirectional) - Write Arabic text in apps that don't support bidirectional text.
|
||||
- [Blank](https://github.com/FPurchess/blank) - Minimalistic, opinionated markdown editor made for writing.
|
||||
- [Ensō](https://enso.sonnet.io) ![closed source] - Write now, edit later. Ensō is a writing tool that helps you enter a state of flow.
|
||||
- [Handwriting keyboard](https://github.com/BigIskander/Handwriting-keyboard-for-Linux-tesseract) - Handwriting keyboard for Linux X11 desktop environment.
|
||||
- [JournalV](https://github.com/ahmedkapro/journalv) - Journaling app for your days and dreams.
|
||||
- [MarkFlowy](https://github.com/drl990114/MarkFlowy) - Modern markdown editor application with built-in ChatGPT extension.
|
||||
- [MD Viewer](https://github.com/kuyoonjo/md-viewer) - Cross-platform markdown viewer.
|
||||
- [MDX Notes](https://github.com/maqi1520/mdx-notes/tree/tauri-app) - Versatile WeChat typesetting editor and cross-platform Markdown note-taking software.
|
||||
- [Noor](https://noor.to/) ![closed source] - Chat app for high-performance teams. Designed for uninterrupted deep work and rapid collaboration.
|
||||
- [Notpad](https://github.com/Muhammed-Rahif/Notpad) - Cross-platform rich text editor with a notepad interface, enhanced with advanced features beyond standard notepad.
|
||||
- [Parchment](https://github.com/tywil04/parchment) - Simple local-only cross-platform text editor with basic markdown support.
|
||||
- [Semanmeter](https://yibiao.fun/) ![closed source] - OCR and document conversion software.
|
||||
- [Ubiquity](https://github.com/opensourcecheemsburgers/ubiquity) - Cross-platform markdown editor; built with Yew, Tailwind, and DaisyUI.
|
||||
- [HuLa](https://github.com/HuLaSpark/HuLa) - HuLa is a desktop instant messaging app built on Tauri+Vue3 (not just instant messaging).
|
||||
- [Gramax](https://github.com/Gram-ax/gramax) - Free, open-source application for creating, editing, and publishing Git-driven documentation sites using Markdown and a visual editor.
|
||||
|
||||
### Productivity
|
||||
|
||||
- [Banban](https://github.com/HubertK05/banban) - Kanban board with tags, categories and markdown support.
|
||||
- [Blink Eye](https://github.com/nomandhoni-cs/blink-eye) - A minimalist eye care reminder app to reduce eye strain, featuring customizable timers , full-screen popups, and screen-on-time.
|
||||
- [BuildLog](https://github.com/rajatkulkarni95/buildlog) - Menu bar for keeping track of Vercel Deployments.
|
||||
- [Clippy](https://github.com/0-don/clippy) - Clipboard manager with sync & encryption.
|
||||
- [Dalgona](https://github.com/GHGHGHKO/dalgona) - GIF meme finder app for Windows and macOS.
|
||||
- [EcoPaste](https://github.com/ayangweb/EcoPaste/tree/master) - Powerful open-source clipboard manager for macOS, Windows and Linux(x11) platforms.
|
||||
- [Fairies](https://fairies.ai) ![closed source] ![paid] - Powerful, general-purpose AI agent for MacOS, Windows, Mobile, and Web.
|
||||
- [Floweb](https://floweb.cn/en) ![closed source] ![paid] - Ultra-lightweight floating desktop pendant that transforms web pages into web applications, supporting features such as pinning and transparency, multi-account, auto-refresh.
|
||||
- [Fluster](https://fluster-one.vercel.app) ![v2] - The one stop, free and open source note taking application for everything a modern academic or STEM professional needs.
|
||||
- [GitBar](https://github.com/mikaelkristiansson/gitbar) - System tray app for GitHub reviews.
|
||||
- [Gitification](https://github.com/Gitification-App/gitification) - Menu bar app for managing Github notifications.
|
||||
- [Google Task Desktop Client](https://github.com/codad5/google-task-tauri) - Google Task Desktop Client
|
||||
- [HackDesk](https://github.com/EastSun5566/hackdesk) - Hackable HackMD desktop application.
|
||||
- [jasnoo](https://jasnoo.com) ![closed source] ![paid] - Desktop software designed to help you solve problems, prioritise daily actions and focus
|
||||
- [Kanri](https://github.com/trobonox/kanri) - Cross-platform, offline-first Kanban board app with a focus on simplicity and user experience.
|
||||
- [Kianalol](https://github.com/zxh3/kianalol) - Spotlight-like efficiency tool for swift website access.
|
||||
- [Kunkun](https://kunkun.sh/) - Cross-platform, extensible app launcher. Alternative to Alfred and Raycast.
|
||||
- [Link Saas](https://github.com/linksaas/desktop) - Efficiency tools for software development teams.
|
||||
- [MacroGraph](https://github.com/Brendonovich/macrograph) - Visual programming for content creators.
|
||||
- [MeadTools](https://github.com/ljreaux/meadtools-desktop) - All-in-one Mead, Wine, and Cider making calculator.
|
||||
- [mynd](https://github.com/Gnarus-G/mynd) - Quick and very simple todo-list management app for developers that live mostly in the terminal.
|
||||
- [Obliqoro](https://github.com/mrjackwills/obliqoro) - Oblique Strategies meets Pomodoro.
|
||||
- [PasteBar](https://github.com/PasteBar/PasteBarApp) - Limitless, Free Clipboard Manager for Mac and Windows. Effortless management of everything you copy and paste.
|
||||
- [Pomodoro](https://github.com/g07cha/pomodoro) - Time management tool based on Pomodoro technique.
|
||||
- [Qopy](https://github.com/0PandaDEV/Qopy) - The fixed Clipboard Manager for Windows and Mac.
|
||||
- [Remind Me Again](https://github.com/probablykasper/remind-me-again) - Toggleable reminders app for Mac, Linux and Windows.
|
||||
- [Takma](https://github.com/jam53/Takma) - Kanban-style to-do app, fully offline with support for Markdown, labels, due dates, checklists and deep linking.
|
||||
- [Tencent Yuanbao](https://yuanbao.tencent.com/) ![closed source] - Tencent Yuanbao is an AI application based on Tencent Hunyuan large model. It is an all-round assistant that can help you with writing, painting, copywriting, translation, programming, searching, reading and summarizing.
|
||||
- [TimeChunks](https://danielulrich.com/en/timechunks/) ![closed source] - Time tracking for freelancers without timers and HH:MM:SS inputs.
|
||||
- [WindowPet](https://github.com/SeakMengs/WindowPet) - Overlay app that lets you have adorable companions such as pets and anime characters on your screen.
|
||||
- [Zawee](https://zawee.net) ![closed source] - Experience the synergy of Kanban boards, note-taking, file sharing, and more, seamlessly integrated into one powerful application.
|
||||
- [Zenith](https://github.com/dewy01/Zenith) ![v1] - Whether you're looking to organize tasks, collaborate on projects, or keep track of important dates, Zenith has got you covered.
|
||||
- [ZeroLaunch-rs](https://github.com/ghost-him/ZeroLaunch-rs) - Focuses on app launching with error correction, supports full/pinyin/abbreviation searches. Features customizable interface and keyboard shortcuts.
|
||||
|
||||
### Search
|
||||
|
||||
- [Coco AI](http://coco.rs/) - 🥥 Coco AI unifies all your enterprise applications and data—Google Workspace, Dropbox, GitHub, and more—into one powerful search and Gen-AI chat platform.
|
||||
- [Harana](https://github.com/harana/search) - Search your desktop and 300+ cloud apps, instantly.
|
||||
- [Spyglass](https://github.com/a5huynh/spyglass) - Personal search engine that indexes your files/folders, cloud accounts, and whatever interests you on the internet.
|
||||
|
||||
### Security
|
||||
|
||||
- [Authme](https://github.com/Levminer/authme) - Two-factor (2FA) authentication app for desktop.
|
||||
- [Calciumdibromid](https://codeberg.org/Calciumdibromid/CaBr2) - Generate "experiment wise safety sheets" in compliance to European law.
|
||||
- [Defguard](https://github.com/defguard/client) - WireGuard VPN destkop client with Two-factor (2FA) authentication.
|
||||
- [Gluhny](https://github.com/angeldollface/gluhny) A graphical interface to validate IMEI numbers.
|
||||
- [OneKeePass](https://github.com/OneKeePass/desktop) - Secure, modern, cross-platform and KeePass compatible password manager.
|
||||
- [Padloc](https://github.com/padloc/padloc) - Modern, open source password manager for individuals and teams.
|
||||
- [Secops](https://github.com/kunalsin9h/secops) - Ubuntu Operating System security made easy.
|
||||
- [Tauthy](https://github.com/pwltr/tauthy) - Cross-platform TOTP authentication client.
|
||||
- [Truthy](https://github.com/fosslife/truthy/) - Modern cross-platform 2FA manager with tons of features and a beautiful UI.
|
||||
|
||||
### Social media
|
||||
|
||||
- [Dorion](https://github.com/SpikeHD/Dorion) - Light weight third-party Discord client with support for plugins and themes.
|
||||
- [Identia](https://github.com/iohzrd/identia) - Decentralized social media on IPFS.
|
||||
- [Kadium](https://github.com/probablykasper/kadium) - App for staying on top of YouTube channel uploads.
|
||||
- [Poll-arize](https://poll-arize.com/?ref=awesome-tauri) ![closed source] - Social media platform focused on polling and aggregating user opinions.
|
||||
- [Scraper Instagram GUI Desktop](https://git.kaki87.net/KaKi87/scraper-instagram-gui-desktop) - Alternative Instagram front-end for desktop.
|
||||
|
||||
### Utilities
|
||||
|
||||
- [AgeTimer](https://github.com/dhextras/age-timer-tauri) - Desktop utility that counts your age in real-time.
|
||||
- [Auto Wallpaper](https://github.com/auto-wallpaper/auto-wallpaper) - Automatically generates 4K wallpapers based on user's location, weather, and time of day or any custom prompts.
|
||||
- [bewCloud Desktop Sync](https://github.com/bewcloud/bewcloud-desktop) - Desktop sync app for bewCloud, a simpler alternative to Nextcloud and ownCloud.
|
||||
- [TypeView - KeyStroke Visualizer](https://github.com/dunkbing/typeview) - Visualizes keys pressed on the screen and simulates the sound of mechanical keyboard.
|
||||
- [Browsernaut](https://github.com/billyjacoby/browsernaut) - Browser picker for macOS.
|
||||
- [Clipboard Record](https://github.com/lesterhnu/clipboard) - Record Clipboard Content.
|
||||
- [Dwall](https://github.com/dwall-rs/dwall) - Change the Windows desktop and lock screen wallpapers according to the sun's azimuth and altitude angles, just like on macOS.
|
||||
- [Fancy Screen Recorder](https://fancyapps.com/freebies/) ![closed source] - Record entire screen or a selected area, trim and save as a GIF or video.
|
||||
- [FanslySync](https://github.com/SticksDev/FanslySync) - Sync your Fansly data with 3rd party applications, securely!
|
||||
- [Flying Carpet](https://github.com/spieglt/flyingcarpet) - File transfer between Android, iOS, Linux, macOS, and Windows over auto-configured hotspot.
|
||||
- [Get Unique ID](https://github.com/hiql/get-unique-id-app) - Generates unique IDs for you to use in debugging, development, or anywhere else you may need a unique ID.
|
||||
- [Happy](https://github.com/thewh1teagle/happy) - Control HappyLight compatible LED strip with ease.
|
||||
- [Imagenie](https://github.com/zhongweili/imagenie) - AI-powered desktop app for stunning image transformations
|
||||
- [KoS - Key on Screen](https://github.com/dubisdev/key-on-screen) - Show in your screen the keys you are pressing.
|
||||
- [Lanaya](https://github.com/ChurchTao/Lanaya) - Easy to use, cross-platform clipboard management.
|
||||
- [Lingo](https://github.com/thewh1teagle/lingo) - Translate offline in every language on every platform.
|
||||
- [Linka!](https://github.com/linka-app/linka) - AI powered, easy to use, cross-platform bookmark management tool.
|
||||
- [Locus](https://github.com/Sushants-Git/locus) - Intelligent activity tracker that helps you understand and improve your focus habits.
|
||||
- [MagicMirror](https://github.com/idootop/MagicMirror) - Instant AI Face Swap, Hairstyles & Outfits — One click to a brand new you!
|
||||
- [MBTiles Viewer](https://github.com/Akylas/mbview-rs) - MBTiles Viewer and Inspector.
|
||||
- [Metronome](https://github.com/ZaneH/metronome) - Visual metronome for Windows, Linux and macOS.
|
||||
- [Mobslide](https://github.com/thewh1teagle/mobslide) - Turn your smartphone into presentation remote controller.
|
||||
- [NeoHtop](https://github.com/Abdenasser/neohtop) - Cross platform system monitoring tool with a model look and feel.
|
||||
- [Overlayed](https://overlayed.dev) - Voice chat overlay for Discord.
|
||||
- [Pachtop](https://pachtop.com/) - Modern Cross-platform system monitor 🚀
|
||||
- [Passwords](https://github.com/hiql/passwords-app) - A random password generator.
|
||||
- [Pavo](https://github.com/zhanglun/pavo) - Cross-platform desktop wallpaper application.
|
||||
- [Peekaboo](https://github.com/angeldollface/peekaboo) A graphical interface to display images.
|
||||
- [Pointless](https://github.com/kkoomen/pointless) - Endless drawing canvas.
|
||||
- [Pot](https://github.com/pot-app/pot-desktop) - Cross-platform Translation Software.
|
||||
- [RMBG](https://github.com/zhbhun/rmbg) - Cross-platform image background removal tool.
|
||||
- [Recordscript](https://github.com/Recordscript/recordscript) - Record & transcribe your online meetings, or subtitle your files. Cross-platform local-only screen recorder & subtitle generator.
|
||||
- [Rounded Corners](https://github.com/RoundedCorners/Application) - Rounded Corners app for Windows.
|
||||
- [RunMath](https://github.com/dubisdev/runmath) - Keyboard-first calculator for Windows.
|
||||
- [SensiMouse](https://github.com/Nicify/sensi-mouse) - Easily change macOS system-wide mouse sensitivity and acceleration settings.
|
||||
- [SlimeVR Server](https://github.com/SlimeVR/SlimeVR-Server) - Server app for SlimeVR, facilitating full-body tracking in virtual reality.
|
||||
- [SoulFire](https://github.com/AlexProgrammerDE/SoulFireClient) - Advanced Minecraft Server-Stresser Tool. Launch bot attacks on your servers to measure performance.
|
||||
- [Stable Diffusion Buddy](https://github.com/breadthe/sd-buddy) - Desktop UI companion for the self-hosted Mac version of Stable Diffusion.
|
||||
- [Stacks](https://github.com/cablehead/stacks) - Modern and capable clipboard manager for macOS. Seeking Linux and Windows contributions.
|
||||
- [SwitchShuttle](https://github.com/s00d/switchshuttle) - Cross-platform system tray application that allows users to run predefined commands in various terminal applications.
|
||||
- [Tauview](https://github.com/sprout2000/tauview) - Minimalist image viewer for macOS and Linux based on Leaflet.js.
|
||||
- [Tmus](https://github.com/saicem/tmus) ![v2] - Monitor your time on desktop apps and visualize it with various charts.
|
||||
- [ToeRings](https://github.com/acarl005/toerings) - Conky Seamod inspired system monitor app.
|
||||
- [Toolcat](https://toolcat.app) ![closed source] - All-in-one toolkit for developers and creators.
|
||||
- [TrayFier](https://github.com/dubisdev/trayfier) - Supercharge your Windows Tray with links, files, executables...
|
||||
- [TrguiNG](https://github.com/openscopeproject/TrguiNG) - Remote GUI for Transmission torrent daemon.
|
||||
- [Verve](https://github.com/ParthJadhav/verve) - Launcher for accessing and opening applications, files and documents.
|
||||
- [Vibe](https://thewh1teagle.github.io/vibe) - Transcribe audio or video in every language on every platform.
|
||||
- [Wallpaper changer](https://github.com/zeet2020/wallpaper-changer-tauri) - Simple wallpaper changer app.
|
||||
- [Zap](https://usezap.sh/?ref=awesometauri) ![closed source] - macOS spotlight-like dock that makes navigating apps convenient.
|
||||
- [Sofast](https://sofast.fun) ![closed source] - A cross-platform Raycast-like app.
|
||||
|
||||
[officially maintained]: https://img.shields.io/badge/official-FFC131?&logo=tauri&logoColor=black
|
||||
[closed source]: https://img.shields.io/badge/closed%20source-FFC131?&logoColor=black
|
||||
[paid]: https://img.shields.io/badge/paid-FFC131?&logoColor=black
|
||||
[youtube]: https://img.shields.io/badge/YouTube-FF0000
|
||||
[v1]: https://img.shields.io/badge/v1-white
|
||||
[v2]: https://img.shields.io/badge/v2-white
|
||||
@@ -9,6 +9,7 @@ import AiClassificationSettings from './pages/AiClassificationSettings';
|
||||
import TemplateManagement from './pages/TemplateManagement';
|
||||
import { MaterialModelBinding } from './pages/MaterialModelBinding';
|
||||
import Tools from './pages/Tools';
|
||||
import OutfitMatch from './pages/OutfitMatch';
|
||||
import Navigation from './components/Navigation';
|
||||
import { NotificationSystem, useNotifications } from './components/NotificationSystem';
|
||||
import { useProjectStore } from './store/projectStore';
|
||||
@@ -80,6 +81,7 @@ function App() {
|
||||
<Route path="/templates" element={<TemplateManagement />} />
|
||||
<Route path="/material-model-binding" element={<MaterialModelBinding />} />
|
||||
<Route path="/tools" element={<Tools />} />
|
||||
<Route path="/outfit-match/:projectId" element={<OutfitMatch />} />
|
||||
</Routes>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
340
apps/desktop/src/components/outfit/ImageUploader.tsx
Normal file
340
apps/desktop/src/components/outfit/ImageUploader.tsx
Normal file
@@ -0,0 +1,340 @@
|
||||
import React, { useState, useCallback, useRef } from 'react';
|
||||
import {
|
||||
PhotoIcon,
|
||||
CloudArrowUpIcon,
|
||||
XMarkIcon,
|
||||
EyeIcon
|
||||
} from '@heroicons/react/24/outline';
|
||||
import { useNotifications } from '../NotificationSystem';
|
||||
|
||||
interface ImageFile {
|
||||
file: File;
|
||||
preview: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
interface ImageUploaderProps {
|
||||
onUpload: (files: File[]) => Promise<void>;
|
||||
isUploading?: boolean;
|
||||
maxFiles?: number;
|
||||
maxFileSize?: number; // in MB
|
||||
acceptedFormats?: string[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const ImageUploader: React.FC<ImageUploaderProps> = ({
|
||||
onUpload,
|
||||
isUploading = false,
|
||||
maxFiles = 5,
|
||||
maxFileSize = 10,
|
||||
acceptedFormats = ['image/jpeg', 'image/png', 'image/webp'],
|
||||
className = ''
|
||||
}) => {
|
||||
const [selectedFiles, setSelectedFiles] = useState<ImageFile[]>([]);
|
||||
const [isDragOver, setIsDragOver] = useState(false);
|
||||
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const { addNotification } = useNotifications();
|
||||
|
||||
// 验证文件
|
||||
const validateFile = useCallback((file: File): string | null => {
|
||||
if (!acceptedFormats.includes(file.type)) {
|
||||
return `不支持的文件类型。请选择 ${acceptedFormats.join(', ')} 格式的图片。`;
|
||||
}
|
||||
|
||||
if (file.size > maxFileSize * 1024 * 1024) {
|
||||
return `文件大小超过限制。请选择小于 ${maxFileSize}MB 的图片。`;
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [acceptedFormats, maxFileSize]);
|
||||
|
||||
// 处理文件选择
|
||||
const handleFiles = useCallback((files: FileList) => {
|
||||
const newFiles: ImageFile[] = [];
|
||||
const errors: string[] = [];
|
||||
|
||||
Array.from(files).forEach((file) => {
|
||||
const error = validateFile(file);
|
||||
if (error) {
|
||||
errors.push(`${file.name}: ${error}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedFiles.length + newFiles.length >= maxFiles) {
|
||||
errors.push(`最多只能选择 ${maxFiles} 个文件`);
|
||||
return;
|
||||
}
|
||||
|
||||
const id = Math.random().toString(36).substr(2, 9);
|
||||
const preview = URL.createObjectURL(file);
|
||||
newFiles.push({ file, preview, id });
|
||||
});
|
||||
|
||||
if (errors.length > 0) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: '文件验证失败',
|
||||
message: errors.join('\n')
|
||||
});
|
||||
}
|
||||
|
||||
if (newFiles.length > 0) {
|
||||
setSelectedFiles(prev => [...prev, ...newFiles]);
|
||||
}
|
||||
}, [selectedFiles.length, maxFiles, validateFile, addNotification]);
|
||||
|
||||
// 拖拽处理
|
||||
const handleDragOver = useCallback((e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
setIsDragOver(true);
|
||||
}, []);
|
||||
|
||||
const handleDragLeave = useCallback((e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
setIsDragOver(false);
|
||||
}, []);
|
||||
|
||||
const handleDrop = useCallback((e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
setIsDragOver(false);
|
||||
|
||||
const files = e.dataTransfer.files;
|
||||
if (files.length > 0) {
|
||||
handleFiles(files);
|
||||
}
|
||||
}, [handleFiles]);
|
||||
|
||||
// 文件选择
|
||||
const handleFileSelect = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = e.target.files;
|
||||
if (files && files.length > 0) {
|
||||
handleFiles(files);
|
||||
}
|
||||
// 清空input值,允许重复选择同一文件
|
||||
e.target.value = '';
|
||||
}, [handleFiles]);
|
||||
|
||||
// 移除文件
|
||||
const removeFile = useCallback((id: string) => {
|
||||
setSelectedFiles(prev => {
|
||||
const updated = prev.filter(f => f.id !== id);
|
||||
// 清理预览URL
|
||||
const removed = prev.find(f => f.id === id);
|
||||
if (removed) {
|
||||
URL.revokeObjectURL(removed.preview);
|
||||
}
|
||||
return updated;
|
||||
});
|
||||
}, []);
|
||||
|
||||
// 清空所有文件
|
||||
const clearAll = useCallback(() => {
|
||||
selectedFiles.forEach(f => URL.revokeObjectURL(f.preview));
|
||||
setSelectedFiles([]);
|
||||
}, [selectedFiles]);
|
||||
|
||||
// 上传文件
|
||||
const handleUpload = useCallback(async () => {
|
||||
if (selectedFiles.length === 0) {
|
||||
addNotification({
|
||||
type: 'warning',
|
||||
title: '请选择文件',
|
||||
message: '请先选择要上传的图片文件'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const files = selectedFiles.map(f => f.file);
|
||||
await onUpload(files);
|
||||
|
||||
addNotification({
|
||||
type: 'success',
|
||||
title: '上传成功',
|
||||
message: `成功上传 ${files.length} 个文件`
|
||||
});
|
||||
|
||||
clearAll();
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: '上传失败',
|
||||
message: error instanceof Error ? error.message : '上传过程中发生错误'
|
||||
});
|
||||
}
|
||||
}, [selectedFiles, onUpload, addNotification, clearAll]);
|
||||
|
||||
// 预览图片
|
||||
const showPreview = useCallback((preview: string) => {
|
||||
setPreviewImage(preview);
|
||||
}, []);
|
||||
|
||||
// 清理内存
|
||||
React.useEffect(() => {
|
||||
return () => {
|
||||
selectedFiles.forEach(f => URL.revokeObjectURL(f.preview));
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={`space-y-4 ${className}`}>
|
||||
{/* 拖拽上传区域 */}
|
||||
<div
|
||||
className={`relative border-2 border-dashed rounded-xl p-8 text-center transition-all duration-300 ${
|
||||
isDragOver
|
||||
? 'border-primary-500 bg-primary-50'
|
||||
: 'border-gray-300 hover:border-gray-400'
|
||||
} ${isUploading ? 'opacity-50 pointer-events-none' : ''}`}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
multiple
|
||||
accept={acceptedFormats.join(',')}
|
||||
onChange={handleFileSelect}
|
||||
className="hidden"
|
||||
/>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="mx-auto w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center">
|
||||
<PhotoIcon className="w-8 h-8 text-gray-400" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
||||
上传服装图片
|
||||
</h3>
|
||||
<p className="text-gray-500 mb-4">
|
||||
拖拽图片到此处,或点击选择文件
|
||||
</p>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={isUploading}
|
||||
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<CloudArrowUpIcon className="w-4 h-4 mr-2" />
|
||||
选择图片
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-gray-400">
|
||||
支持 {acceptedFormats.map(type => type.split('/')[1].toUpperCase()).join(', ')} 格式,
|
||||
最大 {maxFileSize}MB,最多 {maxFiles} 个文件
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 已选择的文件列表 */}
|
||||
{selectedFiles.length > 0 && (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-sm font-medium text-gray-900">
|
||||
已选择 {selectedFiles.length} 个文件
|
||||
</h4>
|
||||
<button
|
||||
type="button"
|
||||
onClick={clearAll}
|
||||
className="text-sm text-gray-500 hover:text-gray-700"
|
||||
>
|
||||
清空全部
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||
{selectedFiles.map((imageFile) => (
|
||||
<div
|
||||
key={imageFile.id}
|
||||
className="relative group bg-white rounded-lg border border-gray-200 overflow-hidden shadow-sm hover:shadow-md transition-shadow"
|
||||
>
|
||||
<div className="aspect-square">
|
||||
<img
|
||||
src={imageFile.preview}
|
||||
alt={imageFile.file.name}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="absolute inset-0 bg-black bg-opacity-0 group-hover:bg-opacity-40 transition-all duration-200 flex items-center justify-center">
|
||||
<div className="opacity-0 group-hover:opacity-100 transition-opacity flex space-x-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => showPreview(imageFile.preview)}
|
||||
className="p-2 bg-white rounded-full text-gray-700 hover:text-gray-900"
|
||||
>
|
||||
<EyeIcon className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeFile(imageFile.id)}
|
||||
className="p-2 bg-white rounded-full text-red-600 hover:text-red-800"
|
||||
>
|
||||
<XMarkIcon className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-2">
|
||||
<p className="text-xs text-gray-600 truncate" title={imageFile.file.name}>
|
||||
{imageFile.file.name}
|
||||
</p>
|
||||
<p className="text-xs text-gray-400">
|
||||
{(imageFile.file.size / 1024 / 1024).toFixed(2)} MB
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleUpload}
|
||||
disabled={isUploading || selectedFiles.length === 0}
|
||||
className="inline-flex items-center px-6 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isUploading ? (
|
||||
<>
|
||||
<div className="animate-spin -ml-1 mr-2 h-4 w-4 border-2 border-white border-t-transparent rounded-full"></div>
|
||||
上传中...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CloudArrowUpIcon className="w-4 h-4 mr-2" />
|
||||
开始分析 ({selectedFiles.length})
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 图片预览模态框 */}
|
||||
{previewImage && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-75">
|
||||
<div className="relative max-w-4xl max-h-full p-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setPreviewImage(null)}
|
||||
className="absolute top-4 right-4 p-2 bg-white rounded-full text-gray-700 hover:text-gray-900 z-10"
|
||||
>
|
||||
<XMarkIcon className="w-6 h-6" />
|
||||
</button>
|
||||
<img
|
||||
src={previewImage}
|
||||
alt="预览"
|
||||
className="max-w-full max-h-full object-contain rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageUploader;
|
||||
334
apps/desktop/src/components/outfit/OutfitAnalysisResult.tsx
Normal file
334
apps/desktop/src/components/outfit/OutfitAnalysisResult.tsx
Normal file
@@ -0,0 +1,334 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
SparklesIcon,
|
||||
EyeIcon,
|
||||
ClockIcon,
|
||||
CheckCircleIcon,
|
||||
ExclamationTriangleIcon,
|
||||
ArrowPathIcon
|
||||
} from '@heroicons/react/24/outline';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { useNotifications } from '../NotificationSystem';
|
||||
|
||||
interface OutfitAnalysis {
|
||||
id: string;
|
||||
project_id: string;
|
||||
image_path: string;
|
||||
image_name: string;
|
||||
analysis_status: 'Pending' | 'Processing' | 'Completed' | 'Failed';
|
||||
analysis_result?: any;
|
||||
error_message?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
analyzed_at?: string;
|
||||
}
|
||||
|
||||
interface OutfitAnalysisResultProps {
|
||||
projectId: string;
|
||||
onCreateItems?: (analysisId: string) => void;
|
||||
}
|
||||
|
||||
const OutfitAnalysisResult: React.FC<OutfitAnalysisResultProps> = ({
|
||||
projectId,
|
||||
onCreateItems
|
||||
}) => {
|
||||
const [analyses, setAnalyses] = useState<OutfitAnalysis[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [selectedAnalysis, setSelectedAnalysis] = useState<OutfitAnalysis | null>(null);
|
||||
const { addNotification } = useNotifications();
|
||||
|
||||
// 加载分析结果
|
||||
const loadAnalyses = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const options = {
|
||||
project_id: projectId,
|
||||
status: null,
|
||||
limit: 50,
|
||||
offset: 0
|
||||
};
|
||||
|
||||
const result = await invoke('list_outfit_analyses', { options });
|
||||
setAnalyses(result as OutfitAnalysis[]);
|
||||
} catch (error) {
|
||||
console.error('加载分析结果失败:', error);
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: '加载失败',
|
||||
message: '无法加载分析结果'
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 创建服装单品
|
||||
const handleCreateItems = async (analysisId: string) => {
|
||||
try {
|
||||
await invoke('create_outfit_items_from_analysis', {
|
||||
projectId,
|
||||
analysisId
|
||||
});
|
||||
|
||||
addNotification({
|
||||
type: 'success',
|
||||
title: '创建成功',
|
||||
message: '成功从分析结果创建服装单品'
|
||||
});
|
||||
|
||||
if (onCreateItems) {
|
||||
onCreateItems(analysisId);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('创建服装单品失败:', error);
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: '创建失败',
|
||||
message: error instanceof Error ? error.message : '创建服装单品失败'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 获取状态图标
|
||||
const getStatusIcon = (status: string) => {
|
||||
switch (status) {
|
||||
case 'Pending':
|
||||
return <ClockIcon className="w-5 h-5 text-yellow-500" />;
|
||||
case 'Processing':
|
||||
return <ArrowPathIcon className="w-5 h-5 text-blue-500 animate-spin" />;
|
||||
case 'Completed':
|
||||
return <CheckCircleIcon className="w-5 h-5 text-green-500" />;
|
||||
case 'Failed':
|
||||
return <ExclamationTriangleIcon className="w-5 h-5 text-red-500" />;
|
||||
default:
|
||||
return <ClockIcon className="w-5 h-5 text-gray-500" />;
|
||||
}
|
||||
};
|
||||
|
||||
// 获取状态文本
|
||||
const getStatusText = (status: string) => {
|
||||
switch (status) {
|
||||
case 'Pending':
|
||||
return '等待分析';
|
||||
case 'Processing':
|
||||
return '分析中...';
|
||||
case 'Completed':
|
||||
return '分析完成';
|
||||
case 'Failed':
|
||||
return '分析失败';
|
||||
default:
|
||||
return '未知状态';
|
||||
}
|
||||
};
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (timeString: string) => {
|
||||
return new Date(timeString).toLocaleString('zh-CN');
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (projectId) {
|
||||
loadAnalyses();
|
||||
|
||||
// 设置定时刷新,检查分析状态
|
||||
const interval = setInterval(() => {
|
||||
loadAnalyses();
|
||||
}, 5000); // 每5秒刷新一次
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}
|
||||
}, [projectId]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600"></div>
|
||||
<span className="ml-2 text-gray-600">加载中...</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (analyses.length === 0) {
|
||||
return (
|
||||
<div className="text-center py-12">
|
||||
<SparklesIcon className="mx-auto h-12 w-12 text-gray-400 mb-4" />
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">暂无分析结果</h3>
|
||||
<p className="text-gray-500">请先上传图片进行分析</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{analyses.map((analysis) => (
|
||||
<div
|
||||
key={analysis.id}
|
||||
className="bg-white rounded-lg border border-gray-200 shadow-sm hover:shadow-md transition-shadow overflow-hidden"
|
||||
>
|
||||
{/* 图片预览 */}
|
||||
<div className="aspect-video bg-gray-100 relative">
|
||||
{analysis.image_path && (
|
||||
<img
|
||||
src={`file://${analysis.image_path}`}
|
||||
alt={analysis.image_name}
|
||||
className="w-full h-full object-cover"
|
||||
onError={(e) => {
|
||||
(e.target as HTMLImageElement).style.display = 'none';
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div className="absolute top-2 right-2">
|
||||
<button
|
||||
onClick={() => setSelectedAnalysis(analysis)}
|
||||
className="p-2 bg-white rounded-full shadow-md hover:shadow-lg transition-shadow"
|
||||
>
|
||||
<EyeIcon className="w-4 h-4 text-gray-600" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 分析信息 */}
|
||||
<div className="p-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h4 className="font-medium text-gray-900 truncate">
|
||||
{analysis.image_name}
|
||||
</h4>
|
||||
<div className="flex items-center space-x-1">
|
||||
{getStatusIcon(analysis.analysis_status)}
|
||||
<span className="text-sm text-gray-600">
|
||||
{getStatusText(analysis.analysis_status)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-gray-500 mb-3">
|
||||
创建时间: {formatTime(analysis.created_at)}
|
||||
</p>
|
||||
|
||||
{/* 分析结果 */}
|
||||
{analysis.analysis_status === 'Completed' && analysis.analysis_result && (
|
||||
<div className="space-y-2 mb-3">
|
||||
<div className="text-sm">
|
||||
<span className="font-medium text-gray-700">检测到:</span>
|
||||
<span className="ml-1 text-gray-600">
|
||||
{analysis.analysis_result.products?.length || 0} 个服装单品
|
||||
</span>
|
||||
</div>
|
||||
{analysis.analysis_result.style_description && (
|
||||
<div className="text-sm">
|
||||
<span className="font-medium text-gray-700">风格:</span>
|
||||
<span className="ml-1 text-gray-600">
|
||||
{analysis.analysis_result.style_description}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 错误信息 */}
|
||||
{analysis.analysis_status === 'Failed' && analysis.error_message && (
|
||||
<div className="mb-3">
|
||||
<p className="text-sm text-red-600">
|
||||
{analysis.error_message}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div className="flex space-x-2">
|
||||
{analysis.analysis_status === 'Completed' && (
|
||||
<button
|
||||
onClick={() => handleCreateItems(analysis.id)}
|
||||
className="flex-1 px-3 py-2 bg-primary-600 text-white text-sm font-medium rounded-md hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
|
||||
>
|
||||
创建单品
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setSelectedAnalysis(analysis)}
|
||||
className="px-3 py-2 border border-gray-300 text-gray-700 text-sm font-medium rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
|
||||
>
|
||||
查看详情
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 详情模态框 */}
|
||||
{selectedAnalysis && (
|
||||
<div className="fixed inset-0 z-50 overflow-y-auto">
|
||||
<div className="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||||
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"></div>
|
||||
|
||||
<div className="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
|
||||
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||
<div className="sm:flex sm:items-start">
|
||||
<div className="mt-3 text-center sm:mt-0 sm:text-left w-full">
|
||||
<h3 className="text-lg leading-6 font-medium text-gray-900 mb-4">
|
||||
分析详情
|
||||
</h3>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<span className="font-medium text-gray-700">文件名:</span>
|
||||
<span className="ml-2 text-gray-600">{selectedAnalysis.image_name}</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span className="font-medium text-gray-700">状态:</span>
|
||||
<span className="ml-2 text-gray-600">{getStatusText(selectedAnalysis.analysis_status)}</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span className="font-medium text-gray-700">创建时间:</span>
|
||||
<span className="ml-2 text-gray-600">{formatTime(selectedAnalysis.created_at)}</span>
|
||||
</div>
|
||||
|
||||
{selectedAnalysis.analyzed_at && (
|
||||
<div>
|
||||
<span className="font-medium text-gray-700">分析时间:</span>
|
||||
<span className="ml-2 text-gray-600">{formatTime(selectedAnalysis.analyzed_at)}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedAnalysis.analysis_result && (
|
||||
<div>
|
||||
<span className="font-medium text-gray-700">分析结果:</span>
|
||||
<pre className="mt-2 text-sm text-gray-600 bg-gray-50 p-3 rounded-md overflow-auto max-h-40">
|
||||
{JSON.stringify(selectedAnalysis.analysis_result, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedAnalysis.error_message && (
|
||||
<div>
|
||||
<span className="font-medium text-red-700">错误信息:</span>
|
||||
<p className="mt-1 text-sm text-red-600">{selectedAnalysis.error_message}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSelectedAnalysis(null)}
|
||||
className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-primary-600 text-base font-medium text-white hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 sm:ml-3 sm:w-auto sm:text-sm"
|
||||
>
|
||||
关闭
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OutfitAnalysisResult;
|
||||
277
apps/desktop/src/components/outfit/OutfitCard.tsx
Normal file
277
apps/desktop/src/components/outfit/OutfitCard.tsx
Normal file
@@ -0,0 +1,277 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
HeartIcon,
|
||||
EyeIcon,
|
||||
TrashIcon,
|
||||
StarIcon,
|
||||
ClockIcon,
|
||||
SparklesIcon
|
||||
} from '@heroicons/react/24/outline';
|
||||
import { HeartIcon as HeartSolidIcon } from '@heroicons/react/24/solid';
|
||||
import { format } from 'date-fns';
|
||||
import { zhCN } from 'date-fns/locale';
|
||||
|
||||
interface OutfitMatching {
|
||||
id: string;
|
||||
project_id: string;
|
||||
matching_name: string;
|
||||
matching_type: string;
|
||||
items: Array<{
|
||||
item_id: string;
|
||||
item_name: string;
|
||||
category: string;
|
||||
color_primary: any;
|
||||
styles: string[];
|
||||
role_in_outfit: string;
|
||||
image_url?: string;
|
||||
}>;
|
||||
score_details: {
|
||||
color_harmony_score: number;
|
||||
style_consistency_score: number;
|
||||
proportion_score: number;
|
||||
occasion_appropriateness: number;
|
||||
trend_factor: number;
|
||||
overall_score: number;
|
||||
};
|
||||
suggestions: Array<{
|
||||
suggestion_type: string;
|
||||
description: string;
|
||||
priority: number;
|
||||
}>;
|
||||
occasion_tags: string[];
|
||||
season_tags: string[];
|
||||
style_description: string;
|
||||
color_palette: any[];
|
||||
is_favorite: boolean;
|
||||
wear_count: number;
|
||||
last_worn_date?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
interface OutfitCardProps {
|
||||
matching: OutfitMatching;
|
||||
onToggleFavorite: () => void;
|
||||
onDelete: () => void;
|
||||
onView: () => void;
|
||||
}
|
||||
|
||||
const OutfitCard: React.FC<OutfitCardProps> = ({
|
||||
matching,
|
||||
onToggleFavorite,
|
||||
onDelete,
|
||||
onView
|
||||
}) => {
|
||||
const formatColorHSV = (color: any) => {
|
||||
if (!color) return '#000000';
|
||||
try {
|
||||
const h = color.hue * 360;
|
||||
const s = color.saturation * 100;
|
||||
const v = color.value * 100;
|
||||
return `hsl(${h}, ${s}%, ${v}%)`;
|
||||
} catch {
|
||||
return '#000000';
|
||||
}
|
||||
};
|
||||
|
||||
const getScoreColor = (score: number) => {
|
||||
if (score >= 0.8) return 'text-green-600 bg-green-100';
|
||||
if (score >= 0.6) return 'text-yellow-600 bg-yellow-100';
|
||||
return 'text-red-600 bg-red-100';
|
||||
};
|
||||
|
||||
const getMatchingTypeLabel = (type: string) => {
|
||||
const typeMap: { [key: string]: string } = {
|
||||
'ColorHarmony': '颜色和谐',
|
||||
'StyleConsistent': '风格一致',
|
||||
'Complementary': '互补搭配',
|
||||
'Seasonal': '季节搭配',
|
||||
'Occasion': '场合搭配',
|
||||
'Trendy': '时尚搭配'
|
||||
};
|
||||
return typeMap[type] || type;
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
try {
|
||||
return format(new Date(dateString), 'MM-dd', { locale: zhCN });
|
||||
} catch {
|
||||
return dateString;
|
||||
}
|
||||
};
|
||||
|
||||
const getCategoryIcon = (category: string) => {
|
||||
// 这里可以根据类别返回不同的图标
|
||||
return '👕'; // 简化处理,实际应用中可以使用更具体的图标
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white border border-gray-200 rounded-lg shadow-sm hover:shadow-md transition-all duration-200 overflow-hidden">
|
||||
{/* 头部 */}
|
||||
<div className="p-4 border-b border-gray-100">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="text-lg font-medium text-gray-900 truncate">
|
||||
{matching.matching_name}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
{matching.style_description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2 ml-3">
|
||||
<button
|
||||
onClick={onToggleFavorite}
|
||||
className={`p-1.5 rounded-full transition-colors ${
|
||||
matching.is_favorite
|
||||
? 'text-red-500 hover:text-red-600'
|
||||
: 'text-gray-400 hover:text-red-500'
|
||||
}`}
|
||||
>
|
||||
{matching.is_favorite ? (
|
||||
<HeartSolidIcon className="h-5 w-5" />
|
||||
) : (
|
||||
<HeartIcon className="h-5 w-5" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={onView}
|
||||
className="p-1.5 rounded-full text-gray-400 hover:text-indigo-500 transition-colors"
|
||||
>
|
||||
<EyeIcon className="h-5 w-5" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={onDelete}
|
||||
className="p-1.5 rounded-full text-gray-400 hover:text-red-500 transition-colors"
|
||||
>
|
||||
<TrashIcon className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 搭配类型和评分 */}
|
||||
<div className="flex items-center justify-between mt-3">
|
||||
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-indigo-100 text-indigo-800">
|
||||
{getMatchingTypeLabel(matching.matching_type)}
|
||||
</span>
|
||||
|
||||
<div className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getScoreColor(matching.score_details.overall_score)}`}>
|
||||
<StarIcon className="h-3 w-3 mr-1" />
|
||||
{(matching.score_details.overall_score * 100).toFixed(0)}分
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 服装单品预览 */}
|
||||
<div className="p-4">
|
||||
<h4 className="text-sm font-medium text-gray-900 mb-3">
|
||||
搭配单品 ({matching.items.length})
|
||||
</h4>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{matching.items.slice(0, 4).map((item, index) => (
|
||||
<div key={item.item_id} className="flex items-center space-x-2 p-2 bg-gray-50 rounded-md">
|
||||
<div className="flex-shrink-0">
|
||||
<div
|
||||
className="w-6 h-6 rounded-full border border-gray-300 flex items-center justify-center text-xs"
|
||||
style={{ backgroundColor: formatColorHSV(item.color_primary) }}
|
||||
>
|
||||
{getCategoryIcon(item.category)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-xs font-medium text-gray-900 truncate">
|
||||
{item.item_name}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
{item.role_in_outfit}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{matching.items.length > 4 && (
|
||||
<div className="flex items-center justify-center p-2 bg-gray-50 rounded-md border-2 border-dashed border-gray-300">
|
||||
<span className="text-xs text-gray-500">
|
||||
+{matching.items.length - 4} 更多
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 色彩搭配方案 */}
|
||||
{matching.color_palette.length > 0 && (
|
||||
<div className="px-4 pb-4">
|
||||
<h4 className="text-sm font-medium text-gray-900 mb-2">色彩方案</h4>
|
||||
<div className="flex space-x-1">
|
||||
{matching.color_palette.slice(0, 6).map((color, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="w-6 h-6 rounded-full border border-gray-300"
|
||||
style={{ backgroundColor: formatColorHSV(color) }}
|
||||
title={`颜色 ${index + 1}`}
|
||||
/>
|
||||
))}
|
||||
{matching.color_palette.length > 6 && (
|
||||
<div className="w-6 h-6 rounded-full border-2 border-dashed border-gray-300 flex items-center justify-center">
|
||||
<span className="text-xs text-gray-500">+</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 标签 */}
|
||||
<div className="px-4 pb-4">
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{matching.occasion_tags.slice(0, 2).map((tag, index) => (
|
||||
<span
|
||||
key={`occasion-${index}`}
|
||||
className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-800"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
{matching.season_tags.slice(0, 2).map((tag, index) => (
|
||||
<span
|
||||
key={`season-${index}`}
|
||||
className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 底部信息 */}
|
||||
<div className="px-4 py-3 bg-gray-50 border-t border-gray-100">
|
||||
<div className="flex items-center justify-between text-xs text-gray-500">
|
||||
<div className="flex items-center space-x-3">
|
||||
<span className="flex items-center">
|
||||
<ClockIcon className="h-3 w-3 mr-1" />
|
||||
{formatDate(matching.created_at)}
|
||||
</span>
|
||||
|
||||
{matching.wear_count > 0 && (
|
||||
<span className="flex items-center">
|
||||
<SparklesIcon className="h-3 w-3 mr-1" />
|
||||
穿过 {matching.wear_count} 次
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{matching.last_worn_date && (
|
||||
<span>
|
||||
最后穿着: {formatDate(matching.last_worn_date)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OutfitCard;
|
||||
236
apps/desktop/src/components/outfit/OutfitItemCard.tsx
Normal file
236
apps/desktop/src/components/outfit/OutfitItemCard.tsx
Normal file
@@ -0,0 +1,236 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
PhotoIcon,
|
||||
TagIcon,
|
||||
SparklesIcon
|
||||
} from '@heroicons/react/24/outline';
|
||||
import { CheckCircleIcon as CheckCircleSolidIcon } from '@heroicons/react/24/solid';
|
||||
|
||||
interface OutfitItem {
|
||||
id: string;
|
||||
project_id: string;
|
||||
analysis_id?: string;
|
||||
name: string;
|
||||
category: string;
|
||||
brand?: string;
|
||||
color_primary: any;
|
||||
styles: string[];
|
||||
image_urls: string[];
|
||||
tags: string[];
|
||||
notes?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
interface OutfitItemCardProps {
|
||||
item: OutfitItem;
|
||||
isSelected: boolean;
|
||||
onSelect: (selected: boolean) => void;
|
||||
}
|
||||
|
||||
const OutfitItemCard: React.FC<OutfitItemCardProps> = ({
|
||||
item,
|
||||
isSelected,
|
||||
onSelect
|
||||
}) => {
|
||||
const formatColorHSV = (color: any) => {
|
||||
if (!color) return '#000000';
|
||||
try {
|
||||
const h = color.hue * 360;
|
||||
const s = color.saturation * 100;
|
||||
const v = color.value * 100;
|
||||
return `hsl(${h}, ${s}%, ${v}%)`;
|
||||
} catch {
|
||||
return '#000000';
|
||||
}
|
||||
};
|
||||
|
||||
const getCategoryLabel = (category: string) => {
|
||||
const categoryMap: { [key: string]: string } = {
|
||||
'Top': '上装',
|
||||
'Bottom': '下装',
|
||||
'Dress': '连衣裙',
|
||||
'Outerwear': '外套',
|
||||
'Footwear': '鞋类',
|
||||
'Accessory': '配饰',
|
||||
'Other': '其他'
|
||||
};
|
||||
return categoryMap[category] || category;
|
||||
};
|
||||
|
||||
const getStyleLabel = (style: string) => {
|
||||
const styleMap: { [key: string]: string } = {
|
||||
'Casual': '休闲',
|
||||
'Formal': '正式',
|
||||
'Business': '商务',
|
||||
'Sporty': '运动',
|
||||
'Vintage': '复古',
|
||||
'Bohemian': '波西米亚',
|
||||
'Minimalist': '极简',
|
||||
'Streetwear': '街头',
|
||||
'Elegant': '优雅',
|
||||
'Trendy': '时尚'
|
||||
};
|
||||
return styleMap[style] || style;
|
||||
};
|
||||
|
||||
const getCategoryIcon = (category: string) => {
|
||||
const iconMap: { [key: string]: string } = {
|
||||
'Top': '👕',
|
||||
'Bottom': '👖',
|
||||
'Dress': '👗',
|
||||
'Outerwear': '🧥',
|
||||
'Footwear': '👟',
|
||||
'Accessory': '👜',
|
||||
'Other': '👔'
|
||||
};
|
||||
return iconMap[category] || '👔';
|
||||
};
|
||||
|
||||
const handleCardClick = () => {
|
||||
onSelect(!isSelected);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
relative bg-white border-2 rounded-lg shadow-sm hover:shadow-md transition-all duration-200 cursor-pointer
|
||||
${isSelected
|
||||
? 'border-indigo-500 bg-indigo-50'
|
||||
: 'border-gray-200 hover:border-gray-300'
|
||||
}
|
||||
`}
|
||||
onClick={handleCardClick}
|
||||
>
|
||||
{/* 选择状态指示器 */}
|
||||
<div className="absolute top-3 right-3 z-10">
|
||||
{isSelected ? (
|
||||
<CheckCircleSolidIcon className="h-6 w-6 text-indigo-600" />
|
||||
) : (
|
||||
<div className="h-6 w-6 border-2 border-gray-300 rounded-full bg-white"></div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 图片区域 */}
|
||||
<div className="aspect-square bg-gray-100 rounded-t-lg relative overflow-hidden">
|
||||
{item.image_urls.length > 0 ? (
|
||||
<img
|
||||
src={item.image_urls[0]}
|
||||
alt={item.name}
|
||||
className="w-full h-full object-cover"
|
||||
onError={(e) => {
|
||||
// 图片加载失败时显示默认图标
|
||||
e.currentTarget.style.display = 'none';
|
||||
e.currentTarget.nextElementSibling?.classList.remove('hidden');
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{/* 默认图标(当没有图片或图片加载失败时显示) */}
|
||||
<div className={`absolute inset-0 flex items-center justify-center ${item.image_urls.length > 0 ? 'hidden' : ''}`}>
|
||||
<div className="text-center">
|
||||
<div className="text-4xl mb-2">
|
||||
{getCategoryIcon(item.category)}
|
||||
</div>
|
||||
<PhotoIcon className="h-8 w-8 text-gray-400 mx-auto" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 颜色指示器 */}
|
||||
<div className="absolute bottom-2 left-2">
|
||||
<div
|
||||
className="w-6 h-6 rounded-full border-2 border-white shadow-sm"
|
||||
style={{ backgroundColor: formatColorHSV(item.color_primary) }}
|
||||
title="主色调"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 类别标签 */}
|
||||
<div className="absolute top-2 left-2">
|
||||
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-white bg-opacity-90 text-gray-800">
|
||||
{getCategoryLabel(item.category)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 内容区域 */}
|
||||
<div className="p-3 space-y-2">
|
||||
{/* 名称和品牌 */}
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-gray-900 truncate">
|
||||
{item.name}
|
||||
</h3>
|
||||
{item.brand && (
|
||||
<p className="text-xs text-gray-500 truncate">
|
||||
{item.brand}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 风格标签 */}
|
||||
{item.styles.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{item.styles.slice(0, 2).map((style, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-800"
|
||||
>
|
||||
{getStyleLabel(style)}
|
||||
</span>
|
||||
))}
|
||||
{item.styles.length > 2 && (
|
||||
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-600">
|
||||
+{item.styles.length - 2}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 自定义标签 */}
|
||||
{item.tags.length > 0 && (
|
||||
<div className="flex items-center space-x-1">
|
||||
<TagIcon className="h-3 w-3 text-gray-400" />
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{item.tags.slice(0, 2).map((tag, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="text-xs text-gray-500"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
{item.tags.length > 2 && (
|
||||
<span className="text-xs text-gray-400">
|
||||
+{item.tags.length - 2}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 备注 */}
|
||||
{item.notes && (
|
||||
<p className="text-xs text-gray-500 line-clamp-2">
|
||||
{item.notes}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* 来源标识 */}
|
||||
{item.analysis_id && (
|
||||
<div className="flex items-center space-x-1 pt-1 border-t border-gray-100">
|
||||
<SparklesIcon className="h-3 w-3 text-indigo-400" />
|
||||
<span className="text-xs text-indigo-600">AI分析生成</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 选中状态的覆盖层 */}
|
||||
{isSelected && (
|
||||
<div className="absolute inset-0 bg-indigo-500 bg-opacity-10 rounded-lg pointer-events-none"></div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OutfitItemCard;
|
||||
337
apps/desktop/src/components/outfit/OutfitSearchPanel.tsx
Normal file
337
apps/desktop/src/components/outfit/OutfitSearchPanel.tsx
Normal file
@@ -0,0 +1,337 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
MagnifyingGlassIcon,
|
||||
FunnelIcon,
|
||||
XMarkIcon,
|
||||
AdjustmentsHorizontalIcon
|
||||
} from '@heroicons/react/24/outline';
|
||||
import {CustomSelect} from '../CustomSelect';
|
||||
|
||||
interface SearchFilters {
|
||||
categories?: string[];
|
||||
styles?: string[];
|
||||
occasions?: string[];
|
||||
seasons?: string[];
|
||||
colors?: string[];
|
||||
minScore?: number;
|
||||
confidenceLevel?: string;
|
||||
matchingTypes?: string[];
|
||||
}
|
||||
|
||||
interface OutfitSearchPanelProps {
|
||||
filters: SearchFilters;
|
||||
onFiltersChange: (filters: SearchFilters) => void;
|
||||
onSearch: () => void;
|
||||
}
|
||||
|
||||
const OutfitSearchPanel: React.FC<OutfitSearchPanelProps> = ({
|
||||
filters,
|
||||
onFiltersChange,
|
||||
onSearch
|
||||
}) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const [searchText, setSearchText] = useState('');
|
||||
|
||||
// 预定义选项
|
||||
const categoryOptions = [
|
||||
{ value: 'Top', label: '上装' },
|
||||
{ value: 'Bottom', label: '下装' },
|
||||
{ value: 'Dress', label: '连衣裙' },
|
||||
{ value: 'Outerwear', label: '外套' },
|
||||
{ value: 'Footwear', label: '鞋类' },
|
||||
{ value: 'Accessory', label: '配饰' }
|
||||
];
|
||||
|
||||
const styleOptions = [
|
||||
{ value: 'Casual', label: '休闲' },
|
||||
{ value: 'Formal', label: '正式' },
|
||||
{ value: 'Business', label: '商务' },
|
||||
{ value: 'Sporty', label: '运动' },
|
||||
{ value: 'Vintage', label: '复古' },
|
||||
{ value: 'Bohemian', label: '波西米亚' },
|
||||
{ value: 'Minimalist', label: '极简' },
|
||||
{ value: 'Streetwear', label: '街头' },
|
||||
{ value: 'Elegant', label: '优雅' },
|
||||
{ value: 'Trendy', label: '时尚' }
|
||||
];
|
||||
|
||||
const occasionOptions = [
|
||||
{ value: 'work', label: '工作' },
|
||||
{ value: 'casual', label: '日常' },
|
||||
{ value: 'party', label: '聚会' },
|
||||
{ value: 'date', label: '约会' },
|
||||
{ value: 'travel', label: '旅行' },
|
||||
{ value: 'sport', label: '运动' },
|
||||
{ value: 'formal', label: '正式场合' }
|
||||
];
|
||||
|
||||
const seasonOptions = [
|
||||
{ value: 'spring', label: '春季' },
|
||||
{ value: 'summer', label: '夏季' },
|
||||
{ value: 'autumn', label: '秋季' },
|
||||
{ value: 'winter', label: '冬季' }
|
||||
];
|
||||
|
||||
const colorOptions = [
|
||||
{ value: 'red', label: '红色' },
|
||||
{ value: 'blue', label: '蓝色' },
|
||||
{ value: 'green', label: '绿色' },
|
||||
{ value: 'yellow', label: '黄色' },
|
||||
{ value: 'purple', label: '紫色' },
|
||||
{ value: 'orange', label: '橙色' },
|
||||
{ value: 'pink', label: '粉色' },
|
||||
{ value: 'brown', label: '棕色' },
|
||||
{ value: 'black', label: '黑色' },
|
||||
{ value: 'white', label: '白色' },
|
||||
{ value: 'gray', label: '灰色' }
|
||||
];
|
||||
|
||||
const confidenceLevelOptions = [
|
||||
{ value: 'Low', label: '低' },
|
||||
{ value: 'Medium', label: '中' },
|
||||
{ value: 'High', label: '高' },
|
||||
{ value: 'Excellent', label: '极佳' }
|
||||
];
|
||||
|
||||
const matchingTypeOptions = [
|
||||
{ value: 'ColorHarmony', label: '颜色和谐' },
|
||||
{ value: 'StyleConsistent', label: '风格一致' },
|
||||
{ value: 'Complementary', label: '互补搭配' },
|
||||
{ value: 'Seasonal', label: '季节搭配' },
|
||||
{ value: 'Occasion', label: '场合搭配' },
|
||||
{ value: 'Trendy', label: '时尚搭配' }
|
||||
];
|
||||
|
||||
const handleFilterChange = (key: keyof SearchFilters, value: any) => {
|
||||
onFiltersChange({
|
||||
...filters,
|
||||
[key]: value
|
||||
});
|
||||
};
|
||||
|
||||
const clearFilters = () => {
|
||||
onFiltersChange({});
|
||||
setSearchText('');
|
||||
};
|
||||
|
||||
const hasActiveFilters = () => {
|
||||
return Object.keys(filters).some(key => {
|
||||
const value = filters[key as keyof SearchFilters];
|
||||
return Array.isArray(value) ? value.length > 0 : value !== undefined && value !== null;
|
||||
});
|
||||
};
|
||||
|
||||
const getActiveFilterCount = () => {
|
||||
let count = 0;
|
||||
Object.keys(filters).forEach(key => {
|
||||
const value = filters[key as keyof SearchFilters];
|
||||
if (Array.isArray(value) && value.length > 0) {
|
||||
count += value.length;
|
||||
} else if (value !== undefined && value !== null) {
|
||||
count += 1;
|
||||
}
|
||||
});
|
||||
return count;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white border border-gray-200 rounded-lg p-4 space-y-4">
|
||||
{/* 搜索栏和展开按钮 */}
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="flex-1 relative">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<MagnifyingGlassIcon className="h-5 w-5 text-gray-400" />
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={searchText}
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
placeholder="搜索搭配..."
|
||||
className="block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-md leading-5 bg-white placeholder-gray-500 focus:outline-none focus:placeholder-gray-400 focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
className={`inline-flex items-center px-3 py-2 border border-gray-300 rounded-md text-sm font-medium ${
|
||||
hasActiveFilters()
|
||||
? 'text-indigo-700 bg-indigo-50 border-indigo-300'
|
||||
: 'text-gray-700 bg-white hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
<FunnelIcon className="h-4 w-4 mr-1" />
|
||||
筛选
|
||||
{hasActiveFilters() && (
|
||||
<span className="ml-1 inline-flex items-center justify-center px-2 py-0.5 rounded-full text-xs font-medium bg-indigo-100 text-indigo-800">
|
||||
{getActiveFilterCount()}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={onSearch}
|
||||
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700"
|
||||
>
|
||||
搜索
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 展开的筛选面板 */}
|
||||
{isExpanded && (
|
||||
<div className="border-t border-gray-200 pt-4 space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{/* 服装类别 */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
服装类别
|
||||
</label>
|
||||
<CustomSelect
|
||||
options={categoryOptions}
|
||||
value={filters.categories || []}
|
||||
onChange={(value) => handleFilterChange('categories', value)}
|
||||
placeholder="选择类别"
|
||||
multiple
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 风格 */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
风格
|
||||
</label>
|
||||
<CustomSelect
|
||||
options={styleOptions}
|
||||
value={filters.styles || []}
|
||||
onChange={(value) => handleFilterChange('styles', value)}
|
||||
placeholder="选择风格"
|
||||
multiple
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 场合 */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
适用场合
|
||||
</label>
|
||||
<CustomSelect
|
||||
options={occasionOptions}
|
||||
value={filters.occasions || []}
|
||||
onChange={(value) => handleFilterChange('occasions', value)}
|
||||
placeholder="选择场合"
|
||||
multiple
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 季节 */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
适用季节
|
||||
</label>
|
||||
<CustomSelect
|
||||
options={seasonOptions}
|
||||
value={filters.seasons || []}
|
||||
onChange={(value) => handleFilterChange('seasons', value)}
|
||||
placeholder="选择季节"
|
||||
multiple
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 颜色 */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
主要颜色
|
||||
</label>
|
||||
<CustomSelect
|
||||
options={colorOptions}
|
||||
value={filters.colors || []}
|
||||
onChange={(value) => handleFilterChange('colors', value)}
|
||||
placeholder="选择颜色"
|
||||
multiple
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 搭配类型 */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
搭配类型
|
||||
</label>
|
||||
<CustomSelect
|
||||
options={matchingTypeOptions}
|
||||
value={filters.matchingTypes || []}
|
||||
onChange={(value) => handleFilterChange('matchingTypes', value)}
|
||||
placeholder="选择搭配类型"
|
||||
multiple
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 评分和置信度 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{/* 最低评分 */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
最低评分
|
||||
</label>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.1"
|
||||
value={filters.minScore || 0}
|
||||
onChange={(e) => handleFilterChange('minScore', parseFloat(e.target.value))}
|
||||
className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer"
|
||||
/>
|
||||
<div className="flex justify-between text-xs text-gray-500 mt-1">
|
||||
<span>0.0</span>
|
||||
<span className="font-medium">{(filters.minScore || 0).toFixed(1)}</span>
|
||||
<span>1.0</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 置信度等级 */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
置信度等级
|
||||
</label>
|
||||
<CustomSelect
|
||||
options={confidenceLevelOptions}
|
||||
value={filters.confidenceLevel || ''}
|
||||
onChange={(value) => handleFilterChange('confidenceLevel', value)}
|
||||
placeholder="选择置信度等级"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div className="flex justify-between items-center pt-4 border-t border-gray-200">
|
||||
<button
|
||||
onClick={clearFilters}
|
||||
className="inline-flex items-center px-3 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"
|
||||
disabled={!hasActiveFilters()}
|
||||
>
|
||||
<XMarkIcon className="h-4 w-4 mr-1" />
|
||||
清空筛选
|
||||
</button>
|
||||
|
||||
<div className="flex space-x-2">
|
||||
<button
|
||||
onClick={() => setIsExpanded(false)}
|
||||
className="inline-flex items-center px-3 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"
|
||||
>
|
||||
收起
|
||||
</button>
|
||||
<button
|
||||
onClick={onSearch}
|
||||
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700"
|
||||
>
|
||||
应用筛选
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OutfitSearchPanel;
|
||||
338
apps/desktop/src/pages/OutfitMatch.tsx
Normal file
338
apps/desktop/src/pages/OutfitMatch.tsx
Normal file
@@ -0,0 +1,338 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
PhotoIcon,
|
||||
SparklesIcon,
|
||||
PlusIcon
|
||||
} from '@heroicons/react/24/outline';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
|
||||
// 导入设计系统样式
|
||||
import '../styles/design-system.css';
|
||||
import '../styles/animations.css';
|
||||
|
||||
// 导入组件
|
||||
import ImageUploader from '../components/outfit/ImageUploader';
|
||||
import OutfitAnalysisResult from '../components/outfit/OutfitAnalysisResult';
|
||||
import { useNotifications } from '../components/NotificationSystem';
|
||||
|
||||
interface OutfitMatchProps {
|
||||
projectId?: string;
|
||||
}
|
||||
|
||||
const OutfitMatch: React.FC<OutfitMatchProps> = ({ projectId }) => {
|
||||
const [activeTab, setActiveTab] = useState<'upload' | 'analysis' | 'items' | 'matching'>('upload');
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const { addNotification } = useNotifications();
|
||||
|
||||
// 处理图像上传
|
||||
const handleImageUpload = async (files: File[]) => {
|
||||
if (!projectId) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: '错误',
|
||||
message: '项目ID不存在,无法上传图片'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setIsUploading(true);
|
||||
|
||||
try {
|
||||
// 为每个文件创建分析记录
|
||||
for (const file of files) {
|
||||
// 读取文件数据
|
||||
const fileData = await file.arrayBuffer();
|
||||
const uint8Array = new Uint8Array(fileData);
|
||||
|
||||
// 保存文件到服务器
|
||||
const savedPath = await invoke('save_outfit_image', {
|
||||
projectId: projectId,
|
||||
fileName: file.name,
|
||||
fileData: Array.from(uint8Array)
|
||||
});
|
||||
|
||||
console.log('文件保存成功:', savedPath);
|
||||
|
||||
// 创建分析记录
|
||||
const analysisRequest = {
|
||||
project_id: projectId,
|
||||
image_path: savedPath,
|
||||
image_name: file.name
|
||||
};
|
||||
|
||||
const analysis = await invoke('create_outfit_analysis', {
|
||||
request: analysisRequest
|
||||
});
|
||||
|
||||
console.log('创建分析记录成功:', analysis);
|
||||
|
||||
// 开始分析
|
||||
await invoke('start_outfit_analysis', {
|
||||
analysisId: (analysis as any).id
|
||||
});
|
||||
|
||||
console.log('开始分析:', (analysis as any).id);
|
||||
}
|
||||
|
||||
addNotification({
|
||||
type: 'success',
|
||||
title: '上传成功',
|
||||
message: `成功上传 ${files.length} 个文件,正在进行AI分析...`
|
||||
});
|
||||
|
||||
// 切换到分析标签页
|
||||
setActiveTab('analysis');
|
||||
|
||||
} catch (error) {
|
||||
console.error('上传失败:', error);
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: '上传失败',
|
||||
message: error instanceof Error ? error.message : '上传过程中发生错误'
|
||||
});
|
||||
} finally {
|
||||
setIsUploading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
{/* 页面头部 - 优雅的渐变背景和动画 */}
|
||||
<div className="mb-8 relative overflow-hidden rounded-2xl bg-gradient-to-r from-primary-600 to-primary-700 p-8 text-white shadow-xl">
|
||||
<div className="absolute inset-0 bg-black opacity-10"></div>
|
||||
<div className="relative z-10">
|
||||
<div className="flex items-center space-x-3 mb-4">
|
||||
<div className="p-3 bg-white bg-opacity-20 rounded-xl backdrop-blur-sm">
|
||||
<SparklesIcon className="h-8 w-8 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">服装搭配</h1>
|
||||
<p className="text-primary-100 mt-1">AI智能分析服装搭配,发现完美组合</p>
|
||||
</div>
|
||||
</div>
|
||||
{projectId && (
|
||||
<div className="inline-flex items-center px-3 py-1 rounded-full bg-white bg-opacity-20 backdrop-blur-sm">
|
||||
<span className="text-sm font-medium">项目: {projectId}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 装饰性背景元素 */}
|
||||
<div className="absolute top-0 right-0 -mt-4 -mr-4 w-24 h-24 bg-white opacity-10 rounded-full"></div>
|
||||
<div className="absolute bottom-0 left-0 -mb-8 -ml-8 w-32 h-32 bg-white opacity-5 rounded-full"></div>
|
||||
</div>
|
||||
|
||||
{/* 标签页导航 */}
|
||||
<div className="bg-white rounded-2xl shadow-lg border border-gray-100 mb-8">
|
||||
<div className="border-b border-gray-200">
|
||||
<nav className="flex space-x-8 px-6">
|
||||
{[
|
||||
{ key: 'upload', label: '图像上传', icon: PhotoIcon },
|
||||
{ key: 'analysis', label: '分析结果', icon: SparklesIcon },
|
||||
{ key: 'items', label: '服装单品', icon: PlusIcon },
|
||||
{ key: 'matching', label: '智能搭配', icon: SparklesIcon }
|
||||
].map(({ key, label, icon: Icon }) => (
|
||||
<button
|
||||
key={key}
|
||||
onClick={() => setActiveTab(key as any)}
|
||||
className={`group inline-flex items-center py-4 px-1 border-b-2 font-medium text-sm ${activeTab === key
|
||||
? 'border-primary-500 text-primary-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
<Icon className="h-5 w-5 mr-2" />
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* 标签页内容 */}
|
||||
<div className="p-6">
|
||||
{/* 图像上传标签页 */}
|
||||
{activeTab === 'upload' && (
|
||||
<div className="space-y-6">
|
||||
<div className="text-center mb-6">
|
||||
<h3 className="text-xl font-bold text-gray-900 mb-2">
|
||||
上传服装图片
|
||||
</h3>
|
||||
<p className="text-gray-600">
|
||||
上传您的服装图片,AI将自动分析服装类别、颜色和风格
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ImageUploader
|
||||
onUpload={handleImageUpload}
|
||||
isUploading={isUploading}
|
||||
maxFiles={10}
|
||||
maxFileSize={20}
|
||||
acceptedFormats={['image/jpeg', 'image/png', 'image/webp']}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 分析结果标签页 */}
|
||||
{activeTab === 'analysis' && (
|
||||
<div className="space-y-6">
|
||||
<div className="text-center mb-6">
|
||||
<h3 className="text-xl font-bold text-gray-900 mb-2">
|
||||
AI分析结果
|
||||
</h3>
|
||||
<p className="text-gray-600">
|
||||
查看AI对您上传图片的分析结果
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{projectId && (
|
||||
<OutfitAnalysisResult
|
||||
projectId={projectId}
|
||||
onCreateItems={(analysisId) => {
|
||||
console.log('创建服装单品:', analysisId);
|
||||
// 切换到服装单品标签页
|
||||
setActiveTab('items');
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 服装单品标签页 */}
|
||||
{activeTab === 'items' && (
|
||||
<div className="space-y-6">
|
||||
<div className="text-center mb-6">
|
||||
<h3 className="text-xl font-bold text-gray-900 mb-2">
|
||||
服装单品管理
|
||||
</h3>
|
||||
<p className="text-gray-600">
|
||||
管理您的服装单品信息和属性
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-50 rounded-lg p-8 text-center">
|
||||
<PlusIcon className="mx-auto h-12 w-12 text-gray-400 mb-4" />
|
||||
<p className="text-gray-500">暂无服装单品</p>
|
||||
<p className="text-sm text-gray-400 mt-2">请先上传图片并完成分析</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 智能搭配标签页 */}
|
||||
{activeTab === 'matching' && (
|
||||
<div className="space-y-6">
|
||||
<div className="text-center mb-6">
|
||||
<h3 className="text-xl font-bold text-gray-900 mb-2">
|
||||
智能搭配推荐
|
||||
</h3>
|
||||
<p className="text-gray-600">
|
||||
AI为您推荐最佳搭配组合
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-50 rounded-lg p-8 text-center">
|
||||
<SparklesIcon className="mx-auto h-12 w-12 text-gray-400 mb-4" />
|
||||
<p className="text-gray-500">暂无搭配推荐</p>
|
||||
<p className="text-sm text-gray-400 mt-2">请先创建服装单品</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 功能特性展示 */}
|
||||
<div className="bg-white rounded-2xl shadow-lg border border-gray-100 p-8">
|
||||
<div className="text-center mb-8">
|
||||
<h3 className="text-xl font-bold text-gray-900 mb-2">
|
||||
AI智能服装搭配
|
||||
</h3>
|
||||
<p className="text-gray-600">
|
||||
专业的时尚搭配建议,让您的穿搭更加出色
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 功能特性卡片 - 现代化设计 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 mt-12">
|
||||
<div className="group relative">
|
||||
<div className="text-center p-6 bg-gradient-to-br from-blue-50 to-blue-100 rounded-xl border border-blue-200 hover:shadow-lg transition-all duration-300 transform hover:-translate-y-1">
|
||||
<div className="mx-auto flex items-center justify-center h-12 w-12 rounded-xl bg-blue-500 mb-4 group-hover:scale-110 transition-transform duration-300">
|
||||
<PhotoIcon className="h-6 w-6 text-white" />
|
||||
</div>
|
||||
<h4 className="font-semibold text-gray-900 mb-2">图像分析</h4>
|
||||
<p className="text-sm text-gray-600 leading-relaxed">
|
||||
AI自动识别服装类别、颜色和风格,提供精准的视觉分析
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="group relative">
|
||||
<div className="text-center p-6 bg-gradient-to-br from-green-50 to-green-100 rounded-xl border border-green-200 hover:shadow-lg transition-all duration-300 transform hover:-translate-y-1">
|
||||
<div className="mx-auto flex items-center justify-center h-12 w-12 rounded-xl bg-green-500 mb-4 group-hover:scale-110 transition-transform duration-300">
|
||||
<PlusIcon className="h-6 w-6 text-white" />
|
||||
</div>
|
||||
<h4 className="font-semibold text-gray-900 mb-2">单品管理</h4>
|
||||
<p className="text-sm text-gray-600 leading-relaxed">
|
||||
智能管理服装单品信息和属性,建立个人衣橱档案
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="group relative">
|
||||
<div className="text-center p-6 bg-gradient-to-br from-purple-50 to-purple-100 rounded-xl border border-purple-200 hover:shadow-lg transition-all duration-300 transform hover:-translate-y-1">
|
||||
<div className="mx-auto flex items-center justify-center h-12 w-12 rounded-xl bg-purple-500 mb-4 group-hover:scale-110 transition-transform duration-300">
|
||||
<SparklesIcon className="h-6 w-6 text-white" />
|
||||
</div>
|
||||
<h4 className="font-semibold text-gray-900 mb-2">智能搭配</h4>
|
||||
<p className="text-sm text-gray-600 leading-relaxed">
|
||||
AI推荐最佳搭配组合,提供专业的时尚建议
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 即将推出的功能 - 优雅的信息展示 */}
|
||||
<div className="bg-white rounded-2xl shadow-lg border border-gray-100 p-8">
|
||||
<div className="text-center mb-8">
|
||||
<h3 className="text-xl font-bold text-gray-900 mb-2">
|
||||
即将推出的功能
|
||||
</h3>
|
||||
<p className="text-gray-600">
|
||||
为您带来更智能的搭配体验
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-12 p-8 bg-gradient-to-r from-indigo-50 to-purple-50 rounded-2xl border border-indigo-200">
|
||||
<div className="text-center mb-6">
|
||||
<h4 className="text-xl font-bold text-indigo-900 mb-2">即将推出的功能</h4>
|
||||
<p className="text-indigo-700">为您带来更智能的搭配体验</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 max-w-4xl mx-auto">
|
||||
<div className="flex items-center space-x-3 p-3 bg-white bg-opacity-60 rounded-lg">
|
||||
<div className="flex-shrink-0 w-2 h-2 bg-indigo-500 rounded-full"></div>
|
||||
<span className="text-sm font-medium text-indigo-800">上传图像进行AI分析</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3 p-3 bg-white bg-opacity-60 rounded-lg">
|
||||
<div className="flex-shrink-0 w-2 h-2 bg-indigo-500 rounded-full"></div>
|
||||
<span className="text-sm font-medium text-indigo-800">自动创建服装单品档案</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3 p-3 bg-white bg-opacity-60 rounded-lg">
|
||||
<div className="flex-shrink-0 w-2 h-2 bg-indigo-500 rounded-full"></div>
|
||||
<span className="text-sm font-medium text-indigo-800">智能搭配推荐算法</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3 p-3 bg-white bg-opacity-60 rounded-lg">
|
||||
<div className="flex-shrink-0 w-2 h-2 bg-indigo-500 rounded-full"></div>
|
||||
<span className="text-sm font-medium text-indigo-800">颜色和风格匹配</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3 p-3 bg-white bg-opacity-60 rounded-lg md:col-span-2">
|
||||
<div className="flex-shrink-0 w-2 h-2 bg-indigo-500 rounded-full"></div>
|
||||
<span className="text-sm font-medium text-indigo-800">场合和季节搭配建议</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OutfitMatch;
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useState, useCallback, useMemo, useRef } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { ArrowLeft, FolderOpen, Upload, FileVideo, FileAudio, FileImage, HardDrive, Brain, Loader2, Link, Layers, Calendar, MapPin, Users, CheckCircle, Filter, Shuffle, Download } from 'lucide-react';
|
||||
import { ArrowLeft, FolderOpen, Upload, FileVideo, FileAudio, FileImage, HardDrive, Brain, Loader2, Link, Layers, Calendar, MapPin, Users, CheckCircle, Filter, Shuffle, Download, Shirt } from 'lucide-react';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { useProjectStore } from '../store/projectStore';
|
||||
import { useMaterialStore } from '../store/materialStore';
|
||||
@@ -42,6 +42,7 @@ import { TemplateMatchingResultManager } from '../components/TemplateMatchingRes
|
||||
import { useNotifications } from '../components/NotificationSystem';
|
||||
import { ProjectMaterialUsageOverviewComponent } from '../components/ProjectMaterialUsageOverview';
|
||||
import { useMaterialUsage } from '../hooks/useMaterialUsage';
|
||||
import OutfitMatch from './OutfitMatch';
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (dateString: string) => {
|
||||
@@ -124,7 +125,7 @@ export const ProjectDetails: React.FC = () => {
|
||||
const [editingBinding, setEditingBinding] = useState<ProjectTemplateBindingDetail | null>(null);
|
||||
const [showMaterialEditDialog, setShowMaterialEditDialog] = useState(false);
|
||||
const [editingMaterial, setEditingMaterial] = useState<Material | null>(null);
|
||||
const [activeTab, setActiveTab] = useState<'overview' | 'materials' | 'segments' | 'templates' | 'matching-results' | 'export-records' | 'usage-stats' | 'ai-logs'>('overview');
|
||||
const [activeTab, setActiveTab] = useState<'overview' | 'materials' | 'segments' | 'templates' | 'matching-results' | 'export-records' | 'usage-stats' | 'ai-logs' | 'outfit-match'>('overview');
|
||||
const [_batchClassificationResult, setBatchClassificationResult] = useState<ProjectBatchClassificationResponse | null>(null);
|
||||
|
||||
// 素材匹配状态
|
||||
@@ -1029,6 +1030,20 @@ export const ProjectDetails: React.FC = () => {
|
||||
<span className="sm:hidden">AI日志</span>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('outfit-match')}
|
||||
className={`py-3 px-4 font-medium text-sm transition-all duration-200 whitespace-nowrap rounded-t-lg relative ${
|
||||
activeTab === 'outfit-match'
|
||||
? 'text-primary-600 border-b-2 border-primary-500'
|
||||
: 'text-gray-500 hover:text-gray-700 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Shirt className="w-4 h-4" />
|
||||
<span className="hidden sm:inline">服装搭配</span>
|
||||
<span className="sm:hidden">搭配</span>
|
||||
</div>
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
@@ -1511,6 +1526,13 @@ export const ProjectDetails: React.FC = () => {
|
||||
<AiAnalysisLogViewer projectId={project.id} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 服装搭配选项卡 */}
|
||||
{activeTab === 'outfit-match' && project && (
|
||||
<div className="h-full">
|
||||
<OutfitMatch projectId={project.id} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
471
apps/desktop/src/stores/outfitStore.ts
Normal file
471
apps/desktop/src/stores/outfitStore.ts
Normal file
@@ -0,0 +1,471 @@
|
||||
import { create } from 'zustand';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
|
||||
// 基础数据类型
|
||||
interface ColorHSV {
|
||||
hue: number;
|
||||
saturation: number;
|
||||
value: number;
|
||||
}
|
||||
|
||||
interface OutfitAnalysis {
|
||||
id: string;
|
||||
project_id: string;
|
||||
image_path: string;
|
||||
image_name: string;
|
||||
analysis_status: 'Pending' | 'Processing' | 'Completed' | 'Failed';
|
||||
analysis_result?: {
|
||||
environment_tags: string[];
|
||||
environment_color_pattern: ColorHSV;
|
||||
dress_color_pattern: ColorHSV;
|
||||
style_description: string;
|
||||
products: Array<{
|
||||
category: string;
|
||||
description: string;
|
||||
color_pattern: ColorHSV;
|
||||
design_styles: string[];
|
||||
color_pattern_match_dress: number;
|
||||
color_pattern_match_environment: number;
|
||||
}>;
|
||||
};
|
||||
error_message?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
analyzed_at?: string;
|
||||
}
|
||||
|
||||
interface OutfitItem {
|
||||
id: string;
|
||||
project_id: string;
|
||||
analysis_id?: string;
|
||||
name: string;
|
||||
category: string;
|
||||
brand?: string;
|
||||
color_primary: ColorHSV;
|
||||
styles: string[];
|
||||
image_urls: string[];
|
||||
tags: string[];
|
||||
notes?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
interface OutfitMatching {
|
||||
id: string;
|
||||
project_id: string;
|
||||
matching_name: string;
|
||||
matching_type: string;
|
||||
items: Array<{
|
||||
item_id: string;
|
||||
item_name: string;
|
||||
category: string;
|
||||
color_primary: ColorHSV;
|
||||
styles: string[];
|
||||
role_in_outfit: string;
|
||||
image_url?: string;
|
||||
}>;
|
||||
score_details: {
|
||||
color_harmony_score: number;
|
||||
style_consistency_score: number;
|
||||
proportion_score: number;
|
||||
occasion_appropriateness: number;
|
||||
trend_factor: number;
|
||||
overall_score: number;
|
||||
};
|
||||
suggestions: Array<{
|
||||
suggestion_type: string;
|
||||
description: string;
|
||||
priority: number;
|
||||
}>;
|
||||
occasion_tags: string[];
|
||||
season_tags: string[];
|
||||
style_description: string;
|
||||
color_palette: ColorHSV[];
|
||||
is_favorite: boolean;
|
||||
wear_count: number;
|
||||
last_worn_date?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
// 查询参数接口
|
||||
interface OutfitAnalysisQueryOptions {
|
||||
project_id?: string;
|
||||
status?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
interface OutfitItemQueryOptions {
|
||||
project_id?: string;
|
||||
category?: string;
|
||||
styles?: string[];
|
||||
brand?: string;
|
||||
tags?: string[];
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
interface OutfitMatchingQueryOptions {
|
||||
project_id?: string;
|
||||
filters?: any;
|
||||
sort_by?: string;
|
||||
sort_order?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
// 创建请求接口
|
||||
interface CreateOutfitAnalysisRequest {
|
||||
project_id: string;
|
||||
image_path: string;
|
||||
image_name: string;
|
||||
}
|
||||
|
||||
interface CreateOutfitItemRequest {
|
||||
project_id: string;
|
||||
analysis_id?: string;
|
||||
name: string;
|
||||
category: string;
|
||||
brand?: string;
|
||||
color_primary: ColorHSV;
|
||||
styles: string[];
|
||||
image_urls: string[];
|
||||
tags: string[];
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
interface CreateOutfitMatchingRequest {
|
||||
project_id: string;
|
||||
matching_name: string;
|
||||
matching_type: string;
|
||||
item_ids: string[];
|
||||
occasion_tags: string[];
|
||||
season_tags: string[];
|
||||
style_description?: string;
|
||||
}
|
||||
|
||||
// Store 接口
|
||||
interface OutfitStore {
|
||||
// 状态
|
||||
analyses: OutfitAnalysis[];
|
||||
items: OutfitItem[];
|
||||
matchings: OutfitMatching[];
|
||||
currentAnalysis: OutfitAnalysis | null;
|
||||
currentItem: OutfitItem | null;
|
||||
currentMatching: OutfitMatching | null;
|
||||
selectedItems: string[];
|
||||
searchFilters: any;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
|
||||
// 分析相关操作
|
||||
fetchAnalyses: (options?: OutfitAnalysisQueryOptions) => Promise<void>;
|
||||
createAnalysis: (request: CreateOutfitAnalysisRequest) => Promise<OutfitAnalysis>;
|
||||
startAnalysis: (analysisId: string) => Promise<void>;
|
||||
deleteAnalysis: (id: string) => Promise<void>;
|
||||
setCurrentAnalysis: (analysis: OutfitAnalysis | null) => void;
|
||||
|
||||
// 单品相关操作
|
||||
fetchItems: (options?: OutfitItemQueryOptions) => Promise<void>;
|
||||
createItem: (request: CreateOutfitItemRequest) => Promise<OutfitItem>;
|
||||
createItemsFromAnalysis: (projectId: string, analysisId: string) => Promise<OutfitItem[]>;
|
||||
updateItem: (id: string, request: any) => Promise<void>;
|
||||
deleteItem: (id: string) => Promise<void>;
|
||||
setCurrentItem: (item: OutfitItem | null) => void;
|
||||
setSelectedItems: (itemIds: string[]) => void;
|
||||
toggleItemSelection: (itemId: string) => void;
|
||||
|
||||
// 搭配相关操作
|
||||
fetchMatchings: (options?: OutfitMatchingQueryOptions) => Promise<void>;
|
||||
createMatching: (request: CreateOutfitMatchingRequest) => Promise<OutfitMatching>;
|
||||
updateMatching: (id: string, request: any) => Promise<void>;
|
||||
deleteMatching: (id: string) => Promise<void>;
|
||||
toggleFavorite: (id: string) => Promise<void>;
|
||||
incrementWearCount: (id: string) => Promise<void>;
|
||||
smartMatching: (request: any) => Promise<any>;
|
||||
setCurrentMatching: (matching: OutfitMatching | null) => void;
|
||||
|
||||
// 搜索和过滤
|
||||
setSearchFilters: (filters: any) => void;
|
||||
clearFilters: () => void;
|
||||
|
||||
// 通用操作
|
||||
clearError: () => void;
|
||||
setLoading: (loading: boolean) => void;
|
||||
}
|
||||
|
||||
export const useOutfitStore = create<OutfitStore>((set, get) => ({
|
||||
// 初始状态
|
||||
analyses: [],
|
||||
items: [],
|
||||
matchings: [],
|
||||
currentAnalysis: null,
|
||||
currentItem: null,
|
||||
currentMatching: null,
|
||||
selectedItems: [],
|
||||
searchFilters: {},
|
||||
isLoading: false,
|
||||
error: null,
|
||||
|
||||
// 分析相关操作
|
||||
fetchAnalyses: async (options = {}) => {
|
||||
set({ isLoading: true, error: null });
|
||||
try {
|
||||
const analyses = await invoke<OutfitAnalysis[]>('list_outfit_analyses', { options });
|
||||
set({ analyses, isLoading: false });
|
||||
} catch (error) {
|
||||
set({ error: String(error), isLoading: false });
|
||||
}
|
||||
},
|
||||
|
||||
createAnalysis: async (request) => {
|
||||
set({ isLoading: true, error: null });
|
||||
try {
|
||||
const analysis = await invoke<OutfitAnalysis>('create_outfit_analysis', { request });
|
||||
set(state => ({
|
||||
analyses: [analysis, ...state.analyses],
|
||||
isLoading: false
|
||||
}));
|
||||
return analysis;
|
||||
} catch (error) {
|
||||
set({ error: String(error), isLoading: false });
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
startAnalysis: async (analysisId) => {
|
||||
try {
|
||||
await invoke('start_outfit_analysis', { analysisId });
|
||||
// 更新分析状态为处理中
|
||||
set(state => ({
|
||||
analyses: state.analyses.map(a =>
|
||||
a.id === analysisId
|
||||
? { ...a, analysis_status: 'Processing' as const }
|
||||
: a
|
||||
)
|
||||
}));
|
||||
} catch (error) {
|
||||
set({ error: String(error) });
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
deleteAnalysis: async (id) => {
|
||||
try {
|
||||
await invoke('delete_outfit_analysis', { id });
|
||||
set(state => ({
|
||||
analyses: state.analyses.filter(a => a.id !== id)
|
||||
}));
|
||||
} catch (error) {
|
||||
set({ error: String(error) });
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
setCurrentAnalysis: (analysis) => {
|
||||
set({ currentAnalysis: analysis });
|
||||
},
|
||||
|
||||
// 单品相关操作
|
||||
fetchItems: async (options = {}) => {
|
||||
set({ isLoading: true, error: null });
|
||||
try {
|
||||
const items = await invoke<OutfitItem[]>('list_outfit_items', { options });
|
||||
set({ items, isLoading: false });
|
||||
} catch (error) {
|
||||
set({ error: String(error), isLoading: false });
|
||||
}
|
||||
},
|
||||
|
||||
createItem: async (request) => {
|
||||
set({ isLoading: true, error: null });
|
||||
try {
|
||||
const item = await invoke<OutfitItem>('create_outfit_item', { request });
|
||||
set(state => ({
|
||||
items: [item, ...state.items],
|
||||
isLoading: false
|
||||
}));
|
||||
return item;
|
||||
} catch (error) {
|
||||
set({ error: String(error), isLoading: false });
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
createItemsFromAnalysis: async (projectId, analysisId) => {
|
||||
set({ isLoading: true, error: null });
|
||||
try {
|
||||
const items = await invoke<OutfitItem[]>('create_outfit_items_from_analysis', {
|
||||
projectId,
|
||||
analysisId
|
||||
});
|
||||
set(state => ({
|
||||
items: [...items, ...state.items],
|
||||
isLoading: false
|
||||
}));
|
||||
return items;
|
||||
} catch (error) {
|
||||
set({ error: String(error), isLoading: false });
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
updateItem: async (id, request) => {
|
||||
try {
|
||||
await invoke('update_outfit_item', { id, request });
|
||||
// 重新获取单品列表
|
||||
const { fetchItems } = get();
|
||||
await fetchItems();
|
||||
} catch (error) {
|
||||
set({ error: String(error) });
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
deleteItem: async (id) => {
|
||||
try {
|
||||
await invoke('delete_outfit_item', { id });
|
||||
set(state => ({
|
||||
items: state.items.filter(i => i.id !== id),
|
||||
selectedItems: state.selectedItems.filter(itemId => itemId !== id)
|
||||
}));
|
||||
} catch (error) {
|
||||
set({ error: String(error) });
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
setCurrentItem: (item) => {
|
||||
set({ currentItem: item });
|
||||
},
|
||||
|
||||
setSelectedItems: (itemIds) => {
|
||||
set({ selectedItems: itemIds });
|
||||
},
|
||||
|
||||
toggleItemSelection: (itemId) => {
|
||||
set(state => ({
|
||||
selectedItems: state.selectedItems.includes(itemId)
|
||||
? state.selectedItems.filter(id => id !== itemId)
|
||||
: [...state.selectedItems, itemId]
|
||||
}));
|
||||
},
|
||||
|
||||
// 搭配相关操作
|
||||
fetchMatchings: async (options = {}) => {
|
||||
set({ isLoading: true, error: null });
|
||||
try {
|
||||
const matchings = await invoke<OutfitMatching[]>('list_outfit_matchings', { options });
|
||||
set({ matchings, isLoading: false });
|
||||
} catch (error) {
|
||||
set({ error: String(error), isLoading: false });
|
||||
}
|
||||
},
|
||||
|
||||
createMatching: async (request) => {
|
||||
set({ isLoading: true, error: null });
|
||||
try {
|
||||
const matching = await invoke<OutfitMatching>('create_outfit_matching', { request });
|
||||
set(state => ({
|
||||
matchings: [matching, ...state.matchings],
|
||||
isLoading: false,
|
||||
selectedItems: [] // 清空选择
|
||||
}));
|
||||
return matching;
|
||||
} catch (error) {
|
||||
set({ error: String(error), isLoading: false });
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
updateMatching: async (id, request) => {
|
||||
try {
|
||||
await invoke('update_outfit_matching', { id, request });
|
||||
// 重新获取搭配列表
|
||||
const { fetchMatchings } = get();
|
||||
await fetchMatchings();
|
||||
} catch (error) {
|
||||
set({ error: String(error) });
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
deleteMatching: async (id) => {
|
||||
try {
|
||||
await invoke('delete_outfit_matching', { id });
|
||||
set(state => ({
|
||||
matchings: state.matchings.filter(m => m.id !== id)
|
||||
}));
|
||||
} catch (error) {
|
||||
set({ error: String(error) });
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
toggleFavorite: async (id) => {
|
||||
try {
|
||||
const matching = get().matchings.find(m => m.id === id);
|
||||
if (matching) {
|
||||
await invoke('update_outfit_matching', {
|
||||
id,
|
||||
request: { is_favorite: !matching.is_favorite }
|
||||
});
|
||||
set(state => ({
|
||||
matchings: state.matchings.map(m =>
|
||||
m.id === id ? { ...m, is_favorite: !m.is_favorite } : m
|
||||
)
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
set({ error: String(error) });
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
incrementWearCount: async (id) => {
|
||||
try {
|
||||
await invoke('increment_outfit_matching_wear_count', { id });
|
||||
// 重新获取搭配列表
|
||||
const { fetchMatchings } = get();
|
||||
await fetchMatchings();
|
||||
} catch (error) {
|
||||
set({ error: String(error) });
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
smartMatching: async (request) => {
|
||||
set({ isLoading: true, error: null });
|
||||
try {
|
||||
const result = await invoke('smart_outfit_matching', { request });
|
||||
set({ isLoading: false });
|
||||
return result;
|
||||
} catch (error) {
|
||||
set({ error: String(error), isLoading: false });
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
setCurrentMatching: (matching) => {
|
||||
set({ currentMatching: matching });
|
||||
},
|
||||
|
||||
// 搜索和过滤
|
||||
setSearchFilters: (filters) => {
|
||||
set({ searchFilters: filters });
|
||||
},
|
||||
|
||||
clearFilters: () => {
|
||||
set({ searchFilters: {} });
|
||||
},
|
||||
|
||||
// 通用操作
|
||||
clearError: () => {
|
||||
set({ error: null });
|
||||
},
|
||||
|
||||
setLoading: (loading) => {
|
||||
set({ isLoading: loading });
|
||||
},
|
||||
}));
|
||||
289
docs/outfit-matching-feature.md
Normal file
289
docs/outfit-matching-feature.md
Normal file
@@ -0,0 +1,289 @@
|
||||
# 服装搭配功能文档
|
||||
|
||||
## 概述
|
||||
|
||||
服装搭配功能是一个基于AI的智能服装分析和搭配推荐系统,集成到现有的Tauri桌面应用中。该功能提供图像分析、服装单品管理和智能搭配推荐等核心能力。
|
||||
|
||||
## 功能特性
|
||||
|
||||
### 1. 图像分析
|
||||
- **AI图像识别**: 使用Google Gemini API分析服装图像
|
||||
- **自动分类**: 识别服装类别(上装、下装、连衣裙、外套等)
|
||||
- **颜色分析**: 提取HSV颜色模型,支持精确的颜色匹配
|
||||
- **风格识别**: 识别服装风格(休闲、正式、运动、时尚等)
|
||||
- **环境分析**: 分析图像环境和场景
|
||||
|
||||
### 2. 服装单品管理
|
||||
- **单品档案**: 创建详细的服装单品信息
|
||||
- **属性管理**: 管理颜色、风格、品牌、尺寸等属性
|
||||
- **标签系统**: 支持自定义标签分类
|
||||
- **图片管理**: 支持多张图片展示
|
||||
- **搜索过滤**: 按类别、颜色、风格等条件搜索
|
||||
|
||||
### 3. 智能搭配推荐
|
||||
- **算法匹配**: 基于颜色和谐度、风格一致性等算法
|
||||
- **评分系统**: 提供详细的搭配评分和建议
|
||||
- **场合推荐**: 根据不同场合推荐合适搭配
|
||||
- **季节搭配**: 考虑季节因素的搭配建议
|
||||
- **收藏管理**: 支持收藏喜欢的搭配方案
|
||||
|
||||
## 技术架构
|
||||
|
||||
### 后端架构 (Rust)
|
||||
|
||||
#### 数据模型层
|
||||
```
|
||||
src/data/models/
|
||||
├── outfit_analysis.rs # 分析记录模型
|
||||
├── outfit_item.rs # 服装单品模型
|
||||
└── outfit_matching.rs # 搭配匹配模型
|
||||
```
|
||||
|
||||
#### 数据访问层
|
||||
```
|
||||
src/data/repositories/
|
||||
├── outfit_analysis_repository.rs
|
||||
├── outfit_item_repository.rs
|
||||
└── outfit_matching_repository.rs
|
||||
```
|
||||
|
||||
#### 业务逻辑层
|
||||
```
|
||||
src/business/services/
|
||||
├── outfit_analysis_service.rs
|
||||
├── outfit_item_service.rs
|
||||
└── outfit_matching_service.rs
|
||||
```
|
||||
|
||||
#### API接口层
|
||||
```
|
||||
src/presentation/commands/
|
||||
└── outfit_commands.rs # Tauri命令接口
|
||||
```
|
||||
|
||||
### 前端架构 (React + TypeScript)
|
||||
|
||||
#### 页面组件
|
||||
```
|
||||
src/pages/
|
||||
└── OutfitMatch.tsx # 主页面组件
|
||||
```
|
||||
|
||||
#### 功能组件
|
||||
```
|
||||
src/components/outfit/
|
||||
├── ImageUploader.tsx # 图像上传组件
|
||||
├── OutfitAnalysisResult.tsx # 分析结果展示
|
||||
├── OutfitSearchPanel.tsx # 搜索面板
|
||||
├── OutfitCard.tsx # 搭配卡片
|
||||
└── OutfitItemCard.tsx # 单品卡片
|
||||
```
|
||||
|
||||
#### 状态管理
|
||||
```
|
||||
src/stores/
|
||||
└── outfitStore.ts # Zustand状态管理
|
||||
```
|
||||
|
||||
## 数据库设计
|
||||
|
||||
### 表结构
|
||||
|
||||
#### outfit_analyses (分析记录表)
|
||||
```sql
|
||||
CREATE TABLE outfit_analyses (
|
||||
id TEXT PRIMARY KEY,
|
||||
project_id TEXT NOT NULL,
|
||||
image_path TEXT NOT NULL,
|
||||
image_name TEXT NOT NULL,
|
||||
analysis_status TEXT NOT NULL DEFAULT 'Pending',
|
||||
analysis_result TEXT,
|
||||
error_message TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
analyzed_at DATETIME,
|
||||
FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE
|
||||
);
|
||||
```
|
||||
|
||||
#### outfit_items (服装单品表)
|
||||
```sql
|
||||
CREATE TABLE outfit_items (
|
||||
id TEXT PRIMARY KEY,
|
||||
project_id TEXT NOT NULL,
|
||||
analysis_id TEXT,
|
||||
name TEXT NOT NULL,
|
||||
category TEXT NOT NULL,
|
||||
brand TEXT,
|
||||
model TEXT,
|
||||
color_primary TEXT NOT NULL,
|
||||
color_secondary TEXT,
|
||||
styles TEXT NOT NULL,
|
||||
design_elements TEXT,
|
||||
size_info TEXT,
|
||||
material_info TEXT,
|
||||
price REAL,
|
||||
purchase_date DATETIME,
|
||||
image_urls TEXT,
|
||||
tags TEXT,
|
||||
notes TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE
|
||||
);
|
||||
```
|
||||
|
||||
#### outfit_matchings (搭配记录表)
|
||||
```sql
|
||||
CREATE TABLE outfit_matchings (
|
||||
id TEXT PRIMARY KEY,
|
||||
project_id TEXT NOT NULL,
|
||||
matching_name TEXT NOT NULL,
|
||||
matching_type TEXT NOT NULL,
|
||||
items TEXT NOT NULL,
|
||||
score_details TEXT NOT NULL,
|
||||
suggestions TEXT,
|
||||
occasion_tags TEXT,
|
||||
season_tags TEXT,
|
||||
style_description TEXT NOT NULL,
|
||||
color_palette TEXT,
|
||||
is_favorite BOOLEAN DEFAULT 0,
|
||||
wear_count INTEGER DEFAULT 0,
|
||||
last_worn_date DATETIME,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE
|
||||
);
|
||||
```
|
||||
|
||||
## API接口
|
||||
|
||||
### Tauri命令接口
|
||||
|
||||
#### 分析相关
|
||||
- `create_outfit_analysis` - 创建分析记录
|
||||
- `start_outfit_analysis` - 开始分析
|
||||
- `list_outfit_analyses` - 获取分析列表
|
||||
- `get_outfit_analysis_by_id` - 获取分析详情
|
||||
- `delete_outfit_analysis` - 删除分析记录
|
||||
|
||||
#### 单品相关
|
||||
- `create_outfit_item` - 创建服装单品
|
||||
- `create_outfit_items_from_analysis` - 从分析结果创建单品
|
||||
- `list_outfit_items` - 获取单品列表
|
||||
- `get_outfit_item_by_id` - 获取单品详情
|
||||
- `update_outfit_item` - 更新单品信息
|
||||
- `delete_outfit_item` - 删除单品
|
||||
|
||||
#### 搭配相关
|
||||
- `create_outfit_matching` - 创建搭配
|
||||
- `list_outfit_matchings` - 获取搭配列表
|
||||
- `get_outfit_matching_by_id` - 获取搭配详情
|
||||
- `update_outfit_matching` - 更新搭配信息
|
||||
- `delete_outfit_matching` - 删除搭配
|
||||
- `smart_outfit_matching` - 智能搭配推荐
|
||||
- `increment_outfit_matching_wear_count` - 增加穿着次数
|
||||
|
||||
## 算法设计
|
||||
|
||||
### 颜色匹配算法
|
||||
使用HSV颜色空间进行颜色相似度计算:
|
||||
```rust
|
||||
pub fn similarity(&self, other: &ColorHSV) -> f64 {
|
||||
let hue_diff = (self.hue - other.hue).abs().min(1.0 - (self.hue - other.hue).abs());
|
||||
let sat_diff = (self.saturation - other.saturation).abs();
|
||||
let val_diff = (self.value - other.value).abs();
|
||||
|
||||
// 加权计算相似度,色相权重最高
|
||||
let similarity = 1.0 - (hue_diff * 0.5 + sat_diff * 0.3 + val_diff * 0.2);
|
||||
similarity.clamp(0.0, 1.0)
|
||||
}
|
||||
```
|
||||
|
||||
### 搭配评分算法
|
||||
综合考虑多个维度:
|
||||
- **颜色和谐度** (权重: 30%): 基于颜色理论的和谐度计算
|
||||
- **风格一致性** (权重: 25%): 风格标签的匹配度
|
||||
- **比例协调度** (权重: 20%): 服装比例的协调性
|
||||
- **场合适宜度** (权重: 15%): 适合特定场合的程度
|
||||
- **时尚度** (权重: 10%): 当前流行趋势的符合度
|
||||
|
||||
## 使用指南
|
||||
|
||||
### 1. 图像分析流程
|
||||
1. 在项目详情页面切换到"服装搭配"标签
|
||||
2. 上传服装图像文件
|
||||
3. 系统自动进行AI分析
|
||||
4. 查看分析结果和识别的服装单品
|
||||
5. 可选择创建服装单品档案
|
||||
|
||||
### 2. 搭配创建流程
|
||||
1. 在服装单品页面选择要搭配的单品
|
||||
2. 点击"创建搭配"按钮
|
||||
3. 系统自动计算搭配评分
|
||||
4. 查看搭配建议和优化提示
|
||||
5. 保存搭配方案
|
||||
|
||||
### 3. 智能推荐流程
|
||||
1. 点击"智能搭配"按钮
|
||||
2. 系统基于现有单品生成推荐搭配
|
||||
3. 查看推荐结果和评分
|
||||
4. 选择喜欢的搭配保存
|
||||
|
||||
## 测试
|
||||
|
||||
### 单元测试
|
||||
- 颜色相似度计算测试
|
||||
- 搭配评分算法测试
|
||||
- 数据模型验证测试
|
||||
|
||||
### 集成测试
|
||||
- 完整分析流程测试
|
||||
- 搭配推荐功能测试
|
||||
- 数据库操作测试
|
||||
|
||||
### 运行测试
|
||||
```bash
|
||||
cd apps/desktop/src-tauri
|
||||
cargo test outfit
|
||||
```
|
||||
|
||||
## 部署和配置
|
||||
|
||||
### 环境要求
|
||||
- Rust 1.70+
|
||||
- Node.js 18+
|
||||
- Google Cloud API密钥
|
||||
- SQLite数据库
|
||||
|
||||
### 配置步骤
|
||||
1. 配置Google Cloud API密钥
|
||||
2. 设置Gemini服务参数
|
||||
3. 初始化数据库表结构
|
||||
4. 编译和运行应用
|
||||
|
||||
## 未来规划
|
||||
|
||||
### 短期目标
|
||||
- [ ] 完善图像上传和分析功能
|
||||
- [ ] 实现完整的搭配推荐算法
|
||||
- [ ] 添加更多服装类别支持
|
||||
- [ ] 优化用户界面和交互
|
||||
|
||||
### 长期目标
|
||||
- [ ] 支持视频分析
|
||||
- [ ] 集成更多AI模型
|
||||
- [ ] 社交分享功能
|
||||
- [ ] 个性化推荐系统
|
||||
- [ ] 移动端适配
|
||||
|
||||
## 贡献指南
|
||||
|
||||
1. 遵循现有的代码规范和架构模式
|
||||
2. 添加适当的单元测试和集成测试
|
||||
3. 更新相关文档
|
||||
4. 提交前进行代码审查
|
||||
|
||||
## 许可证
|
||||
|
||||
本项目遵循与主项目相同的许可证。
|
||||
Reference in New Issue
Block a user