Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
793 changes: 791 additions & 2 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"date-fns": "^4.1.0",
"highlight.js": "^11.11.1",
"katex": "^0.16.45",
"md-editor-v3": "^6.5.0",
"mermaid": "^11.14.0",
"prettier": "^3.6.2",
"vue": "^3.5.24",
Expand All @@ -41,4 +42,4 @@
"optionalDependencies": {
"@rollup/rollup-linux-x64-gnu": "^4.60.1"
}
}
}
23 changes: 23 additions & 0 deletions src/i18n/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,29 @@ export default {
off: "Aus",
},
},
royterEditor: {
title: "Royter Markdown-Editor",
refreshWorks: "Aktualisieren",
workListAria: "Werkliste",
searchPlaceholder: "Werke suchen",
emptyWorks: "Keine bearbeitbaren Werke.",
subjectPlaceholder: "Titel",
save: "Speichern",
previewTitle: "Vorschau",
rendering: "Wird gerendert...",
selectWork: "Wählen Sie links ein Werk aus.",
loginRequiredTitle: "Anmeldung erforderlich",
loginRequiredContent: "Melden Sie sich an, bevor Sie Werke bearbeiten.",
login: "Anmelden",
saveSuccess: "Gespeichert",
emptyPreview: "Kein Inhalt",
untitled: "Ohne Titel",
noPermission: "Dieses Konto kann dieses Werk nicht bearbeiten.",
readSummaryFailed: "Werkzusammenfassung konnte nicht gelesen werden: {status}",
fetchWorksFailed: "Werke konnten nicht geladen werden: {status}",
readWorkspaceFailed: "Arbeitsbereich konnte nicht gelesen werden: {status}",
saveWorkFailed: "Werk konnte nicht gespeichert werden: {status}",
},
footer: {
home: "Startseite",
blackHole: "Schwarzes Loch",
Expand Down
23 changes: 23 additions & 0 deletions src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,29 @@ export default {
off: "Off",
},
},
royterEditor: {
title: "Royter Markdown Editor",
refreshWorks: "Refresh",
workListAria: "Work list",
searchPlaceholder: "Search works",
emptyWorks: "No editable works.",
subjectPlaceholder: "Title",
save: "Save",
previewTitle: "Preview",
rendering: "Rendering...",
selectWork: "Select a work on the left.",
loginRequiredTitle: "Login required",
loginRequiredContent: "Log in before editing works.",
login: "Login",
saveSuccess: "Saved",
emptyPreview: "No content",
untitled: "Untitled",
noPermission: "This account cannot edit this work.",
readSummaryFailed: "Failed to read work summary: {status}",
fetchWorksFailed: "Failed to fetch works: {status}",
readWorkspaceFailed: "Failed to read workspace: {status}",
saveWorkFailed: "Failed to save work: {status}",
},
footer: {
home: "Home",
blackHole: "Black Hole",
Expand Down
23 changes: 23 additions & 0 deletions src/i18n/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,29 @@ export default {
off: "Désactivé",
},
},
royterEditor: {
title: "Éditeur Markdown Royter",
refreshWorks: "Actualiser",
workListAria: "Liste des œuvres",
searchPlaceholder: "Rechercher des œuvres",
emptyWorks: "Aucune œuvre modifiable.",
subjectPlaceholder: "Titre",
save: "Enregistrer",
previewTitle: "Aperçu",
rendering: "Rendu...",
selectWork: "Sélectionnez une œuvre à gauche.",
loginRequiredTitle: "Connexion requise",
loginRequiredContent: "Connectez-vous avant de modifier des œuvres.",
login: "Connexion",
saveSuccess: "Enregistré",
emptyPreview: "Aucun contenu",
untitled: "Sans titre",
noPermission: "Ce compte ne peut pas modifier cette œuvre.",
readSummaryFailed: "Impossible de lire le résumé de l’œuvre : {status}",
fetchWorksFailed: "Impossible de charger les œuvres : {status}",
readWorkspaceFailed: "Impossible de lire l’espace de travail : {status}",
saveWorkFailed: "Impossible d’enregistrer l’œuvre : {status}",
},
footer: {
home: "Accueil",
blackHole: "Trou noir",
Expand Down
23 changes: 23 additions & 0 deletions src/i18n/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,29 @@ export default {
off: "オフ",
},
},
royterEditor: {
title: "Royter Markdown エディター",
refreshWorks: "更新",
workListAria: "作品リスト",
searchPlaceholder: "作品を検索",
emptyWorks: "編集できる作品がありません。",
subjectPlaceholder: "タイトル",
save: "保存",
previewTitle: "プレビュー",
rendering: "レンダリング中...",
selectWork: "左側から作品を選択してください。",
loginRequiredTitle: "ログインが必要です",
loginRequiredContent: "作品を編集する前にログインしてください。",
login: "ログイン",
saveSuccess: "保存しました",
emptyPreview: "内容がありません",
untitled: "無題",
noPermission: "このアカウントではこの作品を編集できません。",
readSummaryFailed: "作品概要の読み込みに失敗しました: {status}",
fetchWorksFailed: "作品一覧の取得に失敗しました: {status}",
readWorkspaceFailed: "ワークスペースの読み込みに失敗しました: {status}",
saveWorkFailed: "作品の保存に失敗しました: {status}",
},
footer: {
home: "ホーム",
blackHole: "ブラックホール",
Expand Down
23 changes: 23 additions & 0 deletions src/i18n/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,29 @@ export default {
off: "关闭",
},
},
royterEditor: {
title: "Royter Markdown 编辑器",
refreshWorks: "刷新作品",
workListAria: "作品列表",
searchPlaceholder: "搜索作品",
emptyWorks: "没有可编辑的作品。",
subjectPlaceholder: "标题",
save: "保存",
previewTitle: "预览",
rendering: "渲染中...",
selectWork: "请从左侧选择作品。",
loginRequiredTitle: "需要登录",
loginRequiredContent: "请先登录后再编辑作品。",
login: "去登录",
saveSuccess: "保存成功",
emptyPreview: "暂无内容",
untitled: "未命名",
noPermission: "当前账号没有编辑该作品的权限。",
readSummaryFailed: "读取作品摘要失败:{status}",
fetchWorksFailed: "获取作品列表失败:{status}",
readWorkspaceFailed: "读取工作区失败:{status}",
saveWorkFailed: "保存作品失败:{status}",
},
footer: {
home: "首页",
blackHole: "黑洞",
Expand Down
6 changes: 6 additions & 0 deletions src/router/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,12 @@ const routes = [
component: () => import("../views/Settings.vue"),
meta: { keepAlive: false },
},
{
path: "/royter",
name: "RoyterMarkdownEditor",
component: () => import("../views/RoyterMarkdownEditor.vue"),
meta: { keepAlive: false },
},
{
path: "/:catchAll(.*)",
component: () => import("../views/NotFound.vue"),
Expand Down
168 changes: 168 additions & 0 deletions src/services/editor/cloudWorks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import i18n from "@i18n/index";
import { getData } from "@services/api/getData.ts";
import storageManager from "@storage/index.ts";
import type {
Category,
ExperimentQuery,
Result,
Summary,
UserInfo,
Workspace,
} from "@services/../pl-serve-type-main/type/main";

export type EditorWork = {
id: string;
contentId: string;
category: Category;
subject: string;
markdown: string;
language: string;
rawSummary: Summary;
};

export type SaveEditorWorkResult = {
requestBody: Record<string, unknown>;
response: Result<Summary> | Result<unknown>;
};

const EDITABLE_VERIFICATIONS = new Set(["Editor", "Administrator"]);

function t(key: string, params?: Record<string, unknown>): string {
return i18n.global.t(key, params || {}) as string;
}

function getCurrentUser(): UserInfo | null {
return storageManager.getObj("userInfo").value as UserInfo | null;
}

export function getCurrentUserId(): string {
return getCurrentUser()?.ID || "";
}

export function canEditSummary(summary: Summary): boolean {
const currentUser = getCurrentUser();
if (!currentUser?.ID) return false;
if (summary.User?.ID === currentUser.ID) return true;
if (summary.Coauthors?.some((user) => user.ID === currentUser.ID)) return true;
return EDITABLE_VERIFICATIONS.has(String(currentUser.Verification || ""));
}

function normalizeDescription(description: Summary["Description"]): string {
if (Array.isArray(description)) return description.join("\n");
return "";
}

function toEditorWork(summary: Summary): EditorWork {
return {
id: summary.ID,
contentId: summary.ContentID || summary.ID,
category: summary.Category || "Discussion",
subject: summary.Subject || t("royterEditor.untitled"),
markdown: normalizeDescription(summary.Description),
language: summary.Language || "Chinese",
rawSummary: summary,
};
}

async function fetchSummary(category: Category, id: string): Promise<Summary> {
const res = await getData("/Contents/GetSummary", {
ContentID: id,
Category: category,
});
if (res.Status !== 200 || !res.Data) {
throw new Error(
res.Message || t("royterEditor.readSummaryFailed", { status: res.Status }),
);
}
return res.Data;
}

export async function fetchEditableWorks(take = 50): Promise<EditorWork[]> {
const userId = getCurrentUserId();
if (!userId) return [];

const query: ExperimentQuery = {
Category: "Discussion",
Languages: [],
ExcludeLanguages: [],
Tags: [],
ExcludeTags: [],
ModelTags: [],
ModelID: undefined,
ParentID: undefined,
UserID: userId,
Special: null,
From: undefined,
Skip: 0,
Take: take,
Days: 0,
Sort: 0,
ShowAnnouncement: false,
};

const res = await getData("/Contents/QueryExperiments", { Query: query });
if (res.Status !== 200) {
throw new Error(
res.Message || t("royterEditor.fetchWorksFailed", { status: res.Status }),
);
}

const summaries = res.Data?.$values || [];
const detailed = await Promise.all(
summaries.map((item) => fetchSummary((item.Category || "Discussion") as Category, item.ID)),
);
return detailed.filter(canEditSummary).map(toEditorWork);
}

export async function fetchWorkspace(work: EditorWork): Promise<Workspace | null> {
const res = await getData("/Contents/GetWorkspace", {
ContentID: work.contentId,
Language: work.language || "Chinese",
});
if (res.Status !== 200) {
throw new Error(
res.Message || t("royterEditor.readWorkspaceFailed", { status: res.Status }),
);
}
return res.Data || null;
}

export async function saveEditorWork(
work: EditorWork,
markdown: string,
subject: string,
): Promise<SaveEditorWorkResult> {
if (!canEditSummary(work.rawSummary)) {
throw new Error(t("royterEditor.noPermission"));
}

const workspace = await fetchWorkspace(work);
const summary: Summary = {
...work.rawSummary,
Subject: subject.trim() || work.subject,
Description: markdown.split("\n"),
Language: work.language as Summary["Language"],
};

const requestBody: Record<string, unknown> = {
Summary: summary,
Workspace: workspace ? { ...workspace, Summary: null } : null,
};

const response = (await getData(
"/Contents/SubmitExperiment" as any,
requestBody,
)) as Result<Summary>;
if (response.Status !== 200) {
throw new Error(
response.Message ||
t("royterEditor.saveWorkFailed", { status: response.Status }),
);
}

work.subject = summary.Subject || work.subject;
work.markdown = markdown;
work.rawSummary = summary;

return { requestBody, response };
}
Loading