分类面板-分类下属文章、(未完)

This commit is contained in:
Chen 2025-05-03 08:55:52 +08:00
parent a34aec74bf
commit d1fc492918
9 changed files with 359 additions and 11 deletions

View file

@ -6,7 +6,12 @@
"get": "get",
"empty": "Cannot be empty",
"input": "Please input",
"enter": "(Press Enter to add)"
"enter": "(Press Enter to add)",
"loading": "Loading...",
"previousPage": "Previous Page",
"nextPage": "Next Page",
"noDataFound": "No data found",
"errorOccurred": "An error occurred"
},
"sync": {
"title": "Sync to Typecho",
@ -19,7 +24,9 @@
"noFileSelected": "No file selected",
"titleOrContentCannotBeEmpty": "Title or content cannot be empty",
"readFileError": "Read file error, please check the file content",
"noTypechoUser": "No Typecho user, please input the user who uses the operation"
"noTypechoUser": "No Typecho user, please input the user who uses the operation",
"failedToFetchArticles": "Failed to fetch articles",
"failedToFetchPostDetails": "Failed to fetch post details"
},
"field": {
"title": "title",

View file

@ -6,7 +6,12 @@
"get": "获取",
"empty": "不能为空",
"input": "请输入",
"enter": "(按回车添加)"
"enter": "(按回车添加)",
"loading": "加载中...",
"previousPage": "上一页",
"nextPage": "下一页",
"noDataFound": "没有找到数据",
"errorOccurred": "发生错误"
},
"sync": {
"title": "同步到Typecho",
@ -19,7 +24,9 @@
"noFileSelected": "未选择文件",
"titleOrContentCannotBeEmpty": "标题或内容不能为空",
"readFileError": "读取文件错误,请检查文件内容",
"noTypechoUser": "未设置 Typecho 用户"
"noTypechoUser": "未设置 Typecho 用户",
"failedToFetchArticles": "获取文章列表失败",
"failedToFetchPostDetails": "获取文章详情失败"
},
"field": {
"title": "标题",

View file

@ -1,6 +1,6 @@
import { Plugin, addIcon } from "obsidian";
import { settingTab } from "./setting/setting_tab";
import { PushModal } from "./view/push_modal";
import { PushModal } from "./view/push";
import { CategoryView, VIEW_TYPE as ArticleViewType } from "./view/categories";
import {
TypechoPluginSettings,

View file

@ -109,3 +109,132 @@
background: #333;
border-left: 1px dashed #444;
}
.browse-articles-modal {
max-width: 800px;
margin: 0 auto;
}
.browse-articles-modal .browse-article-item {
padding: 1rem;
border-bottom: 1px solid var(--background-modifier-border);
transition: background-color 0.2s;
}
.browse-articles-modal .browse-article-item:hover {
background-color: var(--background-secondary);
}
.browse-articles-modal .article-title {
font-size: 1.1rem;
margin-bottom: 0.5rem;
}
.browse-articles-modal .article-title .download-icon {
margin-left: 1rem;
font-size: 0.9rem;
color: var(--text-muted);
cursor: pointer;
}
.browse-articles-modal .article-title a {
color: var(--text-link);
text-decoration: none;
}
.browse-articles-modal .article-date {
color: var(--text-muted);
font-size: 0.9em;
}
.browse-articles-modal .pagination {
display: flex;
justify-content: center;
align-items: center;
margin-top: 1.5rem;
gap: 1rem;
}
.browse-articles-modal .pagination button {
padding: 0.4rem 0.8rem;
border-radius: var(--radius);
border: 1px solid var(--background-modifier-border);
background-color: var(--background-primary);
color: var(--text-normal);
cursor: pointer;
transition: all 0.2s;
}
.browse-articles-modal .pagination button:hover:not(:disabled) {
background-color: var(--interactive-accent);
color: var(--background-primary);
border-color: var(--interactive-accent);
}
.browse-articles-modal .pagination button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.browse-articles-modal .no-articles {
padding: 2rem;
text-align: center;
color: var(--text-muted);
}
.preview-popup {
position: absolute;
background-color: var(--background-secondary);
color: var(--text-normal);
border: 1px solid var(--background-modifier-border);
border-radius: var(--radius);
padding: 0.75rem 1rem;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
z-index: 9999;
font-size: 0.9em;
max-width: 300px;
word-break: break-word;
transition: opacity 0.2s ease-in-out;
}
.preview-popup::after {
content: "";
position: absolute;
top: -6px;
left: 16px;
border-width: 0 6px 6px 6px;
border-style: solid;
border-color: transparent transparent var(--background-secondary) transparent;
}
.article-preview-modal {
max-width: 80vw;
max-height: 80vh;
overflow: auto;
opacity: 1;
transform: scale(1);
transition: opacity 0.3s ease, transform 0.3s ease;
}
/* 初始状态:透明 + 缩小 */
.article-preview-modal.fade-enter {
opacity: 0;
transform: scale(0.95);
}
/* 进入动画激活态 */
.article-preview-modal.fade-enter-active {
opacity: 1;
transform: scale(1);
}
/* 退出动画激活态 */
.article-preview-modal.fade-exit {
opacity: 1;
transform: scale(1);
}
.article-preview-modal.fade-exit-active {
opacity: 0;
transform: scale(0.95);
}

38
view/article_preview.ts Normal file
View file

@ -0,0 +1,38 @@
import { App, Modal } from "obsidian";
export class ArticlePreview extends Modal {
private animationTimeout: number | null = null;
private htmlContent: string;
constructor(app: App, htmlContent: string) {
super(app);
this.htmlContent = htmlContent;
}
onOpen() {
const { contentEl } = this;
contentEl.empty();
contentEl.addClass("article-preview-modal");
// 初始状态:透明 + 缩小
contentEl.addClass("fade-enter");
contentEl.innerHTML = this.htmlContent;
// 延迟触发进入动画,确保浏览器能识别初始状态
requestAnimationFrame(() => {
contentEl.removeClass("fade-enter");
contentEl.addClass("fade-enter-active");
});
}
onClose() {
const { contentEl } = this;
contentEl.addClass("fade-exit");
contentEl.removeClass("fade-enter-active");
// 设置动画持续时间,确保动画完成后再清空内容
this.animationTimeout = window.setTimeout(() => {
contentEl.empty();
}, 300); // 与 CSS 中的 transition 时间匹配
}
}

168
view/browse_articles.ts Normal file
View file

@ -0,0 +1,168 @@
import { App, Modal, htmlToMarkdown, setIcon } from "obsidian";
import { HttpUtils } from "../utils/request";
import i18n from "../utils/i18n";
import { ArticlePreview } from "./article_preview";
export class BrowseArticles extends Modal {
data: any;
currentPage = 1;
totalPages = 1;
contentContainer: HTMLElement;
constructor(app: App, data: any) {
super(app);
this.data = data;
this.modalEl.addClass("browse-articles-modal");
}
async onOpen() {
const { contentEl } = this;
contentEl.empty();
const header = contentEl.createEl("div", { cls: "modal-header" });
header.createEl("h2", {
text: `${this.data.name}-${i18n.t("field.article")}`,
});
// 创建内容容器
this.contentContainer = contentEl.createEl("div", {
cls: "modal-content",
});
await this.loadPage(this.currentPage);
}
async loadPage(page: number) {
this.contentContainer.empty();
const loadingEl = this.contentContainer.createEl("div", {
text: i18n.t("common.loading"),
});
try {
const params = {
page: page.toString(),
pageSize: "10",
filterType: "category",
filterSlug: this.data.slug,
};
let url = "/posts?";
for (const key in params) {
url += `${key}=${params[key as keyof typeof params]}&`;
}
const response = await HttpUtils.get(url);
if (
response.status === "success" &&
response.data?.dataSet?.length > 0
) {
loadingEl.setText("");
this.totalPages = response.data.pages;
this.currentPage = page;
response.data.dataSet.forEach((post: any) => {
const postEl = this.contentContainer.createEl("div", {
cls: "browse-article-item",
});
// 标题部分
const titleEl = postEl.createEl("div", {
cls: "article-title",
});
titleEl.createEl("a", {
text: post.title,
href: post.permalink,
attr: { target: "_blank" },
});
// 日期部分
postEl.createEl("div", {
cls: "article-date",
text: `${post.year}-${post.month}-${post.day}`,
});
// 保存本地
const downloadIcon = titleEl.createEl("span", {
cls: "download-icon",
});
setIcon(downloadIcon, "hard-drive-download");
downloadIcon.addEventListener("click", async (e) => {
const content = htmlToMarkdown(post.digest).toString();
// 保存本地根目录
const rootPath = this.app.vault.getRoot().path;
const fileName = `${post.title}.md`;
const filePath = `${rootPath}/${fileName}`;
this.app.vault.create(filePath, content)
.then(() => {
console.log('ok')
// this.app.workspace.openLinkText(filePath, "");
});
});
// 点击预览内容
postEl.addEventListener("click", (e) => {
const target = e.target as HTMLElement;
if (target.closest("a") || target.closest("svg")) {
return;
}
e.stopPropagation();
e.preventDefault();
new ArticlePreview(this.app, post.digest).open();
});
});
// 清除旧的分页控件再重新渲染
this.paginationEl?.remove();
this.renderPagination();
} else {
this.contentContainer.createEl("p", {
cls: "no-articles",
text: i18n.t("field.no_articles_found"),
});
}
} catch (error) {
loadingEl.setText("");
this.contentContainer.createEl("p", {
text: i18n.t("common.error_occurred"),
});
console.error("Failed to fetch articles:", error);
}
}
private paginationEl: HTMLElement | null = null;
renderPagination() {
// 先清除旧的分页控件
this.paginationEl?.remove();
// 创建新的分页控件并保存引用
this.paginationEl = this.contentContainer.createEl("div", {
cls: "pagination",
});
const prevBtn = this.paginationEl.createEl("button", {
text: "上一页",
});
prevBtn.disabled = this.currentPage <= 1;
prevBtn.addEventListener("click", () => {
if (this.currentPage > 1) {
this.loadPage(this.currentPage - 1);
}
});
this.paginationEl.createEl("span", {
text: `${this.currentPage} 页 / 共 ${this.totalPages}`,
});
const nextBtn = this.paginationEl.createEl("button", {
text: "下一页",
});
nextBtn.disabled = this.currentPage >= this.totalPages;
nextBtn.addEventListener("click", () => {
if (this.currentPage < this.totalPages) {
this.loadPage(this.currentPage + 1);
}
});
}
onClose() {
const { contentEl } = this;
contentEl.empty();
}
}

View file

@ -4,9 +4,9 @@ import i18n from "../utils/i18n";
import { getSettings } from "../main";
import { HttpUtils } from "../utils/request";
import { Util } from "../utils/util";
import { BrowseArticles } from "./browse_articles";
const VIEW_TYPE = "category-view";
class CategoryView extends ItemView {
getViewType(): string {
return VIEW_TYPE;
@ -112,7 +112,7 @@ class CategoryView extends ItemView {
linkIcon.addEventListener("click", (e) => {
e.stopPropagation();
window.open(node.url);
})
});
// 显示节点标签
wrapper.createEl("span", {
text: node.name,
@ -127,9 +127,8 @@ class CategoryView extends ItemView {
setIcon(articleIcon, "newspaper");
articleIcon.addEventListener("click", (e) => {
console.log(e)
console.log(node)
})
new BrowseArticles(this.app, node).open();
});
}
// 初始展开

View file

@ -1,6 +1,6 @@
import { App, Modal, Notice } from "obsidian";
import { getSettings } from "../main";
import { addMetas } from "./add_metas";
import { addMetas } from "./add_metasl";
import { HttpUtils } from "../utils/request";
import i18n from "../utils/i18n";
export class PushModal extends Modal {