refactor(settings): remove diary and template features

This commit is contained in:
eezd committed 2026-10-24 20:14:28 +08:00
1 parent a26ebea843
commit 849cd358d0
12 files changed
+19 -173

No files matched your search

+1 -19
View File
@@ -20,8 +20,6 @@ if (!buildVersion) throw new Error('缺少应用构建版本信息')
const section = computed(() => (ws.settings === 'index' ? 'appearance' : ws.settings!))
// 手机上一次只显示分组列表或分组内容
const dailyTemplates = computed(() => ws.entries.filter(entry => !entry.dir && entry.path.endsWith('.md')))
const missingDailyTemplate = computed(() => settings.dailyTemplate && !dailyTemplates.value.some(entry => entry.path === settings.dailyTemplate))
const showList = ref(ws.settings === 'index')
const current = computed(() => SETTINGS_SECTIONS.find((s) => s.id === section.value)!)
const query = ref('')
@@ -31,7 +29,7 @@ const dialog = ref<HTMLElement>()
const content = ref<HTMLElement>()
const heading = ref<HTMLElement>()
const previousFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null
const preferenceSection = computed(() => ['appearance', 'editor', 'templates', 'shortcuts'].includes(section.value))
const preferenceSection = computed(() => ['appearance', 'editor', 'shortcuts'].includes(section.value))
const saveLabel = computed(() => settingsSave.pending ? '保存中…' : settingsSave.result === 'error' ? '保存失败' : settingsSave.result === 'saved' ? '已保存' : '修改自动保存')
async function open(id: SettingsSection) {
@@ -114,15 +112,6 @@ function changeUIFont(event: Event) {
if (value !== settings.uiFontSize) void change({ uiFontSize: value })
}
function changeFolder(key: 'dailyFolder' | 'templatesFolder', event: Event) {
const input = event.target as HTMLInputElement
const path = input.value.trim()
if ((key === 'templatesFolder' && !path) || path && path.split('/').some(part => !part || part.startsWith('.') || /[\\\0]/.test(part))) {
toast('请输入有效的仓库相对目录'); input.value = settings[key]; return
}
void change({ [key]: path })
}
// ---- 修改登录密码 ----
const pw = ref({ current: '', next: '', confirm: '' })
const pwError = ref('')
@@ -196,13 +185,6 @@ async function changePassword() {
<SettingRow id="tab-size" control="rm-tab-size" resettable @reset="change({ tabSize: DEFAULTS.tabSize })"><select id="rm-tab-size" :value="settings.tabSize" @change="change({ tabSize: Number(($event.target as HTMLSelectElement).value) })"><option v-for="n in TAB_SIZES" :key="n" :value="n">{{ n }} 列</option></select></SettingRow>
</template>
<template v-else-if="section === 'templates'">
<SettingRow id="daily-folder" control="rm-daily-folder"><input id="rm-daily-folder" :value="settings.dailyFolder" placeholder="根目录" @change="changeFolder('dailyFolder', $event)" /></SettingRow>
<SettingRow id="templates-folder" control="rm-templates-folder"><input id="rm-templates-folder" :value="settings.templatesFolder" @change="changeFolder('templatesFolder', $event)" /></SettingRow>
<SettingRow id="daily-template" control="rm-daily-template"><select id="rm-daily-template" :value="settings.dailyTemplate" @change="change({ dailyTemplate: ($event.target as HTMLSelectElement).value })"><option value="">空白笔记</option><option v-for="entry in dailyTemplates" :key="entry.path" :value="entry.path">{{ entry.path }}</option><option v-if="missingDailyTemplate" :value="settings.dailyTemplate">{{ settings.dailyTemplate }}(不存在)</option></select></SettingRow>
<p class="rm-share-note">模板支持 <code v-pre>{{date}}</code>、<code v-pre>{{time}}</code>、<code v-pre>{{title}}</code>。</p>
</template>
<template v-else-if="section === 'account'">
<form id="rm-setting-password" class="rm-auth-card rm-settings-form" tabindex="-1" @submit.prevent="changePassword">
<h3>修改登录密码</h3>
-35
View File
@@ -1,35 +0,0 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import Icon from './Icon.vue'
import { settings } from './settings'
import { createFromTemplate } from './templates'
import { createNote, toast, ws } from './workspace'
const templates = computed(() => ws.entries.filter(entry => !entry.dir && entry.path.startsWith(settings.templatesFolder + '/') && entry.path.endsWith('.md')))
const selected = ref('')
const path = ref('未命名.md')
const busy = ref(false)
async function create() {
if (!path.value.endsWith('.md') || path.value.split('/').some(part => !part || part.startsWith('.') || /[\\\0]/.test(part))) { toast('请输入有效的 Markdown 笔记路径'); return }
busy.value = true
try { if (await createFromTemplate(path.value, selected.value)) ws.templateOpen = false }
catch (error) { toast('读取模板失败:' + (error instanceof Error ? error.message : '网络错误')) }
finally { busy.value = false }
}
</script>
<template>
<div class="rm-dialog-backdrop" @click.self="!busy && (ws.templateOpen = false)" @keydown.esc.stop="!busy && (ws.templateOpen = false)">
<section class="rm-dialog rm-template-dialog" role="dialog" aria-label="从模板新建">
<div class="rm-dialog-head"><h2>从模板开始</h2><button class="rm-icon-btn" title="关闭" :disabled="busy" @click="ws.templateOpen = false"><Icon name="close" /></button></div>
<p class="rm-share-note">模板是 {{ settings.templatesFolder }}/ 下的 Markdown 文件。支持 <code v-pre>{{date}}</code>、<code v-pre>{{time}}</code> 和 <code v-pre>{{title}}</code>,图片沿用原模板资源。</p>
<form @submit.prevent="create">
<label>选择模板<select v-model="selected"><option value="">空白笔记</option><option v-for="template in templates" :key="template.path" :value="template.path">{{ template.path }}</option></select></label>
<label>笔记路径<input v-model="path" aria-label="笔记路径" required /></label>
<div class="rm-template-actions"><button class="rm-btn" type="button" :disabled="busy" @click="ws.templateOpen = false; createNote(settings.templatesFolder)">新建模板文件</button><button class="rm-btn rm-btn-primary" :disabled="busy">创建笔记</button></div>
</form>
</section>
</div>
</template>
<style scoped>
.rm-template-dialog { width: min(520px, calc(100vw - 24px)); overflow-y: auto; } form { display: grid; gap: 20px; padding: 20px; } label { display: grid; gap: 9px; font-size: var(--rm-ui-font-size); } input, select { padding: 10px; border: 1px solid var(--rm-divider); border-radius: 8px; background: var(--rm-bg); color: var(--rm-text); font: inherit; } .rm-template-actions { display: flex; justify-content: space-between; gap: 12px; }
@media(max-width:767px), (pointer:coarse) { input, select { font-size: max(16px, var(--rm-ui-font-size)); } }
</style>
-2
View File
@@ -4,7 +4,6 @@ import { api, ApiError } from './api'
import { registerCommand } from './commands'
import EditorPane from './EditorPane.vue'
import KnowledgePanel from './KnowledgePanel.vue'
import TemplateDialog from './TemplateDialog.vue'
import EditorChrome from './EditorChrome.vue'
import Icon from './Icon.vue'
import SelectionToolbar from './SelectionToolbar.vue'
@@ -133,7 +132,6 @@ onBeforeUnmount(() => {
<OutlinePanel v-if="ws.outlineOpen && !ws.focusMode && ws.active && ws.view === 'notes' && ws.images === null" />
</Transition>
</div>
<TemplateDialog v-if="ws.templateOpen" />
<QuickSwitcher v-if="ws.switcher !== null" />
<SettingsDialog v-if="ws.settings" />
<ImportDialog v-if="ws.importing !== null" :dir="ws.importing" @close="ws.importing = null" />
+14 -2
View File
@@ -41,13 +41,26 @@ async function search(selector: string, query: string) {
}
describe('设置查找与重置', () => {
it('移除日记和模板设置、搜索与命令入口', async () => {
expect(searchSettings('日记')).toEqual([])
expect(searchSettings('日历')).toEqual([])
expect(DEFAULTS).not.toHaveProperty('dailyFolder')
expect(searchSettings('模板')).toEqual([])
expect(DEFAULTS).not.toHaveProperty('templatesFolder')
ws.settings = 'appearance'
mount(SettingsDialog)
expect(host!.textContent).not.toContain('日记')
expect(host!.querySelector('#rm-templates-folder')).toBeNull()
expect(host!.textContent).not.toContain('模板')
})
it('设置和命令不再提供分享入口', async () => {
expect(searchSettings('分享')).toEqual([])
expect(allCommands().map(command => command.id)).not.toContain('note.share')
expect(allCommands().map(command => command.id)).not.toContain('app.shares')
ws.settings = 'appearance'
mount(SettingsDialog)
expect(host!.querySelectorAll('.rm-settings-tab')).toHaveLength(9)
expect(host!.querySelectorAll('.rm-settings-tab')).toHaveLength(8)
expect(host!.textContent).not.toContain('分享')
})
@@ -55,7 +68,6 @@ describe('设置查找与重置', () => {
expect(searchSettings(' ')).toEqual([])
expect(searchSettings('CSS 高级')).toEqual([expect.objectContaining({ id: 'css-snippets', section: 'appearance' })])
expect(searchSettings('api')).toEqual(expect.arrayContaining([expect.objectContaining({ section: 'tokens', title: 'API 令牌' }), expect.objectContaining({ id: 'openapi' })]))
settings.dailyFolder = 'private-note-folder'
settings.cssSnippets = [{ id: 'private', name: 'private-snippet-name', enabled: true, css: ':root { --private: 1 }' }]
expect(searchSettings('private')).toEqual([])
})
+1 -4
View File
@@ -17,7 +17,7 @@ export const CONTENT_WIDTHS = [
export const SIDEBAR_WIDTH = { min: 200, max: 480 }
export const DEFAULTS = { dailyFolder: 'Daily', templatesFolder: 'Templates', dailyTemplate: '', selectionToolbar: true, ...DISPLAY_DEFAULTS, sidebarWidth: 288, spellcheck: false, tabSize: 4 }
export const DEFAULTS = { selectionToolbar: true, ...DISPLAY_DEFAULTS, sidebarWidth: 288, spellcheck: false, tabSize: 4 }
export type Settings = typeof DEFAULTS & Appearance & { shortcuts: Record<string, string[]> }
const CACHE_KEY = 'rawmark.settings'
@@ -36,9 +36,6 @@ export class SettingsAppearanceError extends Error {
function sanitize(raw: Record<string, unknown>): Settings {
const pick = <T>(v: unknown, allowed: T[], def: T) => (allowed.includes(v as T) ? (v as T) : def)
return {
dailyFolder: typeof raw.dailyFolder === 'string' ? raw.dailyFolder : DEFAULTS.dailyFolder,
templatesFolder: typeof raw.templatesFolder === 'string' && raw.templatesFolder !== '' ? raw.templatesFolder : DEFAULTS.templatesFolder,
dailyTemplate: typeof raw.dailyTemplate === 'string' ? raw.dailyTemplate : DEFAULTS.dailyTemplate,
...sanitizeDisplaySettings(raw),
selectionToolbar: typeof raw.selectionToolbar === 'boolean' ? raw.selectionToolbar : true,
shortcuts: sanitizeShortcuts(raw.shortcuts),
-4
View File
@@ -4,7 +4,6 @@ import type { SettingsSection } from './workspace'
export const SETTINGS_SECTIONS: { id: SettingsSection; title: string; icon: IconName; description: string }[] = [
{ id: 'appearance', title: '外观', icon: 'palette', description: '界面字号、主题、明暗模式与 CSS 片段' },
{ id: 'editor', title: '编辑器', icon: 'edit', description: '正文排版、代码字体与编辑行为' },
{ id: 'templates', title: '日记与模板', icon: 'calendar', description: '日记目录、模板目录与日记模板' },
{ id: 'shortcuts', title: '快捷键', icon: 'command', description: '查找命令,添加、移除或重置快捷键' },
{ id: 'plugins', title: '插件', icon: 'grid', description: '安装、启停和管理插件及其设置页' },
{ id: 'tokens', title: 'API 令牌', icon: 'key', description: '创建或撤销 REST API 访问令牌' },
@@ -30,9 +29,6 @@ export const SETTING_ITEMS = {
'spellcheck': { section: 'editor', title: '拼写检查', description: '使用浏览器的拼写检查,检查语言取决于浏览器设置。', keywords: '语言 拼写' },
'tab-size': { section: 'editor', title: '制表符宽度', description: '仅调整 Tab 字符的显示宽度,不改写正文;Tab 键仍按两个空格缩进。', keywords: 'Tab 缩进 空格' },
'typography-preview': { section: 'editor', title: '排版预览', description: '查看当前正文与代码字体的显示效果。', keywords: '字号 行高 段距 预览' },
'daily-folder': { section: 'templates', title: '日记目录', description: '仓库相对目录;留空时保存在根目录,修改不会移动已有日记。', keywords: '路径 Daily' },
'templates-folder': { section: 'templates', title: '模板目录', description: '仓库相对目录,修改不会移动已有模板。', keywords: '路径 Templates' },
'daily-template': { section: 'templates', title: '日记模板', description: '日记按本地日期命名,已有日记直接打开。', keywords: '空白笔记 日期' },
'password': { section: 'account', title: '修改登录密码', description: '修改后其他设备需要重新登录。', keywords: '账户 安全 鉴权' },
'logout': { section: 'account', title: '退出登录', description: '退出当前设备的登录会话。', keywords: '账户 会话' },
'install': { section: 'about', title: '安装应用', description: '浏览器支持安装时,可将 RawMark 添加到桌面或主屏幕。', keywords: 'PWA 安装' },
-27
View File
@@ -1,27 +0,0 @@
import { localDate, renderTemplate } from '../markdown/template'
import { api } from './api'
import { joinPath } from './paths'
import { settings } from './settings'
import { createNoteAt, openNote, toast, ws } from './workspace'
export async function createFromTemplate(path: string, template: string) {
if (ws.entries.some(entry => entry.path === path)) { toast('目标笔记已存在'); return false }
const content = template ? renderTemplate((await api.readFile(template)).content, template, path) : ''
const created = await createNoteAt(path, content)
if (created) ws.sidebarOpen = false
return created
}
let openingDaily = false
export async function openDailyNote() {
// 模板读取也属于打开日记的过程,此时尚未加入乐观目录。
if (openingDaily) return
openingDaily = true
try {
const path = joinPath(settings.dailyFolder, localDate() + '.md')
if (ws.entries.some(entry => !entry.dir && entry.path === path)) { await openNote(path); return }
await createFromTemplate(path, settings.dailyTemplate)
}
catch (error) { toast('创建日记失败:' + (error instanceof Error ? error.message : '网络错误')) }
finally { openingDaily = false }
}
-29
View File
@@ -7,9 +7,6 @@ import { drafts, type Draft } from './drafts'
import { connectEvents } from './events'
import { pluginEvents } from '../plugins/registry'
import { attachView, closeTab, createNoteAt, editorStateOf, flush, keepLocalVersion, importNotes, move, remove, readyToReload, openNote, refreshTree, replaceImage, restore, startWorkspace, stopWorkspace, storeViewState, ws } from './workspace'
import { openDailyNote } from './templates'
import { settings } from './settings'
import { localDate } from '../markdown/template'
vi.mock('./events', () => ({ connectEvents: vi.fn(() => () => {}) }))
@@ -143,32 +140,6 @@ describe('笔记打开与创建', () => {
expect(ws.toasts.some(toast => toast.text.includes('打开失败:文件不存在'))).toBe(false)
})
it('日记模板读取期间重复打开只创建一个标签,之后仍可再次打开', async () => {
const previousFolder = settings.dailyFolder, previousTemplate = settings.dailyTemplate
settings.dailyFolder = 'Daily-review'
settings.dailyTemplate = 'Templates/daily.md'
const path = settings.dailyFolder + '/' + localDate() + '.md'
let finish!: (value: { content: string; rev: string }) => void
vi.spyOn(api, 'readFile').mockImplementation(() => new Promise(resolve => { finish = resolve }))
vi.spyOn(api, 'writeFile').mockResolvedValue('r1')
vi.spyOn(drafts, 'delete').mockResolvedValue(undefined)
try {
const first = openDailyNote(), second = openDailyNote()
finish({ content: '# {{date}}\n\n日记', rev: 'template-r1' })
await Promise.all([first, second])
expect(ws.tabs.filter(tab => tab.path === path)).toHaveLength(1)
expect(editorStateOf(path)?.doc.toString()).toContain('日记')
await closeTab(path)
vi.mocked(api.readFile).mockResolvedValue({ content: '重新打开的日记', rev: 'r1' })
await openDailyNote()
expect(ws.tabs.filter(tab => tab.path === path)).toHaveLength(1)
expect(editorStateOf(path)?.doc.toString()).toBe('重新打开的日记')
await closeTab(path)
} finally {
settings.dailyFolder = previousFolder
settings.dailyTemplate = previousTemplate
}
})
})
describe('保存与关闭', () => {
+2 -3
View File
@@ -35,7 +35,7 @@ export interface Tab {
conflictRev?: string
}
export type SettingsSection = 'appearance' | 'editor' | 'templates' | 'account' | 'shortcuts' | 'tokens' | 'trash' | 'plugins' | 'about'
export type SettingsSection = 'appearance' | 'editor' | 'account' | 'shortcuts' | 'tokens' | 'trash' | 'plugins' | 'about'
export interface Toast {
id: number
@@ -78,7 +78,6 @@ export const ws = reactive({
sourceMode: false,
focusMode: false,
view: 'notes' as 'notes' | 'graph' | 'tags',
templateOpen: false,
// 最近打开的笔记,最新的在前
recent: JSON.parse(localStorage.getItem(RECENT_KEY) ?? '[]') as string[],
toasts: [] as Toast[],
@@ -1078,7 +1077,7 @@ registerBuiltinCommands()
function registerBuiltinCommands() {
for (const command of editorCommands) registerCommand({ ...command,
when: () => ws.view === 'notes' && ws.settings === null && ws.images === null && !ws.templateOpen && ws.active !== null && !!view,
when: () => ws.view === 'notes' && ws.settings === null && ws.images === null && ws.active !== null && !!view,
run: () => { if (view) { command.run(view); view.focus() } },
})
const hasNote = () => ws.active !== null