添加设置页面
This commit is contained in:
82
src/App.vue
82
src/App.vue
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {computed, ref, watch, onMounted, onUnmounted} from "vue";
|
import {computed, ref, watch, onMounted, onUnmounted} from "vue";
|
||||||
import {listen} from "@tauri-apps/api/event";
|
import {listen, type EventCallback, type EventName} from "@tauri-apps/api/event";
|
||||||
import {invoke} from "@tauri-apps/api/core";
|
import {invoke, isTauri} from "@tauri-apps/api/core";
|
||||||
import MarkdownEditor from "./components/MarkdownEditor.vue";
|
import MarkdownEditor from "./components/MarkdownEditor.vue";
|
||||||
import DirectorySidebar from "./components/DirectorySidebar.vue";
|
import DirectorySidebar from "./components/DirectorySidebar.vue";
|
||||||
import OutlinePanel from "./components/OutlinePanel.vue";
|
import OutlinePanel from "./components/OutlinePanel.vue";
|
||||||
@@ -11,6 +11,27 @@ const wordCount = ref(0);
|
|||||||
const charCount = ref(0);
|
const charCount = ref(0);
|
||||||
|
|
||||||
type DocumentMode = "preview" | "edit" | "mixed";
|
type DocumentMode = "preview" | "edit" | "mixed";
|
||||||
|
const DEFAULT_DOCUMENT_MODE_KEY = "yurou:default-document-mode";
|
||||||
|
|
||||||
|
function isDocumentMode(value: string | null): value is DocumentMode {
|
||||||
|
return value === "preview" || value === "edit" || value === "mixed";
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadDefaultDocumentMode(): DocumentMode {
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem(DEFAULT_DOCUMENT_MODE_KEY);
|
||||||
|
if (isDocumentMode(stored)) return stored;
|
||||||
|
} catch { /* localStorage unavailable */
|
||||||
|
}
|
||||||
|
return "edit";
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveDefaultDocumentMode(mode: DocumentMode) {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(DEFAULT_DOCUMENT_MODE_KEY, mode);
|
||||||
|
} catch { /* noop */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// File state
|
// File state
|
||||||
const currentFilePath = ref<string | null>(null);
|
const currentFilePath = ref<string | null>(null);
|
||||||
@@ -81,13 +102,31 @@ function onEditorWheel(e: WheelEvent) {
|
|||||||
|
|
||||||
// ---- toolbar state ----
|
// ---- toolbar state ----
|
||||||
const showOutline = ref(false);
|
const showOutline = ref(false);
|
||||||
const documentMode = ref<DocumentMode>("edit");
|
const defaultDocumentMode = ref<DocumentMode>(loadDefaultDocumentMode());
|
||||||
|
const documentMode = ref<DocumentMode>(defaultDocumentMode.value);
|
||||||
const showMoreMenu = ref(false);
|
const showMoreMenu = ref(false);
|
||||||
|
const settingsRequest = ref(0);
|
||||||
|
const settingsInitialPage = ref<"about" | "editor">("about");
|
||||||
const markdownEditorRef = ref<{ flushPendingChanges: () => string | undefined } | null>(null);
|
const markdownEditorRef = ref<{ flushPendingChanges: () => string | undefined } | null>(null);
|
||||||
const showStartActions = computed(() => {
|
const showStartActions = computed(() => {
|
||||||
return !folderPath.value && !currentFilePath.value && !hasDraftDocument.value;
|
return !folderPath.value && !currentFilePath.value && !hasDraftDocument.value;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function openSettings(page: "about" | "editor" = "about") {
|
||||||
|
settingsInitialPage.value = page;
|
||||||
|
settingsRequest.value += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setDocumentMode(mode: DocumentMode) {
|
||||||
|
documentMode.value = mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateDefaultDocumentMode(mode: DocumentMode) {
|
||||||
|
defaultDocumentMode.value = mode;
|
||||||
|
documentMode.value = mode;
|
||||||
|
saveDefaultDocumentMode(mode);
|
||||||
|
}
|
||||||
|
|
||||||
function handleExportPdf() {
|
function handleExportPdf() {
|
||||||
showMoreMenu.value = false;
|
showMoreMenu.value = false;
|
||||||
// TODO: implement PDF export
|
// TODO: implement PDF export
|
||||||
@@ -162,7 +201,7 @@ function resetMarkdownEditorState() {
|
|||||||
hasDraftDocument.value = false;
|
hasDraftDocument.value = false;
|
||||||
saveStatus.value = "";
|
saveStatus.value = "";
|
||||||
showOutline.value = false;
|
showOutline.value = false;
|
||||||
documentMode.value = "edit";
|
documentMode.value = defaultDocumentMode.value;
|
||||||
showMoreMenu.value = false;
|
showMoreMenu.value = false;
|
||||||
editorResetKey.value += 1;
|
editorResetKey.value += 1;
|
||||||
}
|
}
|
||||||
@@ -345,27 +384,40 @@ function doNew() {
|
|||||||
currentFilePath.value = null;
|
currentFilePath.value = null;
|
||||||
hasDraftDocument.value = true;
|
hasDraftDocument.value = true;
|
||||||
saveStatus.value = "";
|
saveStatus.value = "";
|
||||||
|
documentMode.value = defaultDocumentMode.value;
|
||||||
editorResetKey.value += 1;
|
editorResetKey.value += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function registerTauriListener<T>(eventName: EventName, handler: EventCallback<T>) {
|
||||||
|
if (!isTauri()) return;
|
||||||
|
|
||||||
|
void listen<T>(eventName, handler).catch((error) => {
|
||||||
|
console.error(`Failed to listen for ${eventName}:`, error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Listen for menu events from Rust
|
// Listen for menu events from Rust
|
||||||
listen<string>("folder-opened", (event) => {
|
registerTauriListener<string>("folder-opened", (event) => {
|
||||||
void switchWorkspace(event.payload);
|
void switchWorkspace(event.payload);
|
||||||
});
|
});
|
||||||
|
|
||||||
listen("menu-save", () => {
|
registerTauriListener("menu-save", () => {
|
||||||
doSave();
|
doSave();
|
||||||
});
|
});
|
||||||
|
|
||||||
listen("menu-save-as", () => {
|
registerTauriListener("menu-save-as", () => {
|
||||||
doSaveAs();
|
doSaveAs();
|
||||||
});
|
});
|
||||||
|
|
||||||
listen("menu-new", () => {
|
registerTauriListener("menu-new", () => {
|
||||||
doNew();
|
doNew();
|
||||||
});
|
});
|
||||||
|
|
||||||
listen("menu-event", (event) => {
|
registerTauriListener("show-about", () => {
|
||||||
|
openSettings("about");
|
||||||
|
});
|
||||||
|
|
||||||
|
registerTauriListener("menu-event", (event) => {
|
||||||
console.log("Menu event:", event.payload);
|
console.log("Menu event:", event.payload);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -379,9 +431,13 @@ listen("menu-event", (event) => {
|
|||||||
:folder-path="folderPath"
|
:folder-path="folderPath"
|
||||||
:entries="dirEntries"
|
:entries="dirEntries"
|
||||||
:current-file-path="currentFilePath"
|
:current-file-path="currentFilePath"
|
||||||
|
:settings-request="settingsRequest"
|
||||||
|
:settings-initial-page="settingsInitialPage"
|
||||||
|
:default-document-mode="defaultDocumentMode"
|
||||||
@open-folder="onOpenFolder"
|
@open-folder="onOpenFolder"
|
||||||
@toggle-folder="onToggleFolder"
|
@toggle-folder="onToggleFolder"
|
||||||
@open-file="onOpenFile"
|
@open-file="onOpenFile"
|
||||||
|
@update-default-document-mode="updateDefaultDocumentMode"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Right: Editor area -->
|
<!-- Right: Editor area -->
|
||||||
@@ -414,7 +470,7 @@ listen("menu-event", (event) => {
|
|||||||
class="mode-toggle-btn"
|
class="mode-toggle-btn"
|
||||||
:class="{ 'mode-toggle-btn-active': documentMode === 'preview' }"
|
:class="{ 'mode-toggle-btn-active': documentMode === 'preview' }"
|
||||||
title="预览模式"
|
title="预览模式"
|
||||||
@click="documentMode = 'preview'"
|
@click="setDocumentMode('preview')"
|
||||||
>
|
>
|
||||||
预览
|
预览
|
||||||
</button>
|
</button>
|
||||||
@@ -422,7 +478,7 @@ listen("menu-event", (event) => {
|
|||||||
class="mode-toggle-btn"
|
class="mode-toggle-btn"
|
||||||
:class="{ 'mode-toggle-btn-active': documentMode === 'edit' }"
|
:class="{ 'mode-toggle-btn-active': documentMode === 'edit' }"
|
||||||
title="编辑模式"
|
title="编辑模式"
|
||||||
@click="documentMode = 'edit'"
|
@click="setDocumentMode('edit')"
|
||||||
>
|
>
|
||||||
编辑
|
编辑
|
||||||
</button>
|
</button>
|
||||||
@@ -430,7 +486,7 @@ listen("menu-event", (event) => {
|
|||||||
class="mode-toggle-btn"
|
class="mode-toggle-btn"
|
||||||
:class="{ 'mode-toggle-btn-active': documentMode === 'mixed' }"
|
:class="{ 'mode-toggle-btn-active': documentMode === 'mixed' }"
|
||||||
title="混合模式"
|
title="混合模式"
|
||||||
@click="documentMode = 'mixed'"
|
@click="setDocumentMode('mixed')"
|
||||||
>
|
>
|
||||||
混合
|
混合
|
||||||
</button>
|
</button>
|
||||||
@@ -481,7 +537,7 @@ listen("menu-event", (event) => {
|
|||||||
ref="markdownEditorRef"
|
ref="markdownEditorRef"
|
||||||
:model-value="content"
|
:model-value="content"
|
||||||
:document-mode="documentMode"
|
:document-mode="documentMode"
|
||||||
placeholder="输入 Markdown 内容... 支持标题、粗体、斜体、代码块等 ✨"
|
placeholder="输入 Markdown 内容... 支持标题、粗体、斜体、代码块等"
|
||||||
@update:model-value="onUpdate"
|
@update:model-value="onUpdate"
|
||||||
@save-shortcut="doSave"
|
@save-shortcut="doSave"
|
||||||
class="flex-1 w-full min-w-0"
|
class="flex-1 w-full min-w-0"
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {ref, computed} from "vue";
|
import {ref, computed, watch} from "vue";
|
||||||
|
import SettingsModal from "./SettingsModal.vue";
|
||||||
import TreeEntry from "./TreeEntry.vue";
|
import TreeEntry from "./TreeEntry.vue";
|
||||||
|
|
||||||
|
type SettingsPage = "about" | "editor";
|
||||||
|
type DocumentMode = "preview" | "edit" | "mixed";
|
||||||
|
|
||||||
interface DirEntry {
|
interface DirEntry {
|
||||||
name: string;
|
name: string;
|
||||||
path: string;
|
path: string;
|
||||||
@@ -14,9 +18,15 @@ const props = withDefaults(
|
|||||||
folderPath: string;
|
folderPath: string;
|
||||||
entries: DirEntry[];
|
entries: DirEntry[];
|
||||||
currentFilePath?: string | null;
|
currentFilePath?: string | null;
|
||||||
|
settingsRequest?: number;
|
||||||
|
settingsInitialPage?: SettingsPage;
|
||||||
|
defaultDocumentMode?: DocumentMode;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
currentFilePath: null,
|
currentFilePath: null,
|
||||||
|
settingsRequest: 0,
|
||||||
|
settingsInitialPage: "about",
|
||||||
|
defaultDocumentMode: "edit",
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -26,6 +36,7 @@ const emit = defineEmits<{
|
|||||||
openFile: [path: string];
|
openFile: [path: string];
|
||||||
createFile: [];
|
createFile: [];
|
||||||
createFolder: [];
|
createFolder: [];
|
||||||
|
updateDefaultDocumentMode: [mode: DocumentMode];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
// Wrap the workspace root as a top-level tree node
|
// Wrap the workspace root as a top-level tree node
|
||||||
@@ -56,6 +67,17 @@ function switchView(view: ViewMode) {
|
|||||||
|
|
||||||
// ---- settings modal ----
|
// ---- settings modal ----
|
||||||
const showSettings = ref(false);
|
const showSettings = ref(false);
|
||||||
|
const settingsPage = ref<SettingsPage>("about");
|
||||||
|
|
||||||
|
watch(() => props.settingsRequest, (request) => {
|
||||||
|
if (request <= 0) return;
|
||||||
|
openSettings(props.settingsInitialPage);
|
||||||
|
});
|
||||||
|
|
||||||
|
function openSettings(page: SettingsPage) {
|
||||||
|
settingsPage.value = page;
|
||||||
|
showSettings.value = true;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -85,9 +107,9 @@ const showSettings = ref(false);
|
|||||||
<!-- ===== Settings ===== -->
|
<!-- ===== Settings ===== -->
|
||||||
<button
|
<button
|
||||||
class="px-2 py-1 cursor-pointer rounded-md transition-colors mt-auto"
|
class="px-2 py-1 cursor-pointer rounded-md transition-colors mt-auto"
|
||||||
:class="activeView === 'setting' ? 'text-[#bf6a3b] bg-[#ede8de]' : 'text-[#b8b3a8] hover:text-[#5c574e] hover:bg-[#ede8de]'"
|
:class="showSettings ? 'text-[#bf6a3b] bg-[#ede8de]' : 'text-[#b8b3a8] hover:text-[#5c574e] hover:bg-[#ede8de]'"
|
||||||
title="设置"
|
title="设置"
|
||||||
@click="showSettings = true"
|
@click="openSettings('about')"
|
||||||
>
|
>
|
||||||
<i class="ri-settings-line text-sm"></i>
|
<i class="ri-settings-line text-sm"></i>
|
||||||
</button>
|
</button>
|
||||||
@@ -147,26 +169,10 @@ const showSettings = ref(false);
|
|||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<!-- ===== SETTINGS MODAL ===== -->
|
<SettingsModal
|
||||||
<Teleport to="body">
|
v-model:show="showSettings"
|
||||||
<div
|
:initial-page="settingsPage"
|
||||||
v-if="showSettings"
|
:default-document-mode="defaultDocumentMode"
|
||||||
class="fixed inset-0 z-50 flex items-center justify-center bg-[#38342e]/25 backdrop-blur-sm"
|
@update-default-document-mode="(mode: DocumentMode) => emit('updateDefaultDocumentMode', mode)"
|
||||||
@click.self="showSettings = false"
|
/>
|
||||||
>
|
|
||||||
<div class="bg-white border border-[#e0dbcf] rounded-xl shadow-2xl w-80 max-h-[70vh] overflow-y-auto">
|
|
||||||
<div class="flex items-center justify-between px-4 py-3 border-b border-[#e8e4da]">
|
|
||||||
<span class="text-sm font-semibold text-[#38342e]">设置</span>
|
|
||||||
<button
|
|
||||||
class="p-1 rounded-md text-[#8c877d] hover:text-[#38342e] hover:bg-[#e0dbcf] transition-colors"
|
|
||||||
@click="showSettings = false"
|
|
||||||
>
|
|
||||||
<i class="ri-close-line text-base"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="px-4 py-6 space-y-5">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Teleport>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
632
src/components/SettingsModal.vue
Normal file
632
src/components/SettingsModal.vue
Normal file
@@ -0,0 +1,632 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { getVersion } from "@tauri-apps/api/app";
|
||||||
|
import { computed, onBeforeUnmount, ref, watch } from "vue";
|
||||||
|
|
||||||
|
type SettingsPage = "about" | "editor";
|
||||||
|
type DocumentMode = "preview" | "edit" | "mixed";
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
show: boolean;
|
||||||
|
initialPage?: SettingsPage;
|
||||||
|
defaultDocumentMode?: DocumentMode;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
initialPage: "about",
|
||||||
|
defaultDocumentMode: "edit",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
"update:show": [value: boolean];
|
||||||
|
updateDefaultDocumentMode: [mode: DocumentMode];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const APP_NAME = "yurou";
|
||||||
|
const FALLBACK_VERSION = "0.1.0";
|
||||||
|
const AUTO_UPDATE_KEY = "yurou:auto-update";
|
||||||
|
const LANGUAGE_KEY = "yurou:language";
|
||||||
|
|
||||||
|
const activePage = ref<SettingsPage>(props.initialPage);
|
||||||
|
const appVersion = ref(FALLBACK_VERSION);
|
||||||
|
const autoUpdate = ref(loadBooleanPref(AUTO_UPDATE_KEY, true));
|
||||||
|
const language = ref(loadStringPref(LANGUAGE_KEY, "zh-Hans"));
|
||||||
|
const updateStatus = ref("");
|
||||||
|
let updateStatusTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
|
const pageTitle = computed(() => (activePage.value === "about" ? "关于" : "编辑器"));
|
||||||
|
|
||||||
|
const menuItems: Array<{
|
||||||
|
id: SettingsPage;
|
||||||
|
label: string;
|
||||||
|
icon: string;
|
||||||
|
}> = [
|
||||||
|
{ id: "about", label: "关于", icon: "ri-information-line" },
|
||||||
|
{ id: "editor", label: "编辑器", icon: "ri-edit-2-line" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const documentModeOptions: Array<{
|
||||||
|
value: DocumentMode;
|
||||||
|
label: string;
|
||||||
|
}> = [
|
||||||
|
{ value: "preview", label: "预览" },
|
||||||
|
{ value: "edit", label: "编辑" },
|
||||||
|
{ value: "mixed", label: "混合" },
|
||||||
|
];
|
||||||
|
|
||||||
|
function loadBooleanPref(key: string, fallback: boolean) {
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem(key);
|
||||||
|
if (stored === "true") return true;
|
||||||
|
if (stored === "false") return false;
|
||||||
|
} catch {
|
||||||
|
// localStorage may be unavailable in restricted environments.
|
||||||
|
}
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadStringPref(key: string, fallback: string) {
|
||||||
|
try {
|
||||||
|
return localStorage.getItem(key) || fallback;
|
||||||
|
} catch {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function savePref(key: string, value: string) {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(key, value);
|
||||||
|
} catch {
|
||||||
|
// noop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
emit("update:show", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onOverlayKeydown(event: KeyboardEvent) {
|
||||||
|
if (event.key === "Escape") close();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadAppVersion() {
|
||||||
|
try {
|
||||||
|
appVersion.value = await getVersion();
|
||||||
|
} catch {
|
||||||
|
appVersion.value = FALLBACK_VERSION;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkForUpdates() {
|
||||||
|
updateStatus.value = "当前已是最新版本";
|
||||||
|
if (updateStatusTimer) clearTimeout(updateStatusTimer);
|
||||||
|
updateStatusTimer = setTimeout(() => {
|
||||||
|
updateStatus.value = "";
|
||||||
|
}, 2400);
|
||||||
|
}
|
||||||
|
|
||||||
|
function openReleaseNotes() {
|
||||||
|
window.open("https://gitea.cirry.cn/cirry/yurou/releases", "_blank", "noopener,noreferrer");
|
||||||
|
}
|
||||||
|
|
||||||
|
function openHelp() {
|
||||||
|
window.open("https://gitea.cirry.cn/cirry/yurou", "_blank", "noopener,noreferrer");
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDefaultDocumentModeChange(event: Event) {
|
||||||
|
const mode = (event.target as HTMLSelectElement).value as DocumentMode;
|
||||||
|
emit("updateDefaultDocumentMode", mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.show,
|
||||||
|
(show) => {
|
||||||
|
if (!show) return;
|
||||||
|
activePage.value = props.initialPage;
|
||||||
|
updateStatus.value = "";
|
||||||
|
void loadAppVersion();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.initialPage,
|
||||||
|
(page) => {
|
||||||
|
if (props.show) activePage.value = page;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(autoUpdate, (value) => {
|
||||||
|
savePref(AUTO_UPDATE_KEY, String(value));
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(language, (value) => {
|
||||||
|
savePref(LANGUAGE_KEY, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (updateStatusTimer) clearTimeout(updateStatusTimer);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Teleport to="body">
|
||||||
|
<div
|
||||||
|
v-if="show"
|
||||||
|
class="settings-overlay"
|
||||||
|
role="presentation"
|
||||||
|
@click.self="close"
|
||||||
|
@keydown="onOverlayKeydown"
|
||||||
|
>
|
||||||
|
<section
|
||||||
|
class="settings-shell"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
:aria-label="`设置 - ${pageTitle}`"
|
||||||
|
>
|
||||||
|
<aside class="settings-menu" aria-label="设置菜单">
|
||||||
|
<div class="settings-menu-title">设置</div>
|
||||||
|
<button
|
||||||
|
v-for="item in menuItems"
|
||||||
|
:key="item.id"
|
||||||
|
class="settings-menu-item"
|
||||||
|
:class="{ 'settings-menu-item-active': activePage === item.id }"
|
||||||
|
type="button"
|
||||||
|
@click="activePage = item.id"
|
||||||
|
>
|
||||||
|
<i :class="[item.icon, 'text-base']"></i>
|
||||||
|
<span>{{ item.label }}</span>
|
||||||
|
</button>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<div class="settings-main">
|
||||||
|
<header class="settings-header">
|
||||||
|
<h2>{{ pageTitle }}</h2>
|
||||||
|
<button class="settings-close-btn" type="button" title="关闭设置" @click="close">
|
||||||
|
<i class="ri-close-line text-lg"></i>
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div v-if="activePage === 'about'" class="settings-content">
|
||||||
|
<section class="settings-about-card" aria-label="关于 yurou">
|
||||||
|
<div class="settings-row settings-row-top">
|
||||||
|
<div class="settings-copy">
|
||||||
|
<h3>版本 {{ appVersion }}</h3>
|
||||||
|
<p>安装程序版本: {{ appVersion }}</p>
|
||||||
|
<button class="settings-link" type="button" @click="openReleaseNotes">
|
||||||
|
阅读更新日志
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="settings-action-stack">
|
||||||
|
<button class="settings-primary-btn" type="button" @click="checkForUpdates">
|
||||||
|
检查更新
|
||||||
|
</button>
|
||||||
|
<span v-if="updateStatus" class="settings-status">{{ updateStatus }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-row">
|
||||||
|
<div class="settings-copy">
|
||||||
|
<h3>自动更新</h3>
|
||||||
|
<p>关闭后 {{ APP_NAME }} 将不会自动更新。</p>
|
||||||
|
</div>
|
||||||
|
<label class="settings-switch" title="自动更新">
|
||||||
|
<input v-model="autoUpdate" type="checkbox" />
|
||||||
|
<span></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-row">
|
||||||
|
<div class="settings-copy">
|
||||||
|
<h3>语言</h3>
|
||||||
|
<p>更改界面语言。</p>
|
||||||
|
<button class="settings-link" type="button">
|
||||||
|
显示如何给 {{ APP_NAME }} 添加你的母语
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<select v-model="language" class="settings-select" aria-label="界面语言">
|
||||||
|
<option value="zh-Hans">简体中文</option>
|
||||||
|
<option value="zh-Hant">繁体中文</option>
|
||||||
|
<option value="en">English</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-row">
|
||||||
|
<div class="settings-copy">
|
||||||
|
<h3>获取帮助</h3>
|
||||||
|
<p>获取关于使用 {{ APP_NAME }} 的帮助。</p>
|
||||||
|
</div>
|
||||||
|
<button class="settings-secondary-btn" type="button" @click="openHelp">
|
||||||
|
打开
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="settings-content">
|
||||||
|
<section class="settings-about-card" aria-label="编辑器设置">
|
||||||
|
<div class="settings-row settings-row-top">
|
||||||
|
<div class="settings-copy">
|
||||||
|
<h3>默认编辑模式</h3>
|
||||||
|
<p>在编辑视图下,新标签页默认采用的编辑模式。</p>
|
||||||
|
</div>
|
||||||
|
<select
|
||||||
|
class="settings-select"
|
||||||
|
aria-label="默认编辑模式"
|
||||||
|
:value="defaultDocumentMode"
|
||||||
|
@change="onDefaultDocumentModeChange"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
v-for="option in documentModeOptions"
|
||||||
|
:key="option.value"
|
||||||
|
:value="option.value"
|
||||||
|
>
|
||||||
|
{{ option.label }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</Teleport>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.settings-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 50;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 28px;
|
||||||
|
background: rgba(56, 52, 46, 0.22);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-shell {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 176px minmax(0, 1fr);
|
||||||
|
width: min(820px, calc(100vw - 48px));
|
||||||
|
min-height: 430px;
|
||||||
|
max-height: min(680px, calc(100vh - 48px));
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid #e0dbcf;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #fffdfa;
|
||||||
|
box-shadow:
|
||||||
|
0 24px 70px rgba(56, 52, 46, 0.2),
|
||||||
|
0 2px 10px rgba(56, 52, 46, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-menu {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 14px 10px;
|
||||||
|
border-right: 1px solid #e8e4da;
|
||||||
|
background: #f4f1eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-menu-title {
|
||||||
|
padding: 0 10px 8px;
|
||||||
|
color: #8c877d;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-menu-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 9px;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 34px;
|
||||||
|
padding: 7px 10px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 7px;
|
||||||
|
background: transparent;
|
||||||
|
color: #5c574e;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
background-color 0.15s ease,
|
||||||
|
color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-menu-item:hover {
|
||||||
|
background: #ede8dd;
|
||||||
|
color: #38342e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-menu-item-active {
|
||||||
|
background: #fdf0e5;
|
||||||
|
color: #bf6a3b;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-main {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
flex-direction: column;
|
||||||
|
background: #faf9f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
min-height: 54px;
|
||||||
|
padding: 0 20px;
|
||||||
|
border-bottom: 1px solid #e8e4da;
|
||||||
|
background: rgba(255, 253, 250, 0.72);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
color: #38342e;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-close-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: transparent;
|
||||||
|
color: #8c877d;
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
background-color 0.15s ease,
|
||||||
|
color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-close-btn:hover {
|
||||||
|
background: #ede8dd;
|
||||||
|
color: #38342e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-content {
|
||||||
|
min-height: 0;
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-about-card {
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #fffdfa;
|
||||||
|
box-shadow: inset 0 0 0 1px #eee9de;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
gap: 20px;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 78px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-top: 1px solid #e0dbcf;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-row-top {
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-copy {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-copy h3,
|
||||||
|
.settings-empty-page h3 {
|
||||||
|
margin: 0 0 6px;
|
||||||
|
color: #2d2a26;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-copy p,
|
||||||
|
.settings-empty-page p {
|
||||||
|
margin: 0;
|
||||||
|
color: #5c574e;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-link {
|
||||||
|
display: inline;
|
||||||
|
margin: 2px 0 0;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: #7a55d9;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.4;
|
||||||
|
text-align: left;
|
||||||
|
text-decoration: underline;
|
||||||
|
text-underline-offset: 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-link:hover {
|
||||||
|
color: #6342bd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-action-stack {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-primary-btn,
|
||||||
|
.settings-secondary-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
min-height: 30px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
background-color 0.15s ease,
|
||||||
|
box-shadow 0.15s ease,
|
||||||
|
color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-primary-btn {
|
||||||
|
min-width: 78px;
|
||||||
|
border: none;
|
||||||
|
background: #8c6be8;
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: 0 2px 7px rgba(100, 65, 190, 0.26);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-primary-btn:hover {
|
||||||
|
background: #7a55d9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-secondary-btn {
|
||||||
|
min-width: 50px;
|
||||||
|
border: 1px solid #e0dbcf;
|
||||||
|
background: #fff;
|
||||||
|
color: #38342e;
|
||||||
|
box-shadow: 0 1px 4px rgba(56, 52, 46, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-secondary-btn:hover {
|
||||||
|
background: #f4f1ea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-status {
|
||||||
|
color: #6f6a60;
|
||||||
|
font-size: 11px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-switch {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
width: 40px;
|
||||||
|
height: 22px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-switch input {
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-switch span {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #d4cfc4;
|
||||||
|
transition: background-color 0.16s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-switch span::after {
|
||||||
|
position: absolute;
|
||||||
|
top: 2px;
|
||||||
|
left: 2px;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 1px 3px rgba(56, 52, 46, 0.24);
|
||||||
|
content: "";
|
||||||
|
transition: transform 0.16s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-switch input:checked + span {
|
||||||
|
background: #8c6be8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-switch input:checked + span::after {
|
||||||
|
transform: translateX(18px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-switch input:focus-visible + span {
|
||||||
|
outline: 2px solid #d6aa8e;
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-select {
|
||||||
|
width: 158px;
|
||||||
|
height: 30px;
|
||||||
|
border: 1px solid #e0dbcf;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #fff;
|
||||||
|
color: #38342e;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 0 10px;
|
||||||
|
box-shadow: 0 1px 4px rgba(56, 52, 46, 0.1);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-select:focus {
|
||||||
|
border-color: #d6aa8e;
|
||||||
|
box-shadow: 0 0 0 3px rgba(191, 106, 59, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-empty-page {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #8c877d;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-empty-page > div {
|
||||||
|
max-width: 320px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 680px) {
|
||||||
|
.settings-overlay {
|
||||||
|
align-items: stretch;
|
||||||
|
padding: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-shell {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
width: 100%;
|
||||||
|
max-height: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-menu {
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
border-right: none;
|
||||||
|
border-bottom: 1px solid #e8e4da;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-menu-title {
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-menu-item {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-row {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-action-stack {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user