feat: 完成幽灵阅读器MVP版本基础搭建
实现了: 1. Tauri后端框架搭建与数据库初始化 2. 前端路由与设置页面重构 3. 外观、快捷键、关于三个设置子页面 4. 书架主页UI重构 5. 新增项目文档与依赖配置
This commit is contained in:
parent
a37898adde
commit
a89ee82be4
100
README.md
100
README.md
|
|
@ -1,7 +1,99 @@
|
|||
# Tauri + Vue + TypeScript
|
||||
# 幽灵阅读器 · Stealth Reader
|
||||
|
||||
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||
一款永远浮在屏幕角落、能一键隐身、看起来像系统通知的本地小说阅读器。
|
||||
|
||||
## Recommended IDE Setup
|
||||
核心不是「读小说」,而是「安全地读小说」——解决职场场景下「想摸鱼但怕被发现」的焦虑。
|
||||
|
||||
- [VS Code](https://code.visualstudio.com/) + [Vue - Official](https://marketplace.visualstudio.com/items?itemName=Vue.volar) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)
|
||||
## 定位
|
||||
|
||||
桌面端本地小说阅读器,核心卖点是「职场隐身模式」。
|
||||
|
||||
- 目标平台:Windows / macOS / Linux(基于 Tauri 跨平台)
|
||||
- 核心价值:轻量 + 本地 + 隐蔽
|
||||
|
||||
## 特性
|
||||
|
||||
- **本地导入**:支持 TXT 导入,自动检测 GBK / UTF-8 编码
|
||||
- **自动分章**:正则识别「第 X 章」等常见格式,自动拆分章节
|
||||
- **分页阅读**:按键翻页(↑↓ / PgUp / PgDn),无滚动条,像读实体书
|
||||
- **阅读进度持久化**:自动保存进度,下次打开继续读
|
||||
- **老板键**:系统级全局快捷键,一键瞬间隐身,任务栏无图标
|
||||
- **幽灵模式**:窗口透明 + 鼠标穿透 + 置顶,真正融入桌面
|
||||
- **系统托盘**:支持伪装图标,静默驻留后台
|
||||
- **零打扰**:无账号、无广告、无在线同步,纯本地
|
||||
|
||||
## 四种预设模式
|
||||
|
||||
| 模式 | 尺寸 | 透明度 | 鼠标穿透 | 交互 | 适用场景 |
|
||||
|------|------|--------|---------|------|---------|
|
||||
| 专注模式 | 600×400 | 100% | 关闭 | 完整交互 | 午休 / 独处 |
|
||||
| 幽灵模式 | 300×150 | 30% | 开启 | 无(纯看) | 边工作边偷看 |
|
||||
| 隐身模式 | 隐藏 | 0% | - | 无 | 老板来了 |
|
||||
| 迷你模式 | 200×100 | 50% | 可选 | 仅翻页 | 会议中 |
|
||||
|
||||
## 技术栈
|
||||
|
||||
- [Tauri 2](https://tauri.app/) — 跨平台桌面壳与窗口级 API
|
||||
- [Vue 3](https://vuejs.org/) + TypeScript + Vite
|
||||
- [Naive UI](https://www.naiveui.com/) — 组件库
|
||||
- [Pinia](https://pinia.vuejs.org/) — 状态管理(persistedstate 持久化)
|
||||
- Rust — 后端能力(编码检测、全局快捷键、系统托盘等)
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 环境要求
|
||||
|
||||
- [Node.js](https://nodejs.org/)(18+)与 [pnpm](https://pnpm.io/)
|
||||
- [Rust](https://www.rust-lang.org/) 工具链
|
||||
- Tauri 各平台系统依赖(参考 [Tauri 官方文档](https://tauri.app/start/prerequisites/))
|
||||
|
||||
### 安装与运行
|
||||
|
||||
```bash
|
||||
pnpm install # 安装依赖
|
||||
pnpm tauri dev # 开发模式启动
|
||||
pnpm tauri build # 构建安装包
|
||||
```
|
||||
|
||||
仅调试前端界面:
|
||||
|
||||
```bash
|
||||
pnpm dev # 启动 Vite 开发服务器(http://localhost:1420)
|
||||
pnpm build # 类型检查 + 构建前端产物
|
||||
```
|
||||
|
||||
## 路线图
|
||||
|
||||
### v0.1.0(MVP)
|
||||
|
||||
- TXT 导入 + 编码自动检测
|
||||
- 书架列表 + 删除
|
||||
- 分页阅读 + 字体 / 行距设置
|
||||
- 老板键(显隐)
|
||||
- 窗口置顶 + 透明度调节
|
||||
- 阅读进度保存
|
||||
|
||||
### v1.0.0
|
||||
|
||||
- 幽灵模式(鼠标穿透 + 悬停切换)
|
||||
- 系统托盘 + 伪装图标
|
||||
- 设置面板
|
||||
- 窗口尺寸记忆
|
||||
- 安装包(Windows .msi / macOS .dmg)
|
||||
|
||||
### v1.1.0
|
||||
|
||||
- EPUB 导入支持
|
||||
- 章节手动跳转
|
||||
- 阅读时长统计
|
||||
- 主题切换(IDE 伪装主题)
|
||||
|
||||
### v2.0(未来)
|
||||
|
||||
- 多设备进度同步(WebDAV / 局域网)
|
||||
- 语音朗读(TTS)
|
||||
- 书摘 / 笔记
|
||||
|
||||
## 免责声明
|
||||
|
||||
本项目仅支持本地文件导入,不内置任何在线书源或下载功能。请尊重版权,仅使用您拥有合法权利的书籍内容。
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -14,12 +14,24 @@ edition = "2021"
|
|||
name = "stealthreader_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[workspace]
|
||||
members = ["."]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
tauri-helper = "0.2.1"
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2", features = [] }
|
||||
tauri-plugin-opener = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
|
||||
tauri-plugin-store = "2.4.4"
|
||||
tauri-helper = "0.2.1"
|
||||
sqlx = { version = "0.9.0", features = [
|
||||
"runtime-tokio",
|
||||
"sqlite",
|
||||
"tls-native-tls",
|
||||
"chrono",
|
||||
] }
|
||||
chrono = "0.4.45"
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
fn main() {
|
||||
tauri_helper::generate_command_file(tauri_helper::TauriHelperOptions::default());
|
||||
tauri_build::build()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
use tauri_helper::auto_collect_command;
|
||||
|
||||
#[tauri::command]
|
||||
#[auto_collect_command]
|
||||
pub async fn import_book() -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
pub mod book;
|
||||
|
|
@ -1,14 +1,25 @@
|
|||
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
|
||||
#[tauri::command]
|
||||
fn greet(name: &str) -> String {
|
||||
format!("Hello, {}! You've been greeted from Rust!", name)
|
||||
}
|
||||
mod commands;
|
||||
mod services;
|
||||
mod models;
|
||||
|
||||
use tauri::Manager;
|
||||
use commands::book::*;
|
||||
use services::db::init_pool;
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.invoke_handler(tauri::generate_handler![greet])
|
||||
.setup(|app| {
|
||||
let pool = tauri::async_runtime::block_on(async {
|
||||
init_pool().await.expect("sqlite数据库连接池初始化失败!")
|
||||
});
|
||||
app.manage(pool);
|
||||
Ok(())
|
||||
})
|
||||
.plugin(tauri_plugin_store::Builder::default().build())
|
||||
// .invoke_handler(tauri::generate_handler![greet])
|
||||
.invoke_handler(tauri_helper::tauri_collect_commands!())
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,47 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct PageResult<T> {
|
||||
pub total: i64,
|
||||
pub list: Vec<T>,
|
||||
pub page: i32,
|
||||
pub page_size: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ApiResponse<T> {
|
||||
success: bool,
|
||||
code: i32,
|
||||
msg: String,
|
||||
data: Option<T>,
|
||||
}
|
||||
|
||||
impl<T> ApiResponse<T> {
|
||||
// 成功响应
|
||||
pub fn success(data: T) -> Self {
|
||||
Self {
|
||||
success: true,
|
||||
code: 200,
|
||||
msg: "操作成功".to_string(),
|
||||
data: Some(data),
|
||||
}
|
||||
}
|
||||
// 成功响应,数据为空
|
||||
pub fn success_empty() -> ApiResponse<()> {
|
||||
ApiResponse {
|
||||
success: true,
|
||||
code: 200,
|
||||
msg: "操作成功".to_string(),
|
||||
data: None,
|
||||
}
|
||||
}
|
||||
// 错误响应
|
||||
pub fn error(code: i32, message: &str) -> Self {
|
||||
Self {
|
||||
success: false,
|
||||
code,
|
||||
msg: message.to_string(),
|
||||
data: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
use sqlx::pool::PoolOptions;
|
||||
use sqlx::sqlite::{SqliteConnectOptions, SqlitePool, SqlitePoolOptions};
|
||||
|
||||
pub async fn init_pool() -> Result<SqlitePool, sqlx::Error> {
|
||||
SqlitePoolOptions::new()
|
||||
.max_connections(5)
|
||||
.min_connections(1)
|
||||
.connect("sqlite::memory:")
|
||||
.await
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
pub mod db;
|
||||
|
|
@ -20,7 +20,7 @@
|
|||
"decorations": false,
|
||||
"alwaysOnTop": false,
|
||||
"skipTaskbar": false,
|
||||
"shadow": false,
|
||||
"shadow": true,
|
||||
"visible": true,
|
||||
"center": false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ const routes = [
|
|||
path: "/setting",
|
||||
name: "Setting",
|
||||
component: () => import("@/views/Setting.vue"),
|
||||
redirect: "/setting/appearance",
|
||||
meta: {
|
||||
isCache: true,
|
||||
title: "设置",
|
||||
|
|
@ -44,6 +45,24 @@ const routes = [
|
|||
title: "外观设置",
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/setting/shortcut",
|
||||
name: "Shortcut",
|
||||
component: () => import("@/views/setting/Shortcut.vue"),
|
||||
meta: {
|
||||
isCache: true,
|
||||
title: "快捷键",
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/setting/about",
|
||||
name: "About",
|
||||
component: () => import("@/views/setting/About.vue"),
|
||||
meta: {
|
||||
isCache: true,
|
||||
title: "关于",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ export {}
|
|||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
NAnchor: typeof import('naive-ui')['NAnchor']
|
||||
NAnchorLink: typeof import('naive-ui')['NAnchorLink']
|
||||
NButton: typeof import('naive-ui')['NButton']
|
||||
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
|
||||
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
|
||||
|
|
@ -20,10 +22,14 @@ declare module 'vue' {
|
|||
NLayout: typeof import('naive-ui')['NLayout']
|
||||
NLayoutContent: typeof import('naive-ui')['NLayoutContent']
|
||||
NLayoutHeader: typeof import('naive-ui')['NLayoutHeader']
|
||||
NLayoutSider: typeof import('naive-ui')['NLayoutSider']
|
||||
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
|
||||
NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
|
||||
NRadioButton: typeof import('naive-ui')['NRadioButton']
|
||||
NRadioGroup: typeof import('naive-ui')['NRadioGroup']
|
||||
NScrollbar: typeof import('naive-ui')['NScrollbar']
|
||||
NSpace: typeof import('naive-ui')['NSpace']
|
||||
NSwitch: typeof import('naive-ui')['NSwitch']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,43 +1,157 @@
|
|||
<template>
|
||||
<n-space vertical size="large">
|
||||
<n-layout>
|
||||
<n-layout-header class="header">
|
||||
<n-input round placeholder="搜索">
|
||||
<template #suffix>
|
||||
<n-icon :component="BookSearch24Regular" />
|
||||
</template>
|
||||
</n-input>
|
||||
<n-icon :component="SettingOutlined" @click="goSettingView" />
|
||||
<!-- <n-icon :component="BookReader" @click="goReaderView" /> -->
|
||||
</n-layout-header>
|
||||
<n-layout-content class="content">
|
||||
<div>书籍列表</div>
|
||||
<n-empty v-if="bookList.length == 0" description="还没有导入书籍哦">
|
||||
<template> <div class="home">
|
||||
<header class="home-header">
|
||||
<n-input
|
||||
v-model:value="searchValue"
|
||||
class="search-input"
|
||||
round
|
||||
clearable
|
||||
placeholder="搜索书籍"
|
||||
>
|
||||
<template #prefix>
|
||||
<n-icon :component="BookSearch24Regular" />
|
||||
</template>
|
||||
</n-input>
|
||||
|
||||
<button class="setting-btn" title="设置" aria-label="设置" @click="goSettingView">
|
||||
<n-icon :size="20" :component="SettingOutlined" />
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<section class="home-body">
|
||||
<div class="section-title">
|
||||
<span class="section-name">我的书籍</span>
|
||||
<span class="section-count">{{ bookList.length }} 本</span>
|
||||
</div>
|
||||
|
||||
<n-scrollbar class="book-scrollbar">
|
||||
<n-empty v-if="bookList.length === 0" class="empty" description="还没有导入书籍哦">
|
||||
<template #extra>
|
||||
<n-button size="small"> 立即导入 </n-button>
|
||||
<n-button size="small" type="primary" round>立即导入</n-button>
|
||||
</template>
|
||||
</n-empty>
|
||||
</n-layout-content>
|
||||
</n-layout>
|
||||
</n-space>
|
||||
</n-scrollbar>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { BookSearch24Regular } from "@vicons/fluent"
|
||||
import { SettingOutlined } from "@vicons/antd"
|
||||
import { BookReader } from "@vicons/fa"
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const bookList = ref([])
|
||||
const searchValue = ref("")
|
||||
|
||||
const goSettingView = () => {
|
||||
router.push({ name: "Setting" })
|
||||
}
|
||||
|
||||
const goReaderView = () => {
|
||||
router.push({ name: "Reader" })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.home {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.home-header {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 20px 24px 12px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
margin-right: 16px;
|
||||
|
||||
:deep(.n-input) {
|
||||
background-color: var(--color-surface);
|
||||
border-color: var(--color-border);
|
||||
}
|
||||
|
||||
:deep(.n-input:hover) {
|
||||
border-color: var(--color-border-hover);
|
||||
}
|
||||
|
||||
:deep(.n-input.n-input--focus) {
|
||||
border-color: var(--color-accent);
|
||||
box-shadow: var(--shadow-focus);
|
||||
}
|
||||
}
|
||||
|
||||
.setting-btn {
|
||||
flex-shrink: 0;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
background-color: var(--color-surface);
|
||||
color: var(--color-text-secondary);
|
||||
box-shadow: var(--shadow-card);
|
||||
cursor: pointer;
|
||||
transition: color 0.2s ease, box-shadow 0.2s ease, background-color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-accent);
|
||||
background-color: var(--color-surface-hover);
|
||||
box-shadow: var(--shadow-card-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.home-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0 24px 20px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.section-name {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-primary);
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.section-count {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.book-scrollbar {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
|
||||
:deep(.n-scrollbar-container) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(.n-scrollbar-content) {
|
||||
min-height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.empty {
|
||||
min-height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
pt>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
|
|
|||
|
|
@ -1,18 +1,187 @@
|
|||
<template>
|
||||
<n-space>
|
||||
<n-icon :component="MdArrowBack" @click="goBack" />
|
||||
<p><strong>设置页面</strong></p>
|
||||
</n-space>
|
||||
<div class="setting">
|
||||
<header class="setting-header">
|
||||
<button class="back-btn" title="返回" aria-label="返回" @click="goBack">
|
||||
<n-icon :component="ArrowLeftOutlined" />
|
||||
</button>
|
||||
<span class="setting-title">设置</span>
|
||||
</header>
|
||||
|
||||
<div class="setting-body">
|
||||
<aside class="setting-sider">
|
||||
<n-scrollbar class="setting-sider__scroll">
|
||||
<n-anchor
|
||||
class="setting-anchor"
|
||||
:show-rail="false"
|
||||
:show-background="false"
|
||||
>
|
||||
<n-anchor-link
|
||||
v-for="item in menuItems"
|
||||
:key="item.name"
|
||||
:href="item.href"
|
||||
@click="onNavClick(item, $event)"
|
||||
>
|
||||
<template #title>
|
||||
<span class="nav-item" :class="{ 'nav-item--active': isActive(item) }">
|
||||
<n-icon class="nav-item__icon" :component="item.icon" :size="18" />
|
||||
<span class="nav-item__label">{{ item.label }}</span>
|
||||
</span>
|
||||
</template>
|
||||
</n-anchor-link>
|
||||
</n-anchor>
|
||||
</n-scrollbar>
|
||||
</aside>
|
||||
|
||||
<section class="setting-content">
|
||||
<n-scrollbar class="setting-content__scroll">
|
||||
<RouterView />
|
||||
</n-scrollbar>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { MdArrowBack } from "@vicons/ionicons4"
|
||||
import {
|
||||
ArrowLeftOutlined,
|
||||
BgColorsOutlined,
|
||||
InfoCircleOutlined,
|
||||
KeyOutlined,
|
||||
} from "@vicons/antd"
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const menuItems = [
|
||||
{ label: "外观", name: "Appearance", href: "#appearance", icon: BgColorsOutlined },
|
||||
{ label: "快捷键", name: "Shortcut", href: "#shortcut", icon: KeyOutlined },
|
||||
{ label: "关于", name: "About", href: "#about", icon: InfoCircleOutlined },
|
||||
]
|
||||
|
||||
const goBack = () => {
|
||||
router.back()
|
||||
router.push({ name: "Home" })
|
||||
}
|
||||
|
||||
const isActive = (item: (typeof menuItems)[number]) => route.name === item.name
|
||||
|
||||
const onNavClick = (item: (typeof menuItems)[number], e: Event) => {
|
||||
e.preventDefault()
|
||||
router.push({ name: item.name })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
<style lang="scss" scoped>
|
||||
.setting {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.setting-header {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 20px 24px 16px;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
background-color: var(--color-surface);
|
||||
color: var(--color-text-secondary);
|
||||
box-shadow: var(--shadow-card);
|
||||
cursor: pointer;
|
||||
transition: color 0.2s ease, box-shadow 0.2s ease, background-color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-accent);
|
||||
background-color: var(--color-surface-hover);
|
||||
box-shadow: var(--shadow-card-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.setting-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-primary);
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.setting-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
margin: 0 24px 24px;
|
||||
}
|
||||
|
||||
.setting-sider {
|
||||
flex-shrink: 0;
|
||||
width: 200px;
|
||||
margin-right: 24px;
|
||||
overflow: hidden;
|
||||
background-color: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-card);
|
||||
box-shadow: var(--shadow-card);
|
||||
}
|
||||
|
||||
.setting-sider__scroll {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.setting-anchor {
|
||||
margin: 12px;
|
||||
|
||||
:deep(.n-anchor-link) {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
:deep(.n-anchor-link__title) {
|
||||
padding: 0;
|
||||
color: var(--color-text-secondary);
|
||||
border-radius: var(--radius-sm);
|
||||
transition: color 0.2s ease, background-color 0.2s ease;
|
||||
}
|
||||
|
||||
:deep(.n-anchor-link__title:hover) {
|
||||
color: var(--color-text-primary);
|
||||
background-color: var(--color-surface-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 8px 12px;
|
||||
font-size: 14px;
|
||||
|
||||
&__icon {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
&--active {
|
||||
color: var(--color-accent);
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.setting-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
background-color: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-card);
|
||||
box-shadow: var(--shadow-card);
|
||||
}
|
||||
|
||||
.setting-content__scroll {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,79 @@
|
|||
<template>
|
||||
<div class="setting-page">
|
||||
<h2 class="page-title">关于</h2>
|
||||
<p class="page-desc">关于本应用的信息</p>
|
||||
|
||||
<div class="about-card">
|
||||
<div class="app-logo">幽</div>
|
||||
<div class="app-name">幽灵阅读器</div>
|
||||
<div class="app-version">版本 0.1.0</div>
|
||||
<div class="app-desc">一款安静、沉浸的本地阅读器</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.setting-page {
|
||||
margin: 24px 28px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0 0 4px;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.page-desc {
|
||||
margin: 0 0 20px;
|
||||
font-size: 13px;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.about-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 320px;
|
||||
background-color: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-card);
|
||||
box-shadow: var(--shadow-card);
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.app-logo {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: var(--radius-card);
|
||||
background: var(--color-accent-gradient);
|
||||
color: var(--color-text-invert);
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.app-name {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.app-version {
|
||||
font-size: 13px;
|
||||
color: var(--color-text-secondary);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.app-desc {
|
||||
font-size: 13px;
|
||||
color: var(--color-text-secondary);
|
||||
margin-top: 16px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,13 +1,108 @@
|
|||
<template>
|
||||
<n-space vertical size="large">
|
||||
<strong>外观设置</strong>
|
||||
</n-space>
|
||||
<div class="setting-page">
|
||||
<h2 class="page-title">外观</h2>
|
||||
<p class="page-desc">自定义应用的外观与阅读体验</p>
|
||||
|
||||
<div class="setting-group">
|
||||
<div class="group-title">主题</div>
|
||||
<div class="setting-item">
|
||||
<div class="item-info">
|
||||
<span class="item-label">主题模式</span>
|
||||
<span class="item-desc">选择应用的整体配色方案</span>
|
||||
</div>
|
||||
<n-radio-group v-model:value="theme" name="theme">
|
||||
<n-radio-button value="light">浅色</n-radio-button>
|
||||
<n-radio-button value="dark">深色</n-radio-button>
|
||||
<n-radio-button value="system">跟随系统</n-radio-button>
|
||||
</n-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="setting-group">
|
||||
<div class="group-title">阅读</div>
|
||||
<div class="setting-item">
|
||||
<div class="item-info">
|
||||
<span class="item-label">自动换行</span>
|
||||
<span class="item-desc">长段落自动换行显示</span>
|
||||
</div>
|
||||
<n-switch v-model:value="wordWrap" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
const theme = ref("light")
|
||||
const wordWrap = ref(true)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
<style lang="scss" scoped>
|
||||
.setting-page {
|
||||
margin: 24px 28px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0 0 4px;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.page-desc {
|
||||
margin: 0 0 20px;
|
||||
font-size: 13px;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.setting-group {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.group-title {
|
||||
margin-bottom: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background-color: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
box-shadow: var(--shadow-card);
|
||||
margin-bottom: 0;
|
||||
transition: box-shadow 0.2s ease, border-color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--color-border-hover);
|
||||
box-shadow: var(--shadow-card-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.item-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 12px 16px;
|
||||
}
|
||||
|
||||
.item-label {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.item-desc {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-secondary);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
:deep(.n-radio-group),
|
||||
:deep(.n-switch) {
|
||||
margin: 0 16px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
<template>
|
||||
<div class="setting-page">
|
||||
<h2 class="page-title">快捷键</h2>
|
||||
<p class="page-desc">使用键盘快速完成常用操作</p>
|
||||
|
||||
<div class="setting-group">
|
||||
<div v-for="item in shortcuts" :key="item.action" class="shortcut-item">
|
||||
<span class="shortcut-action">{{ item.action }}</span>
|
||||
<span class="shortcut-keys">
|
||||
<template v-for="(key, i) in item.keys" :key="i">
|
||||
<kbd class="shortcut-key">{{ key }}</kbd>
|
||||
<span v-if="i < item.keys.length - 1" class="shortcut-sep">+</span>
|
||||
</template>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const shortcuts = [
|
||||
{ action: "上一页", keys: ["←"] },
|
||||
{ action: "下一页", keys: ["→"] },
|
||||
{ action: "打开搜索", keys: ["Ctrl", "F"] },
|
||||
{ action: "打开设置", keys: ["Ctrl", ","] },
|
||||
]
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.setting-page {
|
||||
margin: 24px 28px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0 0 4px;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.page-desc {
|
||||
margin: 0 0 20px;
|
||||
font-size: 13px;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.setting-group {
|
||||
background-color: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
box-shadow: var(--shadow-card);
|
||||
}
|
||||
|
||||
.shortcut-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin: 0 16px;
|
||||
min-height: 44px;
|
||||
|
||||
& + & {
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
}
|
||||
|
||||
.shortcut-action {
|
||||
font-size: 14px;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.shortcut-keys {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.shortcut-key {
|
||||
min-width: 24px;
|
||||
height: 24px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 2px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 6px;
|
||||
background-color: var(--color-window-bg);
|
||||
color: var(--color-text-primary);
|
||||
font-size: 12px;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.shortcut-sep {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
Loading…
Reference in New Issue