mirror of
https://github.com/liubinfighter/csv-lite.git
synced 2026-07-22 05:43:52 +00:00
chore: disable .csv-parser-settings for now #6 (waiting for further dev)
This commit is contained in:
parent
002759a4f9
commit
247872b3f8
7 changed files with 1009 additions and 920 deletions
71
src/main.ts
71
src/main.ts
|
|
@ -1,48 +1,55 @@
|
|||
import { Plugin, WorkspaceLeaf, moment } from 'obsidian';
|
||||
import { Plugin, WorkspaceLeaf, moment } from "obsidian";
|
||||
import { CSVView, VIEW_TYPE_CSV } from "./view";
|
||||
import { i18n } from './i18n';
|
||||
import { i18n } from "./i18n";
|
||||
|
||||
interface CSVPluginSettings {
|
||||
csvSettings: string;
|
||||
csvSettings: string;
|
||||
}
|
||||
|
||||
const DEFAULT_SETTINGS: CSVPluginSettings = {
|
||||
csvSettings: 'default'
|
||||
}
|
||||
csvSettings: "default",
|
||||
};
|
||||
|
||||
export default class CSVPlugin extends Plugin {
|
||||
settings: CSVPluginSettings;
|
||||
settings: CSVPluginSettings;
|
||||
|
||||
async onload() {
|
||||
await this.loadSettings();
|
||||
async onload() {
|
||||
await this.loadSettings();
|
||||
|
||||
// 使用 moment.locale() 来安全地获取 Obsidian 的当前语言设置
|
||||
const obsidianLang = moment.locale();
|
||||
i18n.setLocale(obsidianLang);
|
||||
console.log(`CSV Plugin: Setting locale to '${obsidianLang}'`);
|
||||
console.log(`CSV Plugin: Test translation - buttons.undo: '${i18n.t('buttons.undo')}'`);
|
||||
// 使用 moment.locale() 来安全地获取 Obsidian 的当前语言设置
|
||||
const obsidianLang = moment.locale();
|
||||
i18n.setLocale(obsidianLang);
|
||||
console.log(`CSV Plugin: Setting locale to '${obsidianLang}'`);
|
||||
console.log(
|
||||
`CSV Plugin: Test translation - buttons.undo: '${i18n.t(
|
||||
"buttons.undo"
|
||||
)}'`
|
||||
);
|
||||
|
||||
// 注册CSV视图类型
|
||||
this.registerView(
|
||||
VIEW_TYPE_CSV,
|
||||
(leaf: WorkspaceLeaf) => new CSVView(leaf)
|
||||
);
|
||||
// 注册CSV视图类型
|
||||
this.registerView(
|
||||
VIEW_TYPE_CSV,
|
||||
(leaf: WorkspaceLeaf) => new CSVView(leaf)
|
||||
);
|
||||
|
||||
// 将.csv文件扩展名与视图类型绑定
|
||||
this.registerExtensions(["csv"], VIEW_TYPE_CSV);
|
||||
// 将.csv文件扩展名与视图类型绑定
|
||||
this.registerExtensions(["csv"], VIEW_TYPE_CSV);
|
||||
}
|
||||
|
||||
}
|
||||
onunload() {
|
||||
// 移除视图
|
||||
// this.app.workspace.detachLeavesOfType(VIEW_TYPE_CSV); // 删除此行
|
||||
}
|
||||
|
||||
onunload() {
|
||||
// 移除视图
|
||||
// this.app.workspace.detachLeavesOfType(VIEW_TYPE_CSV); // 删除此行
|
||||
}
|
||||
async loadSettings() {
|
||||
this.settings = Object.assign(
|
||||
{},
|
||||
DEFAULT_SETTINGS,
|
||||
await this.loadData()
|
||||
);
|
||||
}
|
||||
|
||||
async loadSettings() {
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
||||
}
|
||||
|
||||
async saveSettings() {
|
||||
await this.saveData(this.settings);
|
||||
}
|
||||
async saveSettings() {
|
||||
await this.saveData(this.settings);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,83 +1,86 @@
|
|||
import * as Papa from 'papaparse';
|
||||
import { Notice } from 'obsidian';
|
||||
import { i18n } from '../i18n';
|
||||
import * as Papa from "papaparse";
|
||||
import { Notice } from "obsidian";
|
||||
import { i18n } from "../i18n";
|
||||
|
||||
export interface CSVParseConfig {
|
||||
header: boolean;
|
||||
dynamicTyping: boolean;
|
||||
skipEmptyLines: boolean;
|
||||
delimiter?: string;
|
||||
quoteChar: string;
|
||||
escapeChar: string;
|
||||
header: boolean;
|
||||
dynamicTyping: boolean;
|
||||
skipEmptyLines: boolean;
|
||||
delimiter?: string;
|
||||
quoteChar: string;
|
||||
escapeChar: string;
|
||||
}
|
||||
|
||||
export class CSVUtils {
|
||||
// 现在这是默认设置的唯一权威来源
|
||||
static defaultConfig: CSVParseConfig = {
|
||||
header: false,
|
||||
dynamicTyping: false,
|
||||
skipEmptyLines: false,
|
||||
delimiter: ',',
|
||||
quoteChar: '"', // 关键:这是修复报告bug的关键
|
||||
escapeChar: '"',
|
||||
};
|
||||
// 现在这是默认设置的唯一权威来源
|
||||
static defaultConfig: CSVParseConfig = {
|
||||
header: false,
|
||||
dynamicTyping: false,
|
||||
skipEmptyLines: false,
|
||||
delimiter: ",",
|
||||
quoteChar: '"', // 关键:这是修复报告bug的关键
|
||||
escapeChar: '"',
|
||||
};
|
||||
|
||||
/**
|
||||
* 解析CSV字符串为二维数组
|
||||
*/
|
||||
static parseCSV(csvString: string, config?: Partial<CSVParseConfig>): string[][] {
|
||||
try {
|
||||
const parseConfig = { ...this.defaultConfig, ...config };
|
||||
const parseResult = Papa.parse(csvString, parseConfig);
|
||||
|
||||
if (parseResult.errors && parseResult.errors.length > 0) {
|
||||
console.warn("CSV解析警告:", parseResult.errors);
|
||||
new Notice(`CSV解析提示: ${parseResult.errors[0].message}`);
|
||||
}
|
||||
|
||||
return parseResult.data as string[][];
|
||||
} catch (error) {
|
||||
console.error("CSV解析错误:", error);
|
||||
new Notice(`${i18n.t('csv.error')}: CSV解析失败,请检查文件格式`);
|
||||
return [[""]];
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 解析CSV字符串为二维数组
|
||||
*/
|
||||
static parseCSV(
|
||||
csvString: string,
|
||||
config?: Partial<CSVParseConfig>
|
||||
): string[][] {
|
||||
try {
|
||||
const parseConfig = { ...this.defaultConfig, ...config };
|
||||
const parseResult = Papa.parse(csvString, parseConfig);
|
||||
|
||||
/**
|
||||
* 将二维数组转换为CSV字符串
|
||||
*/
|
||||
static unparseCSV(data: string[][], config?: Papa.UnparseConfig): string {
|
||||
const defaultUnparseConfig = {
|
||||
header: false,
|
||||
newline: "\n"
|
||||
};
|
||||
|
||||
return Papa.unparse(data, { ...defaultUnparseConfig, ...config });
|
||||
}
|
||||
if (parseResult.errors && parseResult.errors.length > 0) {
|
||||
console.warn("CSV解析警告:", parseResult.errors);
|
||||
new Notice(`CSV解析提示: ${parseResult.errors[0].message}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保表格数据规整(所有行的列数相同)
|
||||
*/
|
||||
static normalizeTableData(tableData: string[][]): string[][] {
|
||||
if (!tableData || tableData.length === 0) return [[""]];
|
||||
|
||||
// 找出最大列数
|
||||
let maxCols = 0;
|
||||
for (const row of tableData) {
|
||||
if (row) {
|
||||
maxCols = Math.max(maxCols, row.length);
|
||||
}
|
||||
}
|
||||
|
||||
// 确保每行都有相同的列数
|
||||
const normalizedData = tableData.map(row => {
|
||||
const newRow = row ? [...row] : [];
|
||||
while (newRow.length < maxCols) {
|
||||
newRow.push("");
|
||||
}
|
||||
return newRow;
|
||||
});
|
||||
|
||||
return normalizedData;
|
||||
}
|
||||
return parseResult.data as string[][];
|
||||
} catch (error) {
|
||||
console.error("CSV解析错误:", error);
|
||||
new Notice(`${i18n.t("csv.error")}: CSV解析失败,请检查文件格式`);
|
||||
return [[""]];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将二维数组转换为CSV字符串
|
||||
*/
|
||||
static unparseCSV(data: string[][], config?: Papa.UnparseConfig): string {
|
||||
const defaultUnparseConfig = {
|
||||
header: false,
|
||||
newline: "\n",
|
||||
};
|
||||
|
||||
return Papa.unparse(data, { ...defaultUnparseConfig, ...config });
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保表格数据规整(所有行的列数相同)
|
||||
*/
|
||||
static normalizeTableData(tableData: string[][]): string[][] {
|
||||
if (!tableData || tableData.length === 0) return [[""]];
|
||||
|
||||
// 找出最大列数
|
||||
let maxCols = 0;
|
||||
for (const row of tableData) {
|
||||
if (row) {
|
||||
maxCols = Math.max(maxCols, row.length);
|
||||
}
|
||||
}
|
||||
|
||||
// 确保每行都有相同的列数
|
||||
const normalizedData = tableData.map((row) => {
|
||||
const newRow = row ? [...row] : [];
|
||||
while (newRow.length < maxCols) {
|
||||
newRow.push("");
|
||||
}
|
||||
return newRow;
|
||||
});
|
||||
|
||||
return normalizedData;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,50 +1,54 @@
|
|||
import { Notice } from 'obsidian';
|
||||
import { Notice } from "obsidian";
|
||||
|
||||
export class FileUtils {
|
||||
/**
|
||||
* Attempts to perform a file operation with retry logic
|
||||
* @param operation The file operation function to execute
|
||||
* @param maxRetries Maximum number of retry attempts
|
||||
* @param delayMs Delay between retries in milliseconds
|
||||
* @returns Promise resolving to the operation result or rejecting with an error
|
||||
*/
|
||||
static async withRetry<T>(
|
||||
operation: () => Promise<T>,
|
||||
maxRetries: number = 3,
|
||||
delayMs: number = 500
|
||||
): Promise<T> {
|
||||
let lastError: Error = new Error("Unknown error occurred");
|
||||
/**
|
||||
* Attempts to perform a file operation with retry logic
|
||||
* @param operation The file operation function to execute
|
||||
* @param maxRetries Maximum number of retry attempts
|
||||
* @param delayMs Delay between retries in milliseconds
|
||||
* @returns Promise resolving to the operation result or rejecting with an error
|
||||
*/
|
||||
static async withRetry<T>(
|
||||
operation: () => Promise<T>,
|
||||
maxRetries: number = 3,
|
||||
delayMs: number = 500
|
||||
): Promise<T> {
|
||||
let lastError: Error = new Error("Unknown error occurred");
|
||||
|
||||
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
// Attempt the operation
|
||||
return await operation();
|
||||
} catch (error) {
|
||||
lastError = error as Error;
|
||||
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
// Attempt the operation
|
||||
return await operation();
|
||||
} catch (error) {
|
||||
lastError = error as Error;
|
||||
|
||||
// Check if this is a "file busy" error
|
||||
const isFileBusyError =
|
||||
error instanceof Error &&
|
||||
(error.message.includes('EBUSY') ||
|
||||
error.message.includes('busy') ||
|
||||
error.message.includes('locked'));
|
||||
// Check if this is a "file busy" error
|
||||
const isFileBusyError =
|
||||
error instanceof Error &&
|
||||
(error.message.includes("EBUSY") ||
|
||||
error.message.includes("busy") ||
|
||||
error.message.includes("locked"));
|
||||
|
||||
// If it's not a file busy error or we've used all retries, throw the error
|
||||
if (!isFileBusyError || attempt === maxRetries) {
|
||||
break;
|
||||
}
|
||||
// If it's not a file busy error or we've used all retries, throw the error
|
||||
if (!isFileBusyError || attempt === maxRetries) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Show a notice on first retry
|
||||
if (attempt === 0) {
|
||||
new Notice(`File is busy. Retrying... (${attempt + 1}/${maxRetries})`);
|
||||
}
|
||||
// Show a notice on first retry
|
||||
if (attempt === 0) {
|
||||
new Notice(
|
||||
`File is busy. Retrying... (${
|
||||
attempt + 1
|
||||
}/${maxRetries})`
|
||||
);
|
||||
}
|
||||
|
||||
// Wait before retrying
|
||||
await new Promise(resolve => setTimeout(resolve, delayMs));
|
||||
}
|
||||
}
|
||||
// Wait before retrying
|
||||
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
||||
}
|
||||
}
|
||||
|
||||
// If we get here, all retries failed
|
||||
throw lastError;
|
||||
}
|
||||
// If we get here, all retries failed
|
||||
throw lastError;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,127 +1,127 @@
|
|||
import { Notice } from 'obsidian';
|
||||
import { Notice } from "obsidian";
|
||||
|
||||
export class HistoryManager<T> {
|
||||
private history: T[] = [];
|
||||
private currentIndex: number = -1;
|
||||
private maxSize: number;
|
||||
|
||||
constructor(initialState?: T, maxSize: number = 50) {
|
||||
this.maxSize = maxSize;
|
||||
if (initialState) {
|
||||
this.push(initialState);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存新状态到历史记录
|
||||
*/
|
||||
push(state: T): void {
|
||||
// 删除当前索引之后的所有历史记录
|
||||
if (this.currentIndex < this.history.length - 1) {
|
||||
this.history = this.history.slice(0, this.currentIndex + 1);
|
||||
}
|
||||
|
||||
// 添加新状态
|
||||
this.history.push(this.cloneState(state));
|
||||
|
||||
// 如果历史记录超过最大限制,删除最早的记录
|
||||
if (this.history.length > this.maxSize) {
|
||||
this.history.shift();
|
||||
} else {
|
||||
this.currentIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 撤销到上一个状态
|
||||
*/
|
||||
undo(): T | null {
|
||||
if (this.canUndo()) {
|
||||
this.currentIndex--;
|
||||
new Notice("已撤销上一步操作");
|
||||
return this.getCurrentState();
|
||||
} else {
|
||||
new Notice("没有更多可撤销的操作");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重做到下一个状态
|
||||
*/
|
||||
redo(): T | null {
|
||||
if (this.canRedo()) {
|
||||
this.currentIndex++;
|
||||
new Notice("已重做操作");
|
||||
return this.getCurrentState();
|
||||
} else {
|
||||
new Notice("没有更多可重做的操作");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前状态
|
||||
*/
|
||||
getCurrentState(): T | null {
|
||||
if (this.currentIndex >= 0 && this.currentIndex < this.history.length) {
|
||||
return this.cloneState(this.history[this.currentIndex]);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否可以撤销
|
||||
*/
|
||||
canUndo(): boolean {
|
||||
return this.currentIndex > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否可以重做
|
||||
*/
|
||||
canRedo(): boolean {
|
||||
return this.currentIndex < this.history.length - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置历史记录
|
||||
*/
|
||||
reset(initialState?: T): void {
|
||||
this.history = [];
|
||||
this.currentIndex = -1;
|
||||
if (initialState) {
|
||||
this.push(initialState);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 克隆状态(泛型方法,需要在使用时重写)
|
||||
*/
|
||||
protected cloneState(state: T): T {
|
||||
// 默认实现,应在子类中重写
|
||||
if (Array.isArray(state)) {
|
||||
// 处理二维数组情况
|
||||
if (state.length > 0 && Array.isArray(state[0])) {
|
||||
return state.map(row => [...row]) as unknown as T;
|
||||
}
|
||||
return [...state] as unknown as T;
|
||||
}
|
||||
|
||||
// 对象类型尝试深拷贝
|
||||
if (typeof state === 'object' && state !== null) {
|
||||
return JSON.parse(JSON.stringify(state));
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
private history: T[] = [];
|
||||
private currentIndex: number = -1;
|
||||
private maxSize: number;
|
||||
|
||||
constructor(initialState?: T, maxSize: number = 50) {
|
||||
this.maxSize = maxSize;
|
||||
if (initialState) {
|
||||
this.push(initialState);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存新状态到历史记录
|
||||
*/
|
||||
push(state: T): void {
|
||||
// 删除当前索引之后的所有历史记录
|
||||
if (this.currentIndex < this.history.length - 1) {
|
||||
this.history = this.history.slice(0, this.currentIndex + 1);
|
||||
}
|
||||
|
||||
// 添加新状态
|
||||
this.history.push(this.cloneState(state));
|
||||
|
||||
// 如果历史记录超过最大限制,删除最早的记录
|
||||
if (this.history.length > this.maxSize) {
|
||||
this.history.shift();
|
||||
} else {
|
||||
this.currentIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 撤销到上一个状态
|
||||
*/
|
||||
undo(): T | null {
|
||||
if (this.canUndo()) {
|
||||
this.currentIndex--;
|
||||
new Notice("已撤销上一步操作");
|
||||
return this.getCurrentState();
|
||||
} else {
|
||||
new Notice("没有更多可撤销的操作");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重做到下一个状态
|
||||
*/
|
||||
redo(): T | null {
|
||||
if (this.canRedo()) {
|
||||
this.currentIndex++;
|
||||
new Notice("已重做操作");
|
||||
return this.getCurrentState();
|
||||
} else {
|
||||
new Notice("没有更多可重做的操作");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前状态
|
||||
*/
|
||||
getCurrentState(): T | null {
|
||||
if (this.currentIndex >= 0 && this.currentIndex < this.history.length) {
|
||||
return this.cloneState(this.history[this.currentIndex]);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否可以撤销
|
||||
*/
|
||||
canUndo(): boolean {
|
||||
return this.currentIndex > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否可以重做
|
||||
*/
|
||||
canRedo(): boolean {
|
||||
return this.currentIndex < this.history.length - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置历史记录
|
||||
*/
|
||||
reset(initialState?: T): void {
|
||||
this.history = [];
|
||||
this.currentIndex = -1;
|
||||
if (initialState) {
|
||||
this.push(initialState);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 克隆状态(泛型方法,需要在使用时重写)
|
||||
*/
|
||||
protected cloneState(state: T): T {
|
||||
// 默认实现,应在子类中重写
|
||||
if (Array.isArray(state)) {
|
||||
// 处理二维数组情况
|
||||
if (state.length > 0 && Array.isArray(state[0])) {
|
||||
return state.map((row) => [...row]) as unknown as T;
|
||||
}
|
||||
return [...state] as unknown as T;
|
||||
}
|
||||
|
||||
// 对象类型尝试深拷贝
|
||||
if (typeof state === "object" && state !== null) {
|
||||
return JSON.parse(JSON.stringify(state));
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 特化版本,专门用于处理CSV表格数据(二维字符串数组)
|
||||
*/
|
||||
export class TableHistoryManager extends HistoryManager<string[][]> {
|
||||
protected cloneState(state: string[][]): string[][] {
|
||||
return state.map(row => [...row]);
|
||||
}
|
||||
protected cloneState(state: string[][]): string[][] {
|
||||
return state.map((row) => [...row]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,88 +1,94 @@
|
|||
import { Notice } from 'obsidian';
|
||||
import { Notice } from "obsidian";
|
||||
|
||||
export class TableUtils {
|
||||
/**
|
||||
* 计算表格列宽
|
||||
*/
|
||||
static calculateColumnWidths(tableData: string[][]): number[] {
|
||||
if (!tableData || tableData.length === 0 || !tableData[0]) return [];
|
||||
|
||||
// 初始化所有列为默认宽度
|
||||
const columnWidths = tableData[0].map(() => 100);
|
||||
|
||||
// 根据内容长度进行简单调整
|
||||
tableData.forEach(row => {
|
||||
row.forEach((cell, index) => {
|
||||
// 根据内容长度估算合适的宽度
|
||||
const estimatedWidth = Math.max(50, Math.min(300, cell.length * 10));
|
||||
columnWidths[index] = Math.max(columnWidths[index], estimatedWidth);
|
||||
});
|
||||
});
|
||||
|
||||
return columnWidths;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加新行
|
||||
*/
|
||||
static addRow(tableData: string[][]): string[][] {
|
||||
const colCount = tableData.length > 0 ? tableData[0].length : 1;
|
||||
const newRow = Array(colCount).fill("");
|
||||
return [...tableData, newRow];
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除最后一行
|
||||
*/
|
||||
static deleteRow(tableData: string[][]): string[][] {
|
||||
if (tableData.length <= 1) {
|
||||
new Notice("至少需要保留一行");
|
||||
return tableData;
|
||||
}
|
||||
|
||||
return tableData.slice(0, -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加新列
|
||||
*/
|
||||
static addColumn(tableData: string[][]): string[][] {
|
||||
return tableData.map(row => [...row, ""]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除最后一列
|
||||
*/
|
||||
static deleteColumn(tableData: string[][]): string[][] {
|
||||
if (!tableData[0] || tableData[0].length <= 1) {
|
||||
new Notice("至少需要保留一列");
|
||||
return tableData;
|
||||
}
|
||||
|
||||
return tableData.map(row => row.slice(0, -1));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取列标签 (A, B, C, ..., Z, AA, AB, ...)
|
||||
*/
|
||||
static getColumnLabel(index: number): string {
|
||||
let label = '';
|
||||
let n = index;
|
||||
|
||||
while (n >= 0) {
|
||||
label = String.fromCharCode(65 + (n % 26)) + label;
|
||||
n = Math.floor(n / 26) - 1;
|
||||
}
|
||||
|
||||
return label;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单元格地址标识(例如A1, B2)
|
||||
*/
|
||||
static getCellAddress(rowIndex: number, colIndex: number): string {
|
||||
const colAddress = this.getColumnLabel(colIndex);
|
||||
const rowAddress = rowIndex + 1;
|
||||
return `${colAddress}${rowAddress}`;
|
||||
}
|
||||
/**
|
||||
* 计算表格列宽
|
||||
*/
|
||||
static calculateColumnWidths(tableData: string[][]): number[] {
|
||||
if (!tableData || tableData.length === 0 || !tableData[0]) return [];
|
||||
|
||||
// 初始化所有列为默认宽度
|
||||
const columnWidths = tableData[0].map(() => 100);
|
||||
|
||||
// 根据内容长度进行简单调整
|
||||
tableData.forEach((row) => {
|
||||
row.forEach((cell, index) => {
|
||||
// 根据内容长度估算合适的宽度
|
||||
const estimatedWidth = Math.max(
|
||||
50,
|
||||
Math.min(300, cell.length * 10)
|
||||
);
|
||||
columnWidths[index] = Math.max(
|
||||
columnWidths[index],
|
||||
estimatedWidth
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
return columnWidths;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加新行
|
||||
*/
|
||||
static addRow(tableData: string[][]): string[][] {
|
||||
const colCount = tableData.length > 0 ? tableData[0].length : 1;
|
||||
const newRow = Array(colCount).fill("");
|
||||
return [...tableData, newRow];
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除最后一行
|
||||
*/
|
||||
static deleteRow(tableData: string[][]): string[][] {
|
||||
if (tableData.length <= 1) {
|
||||
new Notice("至少需要保留一行");
|
||||
return tableData;
|
||||
}
|
||||
|
||||
return tableData.slice(0, -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加新列
|
||||
*/
|
||||
static addColumn(tableData: string[][]): string[][] {
|
||||
return tableData.map((row) => [...row, ""]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除最后一列
|
||||
*/
|
||||
static deleteColumn(tableData: string[][]): string[][] {
|
||||
if (!tableData[0] || tableData[0].length <= 1) {
|
||||
new Notice("至少需要保留一列");
|
||||
return tableData;
|
||||
}
|
||||
|
||||
return tableData.map((row) => row.slice(0, -1));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取列标签 (A, B, C, ..., Z, AA, AB, ...)
|
||||
*/
|
||||
static getColumnLabel(index: number): string {
|
||||
let label = "";
|
||||
let n = index;
|
||||
|
||||
while (n >= 0) {
|
||||
label = String.fromCharCode(65 + (n % 26)) + label;
|
||||
n = Math.floor(n / 26) - 1;
|
||||
}
|
||||
|
||||
return label;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单元格地址标识(例如A1, B2)
|
||||
*/
|
||||
static getCellAddress(rowIndex: number, colIndex: number): string {
|
||||
const colAddress = this.getColumnLabel(colIndex);
|
||||
const rowAddress = rowIndex + 1;
|
||||
return `${colAddress}${rowAddress}`;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
1019
src/view.ts
1019
src/view.ts
File diff suppressed because it is too large
Load diff
190
styles.css
190
styles.css
|
|
@ -37,48 +37,48 @@ If your plugin does not need CSS, delete this file.
|
|||
.csv-lite-cell-long {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
/* 输入框共享样式 */
|
||||
.csv-cell-input,
|
||||
.csv-edit-input {
|
||||
height: auto;
|
||||
padding: 4px 8px;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
background: var(--background-primary);
|
||||
color: var(--text-normal);
|
||||
font-size: inherit;
|
||||
line-height: 1.4;
|
||||
}
|
||||
/* 输入框共享样式 */
|
||||
.csv-cell-input,
|
||||
.csv-edit-input {
|
||||
height: auto;
|
||||
padding: 4px 8px;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
background: var(--background-primary);
|
||||
color: var(--text-normal);
|
||||
font-size: inherit;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* csv-cell-input 特有样式(宽度由 JS 控制) */
|
||||
.csv-cell-input {
|
||||
width: 100%;
|
||||
}
|
||||
/* csv-cell-input 特有样式(宽度由 JS 控制) */
|
||||
.csv-cell-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* csv-edit-input 特有样式 */
|
||||
.csv-edit-input {
|
||||
width: 75%;
|
||||
margin: 0 auto;
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
/* csv-edit-input 特有样式 */
|
||||
.csv-edit-input {
|
||||
width: 75%;
|
||||
margin: 0 auto;
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 焦点状态样式 */
|
||||
.csv-cell-input:focus,
|
||||
.csv-edit-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--interactive-accent);
|
||||
box-shadow: 0 0 0 2px rgba(var(--interactive-accent-rgb), 0.2);
|
||||
}
|
||||
.csv-operation-buttons {
|
||||
/* 焦点状态样式 */
|
||||
.csv-cell-input:focus,
|
||||
.csv-edit-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--interactive-accent);
|
||||
box-shadow: 0 0 0 2px rgba(var(--interactive-accent-rgb), 0.2);
|
||||
}
|
||||
.csv-operation-buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* 列宽调整手柄 */
|
||||
.resize-handle {
|
||||
}
|
||||
|
||||
/* 列宽调整手柄 */
|
||||
.resize-handle {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
|
|
@ -86,104 +86,102 @@ If your plugin does not need CSS, delete this file.
|
|||
width: 5px;
|
||||
background-color: transparent;
|
||||
cursor: col-resize;
|
||||
}
|
||||
|
||||
.resize-handle:hover,
|
||||
.resize-handle:active {
|
||||
}
|
||||
|
||||
.resize-handle:hover,
|
||||
.resize-handle:active {
|
||||
background-color: var(--interactive-accent);
|
||||
}
|
||||
|
||||
|
||||
/* 长文本单元格样式 */
|
||||
td input:focus {
|
||||
}
|
||||
|
||||
/* 长文本单元格样式 */
|
||||
td input:focus {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
/* 表格容器,允许水平滚动 */
|
||||
.table-container {
|
||||
}
|
||||
|
||||
/* 表格容器,允许水平滚动 */
|
||||
.table-container {
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
/* 标签页样式 */
|
||||
.csv-tabs {
|
||||
}
|
||||
|
||||
/* 标签页样式 */
|
||||
.csv-tabs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.csv-tab-headers {
|
||||
}
|
||||
|
||||
.csv-tab-headers {
|
||||
display: flex;
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.csv-tab-header {
|
||||
}
|
||||
|
||||
.csv-tab-header {
|
||||
padding: 8px 16px;
|
||||
cursor: pointer;
|
||||
border-bottom: 2px solid transparent;
|
||||
margin-right: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.csv-tab-header:hover {
|
||||
}
|
||||
|
||||
.csv-tab-header:hover {
|
||||
color: var(--interactive-accent);
|
||||
}
|
||||
|
||||
.csv-tab-header.csv-tab-active {
|
||||
}
|
||||
|
||||
.csv-tab-header.csv-tab-active {
|
||||
color: var(--interactive-accent);
|
||||
border-bottom-color: var(--interactive-accent);
|
||||
}
|
||||
|
||||
.csv-tab-panel {
|
||||
}
|
||||
|
||||
.csv-tab-panel {
|
||||
display: none;
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
.csv-tab-panel-active {
|
||||
}
|
||||
|
||||
.csv-tab-panel-active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* CSV导入导出选项样式 */
|
||||
.csv-export-import {
|
||||
}
|
||||
|
||||
/* CSV导入导出选项样式 */
|
||||
.csv-export-import {
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px dashed var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.csv-delimiter-container {
|
||||
}
|
||||
|
||||
.csv-delimiter-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
/* 表头行样式 */
|
||||
.csv-header-row th {
|
||||
}
|
||||
|
||||
/* 表头行样式 */
|
||||
.csv-header-row th {
|
||||
background-color: var(--interactive-accent);
|
||||
color: var(--text-on-accent);
|
||||
}
|
||||
|
||||
/* 解析器设置样式 */
|
||||
}
|
||||
|
||||
/* 解析器设置样式 暂时注释掉 */
|
||||
.csv-parser-settings {
|
||||
margin-bottom: 16px;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 6px;
|
||||
background-color: var(--background-secondary);
|
||||
display: none;
|
||||
margin-bottom: 16px;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 6px;
|
||||
background-color: var(--background-secondary);
|
||||
}
|
||||
|
||||
.csv-parser-settings .setting-item {
|
||||
border: none;
|
||||
padding: 8px 0;
|
||||
border: none;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.csv-parser-settings .setting-item-info {
|
||||
margin-right: 16px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.csv-parser-settings .setting-item-control input {
|
||||
max-width: 200px;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue