The interface was optimized and some bugs were fixed

This commit is contained in:
maigamo 2025-06-23 11:21:11 +08:00
parent 2d27bde9d7
commit e717e56245
7 changed files with 465 additions and 28 deletions

View file

@ -16,6 +16,16 @@ export type TranslationKey =
| 'settings.mode.none'
| 'settings.mode.daily'
| 'settings.mode.workday'
// 文件生成模式相关
| 'settings.fileGenerationMode'
| 'settings.fileGenerationMode.desc'
| 'settings.mode.monthly'
| 'settings.mode.dailyFile'
| 'settings.dailyFilePrefix'
| 'settings.dailyFilePrefix.desc'
| 'notification.dailyFileExists'
| 'settings.language'
| 'settings.language.desc'
| 'settings.language.auto'
@ -145,6 +155,16 @@ const translationsZH: Record<TranslationKey, string> = {
'settings.mode.none': '❌ 关闭',
'settings.mode.daily': '📆 每天',
'settings.mode.workday': '💼 仅工作日',
// 文件生成模式相关
'settings.fileGenerationMode': '📄 文件生成模式',
'settings.fileGenerationMode.desc': '选择任务文件的生成方式',
'settings.mode.monthly': '📅 月度文件',
'settings.mode.dailyFile': '📆 日度文件',
'settings.dailyFilePrefix': '🏷️ 文件名前缀',
'settings.dailyFilePrefix.desc': '日度文件模式下的文件名前缀(可选)',
'notification.dailyFileExists': '今日任务文件已存在',
'settings.language': '🔤 界面语言',
'settings.language.desc': '选择插件界面显示的语言',
'settings.language.auto': '🔍 自动检测',
@ -221,6 +241,16 @@ const translationsEN: Record<TranslationKey, string> = {
'settings.mode.none': '❌ Off',
'settings.mode.daily': '📆 Daily',
'settings.mode.workday': '💼 Workdays only',
// 文件生成模式相关
'settings.fileGenerationMode': '📄 File Generation Mode',
'settings.fileGenerationMode.desc': 'Choose how task files are generated',
'settings.mode.monthly': '📅 Monthly Files',
'settings.mode.dailyFile': '📆 Daily Files',
'settings.dailyFilePrefix': '🏷️ File Name Prefix',
'settings.dailyFilePrefix.desc': 'Optional prefix for daily file names',
'notification.dailyFileExists': 'Today\'s task file already exists',
'settings.language': '🔤 Interface language',
'settings.language.desc': 'Select the language for the plugin interface',
'settings.language.auto': '🔍 Auto detect',

View file

@ -7,6 +7,14 @@ export enum AutoGenerateMode {
WORKDAY = 'workday' // 仅工作日自动生成
}
/**
*
*/
export enum FileGenerationMode {
MONTHLY = 'monthly', // 月度文件模式(默认)
DAILY = 'daily' // 日度文件模式
}
/**
*
*/
@ -25,6 +33,10 @@ export interface DailyTaskSettings {
autoGenerateMode: AutoGenerateMode; // 自动生成模式
language: string; // 界面语言
// 文件生成模式配置
fileGenerationMode: FileGenerationMode; // 文件生成模式
dailyFilePrefix: string; // 日度文件前缀(仅日度模式使用)
// 模板配置
templateZh: string; // 中文任务模板
templateEn: string; // 英文任务模板
@ -87,6 +99,8 @@ export const DEFAULT_SETTINGS: DailyTaskSettings = {
rootDir: 'DailyTasks',
autoGenerateMode: AutoGenerateMode.WORKDAY,
language: Language.AUTO,
fileGenerationMode: FileGenerationMode.MONTHLY, // 默认为月度文件模式
dailyFilePrefix: '', // 默认无前缀
templateZh: DEFAULT_TEMPLATE_ZH,
templateEn: DEFAULT_TEMPLATE_EN,
customTemplate: '', // 默认为空,表示使用语言相关的默认模板

View file

@ -1,5 +1,5 @@
import { App, ButtonComponent, DropdownComponent, Notice, Plugin, PluginSettingTab, Setting, TextAreaComponent, TextComponent, ToggleComponent } from 'obsidian';
import { AutoGenerateMode, DEFAULT_SETTINGS, DEFAULT_TEMPLATE_EN, DEFAULT_TEMPLATE_ZH, DailyTaskSettings, Language } from '../models/settings';
import { AutoGenerateMode, DEFAULT_SETTINGS, DEFAULT_TEMPLATE_EN, DEFAULT_TEMPLATE_ZH, DailyTaskSettings, Language, FileGenerationMode } from '../models/settings';
import { getTranslation, setCurrentLanguage } from '../i18n/i18n';
import { renderTemplate } from '../utils/templateEngine';
import { TaskGenerator } from '../taskGenerator';
@ -45,12 +45,19 @@ export class DailyTaskSettingTab extends PluginSettingTab {
// 目录输入框
rootDirInput: TextComponent | null = null;
// 日度文件前缀输入框
dailyFilePrefixInput: TextComponent | null = null;
dailyFilePrefixSetting: HTMLElement | null = null;
// 标记设置是否已修改但未保存
dirtySettings: boolean = false;
// 自动保存目录的方法
autoSaveRootDir: (value: string) => Promise<void>;
// 自动保存日度文件前缀的方法
autoSaveDailyFilePrefix: (value: string) => Promise<void>;
constructor(app: App, plugin: Plugin) {
super(app, plugin);
this.plugin = plugin;
@ -210,6 +217,141 @@ export class DailyTaskSettingTab extends PluginSettingTab {
toggleContainer.appendChild(option);
});
// 文件生成模式设置
const fileGenSetting = new Setting(containerEl)
.setName(getTranslation('settings.fileGenerationMode'))
.setDesc(getTranslation('settings.fileGenerationMode.desc'));
// 创建文件生成模式选择器(二选滑块)
const fileGenToggleContainer = document.createElement('div');
fileGenToggleContainer.classList.add('mode-toggle-container');
fileGenSetting.controlEl.appendChild(fileGenToggleContainer);
const fileGenModes = [
{ value: FileGenerationMode.MONTHLY, label: getTranslation('settings.mode.monthly') },
{ value: FileGenerationMode.DAILY, label: getTranslation('settings.mode.dailyFile') }
];
// 文件生成模式滑块指示器
const fileGenSlider = document.createElement('div');
fileGenSlider.classList.add('mode-toggle-slider');
fileGenToggleContainer.appendChild(fileGenSlider);
// 更新文件生成模式滑块位置
const updateFileGenSlider = (index: number) => {
// 移除之前的位置类(二选滑块)
fileGenSlider.classList.remove('slider-position-0', 'slider-position-1');
// 添加新的位置类
fileGenSlider.classList.add(`slider-position-${index}`);
};
// 设置当前文件生成模式
let currentFileGenModeIndex = fileGenModes.findIndex(mode => mode.value === settings.fileGenerationMode);
if (currentFileGenModeIndex === -1) {
// 如果没有找到有效的模式,设置为月度模式(默认)
currentFileGenModeIndex = 0;
this.settingsManager.updateSettings({ fileGenerationMode: FileGenerationMode.MONTHLY });
}
updateFileGenSlider(currentFileGenModeIndex);
// 创建文件生成模式选项
fileGenModes.forEach((mode, index) => {
const option = document.createElement('div');
option.classList.add('mode-toggle-option');
option.textContent = mode.label;
if (mode.value === settings.fileGenerationMode) {
option.classList.add('active');
}
option.addEventListener('click', async () => {
// 更新视觉状态
fileGenToggleContainer.querySelectorAll('.mode-toggle-option').forEach(el => {
el.classList.remove('active');
});
option.classList.add('active');
// 更新滑块位置
updateFileGenSlider(index);
// 保存设置
await this.settingsManager.updateSettings({ fileGenerationMode: mode.value });
// 更新日度文件前缀设置的显示状态
this.updateDailyFilePrefixVisibility(mode.value);
});
fileGenToggleContainer.appendChild(option);
});
// 日度文件前缀设置
const dailyFilePrefixSetting = new Setting(containerEl)
.setName(getTranslation('settings.dailyFilePrefix'))
.setDesc(getTranslation('settings.dailyFilePrefix.desc'));
this.dailyFilePrefixSetting = dailyFilePrefixSetting.controlEl.parentElement;
// 创建前缀输入框容器
const prefixInputContainer = document.createElement('div');
prefixInputContainer.classList.add(InputContainerCSS);
dailyFilePrefixSetting.controlEl.appendChild(prefixInputContainer);
this.dailyFilePrefixInput = new TextComponent(prefixInputContainer)
.setValue(settings.dailyFilePrefix || '')
.onChange(async (value) => {
// 设置已更改,准备自动保存
this.dirtySettings = true;
// 启动自动保存定时器
this.autoSaveDailyFilePrefix(value);
});
// 给input元素设置类和placeholder属性
if (this.dailyFilePrefixInput && this.dailyFilePrefixInput.inputEl) {
this.dailyFilePrefixInput.inputEl.classList.add(InputCSS);
this.dailyFilePrefixInput.inputEl.placeholder = getTranslation('settings.dailyFilePrefix.desc');
}
// 添加前缀自动保存指示器
const prefixSaveIndicator = document.createElement('div');
prefixSaveIndicator.classList.add(SaveIndicatorCSS);
prefixInputContainer.appendChild(prefixSaveIndicator);
// 创建前缀保存成功图标
const prefixSaveSuccessIcon = document.createElement('span');
prefixSaveSuccessIcon.classList.add('svg-icon', 'lucide-check', SuccessIconCSS);
prefixSaveIndicator.appendChild(prefixSaveSuccessIcon);
// 记录前缀自动保存定时器
let prefixAutoSaveTimer: number | null = null;
// 前缀自动保存方法
this.autoSaveDailyFilePrefix = async (value: string) => {
// 清除之前的定时器
if (prefixAutoSaveTimer !== null) {
window.clearTimeout(prefixAutoSaveTimer);
}
// 设置新的定时器延迟800ms保存在用户停止输入后
prefixAutoSaveTimer = window.setTimeout(async () => {
// 实际保存设置
await this.settingsManager.updateSettings({ dailyFilePrefix: value });
this.dirtySettings = false;
// 显示保存成功的视觉反馈
prefixSaveIndicator.classList.add('save-indicator-visible');
prefixSaveIndicator.classList.remove('save-indicator-hidden');
window.setTimeout(() => {
prefixSaveIndicator.classList.remove('save-indicator-visible');
prefixSaveIndicator.classList.add('save-indicator-hidden');
}, 1500);
}, 800);
};
// 初始化日度文件前缀设置的可见性
this.updateDailyFilePrefixVisibility(settings.fileGenerationMode);
// 语言设置
// TODO: 应该使用Obsidian API的app.i18n.locale获取系统语言设置
// 需要在manifest.json中设置minAppVersion: "1.8.0"或更高
@ -398,9 +540,9 @@ export class DailyTaskSettingTab extends PluginSettingTab {
resetBtn.buttonEl.addClass('daily-task-button-common');
resetBtn.buttonEl.addClass('daily-task-button-lg');
// 添加重置图标
// 添加重置图标 - 使用撤销图标更直观
const resetIcon = document.createElement('span');
resetIcon.classList.add('svg-icon', 'lucide-refresh-cw');
resetIcon.classList.add('svg-icon', 'lucide-undo2');
resetBtn.buttonEl.prepend(resetIcon);
// 添加重置事件
@ -447,9 +589,9 @@ export class DailyTaskSettingTab extends PluginSettingTab {
resetDefaultBtn.buttonEl.addClass('daily-task-button-common');
resetDefaultBtn.buttonEl.addClass('daily-task-button-lg');
// 添加重置图标
// 添加重置图标 - 使用返回主页图标表示恢复到默认状态
const resetIcon2 = document.createElement('span');
resetIcon2.classList.add('svg-icon', 'lucide-refresh-cw');
resetIcon2.classList.add('svg-icon', 'lucide-home');
resetDefaultBtn.buttonEl.prepend(resetIcon2);
// 为全局重置按钮添加事件处理
@ -506,6 +648,20 @@ export class DailyTaskSettingTab extends PluginSettingTab {
this.addTaskButton.buttonEl.prepend(calendarIcon);
}
/**
*
* @param mode
*/
private updateDailyFilePrefixVisibility(mode: FileGenerationMode): void {
if (!this.dailyFilePrefixSetting) return;
if (mode === FileGenerationMode.DAILY) {
this.dailyFilePrefixSetting.style.display = 'block';
} else {
this.dailyFilePrefixSetting.style.display = 'none';
}
}
/**
*
*/
@ -725,4 +881,4 @@ export class SettingsManager {
hasCustomTemplate(): boolean {
return !!this.settings.customTemplate;
}
}
}

View file

@ -1,9 +1,10 @@
import { App, Notice, TAbstractFile, TFile, Vault } from "obsidian";
import { getTranslation } from "./i18n/i18n";
import { SettingsManager } from "./settings/settings";
import { appendToFile, ensureFileExists, ensureFolderExists, getTaskFilePath, todayTaskExists, fileContains, getYesterdayDate, getYesterdayTaskFilePath, extractTasksForDate, analyzeTaskCompletion, yesterdayStatisticsExists, generateStatisticsContent } from "./utils/fileUtils";
import { appendToFile, ensureFileExists, ensureFolderExists, getTaskFilePath, todayTaskExists, fileContains, getYesterdayDate, getYesterdayTaskFilePath, extractTasksForDate, analyzeTaskCompletion, yesterdayStatisticsExists, generateStatisticsContent, getTaskFilePathByMode, getDailyTaskFilePath, getDailyTaskFolderPaths, dailyTaskFileExists, createDailyTaskFile } from "./utils/fileUtils";
import { renderTemplate } from "./utils/templateEngine";
import { getCurrentDate } from "./utils/dateUtils";
import { FileGenerationMode } from "./models/settings";
/**
*
@ -21,12 +22,28 @@ export class TaskGenerator {
}
/**
*
*
* @param openFile
* @param quietMode
* @returns
*/
async generateDailyTask(openFile: boolean = true, quietMode: boolean = false): Promise<boolean> {
const settings = this.settingsManager.getSettings();
if (settings.fileGenerationMode === FileGenerationMode.MONTHLY) {
return await this.generateMonthlyTask(openFile, quietMode);
} else {
return await this.generateDailyTaskFile(openFile, quietMode);
}
}
/**
*
* @param openFile
* @param quietMode
* @returns
*/
private async generateMonthlyTask(openFile: boolean = true, quietMode: boolean = false): Promise<boolean> {
try {
const settings = this.settingsManager.getSettings();
const rootDir = settings.rootDir.trim() || 'DailyTasks'; // 使用默认目录
@ -145,6 +162,107 @@ export class TaskGenerator {
return false;
}
}
/**
*
* @param openFile
* @param quietMode
* @returns
*/
private async generateDailyTaskFile(openFile: boolean = true, quietMode: boolean = false): Promise<boolean> {
try {
const settings = this.settingsManager.getSettings();
const rootDir = settings.rootDir.trim() || 'DailyTasks';
const prefix = settings.dailyFilePrefix || '';
// 获取日度任务文件路径
const filePath = getDailyTaskFilePath(rootDir, prefix);
// 检查文件是否已存在
const fileExists = await dailyTaskFileExists(this.vault, rootDir, prefix);
if (fileExists) {
// 如果需要打开文件
if (openFile) {
// 显示提示并打开文件
this.showWarningNotice(`📌 ${getTranslation('notification.dailyFileExists')}`);
const file = this.vault.getAbstractFileByPath(filePath);
if (file && file instanceof TFile) {
const leaf = this.app.workspace.getLeaf();
await leaf.openFile(file);
}
}
return true; // 文件已存在,视为成功
}
// 获取年份和月份文件夹路径
const { yearPath, monthPath } = getDailyTaskFolderPaths(rootDir);
// 确保根目录存在
const rootCreated = await ensureFolderExists(this.vault, rootDir);
if (!rootCreated) {
throw new Error(`无法访问根目录: ${rootDir},可能是存在同名文件或权限问题`);
}
// 确保年份目录存在
const yearCreated = await ensureFolderExists(this.vault, yearPath);
if (!yearCreated) {
throw new Error(`无法访问年份目录: ${yearPath},可能是存在同名文件或权限问题`);
}
// 确保月份目录存在
const monthCreated = await ensureFolderExists(this.vault, monthPath);
if (!monthCreated) {
throw new Error(`无法访问月份目录: ${monthPath},可能是存在同名文件或权限问题`);
}
// 获取任务模板内容
let template = '';
if (this.settingsManager.hasCustomTemplate()) {
// 使用用户自定义模板
template = this.settingsManager.getSettings().customTemplate;
} else {
// 使用语言相关的默认模板
template = this.settingsManager.getTemplateByLanguage();
}
// 渲染模板
const renderedContent = renderTemplate(template);
// 创建日度任务文件(不追加,直接创建完整文件)
const success = await createDailyTaskFile(this.vault, filePath, renderedContent);
if (success) {
// 如果需要打开文件
if (openFile) {
// 打开创建的文件
const file = this.vault.getAbstractFileByPath(filePath);
if (file && file instanceof TFile) {
const leaf = this.app.workspace.getLeaf();
await leaf.openFile(file);
// 延迟一下再显示通知,确保文件已经打开
setTimeout(() => {
this.showSuccessNotice(`${getTranslation('notification.taskAdded')}`);
}, 300);
} else {
throw new Error(`文件创建成功但无法打开: ${filePath}`);
}
}
} else {
throw new Error(`无法创建日度任务文件: ${filePath}`);
}
return success;
} catch (error) {
// 显示错误通知
const errorMsg = error instanceof Error ? error.message : String(error);
this.showErrorNotice(`${getTranslation('notification.error')} ${errorMsg}`);
return false;
}
}
/**
*

View file

@ -36,20 +36,21 @@ export function setTextContentByLines(targetEl: HTMLElement, textContent: string
targetEl.removeChild(targetEl.firstChild);
}
// 将内容按行分割
const lines = textContent.split('\n');
// 创建预格式化的div来保持原始格式
const preEl = document.createElement('div');
preEl.style.whiteSpace = 'pre-wrap';
preEl.style.wordWrap = 'break-word';
preEl.style.textAlign = 'left';
preEl.style.margin = '0';
preEl.style.padding = '0';
preEl.style.fontFamily = 'inherit';
preEl.style.lineHeight = 'inherit';
// 为每行创建一个div
lines.forEach((line, index) => {
const lineEl = document.createElement('div');
lineEl.textContent = line;
targetEl.appendChild(lineEl);
// 如果不是最后一行,添加换行符
if (index < lines.length - 1) {
targetEl.appendChild(document.createElement('br'));
}
});
// 直接设置文本内容,保持原始格式
preEl.textContent = textContent;
// 添加到目标元素
targetEl.appendChild(preEl);
}
/**

View file

@ -3,6 +3,7 @@ import { getCurrentDate, getCurrentMonth, getCurrentYear, getLocalizedMonthName,
import { getTranslation, TranslationKey } from '../i18n/i18n';
import { Notice } from 'obsidian';
import { DEFAULT_SETTINGS } from '../settings/index';
import { FileGenerationMode } from '../models/settings';
/**
*
@ -175,6 +176,113 @@ export function getTaskFilePath(rootDir: string): string {
return normalizePath(`${rootDir}/${yearDir}/${monthFile}`);
}
/**
*
* @param rootDir
* @param mode
* @param prefix 使
* @returns
*/
export function getTaskFilePathByMode(rootDir: string, mode: FileGenerationMode, prefix: string = ''): string {
if (mode === FileGenerationMode.MONTHLY) {
return getTaskFilePath(rootDir);
} else {
return getDailyTaskFilePath(rootDir, prefix);
}
}
/**
*
* @param rootDir
* @param prefix
* @returns rootDir/year/monthFolder/prefix+date.md
*/
export function getDailyTaskFilePath(rootDir: string, prefix: string = ''): string {
const now = new Date();
const year = now.getFullYear();
const month = now.getMonth() + 1; // 1-12
const day = now.getDate();
const yearDir = year.toString();
// 使用数字格式的月份文件夹,保持两位数格式
const monthFolder = month.toString().padStart(2, '0');
// 生成日期字符串 YYYY-MM-DD
const dateStr = `${year}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}`;
// 构建文件名:前缀 + 日期 + .md
const fileName = prefix.trim() ? `${prefix.trim()}${dateStr}.md` : `${dateStr}.md`;
return normalizePath(`${rootDir}/${yearDir}/${monthFolder}/${fileName}`);
}
/**
*
* @param rootDir
* @returns
*/
export function getDailyTaskFolderPaths(rootDir: string): { yearPath: string; monthPath: string } {
const now = new Date();
const year = now.getFullYear();
const month = now.getMonth() + 1; // 1-12
const yearDir = year.toString();
const monthFolder = month.toString().padStart(2, '0');
const yearPath = normalizePath(`${rootDir}/${yearDir}`);
const monthPath = normalizePath(`${rootDir}/${yearDir}/${monthFolder}`);
return { yearPath, monthPath };
}
/**
*
* @param vault Obsidian文件系统
* @param rootDir
* @param prefix
* @returns
*/
export async function dailyTaskFileExists(vault: Vault, rootDir: string, prefix: string = ''): Promise<boolean> {
const filePath = getDailyTaskFilePath(rootDir, prefix);
const file = vault.getAbstractFileByPath(filePath);
return file !== null && file instanceof TFile;
}
/**
*
* @param vault Obsidian文件系统
* @param filePath
* @param content
* @returns
*/
export async function createDailyTaskFile(vault: Vault, filePath: string, content: string): Promise<boolean> {
try {
const normalizedPath = normalizePath(filePath);
// 确保文件夹结构存在
const folderPath = normalizedPath.substring(0, normalizedPath.lastIndexOf('/'));
if (folderPath) {
const folderExists = await ensureFolderExists(vault, folderPath);
if (!folderExists) {
return false;
}
}
// 检查文件是否已存在
const existingFile = vault.getAbstractFileByPath(normalizedPath);
if (existingFile) {
return false; // 文件已存在,不重复创建
}
// 创建新文件
await vault.create(normalizedPath, content);
return true;
} catch (error) {
return false;
}
}
/**
*
* @param vault Obsidian文件系统

View file

@ -256,15 +256,20 @@ button {
/* 模板预览容器 */
.template-preview {
background-color: var(--background-primary);
border: 1px solid var(--background-modifier-border);
border-radius: 5px;
margin-top: 15px;
padding: 15px;
margin-top: 10px;
max-height: 200px;
overflow-y: auto;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
border: 1px dashed var(--background-modifier-border);
border-radius: 8px;
background-color: var(--background-secondary);
display: none;
max-height: 200px;
overflow: auto;
box-shadow: inset 0 0 5px rgba(0,0,0,0.1);
text-align: left;
line-height: 1.6;
font-family: var(--font-text);
white-space: pre-wrap;
word-wrap: break-word;
}
.template-preview.visible {
@ -672,6 +677,11 @@ button {
max-height: 200px;
overflow: auto;
box-shadow: inset 0 0 5px rgba(0,0,0,0.1);
text-align: left;
line-height: 1.6;
font-family: var(--font-text);
white-space: pre-wrap;
word-wrap: break-word;
}
.template-preview.visible {