mirror of
https://github.com/infinilabs/coco-app.git
synced 2025-12-16 11:37:47 +01:00
feat: add window webview & add Footer & change style (#2)
* chore: add Footer * feat: add winfow webview
This commit is contained in:
18
README.md
18
README.md
@@ -28,8 +28,7 @@ To set up the Coco App for development:
|
||||
```bash
|
||||
cd coco
|
||||
pnpm install
|
||||
pnpm tauri android init
|
||||
pnpm tauri ios init
|
||||
pnpm tauri dev
|
||||
```
|
||||
|
||||
#### Desktop Development:
|
||||
@@ -39,18 +38,3 @@ To start desktop development, run:
|
||||
```
|
||||
pnpm tauri dev
|
||||
```
|
||||
|
||||
#### Android Development:
|
||||
|
||||
For Android development, run:
|
||||
|
||||
```
|
||||
pnpm tauri android dev
|
||||
```
|
||||
|
||||
#### iOS Development:
|
||||
|
||||
For iOS development, run:
|
||||
```
|
||||
pnpm tauri ios dev
|
||||
```
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": ">=2.0.0",
|
||||
"@types/lodash": "^4.17.12",
|
||||
"@types/node": "^22.8.4",
|
||||
"@types/react": "^18.2.15",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@types/react-i18next": "^8.1.0",
|
||||
|
||||
26
pnpm-lock.yaml
generated
26
pnpm-lock.yaml
generated
@@ -57,6 +57,9 @@ importers:
|
||||
'@types/lodash':
|
||||
specifier: ^4.17.12
|
||||
version: 4.17.12
|
||||
'@types/node':
|
||||
specifier: ^22.8.4
|
||||
version: 22.8.4
|
||||
'@types/react':
|
||||
specifier: ^18.2.15
|
||||
version: 18.3.11
|
||||
@@ -68,7 +71,7 @@ importers:
|
||||
version: 8.1.0(i18next@23.16.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@vitejs/plugin-react':
|
||||
specifier: ^4.2.1
|
||||
version: 4.3.2(vite@5.4.8)
|
||||
version: 4.3.2(vite@5.4.8(@types/node@22.8.4))
|
||||
autoprefixer:
|
||||
specifier: ^10.4.20
|
||||
version: 10.4.20(postcss@8.4.47)
|
||||
@@ -83,7 +86,7 @@ importers:
|
||||
version: 5.6.3
|
||||
vite:
|
||||
specifier: ^5.3.1
|
||||
version: 5.4.8
|
||||
version: 5.4.8(@types/node@22.8.4)
|
||||
|
||||
packages:
|
||||
|
||||
@@ -622,6 +625,9 @@ packages:
|
||||
'@types/lodash@4.17.12':
|
||||
resolution: {integrity: sha512-sviUmCE8AYdaF/KIHLDJBQgeYzPBI0vf/17NaYehBJfYD1j6/L95Slh07NlyK2iNyBNaEkb3En2jRt+a8y3xZQ==}
|
||||
|
||||
'@types/node@22.8.4':
|
||||
resolution: {integrity: sha512-SpNNxkftTJOPk0oN+y2bIqurEXHTA2AOZ3EJDDKeJ5VzkvvORSvmQXGQarcOzWV1ac7DCaPBEdMDxBsM+d8jWw==}
|
||||
|
||||
'@types/prop-types@15.7.13':
|
||||
resolution: {integrity: sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==}
|
||||
|
||||
@@ -1200,6 +1206,9 @@ packages:
|
||||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
|
||||
undici-types@6.19.8:
|
||||
resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==}
|
||||
|
||||
update-browserslist-db@1.1.1:
|
||||
resolution: {integrity: sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==}
|
||||
hasBin: true
|
||||
@@ -1744,6 +1753,10 @@ snapshots:
|
||||
|
||||
'@types/lodash@4.17.12': {}
|
||||
|
||||
'@types/node@22.8.4':
|
||||
dependencies:
|
||||
undici-types: 6.19.8
|
||||
|
||||
'@types/prop-types@15.7.13': {}
|
||||
|
||||
'@types/react-dom@18.3.1':
|
||||
@@ -1764,14 +1777,14 @@ snapshots:
|
||||
'@types/prop-types': 15.7.13
|
||||
csstype: 3.1.3
|
||||
|
||||
'@vitejs/plugin-react@4.3.2(vite@5.4.8)':
|
||||
'@vitejs/plugin-react@4.3.2(vite@5.4.8(@types/node@22.8.4))':
|
||||
dependencies:
|
||||
'@babel/core': 7.25.8
|
||||
'@babel/plugin-transform-react-jsx-self': 7.25.7(@babel/core@7.25.8)
|
||||
'@babel/plugin-transform-react-jsx-source': 7.25.7(@babel/core@7.25.8)
|
||||
'@types/babel__core': 7.20.5
|
||||
react-refresh: 0.14.2
|
||||
vite: 5.4.8
|
||||
vite: 5.4.8(@types/node@22.8.4)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -2316,6 +2329,8 @@ snapshots:
|
||||
|
||||
typescript@5.6.3: {}
|
||||
|
||||
undici-types@6.19.8: {}
|
||||
|
||||
update-browserslist-db@1.1.1(browserslist@4.24.0):
|
||||
dependencies:
|
||||
browserslist: 4.24.0
|
||||
@@ -2324,12 +2339,13 @@ snapshots:
|
||||
|
||||
util-deprecate@1.0.2: {}
|
||||
|
||||
vite@5.4.8:
|
||||
vite@5.4.8(@types/node@22.8.4):
|
||||
dependencies:
|
||||
esbuild: 0.21.5
|
||||
postcss: 8.4.47
|
||||
rollup: 4.24.0
|
||||
optionalDependencies:
|
||||
'@types/node': 22.8.4
|
||||
fsevents: 2.3.3
|
||||
|
||||
void-elements@3.1.0: {}
|
||||
|
||||
37
src-tauri/Cargo.lock
generated
37
src-tauri/Cargo.lock
generated
@@ -347,6 +347,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-nspanel",
|
||||
"tauri-plugin-http",
|
||||
"tauri-plugin-shell",
|
||||
]
|
||||
@@ -1876,6 +1877,17 @@ dependencies = [
|
||||
"malloc_buf",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc-foundation"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9"
|
||||
dependencies = [
|
||||
"block",
|
||||
"objc",
|
||||
"objc_id",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc-sys"
|
||||
version = "0.3.5"
|
||||
@@ -2094,6 +2106,15 @@ dependencies = [
|
||||
"objc2-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc_id"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b"
|
||||
dependencies = [
|
||||
"objc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "object"
|
||||
version = "0.36.5"
|
||||
@@ -3429,6 +3450,22 @@ dependencies = [
|
||||
"tauri-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-nspanel"
|
||||
version = "2.0.0-beta"
|
||||
source = "git+https://github.com/ahkohd/tauri-nspanel?rev=005240c#005240ce43fe02108ce6d0aec6e2e436c46f46b7"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"block",
|
||||
"cocoa",
|
||||
"core-foundation 0.10.0",
|
||||
"core-graphics",
|
||||
"objc",
|
||||
"objc-foundation",
|
||||
"objc_id",
|
||||
"tauri",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin"
|
||||
version = "2.0.1"
|
||||
|
||||
@@ -24,6 +24,8 @@ serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
tauri-plugin-http = "2"
|
||||
|
||||
tauri-nspanel = { git = "https://github.com/ahkohd/tauri-nspanel", rev = "005240c" }
|
||||
|
||||
[profile.dev]
|
||||
incremental = true # Compile your binary in smaller steps.
|
||||
|
||||
|
||||
@@ -4,6 +4,17 @@
|
||||
"description": "Capability for the main window",
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:event:allow-listen",
|
||||
"core:window:default",
|
||||
"core:window:allow-start-dragging",
|
||||
"core:webview:allow-create-webview",
|
||||
"core:window:allow-show",
|
||||
"core:webview:allow-create-webview-window",
|
||||
"core:webview:allow-webview-close",
|
||||
"core:window:allow-close",
|
||||
"core:window:allow-hide",
|
||||
"core:webview:allow-set-webview-size",
|
||||
"core:window:allow-set-size",
|
||||
"core:default",
|
||||
"shell:allow-open",
|
||||
"http:default",
|
||||
|
||||
@@ -1,6 +1,80 @@
|
||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
#![cfg_attr(
|
||||
not(debug_assertions),
|
||||
target_os = "windows",
|
||||
windows_subsystem = "windows"
|
||||
)]
|
||||
|
||||
use tauri::{AppHandle, Manager, WebviewWindow};
|
||||
use tauri_nspanel::{panel_delegate, ManagerExt, WebviewWindowExt};
|
||||
|
||||
fn main() {
|
||||
coco_lib::run()
|
||||
coco_lib::run();
|
||||
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_nspanel::init())
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
show_panel,
|
||||
hide_panel,
|
||||
close_panel
|
||||
])
|
||||
.setup(|app| {
|
||||
init(app.app_handle());
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
|
||||
fn init(app_handle: &AppHandle) {
|
||||
let window: WebviewWindow = app_handle.get_webview_window("main").unwrap();
|
||||
|
||||
let panel = window.to_panel().unwrap();
|
||||
|
||||
let delegate = panel_delegate!(MyPanelDelegate {
|
||||
window_did_become_key,
|
||||
window_did_resign_key
|
||||
});
|
||||
|
||||
let handle = app_handle.to_owned();
|
||||
|
||||
delegate.set_listener(Box::new(move |delegate_name: String| {
|
||||
match delegate_name.as_str() {
|
||||
"window_did_become_key" => {
|
||||
let app_name = handle.package_info().name.to_owned();
|
||||
|
||||
println!("[info]: {:?} panel becomes key window!", app_name);
|
||||
}
|
||||
"window_did_resign_key" => {
|
||||
println!("[info]: panel resigned from key window!");
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}));
|
||||
|
||||
panel.set_delegate(delegate);
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn show_panel(handle: AppHandle) {
|
||||
let panel = handle.get_webview_panel("main").unwrap();
|
||||
|
||||
panel.show();
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn hide_panel(handle: AppHandle) {
|
||||
let panel = handle.get_webview_panel("main").unwrap();
|
||||
|
||||
panel.order_out(None);
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn close_panel(handle: AppHandle) {
|
||||
let panel = handle.get_webview_panel("main").unwrap();
|
||||
|
||||
panel.released_when_closed(true);
|
||||
|
||||
panel.close();
|
||||
}
|
||||
|
||||
@@ -14,10 +14,14 @@
|
||||
{
|
||||
"title": "Coco AI",
|
||||
"width": 800,
|
||||
"height": 600,
|
||||
"height": 150,
|
||||
"maxHeight": 600,
|
||||
"transparent": true,
|
||||
"resizable": false,
|
||||
"fullscreen": false,
|
||||
"decorations": false
|
||||
"decorations": false,
|
||||
"label": "main",
|
||||
"url": "/"
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
@@ -27,6 +31,11 @@
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"windows": {
|
||||
"certificateThumbprint": null,
|
||||
"digestAlgorithm": "sha256",
|
||||
"timestampUrl": ""
|
||||
},
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
@@ -38,4 +47,4 @@
|
||||
"plugins": {
|
||||
"window": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,9 +5,9 @@ import { Link } from "react-router-dom";
|
||||
const Footer = () => {
|
||||
return (
|
||||
<div className="fixed bottom-0 left-0 right-0 bg-white dark:bg-gray-800 border-t dark:border-gray-700">
|
||||
<div className="max-w-6xl mx-auto px-4 h-14 flex items-center justify-between">
|
||||
<div className="max-w-6xl mx-auto px-4 h-8 flex items-center justify-between">
|
||||
<Menu as="div" className="relative">
|
||||
<MenuButton className="flex items-center space-x-2 px-3 py-1.5 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
|
||||
<MenuButton className="h-7 flex items-center space-x-2 px-3 py-1.5 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
|
||||
<Command className="w-5 h-5 text-gray-600 dark:text-gray-400" />
|
||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Coco
|
||||
|
||||
@@ -10,7 +10,7 @@ export const DocumentDetail: React.FC<DocumentDetailProps> = ({
|
||||
}) => {
|
||||
if (!documentId) {
|
||||
return (
|
||||
<div className="h-full flex items-center justify-center text-gray-400">
|
||||
<div className="h-full flex items-center justify-center text-gray-400 dark:text-gray-500">
|
||||
请选择一个文档查看详情
|
||||
</div>
|
||||
);
|
||||
@@ -19,22 +19,26 @@ export const DocumentDetail: React.FC<DocumentDetailProps> = ({
|
||||
return (
|
||||
<div className="p-8 space-y-8">
|
||||
<div className="space-y-6">
|
||||
<h2 className="text-2xl font-semibold text-gray-900">
|
||||
<h2 className="text-2xl font-semibold text-gray-900 dark:text-gray-100">
|
||||
产品需求规划文档
|
||||
</h2>
|
||||
|
||||
<div className="flex items-center gap-6 text-sm text-gray-500">
|
||||
<div className="flex items-center gap-2">
|
||||
<Calendar className="w-4 h-4" />
|
||||
<span>2024-02-20</span>
|
||||
<div>
|
||||
<div className="flex items-center gap-6 text-sm text-gray-500 dark:text-gray-400">
|
||||
<div className="flex items-center gap-2">
|
||||
<Calendar className="w-4 h-4" />
|
||||
<span>2024-02-20</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<User className="w-4 h-4" />
|
||||
<span>张小明</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<User className="w-4 h-4" />
|
||||
<span>张小明</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Clock className="w-4 h-4" />
|
||||
<span>最近更新于 2小时前</span>
|
||||
<div className="flex items-center gap-6 text-sm text-gray-500 dark:text-gray-400">
|
||||
<div className="flex items-center gap-2">
|
||||
<Clock className="w-4 h-4" />
|
||||
<span>最近更新于 2小时前</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -45,15 +49,19 @@ export const DocumentDetail: React.FC<DocumentDetailProps> = ({
|
||||
className="w-full aspect-video object-cover rounded-xl shadow-md"
|
||||
/>
|
||||
|
||||
<div className="prose prose-gray max-w-none">
|
||||
<h3 className="text-lg font-medium text-gray-900">文档概述</h3>
|
||||
<p className="text-gray-600 leading-relaxed">
|
||||
<div className="prose prose-gray dark:prose-invert max-w-none">
|
||||
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100">
|
||||
文档概述
|
||||
</h3>
|
||||
<p className="text-gray-600 dark:text-gray-300 leading-relaxed">
|
||||
本文档详细说明了2024年Q1的产品规划方向和具体功能需求。包含了用户研究结果、
|
||||
竞品分析、功能优先级排序等重要内容。产品团队可以基于此文档进行后续的设计和开发工作。
|
||||
</p>
|
||||
|
||||
<h3 className="text-lg font-medium text-gray-900 mt-6">主要内容</h3>
|
||||
<ul className="list-disc pl-4 text-gray-600 space-y-2">
|
||||
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mt-6">
|
||||
主要内容
|
||||
</h3>
|
||||
<ul className="list-disc pl-4 text-gray-600 dark:text-gray-300 space-y-2">
|
||||
<li>用户痛点分析与解决方案</li>
|
||||
<li>核心功能详细说明</li>
|
||||
<li>交互流程设计</li>
|
||||
@@ -61,7 +69,7 @@ export const DocumentDetail: React.FC<DocumentDetailProps> = ({
|
||||
<li>项目时间节点规划</li>
|
||||
</ul>
|
||||
|
||||
<p className="text-gray-600 leading-relaxed mt-6">
|
||||
<p className="text-gray-600 dark:text-gray-300 leading-relaxed mt-6">
|
||||
通过实施本文档中规划的功能,我们期望能够提升用户体验,增强产品竞争力,
|
||||
实现Q1的业务增长目标。
|
||||
</p>
|
||||
|
||||
@@ -1,63 +1,61 @@
|
||||
import React from "react";
|
||||
import { FileText, Image, FileCode, Users, User, Globe } from "lucide-react";
|
||||
import React from 'react';
|
||||
import { FileText, Image, FileCode, Users, User, Globe } from 'lucide-react';
|
||||
|
||||
interface Document {
|
||||
id: string;
|
||||
title: string;
|
||||
type: "text" | "image" | "code";
|
||||
owner: "personal" | "team" | "public";
|
||||
type: 'text' | 'image' | 'code';
|
||||
owner: 'personal' | 'team' | 'public';
|
||||
description: string;
|
||||
date: string;
|
||||
}
|
||||
|
||||
const documents: Document[] = [
|
||||
{
|
||||
id: "1",
|
||||
title: "产品需求规划文档.doc",
|
||||
type: "text",
|
||||
owner: "team",
|
||||
description:
|
||||
"2024年Q1产品规划及功能需求文档,包含详细的功能描述和交互设计说明。",
|
||||
date: "2024-02-20",
|
||||
id: '1',
|
||||
title: '产品需求规划文档.doc',
|
||||
type: 'text',
|
||||
owner: 'team',
|
||||
description: '2024年Q1产品规划及功能需求文档,包含详细的功能描述和交互设计说明。',
|
||||
date: '2024-02-20'
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
title: "UI设计规范.fig",
|
||||
type: "image",
|
||||
owner: "public",
|
||||
description: "最新的设计系统规范文档,包含组件库使用说明和设计标准。",
|
||||
date: "2024-02-19",
|
||||
id: '2',
|
||||
title: 'UI设计规范.fig',
|
||||
type: 'image',
|
||||
owner: 'public',
|
||||
description: '最新的设计系统规范文档,包含组件库使用说明和设计标准。',
|
||||
date: '2024-02-19'
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
title: "API接口文档.ts",
|
||||
type: "code",
|
||||
owner: "personal",
|
||||
description:
|
||||
"TypeScript版本的API接口定义文档,包含所有接口的请求和响应类型。",
|
||||
date: "2024-02-18",
|
||||
id: '3',
|
||||
title: 'API接口文档.ts',
|
||||
type: 'code',
|
||||
owner: 'personal',
|
||||
description: 'TypeScript版本的API接口定义文档,包含所有接口的请求和响应类型。',
|
||||
date: '2024-02-18'
|
||||
},
|
||||
];
|
||||
|
||||
const getIcon = (type: Document["type"]) => {
|
||||
const getIcon = (type: Document['type']) => {
|
||||
switch (type) {
|
||||
case "image":
|
||||
return <Image className="w-5 h-5 text-blue-500" />;
|
||||
case "code":
|
||||
return <FileCode className="w-5 h-5 text-green-500" />;
|
||||
case 'image':
|
||||
return <Image className="w-5 h-5 text-blue-500 dark:text-blue-400" />;
|
||||
case 'code':
|
||||
return <FileCode className="w-5 h-5 text-green-500 dark:text-green-400" />;
|
||||
default:
|
||||
return <FileText className="w-5 h-5 text-purple-500" />;
|
||||
return <FileText className="w-5 h-5 text-purple-500 dark:text-purple-400" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getOwnerIcon = (owner: Document["owner"]) => {
|
||||
const getOwnerIcon = (owner: Document['owner']) => {
|
||||
switch (owner) {
|
||||
case "team":
|
||||
return <Users className="w-4 h-4 text-blue-500" />;
|
||||
case "public":
|
||||
return <Globe className="w-4 h-4 text-green-500" />;
|
||||
case 'team':
|
||||
return <Users className="w-4 h-4 text-blue-500 dark:text-blue-400" />;
|
||||
case 'public':
|
||||
return <Globe className="w-4 h-4 text-green-500 dark:text-green-400" />;
|
||||
default:
|
||||
return <User className="w-4 h-4 text-gray-500" />;
|
||||
return <User className="w-4 h-4 text-gray-500 dark:text-gray-400" />;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -66,35 +64,28 @@ interface DocumentListProps {
|
||||
selectedId?: string;
|
||||
}
|
||||
|
||||
export const DocumentList: React.FC<DocumentListProps> = ({
|
||||
onSelectDocument,
|
||||
selectedId,
|
||||
}) => {
|
||||
export const DocumentList: React.FC<DocumentListProps> = ({ onSelectDocument, selectedId }) => {
|
||||
return (
|
||||
<div className="space-y-1 py-2">
|
||||
{documents.map((doc) => (
|
||||
<button
|
||||
key={doc.id}
|
||||
onClick={() => onSelectDocument(doc.id)}
|
||||
className={`w-full flex items-start px-4 py-3 rounded-lg hover:bg-gray-50 transition-colors ${
|
||||
selectedId === doc.id ? "bg-blue-50" : ""
|
||||
className={`w-full flex items-start px-4 py-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors ${
|
||||
selectedId === doc.id ? 'bg-blue-50 dark:bg-blue-900/50' : ''
|
||||
}`}
|
||||
>
|
||||
<span className="mr-3 mt-0.5">{getIcon(doc.type)}</span>
|
||||
<div className="flex-1 text-left">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-gray-900">
|
||||
{doc.title}
|
||||
</span>
|
||||
<span className="text-sm font-medium text-gray-900 dark:text-gray-100">{doc.title}</span>
|
||||
<span className="mt-0.5">{getOwnerIcon(doc.owner)}</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 mt-1 line-clamp-2">
|
||||
{doc.description}
|
||||
</p>
|
||||
<span className="text-xs text-gray-400 mt-1 block">{doc.date}</span>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1 line-clamp-2">{doc.description}</p>
|
||||
<span className="text-xs text-gray-400 dark:text-gray-500 mt-1 block">{doc.date}</span>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
130
src/components/SearchChat/Footer.tsx
Normal file
130
src/components/SearchChat/Footer.tsx
Normal file
@@ -0,0 +1,130 @@
|
||||
import React from "react";
|
||||
import { Settings, LogOut, Command, User, Home, ChevronUp } from "lucide-react";
|
||||
import { Menu, MenuButton, MenuItems, MenuItem } from "@headlessui/react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { WebviewWindow } from "@tauri-apps/api/webviewWindow";
|
||||
|
||||
const shortcuts = [
|
||||
{ label: "翻页/换行", keys: "↓" },
|
||||
{ label: "快速换行", keys: "Tab" },
|
||||
{ label: "Talk to AI", keys: "⌘ + /" },
|
||||
{ label: "Open", keys: "⌘ + O" },
|
||||
];
|
||||
|
||||
export const Footer: React.FC = () => {
|
||||
async function openWebviewWindowSettings() {
|
||||
const webview = new WebviewWindow("settings", {
|
||||
dragDropEnabled: true,
|
||||
center: true,
|
||||
width: 900,
|
||||
height: 700,
|
||||
alwaysOnTop: true,
|
||||
skipTaskbar: true,
|
||||
decorations: true,
|
||||
closable: true,
|
||||
url: "/settings",
|
||||
});
|
||||
webview.once("tauri://created", function () {
|
||||
console.log("webview created");
|
||||
});
|
||||
webview.once("tauri://error", function (e) {
|
||||
console.log("error creating webview", e);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-0 left-0 right-0 bg-white dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 px-4 h-8 flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<Menu as="div" className="relative">
|
||||
<MenuButton className="h-7 flex items-center space-x-2 px-3 py-1.5 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
|
||||
<Command className="w-5 h-5 text-gray-600 dark:text-gray-400" />
|
||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Coco
|
||||
</span>
|
||||
<ChevronUp className="w-4 h-4 text-gray-500 dark:text-gray-400" />
|
||||
</MenuButton>
|
||||
|
||||
<MenuItems className="absolute bottom-full mb-2 left-0 w-64 rounded-lg bg-white dark:bg-gray-800 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
|
||||
<div className="p-1">
|
||||
<MenuItem>
|
||||
{({ active }) => (
|
||||
<button
|
||||
className={`${
|
||||
active
|
||||
? "bg-gray-100 dark:bg-gray-700"
|
||||
: "text-gray-900 dark:text-gray-100"
|
||||
} group flex w-full items-center rounded-md px-3 py-2 text-sm`}
|
||||
>
|
||||
<Home className="w-4 h-4 mr-2" />
|
||||
<Link to={`/`}>Home</Link>
|
||||
</button>
|
||||
)}
|
||||
</MenuItem>
|
||||
{/* <MenuItem>
|
||||
{({ active }) => (
|
||||
<button
|
||||
className={`${
|
||||
active
|
||||
? "bg-gray-100 dark:bg-gray-700"
|
||||
: "text-gray-900 dark:text-gray-100"
|
||||
} group flex w-full items-center rounded-md px-3 py-2 text-sm`}
|
||||
>
|
||||
<User className="w-4 h-4 mr-2" />
|
||||
Profile
|
||||
</button>
|
||||
)}
|
||||
</MenuItem> */}
|
||||
<MenuItem>
|
||||
{({ active }) => (
|
||||
<button
|
||||
className={`${
|
||||
active
|
||||
? "bg-gray-100 dark:bg-gray-700"
|
||||
: "text-gray-900 dark:text-gray-100"
|
||||
} group flex w-full items-center rounded-md px-3 py-2 text-sm`}
|
||||
onClick={openWebviewWindowSettings}
|
||||
>
|
||||
<Settings className="w-4 h-4 mr-2" />
|
||||
Settings
|
||||
</button>
|
||||
)}
|
||||
</MenuItem>
|
||||
{/* <div className="h-px bg-gray-200 dark:bg-gray-700 my-1" />
|
||||
<MenuItem>
|
||||
{({ active }) => (
|
||||
<button
|
||||
className={`${
|
||||
active
|
||||
? "bg-gray-100 dark:bg-gray-700"
|
||||
: "text-gray-900 dark:text-gray-100"
|
||||
} group flex w-full items-center rounded-md px-3 py-2 text-sm`}
|
||||
>
|
||||
<LogOut className="w-4 h-4 mr-2" />
|
||||
Sign Out
|
||||
</button>
|
||||
)}
|
||||
</MenuItem> */}
|
||||
</div>
|
||||
</MenuItems>
|
||||
</Menu>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
{shortcuts.map((shortcut, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center text-gray-500 dark:text-gray-400 text-sm"
|
||||
>
|
||||
{index > 0 && (
|
||||
<div className="w-px h-4 bg-gray-200 dark:bg-gray-700 mr-4" />
|
||||
)}
|
||||
<span className="mr-1.5">{shortcut.label}</span>
|
||||
<kbd className="px-1.5 py-0.5 rounded bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 text-xs font-medium">
|
||||
{shortcut.keys}
|
||||
</kbd>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, Fragment } from "react";
|
||||
import React, { useState } from "react";
|
||||
import { Menu, MenuButton, MenuItems, MenuItem } from "@headlessui/react";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
|
||||
@@ -21,24 +21,24 @@ const FilterDropdown: React.FC<FilterDropdownProps> = ({
|
||||
onChange,
|
||||
}) => {
|
||||
return (
|
||||
<Menu as="div" className="relative text-xs">
|
||||
<MenuButton className="inline-flex items-center px-3 py-1.5 text-sm bg-white text-gray-700 hover:bg-gray-50 rounded-lg border border-gray-200 font-medium">
|
||||
<Menu as="div" className="relative">
|
||||
<MenuButton className="inline-flex items-center px-2.5 py-1 text-xs bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 rounded-lg border border-gray-200 dark:border-gray-700 font-medium">
|
||||
{label}
|
||||
<ChevronDown className="w-4 h-4 ml-1.5 text-gray-500" />
|
||||
<ChevronDown className="w-3.5 h-3.5 ml-1 text-gray-500 dark:text-gray-400" />
|
||||
</MenuButton>
|
||||
|
||||
<MenuItems className="absolute right-0 mt-1 w-48 bg-white rounded-lg shadow-lg border border-gray-200 py-1 z-10 focus:outline-none">
|
||||
<MenuItems className="absolute right-0 mt-1 w-44 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 py-1 z-10 focus:outline-none">
|
||||
{options.map((option) => (
|
||||
<MenuItem key={option.id}>
|
||||
{({ active }) => (
|
||||
<button
|
||||
onClick={() => onChange(option.id)}
|
||||
className={`w-full text-left px-4 py-2 text-sm ${
|
||||
active ? "bg-gray-50" : ""
|
||||
className={`w-full text-left px-3 py-1.5 text-xs ${
|
||||
active ? "bg-gray-50 dark:bg-gray-700" : ""
|
||||
} ${
|
||||
value === option.id
|
||||
? "text-blue-600 bg-blue-50"
|
||||
: "text-gray-700"
|
||||
? "text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900/50"
|
||||
: "text-gray-700 dark:text-gray-300"
|
||||
}`}
|
||||
>
|
||||
{option.label}
|
||||
@@ -77,11 +77,15 @@ export const SearchHeader: React.FC = () => {
|
||||
const [creatorFilter, setCreatorFilter] = useState("all");
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between py-4 border-b border-gray-200 text-xs">
|
||||
<div className="text-gray-600">
|
||||
搜索到 <span className="font-medium text-gray-900">200</span> 条数据
|
||||
<div className="flex items-center justify-between py-3 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="text-xs text-gray-600 dark:text-gray-400">
|
||||
搜索到{" "}
|
||||
<span className="font-medium text-gray-900 dark:text-gray-100">
|
||||
200
|
||||
</span>{" "}
|
||||
条数据
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<div className="flex gap-2">
|
||||
<FilterDropdown
|
||||
label="类型"
|
||||
options={typeOptions}
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
import React, { useState } from "react";
|
||||
|
||||
import { SearchHeader } from "./SearchHeader";
|
||||
import { DocumentList } from "./DocumentList";
|
||||
import { DocumentDetail } from "./DocumentDetail";
|
||||
|
||||
export const SearchResults: React.FC = () => {
|
||||
const [selectedDocumentId, setSelectedDocumentId] = useState("1"); // Default to first document
|
||||
const [selectedDocumentId, setSelectedDocumentId] = useState("1");
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-xl shadow-sm border border-gray-200 mt-4 overflow-hidden">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 mt-4 overflow-hidden">
|
||||
<div className="flex h-[calc(100vh-220px)]">
|
||||
{/* Left Panel */}
|
||||
<div className="w-[420px] border-r border-gray-200 flex flex-col overflow-hidden">
|
||||
<div className="w-[420px] border-r border-gray-200 dark:border-gray-700 flex flex-col overflow-hidden">
|
||||
<div className="px-4 flex-shrink-0">
|
||||
<SearchHeader />
|
||||
</div>
|
||||
<div className="overflow-y-auto flex-1">
|
||||
<div className="overflow-y-auto flex-1 custom-scrollbar">
|
||||
<DocumentList
|
||||
onSelectDocument={setSelectedDocumentId}
|
||||
selectedId={selectedDocumentId}
|
||||
@@ -24,7 +23,7 @@ export const SearchResults: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{/* Right Panel */}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<div className="flex-1 overflow-y-auto custom-scrollbar">
|
||||
<DocumentDetail documentId={selectedDocumentId} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import React, { useState } from "react";
|
||||
import { Mic, Filter, Upload, MessageSquare } from "lucide-react";
|
||||
import { Mic, Filter, Upload } from "lucide-react";
|
||||
import { Switch } from "@headlessui/react";
|
||||
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
|
||||
|
||||
import { SearchResults } from "./SearchResults";
|
||||
import { Footer } from "./Footer";
|
||||
import { LogicalSize } from "@tauri-apps/api/dpi";
|
||||
|
||||
interface Tag {
|
||||
id: string;
|
||||
@@ -14,34 +17,39 @@ function Search() {
|
||||
const [input, setInput] = useState("");
|
||||
const [isChatMode, setIsChatMode] = useState(false);
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
const handleKeyDown = async (e: React.KeyboardEvent) => {
|
||||
if (e.key === "Enter" && input.trim()) {
|
||||
setTags([...tags, { id: Date.now().toString(), text: input.trim() }]);
|
||||
setInput("");
|
||||
await getCurrentWebviewWindow().setSize(new LogicalSize(800, 600));
|
||||
}
|
||||
};
|
||||
|
||||
const removeTag = (tagId: string) => {
|
||||
setTags(tags.filter((tag) => tag.id !== tagId));
|
||||
const removeTag = async (tagId: string) => {
|
||||
const newTag = tags.filter((tag) => tag.id !== tagId)
|
||||
setTags(newTag);
|
||||
if (newTag.length === 0) {
|
||||
await getCurrentWebviewWindow().setSize(new LogicalSize(800, 150));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex items-start justify-center pt-10 px-4">
|
||||
<div className="w-full max-w-3xl space-y-4">
|
||||
<div className="border b-t-none border-gray-200 rounded-xl">
|
||||
<div className="max-h-screen flex items-start justify-center pb-8 rounded-xl">
|
||||
<div className="w-full space-y-4">
|
||||
<div className="border b-t-none border-gray-200 dark:border-gray-700 rounded-xl">
|
||||
{/* Search Bar */}
|
||||
<div className="relative">
|
||||
<div className="flex items-center bg-white rounded-xl shadow-sm border border-gray-200 p-2 focus-within:ring-2 focus-within:ring-blue-100 focus-within:border-blue-400 transition-all">
|
||||
<div className="flex items-center bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 p-2 focus-within:ring-2 focus-within:ring-blue-100 dark:focus-within:ring-blue-900 focus-within:border-blue-400 dark:focus-within:border-blue-500 transition-all">
|
||||
<div className="flex flex-wrap gap-2 flex-1 min-h-12 items-center">
|
||||
{tags.map((tag) => (
|
||||
<span
|
||||
key={tag.id}
|
||||
className="inline-flex items-center bg-blue-50 text-blue-600 px-2.5 py-1 rounded-lg text-sm"
|
||||
className="inline-flex items-center bg-blue-50 dark:bg-blue-900/50 text-blue-600 dark:text-blue-400 px-2.5 py-1 rounded-lg text-sm"
|
||||
>
|
||||
{tag.text}
|
||||
<button
|
||||
onClick={() => removeTag(tag.id)}
|
||||
className="ml-1.5 text-blue-400 hover:text-blue-600"
|
||||
className="ml-1.5 text-blue-400 hover:text-blue-600 dark:text-blue-500 dark:hover:text-blue-400"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
@@ -49,46 +57,44 @@ function Search() {
|
||||
))}
|
||||
<input
|
||||
type="text"
|
||||
className="flex-1 outline-none min-w-[200px] text-gray-800 placeholder-gray-400"
|
||||
className="flex-1 outline-none min-w-[200px] text-gray-800 dark:text-gray-200 placeholder-gray-400 dark:placeholder-gray-500 bg-transparent"
|
||||
placeholder="有问题尽管问 Coco"
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
</div>
|
||||
<button className="p-2 hover:bg-gray-50 rounded-lg transition-colors">
|
||||
<Mic className="w-5 h-5 text-gray-400" />
|
||||
<button className="p-2 hover:bg-gray-50 dark:hover:bg-gray-700 rounded-lg transition-colors">
|
||||
<Mic className="w-5 h-5 text-gray-400 dark:text-gray-500" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Controls */}
|
||||
<div className="flex justify-between items-center p-2">
|
||||
<div className="flex gap-3">
|
||||
<button className="inline-flex items-center px-2 py-1 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">
|
||||
<MessageSquare className="w-4 h-4 mr-2" />问 Coco
|
||||
<div className="flex gap-3 text-xs">
|
||||
<button className="inline-flex items-center px-2 py-1 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors text-gray-700 dark:text-gray-300">
|
||||
<Filter className="w-4 h-4 mr-2" />问 Coco
|
||||
</button>
|
||||
<button className="inline-flex items-center px-2 py-1 bg-white border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors text-gray-700">
|
||||
<Filter className="w-4 h-4 mr-2" />
|
||||
筛选
|
||||
</button>
|
||||
<button className="inline-flex items-center px-2 py-1 bg-white border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors text-gray-700">
|
||||
<button className="inline-flex items-center px-2 py-1 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors text-gray-700 dark:text-gray-300">
|
||||
<Upload className="w-4 h-4 mr-2" />
|
||||
上传
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Switch */}
|
||||
<div className="flex items-center">
|
||||
<span className="mr-3 text-sm font-medium text-gray-700">
|
||||
Chat 模式
|
||||
<div className="flex items-center text-xs">
|
||||
<span className="mr-3 text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Chat
|
||||
</span>
|
||||
<Switch
|
||||
checked={isChatMode}
|
||||
onChange={setIsChatMode}
|
||||
className={`${
|
||||
isChatMode ? "bg-blue-600" : "bg-gray-200"
|
||||
} relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2`}
|
||||
isChatMode
|
||||
? "bg-blue-600 dark:bg-blue-500"
|
||||
: "bg-gray-200 dark:bg-gray-700"
|
||||
} relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:ring-offset-2 dark:focus:ring-offset-gray-900`}
|
||||
>
|
||||
<span
|
||||
className={`${
|
||||
@@ -103,6 +109,8 @@ function Search() {
|
||||
{/* Search Results Panel */}
|
||||
<SearchResults />
|
||||
</div>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,21 +1,7 @@
|
||||
import { createContext, useContext, useState } from "react";
|
||||
import { Tab, TabGroup, TabList, TabPanel, TabPanels } from "@headlessui/react";
|
||||
import {
|
||||
Monitor,
|
||||
Moon,
|
||||
Sun,
|
||||
Keyboard,
|
||||
Bell,
|
||||
Palette,
|
||||
Shield,
|
||||
Workflow,
|
||||
Settings,
|
||||
Puzzle,
|
||||
User,
|
||||
Users,
|
||||
Settings2,
|
||||
Info,
|
||||
} from "lucide-react";
|
||||
import { Settings, Puzzle, User, Users, Settings2, Info } from "lucide-react";
|
||||
|
||||
import SettingsPanel from "./SettingsPanel";
|
||||
import GeneralSettings from "./GeneralSettings";
|
||||
import Footer from "../Footer";
|
||||
@@ -45,7 +31,7 @@ function SettingsPage() {
|
||||
return (
|
||||
<ThemeContext.Provider value={{ theme, setTheme }}>
|
||||
<div className={theme}>
|
||||
<div className="min-h-screen bg-white dark:bg-gray-900 text-gray-900 dark:text-white">
|
||||
<div className="min-h-screen pb-8 bg-white dark:bg-gray-900 text-gray-900 dark:text-white">
|
||||
<div className="max-w-6xl mx-auto p-4">
|
||||
<div className="flex items-center justify-center mb-2">
|
||||
<h1 className="text-xl font-bold">Coco Settings</h1>
|
||||
|
||||
32
src/main.css
32
src/main.css
@@ -22,11 +22,11 @@
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-gray-50 text-gray-900 antialiased;
|
||||
@apply bg-gray-50 text-gray-900 rounded-lg antialiased;
|
||||
}
|
||||
|
||||
.dark body {
|
||||
@apply bg-gray-900 text-gray-100;
|
||||
@apply bg-gray-900 text-gray-100 rounded-lg;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,3 +47,31 @@
|
||||
transition-colors duration-200;
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.custom-scrollbar {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #cbd5e1 transparent;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background-color: #cbd5e1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.dark .custom-scrollbar {
|
||||
scrollbar-color: #475569 transparent;
|
||||
}
|
||||
|
||||
.dark .custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background-color: #475569;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { RouterProvider } from "react-router-dom";
|
||||
import { ThemeProvider } from "./components/ThemeProvider";
|
||||
import { router } from "./routes/index";
|
||||
|
||||
import './main.css';
|
||||
import "./main.css";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||
<React.StrictMode>
|
||||
|
||||
21
src/routes/Header.tsx
Normal file
21
src/routes/Header.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||
|
||||
export default function Header() {
|
||||
// 调用 navigate() 去你想去的地方 ⛱️
|
||||
const navigate = useNavigate();
|
||||
// 我在哪?
|
||||
const location = useLocation();
|
||||
const showBack = location.pathname !== "/";
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div onClick={() => navigate("/")}>Home</div>
|
||||
<div>
|
||||
{/* 相当于 HTML 中的 <a>,点击后跳转页面 */}
|
||||
<Link to="/settings" title="更多">
|
||||
Settings
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
11
src/routes/Layout.tsx
Normal file
11
src/routes/Layout.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Outlet } from "react-router-dom";
|
||||
|
||||
export default function Layout() {
|
||||
return (
|
||||
<div>
|
||||
<main>
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -5,16 +5,16 @@ import ErrorPage from "../error-page";
|
||||
import Settings from "../components/Settings";
|
||||
import Settings2 from "../components/Settings/index2";
|
||||
import SearchChat from "../components/SearchChat";
|
||||
import Layout from './Layout'
|
||||
|
||||
export const router = createBrowserRouter([
|
||||
{
|
||||
path: "/",
|
||||
element: <SearchChat />,
|
||||
errorElement: <ErrorPage />,
|
||||
},
|
||||
{
|
||||
path: "/settings",
|
||||
element: <Settings2 />,
|
||||
path: '/',
|
||||
element: <Layout />,
|
||||
errorElement: <ErrorPage />,
|
||||
children: [
|
||||
{ path: '/', element: <SearchChat /> },
|
||||
{ path: '/settings', element: <Settings2 /> }
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
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/
|
||||
|
||||
Reference in New Issue
Block a user