From a34aec74bf300022ac66219d7bd671700a250333 Mon Sep 17 00:00:00 2001 From: Chen Date: Thu, 1 May 2025 18:42:09 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=86=E7=B1=BB=E9=9D=A2=E6=9D=BF=E4=BF=AE?= =?UTF-8?q?=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- locales/en.json | 3 +- locales/zh.json | 3 +- main.ts | 4 +- styles.css | 96 +++++++++++++++++++++++---- utils/util.ts | 79 +++++++++++++++++++++++ view/article_view.ts | 104 ------------------------------ view/categories.ts | 150 +++++++++++++++++++++++++++++++++++++++++++ view/mockData.ts | 31 --------- 8 files changed, 319 insertions(+), 151 deletions(-) create mode 100644 utils/util.ts delete mode 100644 view/article_view.ts create mode 100644 view/categories.ts delete mode 100644 view/mockData.ts diff --git a/locales/en.json b/locales/en.json index 1406533..2233f66 100644 --- a/locales/en.json +++ b/locales/en.json @@ -18,7 +18,8 @@ "requestFailed": "Request failed, please check the network or API configuration", "noFileSelected": "No file selected", "titleOrContentCannotBeEmpty": "Title or content cannot be empty", - "readFileError": "Read file error, please check the file content" + "readFileError": "Read file error, please check the file content", + "noTypechoUser": "No Typecho user, please input the user who uses the operation" }, "field": { "title": "title", diff --git a/locales/zh.json b/locales/zh.json index 48de840..ebad838 100644 --- a/locales/zh.json +++ b/locales/zh.json @@ -18,7 +18,8 @@ "requestFailed": "请求失败,请检查网络或 API 配置", "noFileSelected": "未选择文件", "titleOrContentCannotBeEmpty": "标题或内容不能为空", - "readFileError": "读取文件错误,请检查文件内容" + "readFileError": "读取文件错误,请检查文件内容", + "noTypechoUser": "未设置 Typecho 用户" }, "field": { "title": "标题", diff --git a/main.ts b/main.ts index 1932419..b5b2c8e 100644 --- a/main.ts +++ b/main.ts @@ -1,7 +1,7 @@ import { Plugin, addIcon } from "obsidian"; import { settingTab } from "./setting/setting_tab"; import { PushModal } from "./view/push_modal"; -import { ArticleView, VIEW_TYPE as ArticleViewType } from "./view/article_view"; +import { CategoryView, VIEW_TYPE as ArticleViewType } from "./view/categories"; import { TypechoPluginSettings, DEFAULT_SETTINGS, @@ -28,7 +28,7 @@ export default class TypechoPlugin extends Plugin { } ); - this.registerView(ArticleViewType, (leaf) => new ArticleView(leaf)); + this.registerView(ArticleViewType, (leaf) => new CategoryView(leaf)); this.openPluginView(); } diff --git a/styles.css b/styles.css index 7cf345d..9d2a15a 100644 --- a/styles.css +++ b/styles.css @@ -1,17 +1,24 @@ .tree-container { - font-family: sans-serif; - color: #ccc; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, + sans-serif; + color: #e0e0e0; + padding: 8px; } + .tree-node-list { list-style: none; padding-left: 0; -} -.tree-children { - margin-top: 4px; + margin: 0; } -.tree-node-label { - font-size: 14px; +.tree-node-item { + position: relative; + margin-bottom: 4px; +} + +.tree-children { + margin-top: 4px; + transition: all 0.2s ease-in-out; } .tree-node-wrapper { @@ -19,21 +26,86 @@ align-items: center; cursor: pointer; user-select: none; + padding: 6px 8px; + border-radius: 6px; + transition: background-color 0.2s ease-in-out; } -.tree-file-icon { - margin-right: 8px; - font-size: 14px; - color: #888; +.tree-node-wrapper:hover { + background-color: #2a2a2a; } .tree-toggle-icon { margin-right: 8px; font-size: 12px; - user-select: none; + color: #888; + transition: transform 0.2s ease-in-out; cursor: pointer; } +.tree-toggle-icon.expanded { + transform: rotate(90deg); + color: #bbb; +} + +.tree-article-icon { + margin-right: 6px; + color: #66afee; + font-size: 14px; + transition: color 0.2s ease-in-out; +} + +.tree-node-wrapper:hover .tree-article-icon { + color: #88caff; +} + +.tree-link-icon { + margin-right: 6px; + color: #3c9cf0; + font-size: 14px; + transition: color 0.2s ease-in-out; +} + +.tree-node-wrapper:hover .tree-link-icon { + color: #008cff; +} + .tree-node-label { font-size: 14px; + flex: 1; + transition: color 0.2s ease-in-out; +} + +.tree-indent { + position: absolute; + left: 0; + top: 50%; + transform: translateY(-50%); + width: 16px; + height: 16px; +} + +.tree-indent::before { + content: ""; + position: absolute; + bottom: 0; + left: 8px; + width: 1px; + background: #444; +} + +.tree-node-list ul { + padding-left: 16px; + position: relative; +} + +.tree-node-list ul::before { + content: ""; + position: absolute; + top: 0; + left: 8px; + bottom: 0; + width: 1px; + background: #333; + border-left: 1px dashed #444; } diff --git a/utils/util.ts b/utils/util.ts new file mode 100644 index 0000000..1630a20 --- /dev/null +++ b/utils/util.ts @@ -0,0 +1,79 @@ +// 定义树节点接口 +export interface TreeNode { + id?: number; + [key: string]: any; + children: TreeNode[]; + parent?: TreeNode; +} + +export const Util = { + // tree + treeUtil: { + generateTreeData: function>( + data: T[], + nodeId: keyof T, + parentNodeKey: keyof T + ): TreeNode[] { + // 将数据存储为以 nodeId 为 KEY 的 map 索引数据列 + const map: Record = {}; + data.forEach((item) => { + (item as unknown as TreeNode).children = []; + map[item[nodeId] as string | number] = item as unknown as TreeNode; + }); + + // 遍历数据,构建树形结构 + const rootNodes: TreeNode[] = []; + data.forEach((item) => { + const parent = map[item[parentNodeKey] as string | number]; + if (parent) { + parent.children.push(item as unknown as TreeNode); + (item as unknown as TreeNode).parent = parent; + } else { + rootNodes.push(item as unknown as TreeNode); + } + }); + + return rootNodes; + }, + // 在树形结构中查找指定id节点 + findNodeInTree: function(tree: TreeNode[], id: number): TreeNode | undefined { + for (let i = 0; i < tree.length; i++) { + if (tree[i].id === id) { + return tree[i]; + } else { + if (tree[i].children) { + const node = this.findNodeInTree(tree[i].children, id); + if (node) { + return node; + } + } + } + } + return undefined; + }, + // 在树形结构中查找指定id节点的上一个父节点 + findParentNodeInTree: function(tree: TreeNode[], id: number): TreeNode | null { + // Id等于0的节点为根节点,没有父节点 + // id找到了的话,往上找一层父节点返回 + if (id === 0) { + return null; + } + for (let i = 0; i < tree.length; i++) { + if (tree[i].id === id) { + return tree[i].parent || null; + } else { + if (tree[i].children) { + const node = this.findParentNodeInTree( + tree[i].children, + id + ); + if (node) { + return node; + } + } + } + } + return null; + }, + }, +}; \ No newline at end of file diff --git a/view/article_view.ts b/view/article_view.ts deleted file mode 100644 index ab2b78f..0000000 --- a/view/article_view.ts +++ /dev/null @@ -1,104 +0,0 @@ -// main.ts -import { ItemView } from "obsidian"; -import i18n from "../utils/i18n"; -import { mockTreeData } from "./mockData"; - -const VIEW_TYPE = "article-view"; - -class ArticleView extends ItemView { - getViewType(): string { - return VIEW_TYPE; - } - - getDisplayText() { - return "我的插件面板"; - } - - getIcon(): string { - return "typecho"; - } - - async onOpen() { - const container = this.containerEl.children[1]; - container.empty(); - - // 标题 - container.createEl("h4", { text: i18n.t("field.article") }); - - // 渲染树结构 - const treeContainer = container.createDiv({ cls: "tree-container" }); - this.renderTree(treeContainer, mockTreeData); - } - - renderTree(container: HTMLElement, nodes: any[], depth = 0) { - const ul = container.createEl("ul", { cls: "tree-node-list" }); - - for (const node of nodes) { - const li = ul.createEl("li"); - const wrapper = li.createDiv({ cls: "tree-node-wrapper" }); - - // 缩进 - const indent = wrapper.createSpan({ cls: "tree-indent" }); - indent.style.paddingLeft = `${depth * 16}px`; - - // 判断并添加展开/折叠图标 - if (node.children && node.children.length > 0) { - const toggleIcon = wrapper.createEl("span", { - text: node.isExpanded ? "▼" : "▶", - cls: "tree-toggle-icon", - }); - - toggleIcon.addEventListener("click", (e) => { - e.stopPropagation(); - node.isExpanded = !node.isExpanded; - toggleIcon.setText(node.isExpanded ? "▼" : "▶"); - - if (node.isExpanded) { - const childContainer = - li.querySelector(".tree-children") || - li.createEl("div", { cls: "tree-children" }); - childContainer.empty(); - this.renderTree( - childContainer, - node.children, - depth + 1 - ); - } else { - const childContainer = - li.querySelector(".tree-children"); - if (childContainer) childContainer.remove(); - } - }); - - // 判断并添加文件图标(仅当 count > 0) - if (node.count && node.count > 0) { - const fileIcon = wrapper.createEl("span", { - cls: "tree-file-icon", - }); - fileIcon.setAttr("aria-label", `含 ${node.count} 个文件`); - fileIcon.setText("📄"); // 替换为 Obsidian 图标:"file" - } - } - - // 显示节点标签 - const label = wrapper.createEl("span", { - text: node.label, - cls: "tree-node-label", - }); - - // 初始展开 - if (node.children && node.children.length > 0 && node.isExpanded) { - const childContainer = li.createEl("div", { - cls: "tree-children", - }); - this.renderTree(childContainer, node.children, depth + 1); - } - } - } - - async onClose() { - // 清理资源 - } -} - -export { ArticleView, VIEW_TYPE }; diff --git a/view/categories.ts b/view/categories.ts new file mode 100644 index 0000000..dd7990c --- /dev/null +++ b/view/categories.ts @@ -0,0 +1,150 @@ +// main.ts +import { ItemView, setIcon } from "obsidian"; +import i18n from "../utils/i18n"; +import { getSettings } from "../main"; +import { HttpUtils } from "../utils/request"; +import { Util } from "../utils/util"; + +const VIEW_TYPE = "category-view"; + +class CategoryView extends ItemView { + getViewType(): string { + return VIEW_TYPE; + } + + getDisplayText() { + return i18n.t("field.category"); + } + + getIcon(): string { + return "typecho"; + } + + async onOpen() { + const container = this.containerEl.children[1]; + container.empty(); + + // 标题 + container.createEl("h4", { text: i18n.t("field.category") }); + + const refreshButton = container.createEl("button", { + cls: "refresh-button", + }); + setIcon(refreshButton, "rotate-ccw"); + refreshButton.addEventListener("click", () => { + this.initCategory(container); + }); + + this.initCategory(container); + } + + async initCategory(container: Element) { + for (let i = 0; i < container.children.length; i++) { + if (i != 0 && i != 1) { + container.children[i].remove(); + } + } + + // 校验是否选择操作用户 + if (!getSettings().User) { + container.createEl("p", { + text: i18n.t("error.noTypechoUser"), + }); + } else { + const response = await HttpUtils.get("/categories", {}); + const data = Util.treeUtil.generateTreeData( + response.data, + "mid", + "parent" + ); + // 渲染树结构 + const treeContainer = container.createDiv({ + cls: "tree-container", + }); + this.renderTree(treeContainer, data); + } + } + renderTree(container: Element, nodes: any[], depth = 0) { + const ul = container.createEl("ul", { cls: "tree-node-list" }); + + for (const node of nodes) { + const li = ul.createEl("li"); + const wrapper = li.createDiv({ cls: "tree-node-wrapper" }); + + // 缩进 + const indent = wrapper.createSpan({ cls: "tree-indent" }); + indent.style.paddingLeft = `${depth * 16}px`; + + // 判断并添加展开/折叠图标 + if (node.children && node.children.length > 0) { + const toggleIcon = wrapper.createEl("span", { + text: node.isExpanded ? "▼" : "▶", + cls: "tree-toggle-icon", + }); + + toggleIcon.addEventListener("click", (e) => { + e.stopPropagation(); + node.isExpanded = !node.isExpanded; + toggleIcon.setText(node.isExpanded ? "▼" : "▶"); + + if (node.isExpanded) { + const childContainer = + li.querySelector(".tree-children") || + li.createEl("div", { cls: "tree-children" }); + childContainer.empty(); + this.renderTree( + childContainer, + node.children, + depth + 1 + ); + } else { + const childContainer = + li.querySelector(".tree-children"); + if (childContainer) childContainer.remove(); + } + }); + } + + const linkIcon = wrapper.createEl("span", { + cls: "tree-link-icon", + }); + setIcon(linkIcon, "link"); + linkIcon.addEventListener("click", (e) => { + e.stopPropagation(); + window.open(node.url); + }) + // 显示节点标签 + wrapper.createEl("span", { + text: node.name, + cls: "tree-node-label", + }); + + // 判断并添加文件图标(仅当 count > 0) + if (node.count && node.count > 0) { + const articleIcon = wrapper.createEl("span", { + cls: "tree-article-icon", + }); + setIcon(articleIcon, "newspaper"); + + articleIcon.addEventListener("click", (e) => { + console.log(e) + console.log(node) + }) + } + + // 初始展开 + if (node.children && node.children.length > 0 && node.isExpanded) { + const childContainer = li.createEl("div", { + cls: "tree-children", + }); + this.renderTree(childContainer, node.children, depth + 1); + } + } + } + + async onClose() { + // 清理资源 + } +} + +export { CategoryView, VIEW_TYPE }; diff --git a/view/mockData.ts b/view/mockData.ts deleted file mode 100644 index a8fa27e..0000000 --- a/view/mockData.ts +++ /dev/null @@ -1,31 +0,0 @@ -// mockData.ts - -export const mockTreeData = [ - { - id: '1', - label: '根节点', - isExpanded: true, - count: 5, // 文件数 - children: [ - { - id: '1-1', - label: '子节点1', - count: 0, // 无文件 - isExpanded: false, - children: [ - { id: '1-1-1', label: '孙节点A', count: 3 }, - { id: '1-1-2', label: '孙节点B' }, // 未设置 count,默认视为 0 - ], - }, - { - id: '1-2', - label: '子节点2', - count: 2, - isExpanded: true, - children: [ - { id: '1-2-1', label: '孙节点C', count: 0 }, - ], - }, - ], - }, - ]; \ No newline at end of file