Add multi-theme system with random theme selection on app startup
42
.gitignore
vendored
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
# private key and sensitive files
|
||||||
|
keystore.properties
|
||||||
|
upload-keystore.properties
|
||||||
|
upload-keystore.jks
|
||||||
|
*.jks
|
||||||
|
*.pem
|
||||||
|
*.p12
|
||||||
|
*.pfx
|
||||||
|
.env*
|
||||||
|
*.key
|
||||||
|
|
||||||
|
# Tauri build artifacts and generated files
|
||||||
|
src-tauri/gen/
|
||||||
|
src-tauri/target/
|
||||||
|
src-tauri/.gradle/
|
||||||
|
*.apk
|
||||||
|
*.aab
|
48
CLAUDE.md
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Development Commands
|
||||||
|
|
||||||
|
### Frontend Development
|
||||||
|
- `pnpm dev` - Start the frontend development server (runs Vite on localhost:1420)
|
||||||
|
- `pnpm build` - Build the frontend for production (TypeScript compilation + Vite build)
|
||||||
|
- `pnpm preview` - Preview the production build
|
||||||
|
|
||||||
|
### Tauri Development
|
||||||
|
- `pnpm tauri dev` - Start Tauri development mode (builds Rust backend + starts frontend)
|
||||||
|
- `pnpm tauri build` - Build the complete Tauri application for production
|
||||||
|
|
||||||
|
### Build Scripts
|
||||||
|
- `./build_desktop.sh` - Build desktop application
|
||||||
|
- `./build_android.sh` - Build Android application
|
||||||
|
|
||||||
|
## Architecture Overview
|
||||||
|
|
||||||
|
This is a **Tauri application** combining a Rust backend with a React/TypeScript frontend for building cross-platform desktop and mobile apps.
|
||||||
|
|
||||||
|
### Backend (Rust)
|
||||||
|
- **Database**: SQLite with rusqlite for storing anniversary records
|
||||||
|
- **Location**: Database stored in app data directory (`anniversaries.db`)
|
||||||
|
- **Schema**: `anniversaries` table with id, title, start_date, created_at
|
||||||
|
- **Core Commands**: All database operations exposed as Tauri commands in `src-tauri/src/lib.rs`
|
||||||
|
- `get_anniversaries` - Fetch all anniversaries with calculated days
|
||||||
|
- `get_anniversary_by_id` - Fetch single anniversary by ID
|
||||||
|
- `add_anniversary` - Add new anniversary
|
||||||
|
- `delete_anniversary` - Delete anniversary by ID
|
||||||
|
|
||||||
|
### Frontend (React + TypeScript)
|
||||||
|
- **Router**: React Router DOM with three main routes:
|
||||||
|
- `/` - HomePage (list all anniversaries)
|
||||||
|
- `/detail/:id` - DetailPage (view/delete specific anniversary)
|
||||||
|
- `/add` - AddPage (create new anniversary)
|
||||||
|
- **Data Flow**: Frontend calls Tauri commands to interact with Rust backend
|
||||||
|
- **Styling**: Component-specific CSS files for each page
|
||||||
|
|
||||||
|
### Key Dependencies
|
||||||
|
- **Frontend**: React 18, React Router DOM, Tauri API
|
||||||
|
- **Backend**: rusqlite (SQLite), chrono (date handling), serde (serialization)
|
||||||
|
- **Build**: Vite, TypeScript, Tauri CLI
|
||||||
|
|
||||||
|
### Package Manager
|
||||||
|
This project uses **pnpm** as specified in `tauri.conf.json` build commands.
|
7
README.md
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# Tauri + React + Typescript
|
||||||
|
|
||||||
|
This template should help get you started developing with Tauri, React and Typescript in Vite.
|
||||||
|
|
||||||
|
## Recommended IDE Setup
|
||||||
|
|
||||||
|
- [VS Code](https://code.visualstudio.com/) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)
|
1
build_android.sh
Executable file
@ -0,0 +1 @@
|
|||||||
|
pnpm tauri android build --apk -t aarch64
|
1
build_desktop.sh
Executable file
@ -0,0 +1 @@
|
|||||||
|
pnpm tauri build --no-bundle
|
14
index.html
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Tauri + React + Typescript</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
27
package.json
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"name": "countdown",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.1.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"tauri": "cargo tauri"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@tauri-apps/api": "^2.5.0",
|
||||||
|
"@tauri-apps/plugin-opener": "^2.2.7",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
|
"react-router-dom": "^7.1.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tauri-apps/cli": "^2",
|
||||||
|
"@types/react": "^18.3.1",
|
||||||
|
"@types/react-dom": "^18.3.1",
|
||||||
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
|
"typescript": "~5.6.2",
|
||||||
|
"vite": "^6.0.3"
|
||||||
|
}
|
||||||
|
}
|
1239
pnpm-lock.yaml
generated
Normal file
7
src-tauri/.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# Generated by Cargo
|
||||||
|
# will have compiled files and executables
|
||||||
|
/target/
|
||||||
|
|
||||||
|
# Generated by Tauri
|
||||||
|
# will have schema files for capabilities auto-completion
|
||||||
|
/gen/schemas
|
5101
src-tauri/Cargo.lock
generated
Normal file
26
src-tauri/Cargo.toml
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
[package]
|
||||||
|
name = "countdown"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "A Countdown app"
|
||||||
|
authors = ["sangge"]
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
# The `_lib` suffix may seem redundant but it is necessary
|
||||||
|
# to make the lib name unique and wouldn't conflict with the bin name.
|
||||||
|
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
|
||||||
|
name = "countdown_lib"
|
||||||
|
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
tauri-build = { version = "2", features = [] }
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
rusqlite = { version = "0.33.0", features = ["bundled"] }
|
||||||
|
tauri = { version = "2", features = [] }
|
||||||
|
tauri-plugin-opener = "2"
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
chrono = "0.4.39"
|
3
src-tauri/build.rs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
tauri_build::build()
|
||||||
|
}
|
10
src-tauri/capabilities/default.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../gen/schemas/desktop-schema.json",
|
||||||
|
"identifier": "default",
|
||||||
|
"description": "Capability for the main window",
|
||||||
|
"windows": ["main"],
|
||||||
|
"permissions": [
|
||||||
|
"core:default",
|
||||||
|
"opener:default"
|
||||||
|
]
|
||||||
|
}
|
BIN
src-tauri/icons/128x128.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
src-tauri/icons/128x128@2x.png
Normal file
After Width: | Height: | Size: 4.1 KiB |
BIN
src-tauri/icons/32x32.png
Normal file
After Width: | Height: | Size: 482 B |
BIN
src-tauri/icons/4096x4096.png
Normal file
After Width: | Height: | Size: 118 KiB |
BIN
src-tauri/icons/Square107x107Logo.png
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
src-tauri/icons/Square142x142Logo.png
Normal file
After Width: | Height: | Size: 3.8 KiB |
BIN
src-tauri/icons/Square150x150Logo.png
Normal file
After Width: | Height: | Size: 3.9 KiB |
BIN
src-tauri/icons/Square284x284Logo.png
Normal file
After Width: | Height: | Size: 7.6 KiB |
BIN
src-tauri/icons/Square30x30Logo.png
Normal file
After Width: | Height: | Size: 447 B |
BIN
src-tauri/icons/Square310x310Logo.png
Normal file
After Width: | Height: | Size: 8.4 KiB |
BIN
src-tauri/icons/Square44x44Logo.png
Normal file
After Width: | Height: | Size: 509 B |
BIN
src-tauri/icons/Square71x71Logo.png
Normal file
After Width: | Height: | Size: 755 B |
BIN
src-tauri/icons/Square89x89Logo.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
src-tauri/icons/StoreLogo.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
src-tauri/icons/icon.icns
Normal file
BIN
src-tauri/icons/icon.ico
Normal file
After Width: | Height: | Size: 103 KiB |
BIN
src-tauri/icons/icon.png
Normal file
After Width: | Height: | Size: 7.4 KiB |
174
src-tauri/src/lib.rs
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
use chrono::{Local, NaiveDate};
|
||||||
|
use rusqlite::{Connection, Result};
|
||||||
|
use serde::Serialize;
|
||||||
|
use std::fs;
|
||||||
|
use tauri::Manager;
|
||||||
|
|
||||||
|
fn init_database(app: &tauri::App) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
// 获取应用数据目录
|
||||||
|
let app_dir = app.path().app_data_dir()?;
|
||||||
|
|
||||||
|
// 确保目录存在
|
||||||
|
fs::create_dir_all(&app_dir)?;
|
||||||
|
|
||||||
|
// 构建数据库文件路径
|
||||||
|
let db_path = app_dir.join("anniversaries.db");
|
||||||
|
|
||||||
|
// 打开数据库连接
|
||||||
|
let conn = Connection::open(db_path)?;
|
||||||
|
|
||||||
|
// 创建表
|
||||||
|
conn.execute(
|
||||||
|
"CREATE TABLE IF NOT EXISTS anniversaries (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
start_date TEXT NOT NULL,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)",
|
||||||
|
[],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct Anniversary {
|
||||||
|
id: i32,
|
||||||
|
title: String,
|
||||||
|
start_date: String,
|
||||||
|
days: i64,
|
||||||
|
}
|
||||||
|
#[tauri::command]
|
||||||
|
async fn get_anniversaries(app_handle: tauri::AppHandle) -> Result<Vec<Anniversary>, String> {
|
||||||
|
// 返回主页数据
|
||||||
|
let app_dir = app_handle
|
||||||
|
.path()
|
||||||
|
.app_data_dir()
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
let db_path = app_dir.join("anniversaries.db");
|
||||||
|
let conn = Connection::open(db_path).map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
let mut stmt = conn
|
||||||
|
.prepare("SELECT id, title, start_date FROM anniversaries ORDER BY created_at DESC")
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
let today = Local::now().date_naive();
|
||||||
|
|
||||||
|
let anniversaries = stmt
|
||||||
|
.query_map([], |row| {
|
||||||
|
let id: i32 = row.get(0)?;
|
||||||
|
let title: String = row.get(1)?;
|
||||||
|
let start_date: String = row.get(2)?;
|
||||||
|
|
||||||
|
// 计算天数
|
||||||
|
let start = NaiveDate::parse_from_str(&start_date, "%Y-%m-%d")
|
||||||
|
.map_err(|e| rusqlite::Error::InvalidParameterName(e.to_string()))?;
|
||||||
|
let days = (today - start).num_days();
|
||||||
|
|
||||||
|
Ok(Anniversary {
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
start_date,
|
||||||
|
days,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
let result: Result<Vec<_>, _> = anniversaries.collect();
|
||||||
|
result.map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn get_anniversary_by_id(
|
||||||
|
app_handle: tauri::AppHandle,
|
||||||
|
id: i32,
|
||||||
|
) -> Result<Anniversary, String> {
|
||||||
|
// 返回详情页数据
|
||||||
|
let app_dir = app_handle
|
||||||
|
.path()
|
||||||
|
.app_data_dir()
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
let db_path = app_dir.join("anniversaries.db");
|
||||||
|
let conn = Connection::open(db_path).map_err(|e| e.to_string())?;
|
||||||
|
let mut stmt = conn
|
||||||
|
.prepare("SELECT title, start_date FROM anniversaries WHERE id = ?")
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
let today = Local::now().date_naive();
|
||||||
|
|
||||||
|
let result = stmt
|
||||||
|
.query_row([id], |row| {
|
||||||
|
let title: String = row.get(0)?;
|
||||||
|
let start_date: String = row.get(1)?;
|
||||||
|
|
||||||
|
// 计算天数
|
||||||
|
let start = NaiveDate::parse_from_str(&start_date, "%Y-%m-%d")
|
||||||
|
.map_err(|e| rusqlite::Error::InvalidParameterName(e.to_string()))?;
|
||||||
|
let days = (today - start).num_days();
|
||||||
|
|
||||||
|
Ok(Anniversary {
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
start_date,
|
||||||
|
days,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn delete_anniversary(app_handle: tauri::AppHandle, id: i64) -> Result<(), String> {
|
||||||
|
// 详情页删除
|
||||||
|
let app_dir = app_handle
|
||||||
|
.path()
|
||||||
|
.app_data_dir()
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
let db_path = app_dir.join("anniversaries.db");
|
||||||
|
let conn = Connection::open(db_path).map_err(|e| e.to_string())?;
|
||||||
|
conn.execute("DELETE FROM anniversaries WHERE id = ?", [id])
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn add_anniversary(
|
||||||
|
app_handle: tauri::AppHandle,
|
||||||
|
title: String,
|
||||||
|
start_date: String,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
// 主页添加
|
||||||
|
let app_dir = app_handle
|
||||||
|
.path()
|
||||||
|
.app_data_dir()
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
let db_path = app_dir.join("anniversaries.db");
|
||||||
|
let conn = Connection::open(db_path).map_err(|e| e.to_string())?;
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO anniversaries (title, start_date) VALUES (?1, ?2)",
|
||||||
|
[&title, &start_date],
|
||||||
|
)
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
|
pub fn run() {
|
||||||
|
tauri::Builder::default()
|
||||||
|
.plugin(tauri_plugin_opener::init())
|
||||||
|
.setup(|app| {
|
||||||
|
init_database(app).expect("failed to initialize database");
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.invoke_handler(tauri::generate_handler![
|
||||||
|
get_anniversaries,
|
||||||
|
get_anniversary_by_id,
|
||||||
|
add_anniversary,
|
||||||
|
delete_anniversary,
|
||||||
|
])
|
||||||
|
.run(tauri::generate_context!())
|
||||||
|
.expect("error while running tauri application");
|
||||||
|
}
|
6
src-tauri/src/main.rs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||||
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
countdown_lib::run()
|
||||||
|
}
|
36
src-tauri/tauri.conf.json
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
|
"productName": "countdown",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"identifier": "com.countdown.app",
|
||||||
|
"build": {
|
||||||
|
"beforeDevCommand": "pnpm dev",
|
||||||
|
"devUrl": "http://localhost:1420",
|
||||||
|
"beforeBuildCommand": "pnpm build",
|
||||||
|
"frontendDist": "../dist"
|
||||||
|
},
|
||||||
|
"app": {
|
||||||
|
"windows": [
|
||||||
|
{
|
||||||
|
"title": "countdown",
|
||||||
|
"width": 800,
|
||||||
|
"height": 600
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"security": {
|
||||||
|
"csp": "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bundle": {
|
||||||
|
"active": true,
|
||||||
|
"targets": ["deb", "rpm", "appimage", "nsis", "msi", "dmg", "app"],
|
||||||
|
"icon": [
|
||||||
|
"icons/32x32.png",
|
||||||
|
"icons/128x128.png",
|
||||||
|
"icons/128x128@2x.png",
|
||||||
|
"icons/icon.png",
|
||||||
|
"icons/icon.icns",
|
||||||
|
"icons/icon.ico"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
222
src/AddPage.css
Normal file
@ -0,0 +1,222 @@
|
|||||||
|
/* 基础样式 */
|
||||||
|
.container {
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 24px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 8px 16px;
|
||||||
|
font-size: 16px;
|
||||||
|
color: var(--color-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button:hover {
|
||||||
|
transform: translateX(-4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-icon {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
margin: 0;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-card {
|
||||||
|
background: var(--bg-card);
|
||||||
|
padding: 32px;
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border: 2px solid var(--color-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
transition: border-color 0.2s ease;
|
||||||
|
outline: none;
|
||||||
|
background: var(--bg-card);
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input:focus {
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input::placeholder {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-button {
|
||||||
|
width: 100%;
|
||||||
|
padding: 14px;
|
||||||
|
background: var(--color-primary);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-button:hover {
|
||||||
|
background: var(--color-primary-hover);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: var(--shadow-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-button:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 桌面端样式 */
|
||||||
|
@media screen and (min-width: 768px) {
|
||||||
|
.container {
|
||||||
|
padding: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-card {
|
||||||
|
padding: 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 平板端样式 */
|
||||||
|
@media screen and (max-width: 767px) and (min-width: 481px) {
|
||||||
|
.container {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-card {
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端样式 */
|
||||||
|
@media screen and (max-width: 480px) {
|
||||||
|
.container {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-bar {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button {
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-icon {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-card {
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input {
|
||||||
|
padding: 10px 14px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-button {
|
||||||
|
padding: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 特小屏幕样式 */
|
||||||
|
@media screen and (max-width: 320px) {
|
||||||
|
.container {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-card {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button {
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 确保所有元素使用 border-box */
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 优化输入控件在移动端的表现 */
|
||||||
|
@media (hover: none) {
|
||||||
|
input {
|
||||||
|
font-size: 16px !important; /* 防止 iOS 自动缩放 */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 针对暗色模式的优化 */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.form-card {
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.16);
|
||||||
|
}
|
||||||
|
}
|
59
src/AddPage.tsx
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
import "./AddPage.css";
|
||||||
|
|
||||||
|
function AddPage() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [title, setTitle] = useState("");
|
||||||
|
const [date, setDate] = useState("");
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
try {
|
||||||
|
await invoke("add_anniversary", {
|
||||||
|
title,
|
||||||
|
startDate: date,
|
||||||
|
});
|
||||||
|
// 添加成功后返回主页
|
||||||
|
navigate("/");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to add anniversary:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container">
|
||||||
|
<div className="nav-bar">
|
||||||
|
<button className="back-button" onClick={() => navigate("/")}>
|
||||||
|
<span className="back-icon">←</span>
|
||||||
|
<span>Back</span>
|
||||||
|
</button>
|
||||||
|
<h1 className="page-title">Add New</h1>
|
||||||
|
</div>
|
||||||
|
<div className="form-card">
|
||||||
|
<div className="form-group">
|
||||||
|
<label>Title</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={title}
|
||||||
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
|
placeholder="Enter anniversary title"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="form-group">
|
||||||
|
<label>Start Date</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={date}
|
||||||
|
onChange={(e) => setDate(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button className="submit-button" onClick={handleSubmit}>
|
||||||
|
Create Anniversary
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AddPage;
|
1
src/App.css
Normal file
@ -0,0 +1 @@
|
|||||||
|
/* App.css */
|
36
src/App.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import HomePage from "./HomePage";
|
||||||
|
import DetailPage from "./DetailPage";
|
||||||
|
import AddPage from "./AddPage";
|
||||||
|
import "./themes.css";
|
||||||
|
|
||||||
|
const THEMES = [
|
||||||
|
"blue",
|
||||||
|
"green",
|
||||||
|
"purple",
|
||||||
|
"orange",
|
||||||
|
"pink",
|
||||||
|
"cyan",
|
||||||
|
"dark",
|
||||||
|
"gradient"
|
||||||
|
];
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
useEffect(() => {
|
||||||
|
const randomTheme = THEMES[Math.floor(Math.random() * THEMES.length)];
|
||||||
|
document.documentElement.setAttribute('data-theme', randomTheme);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BrowserRouter>
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<HomePage />} />
|
||||||
|
<Route path="/detail/:id" element={<DetailPage />} />
|
||||||
|
<Route path="/add" element={<AddPage />} />
|
||||||
|
</Routes>
|
||||||
|
</BrowserRouter>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
79
src/DetailPage.css
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
.container {
|
||||||
|
padding: 24px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
min-height: 100vh;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--color-primary);
|
||||||
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 8px 16px;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button:hover {
|
||||||
|
transform: translateX(-4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-button {
|
||||||
|
background-color: var(--color-danger);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 15px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-button:hover {
|
||||||
|
background-color: var(--color-danger-hover);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(255, 68, 68, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: var(--bg-card);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 32px;
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 32px;
|
||||||
|
color: var(--color-text);
|
||||||
|
margin: 0 0 24px 0;
|
||||||
|
font-weight: 600;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.days {
|
||||||
|
font-size: 64px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--color-primary);
|
||||||
|
margin: 32px 0;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-date {
|
||||||
|
font-size: 16px;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
66
src/DetailPage.tsx
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { useParams, useNavigate } from "react-router-dom";
|
||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
import "./DetailPage.css";
|
||||||
|
|
||||||
|
interface Anniversary {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
start_date: string;
|
||||||
|
days: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DetailPage() {
|
||||||
|
const { id } = useParams(); // 获取路由参数
|
||||||
|
const navigate = useNavigate(); // 获取导航函数
|
||||||
|
const [anniversary, setAnniversary] = useState<Anniversary>({
|
||||||
|
id: 0,
|
||||||
|
title: "",
|
||||||
|
start_date: "",
|
||||||
|
days: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const getAnniversaryData = async () => {
|
||||||
|
if (id) {
|
||||||
|
const result = await invoke<Anniversary>("get_anniversary_by_id", {
|
||||||
|
id: parseInt(id),
|
||||||
|
});
|
||||||
|
setAnniversary(result);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
getAnniversaryData();
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
if (id) {
|
||||||
|
try {
|
||||||
|
await invoke("delete_anniversary", { id: parseInt(id) });
|
||||||
|
navigate("/"); // 删除成功后返回主页
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to delete anniversary:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container">
|
||||||
|
<div className="nav-bar">
|
||||||
|
<button className="back-button" onClick={() => navigate("/")}>
|
||||||
|
<span className="back-icon">←</span>
|
||||||
|
<span>Back</span>
|
||||||
|
</button>
|
||||||
|
<button className="delete-button" onClick={handleDelete}>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="card">
|
||||||
|
<h1 className="title">{anniversary.title}</h1>
|
||||||
|
<div className="days">{anniversary.days}</div>
|
||||||
|
<div className="start-date">Start Date: {anniversary.start_date}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DetailPage;
|
181
src/HomePage.css
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
.container {
|
||||||
|
padding: 16px;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
min-height: 100vh;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
position: relative;
|
||||||
|
padding: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 28px;
|
||||||
|
color: var(--color-text);
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-button {
|
||||||
|
position: absolute;
|
||||||
|
right: 20px;
|
||||||
|
background-color: var(--color-primary);
|
||||||
|
border: none;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
box-shadow: var(--shadow-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-button:hover {
|
||||||
|
background-color: var(--color-primary-hover);
|
||||||
|
transform: scale(1.05);
|
||||||
|
box-shadow: var(--shadow-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.plus-icon {
|
||||||
|
color: white;
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 1;
|
||||||
|
display: block;
|
||||||
|
margin-top: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.anniversary-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
overflow-y: auto;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.anniversary-card {
|
||||||
|
background: var(--bg-card);
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
transform 0.2s ease,
|
||||||
|
box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.anniversary-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.anniversary-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.anniversary-info {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.days-count {
|
||||||
|
font-size: 20px;
|
||||||
|
color: var(--color-primary);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 桌面端样式 */
|
||||||
|
@media screen and (min-width: 768px) {
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.anniversary-list {
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.anniversary-card {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端样式 */
|
||||||
|
@media screen and (max-width: 767px) {
|
||||||
|
.container {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-button {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plus-icon {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.anniversary-card {
|
||||||
|
padding: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.anniversary-title {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.days-count {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 针对特别窄的屏幕 */
|
||||||
|
@media screen and (max-width: 320px) {
|
||||||
|
.container {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-button {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plus-icon {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.anniversary-title {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.days-count {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 确保所有元素都使用 border-box */
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
61
src/HomePage.tsx
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import "./HomePage.css";
|
||||||
|
|
||||||
|
interface Anniversary {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
start_date: string;
|
||||||
|
days: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function HomePage() {
|
||||||
|
const [anniversaries, setAnniversaries] = useState<Anniversary[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const loadAnniversaries = async () => {
|
||||||
|
const result = await invoke<Anniversary[]>("get_anniversaries");
|
||||||
|
setAnniversaries(result);
|
||||||
|
};
|
||||||
|
loadAnniversaries();
|
||||||
|
}, []);
|
||||||
|
// 获取导航函数
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
// 点击卡片时跳转到详情页
|
||||||
|
const handleCardClick = (id: number) => {
|
||||||
|
navigate(`/detail/${id}`); // 例如:/detail/1
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container">
|
||||||
|
<div className="header-bar">
|
||||||
|
<h1>Count Down</h1>
|
||||||
|
<button className="add-button" onClick={() => navigate("/add")}>
|
||||||
|
<span className="plus-icon">+</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="anniversary-list">
|
||||||
|
{anniversaries.map((item) => (
|
||||||
|
<div
|
||||||
|
key={item.id}
|
||||||
|
className="anniversary-card"
|
||||||
|
onClick={() => {
|
||||||
|
handleCardClick(item.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="anniversary-title">{item.title}</div>
|
||||||
|
<div className="anniversary-info">
|
||||||
|
<span className="days-count">{item.days}</span>
|
||||||
|
<span className="start-date">{item.start_date}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HomePage;
|
9
src/main.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import React from "react";
|
||||||
|
import ReactDOM from "react-dom/client";
|
||||||
|
import App from "./App";
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>,
|
||||||
|
);
|
153
src/themes.css
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
/* 主题系统 - CSS变量定义 */
|
||||||
|
|
||||||
|
/* 默认主题(蓝色) */
|
||||||
|
:root[data-theme="blue"] {
|
||||||
|
--bg-primary: #f5f5f5;
|
||||||
|
--bg-secondary: #f8f9fa;
|
||||||
|
--bg-card: #ffffff;
|
||||||
|
--color-primary: #4a90e2;
|
||||||
|
--color-primary-hover: #357abd;
|
||||||
|
--color-text: #2c3e50;
|
||||||
|
--color-text-secondary: #666;
|
||||||
|
--color-text-muted: #6c757d;
|
||||||
|
--color-border: #e9ecef;
|
||||||
|
--color-danger: #ff4444;
|
||||||
|
--color-danger-hover: #e60000;
|
||||||
|
--shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
--shadow-md: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
--shadow-lg: 0 4px 16px rgba(0, 0, 0, 0.08);
|
||||||
|
--shadow-primary: 0 2px 8px rgba(74, 144, 226, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 绿色主题 */
|
||||||
|
:root[data-theme="green"] {
|
||||||
|
--bg-primary: #f0f8f0;
|
||||||
|
--bg-secondary: #f5fbf5;
|
||||||
|
--bg-card: #ffffff;
|
||||||
|
--color-primary: #28a745;
|
||||||
|
--color-primary-hover: #218838;
|
||||||
|
--color-text: #2d5016;
|
||||||
|
--color-text-secondary: #555;
|
||||||
|
--color-text-muted: #6c7b6c;
|
||||||
|
--color-border: #d4edda;
|
||||||
|
--color-danger: #dc3545;
|
||||||
|
--color-danger-hover: #c82333;
|
||||||
|
--shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
--shadow-md: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
--shadow-lg: 0 4px 16px rgba(0, 0, 0, 0.08);
|
||||||
|
--shadow-primary: 0 2px 8px rgba(40, 167, 69, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 紫色主题 */
|
||||||
|
:root[data-theme="purple"] {
|
||||||
|
--bg-primary: #f8f5ff;
|
||||||
|
--bg-secondary: #faf7ff;
|
||||||
|
--bg-card: #ffffff;
|
||||||
|
--color-primary: #8b5cf6;
|
||||||
|
--color-primary-hover: #7c3aed;
|
||||||
|
--color-text: #4c1d95;
|
||||||
|
--color-text-secondary: #666;
|
||||||
|
--color-text-muted: #7c6c95;
|
||||||
|
--color-border: #e9d5ff;
|
||||||
|
--color-danger: #ef4444;
|
||||||
|
--color-danger-hover: #dc2626;
|
||||||
|
--shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
--shadow-md: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
--shadow-lg: 0 4px 16px rgba(0, 0, 0, 0.08);
|
||||||
|
--shadow-primary: 0 2px 8px rgba(139, 92, 246, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 橙色主题 */
|
||||||
|
:root[data-theme="orange"] {
|
||||||
|
--bg-primary: #fff7ed;
|
||||||
|
--bg-secondary: #fffbf5;
|
||||||
|
--bg-card: #ffffff;
|
||||||
|
--color-primary: #f97316;
|
||||||
|
--color-primary-hover: #ea580c;
|
||||||
|
--color-text: #9a3412;
|
||||||
|
--color-text-secondary: #666;
|
||||||
|
--color-text-muted: #a16c3c;
|
||||||
|
--color-border: #fed7aa;
|
||||||
|
--color-danger: #dc2626;
|
||||||
|
--color-danger-hover: #b91c1c;
|
||||||
|
--shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
--shadow-md: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
--shadow-lg: 0 4px 16px rgba(0, 0, 0, 0.08);
|
||||||
|
--shadow-primary: 0 2px 8px rgba(249, 115, 22, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 粉色主题 */
|
||||||
|
:root[data-theme="pink"] {
|
||||||
|
--bg-primary: #fdf2f8;
|
||||||
|
--bg-secondary: #fef7f0;
|
||||||
|
--bg-card: #ffffff;
|
||||||
|
--color-primary: #ec4899;
|
||||||
|
--color-primary-hover: #db2777;
|
||||||
|
--color-text: #831843;
|
||||||
|
--color-text-secondary: #666;
|
||||||
|
--color-text-muted: #a15a7a;
|
||||||
|
--color-border: #f9a8d4;
|
||||||
|
--color-danger: #dc2626;
|
||||||
|
--color-danger-hover: #b91c1c;
|
||||||
|
--shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
--shadow-md: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
--shadow-lg: 0 4px 16px rgba(0, 0, 0, 0.08);
|
||||||
|
--shadow-primary: 0 2px 8px rgba(236, 72, 153, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 青色主题 */
|
||||||
|
:root[data-theme="cyan"] {
|
||||||
|
--bg-primary: #ecfdf5;
|
||||||
|
--bg-secondary: #f0fdfa;
|
||||||
|
--bg-card: #ffffff;
|
||||||
|
--color-primary: #06b6d4;
|
||||||
|
--color-primary-hover: #0891b2;
|
||||||
|
--color-text: #164e63;
|
||||||
|
--color-text-secondary: #666;
|
||||||
|
--color-text-muted: #4c7a7a;
|
||||||
|
--color-border: #a7f3d0;
|
||||||
|
--color-danger: #dc2626;
|
||||||
|
--color-danger-hover: #b91c1c;
|
||||||
|
--shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
--shadow-md: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
--shadow-lg: 0 4px 16px rgba(0, 0, 0, 0.08);
|
||||||
|
--shadow-primary: 0 2px 8px rgba(6, 182, 212, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 暗色主题 */
|
||||||
|
:root[data-theme="dark"] {
|
||||||
|
--bg-primary: #1f2937;
|
||||||
|
--bg-secondary: #111827;
|
||||||
|
--bg-card: #374151;
|
||||||
|
--color-primary: #60a5fa;
|
||||||
|
--color-primary-hover: #3b82f6;
|
||||||
|
--color-text: #f9fafb;
|
||||||
|
--color-text-secondary: #d1d5db;
|
||||||
|
--color-text-muted: #9ca3af;
|
||||||
|
--color-border: #4b5563;
|
||||||
|
--color-danger: #ef4444;
|
||||||
|
--color-danger-hover: #dc2626;
|
||||||
|
--shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||||
|
--shadow-md: 0 4px 8px rgba(0, 0, 0, 0.4);
|
||||||
|
--shadow-lg: 0 4px 16px rgba(0, 0, 0, 0.5);
|
||||||
|
--shadow-primary: 0 2px 8px rgba(96, 165, 250, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 渐变主题 */
|
||||||
|
:root[data-theme="gradient"] {
|
||||||
|
--bg-primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
--bg-secondary: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||||
|
--bg-card: rgba(255, 255, 255, 0.95);
|
||||||
|
--color-primary: #667eea;
|
||||||
|
--color-primary-hover: #5a6fd8;
|
||||||
|
--color-text: #2d3748;
|
||||||
|
--color-text-secondary: #4a5568;
|
||||||
|
--color-text-muted: #718096;
|
||||||
|
--color-border: rgba(255, 255, 255, 0.3);
|
||||||
|
--color-danger: #e53e3e;
|
||||||
|
--color-danger-hover: #c53030;
|
||||||
|
--shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
--shadow-md: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
--shadow-lg: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||||
|
--shadow-primary: 0 2px 8px rgba(102, 126, 234, 0.3);
|
||||||
|
}
|
1
src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
25
tsconfig.json
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
|
}
|
10
tsconfig.node.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
32
vite.config.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
|
||||||
|
// @ts-expect-error process is a nodejs global
|
||||||
|
const host = process.env.TAURI_DEV_HOST;
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig(async () => ({
|
||||||
|
plugins: [react()],
|
||||||
|
|
||||||
|
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
|
||||||
|
//
|
||||||
|
// 1. prevent vite from obscuring rust errors
|
||||||
|
clearScreen: false,
|
||||||
|
// 2. tauri expects a fixed port, fail if that port is not available
|
||||||
|
server: {
|
||||||
|
port: 1420,
|
||||||
|
strictPort: true,
|
||||||
|
host: host || false,
|
||||||
|
hmr: host
|
||||||
|
? {
|
||||||
|
protocol: "ws",
|
||||||
|
host,
|
||||||
|
port: 1421,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
watch: {
|
||||||
|
// 3. tell vite to ignore watching `src-tauri`
|
||||||
|
ignored: ["**/src-tauri/**"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|