更新README.md,添加个人数据库设计计划;优化CSV视图,增加标签页和导入导出选项,支持自定义分隔符和表头设置

This commit is contained in:
JayBridge 2025-03-23 20:23:58 +08:00
parent 3e844b0cf7
commit 60a5534994
5 changed files with 841 additions and 2440 deletions

View file

@ -8,6 +8,8 @@ Keep your mind on track! Don't waste time creating fancy tables.
A plugin designed to view and edit CSV files directly within Obsidian.
I have a plan to design my own database using json and csv only. If you have fancy idea about tables or csv, please wait for another plugin or search it in community. For personal preference and in-markdown edit, I recommend `anyblock`.
## Why another csv plugin?
There are so many csv plugin, why you need this one?
@ -30,9 +32,10 @@ No fancy feature, just open and edit.
- All in TextFileView/workspace.
- No more pollution to your vault, all metadata store in `./.obsidian/plugins/csv` in json format.
- Every function must be completed within 3 steps:
0. Locate it visually
1. Click/Hotkey
2. Input (if needed)
3. Confirm
3. Confirm/Leave
- The interface should remain minimal yet functional
- Users shouldn't need to leave their workflow environment
- CSV manipulation should be as natural as text editing.

2987
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,24 +1,26 @@
{
"name": "obsidian-csv-plugin",
"version": "1.0.0",
"description": "This is a csv plugin for Obsidian (https://obsidian.md)",
"main": "main.js",
"scripts": {
"dev": "node esbuild.config.mjs",
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
"version": "node version-bump.mjs && git add manifest.json versions.json"
},
"keywords": [],
"author": "",
"license": "MIT",
"devDependencies": {
"@types/node": "^16.11.6",
"@typescript-eslint/eslint-plugin": "5.29.0",
"@typescript-eslint/parser": "5.29.0",
"builtin-modules": "3.3.0",
"esbuild": "0.17.3",
"obsidian": "latest",
"tslib": "2.4.0",
"typescript": "4.7.4"
}
"name": "obsidian-csv",
"version": "1.0.0",
"description": "CSV viewer and editor for Obsidian",
"main": "main.js",
"scripts": {
"dev": "node esbuild.config.mjs",
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
"version": "node version-bump.mjs && git add manifest.json versions.json"
},
"keywords": [],
"author": "",
"license": "MIT",
"dependencies": {
"papaparse": "^5.4.1"
},
"devDependencies": {
"@types/node": "^16.11.6",
"@types/papaparse": "^5.3.7",
"builtin-modules": "^3.3.0",
"esbuild": "0.17.3",
"obsidian": "latest",
"tslib": "2.4.0",
"typescript": "4.7.4"
}
}

View file

@ -1,4 +1,5 @@
import { TextFileView, ButtonComponent, Notice, DropdownComponent } from "obsidian";
import { TextFileView, ButtonComponent, Notice, DropdownComponent, requestUrl } from "obsidian";
import * as Papa from 'papaparse';
export const VIEW_TYPE_CSV = "csv-view";
@ -15,20 +16,78 @@ export class CSVView extends TextFileView {
// 添加列宽调整设置
private columnWidths: number[] = [];
private autoResize: boolean = true; // 始终启用自动调整
// Papa Parse 配置选项
private papaConfig = {
header: false,
dynamicTyping: false, // 保持所有值为字符串
skipEmptyLines: false // 保留空行以保持行号一致
};
getViewData() {
return this.tableData.map((row) => row.join(",")).join("\n");
// 使用 Papa Parse 将数据序列化为CSV字符串
return Papa.unparse(this.tableData, {
header: false,
newline: "\n"
});
}
setViewData(data: string, clear: boolean) {
this.tableData = data.split("\n").map((line) => line.split(","));
try {
// 使用 Papa Parse 解析CSV数据
const parseResult = Papa.parse(data, this.papaConfig);
if (parseResult.errors && parseResult.errors.length > 0) {
// 显示解析错误
console.warn("CSV解析警告:", parseResult.errors);
new Notice(`CSV解析提示: ${parseResult.errors[0].message}`);
}
this.tableData = parseResult.data as string[][];
// 确保至少有一行一列
if (!this.tableData || this.tableData.length === 0) {
this.tableData = [[""]];
}
// 使所有行的列数一致
this.normalizeTableData();
// 初始化历史记录
if (clear) {
this.initHistory();
}
this.refresh();
} catch (error) {
console.error("CSV解析错误:", error);
new Notice("CSV解析失败请检查文件格式");
// 出错时设置为空表格
this.tableData = [[""]];
if (clear) {
this.initHistory();
}
this.refresh();
}
}
// 确保表格数据规整(所有行有相同的列数)
private normalizeTableData() {
if (!this.tableData || this.tableData.length === 0) return;
// 初始化历史记录
if (clear) {
this.initHistory();
// 找出最大列数
let maxCols = 0;
for (const row of this.tableData) {
maxCols = Math.max(maxCols, row.length);
}
this.refresh();
// 确保每行都有相同的列数
for (let i = 0; i < this.tableData.length; i++) {
while (this.tableData[i].length < maxCols) {
this.tableData[i].push("");
}
}
}
// 初始化历史记录
@ -271,53 +330,82 @@ export class CSVView extends TextFileView {
// 创建操作区
this.operationEl = this.contentEl.createEl("div", { cls: "csv-operations" });
// 添加常用操作按钮
const buttonContainer = this.operationEl.createEl("div", { cls: "csv-operation-buttons" });
// 创建两个标签页容器
const tabsContainer = this.operationEl.createEl("div", { cls: "csv-tabs" });
// 创建标签头部
const tabHeaders = tabsContainer.createEl("div", { cls: "csv-tab-headers" });
const contentTabHeader = tabHeaders.createEl("div", { cls: "csv-tab-header csv-tab-active", text: "内容" });
const appearanceTabHeader = tabHeaders.createEl("div", { cls: "csv-tab-header", text: "外观" });
// 创建内容标签页面板
const contentTabPanel = tabsContainer.createEl("div", { cls: "csv-tab-panel csv-tab-panel-active" });
// 添加内容相关操作按钮
const contentButtonContainer = contentTabPanel.createEl("div", { cls: "csv-operation-buttons" });
// 撤销按钮
new ButtonComponent(buttonContainer)
new ButtonComponent(contentButtonContainer)
.setButtonText("撤销")
.setIcon("undo")
.onClick(() => this.undo());
// 重做按钮
new ButtonComponent(buttonContainer)
new ButtonComponent(contentButtonContainer)
.setButtonText("重做")
.setIcon("redo")
.onClick(() => this.redo());
// 添加行按钮
new ButtonComponent(buttonContainer)
new ButtonComponent(contentButtonContainer)
.setButtonText("添加行")
.onClick(() => this.addRow());
// 删除行按钮
new ButtonComponent(buttonContainer)
new ButtonComponent(contentButtonContainer)
.setButtonText("删除行")
.onClick(() => this.deleteRow());
// 添加列按钮
new ButtonComponent(buttonContainer)
new ButtonComponent(contentButtonContainer)
.setButtonText("添加列")
.onClick(() => this.addColumn());
// 删除列按钮
new ButtonComponent(buttonContainer)
new ButtonComponent(contentButtonContainer)
.setButtonText("删除列")
.onClick(() => this.deleteColumn());
// 添加列宽自动调整选项
const settingsContainer = this.operationEl.createEl("div", { cls: "csv-settings" });
// 删除自动调整行高开关,只保留重置列宽按钮
new ButtonComponent(settingsContainer)
// 创建外观标签页面板
const appearanceTabPanel = tabsContainer.createEl("div", { cls: "csv-tab-panel" });
// 添加外观相关设置
const appearanceSettings = appearanceTabPanel.createEl("div", { cls: "csv-settings" });
// 重置列宽按钮
new ButtonComponent(appearanceSettings)
.setButtonText("重置列宽")
.onClick(() => {
this.columnWidths = [];
this.calculateColumnWidths();
this.refresh();
});
// 添加标签切换功能
contentTabHeader.addEventListener("click", () => {
contentTabHeader.addClass("csv-tab-active");
appearanceTabHeader.removeClass("csv-tab-active");
contentTabPanel.addClass("csv-tab-panel-active");
appearanceTabPanel.removeClass("csv-tab-panel-active");
});
appearanceTabHeader.addEventListener("click", () => {
appearanceTabHeader.addClass("csv-tab-active");
contentTabHeader.removeClass("csv-tab-active");
appearanceTabPanel.addClass("csv-tab-panel-active");
contentTabPanel.removeClass("csv-tab-panel-active");
});
// 创建一个分隔线
this.contentEl.createEl("hr");
@ -343,6 +431,42 @@ export class CSVView extends TextFileView {
}
});
// 添加CSV导入导出选项到内容标签页
const exportImportContainer = contentTabPanel.createEl("div", { cls: "csv-export-import" });
// 添加分隔符选择
const delimiterContainer = exportImportContainer.createEl("div", { cls: "csv-delimiter-container" });
delimiterContainer.createEl("span", { text: "分隔符: " });
const delimiterDropdown = new DropdownComponent(delimiterContainer);
delimiterDropdown.addOption(",", "逗号 (,)");
delimiterDropdown.addOption(";", "分号 (;)");
delimiterDropdown.addOption("\t", "制表符 (Tab)");
delimiterDropdown.setValue(",");
// 设置分隔符改变时的处理函数
delimiterDropdown.onChange(value => {
this.papaConfig.delimiter = value;
this.refresh();
});
// 添加高级解析设置
const advancedContainer = appearanceSettings.createEl("div", { cls: "csv-advanced-settings" });
// 添加首行作为表头选项
const headerOption = advancedContainer.createEl("div", { cls: "csv-setting-item" });
headerOption.createEl("span", { text: "首行作为表头" });
const headerToggle = headerOption.createEl("input", { attr: { type: "checkbox" } });
headerToggle.addEventListener("change", e => {
if (e.currentTarget instanceof HTMLInputElement) {
const headerRow = document.querySelector("thead tr");
if (headerRow) {
headerRow.toggleClass("csv-header-row", e.currentTarget.checked);
}
}
});
// 初始化时刷新视图
this.refresh();
}
@ -356,8 +480,13 @@ export class CSVView extends TextFileView {
// 保存当前状态到历史记录
this.saveSnapshot();
// 获取最大列数
const colCount = this.tableData.length > 0
? this.tableData[0].length
: 1;
// 在表格末尾添加一个空行
const newRow = Array(this.tableData[0]?.length || 1).fill("");
const newRow = Array(colCount).fill("");
this.tableData.push(newRow);
this.refresh();
this.requestSave();

View file

@ -102,3 +102,73 @@ td input:focus {
max-width: 100%;
overflow-x: auto;
}
/* 标签页样式 */
.csv-tabs {
display: flex;
flex-direction: column;
width: 100%;
margin-bottom: 10px;
}
.csv-tab-headers {
display: flex;
border-bottom: 1px solid var(--background-modifier-border);
}
.csv-tab-header {
padding: 8px 16px;
cursor: pointer;
border-bottom: 2px solid transparent;
margin-right: 8px;
font-weight: 500;
}
.csv-tab-header:hover {
color: var(--interactive-accent);
}
.csv-tab-header.csv-tab-active {
color: var(--interactive-accent);
border-bottom-color: var(--interactive-accent);
}
.csv-tab-panel {
display: none;
padding: 12px 0;
}
.csv-tab-panel-active {
display: block;
}
/* CSV导入导出选项样式 */
.csv-export-import {
margin-top: 12px;
padding-top: 12px;
border-top: 1px dashed var(--background-modifier-border);
}
.csv-delimiter-container {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
}
.csv-advanced-settings {
margin-top: 16px;
}
.csv-setting-item {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
/* 表头行样式 */
.csv-header-row th {
background-color: var(--interactive-accent);
color: var(--text-on-accent);
}