From 19c1c0da1f0e285055644e65917be6c3081ca98a Mon Sep 17 00:00:00 2001 From: Innei Date: Sun, 24 May 2026 13:31:44 +0800 Subject: [PATCH 1/2] refactor(admin): migrate admin app to React --- CLAUDE.md | 41 +- apps/admin/index.html | 10 +- apps/admin/package.json | 67 +- apps/admin/src/App.tsx | 169 +- apps/admin/src/api/activity.ts | 84 - apps/admin/src/api/aggregate.ts | 115 - apps/admin/src/api/ai-agent.ts | 58 - apps/admin/src/api/ai.ts | 501 --- apps/admin/src/api/analyze.ts | 74 - apps/admin/src/api/auth.ts | 69 - apps/admin/src/api/backup.ts | 43 - apps/admin/src/api/categories.ts | 46 - apps/admin/src/api/comments.ts | 53 - apps/admin/src/api/cron-task.ts | 100 - apps/admin/src/api/debug.ts | 11 - apps/admin/src/api/dependencies.ts | 10 - apps/admin/src/api/drafts.ts | 74 - apps/admin/src/api/enrichment.ts | 109 - apps/admin/src/api/files.ts | 156 - apps/admin/src/api/health.ts | 7 - apps/admin/src/api/index.ts | 30 - apps/admin/src/api/links.ts | 65 - apps/admin/src/api/markdown.ts | 32 - apps/admin/src/api/meta-presets.ts | 49 - apps/admin/src/api/notes.ts | 85 - apps/admin/src/api/options.ts | 51 - apps/admin/src/api/pages.ts | 45 - apps/admin/src/api/posts.ts | 57 - apps/admin/src/api/projects.ts | 41 - apps/admin/src/api/pty.ts | 24 - apps/admin/src/api/readers.ts | 25 - apps/admin/src/api/recently.ts | 28 - apps/admin/src/api/says.ts | 35 - apps/admin/src/api/search-index.ts | 38 - apps/admin/src/api/search.ts | 21 - apps/admin/src/api/serverless.ts | 47 - apps/admin/src/api/snippets.ts | 73 - apps/admin/src/api/subscribe.ts | 41 - apps/admin/src/api/system.ts | 95 - apps/admin/src/api/templates.ts | 28 - apps/admin/src/api/topics.ts | 43 - apps/admin/src/api/user.ts | 131 - apps/admin/src/api/webhooks.ts | 71 - apps/admin/src/app/api/ai.ts | 534 +++ apps/admin/src/app/api/analyze.ts | 85 + apps/admin/src/app/api/backups.ts | 85 + apps/admin/src/app/api/categories.ts | 44 + apps/admin/src/app/api/comments.ts | 52 + apps/admin/src/app/api/cron-tasks.ts | 128 + apps/admin/src/app/api/drafts.ts | 82 + apps/admin/src/app/api/enrichment.ts | 175 + apps/admin/src/app/api/files.ts | 152 + apps/admin/src/app/api/http.ts | 143 + apps/admin/src/app/api/links.ts | 74 + apps/admin/src/app/api/markdown.ts | 49 + apps/admin/src/app/api/notes.ts | 85 + apps/admin/src/app/api/options.ts | 72 + apps/admin/src/app/api/pages.ts | 51 + apps/admin/src/app/api/posts.ts | 76 + apps/admin/src/app/api/projects.ts | 34 + apps/admin/src/app/api/readers.ts | 18 + apps/admin/src/app/api/recently.ts | 23 + apps/admin/src/app/api/says.ts | 25 + apps/admin/src/app/api/search-index.ts | 83 + apps/admin/src/app/api/snippets.ts | 85 + apps/admin/src/app/api/subscribe.ts | 51 + apps/admin/src/app/api/system.ts | 63 + apps/admin/src/app/api/topics.ts | 60 + apps/admin/src/app/api/webhooks.ts | 84 + apps/admin/src/{ => app}/constants/env.ts | 0 apps/admin/src/{ => app}/constants/keys.ts | 0 .../src/app/hooks/use-local-storage-state.ts | 22 + apps/admin/src/{ => app}/models/activity.ts | 0 apps/admin/src/{ => app}/models/ai.ts | 0 apps/admin/src/{ => app}/models/amap.ts | 0 apps/admin/src/{ => app}/models/analyze.ts | 0 apps/admin/src/{ => app}/models/base.ts | 0 apps/admin/src/{ => app}/models/category.ts | 0 apps/admin/src/{ => app}/models/comment.ts | 0 apps/admin/src/{ => app}/models/draft.ts | 0 apps/admin/src/{ => app}/models/enrichment.ts | 0 apps/admin/src/{ => app}/models/link.ts | 0 .../admin/src/{ => app}/models/meta-preset.ts | 0 apps/admin/src/{ => app}/models/note.ts | 0 apps/admin/src/{ => app}/models/options.ts | 0 apps/admin/src/{ => app}/models/page.ts | 0 apps/admin/src/{ => app}/models/post.ts | 0 apps/admin/src/{ => app}/models/project.ts | 0 apps/admin/src/{ => app}/models/recently.ts | 0 apps/admin/src/{ => app}/models/say.ts | 0 .../src/{ => app}/models/search-index.ts | 0 apps/admin/src/{ => app}/models/snippet.ts | 0 apps/admin/src/{ => app}/models/stat.ts | 0 apps/admin/src/{ => app}/models/system.ts | 0 apps/admin/src/{ => app}/models/token.ts | 0 apps/admin/src/{ => app}/models/topic.ts | 0 apps/admin/src/{ => app}/models/user.ts | 0 apps/admin/src/app/providers.tsx | 32 + apps/admin/src/app/query-client.ts | 11 + apps/admin/src/app/routes.tsx | 381 ++ apps/admin/src/app/shell.tsx | 74 + apps/admin/src/{ => app}/socket/types.ts | 7 +- apps/admin/src/app/theme.ts | 62 + apps/admin/src/app/ui/button.tsx | 54 + apps/admin/src/app/ui/checkbox.tsx | 50 + apps/admin/src/app/ui/cn.ts | 5 + apps/admin/src/app/ui/compact-pagination.tsx | 64 + apps/admin/src/app/ui/data-table.tsx | 56 + apps/admin/src/app/ui/metric-card.tsx | 24 + apps/admin/src/app/ui/panel.tsx | 30 + apps/admin/src/app/ui/select.tsx | 75 + apps/admin/src/app/ui/switch.tsx | 44 + apps/admin/src/app/ui/text-field.tsx | 147 + apps/admin/src/{ => app}/utils/authjs/auth.ts | 2 +- apps/admin/src/{ => app}/utils/confetti.ts | 0 .../src/{ => app}/utils/markdown-parser.ts | 4 +- apps/admin/src/{ => app}/utils/time.ts | 0 apps/admin/src/app/views/ai-page.tsx | 1830 ++++++++++ apps/admin/src/app/views/analyze-page.tsx | 323 ++ apps/admin/src/app/views/authn-debug-page.tsx | 101 + apps/admin/src/app/views/backup-page.tsx | 578 +++ apps/admin/src/app/views/categories-page.tsx | 678 ++++ apps/admin/src/app/views/comments-page.tsx | 695 ++++ apps/admin/src/app/views/cron-page.tsx | 753 ++++ apps/admin/src/app/views/dashboard-page.tsx | 83 + apps/admin/src/app/views/drafts-page.tsx | 404 +++ apps/admin/src/app/views/enrichment-page.tsx | 1215 +++++++ .../admin/src/app/views/events-debug-page.tsx | 252 ++ apps/admin/src/app/views/files-page.tsx | 906 +++++ apps/admin/src/app/views/friends-page.tsx | 773 ++++ apps/admin/src/app/views/login-page.tsx | 330 ++ apps/admin/src/app/views/markdown-page.tsx | 527 +++ apps/admin/src/app/views/notes-page.tsx | 304 ++ apps/admin/src/app/views/pages-page.tsx | 193 + apps/admin/src/app/views/posts-page.tsx | 334 ++ apps/admin/src/app/views/projects-page.tsx | 613 ++++ apps/admin/src/app/views/readers-page.tsx | 199 ++ apps/admin/src/app/views/recently-page.tsx | 473 +++ apps/admin/src/app/views/says-page.tsx | 373 ++ .../admin/src/app/views/search-index-page.tsx | 600 ++++ .../src/app/views/serverless-debug-page.tsx | 119 + apps/admin/src/app/views/settings-page.tsx | 407 +++ apps/admin/src/app/views/setup-api-page.tsx | 215 ++ apps/admin/src/app/views/setup-page.tsx | 619 ++++ apps/admin/src/app/views/snippets-page.tsx | 482 +++ apps/admin/src/app/views/subscribe-page.tsx | 446 +++ apps/admin/src/app/views/template-page.tsx | 177 + apps/admin/src/app/views/toast-debug-page.tsx | 275 ++ apps/admin/src/app/views/topics-page.tsx | 848 +++++ apps/admin/src/app/views/webhooks-page.tsx | 854 +++++ apps/admin/src/app/views/write-page.tsx | 1079 ++++++ .../components/ai-task-queue/AiTaskQueue.tsx | 307 -- .../src/components/ai-task-queue/index.ts | 3 - .../src/components/ai-task-queue/types.ts | 30 - .../ai-task-queue/use-ai-task-queue.ts | 273 -- apps/admin/src/components/ai/ai-helper.tsx | 108 - apps/admin/src/components/avatar/Avatar.tsx | 56 - .../src/components/avatar/avatar.module.css | 18 - apps/admin/src/components/avatar/index.tsx | 4 - .../button/header-action-button.tsx | 128 - .../src/components/code-highlight/index.tsx | 30 - .../src/components/config-form/index.tsx | 333 -- .../admin/src/components/config-form/types.ts | 70 - apps/admin/src/components/directives/if.tsx | 25 - .../src/components/draft/diff-preview.tsx | 121 - .../src/components/draft/draft-list-modal.tsx | 176 - .../components/draft/draft-recovery-modal.tsx | 643 ---- .../components/draft/draft-save-indicator.tsx | 122 - .../src/components/draft/file-preview.tsx | 111 - .../components/draft/version-list-item.tsx | 137 - .../components/image-detail-section.tsx | 423 --- .../drawer/components/json-editor.tsx | 54 - .../lexical-image-detail-section.tsx | 218 -- .../drawer/components/meta-preset-section.tsx | 298 -- .../components/preset-field-renderer.tsx | 555 --- .../src/components/drawer/components/ui.tsx | 250 -- .../drawer/lexical-debug-drawer.tsx | 104 - .../components/drawer/text-base-drawer.tsx | 283 -- .../editor/codemirror/ImageDropZone.tsx | 133 - .../editor/codemirror/ImageEditPopover.tsx | 222 -- .../editor/codemirror/codemirror.css | 791 ----- .../editor/codemirror/codemirror.tsx | 196 -- .../editor/codemirror/editor-store.ts | 124 - .../components/editor/codemirror/extension.ts | 37 - .../editor/codemirror/image-popover-state.ts | 26 - .../editor/codemirror/language-icons.ts | 42 - .../editor/codemirror/syntax-highlight.ts | 62 - .../editor/codemirror/upload-store.ts | 60 - .../editor/codemirror/use-auto-fonts.ts | 60 - .../editor/codemirror/use-auto-theme.ts | 41 - .../editor/codemirror/use-codemirror.ts | 328 -- .../codemirror/wysiwyg/block-registry.ts | 90 - .../editor/codemirror/wysiwyg/blockquote.ts | 401 --- .../editor/codemirror/wysiwyg/codeblock.ts | 1577 --------- .../editor/codemirror/wysiwyg/details.ts | 849 ----- .../editor/codemirror/wysiwyg/divider.ts | 55 - .../editor/codemirror/wysiwyg/empty-line.ts | 204 -- .../editor/codemirror/wysiwyg/heading.ts | 112 - .../editor/codemirror/wysiwyg/image.ts | 556 --- .../editor/codemirror/wysiwyg/index.ts | 31 - .../editor/codemirror/wysiwyg/inline.ts | 457 --- .../editor/codemirror/wysiwyg/line-break.ts | 68 - .../editor/codemirror/wysiwyg/list.ts | 273 -- .../editor/codemirror/wysiwyg/math.ts | 303 -- .../editor/codemirror/wysiwyg/measure.ts | 8 - .../src/components/editor/plain/plain.tsx | 48 - .../components/editor/rich/RichDiffBridge.tsx | 71 - .../src/components/editor/rich/RichEditor.tsx | 197 -- .../editor/rich/RichEditorWithAgent.tsx | 317 -- .../editor/rich/agent-chat/AgentChatPanel.tsx | 166 - .../editor/rich/agent-chat/ChatInput.tsx | 140 - .../rich/agent-chat/ChatMessageList.tsx | 245 -- .../rich/agent-chat/ModelSelector.test.tsx | 181 - .../editor/rich/agent-chat/ModelSelector.tsx | 225 -- .../editor/rich/agent-chat/SessionHeader.tsx | 218 -- .../agent-chat/bubbles/DiffReviewBubble.tsx | 155 - .../agent-chat/bubbles/DiffSummaryBubble.tsx | 18 - .../rich/agent-chat/bubbles/ErrorBubble.tsx | 23 - .../agent-chat/bubbles/StreamdownBubble.tsx | 68 - .../rich/agent-chat/bubbles/ThinkingChain.tsx | 86 - .../rich/agent-chat/bubbles/ToolCall.tsx | 260 -- .../rich/agent-chat/bubbles/ToolCallGroup.tsx | 205 -- .../rich/agent-chat/bubbles/UserBubble.tsx | 15 - .../composables/context-aware-engine.ts | 221 -- .../agent-chat/composables/use-agent-loop.ts | 317 -- .../composables/use-agent-reapply.ts | 387 -- .../composables/use-agent-selected-model.ts | 83 - .../agent-chat/composables/use-agent-store.ts | 34 - .../agent-chat/composables/use-meta-tools.ts | 178 - .../composables/use-session-manager.ts | 335 -- .../editor/rich/agent-chat/index.ts | 7 - .../rich/agent-chat/model-selector-recents.ts | 69 - .../src/components/editor/slash-menu/index.ts | 12 - .../editor/slash-menu/slash-menu-extension.ts | 127 - .../editor/slash-menu/slash-menu-items.ts | 331 -- .../editor/slash-menu/slash-menu.css | 40 - .../editor/slash-menu/slash-menu.tsx | 274 -- .../editor/slash-menu/use-slash-menu.ts | 274 -- .../editor/toolbar/emoji-picker.tsx | 54 - .../editor/toolbar/floating-toolbar.css | 14 - .../editor/toolbar/floating-toolbar.tsx | 262 -- .../src/components/editor/toolbar/index.tsx | 4 - .../editor/toolbar/keymap-extension.ts | 72 - .../editor/toolbar/markdown-commands.ts | 419 --- .../src/components/editor/toolbar/toolbar.tsx | 357 -- .../editor/toolbar/use-selection-position.ts | 158 - .../components/editor/universal/constants.ts | 10 - .../editor/universal/editor-config.ts | 14 - .../editor/universal/editor.module.css | 3 - .../src/components/editor/universal/index.css | 14 - .../src/components/editor/universal/index.tsx | 64 - .../src/components/editor/universal/props.ts | 28 - .../editor/universal/use-editor-setting.tsx | 28 - .../write-editor/MarkdownWriteEditor.tsx | 99 - .../editor/write-editor/RichWriteEditor.tsx | 166 - .../editor/write-editor/WriteEditorBase.tsx | 345 -- .../components/editor/write-editor/index.css | 196 -- .../components/editor/write-editor/index.tsx | 141 - .../editor/write-editor/slug-input.tsx | 112 - .../components/editor/write-editor/types.ts | 1 - .../src/components/enrichment-card/index.tsx | 589 ---- apps/admin/src/components/input/base.tsx | 18 - .../src/components/input/borderless-input.tsx | 40 - .../src/components/input/ghost-input.tsx | 63 - .../components/input/inline-editable-text.tsx | 151 - .../src/components/input/material-input.tsx | 45 - .../src/components/input/material.module.css | 77 - .../src/components/input/underline-input.tsx | 29 - .../src/components/input/underline.module.css | 24 - apps/admin/src/components/ip-info/index.tsx | 98 - .../src/components/json-highlight/index.tsx | 47 - apps/admin/src/components/k-bar/index.tsx | 149 - apps/admin/src/components/k-bar/kbar.css | 266 -- apps/admin/src/components/kv-editor/index.tsx | 133 - apps/admin/src/components/layout/index.ts | 10 - .../layout/master-detail-layout.tsx | 129 - .../components/layout/split-panel-layout.tsx | 130 - .../src/components/layout/split-panel.tsx | 98 - .../layout/split-resize-trigger.tsx | 28 - apps/admin/src/components/link/title-link.tsx | 100 - .../location/get-location-button.tsx | 90 - .../src/components/location/search-button.tsx | 157 - .../components/markdown/markdown-render.tsx | 110 - .../monaco-editor/FunctionCodeEditor.tsx | 118 - .../src/components/monaco-editor/index.ts | 3 - .../components/monaco-editor/theme/dark.json | 348 -- .../components/monaco-editor/theme/light.json | 348 -- .../monaco-editor/use-async-load-monaco.ts | 169 - .../src/components/monaco-editor/use-ata.ts | 288 -- .../monaco-editor/use-define-theme.ts | 41 - .../src/components/output-modal/normal.tsx | 94 - .../src/components/output-modal/xterm.tsx | 110 - .../pagination/compact-pagination.tsx | 77 - apps/admin/src/components/preview/index.tsx | 13 - apps/admin/src/components/shorthand/index.tsx | 275 -- apps/admin/src/components/sidebar/hooks.ts | 56 - .../src/components/sidebar/index.module.css | 286 -- apps/admin/src/components/sidebar/index.tsx | 452 --- apps/admin/src/components/sidebar/uwu.png | Bin 69469 -> 0 bytes .../special-button/copy-text-button.tsx | 30 - .../special-button/delete-confirm.tsx | 89 - .../special-button/fetch-github-repo.tsx | 108 - .../special-button/iframe-preview.tsx | 44 - .../special-button/parse-content.tsx | 166 - .../src/components/special-button/preview.tsx | 255 -- apps/admin/src/components/spin/index.tsx | 12 - .../src/components/status-toggle/index.tsx | 89 - .../src/components/table/edit-column.tsx | 130 - .../src/components/table/index.module.css | 7 - apps/admin/src/components/table/index.tsx | 190 - .../task-queue-panel/TaskQueuePanel.tsx | 193 - .../src/components/task-queue-panel/index.ts | 1 - .../src/components/time/relative-time.tsx | 73 - apps/admin/src/components/ui/SplitPanel.tsx | 129 - .../components/update-detail-modal/index.tsx | 211 -- .../update-detail-modal/markdown-styles.css | 156 - apps/admin/src/components/upload/index.tsx | 52 - apps/admin/src/components/xterm/index.tsx | 126 - apps/admin/src/configs.ts | 3 - apps/admin/src/constants/kaomoji.ts | 346 -- apps/admin/src/constants/note.ts | 18 - apps/admin/src/constants/social.ts | 6 - .../src/external/api/github-check-update.ts | 39 - .../src/external/api/github-mx-snippets.ts | 30 - apps/admin/src/external/api/github-repo.ts | 163 - apps/admin/src/external/api/hitokoto.ts | 39 - apps/admin/src/external/api/npm.ts | 6 - apps/admin/src/external/api/octokit.ts | 29 - apps/admin/src/external/types/npm-pkg.ts | 28 - apps/admin/src/hooks/queries/index.ts | 13 - apps/admin/src/hooks/queries/keys.ts | 275 -- apps/admin/src/hooks/queries/use-aggregate.ts | 111 - apps/admin/src/hooks/queries/use-ai.ts | 90 - .../admin/src/hooks/queries/use-categories.ts | 104 - apps/admin/src/hooks/queries/use-comments.ts | 99 - apps/admin/src/hooks/queries/use-drafts.ts | 121 - apps/admin/src/hooks/queries/use-links.ts | 116 - .../src/hooks/queries/use-meta-presets.ts | 99 - apps/admin/src/hooks/queries/use-notes.ts | 96 - apps/admin/src/hooks/queries/use-pages.ts | 77 - apps/admin/src/hooks/queries/use-posts.ts | 93 - apps/admin/src/hooks/queries/use-projects.ts | 81 - apps/admin/src/hooks/queries/use-topics.ts | 93 - apps/admin/src/hooks/use-data-table.ts | 271 -- apps/admin/src/hooks/use-layout.ts | 68 - apps/admin/src/hooks/use-lifecycle.ts | 12 - .../src/hooks/use-memo-fetch-data-list.ts | 66 - apps/admin/src/hooks/use-parse-payload.ts | 13 - apps/admin/src/hooks/use-portal-element.ts | 12 - .../src/hooks/use-preferred-content-format.ts | 18 - .../admin/src/hooks/use-props-value-to-ref.ts | 12 - apps/admin/src/hooks/use-save-confirm.ts | 79 - apps/admin/src/hooks/use-server-draft.ts | 298 -- apps/admin/src/hooks/use-storage.ts | 107 - apps/admin/src/hooks/use-store-ref.ts | 5 - apps/admin/src/hooks/use-table.ts | 52 - apps/admin/src/hooks/use-write-draft.ts | 216 -- apps/admin/src/index.css | 223 +- apps/admin/src/layouts/app-layout.tsx | 28 - apps/admin/src/layouts/content/index.tsx | 180 - apps/admin/src/layouts/router-view.tsx | 34 - apps/admin/src/layouts/settings-layout.tsx | 142 - apps/admin/src/layouts/setup-view.module.css | 5 - apps/admin/src/layouts/setup-view.tsx | 39 - .../src/layouts/sidebar/index.module.css | 62 - apps/admin/src/layouts/sidebar/index.tsx | 127 - apps/admin/src/layouts/two-col.tsx | 12 - apps/admin/src/lib/query-client.ts | 76 - apps/admin/src/{main.ts => main.tsx} | 43 +- apps/admin/src/models/wehbook.ts | 34 - apps/admin/src/monaco.ts | 24 - apps/admin/src/router/guard.ts | 100 - apps/admin/src/router/index.ts | 8 - apps/admin/src/router/name.ts | 56 - apps/admin/src/router/route.tsx | 589 ---- apps/admin/src/router/router.ts | 9 - apps/admin/src/shared/types/base.ts | 17 - apps/admin/src/socket/index.ts | 5 - apps/admin/src/socket/socket-client.tsx | 211 -- apps/admin/src/stores/app.ts | 30 - apps/admin/src/stores/category.ts | 30 - apps/admin/src/stores/index.ts | 22 - apps/admin/src/stores/layout.ts | 198 -- apps/admin/src/stores/ui.ts | 146 - apps/admin/src/stores/user.ts | 32 - apps/admin/src/types.d.ts | 14 +- apps/admin/src/utils/authjs/session.ts | 11 - apps/admin/src/utils/authn.ts | 39 - apps/admin/src/utils/build-menus.ts | 91 - apps/admin/src/utils/camelcase-keys.ts | 44 - apps/admin/src/utils/color.ts | 154 - apps/admin/src/utils/endpoint.ts | 6 - apps/admin/src/utils/event-bus.ts | 65 - apps/admin/src/utils/image.ts | 140 - apps/admin/src/utils/index.ts | 93 - apps/admin/src/utils/is-init.ts | 23 - apps/admin/src/utils/json.ts | 15 - apps/admin/src/utils/markdown.ts | 24 - apps/admin/src/utils/notification.ts | 36 - apps/admin/src/utils/number.ts | 25 - apps/admin/src/utils/request.ts | 182 - apps/admin/src/utils/version.ts | 59 - apps/admin/src/utils/word.ts | 12 - .../ai/components/article-selector-modal.tsx | 469 --- .../insights-article-selector-modal.tsx | 421 --- .../ai/components/insights-detail-panel.tsx | 854 ----- .../src/views/ai/components/insights-list.tsx | 247 -- .../ai/components/summary-detail-panel.tsx | 547 --- .../src/views/ai/components/summary-list.tsx | 214 -- .../components/translation-detail-panel.tsx | 859 ----- .../views/ai/components/translation-list.tsx | 267 -- apps/admin/src/views/ai/insights.tsx | 142 - apps/admin/src/views/ai/slug-backfill.tsx | 130 - apps/admin/src/views/ai/summary.tsx | 141 - apps/admin/src/views/ai/tasks.tsx | 1294 ------- .../src/views/ai/translation-entries.tsx | 305 -- apps/admin/src/views/ai/translation.tsx | 212 -- .../analyze/components/analyze-data-table.tsx | 268 -- .../analyze/components/guest-activity.tsx | 472 --- .../views/analyze/components/reading-rank.tsx | 233 -- apps/admin/src/views/analyze/index.module.css | 481 --- apps/admin/src/views/analyze/index.tsx | 663 ---- apps/admin/src/views/analyze/types.tsx | 36 - .../comments/components/comment-detail.tsx | 544 --- .../components/comment-empty-state.tsx | 14 - .../comments/components/comment-list-item.tsx | 102 - .../comments/components/comment-list.tsx | 220 -- apps/admin/src/views/comments/index.tsx | 362 -- .../src/views/comments/markdown-render.tsx | 27 - apps/admin/src/views/dashboard/badge.tsx | 14 - apps/admin/src/views/dashboard/card.tsx | 88 - .../dashboard/components/CategoryPie.tsx | 105 - .../views/dashboard/components/ChartCard.tsx | 35 - .../dashboard/components/CommentActivity.tsx | 114 - .../dashboard/components/PublicationTrend.tsx | 129 - .../components/SearchIndexRebuildCard.tsx | 101 - .../views/dashboard/components/TagCloud.tsx | 113 - .../dashboard/components/TopArticles.tsx | 98 - .../dashboard/components/TrafficSource.tsx | 187 - .../dashboard/components/use-chart-theme.ts | 55 - apps/admin/src/views/dashboard/index.tsx | 722 ---- apps/admin/src/views/dashboard/statistic.tsx | 32 - .../src/views/dashboard/update-panel.tsx | 37 - apps/admin/src/views/debug/authn/index.tsx | 52 - apps/admin/src/views/debug/events/index.tsx | 126 - apps/admin/src/views/debug/rich/index.tsx | 14 - .../src/views/debug/serverless/index.tsx | 73 - apps/admin/src/views/debug/toast/index.tsx | 328 -- .../drafts/components/draft-detail-base.tsx | 918 ----- .../views/drafts/components/draft-detail.tsx | 57 - .../drafts/components/draft-empty-state.tsx | 23 - .../drafts/components/draft-list-item.tsx | 105 - .../views/drafts/components/draft-list.tsx | 130 - .../drafts/components/markdown-diff-panel.tsx | 27 - .../drafts/components/rich-diff-panel.tsx | 41 - apps/admin/src/views/drafts/index.tsx | 170 - .../components/cache/cache-detail-panel.tsx | 344 -- .../components/cache/cache-empty-state.tsx | 22 - .../components/cache/cache-list-item.tsx | 81 - .../components/cache/cache-list.tsx | 86 - .../cache/cache-normalized-section.tsx | 134 - .../components/probe/probe-console.tsx | 159 - .../components/probe/probe-list.tsx | 130 - .../components/probe/probe-result-view.tsx | 126 - .../enrichment/components/probe/types.ts | 9 - .../components/providers-status-bar.tsx | 217 -- .../enrichment/components/raw-json-block.tsx | 63 - .../screenshots/screenshot-detail-panel.tsx | 285 -- .../screenshots/screenshot-empty-state.tsx | 39 - .../screenshots/screenshot-list-item.tsx | 72 - .../screenshots/screenshot-list.tsx | 155 - .../screenshots/screenshot-quota-chip.tsx | 78 - .../enrichment/components/source-switcher.tsx | 48 - apps/admin/src/views/enrichment/index.tsx | 469 --- apps/admin/src/views/enrichment/utils.ts | 8 - .../assets/template/code-editor.tsx | 36 - .../assets/template/ejs-render.tsx | 45 - .../extra-features/assets/template/index.tsx | 27 - .../assets/template/tabs/email.tsx | 154 - .../assets/template/tabs/markdown.tsx | 7 - .../views/extra-features/markdown-helper.tsx | 504 --- .../snippets/components/code-editor.tsx | 54 - .../snippets/components/fn-log-drawer.tsx | 315 -- .../components/import-snippets-button.tsx | 401 --- .../components/install-dep-button.tsx | 64 - .../snippets/components/install-dep-xterm.tsx | 22 - .../snippets/components/snippet-card.tsx | 281 -- .../components/snippet-empty-state.tsx | 55 - .../snippets/components/snippet-list.tsx | 177 - .../snippets/components/snippet-meta-form.tsx | 281 -- .../components/update-deps-button.tsx | 199 -- .../composables/use-snippet-editor.ts | 209 -- .../snippets/composables/use-snippet-list.ts | 114 - .../views/extra-features/snippets/index.tsx | 416 --- .../snippets/interfaces/snippet-group.ts | 4 - .../extra-features/subscribe/constants.ts | 14 - .../views/extra-features/subscribe/index.tsx | 516 --- .../views/extra-features/webhook/index.tsx | 1000 ------ apps/admin/src/views/login/index.tsx | 245 -- apps/admin/src/views/maintenance/backup.tsx | 652 ---- apps/admin/src/views/maintenance/cron.tsx | 692 ---- .../search-index/components/constants.ts | 39 - .../components/search-index-detail-panel.tsx | 171 - .../components/search-index-empty-state.tsx | 36 - .../components/search-index-filter-bar.tsx | 87 - .../components/search-index-list-item.tsx | 68 - .../components/search-index-list.tsx | 117 - .../views/maintenance/search-index/index.tsx | 280 -- .../src/views/manage-files/comment-images.tsx | 329 -- apps/admin/src/views/manage-files/index.tsx | 444 --- apps/admin/src/views/manage-files/orphans.tsx | 488 --- .../manage-friends/components/avatar.tsx | 38 - .../manage-friends/components/fallback.jpg | Bin 10398 -> 0 bytes .../components/reason-modal.tsx | 63 - apps/admin/src/views/manage-friends/index.tsx | 577 --- .../views/manage-friends/url-components.tsx | 35 - .../manage-notes/components/topic-card.tsx | 171 - .../components/topic-detail-panel.tsx | 621 ---- .../manage-notes/components/topic-detail.tsx | 606 ---- .../manage-notes/components/topic-list.tsx | 169 - .../manage-notes/components/topic-modal.tsx | 304 -- .../manage-notes/hooks/use-memo-note-list.ts | 14 - apps/admin/src/views/manage-notes/list.tsx | 626 ---- apps/admin/src/views/manage-notes/topic.tsx | 160 - apps/admin/src/views/manage-notes/write.tsx | 771 ---- apps/admin/src/views/manage-pages/list.tsx | 252 -- apps/admin/src/views/manage-pages/write.tsx | 391 --- .../admin/src/views/manage-posts/category.tsx | 1008 ------ .../views/manage-posts/components/ask-ai.tsx | 222 -- .../manage-posts/hooks/use-memo-post-list.ts | 19 - apps/admin/src/views/manage-posts/list.tsx | 577 --- apps/admin/src/views/manage-posts/write.tsx | 651 ---- .../components/project-detail-panel.tsx | 654 ---- .../components/project-list.tsx | 156 - apps/admin/src/views/manage-project/index.tsx | 161 - .../manage-says/components/say-list-item.tsx | 339 -- apps/admin/src/views/manage-says/list.tsx | 134 - apps/admin/src/views/reader/index.tsx | 221 -- .../setting/components/SettingListPanel.tsx | 142 - apps/admin/src/views/setting/index.tsx | 223 -- apps/admin/src/views/setting/tabs/account.tsx | 1249 ------- .../tabs/components/meta-preset-card.tsx | 144 - .../tabs/components/meta-preset-modal.tsx | 478 --- .../src/views/setting/tabs/meta-presets.tsx | 209 -- .../src/views/setting/tabs/providers/oauth.ts | 55 - .../views/setting/tabs/sections/ai-config.tsx | 1067 ------ .../src/views/setting/tabs/sections/oauth.tsx | 204 -- apps/admin/src/views/setting/tabs/system.tsx | 337 -- apps/admin/src/views/setting/tabs/user.tsx | 403 --- apps/admin/src/views/setup-api/index.tsx | 191 - apps/admin/src/views/setup/index.tsx | 644 ---- .../src/views/shorthand/index.module.css | 262 -- apps/admin/src/views/shorthand/index.tsx | 359 -- apps/admin/src/vite-env.d.ts | 1 + apps/admin/src/vue-app-env.d.ts | 9 - apps/admin/tsconfig.json | 36 +- apps/admin/uno.config.ts | 4 +- apps/admin/vite.config.mts | 30 +- docs/typography.md | 27 +- pnpm-lock.yaml | 3105 +---------------- pnpm-workspace.yaml | 1 - readme.md | 22 +- 562 files changed, 21914 insertions(+), 79394 deletions(-) delete mode 100644 apps/admin/src/api/activity.ts delete mode 100644 apps/admin/src/api/aggregate.ts delete mode 100644 apps/admin/src/api/ai-agent.ts delete mode 100644 apps/admin/src/api/ai.ts delete mode 100644 apps/admin/src/api/analyze.ts delete mode 100644 apps/admin/src/api/auth.ts delete mode 100644 apps/admin/src/api/backup.ts delete mode 100644 apps/admin/src/api/categories.ts delete mode 100644 apps/admin/src/api/comments.ts delete mode 100644 apps/admin/src/api/cron-task.ts delete mode 100644 apps/admin/src/api/debug.ts delete mode 100644 apps/admin/src/api/dependencies.ts delete mode 100644 apps/admin/src/api/drafts.ts delete mode 100644 apps/admin/src/api/enrichment.ts delete mode 100644 apps/admin/src/api/files.ts delete mode 100644 apps/admin/src/api/health.ts delete mode 100644 apps/admin/src/api/index.ts delete mode 100644 apps/admin/src/api/links.ts delete mode 100644 apps/admin/src/api/markdown.ts delete mode 100644 apps/admin/src/api/meta-presets.ts delete mode 100644 apps/admin/src/api/notes.ts delete mode 100644 apps/admin/src/api/options.ts delete mode 100644 apps/admin/src/api/pages.ts delete mode 100644 apps/admin/src/api/posts.ts delete mode 100644 apps/admin/src/api/projects.ts delete mode 100644 apps/admin/src/api/pty.ts delete mode 100644 apps/admin/src/api/readers.ts delete mode 100644 apps/admin/src/api/recently.ts delete mode 100644 apps/admin/src/api/says.ts delete mode 100644 apps/admin/src/api/search-index.ts delete mode 100644 apps/admin/src/api/search.ts delete mode 100644 apps/admin/src/api/serverless.ts delete mode 100644 apps/admin/src/api/snippets.ts delete mode 100644 apps/admin/src/api/subscribe.ts delete mode 100644 apps/admin/src/api/system.ts delete mode 100644 apps/admin/src/api/templates.ts delete mode 100644 apps/admin/src/api/topics.ts delete mode 100644 apps/admin/src/api/user.ts delete mode 100644 apps/admin/src/api/webhooks.ts create mode 100644 apps/admin/src/app/api/ai.ts create mode 100644 apps/admin/src/app/api/analyze.ts create mode 100644 apps/admin/src/app/api/backups.ts create mode 100644 apps/admin/src/app/api/categories.ts create mode 100644 apps/admin/src/app/api/comments.ts create mode 100644 apps/admin/src/app/api/cron-tasks.ts create mode 100644 apps/admin/src/app/api/drafts.ts create mode 100644 apps/admin/src/app/api/enrichment.ts create mode 100644 apps/admin/src/app/api/files.ts create mode 100644 apps/admin/src/app/api/http.ts create mode 100644 apps/admin/src/app/api/links.ts create mode 100644 apps/admin/src/app/api/markdown.ts create mode 100644 apps/admin/src/app/api/notes.ts create mode 100644 apps/admin/src/app/api/options.ts create mode 100644 apps/admin/src/app/api/pages.ts create mode 100644 apps/admin/src/app/api/posts.ts create mode 100644 apps/admin/src/app/api/projects.ts create mode 100644 apps/admin/src/app/api/readers.ts create mode 100644 apps/admin/src/app/api/recently.ts create mode 100644 apps/admin/src/app/api/says.ts create mode 100644 apps/admin/src/app/api/search-index.ts create mode 100644 apps/admin/src/app/api/snippets.ts create mode 100644 apps/admin/src/app/api/subscribe.ts create mode 100644 apps/admin/src/app/api/system.ts create mode 100644 apps/admin/src/app/api/topics.ts create mode 100644 apps/admin/src/app/api/webhooks.ts rename apps/admin/src/{ => app}/constants/env.ts (100%) rename apps/admin/src/{ => app}/constants/keys.ts (100%) create mode 100644 apps/admin/src/app/hooks/use-local-storage-state.ts rename apps/admin/src/{ => app}/models/activity.ts (100%) rename apps/admin/src/{ => app}/models/ai.ts (100%) rename apps/admin/src/{ => app}/models/amap.ts (100%) rename apps/admin/src/{ => app}/models/analyze.ts (100%) rename apps/admin/src/{ => app}/models/base.ts (100%) rename apps/admin/src/{ => app}/models/category.ts (100%) rename apps/admin/src/{ => app}/models/comment.ts (100%) rename apps/admin/src/{ => app}/models/draft.ts (100%) rename apps/admin/src/{ => app}/models/enrichment.ts (100%) rename apps/admin/src/{ => app}/models/link.ts (100%) rename apps/admin/src/{ => app}/models/meta-preset.ts (100%) rename apps/admin/src/{ => app}/models/note.ts (100%) rename apps/admin/src/{ => app}/models/options.ts (100%) rename apps/admin/src/{ => app}/models/page.ts (100%) rename apps/admin/src/{ => app}/models/post.ts (100%) rename apps/admin/src/{ => app}/models/project.ts (100%) rename apps/admin/src/{ => app}/models/recently.ts (100%) rename apps/admin/src/{ => app}/models/say.ts (100%) rename apps/admin/src/{ => app}/models/search-index.ts (100%) rename apps/admin/src/{ => app}/models/snippet.ts (100%) rename apps/admin/src/{ => app}/models/stat.ts (100%) rename apps/admin/src/{ => app}/models/system.ts (100%) rename apps/admin/src/{ => app}/models/token.ts (100%) rename apps/admin/src/{ => app}/models/topic.ts (100%) rename apps/admin/src/{ => app}/models/user.ts (100%) create mode 100644 apps/admin/src/app/providers.tsx create mode 100644 apps/admin/src/app/query-client.ts create mode 100644 apps/admin/src/app/routes.tsx create mode 100644 apps/admin/src/app/shell.tsx rename apps/admin/src/{ => app}/socket/types.ts (80%) create mode 100644 apps/admin/src/app/theme.ts create mode 100644 apps/admin/src/app/ui/button.tsx create mode 100644 apps/admin/src/app/ui/checkbox.tsx create mode 100644 apps/admin/src/app/ui/cn.ts create mode 100644 apps/admin/src/app/ui/compact-pagination.tsx create mode 100644 apps/admin/src/app/ui/data-table.tsx create mode 100644 apps/admin/src/app/ui/metric-card.tsx create mode 100644 apps/admin/src/app/ui/panel.tsx create mode 100644 apps/admin/src/app/ui/select.tsx create mode 100644 apps/admin/src/app/ui/switch.tsx create mode 100644 apps/admin/src/app/ui/text-field.tsx rename apps/admin/src/{ => app}/utils/authjs/auth.ts (92%) rename apps/admin/src/{ => app}/utils/confetti.ts (100%) rename apps/admin/src/{ => app}/utils/markdown-parser.ts (95%) rename apps/admin/src/{ => app}/utils/time.ts (100%) create mode 100644 apps/admin/src/app/views/ai-page.tsx create mode 100644 apps/admin/src/app/views/analyze-page.tsx create mode 100644 apps/admin/src/app/views/authn-debug-page.tsx create mode 100644 apps/admin/src/app/views/backup-page.tsx create mode 100644 apps/admin/src/app/views/categories-page.tsx create mode 100644 apps/admin/src/app/views/comments-page.tsx create mode 100644 apps/admin/src/app/views/cron-page.tsx create mode 100644 apps/admin/src/app/views/dashboard-page.tsx create mode 100644 apps/admin/src/app/views/drafts-page.tsx create mode 100644 apps/admin/src/app/views/enrichment-page.tsx create mode 100644 apps/admin/src/app/views/events-debug-page.tsx create mode 100644 apps/admin/src/app/views/files-page.tsx create mode 100644 apps/admin/src/app/views/friends-page.tsx create mode 100644 apps/admin/src/app/views/login-page.tsx create mode 100644 apps/admin/src/app/views/markdown-page.tsx create mode 100644 apps/admin/src/app/views/notes-page.tsx create mode 100644 apps/admin/src/app/views/pages-page.tsx create mode 100644 apps/admin/src/app/views/posts-page.tsx create mode 100644 apps/admin/src/app/views/projects-page.tsx create mode 100644 apps/admin/src/app/views/readers-page.tsx create mode 100644 apps/admin/src/app/views/recently-page.tsx create mode 100644 apps/admin/src/app/views/says-page.tsx create mode 100644 apps/admin/src/app/views/search-index-page.tsx create mode 100644 apps/admin/src/app/views/serverless-debug-page.tsx create mode 100644 apps/admin/src/app/views/settings-page.tsx create mode 100644 apps/admin/src/app/views/setup-api-page.tsx create mode 100644 apps/admin/src/app/views/setup-page.tsx create mode 100644 apps/admin/src/app/views/snippets-page.tsx create mode 100644 apps/admin/src/app/views/subscribe-page.tsx create mode 100644 apps/admin/src/app/views/template-page.tsx create mode 100644 apps/admin/src/app/views/toast-debug-page.tsx create mode 100644 apps/admin/src/app/views/topics-page.tsx create mode 100644 apps/admin/src/app/views/webhooks-page.tsx create mode 100644 apps/admin/src/app/views/write-page.tsx delete mode 100644 apps/admin/src/components/ai-task-queue/AiTaskQueue.tsx delete mode 100644 apps/admin/src/components/ai-task-queue/index.ts delete mode 100644 apps/admin/src/components/ai-task-queue/types.ts delete mode 100644 apps/admin/src/components/ai-task-queue/use-ai-task-queue.ts delete mode 100644 apps/admin/src/components/ai/ai-helper.tsx delete mode 100644 apps/admin/src/components/avatar/Avatar.tsx delete mode 100644 apps/admin/src/components/avatar/avatar.module.css delete mode 100644 apps/admin/src/components/avatar/index.tsx delete mode 100644 apps/admin/src/components/button/header-action-button.tsx delete mode 100644 apps/admin/src/components/code-highlight/index.tsx delete mode 100644 apps/admin/src/components/config-form/index.tsx delete mode 100644 apps/admin/src/components/config-form/types.ts delete mode 100644 apps/admin/src/components/directives/if.tsx delete mode 100644 apps/admin/src/components/draft/diff-preview.tsx delete mode 100644 apps/admin/src/components/draft/draft-list-modal.tsx delete mode 100644 apps/admin/src/components/draft/draft-recovery-modal.tsx delete mode 100644 apps/admin/src/components/draft/draft-save-indicator.tsx delete mode 100644 apps/admin/src/components/draft/file-preview.tsx delete mode 100644 apps/admin/src/components/draft/version-list-item.tsx delete mode 100644 apps/admin/src/components/drawer/components/image-detail-section.tsx delete mode 100644 apps/admin/src/components/drawer/components/json-editor.tsx delete mode 100644 apps/admin/src/components/drawer/components/lexical-image-detail-section.tsx delete mode 100644 apps/admin/src/components/drawer/components/meta-preset-section.tsx delete mode 100644 apps/admin/src/components/drawer/components/preset-field-renderer.tsx delete mode 100644 apps/admin/src/components/drawer/components/ui.tsx delete mode 100644 apps/admin/src/components/drawer/lexical-debug-drawer.tsx delete mode 100644 apps/admin/src/components/drawer/text-base-drawer.tsx delete mode 100644 apps/admin/src/components/editor/codemirror/ImageDropZone.tsx delete mode 100644 apps/admin/src/components/editor/codemirror/ImageEditPopover.tsx delete mode 100644 apps/admin/src/components/editor/codemirror/codemirror.css delete mode 100644 apps/admin/src/components/editor/codemirror/codemirror.tsx delete mode 100644 apps/admin/src/components/editor/codemirror/editor-store.ts delete mode 100644 apps/admin/src/components/editor/codemirror/extension.ts delete mode 100644 apps/admin/src/components/editor/codemirror/image-popover-state.ts delete mode 100644 apps/admin/src/components/editor/codemirror/language-icons.ts delete mode 100644 apps/admin/src/components/editor/codemirror/syntax-highlight.ts delete mode 100644 apps/admin/src/components/editor/codemirror/upload-store.ts delete mode 100644 apps/admin/src/components/editor/codemirror/use-auto-fonts.ts delete mode 100644 apps/admin/src/components/editor/codemirror/use-auto-theme.ts delete mode 100644 apps/admin/src/components/editor/codemirror/use-codemirror.ts delete mode 100644 apps/admin/src/components/editor/codemirror/wysiwyg/block-registry.ts delete mode 100644 apps/admin/src/components/editor/codemirror/wysiwyg/blockquote.ts delete mode 100644 apps/admin/src/components/editor/codemirror/wysiwyg/codeblock.ts delete mode 100644 apps/admin/src/components/editor/codemirror/wysiwyg/details.ts delete mode 100644 apps/admin/src/components/editor/codemirror/wysiwyg/divider.ts delete mode 100644 apps/admin/src/components/editor/codemirror/wysiwyg/empty-line.ts delete mode 100644 apps/admin/src/components/editor/codemirror/wysiwyg/heading.ts delete mode 100644 apps/admin/src/components/editor/codemirror/wysiwyg/image.ts delete mode 100644 apps/admin/src/components/editor/codemirror/wysiwyg/index.ts delete mode 100644 apps/admin/src/components/editor/codemirror/wysiwyg/inline.ts delete mode 100644 apps/admin/src/components/editor/codemirror/wysiwyg/line-break.ts delete mode 100644 apps/admin/src/components/editor/codemirror/wysiwyg/list.ts delete mode 100644 apps/admin/src/components/editor/codemirror/wysiwyg/math.ts delete mode 100644 apps/admin/src/components/editor/codemirror/wysiwyg/measure.ts delete mode 100644 apps/admin/src/components/editor/plain/plain.tsx delete mode 100644 apps/admin/src/components/editor/rich/RichDiffBridge.tsx delete mode 100644 apps/admin/src/components/editor/rich/RichEditor.tsx delete mode 100644 apps/admin/src/components/editor/rich/RichEditorWithAgent.tsx delete mode 100644 apps/admin/src/components/editor/rich/agent-chat/AgentChatPanel.tsx delete mode 100644 apps/admin/src/components/editor/rich/agent-chat/ChatInput.tsx delete mode 100644 apps/admin/src/components/editor/rich/agent-chat/ChatMessageList.tsx delete mode 100644 apps/admin/src/components/editor/rich/agent-chat/ModelSelector.test.tsx delete mode 100644 apps/admin/src/components/editor/rich/agent-chat/ModelSelector.tsx delete mode 100644 apps/admin/src/components/editor/rich/agent-chat/SessionHeader.tsx delete mode 100644 apps/admin/src/components/editor/rich/agent-chat/bubbles/DiffReviewBubble.tsx delete mode 100644 apps/admin/src/components/editor/rich/agent-chat/bubbles/DiffSummaryBubble.tsx delete mode 100644 apps/admin/src/components/editor/rich/agent-chat/bubbles/ErrorBubble.tsx delete mode 100644 apps/admin/src/components/editor/rich/agent-chat/bubbles/StreamdownBubble.tsx delete mode 100644 apps/admin/src/components/editor/rich/agent-chat/bubbles/ThinkingChain.tsx delete mode 100644 apps/admin/src/components/editor/rich/agent-chat/bubbles/ToolCall.tsx delete mode 100644 apps/admin/src/components/editor/rich/agent-chat/bubbles/ToolCallGroup.tsx delete mode 100644 apps/admin/src/components/editor/rich/agent-chat/bubbles/UserBubble.tsx delete mode 100644 apps/admin/src/components/editor/rich/agent-chat/composables/context-aware-engine.ts delete mode 100644 apps/admin/src/components/editor/rich/agent-chat/composables/use-agent-loop.ts delete mode 100644 apps/admin/src/components/editor/rich/agent-chat/composables/use-agent-reapply.ts delete mode 100644 apps/admin/src/components/editor/rich/agent-chat/composables/use-agent-selected-model.ts delete mode 100644 apps/admin/src/components/editor/rich/agent-chat/composables/use-agent-store.ts delete mode 100644 apps/admin/src/components/editor/rich/agent-chat/composables/use-meta-tools.ts delete mode 100644 apps/admin/src/components/editor/rich/agent-chat/composables/use-session-manager.ts delete mode 100644 apps/admin/src/components/editor/rich/agent-chat/index.ts delete mode 100644 apps/admin/src/components/editor/rich/agent-chat/model-selector-recents.ts delete mode 100644 apps/admin/src/components/editor/slash-menu/index.ts delete mode 100644 apps/admin/src/components/editor/slash-menu/slash-menu-extension.ts delete mode 100644 apps/admin/src/components/editor/slash-menu/slash-menu-items.ts delete mode 100644 apps/admin/src/components/editor/slash-menu/slash-menu.css delete mode 100644 apps/admin/src/components/editor/slash-menu/slash-menu.tsx delete mode 100644 apps/admin/src/components/editor/slash-menu/use-slash-menu.ts delete mode 100644 apps/admin/src/components/editor/toolbar/emoji-picker.tsx delete mode 100644 apps/admin/src/components/editor/toolbar/floating-toolbar.css delete mode 100644 apps/admin/src/components/editor/toolbar/floating-toolbar.tsx delete mode 100644 apps/admin/src/components/editor/toolbar/index.tsx delete mode 100644 apps/admin/src/components/editor/toolbar/keymap-extension.ts delete mode 100644 apps/admin/src/components/editor/toolbar/markdown-commands.ts delete mode 100644 apps/admin/src/components/editor/toolbar/toolbar.tsx delete mode 100644 apps/admin/src/components/editor/toolbar/use-selection-position.ts delete mode 100644 apps/admin/src/components/editor/universal/constants.ts delete mode 100644 apps/admin/src/components/editor/universal/editor-config.ts delete mode 100644 apps/admin/src/components/editor/universal/editor.module.css delete mode 100644 apps/admin/src/components/editor/universal/index.css delete mode 100644 apps/admin/src/components/editor/universal/index.tsx delete mode 100644 apps/admin/src/components/editor/universal/props.ts delete mode 100644 apps/admin/src/components/editor/universal/use-editor-setting.tsx delete mode 100644 apps/admin/src/components/editor/write-editor/MarkdownWriteEditor.tsx delete mode 100644 apps/admin/src/components/editor/write-editor/RichWriteEditor.tsx delete mode 100644 apps/admin/src/components/editor/write-editor/WriteEditorBase.tsx delete mode 100644 apps/admin/src/components/editor/write-editor/index.css delete mode 100644 apps/admin/src/components/editor/write-editor/index.tsx delete mode 100644 apps/admin/src/components/editor/write-editor/slug-input.tsx delete mode 100644 apps/admin/src/components/editor/write-editor/types.ts delete mode 100644 apps/admin/src/components/enrichment-card/index.tsx delete mode 100644 apps/admin/src/components/input/base.tsx delete mode 100644 apps/admin/src/components/input/borderless-input.tsx delete mode 100644 apps/admin/src/components/input/ghost-input.tsx delete mode 100644 apps/admin/src/components/input/inline-editable-text.tsx delete mode 100644 apps/admin/src/components/input/material-input.tsx delete mode 100644 apps/admin/src/components/input/material.module.css delete mode 100644 apps/admin/src/components/input/underline-input.tsx delete mode 100644 apps/admin/src/components/input/underline.module.css delete mode 100644 apps/admin/src/components/ip-info/index.tsx delete mode 100644 apps/admin/src/components/json-highlight/index.tsx delete mode 100644 apps/admin/src/components/k-bar/index.tsx delete mode 100644 apps/admin/src/components/k-bar/kbar.css delete mode 100644 apps/admin/src/components/kv-editor/index.tsx delete mode 100644 apps/admin/src/components/layout/index.ts delete mode 100644 apps/admin/src/components/layout/master-detail-layout.tsx delete mode 100644 apps/admin/src/components/layout/split-panel-layout.tsx delete mode 100644 apps/admin/src/components/layout/split-panel.tsx delete mode 100644 apps/admin/src/components/layout/split-resize-trigger.tsx delete mode 100644 apps/admin/src/components/link/title-link.tsx delete mode 100644 apps/admin/src/components/location/get-location-button.tsx delete mode 100644 apps/admin/src/components/location/search-button.tsx delete mode 100644 apps/admin/src/components/markdown/markdown-render.tsx delete mode 100644 apps/admin/src/components/monaco-editor/FunctionCodeEditor.tsx delete mode 100644 apps/admin/src/components/monaco-editor/index.ts delete mode 100644 apps/admin/src/components/monaco-editor/theme/dark.json delete mode 100644 apps/admin/src/components/monaco-editor/theme/light.json delete mode 100644 apps/admin/src/components/monaco-editor/use-async-load-monaco.ts delete mode 100644 apps/admin/src/components/monaco-editor/use-ata.ts delete mode 100644 apps/admin/src/components/monaco-editor/use-define-theme.ts delete mode 100644 apps/admin/src/components/output-modal/normal.tsx delete mode 100644 apps/admin/src/components/output-modal/xterm.tsx delete mode 100644 apps/admin/src/components/pagination/compact-pagination.tsx delete mode 100644 apps/admin/src/components/preview/index.tsx delete mode 100644 apps/admin/src/components/shorthand/index.tsx delete mode 100644 apps/admin/src/components/sidebar/hooks.ts delete mode 100644 apps/admin/src/components/sidebar/index.module.css delete mode 100644 apps/admin/src/components/sidebar/index.tsx delete mode 100644 apps/admin/src/components/sidebar/uwu.png delete mode 100644 apps/admin/src/components/special-button/copy-text-button.tsx delete mode 100644 apps/admin/src/components/special-button/delete-confirm.tsx delete mode 100644 apps/admin/src/components/special-button/fetch-github-repo.tsx delete mode 100644 apps/admin/src/components/special-button/iframe-preview.tsx delete mode 100644 apps/admin/src/components/special-button/parse-content.tsx delete mode 100644 apps/admin/src/components/special-button/preview.tsx delete mode 100644 apps/admin/src/components/spin/index.tsx delete mode 100644 apps/admin/src/components/status-toggle/index.tsx delete mode 100644 apps/admin/src/components/table/edit-column.tsx delete mode 100644 apps/admin/src/components/table/index.module.css delete mode 100644 apps/admin/src/components/table/index.tsx delete mode 100644 apps/admin/src/components/task-queue-panel/TaskQueuePanel.tsx delete mode 100644 apps/admin/src/components/task-queue-panel/index.ts delete mode 100644 apps/admin/src/components/time/relative-time.tsx delete mode 100644 apps/admin/src/components/ui/SplitPanel.tsx delete mode 100644 apps/admin/src/components/update-detail-modal/index.tsx delete mode 100644 apps/admin/src/components/update-detail-modal/markdown-styles.css delete mode 100644 apps/admin/src/components/upload/index.tsx delete mode 100644 apps/admin/src/components/xterm/index.tsx delete mode 100644 apps/admin/src/configs.ts delete mode 100644 apps/admin/src/constants/kaomoji.ts delete mode 100644 apps/admin/src/constants/note.ts delete mode 100644 apps/admin/src/constants/social.ts delete mode 100644 apps/admin/src/external/api/github-check-update.ts delete mode 100644 apps/admin/src/external/api/github-mx-snippets.ts delete mode 100644 apps/admin/src/external/api/github-repo.ts delete mode 100644 apps/admin/src/external/api/hitokoto.ts delete mode 100644 apps/admin/src/external/api/npm.ts delete mode 100644 apps/admin/src/external/api/octokit.ts delete mode 100644 apps/admin/src/external/types/npm-pkg.ts delete mode 100644 apps/admin/src/hooks/queries/index.ts delete mode 100644 apps/admin/src/hooks/queries/keys.ts delete mode 100644 apps/admin/src/hooks/queries/use-aggregate.ts delete mode 100644 apps/admin/src/hooks/queries/use-ai.ts delete mode 100644 apps/admin/src/hooks/queries/use-categories.ts delete mode 100644 apps/admin/src/hooks/queries/use-comments.ts delete mode 100644 apps/admin/src/hooks/queries/use-drafts.ts delete mode 100644 apps/admin/src/hooks/queries/use-links.ts delete mode 100644 apps/admin/src/hooks/queries/use-meta-presets.ts delete mode 100644 apps/admin/src/hooks/queries/use-notes.ts delete mode 100644 apps/admin/src/hooks/queries/use-pages.ts delete mode 100644 apps/admin/src/hooks/queries/use-posts.ts delete mode 100644 apps/admin/src/hooks/queries/use-projects.ts delete mode 100644 apps/admin/src/hooks/queries/use-topics.ts delete mode 100644 apps/admin/src/hooks/use-data-table.ts delete mode 100644 apps/admin/src/hooks/use-layout.ts delete mode 100644 apps/admin/src/hooks/use-lifecycle.ts delete mode 100644 apps/admin/src/hooks/use-memo-fetch-data-list.ts delete mode 100644 apps/admin/src/hooks/use-parse-payload.ts delete mode 100644 apps/admin/src/hooks/use-portal-element.ts delete mode 100644 apps/admin/src/hooks/use-preferred-content-format.ts delete mode 100644 apps/admin/src/hooks/use-props-value-to-ref.ts delete mode 100644 apps/admin/src/hooks/use-save-confirm.ts delete mode 100644 apps/admin/src/hooks/use-server-draft.ts delete mode 100644 apps/admin/src/hooks/use-storage.ts delete mode 100644 apps/admin/src/hooks/use-store-ref.ts delete mode 100644 apps/admin/src/hooks/use-table.ts delete mode 100644 apps/admin/src/hooks/use-write-draft.ts delete mode 100644 apps/admin/src/layouts/app-layout.tsx delete mode 100644 apps/admin/src/layouts/content/index.tsx delete mode 100644 apps/admin/src/layouts/router-view.tsx delete mode 100644 apps/admin/src/layouts/settings-layout.tsx delete mode 100644 apps/admin/src/layouts/setup-view.module.css delete mode 100644 apps/admin/src/layouts/setup-view.tsx delete mode 100644 apps/admin/src/layouts/sidebar/index.module.css delete mode 100644 apps/admin/src/layouts/sidebar/index.tsx delete mode 100644 apps/admin/src/layouts/two-col.tsx delete mode 100644 apps/admin/src/lib/query-client.ts rename apps/admin/src/{main.ts => main.tsx} (55%) delete mode 100644 apps/admin/src/models/wehbook.ts delete mode 100644 apps/admin/src/monaco.ts delete mode 100644 apps/admin/src/router/guard.ts delete mode 100644 apps/admin/src/router/index.ts delete mode 100644 apps/admin/src/router/name.ts delete mode 100644 apps/admin/src/router/route.tsx delete mode 100644 apps/admin/src/router/router.ts delete mode 100644 apps/admin/src/shared/types/base.ts delete mode 100644 apps/admin/src/socket/index.ts delete mode 100644 apps/admin/src/socket/socket-client.tsx delete mode 100644 apps/admin/src/stores/app.ts delete mode 100644 apps/admin/src/stores/category.ts delete mode 100644 apps/admin/src/stores/index.ts delete mode 100644 apps/admin/src/stores/layout.ts delete mode 100644 apps/admin/src/stores/ui.ts delete mode 100644 apps/admin/src/stores/user.ts delete mode 100644 apps/admin/src/utils/authjs/session.ts delete mode 100644 apps/admin/src/utils/authn.ts delete mode 100644 apps/admin/src/utils/build-menus.ts delete mode 100644 apps/admin/src/utils/camelcase-keys.ts delete mode 100644 apps/admin/src/utils/color.ts delete mode 100644 apps/admin/src/utils/endpoint.ts delete mode 100644 apps/admin/src/utils/event-bus.ts delete mode 100644 apps/admin/src/utils/image.ts delete mode 100644 apps/admin/src/utils/index.ts delete mode 100644 apps/admin/src/utils/is-init.ts delete mode 100644 apps/admin/src/utils/json.ts delete mode 100644 apps/admin/src/utils/markdown.ts delete mode 100644 apps/admin/src/utils/notification.ts delete mode 100644 apps/admin/src/utils/number.ts delete mode 100644 apps/admin/src/utils/request.ts delete mode 100644 apps/admin/src/utils/version.ts delete mode 100644 apps/admin/src/utils/word.ts delete mode 100644 apps/admin/src/views/ai/components/article-selector-modal.tsx delete mode 100644 apps/admin/src/views/ai/components/insights-article-selector-modal.tsx delete mode 100644 apps/admin/src/views/ai/components/insights-detail-panel.tsx delete mode 100644 apps/admin/src/views/ai/components/insights-list.tsx delete mode 100644 apps/admin/src/views/ai/components/summary-detail-panel.tsx delete mode 100644 apps/admin/src/views/ai/components/summary-list.tsx delete mode 100644 apps/admin/src/views/ai/components/translation-detail-panel.tsx delete mode 100644 apps/admin/src/views/ai/components/translation-list.tsx delete mode 100644 apps/admin/src/views/ai/insights.tsx delete mode 100644 apps/admin/src/views/ai/slug-backfill.tsx delete mode 100644 apps/admin/src/views/ai/summary.tsx delete mode 100644 apps/admin/src/views/ai/tasks.tsx delete mode 100644 apps/admin/src/views/ai/translation-entries.tsx delete mode 100644 apps/admin/src/views/ai/translation.tsx delete mode 100644 apps/admin/src/views/analyze/components/analyze-data-table.tsx delete mode 100644 apps/admin/src/views/analyze/components/guest-activity.tsx delete mode 100644 apps/admin/src/views/analyze/components/reading-rank.tsx delete mode 100644 apps/admin/src/views/analyze/index.module.css delete mode 100644 apps/admin/src/views/analyze/index.tsx delete mode 100644 apps/admin/src/views/analyze/types.tsx delete mode 100644 apps/admin/src/views/comments/components/comment-detail.tsx delete mode 100644 apps/admin/src/views/comments/components/comment-empty-state.tsx delete mode 100644 apps/admin/src/views/comments/components/comment-list-item.tsx delete mode 100644 apps/admin/src/views/comments/components/comment-list.tsx delete mode 100644 apps/admin/src/views/comments/index.tsx delete mode 100644 apps/admin/src/views/comments/markdown-render.tsx delete mode 100644 apps/admin/src/views/dashboard/badge.tsx delete mode 100644 apps/admin/src/views/dashboard/card.tsx delete mode 100644 apps/admin/src/views/dashboard/components/CategoryPie.tsx delete mode 100644 apps/admin/src/views/dashboard/components/ChartCard.tsx delete mode 100644 apps/admin/src/views/dashboard/components/CommentActivity.tsx delete mode 100644 apps/admin/src/views/dashboard/components/PublicationTrend.tsx delete mode 100644 apps/admin/src/views/dashboard/components/SearchIndexRebuildCard.tsx delete mode 100644 apps/admin/src/views/dashboard/components/TagCloud.tsx delete mode 100644 apps/admin/src/views/dashboard/components/TopArticles.tsx delete mode 100644 apps/admin/src/views/dashboard/components/TrafficSource.tsx delete mode 100644 apps/admin/src/views/dashboard/components/use-chart-theme.ts delete mode 100644 apps/admin/src/views/dashboard/index.tsx delete mode 100644 apps/admin/src/views/dashboard/statistic.tsx delete mode 100644 apps/admin/src/views/dashboard/update-panel.tsx delete mode 100644 apps/admin/src/views/debug/authn/index.tsx delete mode 100644 apps/admin/src/views/debug/events/index.tsx delete mode 100644 apps/admin/src/views/debug/rich/index.tsx delete mode 100644 apps/admin/src/views/debug/serverless/index.tsx delete mode 100644 apps/admin/src/views/debug/toast/index.tsx delete mode 100644 apps/admin/src/views/drafts/components/draft-detail-base.tsx delete mode 100644 apps/admin/src/views/drafts/components/draft-detail.tsx delete mode 100644 apps/admin/src/views/drafts/components/draft-empty-state.tsx delete mode 100644 apps/admin/src/views/drafts/components/draft-list-item.tsx delete mode 100644 apps/admin/src/views/drafts/components/draft-list.tsx delete mode 100644 apps/admin/src/views/drafts/components/markdown-diff-panel.tsx delete mode 100644 apps/admin/src/views/drafts/components/rich-diff-panel.tsx delete mode 100644 apps/admin/src/views/drafts/index.tsx delete mode 100644 apps/admin/src/views/enrichment/components/cache/cache-detail-panel.tsx delete mode 100644 apps/admin/src/views/enrichment/components/cache/cache-empty-state.tsx delete mode 100644 apps/admin/src/views/enrichment/components/cache/cache-list-item.tsx delete mode 100644 apps/admin/src/views/enrichment/components/cache/cache-list.tsx delete mode 100644 apps/admin/src/views/enrichment/components/cache/cache-normalized-section.tsx delete mode 100644 apps/admin/src/views/enrichment/components/probe/probe-console.tsx delete mode 100644 apps/admin/src/views/enrichment/components/probe/probe-list.tsx delete mode 100644 apps/admin/src/views/enrichment/components/probe/probe-result-view.tsx delete mode 100644 apps/admin/src/views/enrichment/components/probe/types.ts delete mode 100644 apps/admin/src/views/enrichment/components/providers-status-bar.tsx delete mode 100644 apps/admin/src/views/enrichment/components/raw-json-block.tsx delete mode 100644 apps/admin/src/views/enrichment/components/screenshots/screenshot-detail-panel.tsx delete mode 100644 apps/admin/src/views/enrichment/components/screenshots/screenshot-empty-state.tsx delete mode 100644 apps/admin/src/views/enrichment/components/screenshots/screenshot-list-item.tsx delete mode 100644 apps/admin/src/views/enrichment/components/screenshots/screenshot-list.tsx delete mode 100644 apps/admin/src/views/enrichment/components/screenshots/screenshot-quota-chip.tsx delete mode 100644 apps/admin/src/views/enrichment/components/source-switcher.tsx delete mode 100644 apps/admin/src/views/enrichment/index.tsx delete mode 100644 apps/admin/src/views/enrichment/utils.ts delete mode 100644 apps/admin/src/views/extra-features/assets/template/code-editor.tsx delete mode 100644 apps/admin/src/views/extra-features/assets/template/ejs-render.tsx delete mode 100644 apps/admin/src/views/extra-features/assets/template/index.tsx delete mode 100644 apps/admin/src/views/extra-features/assets/template/tabs/email.tsx delete mode 100644 apps/admin/src/views/extra-features/assets/template/tabs/markdown.tsx delete mode 100644 apps/admin/src/views/extra-features/markdown-helper.tsx delete mode 100644 apps/admin/src/views/extra-features/snippets/components/code-editor.tsx delete mode 100644 apps/admin/src/views/extra-features/snippets/components/fn-log-drawer.tsx delete mode 100644 apps/admin/src/views/extra-features/snippets/components/import-snippets-button.tsx delete mode 100644 apps/admin/src/views/extra-features/snippets/components/install-dep-button.tsx delete mode 100644 apps/admin/src/views/extra-features/snippets/components/install-dep-xterm.tsx delete mode 100644 apps/admin/src/views/extra-features/snippets/components/snippet-card.tsx delete mode 100644 apps/admin/src/views/extra-features/snippets/components/snippet-empty-state.tsx delete mode 100644 apps/admin/src/views/extra-features/snippets/components/snippet-list.tsx delete mode 100644 apps/admin/src/views/extra-features/snippets/components/snippet-meta-form.tsx delete mode 100644 apps/admin/src/views/extra-features/snippets/components/update-deps-button.tsx delete mode 100644 apps/admin/src/views/extra-features/snippets/composables/use-snippet-editor.ts delete mode 100644 apps/admin/src/views/extra-features/snippets/composables/use-snippet-list.ts delete mode 100644 apps/admin/src/views/extra-features/snippets/index.tsx delete mode 100644 apps/admin/src/views/extra-features/snippets/interfaces/snippet-group.ts delete mode 100644 apps/admin/src/views/extra-features/subscribe/constants.ts delete mode 100644 apps/admin/src/views/extra-features/subscribe/index.tsx delete mode 100644 apps/admin/src/views/extra-features/webhook/index.tsx delete mode 100644 apps/admin/src/views/login/index.tsx delete mode 100644 apps/admin/src/views/maintenance/backup.tsx delete mode 100644 apps/admin/src/views/maintenance/cron.tsx delete mode 100644 apps/admin/src/views/maintenance/search-index/components/constants.ts delete mode 100644 apps/admin/src/views/maintenance/search-index/components/search-index-detail-panel.tsx delete mode 100644 apps/admin/src/views/maintenance/search-index/components/search-index-empty-state.tsx delete mode 100644 apps/admin/src/views/maintenance/search-index/components/search-index-filter-bar.tsx delete mode 100644 apps/admin/src/views/maintenance/search-index/components/search-index-list-item.tsx delete mode 100644 apps/admin/src/views/maintenance/search-index/components/search-index-list.tsx delete mode 100644 apps/admin/src/views/maintenance/search-index/index.tsx delete mode 100644 apps/admin/src/views/manage-files/comment-images.tsx delete mode 100644 apps/admin/src/views/manage-files/index.tsx delete mode 100644 apps/admin/src/views/manage-files/orphans.tsx delete mode 100644 apps/admin/src/views/manage-friends/components/avatar.tsx delete mode 100644 apps/admin/src/views/manage-friends/components/fallback.jpg delete mode 100644 apps/admin/src/views/manage-friends/components/reason-modal.tsx delete mode 100644 apps/admin/src/views/manage-friends/index.tsx delete mode 100644 apps/admin/src/views/manage-friends/url-components.tsx delete mode 100644 apps/admin/src/views/manage-notes/components/topic-card.tsx delete mode 100644 apps/admin/src/views/manage-notes/components/topic-detail-panel.tsx delete mode 100644 apps/admin/src/views/manage-notes/components/topic-detail.tsx delete mode 100644 apps/admin/src/views/manage-notes/components/topic-list.tsx delete mode 100644 apps/admin/src/views/manage-notes/components/topic-modal.tsx delete mode 100644 apps/admin/src/views/manage-notes/hooks/use-memo-note-list.ts delete mode 100644 apps/admin/src/views/manage-notes/list.tsx delete mode 100644 apps/admin/src/views/manage-notes/topic.tsx delete mode 100644 apps/admin/src/views/manage-notes/write.tsx delete mode 100644 apps/admin/src/views/manage-pages/list.tsx delete mode 100644 apps/admin/src/views/manage-pages/write.tsx delete mode 100644 apps/admin/src/views/manage-posts/category.tsx delete mode 100644 apps/admin/src/views/manage-posts/components/ask-ai.tsx delete mode 100644 apps/admin/src/views/manage-posts/hooks/use-memo-post-list.ts delete mode 100644 apps/admin/src/views/manage-posts/list.tsx delete mode 100644 apps/admin/src/views/manage-posts/write.tsx delete mode 100644 apps/admin/src/views/manage-project/components/project-detail-panel.tsx delete mode 100644 apps/admin/src/views/manage-project/components/project-list.tsx delete mode 100644 apps/admin/src/views/manage-project/index.tsx delete mode 100644 apps/admin/src/views/manage-says/components/say-list-item.tsx delete mode 100644 apps/admin/src/views/manage-says/list.tsx delete mode 100644 apps/admin/src/views/reader/index.tsx delete mode 100644 apps/admin/src/views/setting/components/SettingListPanel.tsx delete mode 100644 apps/admin/src/views/setting/index.tsx delete mode 100644 apps/admin/src/views/setting/tabs/account.tsx delete mode 100644 apps/admin/src/views/setting/tabs/components/meta-preset-card.tsx delete mode 100644 apps/admin/src/views/setting/tabs/components/meta-preset-modal.tsx delete mode 100644 apps/admin/src/views/setting/tabs/meta-presets.tsx delete mode 100644 apps/admin/src/views/setting/tabs/providers/oauth.ts delete mode 100644 apps/admin/src/views/setting/tabs/sections/ai-config.tsx delete mode 100644 apps/admin/src/views/setting/tabs/sections/oauth.tsx delete mode 100644 apps/admin/src/views/setting/tabs/system.tsx delete mode 100644 apps/admin/src/views/setting/tabs/user.tsx delete mode 100644 apps/admin/src/views/setup-api/index.tsx delete mode 100644 apps/admin/src/views/setup/index.tsx delete mode 100644 apps/admin/src/views/shorthand/index.module.css delete mode 100644 apps/admin/src/views/shorthand/index.tsx create mode 100644 apps/admin/src/vite-env.d.ts delete mode 100644 apps/admin/src/vue-app-env.d.ts diff --git a/CLAUDE.md b/CLAUDE.md index f2165091b..f4c747806 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project Overview -MX Admin (admin-vue3) is the dashboard for MX Space, a personal blog management system. Built with Vue 3, Naive UI, and UnoCSS. This is the v4.0 admin interface for Mix Space Server v5.0. +MX Admin is the dashboard for MX Space, a personal blog management system. The active admin app is a React application built with Base UI primitives, React Router, TanStack Query, Sonner, and UnoCSS. ## Development Commands @@ -12,22 +12,22 @@ MX Admin (admin-vue3) is the dashboard for MX Space, a personal blog management pnpm install # Install dependencies pnpm dev # Start development server (opens browser automatically) pnpm build # Build for production -pnpm lint # Lint code with Biome +pnpm lint # Lint code with oxlint pnpm lint:fix # Lint and auto-fix -npx tsc --noEmit # Type check (use this instead of build for validation) +pnpm -C apps/admin exec tsc --noEmit --pretty false ``` ## Architecture Overview ### Technology Stack -- **Vue 3** with Composition API and TSX (JSX via `@vitejs/plugin-vue-jsx`) -- **Naive UI** - Component library with Vercel-style neutral theme +- **React** with TSX +- **Base UI** - Headless component primitives +- **React Router** - Route rendering and shell navigation - **UnoCSS** (preset-wind4) - Tailwind-compatible utility classes -- **Pinia** - State management -- **TanStack Query** (`@tanstack/vue-query`) - Server state management with localStorage persistence +- **TanStack Query** (`@tanstack/react-query`) - Server state management +- **Sonner** - Toast notifications - **Socket.IO** - Real-time WebSocket updates -- **CodeMirror/Monaco** - Code editors ### Path Aliases @@ -35,9 +35,9 @@ npx tsc --noEmit # Type check (use this instead of build for validation) import { something } from '~/utils/...' // ~ maps to ./src ``` -### API Layer (`src/api/`) +### API Layer (`src/app/api/`) -API services use the custom request layer built on `ofetch`. The backend wraps array responses as `{ data: [...] }`, which is automatically unwrapped by the request layer. +React app API services use the fetch-based helpers in `src/app/api/http.ts`. When using TanStack Query, extract arrays with: ```typescript @@ -48,14 +48,6 @@ select: (res: any) => Array.isArray(res) ? res : res?.data ?? [] - `BusinessError` - Application-level errors (4xx responses) - `SystemError` - Network/server errors (5xx responses, network failures) -### State Management - -**Pinia Stores (`src/stores/`):** -- `useUIStore` - Theme mode (light/dark/system), viewport dimensions, sidebar state -- `useUserStore` - User authentication state -- `useAppStore` - Global application state -- `useCategoryStore` - Category data - ### Responsive Breakpoints (UnoCSS) - `phone:` - max-width: 768px @@ -66,7 +58,7 @@ select: (res: any) => Array.isArray(res) ? res : res?.data ?? [] ### Validation -After modifying code, run type check only (`npx tsc --noEmit`). Do not run `pnpm build` for validation. +After modifying code, run focused type checking and linting. Run production build before reporting completion for broad application changes. ### Gray Scale Colors @@ -92,8 +84,8 @@ See `docs/typography.md` for full guidelines. ## Configuration Files - `uno.config.ts` - UnoCSS configuration with custom breakpoints and theme colors -- `src/utils/color.ts` - Naive UI theme overrides (Vercel-style neutral gray palette) -- `biome.json` - Linter/formatter configuration with Vue globals +- `src/app/theme.ts` - CSS token installation for the React shell +- `src/app/` - React routes, shell, API helpers, UI primitives, and migrated views - `.env` - Local dev API endpoint (`VITE_APP_BASE_API`) ## Related Projects @@ -102,9 +94,6 @@ See `docs/typography.md` for full guidelines. - **Shiroi** — Next.js frontend (blog), located at `../Shiroi` - **haklex** — Rich editor packages (`@haklex/*`), located at `../haklex` (standalone) or `../Shiroi/haklex` (original host) -### Rich Editor Integration (React-in-Vue) +### Rich Editor Integration -admin-vue3 is a Vue 3 project but embeds the React-based haklex editor via a bridge pattern in `src/components/editor/rich/RichEditor.tsx`: -- Uses `createRoot()` from `react-dom/client` inside Vue `defineComponent` to mount the local `ShiroEditor` (`packages/rich-react/src/shiro/`) -- Local Shiro lives in `packages/rich-react/src/shiro/` — composes `@haklex/rich-editor` + per-feature `@haklex/rich-ext-*` / `@haklex/rich-renderer-*` packages directly -- All `@haklex/*` packages are pinned npm versions (not workspace links). After haklex releases, update versions here. +The admin app no longer mounts rich editor surfaces through a framework bridge. React editor work should be integrated as ordinary React components and kept out of compatibility shims. diff --git a/apps/admin/index.html b/apps/admin/index.html index d02774d1b..4ce7eb789 100644 --- a/apps/admin/index.html +++ b/apps/admin/index.html @@ -15,7 +15,7 @@ link.href = favicon document.head.appendChild(link) - Mx Space Admin Vue 3 v2 + Mx Space Admin - + diff --git a/apps/admin/package.json b/apps/admin/package.json index 9b7ff9121..bd7343aad 100644 --- a/apps/admin/package.json +++ b/apps/admin/package.json @@ -12,100 +12,54 @@ }, "dependencies": { "@antv/g2": "^5.4.8", + "@base-ui/react": "1.5.0", "@better-auth/passkey": "1.4.18", - "@bytebase/vue-kbar": "0.1.8", - "@codemirror/commands": "6.10.3", - "@codemirror/lang-markdown": "6.5.0", - "@codemirror/language": "6.12.3", - "@codemirror/language-data": "6.5.2", - "@codemirror/search": "6.7.0", - "@codemirror/state": "6.6.0", - "@codemirror/theme-one-dark": "6.1.3", - "@codemirror/view": "6.42.1", - "@ddietr/codemirror-themes": "1.5.2", - "@emoji-mart/data": "1.2.1", "@excalidraw/excalidraw": "^0.18.0", - "@haklex/rich-agent-chat": "0.8.0", - "@haklex/rich-agent-core": "0.8.0", - "@haklex/rich-diff": "0.8.0", - "@haklex/rich-editor": "0.8.0", - "@haklex/rich-ext-ai-agent": "0.8.0", - "@haklex/rich-ext-nested-doc": "0.8.0", - "@haklex/rich-style-token": "0.8.0", - "@lexical/code-core": "^0.44.0", - "@lexical/markdown": "^0.44.0", - "@lezer/highlight": "1.2.3", "@mx-admin/rich-react": "workspace:*", - "@pierre/diffs": "1.1.3", "@simplewebauthn/browser": "13.3.0", "@tanstack/query-async-storage-persister": "5.95.0", "@tanstack/query-persist-client-core": "5.95.0", - "@tanstack/vue-query": "5.95.0", + "@tanstack/react-query": "5.100.13", "@types/canvas-confetti": "1.9.0", - "@typescript/ata": "0.9.8", - "@vicons/utils": "0.1.4", - "@vueuse/core": "14.2.1", - "@xterm/addon-fit": "0.11.0", - "@xterm/xterm": "6.0.0", - "ansi_up": "6.0.6", "better-auth": "1.4.18", "blurhash": "2.0.5", "buffer": "6.0.3", "canvas-confetti": "1.9.4", "date-fns": "4.1.0", - "ejs": "4.0.1", - "emoji-mart": "5.6.0", "es-toolkit": "1.45.1", "event-source-polyfill": "1.0.31", "fuse.js": "7.1.0", - "highlight.js": "11.11.1", "js-cookie": "3.0.5", "js-yaml": "4.1.1", "json5": "2.2.3", - "jsondiffpatch": "0.7.3", - "katex": "0.16.40", - "lexical": "^0.44.0", "lit": "3.3.2", "lodash.transform": "4.6.0", - "lucide-vue-next": "0.574.0", - "markdown-escape": "2.0.0", + "lucide-react": "1.8.0", "marked": "17.0.5", - "monaco-editor": "0.55.1", - "naive-ui": "2.44.1", - "octokit": "5.0.5", "ofetch": "1.5.1", - "openai": "6.32.0", "os-browserify": "0.3.0", "path-browserify": "1.0.1", - "pinia": "3.0.4", "qier-progress": "1.0.4", "qs": "6.15.0", - "shiki": "3.21.0", + "react": "19.2.4", + "react-dom": "19.2.4", + "react-router": "7.15.1", "socket.io-client": "4.8.3", - "sortablejs": "1.15.7", - "umi-request": "1.4.0", + "sonner": "2.0.7", "validator": "13.15.26", - "vue": "3.5.30", - "vue-router": "4.6.4", - "vue-sonner": "2.0.9", "xss": "1.0.15", - "xterm-theme": "1.1.0", "zod": "4.3.6" }, "devDependencies": { - "@types/ejs": "3.1.5", "@types/event-source-polyfill": "1.0.5", "@types/js-yaml": "4.0.9", - "@types/markdown-escape": "1.1.3", "@types/qs": "6.15.0", - "@types/sortablejs": "1.15.9", + "@types/react": "19.2.14", + "@types/react-dom": "19.2.3", "@types/validator": "13.15.10", "@unocss/postcss": "^66.6.8", "@unocss/preset-typography": "66.6.7", - "@vitejs/plugin-vue": "6.0.5", - "@vitejs/plugin-vue-jsx": "5.1.5", - "@vue/compiler-sfc": "3.5.30", - "@vue/test-utils": "^2.4.0", + "@vitejs/plugin-react": "6.0.2", "cors": "2.8.6", "happy-dom": "^15.11.0", "postcss": "8.5.8", @@ -117,7 +71,6 @@ "vite": "8.0.1", "vite-plugin-checker": "0.12.0", "vite-plugin-mkcert": "1.17.10", - "vite-plugin-vue-inspector": "5.4.0", "vitest": "^4.1.5" } } diff --git a/apps/admin/src/App.tsx b/apps/admin/src/App.tsx index 4a5f783cb..f38ef0eeb 100644 --- a/apps/admin/src/App.tsx +++ b/apps/admin/src/App.tsx @@ -1,132 +1,43 @@ -import { - darkTheme, - dateZhCN, - lightTheme, - NConfigProvider, - NDialogProvider, - NElement, - useDialog, - useThemeVars, - zhCN, -} from 'naive-ui' -import { defineComponent, onMounted, provide, ref, watchEffect } from 'vue' -import { RouterView } from 'vue-router' -import { Toaster } from 'vue-sonner' -import type { VNode } from 'vue' +import { useEffect } from 'react' +import { HashRouter, useLocation } from 'react-router' + +import { AppProviders } from './app/providers' +import { AppRoutes } from './app/routes' +import { AdminShell } from './app/shell' +import { installThemeTokens } from './app/theme' + +function App() { + useEffect(() => { + document.title = 'Mx Space Admin' + installThemeTokens() + }, []) + + return ( + + + + + + ) +} + +function AppContent() { + const location = useLocation() + + if ( + location.pathname === '/setup-api' || + location.pathname === '/setup' || + location.pathname === '/login' + ) { + return + } + + return ( + + + + ) +} -import { AiTaskQueue } from '~/components/ai-task-queue' -import { PortalInjectKey } from '~/hooks/use-portal-element' - -import { useUIStore } from './stores/ui' -import { - commonThemeVars, - componentThemeOverrides, - darkThemeColors, - lightThemeColors, -} from './utils/color' - -const Root = defineComponent({ - name: 'RootView', - - setup() { - onMounted(() => { - window.dialog = useDialog() - }) - const $portalElement = ref(null) - - provide(PortalInjectKey, { - setElement(el) { - $portalElement.value = el - return () => { - $portalElement.value = null - } - }, - }) - - return () => { - return ( - <> - - {$portalElement.value ?? <>} - - ) - } - }, -}) - -const App = defineComponent({ - setup() { - const uiStore = useUIStore() - return () => { - const { isDark, naiveUIDark } = uiStore - const isCurrentDark = naiveUIDark || isDark - - return ( - - - - - - - - - - - ) - } - }, -}) - -const AccentColorInjector = defineComponent({ - setup() { - const vars = useThemeVars() - watchEffect(() => { - const { primaryColor, primaryColorHover, primaryColorSuppl } = vars.value - - document.documentElement.style.setProperty( - '--color-primary', - primaryColor, - ) - document.documentElement.style.setProperty( - '--color-primary-shallow', - primaryColorHover, - ) - document.documentElement.style.setProperty( - '--color-primary-deep', - primaryColorSuppl, - ) - }) - - return () => <> - }, -}) // eslint-disable-next-line import/no-default-export export default App diff --git a/apps/admin/src/api/activity.ts b/apps/admin/src/api/activity.ts deleted file mode 100644 index 3e47f1ebb..000000000 --- a/apps/admin/src/api/activity.ts +++ /dev/null @@ -1,84 +0,0 @@ -import type { PaginateResult } from '~/models/base' -import type { NoteModel } from '~/models/note' -import type { PageModel } from '~/models/page' -import type { PostModel } from '~/models/post' -import type { RecentlyModel } from '~/models/recently' - -import { request } from '~/utils/request' - -export interface ActivityPresence { - operationTime: number - updatedAt: number - connectedAt: number - identity: string - roomName: string - position: number - sid: string - displayName?: string - ts?: number -} - -export interface ActivityItem { - id: string - created: string - payload: any - type: number -} - -export interface ActivityListResponse extends PaginateResult { - objects?: { - posts?: PostModel[] - notes?: NoteModel[] - pages?: PageModel[] - recentlies?: RecentlyModel[] - } -} - -export interface ReadingRankItem { - refId: string - count: number - ref: { - id?: string - title?: string - slug?: string - nid?: number - } -} - -export interface GetActivityParams { - page?: number - size?: number - type?: number - before?: string - after?: string -} - -export interface OnlineCountResponse { - total: number - rooms: Record -} - -export const activityApi = { - // 获取活动列表 - getList: (params?: GetActivityParams) => - request.get('/activity', { params }), - - // 获取阅读排行(轻量接口,带缓存) - getTopReadings: (params?: { top?: number; days?: number }) => - request.get('/activity/reading/top', { params }), - - // 获取阅读排行 - getReadingRank: (params?: { start?: number; end?: number; limit?: number }) => - request.get('/activity/reading/rank', { params }), - - // 获取最近动态列表 - getRecentlyList: (params?: GetActivityParams) => - request.get>('/recently/all', { params }), - - // 删除最近动态 - deleteRecently: (id: string) => request.delete(`/recently/${id}`), - - // 获取在线人数 - getOnlineCount: () => - request.get('/activity/online-count'), -} diff --git a/apps/admin/src/api/aggregate.ts b/apps/admin/src/api/aggregate.ts deleted file mode 100644 index 27854b180..000000000 --- a/apps/admin/src/api/aggregate.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { request } from '~/utils/request' - -export interface StatCount { - posts: number - notes: number - pages: number - categories: number - tags: number - comments: number - links: number - says: number - recently: number - unreadComments: number - online: number - todayMaxOnline: number - todayOnlineTotal: number - callTime: number - uv: number - todayIpAccessCount: number -} - -export interface CategoryDistribution { - id: string - name: string - slug: string - count: number -} - -export interface PublicationTrend { - date: string - posts: number - notes: number -} - -export interface TagCloudItem { - tag: string - count: number -} - -export interface TopArticle { - id: string - title: string - slug: string - reads: number - likes: number - category: { - name: string - slug: string - } | null -} - -export interface CommentActivityItem { - date: string - count: number -} - -export interface TrafficSourceData { - os: Array<{ name: string; count: number }> - browser: Array<{ name: string; count: number }> -} - -export interface WordCount { - count: number -} - -export interface ReadAndLikeCount { - totalLikes: number - totalReads: number -} - -export const aggregateApi = { - // 获取统计数据 - getStat: () => request.get('/aggregate/stat'), - - // 获取分类分布 - getCategoryDistribution: () => - request.get( - '/aggregate/stat/category-distribution', - ), - - // 获取发布趋势 - getPublicationTrend: () => - request.get('/aggregate/stat/publication-trend'), - - // 获取标签云 - getTagCloud: () => request.get('/aggregate/stat/tag-cloud'), - - // 获取热门文章 - getTopArticles: () => - request.get('/aggregate/stat/top-articles'), - - // 获取评论活动 - getCommentActivity: () => - request.get('/aggregate/stat/comment-activity'), - - // 获取流量来源 - getTrafficSource: () => - request.get('/aggregate/stat/traffic-source'), - - // 获取站点字数统计 - countSiteWords: () => request.get('/aggregate/count_site_words'), - - // 获取阅读和点赞统计 - countReadAndLike: () => - request.get('/aggregate/count_read_and_like'), - - // 获取站点点赞数 - getSiteLikeCount: () => request.get('/like_this'), - - // 清理缓存 - cleanCache: () => request.get('/clean_catch'), - - // 清理 Redis - cleanRedis: () => request.get('/clean_redis'), -} diff --git a/apps/admin/src/api/ai-agent.ts b/apps/admin/src/api/ai-agent.ts deleted file mode 100644 index b25f2332d..000000000 --- a/apps/admin/src/api/ai-agent.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { request } from '~/utils/request' - -export interface AgentConversation { - id: string - refId: string - refType: string - title?: string - model: string - providerId: string - createdAt: string - updatedAt: string - messageCount: number - messages?: Record[] - reviewState?: Record - diffState?: Record -} - -export const aiAgentApi = { - createConversation: (data: { - refId: string - refType: string - model: string - providerId: string - title?: string - messages?: Record[] - }) => request.post('/ai/agent/conversations', { data }), - - listConversations: (refId: string, refType: string) => - request.get('/ai/agent/conversations', { - params: { refId, refType }, - }), - - getConversation: (id: string) => - request.get(`/ai/agent/conversations/${id}`), - - appendMessages: (id: string, messages: Record[]) => - request.patch(`/ai/agent/conversations/${id}/messages`, { - data: { messages }, - }), - - replaceMessages: (id: string, messages: Record[]) => - request.put(`/ai/agent/conversations/${id}/messages`, { - data: { messages }, - }), - - updateConversation: ( - id: string, - data: { - title?: string - reviewState?: Record | null - diffState?: Record | null - }, - ) => - request.patch(`/ai/agent/conversations/${id}`, { data }), - - deleteConversation: (id: string) => - request.delete(`/ai/agent/conversations/${id}`), -} diff --git a/apps/admin/src/api/ai.ts b/apps/admin/src/api/ai.ts deleted file mode 100644 index a892a2b3f..000000000 --- a/apps/admin/src/api/ai.ts +++ /dev/null @@ -1,501 +0,0 @@ -import type { ContentFormat } from '~/shared/types/base' - -import { request } from '~/utils/request' - -// AI Writer 类型 -export enum AiQueryType { - TitleSlug = 'title-slug', - Slug = 'slug', -} - -export interface AIWriterGenerateData { - type: AiQueryType - text?: string // 当 type 为 title-slug 时需要 - title?: string // 当 type 为 slug 时需要 -} - -export interface AIWriterGenerateResponse { - title?: string - slug?: string -} - -// AI Summary 类型 -export interface AISummary { - id: string - createdAt: string - summary: string - hash: string - refId: string - lang: string -} - -export interface GroupedSummary { - type: string - items: AISummary[] -} - -export interface ArticleInfo { - type: 'Post' | 'Note' | 'Page' | 'Recently' - title: string - id: string -} - -export interface GroupedSummaryData { - article: ArticleInfo - summaries: AISummary[] -} - -export interface GroupedSummaryResponse { - data: GroupedSummaryData[] - pagination: { - total: number - currentPage: number - totalPage: number - size: number - hasNextPage: boolean - hasPrevPage: boolean - } -} - -export interface SummaryByRefResponse { - summaries: AISummary[] - article: { - type: 'Post' | 'Note' | 'Page' | 'Recently' - document: { title: string } - } -} - -// AI Insights 类型 -export interface AIInsights { - id: string - createdAt: string - refId: string - lang: string - hash: string - content: string - isTranslation: boolean - sourceInsightsId?: string - sourceLang?: string -} - -export interface GroupedInsightsData { - article: ArticleInfo - insights: AIInsights[] -} - -export interface GroupedInsightsResponse { - data: GroupedInsightsData[] - pagination: { - total: number - currentPage: number - totalPage: number - size: number - hasNextPage: boolean - hasPrevPage: boolean - } -} - -export interface InsightsByRefResponse { - insights: AIInsights[] - article: { - type: 'Post' | 'Note' | 'Page' | 'Recently' - document: { title: string } - } | null -} - -// AI Translation 类型 -export interface AITranslation { - id: string - createdAt: string - hash: string - refId: string - refType: string - lang: string - sourceLang: string - title: string - subtitle?: string - text: string - summary?: string - tags?: string[] - aiModel?: string - aiProvider?: string - contentFormat?: ContentFormat - content?: string -} - -export interface GroupedTranslationData { - article: ArticleInfo - translations: AITranslation[] -} - -export interface GroupedTranslationResponse { - data: GroupedTranslationData[] - pagination: { - total: number - currentPage: number - totalPage: number - size: number - hasNextPage: boolean - hasPrevPage: boolean - } -} - -export interface TranslationByRefResponse { - translations: AITranslation[] - article: { - type: 'Post' | 'Note' | 'Page' | 'Recently' - document: { title: string } - } -} - -export interface ProviderModel { - id: string - name: string -} - -export interface ProviderModelsResponse { - providerId: string - providerName: string - providerType: string - models: ProviderModel[] - error?: string -} - -export interface AITestData { - providerId: string - type: string - apiKey?: string - endpoint?: string - model?: string -} - -export interface AIModelListData { - providerId: string - type: string - apiKey?: string - endpoint?: string -} - -// AI Task 类型 -export enum AITaskType { - Summary = 'ai:summary', - Translation = 'ai:translation', - TranslationBatch = 'ai:translation:batch', - TranslationAll = 'ai:translation:all', - SlugBackfill = 'ai:slug:backfill', - Insights = 'ai:insights', - InsightsTranslation = 'ai:insights:translation', -} - -export enum AITaskStatus { - Pending = 'pending', - Running = 'running', - Completed = 'completed', - PartialFailed = 'partial_failed', - Failed = 'failed', - Cancelled = 'cancelled', -} - -export interface AITaskLog { - timestamp: number - level: 'info' | 'warn' | 'error' - message: string -} - -export interface SubTaskStats { - total: number - completed: number - failed: number - running: number - pending: number -} - -export interface AITask { - id: string - type: AITaskType - status: AITaskStatus - payload: Record - groupId?: string - - progress?: number - progressMessage?: string - totalItems?: number - completedItems?: number - tokensGenerated?: number - - createdAt: number - startedAt?: number - completedAt?: number - - result?: unknown - error?: string - logs: AITaskLog[] - - workerId?: string - retryCount: number - - // For batch tasks: sub-task statistics - subTaskStats?: SubTaskStats -} - -export interface AITasksResponse { - data: AITask[] - total: number -} - -export interface CreateTaskResponse { - taskId: string - created: boolean -} - -export interface AICommentReviewTestData { - text: string - author?: string -} - -export interface AICommentReviewTestResponse { - isSpam: boolean - score?: number - reason?: string -} - -// Translation Entry (词表) 类型 -export type TranslationEntryKeyPath = - | 'category.name' - | 'topic.name' - | 'topic.introduce' - | 'note.mood' - | 'note.weather' - -export interface TranslationEntry { - id: string - createdAt: string - keyPath: TranslationEntryKeyPath - lang: string - keyType: 'entity' | 'dict' - lookupKey: string - sourceText: string - translatedText: string - sourceUpdatedAt?: string -} - -export interface TranslationEntriesResponse { - data: TranslationEntry[] - pagination: { - total: number - page: number - size: number - } -} - -export interface GenerateEntriesResponse { - created: number - skipped: number -} - -export const aiApi = { - // AI 评论审核测试 - testCommentReview: (data: AICommentReviewTestData) => - request.post('/ai/comment-review/test', { - data, - }), - - // AI 写作生成标题/Slug - writerGenerate: (data: AIWriterGenerateData) => - request.post('/ai/writer/generate', { data }), - - // 获取摘要列表(分组) - getSummariesGrouped: (params?: { - page?: number - size?: number - search?: string - }) => - request.get('/ai/summaries/grouped', { params }), - - // 根据引用获取摘要 - getSummaryByRef: (refId: string) => - request.get(`/ai/summaries/ref/${refId}`), - - // 删除摘要 - deleteSummary: (id: string) => request.delete(`/ai/summaries/${id}`), - - // 更新摘要 - updateSummary: (id: string, data: { summary: string }) => - request.patch(`/ai/summaries/${id}`, { data }), - - // 生成摘要(创建任务) - createSummaryTask: (data: { refId: string; lang?: string }) => - request.post('/ai/summaries/task', { data }), - - // === AI Insights === - - // 获取精读列表(分组) - getInsightsGrouped: (params: { - page: number - size?: number - search?: string - }) => - request.get('/ai/insights/grouped', { params }), - - // 根据引用获取精读 - getInsightsByRef: (refId: string) => - request.get(`/ai/insights/ref/${refId}`), - - // 删除精读 - deleteInsights: (id: string) => request.delete(`/ai/insights/${id}`), - - // 更新精读 - updateInsights: (id: string, data: { content: string }) => - request.patch(`/ai/insights/${id}`, { data }), - - // 生成精读(创建任务) - createInsightsTask: (data: { refId: string }) => - request.post('/ai/insights/task', { data }), - - // 翻译精读(创建任务) - createInsightsTranslationTask: (data: { - refId: string - targetLang: string - }) => - request.post('/ai/insights/task/translate', { data }), - - // 获取可用模型列表 - getModels: () => request.get('/ai/models'), - - // 获取指定 provider 的模型列表 - getModelList: (data: AIModelListData) => - request.post<{ models: ProviderModel[]; error?: string }>( - '/ai/models/list', - { data }, - ), - - // 测试 AI 配置 - testConfig: (data: AITestData) => request.post('/ai/test', { data }), - - // === AI Translation === - - // 获取翻译列表(分组) - getTranslationsGrouped: (params?: { - page?: number - size?: number - search?: string - }) => - request.get('/ai/translations/grouped', { - params, - }), - - // 根据引用获取翻译 - getTranslationsByRef: (refId: string) => - request.get(`/ai/translations/ref/${refId}`), - - // 删除翻译 - deleteTranslation: (id: string) => - request.delete(`/ai/translations/${id}`), - - // 更新翻译 - updateTranslation: ( - id: string, - data: { - title?: string - subtitle?: string - text?: string - summary?: string - tags?: string[] - content?: string - }, - ) => request.patch(`/ai/translations/${id}`, { data }), - - // 生成翻译(创建任务) - createTranslationTask: (data: { - refId: string - targetLanguages?: string[] - }) => request.post('/ai/translations/task', { data }), - - // 批量生成翻译(创建任务) - createTranslationBatchTask: (data: { - refIds: string[] - targetLanguages?: string[] - }) => - request.post('/ai/translations/task/batch', { data }), - - // 为全部文章生成翻译(创建任务) - createTranslationAllTask: (data: { targetLanguages?: string[] }) => - request.post('/ai/translations/task/all', { data }), - - // === AI Tasks === - - // 获取任务列表 - getTasks: (params?: { - status?: AITaskStatus - type?: AITaskType - page?: number - size?: number - }) => request.get('/ai/tasks', { params }), - - // 获取单个任务 - getTask: (taskId: string) => request.get(`/ai/tasks/${taskId}`), - - // 重试任务 - retryTask: (taskId: string) => - request.post(`/ai/tasks/${taskId}/retry`), - - // 取消任务 - cancelTask: (taskId: string) => - request.post<{ success: boolean }>(`/ai/tasks/${taskId}/cancel`), - - // 删除单个任务 - deleteTask: (taskId: string) => - request.delete<{ success: boolean }>(`/ai/tasks/${taskId}`), - - // 批量删除任务 - deleteTasks: (params: { - status?: AITaskStatus - type?: AITaskType - before: number - }) => request.delete<{ deleted: number }>('/ai/tasks', { params }), - - // 获取组内所有任务(子任务) - getTasksByGroupId: (groupId: string) => - request.get(`/ai/tasks/group/${groupId}`), - - // 取消组内所有任务 - cancelTasksByGroupId: (groupId: string) => - request.delete<{ cancelled: number }>(`/ai/tasks/group/${groupId}`), - - // === Translation Entries (词表) === - - getTranslationEntries: (params?: { - keyPath?: TranslationEntryKeyPath - lang?: string - page?: number - size?: number - }) => - request.get('/ai/translations/entries', { - params, - }), - - generateTranslationEntries: (data?: { - keyPaths?: TranslationEntryKeyPath[] - targetLanguages?: string[] - }) => - request.post('/ai/translations/entries/generate', { - data, - }), - - updateTranslationEntry: (id: string, data: { translatedText: string }) => - request.patch(`/ai/translations/entries/${id}`, { data }), - - deleteTranslationEntry: (id: string) => - request.delete(`/ai/translations/entries/${id}`), - - // === Slug Backfill === - - getSlugBackfillStatus: () => - request.get<{ - count: number - notes: Array<{ id: string; title: string; nid: number }> - }>('/ai/writer/backfill-slugs/status'), - - createSlugBackfillTask: () => - request.post('/ai/writer/backfill-slugs'), -} diff --git a/apps/admin/src/api/analyze.ts b/apps/admin/src/api/analyze.ts deleted file mode 100644 index 1c04e45d7..000000000 --- a/apps/admin/src/api/analyze.ts +++ /dev/null @@ -1,74 +0,0 @@ -import type { UA } from '~/models/analyze' -import type { PaginateResult } from '~/models/base' - -import { request } from '~/utils/request' - -export type AnalyzeRecord = UA.Root & { - country?: string | null - referer?: string | null -} - -export interface IPAggregate { - today: Array<{ - hour: string - key: 'ip' | 'pv' - value: number - }> - weeks: Array<{ - day: string - key: 'ip' | 'pv' - value: number - }> - months: Array<{ - date: string - key: 'ip' | 'pv' - value: number - }> - paths: Array<{ - count: number - path: string - }> - total: { - callTime: number - uv: number - } - todayIps: string[] -} - -export interface GetAnalyzeParams { - page?: number - size?: number - from?: string - to?: string -} - -export interface TrafficSourceResponse { - categories: Array<{ name: string; value: number }> - details: Array<{ source: string; count: number }> -} - -export interface DeviceDistributionResponse { - browsers: Array<{ name: string; value: number }> - os: Array<{ name: string; value: number }> - devices: Array<{ name: string; value: number }> -} - -export const analyzeApi = { - // 获取分析列表 - getList: (params?: GetAnalyzeParams) => - request.get>('/analyze', { params }), - - // 获取聚合数据 - getAggregate: () => request.get('/analyze/aggregate'), - - // 获取流量来源 - getTrafficSource: (params?: { from?: string; to?: string }) => - request.get('/analyze/traffic-source', { params }), - - // 获取设备分布 - getDeviceDistribution: (params?: { from?: string; to?: string }) => - request.get('/analyze/device', { params }), - - // 清空分析数据 - deleteAll: () => request.delete('/analyze'), -} diff --git a/apps/admin/src/api/auth.ts b/apps/admin/src/api/auth.ts deleted file mode 100644 index f377a5fc5..000000000 --- a/apps/admin/src/api/auth.ts +++ /dev/null @@ -1,69 +0,0 @@ -import type { TokenModel } from '~/models/token' - -import { authClient } from '~/utils/authjs/auth' -import { request } from '~/utils/request' - -export interface CreateTokenData { - name: string - expired?: Date | string -} - -export interface PasskeyItem { - id: string - name?: string - credentialID: string - publicKey: string - createdAt: string -} - -export const authApi = { - // === Token 管理 === - - // 获取 Token 列表 - getTokens: () => request.get('/auth/token'), - - // 获取单个 Token - getToken: (id: string) => - request.get('/auth/token', { params: { id } }), - - // 创建 Token - createToken: (data: CreateTokenData) => - request.post('/auth/token', { data }), - - // 删除 Token - deleteToken: (id: string) => - request.delete('/auth/token', { params: { id } }), - - // === Passkey 管理(使用 Better Auth 客户端)=== - - // 获取 Passkey 列表 - getPasskeys: async () => { - const result = await authClient.passkey.listUserPasskeys() - if (result.error) { - throw new Error(result.error.message) - } - return (result.data || []).map((p: any) => ({ - id: p.id, - name: p.name, - credentialID: p.id, - publicKey: p.publicKey, - createdAt: p.createdAt, - })) - }, - - // 删除 Passkey - deletePasskey: async (id: string) => { - const result = await authClient.passkey.deletePasskey({ id }) - if (result.error) { - throw new Error(result.error.message) - } - }, - - // === 第三方认证 === - - // 获取 Session - getSession: () => request.get('/auth/session'), - - // 作为 Owner 认证 - authAsOwner: () => request.patch('/auth/as-owner'), -} diff --git a/apps/admin/src/api/backup.ts b/apps/admin/src/api/backup.ts deleted file mode 100644 index 6fa1c2872..000000000 --- a/apps/admin/src/api/backup.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { $api, request } from '~/utils/request' - -export interface BackupFile { - filename: string - size: string - createdAt: string -} - -export const backupApi = { - // 获取备份列表(响应会被自动解包) - getList: () => request.get('/backups'), - - // 创建新备份 - createNew: () => - $api('/backups/new', { - method: 'GET', - responseType: 'blob', - }) as Promise, - - // 下载备份文件 - download: (filename: string) => - $api(`/backups/${filename}`, { - method: 'GET', - responseType: 'blob', - }) as Promise, - - // 删除备份 - delete: (filename: string) => - request.delete(`/backups/${encodeURIComponent(filename)}`), - - // 从备份恢复 - rollback: (filename: string) => - request.patch(`/backups/rollback/${filename}`), - - // 上传备份文件并恢复 - uploadAndRestore: (file: File) => { - const formData = new FormData() - formData.append('file', file) - return request.post('/backups/rollback', { - data: formData, - }) - }, -} diff --git a/apps/admin/src/api/categories.ts b/apps/admin/src/api/categories.ts deleted file mode 100644 index b309a6050..000000000 --- a/apps/admin/src/api/categories.ts +++ /dev/null @@ -1,46 +0,0 @@ -import type { CategoryModel, TagModel } from '~/models/category' -import type { PostModel } from '~/models/post' - -import { request } from '~/utils/request' - -export interface GetCategoriesParams { - type?: 'Category' | 'Tag' | 'tag' -} - -export interface CreateCategoryData { - name: string - slug: string - type?: number -} - -export interface UpdateCategoryData extends Partial {} - -export const categoriesApi = { - // 获取分类列表(响应会被自动解包) - getList: (params?: GetCategoriesParams) => - request.get('/categories', { params }), - - // 获取单个分类(响应会被自动解包) - getById: (id: string) => request.get(`/categories/${id}`), - - // 创建分类(响应会被自动解包) - create: (data: CreateCategoryData) => - request.post('/categories', { data }), - - // 更新分类 - update: (id: string, data: UpdateCategoryData) => - request.put(`/categories/${id}`, { data }), - - // 删除分类 - delete: (id: string) => request.delete(`/categories/${id}`), - - // 获取标签列表(响应会被自动解包) - getTags: () => - request.get('/categories', { params: { type: 'tag' } }), - - // 获取标签关联的文章(响应会被自动解包) - getPostsByTag: (tagName: string) => - request.get(`/categories/${tagName}`, { - params: { tag: 'true' }, - }), -} diff --git a/apps/admin/src/api/comments.ts b/apps/admin/src/api/comments.ts deleted file mode 100644 index 7b15c2775..000000000 --- a/apps/admin/src/api/comments.ts +++ /dev/null @@ -1,53 +0,0 @@ -import type { CommentModel, CommentsResponse } from '~/models/comment' - -import { request } from '~/utils/request' - -export interface GetCommentsParams { - page?: number - size?: number - state?: number -} - -export interface ReplyCommentData { - text: string - author: string - mail: string - source?: string -} - -export const commentsApi = { - // 获取评论列表 - getList: (params?: GetCommentsParams) => - request.get('/comments', { params }), - - // 获取单个评论 - getById: (id: string) => request.get(`/comments/${id}`), - - // 回复评论(普通) - reply: (id: string, data: ReplyCommentData) => - request.post(`/comments/reader/reply/${id}`, { data }), - - // 登录态回复评论(只需 text) - readerReply: (id: string, text: string) => - request.post(`/comments/reader/reply/${id}`, { - data: { text }, - }), - - // 更新评论状态 - updateState: (id: string, state: number) => - request.patch(`/comments/${id}`, { data: { state } }), - - // 批量更新状态 - batchUpdateState: ( - options: - | { ids: string[]; state: number } - | { all: true; state: number; currentState: number }, - ) => request.patch('/comments/batch/state', { data: options }), - - // 删除评论 - delete: (id: string) => request.delete(`/comments/${id}`), - - // 批量删除 - batchDelete: (options: { ids: string[] } | { all: true; state: number }) => - request.delete('/comments/batch', { data: options }), -} diff --git a/apps/admin/src/api/cron-task.ts b/apps/admin/src/api/cron-task.ts deleted file mode 100644 index 53c03c7b1..000000000 --- a/apps/admin/src/api/cron-task.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { request } from '~/utils/request' - -export enum CronTaskType { - CleanAccessRecord = 'cron:clean-access-record', - ResetIPAccess = 'cron:reset-ip-access', - ResetLikedOrReadArticleRecord = 'cron:reset-liked-or-read', - CleanTempDirectory = 'cron:clean-temp-directory', - PushToBaiduSearch = 'cron:push-to-baidu-search', - PushToBingSearch = 'cron:push-to-bing-search', - DeleteExpiredJWT = 'cron:delete-expired-jwt', - RebuildSearchIndex = 'cron:rebuild-search-index', - CleanCommentUploads = 'cron:clean-comment-uploads', -} - -export enum CronTaskStatus { - Pending = 'pending', - Running = 'running', - Completed = 'completed', - PartialFailed = 'partial_failed', - Failed = 'failed', - Cancelled = 'cancelled', -} - -export interface CronTaskDefinition { - type: CronTaskType - name: string - description: string - cronExpression: string - lastDate?: string | null - nextDate?: string | null -} - -export interface CronTaskLog { - timestamp: number - level: 'info' | 'warn' | 'error' - message: string -} - -export interface CronTask { - id: string - type: CronTaskType - status: CronTaskStatus - payload: Record - - progress?: number - progressMessage?: string - - createdAt: number - startedAt?: number - completedAt?: number - - result?: unknown - error?: string - logs: CronTaskLog[] - - workerId?: string - retryCount: number -} - -export interface CronTasksResponse { - data: CronTask[] - total: number -} - -export interface CreateTaskResponse { - taskId: string - created: boolean -} - -export const cronTaskApi = { - getDefinitions: () => request.get('/cron-task'), - - getTasks: (params?: { - status?: CronTaskStatus - type?: CronTaskType - page?: number - size?: number - }) => request.get('/cron-task/tasks', { params }), - - getTask: (taskId: string) => - request.get(`/cron-task/tasks/${taskId}`), - - runTask: (type: CronTaskType) => - request.post(`/cron-task/run/${type}`), - - cancelTask: (taskId: string) => - request.post<{ success: boolean }>(`/cron-task/tasks/${taskId}/cancel`), - - retryTask: (taskId: string) => - request.post(`/cron-task/tasks/${taskId}/retry`), - - deleteTask: (taskId: string) => - request.delete<{ success: boolean }>(`/cron-task/tasks/${taskId}`), - - deleteTasks: (params: { - status?: CronTaskStatus - type?: CronTaskType - before: number - }) => request.delete<{ deleted: number }>('/cron-task/tasks', { params }), -} diff --git a/apps/admin/src/api/debug.ts b/apps/admin/src/api/debug.ts deleted file mode 100644 index 9cbcf402a..000000000 --- a/apps/admin/src/api/debug.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { request } from '~/utils/request' - -export interface ServerlessFunctionData { - function: string -} - -export const debugApi = { - // 执行 Serverless 函数 - executeFunction: (data: ServerlessFunctionData) => - request.post('/debug/function', { data }), -} diff --git a/apps/admin/src/api/dependencies.ts b/apps/admin/src/api/dependencies.ts deleted file mode 100644 index d5df78c94..000000000 --- a/apps/admin/src/api/dependencies.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { request } from '~/utils/request' - -export interface DependencyGraph { - dependencies: Record -} - -export const dependenciesApi = { - // 获取依赖图 - getGraph: () => request.get('/dependencies/graph'), -} diff --git a/apps/admin/src/api/drafts.ts b/apps/admin/src/api/drafts.ts deleted file mode 100644 index c8a26ab4f..000000000 --- a/apps/admin/src/api/drafts.ts +++ /dev/null @@ -1,74 +0,0 @@ -import type { Image, PaginateResult } from '~/models/base' -import type { - DraftHistoryListItem, - DraftModel, - DraftRefType, - TypeSpecificData, -} from '~/models/draft' - -import { request } from '~/utils/request' - -export type DraftSortOrder = 'asc' | 'desc' - -export interface GetDraftsParams { - page?: number - size?: number - refType?: DraftRefType - hasRef?: boolean - sort_by?: string - sort_order?: DraftSortOrder -} - -export interface CreateDraftData { - refType: DraftRefType - refId?: string - title?: string - text?: string - contentFormat?: 'markdown' | 'lexical' - content?: string - images?: Image[] - meta?: Record - typeSpecificData?: TypeSpecificData -} - -export interface UpdateDraftData extends Partial {} - -export const draftsApi = { - // 获取草稿列表 - getList: (params?: GetDraftsParams) => - request.get>('/drafts', { params }), - - // 获取单个草稿 - getById: (id: string) => request.get(`/drafts/${id}`), - - // 根据引用获取草稿 - getByRef: (refType: DraftRefType, refId: string) => - request.get(`/drafts/by-ref/${refType}/${refId}`), - - // 获取新草稿列表(无关联的草稿) - getNewDrafts: (refType: DraftRefType) => - request.get(`/drafts/by-ref/${refType}/new`), - - // 获取历史版本列表 - getHistory: (id: string) => - request.get(`/drafts/${id}/history`), - - // 获取特定历史版本 - getHistoryVersion: (id: string, version: number) => - request.get(`/drafts/${id}/history/${version}`), - - // 创建草稿 - create: (data: CreateDraftData) => - request.post('/drafts', { data }), - - // 更新草稿 - update: (id: string, data: UpdateDraftData) => - request.put(`/drafts/${id}`, { data }), - - // 删除草稿 - delete: (id: string) => request.delete<{ success: boolean }>(`/drafts/${id}`), - - // 恢复到特定版本 - restoreVersion: (id: string, version: number) => - request.post(`/drafts/${id}/restore/${version}`), -} diff --git a/apps/admin/src/api/enrichment.ts b/apps/admin/src/api/enrichment.ts deleted file mode 100644 index 744f4dac4..000000000 --- a/apps/admin/src/api/enrichment.ts +++ /dev/null @@ -1,109 +0,0 @@ -import type { - EnrichmentCaptureListResponse, - EnrichmentCaptureQuota, - EnrichmentImage, - EnrichmentListResponse, - EnrichmentProbeResult, - EnrichmentProviderMeta, - EnrichmentResult, - EnrichmentRowDetail, -} from '~/models/enrichment' - -import { request } from '~/utils/request' - -const encodeId = (id: string) => encodeURIComponent(id) - -export const enrichmentApi = { - resolve: (url: string, lang?: string) => - request.get('/enrichment/resolve', { - params: { url, ...(lang ? { lang } : {}) }, - }), - - list: ( - params: { - page?: number - size?: number - onlyFailed?: boolean - locale?: string - } = {}, - ) => - request.get('/enrichment/admin/list', { - params: { - ...params, - ...(params.onlyFailed ? { onlyFailed: true } : {}), - ...(params.locale !== undefined ? { locale: params.locale } : {}), - }, - }), - - providers: () => - request.get('/enrichment/admin/providers'), - - /** - * Refresh a single cache row. Pass `locale` (the row's locale, or empty for - * the default row) so the right per-locale row is updated. Omit (or pass - * empty string) to refresh the default (`''`) row. - */ - refresh: (provider: string, externalId: string, locale?: string) => - request.post( - `/enrichment/admin/refresh/${encodeURIComponent(provider)}/${encodeId(externalId)}`, - { - params: locale ? { lang: locale } : undefined, - }, - ), - - /** - * Drop cache for a (provider, externalId). Without `locale`, every locale - * variant of the resource is purged — admin "clear cache" semantics. - */ - invalidate: (provider: string, externalId: string, locale?: string) => - request.delete( - `/enrichment/admin/cache/${encodeURIComponent(provider)}/${encodeId(externalId)}`, - { - params: locale !== undefined ? { lang: locale } : undefined, - }, - ), - - byId: (id: string) => - request.get(`/enrichment/admin/by-id/${encodeId(id)}`), - - captures: { - list: ( - params: { - page?: number - size?: number - sort?: 'last_accessed' | 'created' | 'bytes' - order?: 'asc' | 'desc' - } = {}, - ) => { - const { sort = 'last_accessed', order = 'desc', ...rest } = params - return request.get( - '/enrichment/admin/captures', - { - params: { - ...rest, - sort, - order, - }, - }, - ) - }, - - quota: () => - request.get('/enrichment/admin/captures/quota'), - - delete: (enrichmentId: string) => - request.delete( - `/enrichment/admin/captures/${encodeId(enrichmentId)}`, - ), - - recapture: (enrichmentId: string) => - request.post( - `/enrichment/admin/captures/${encodeId(enrichmentId)}/recapture`, - ), - }, - - probe: (url: string, useCache?: boolean) => - request.post('/enrichment/admin/probe', { - data: { url, ...(useCache !== undefined ? { useCache } : {}) }, - }), -} diff --git a/apps/admin/src/api/files.ts b/apps/admin/src/api/files.ts deleted file mode 100644 index 561f7833b..000000000 --- a/apps/admin/src/api/files.ts +++ /dev/null @@ -1,156 +0,0 @@ -import { request } from '~/utils/request' - -export interface FileItem { - name: string - url: string - created?: number -} - -export interface UploadResponse { - url: string - name: string -} - -export interface OrphanFile { - id: string - fileName: string - fileUrl: string - status?: 'pending' | 'active' | 'detached' - uploadedBy?: string | null - readerId?: string | null - mimeType?: string | null - byteSize?: number | null - refType?: string | null - refId?: string | null - detachedAt?: string | null - createdAt: string -} - -export interface OrphanListResponse { - data: OrphanFile[] - pagination: { - currentPage: number - totalPage: number - size: number - total: number - hasNextPage: boolean - hasPrevPage: boolean - } -} - -export interface CleanupResult { - deletedCount: number - totalOrphan: number -} - -export interface ImageStorageOptions { - enable?: boolean - endpoint?: string - bucket?: string - region?: string - customDomain?: string - prefix?: string -} - -export const filesApi = { - // 按类型获取文件列表 - getByType: (type: string) => request.get(`/files/${type}`), - - // 上传文件 - upload: (file: File, type?: string) => { - const formData = new FormData() - formData.append('file', file) - return request.post('/files/upload', { - data: formData, - params: type ? { type } : undefined, - }) - }, - - // 更新已有文件(覆盖内容,保持文件名不变) - update: (type: string, name: string, file: File) => { - const formData = new FormData() - formData.append('file', file) - return request.put(`/files/${type}/${name}`, { - data: formData, - }) - }, - - // 按类型和名称删除文件 - deleteByTypeAndName: (type: string, name: string) => - request.delete(`/files/${type}/${name}`), - - // 重命名文件 - rename: (type: string, name: string, newName: string) => - request.patch(`/files/${type}/${name}/rename`, { - data: { name: newName }, - }), - - // 孤儿图片相关 - orphans: { - // 获取孤儿文件列表 - list: (page = 1, size = 24) => - request.get('/files/orphans/list', { - query: { page, size }, - }), - - // 获取孤儿文件数量 - count: () => request.get<{ count: number }>('/files/orphans/count'), - - // 清理孤儿文件 - cleanup: (maxAgeMinutes = 60) => - request.post('/files/orphans/cleanup', { - query: { maxAgeMinutes }, - }), - - // 批量删除孤儿文件 - batchDelete: (options: { ids: string[] } | { all: true }) => - request.delete<{ deletedCount: number }>('/files/orphans/batch', { - data: options, - }), - }, - - // 评论图片管理(reader uploads) - commentUploads: { - list: (params: { - page?: number - size?: number - status?: 'pending' | 'active' | 'detached' - readerId?: string - refId?: string - }) => - request.get('/files/comment-uploads/list', { - query: params, - }), - - delete: (id: string) => - request.delete<{ storageRemoved: boolean }>( - `/files/comment-uploads/${id}`, - ), - }, -} - -export interface CommentUploadFile { - id: string - fileName: string - fileUrl: string - status: 'pending' | 'active' | 'detached' - readerId?: string - mimeType?: string - byteSize?: number - refType?: string - refId?: string - detachedAt?: string - createdAt: string -} - -export interface CommentUploadListResponse { - data: CommentUploadFile[] - pagination: { - currentPage: number - totalPage: number - size: number - total: number - hasNextPage: boolean - hasPrevPage: boolean - } -} diff --git a/apps/admin/src/api/health.ts b/apps/admin/src/api/health.ts deleted file mode 100644 index 40c84dfc4..000000000 --- a/apps/admin/src/api/health.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { request } from '~/utils/request' - -export const healthApi = { - // 发送测试邮件 - sendTestEmail: () => - request.get<{ message?: string; trace?: string }>('/health/email/test'), -} diff --git a/apps/admin/src/api/index.ts b/apps/admin/src/api/index.ts deleted file mode 100644 index d82eda363..000000000 --- a/apps/admin/src/api/index.ts +++ /dev/null @@ -1,30 +0,0 @@ -// API 服务层统一导出 -export * from './activity' -export * from './aggregate' -export * from './ai' -export * from './analyze' -export * from './auth' -export * from './backup' -export * from './categories' -export * from './comments' -export * from './debug' -export * from './dependencies' -export * from './drafts' -export * from './files' -export * from './links' -export * from './markdown' -export * from './meta-presets' -export * from './notes' -export * from './options' -export * from './pages' -export * from './posts' -export * from './projects' -export * from './readers' -export * from './recently' -export * from './says' -export * from './search-index' -export * from './serverless' -export * from './snippets' -export * from './system' -export * from './topics' -export * from './user' diff --git a/apps/admin/src/api/links.ts b/apps/admin/src/api/links.ts deleted file mode 100644 index 89c271f97..000000000 --- a/apps/admin/src/api/links.ts +++ /dev/null @@ -1,65 +0,0 @@ -import type { LinkModel, LinkResponse, LinkStateCount } from '~/models/link' - -import { request } from '~/utils/request' - -export interface GetLinksParams { - page?: number - size?: number - state?: number -} - -export interface CreateLinkData { - name: string - url: string - avatar?: string - description?: string - type?: number - state?: number -} - -export interface UpdateLinkData extends Partial {} - -export const linksApi = { - // 获取友链列表 - getList: (params?: GetLinksParams) => - request.get('/links', { params }), - - // 获取状态计数 - getStateCount: () => request.get('/links/state'), - - // 获取单个友链 - getById: (id: string) => request.get(`/links/${id}`), - - // 创建友链 - create: (data: CreateLinkData) => request.post('/links', { data }), - - // 更新友链 - update: (id: string, data: UpdateLinkData) => - request.put(`/links/${id}`, { data }), - - // 删除友链 - delete: (id: string) => request.delete(`/links/${id}`), - - // 更新友链状态 - updateState: (id: string, state: number) => - request.patch(`/links/${id}`, { data: { state } }), - - // 检查友链健康状态 - checkHealth: (options?: { timeout?: number }) => - request.get< - Record - >('/links/health', { timeout: options?.timeout }), - - // 审核通过友链 - auditPass: (id: string) => request.patch(`/links/audit/${id}`), - - // 审核友链并发送理由 - auditWithReason: (id: string, state: number, reason: string) => - request.post(`/links/audit/reason/${id}`, { - data: { state, reason }, - }), - - // 迁移头像 - migrateAvatars: (options?: { timeout?: number }) => - request.post('/links/avatar/migrate', { timeout: options?.timeout }), -} diff --git a/apps/admin/src/api/markdown.ts b/apps/admin/src/api/markdown.ts deleted file mode 100644 index 3c3d17030..000000000 --- a/apps/admin/src/api/markdown.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { request } from '~/utils/request' - -export interface MarkdownImportData { - content?: string - type?: 'post' | 'note' | 'page' - data?: any[] -} - -export interface MarkdownExportParams { - type?: 'post' | 'note' | 'page' - id?: string - slug?: boolean - yaml?: boolean - show_title?: boolean - with_meta_json?: boolean -} - -export const markdownApi = { - // 导入 Markdown - import: (data: MarkdownImportData) => - request.post<{ id: string }>('/markdown/import', { data }), - - // 导出 Markdown - export: async (params?: MarkdownExportParams): Promise => { - // Use $api directly for blob responses - const { $api } = await import('~/utils/request') - return $api('/markdown/export', { - params, - responseType: 'blob' as any, - }) as Promise - }, -} diff --git a/apps/admin/src/api/meta-presets.ts b/apps/admin/src/api/meta-presets.ts deleted file mode 100644 index 7e17b48cf..000000000 --- a/apps/admin/src/api/meta-presets.ts +++ /dev/null @@ -1,49 +0,0 @@ -import type { - CreateMetaPresetDto, - MetaPresetField, - MetaPresetScope, - UpdateMetaPresetDto, -} from '~/models/meta-preset' - -import { request } from '~/utils/request' - -export interface MetaPresetQueryParams { - scope?: MetaPresetScope - enabledOnly?: boolean -} - -export const metaPresetsApi = { - /** - * 获取所有预设字段 - */ - getAll: (params?: MetaPresetQueryParams) => - request.get('/meta-presets', { params }), - - /** - * 获取单个预设字段 - */ - getById: (id: string) => request.get(`/meta-presets/${id}`), - - /** - * 创建自定义预设字段 - */ - create: (data: CreateMetaPresetDto) => - request.post('/meta-presets', { data }), - - /** - * 更新预设字段 - */ - update: (id: string, data: UpdateMetaPresetDto) => - request.patch(`/meta-presets/${id}`, { data }), - - /** - * 删除预设字段 - */ - delete: (id: string) => request.delete(`/meta-presets/${id}`), - - /** - * 批量更新排序 - */ - updateOrder: (ids: string[]) => - request.put('/meta-presets/order', { data: { ids } }), -} diff --git a/apps/admin/src/api/notes.ts b/apps/admin/src/api/notes.ts deleted file mode 100644 index b0b615b34..000000000 --- a/apps/admin/src/api/notes.ts +++ /dev/null @@ -1,85 +0,0 @@ -import type { PaginateResult } from '~/models/base' -import type { NoteModel } from '~/models/note' - -import { request } from '~/utils/request' - -export type NoteSortKey = - | 'title' - | 'createdAt' - | 'modifiedAt' - | 'weather' - | 'mood' -export type SortOrder = 'asc' | 'desc' - -export interface GetNotesParams { - page?: number - size?: number - sort_by?: NoteSortKey - sort_order?: SortOrder - /** - * @deprecated backend dropped db_query in v12.10.x pager refactor; param is silently ignored - */ - db_query?: Record -} - -export interface CreateNoteData { - title: string - text: string - slug?: string - mood?: string - weather?: string - password?: string | null - publicAt?: Date | null - bookmark?: boolean - location?: string | null - coordinates?: { longitude: number; latitude: number } | null - topicId?: string | null - isPublished?: boolean - meta?: Record - /** 关联的草稿 ID,发布时传递以标记草稿为已发布 */ - draftId?: string -} - -export interface UpdateNoteData extends Partial {} - -// 用于 patch 操作的数据类型,允许将某些字段设为 null -export interface PatchNoteData { - topicId?: string | null - slug?: string | null - [key: string]: unknown -} - -export const notesApi = { - // 获取日记列表 - getList: (params?: GetNotesParams) => - request.get>('/notes', { params }), - - // 获取单篇日记 - getById: (id: string, params?: { single?: boolean }) => - request.get(`/notes/${id}`, { params }), - - // 创建日记 - create: (data: CreateNoteData) => request.post('/notes', { data }), - - // 更新日记 - update: (id: string, data: UpdateNoteData) => - request.put(`/notes/${id}`, { data }), - - // 删除日记 - delete: (id: string) => request.delete(`/notes/${id}`), - - // 更新部分字段 - patch: (id: string, data: PatchNoteData) => - request.patch(`/notes/${id}`, { data }), - - // 更新发布状态 - patchPublish: (id: string, isPublished: boolean) => - request.patch(`/notes/${id}/publish`, { data: { isPublished } }), - - // 获取专栏下的日记列表 - getByTopic: (topicId: string, params?: { page?: number; size?: number }) => - request.get>>( - `/notes/topics/${topicId}`, - { params }, - ), -} diff --git a/apps/admin/src/api/options.ts b/apps/admin/src/api/options.ts deleted file mode 100644 index bf5c83ca1..000000000 --- a/apps/admin/src/api/options.ts +++ /dev/null @@ -1,51 +0,0 @@ -import type { FormDSL } from '~/components/config-form/types' - -import { request } from '~/utils/request' - -export interface SystemOptions { - [key: string]: any -} - -export interface EmailTemplateResponse { - template: string - props: any -} - -export const optionsApi = { - // 获取所有配置 - getAll: () => request.get('/options'), - - // 获取指定配置(后端直接返回配置对象) - get: (key: string) => request.get(`/options/${key}`), - - // 获取 URL 配置 - getUrl: () => - request.get<{ - webUrl: string - adminUrl: string - serverUrl: string - wsUrl: string - }>('/options/url'), - - // 更新指定配置 - patch: (key: string, data: any) => - request.patch(`/options/${key}`, { data }), - - // 获取表单 DSL Schema - getFormSchema: () => request.get('/config/form-schema'), - - // 获取邮件模板 - getEmailTemplate: (params: { type: string }) => - request.get('/options/email/template', { - params, - bypassTransform: true, - }), - - // 更新邮件模板 - updateEmailTemplate: (params: { type: string }, data: { source: string }) => - request.put('/options/email/template', { params, data }), - - // 删除邮件模板 - deleteEmailTemplate: (params: { type: string }) => - request.delete('/options/email/template', { params }), -} diff --git a/apps/admin/src/api/pages.ts b/apps/admin/src/api/pages.ts deleted file mode 100644 index 014cf0dad..000000000 --- a/apps/admin/src/api/pages.ts +++ /dev/null @@ -1,45 +0,0 @@ -import type { PaginateResult } from '~/models/base' -import type { PageModel } from '~/models/page' - -import { request } from '~/utils/request' - -export interface GetPagesParams { - page?: number - size?: number -} - -export interface CreatePageData { - title: string - text: string - slug: string - subtitle?: string - order?: number - meta?: Record - /** 关联的草稿 ID,发布时传递以标记草稿为已发布 */ - draftId?: string -} - -export interface UpdatePageData extends Partial {} - -export const pagesApi = { - // 获取页面列表 - getList: (params?: GetPagesParams) => - request.get>('/pages', { params }), - - // 获取单个页面 - getById: (id: string) => request.get(`/pages/${id}`), - - // 创建页面 - create: (data: CreatePageData) => request.post('/pages', { data }), - - // 更新页面 - update: (id: string, data: UpdatePageData) => - request.put(`/pages/${id}`, { data }), - - // 删除页面 - delete: (id: string) => request.delete(`/pages/${id}`), - - // 重新排序 - reorder: (seq: Array<{ id: string; order: number }>) => - request.patch('/pages/reorder', { data: { seq } }), -} diff --git a/apps/admin/src/api/posts.ts b/apps/admin/src/api/posts.ts deleted file mode 100644 index 4b80fa427..000000000 --- a/apps/admin/src/api/posts.ts +++ /dev/null @@ -1,57 +0,0 @@ -import type { PaginateResult } from '~/models/base' -import type { PostModel } from '~/models/post' - -import { request } from '~/utils/request' - -export type PostSortKey = 'createdAt' | 'modifiedAt' | 'pinAt' -export type SortOrder = 'asc' | 'desc' - -export interface GetPostsParams { - page?: number - size?: number - sort_by?: PostSortKey - sort_order?: SortOrder - categoryIds?: string[] -} - -export interface CreatePostData { - title: string - text: string - categoryId: string - slug?: string - tags?: string[] - summary?: string | null - copyright?: boolean - isPublished?: boolean - pin?: string | null - pinOrder?: number - relatedId?: string[] - meta?: Record - /** 关联的草稿 ID,发布时传递以标记草稿为已发布 */ - draftId?: string -} - -export interface UpdatePostData extends Partial {} - -export const postsApi = { - // 获取文章列表 - getList: (params?: GetPostsParams) => - request.get>('/posts', { params }), - - // 获取单篇文章 - getById: (id: string) => request.get(`/posts/${id}`), - - // 创建文章 - create: (data: CreatePostData) => request.post('/posts', { data }), - - // 更新文章 - update: (id: string, data: UpdatePostData) => - request.put(`/posts/${id}`, { data }), - - // 删除文章 - delete: (id: string) => request.delete(`/posts/${id}`), - - // 更新发布状态 - patch: (id: string, data: Partial) => - request.patch(`/posts/${id}`, { data }), -} diff --git a/apps/admin/src/api/projects.ts b/apps/admin/src/api/projects.ts deleted file mode 100644 index 33afa9aae..000000000 --- a/apps/admin/src/api/projects.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { ProjectModel, ProjectResponse } from '~/models/project' - -import { request } from '~/utils/request' - -export interface GetProjectsParams { - page?: number - size?: number -} - -export interface CreateProjectData { - name: string - description: string - text: string - previewUrl?: string - docUrl?: string - projectUrl?: string - images?: string[] - avatar?: string -} - -export interface UpdateProjectData extends Partial {} - -export const projectsApi = { - // 获取项目列表 - getList: (params?: GetProjectsParams) => - request.get('/projects', { params }), - - // 获取单个项目 - getById: (id: string) => request.get(`/projects/${id}`), - - // 创建项目 - create: (data: CreateProjectData) => - request.post('/projects', { data }), - - // 更新项目 - update: (id: string, data: UpdateProjectData) => - request.put(`/projects/${id}`, { data }), - - // 删除项目 - delete: (id: string) => request.delete(`/projects/${id}`), -} diff --git a/apps/admin/src/api/pty.ts b/apps/admin/src/api/pty.ts deleted file mode 100644 index 77b6dff4e..000000000 --- a/apps/admin/src/api/pty.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { request } from '~/utils/request' - -export interface PTYRecord { - id: string - command: string - output: string - exitCode?: number - created: string - duration?: number -} - -export const ptyApi = { - // 获取 PTY 记录列表 - getRecords: () => request.get('/pty/record'), - - // 获取单个 PTY 记录 - getRecord: (id: string) => request.get(`/pty/record/${id}`), - - // 删除 PTY 记录 - deleteRecord: (id: string) => request.delete(`/pty/record/${id}`), - - // 清空所有记录 - clearRecords: () => request.delete('/pty/record'), -} diff --git a/apps/admin/src/api/readers.ts b/apps/admin/src/api/readers.ts deleted file mode 100644 index b145fe6f9..000000000 --- a/apps/admin/src/api/readers.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { PaginateResult } from '~/models/base' - -import { request } from '~/utils/request' - -export interface ReaderModel { - id: string - provider?: string - type?: string - name: string - email: string - image: string - handle?: string - role: 'reader' | 'owner' -} - -export interface GetReadersParams { - page?: number - size?: number -} - -export const readersApi = { - // 获取读者列表 - getList: (params?: GetReadersParams) => - request.get>('/readers', { params }), -} diff --git a/apps/admin/src/api/recently.ts b/apps/admin/src/api/recently.ts deleted file mode 100644 index 8f06895c7..000000000 --- a/apps/admin/src/api/recently.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { RecentlyModel } from '~/models/recently' - -import { request } from '~/utils/request' - -export interface RecentlyCreatePayload { - content: string -} - -export type RecentlyUpdatePayload = RecentlyCreatePayload - -export const recentlyApi = { - // 获取最近访问列表 - getAll: () => request.get('/recently/all'), - - // 创建速记 - create: (data: RecentlyCreatePayload) => - request.post('/recently', { data }), - - // 更新速记 - update: (id: string, data: RecentlyUpdatePayload) => - request.put(`/recently/${id}`, { data }), - - // 删除最近访问项 - delete: (id: string) => request.delete(`/recently/${id}`), - - // 清空最近访问 - clear: () => request.delete('/recently/all'), -} diff --git a/apps/admin/src/api/says.ts b/apps/admin/src/api/says.ts deleted file mode 100644 index b6ccd1714..000000000 --- a/apps/admin/src/api/says.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { SayModel, SayResponse } from '~/models/say' - -import { request } from '~/utils/request' - -export interface GetSaysParams { - page?: number - size?: number -} - -export interface CreateSayData { - text: string - source?: string - author?: string -} - -export interface UpdateSayData extends Partial {} - -export const saysApi = { - // 获取一言列表 - getList: (params?: GetSaysParams) => - request.get('/says', { params }), - - // 获取单个一言 - getById: (id: string) => request.get(`/says/${id}`), - - // 创建一言 - create: (data: CreateSayData) => request.post('/says', { data }), - - // 更新一言 - update: (id: string, data: UpdateSayData) => - request.put(`/says/${id}`, { data }), - - // 删除一言 - delete: (id: string) => request.delete(`/says/${id}`), -} diff --git a/apps/admin/src/api/search-index.ts b/apps/admin/src/api/search-index.ts deleted file mode 100644 index e6235ce0b..000000000 --- a/apps/admin/src/api/search-index.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { - SearchDocumentAdminListParams, - SearchDocumentAdminListResponse, - SearchIndexRebuildOneResult, - SearchIndexRebuildResult, -} from '~/models/search-index' - -import { request } from '~/utils/request' - -const encode = (v: string) => encodeURIComponent(v) - -export const searchIndexApi = { - /** Trigger a global rebuild. `force=true` clears the table before rebuilding. */ - rebuildAll: (force = false) => - request.post('/search/rebuild', { - params: force ? { force: true } : undefined, - }), - - /** Rebuild index rows for a single (refType, refId). */ - rebuildOne: (refType: string, refId: string) => - request.post( - `/search/rebuild/${encode(refType)}/${encode(refId)}`, - ), - - /** Paginated listing of admin index rows. */ - listDocuments: (params: SearchDocumentAdminListParams = {}) => { - const query: Record = {} - if (params.refType) query.refType = params.refType - if (params.lang) query.lang = params.lang - if (params.keyword) query.keyword = params.keyword - if (params.page) query.page = params.page - if (params.size) query.size = params.size - return request.get( - '/search/admin/documents', - { params: query }, - ) - }, -} diff --git a/apps/admin/src/api/search.ts b/apps/admin/src/api/search.ts deleted file mode 100644 index 3b035db91..000000000 --- a/apps/admin/src/api/search.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { PaginateResult } from '~/models/base' -import type { NoteModel } from '~/models/note' -import type { PostModel } from '~/models/post' - -import { request } from '~/utils/request' - -export interface SearchParams { - keyword: string - page?: number - size?: number -} - -export const searchApi = { - // 搜索博文 - searchPosts: (params: SearchParams) => - request.get>('/search/post', { params }), - - // 搜索手记 - searchNotes: (params: SearchParams) => - request.get>('/search/note', { params }), -} diff --git a/apps/admin/src/api/serverless.ts b/apps/admin/src/api/serverless.ts deleted file mode 100644 index f307ee24c..000000000 --- a/apps/admin/src/api/serverless.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { request } from '~/utils/request' - -export interface ServerlessLogEntry { - id: string - functionId: string - reference: string - name: string - method: string - ip: string - status: 'success' | 'error' - executionTime: number - createdAt: string - logs?: { level: string; timestamp: number; args: unknown[] }[] - error?: { name: string; message: string; stack?: string } -} - -export interface ServerlessLogPagination { - total: number - size: number - currentPage: number - totalPage: number - hasNextPage: boolean - hasPrevPage: boolean -} - -export interface ServerlessLogListResponse { - data: ServerlessLogEntry[] - pagination: ServerlessLogPagination -} - -export interface GetServerlessLogsParams { - page?: number - size?: number - status?: 'success' | 'error' -} - -export const serverlessApi = { - getInvocationLogs: (id: string, params?: GetServerlessLogsParams) => - request.get(`/fn/logs/${id}`, { - params, - }), - - getInvocationLogDetail: (id: string) => - request.get(`/fn/log/${id}`), - - getCompiledCode: (id: string) => request.get(`/fn/compiled/${id}`), -} diff --git a/apps/admin/src/api/snippets.ts b/apps/admin/src/api/snippets.ts deleted file mode 100644 index 2e9796083..000000000 --- a/apps/admin/src/api/snippets.ts +++ /dev/null @@ -1,73 +0,0 @@ -import type { PaginateResult } from '~/models/base' -import type { SnippetModel, SnippetType } from '~/models/snippet' - -import { request } from '~/utils/request' - -export interface GetSnippetsParams { - page?: number - size?: number - type?: SnippetType - reference?: string -} - -export interface CreateSnippetData { - name: string - type: SnippetType - raw: string - reference?: string - private?: boolean - comment?: string - metatype?: string - schema?: string - enable?: boolean - method?: string - secret?: Record - customPath?: string -} - -export interface UpdateSnippetData extends Partial {} - -export interface SnippetGroup { - reference: string - count: number -} - -export interface ImportSnippetsData { - snippets: SnippetModel[] - packages?: string[] -} - -export const snippetsApi = { - // 获取片段列表 - getList: (params?: GetSnippetsParams) => - request.get>('/snippets', { params }), - - // 获取单个片段 - getById: (id: string) => request.get(`/snippets/${id}`), - - // 创建片段 - create: (data: CreateSnippetData) => - request.post('/snippets', { data }), - - // 更新片段 - update: (id: string, data: UpdateSnippetData) => - request.put(`/snippets/${id}`, { data }), - - // 删除片段 - delete: (id: string) => request.delete(`/snippets/${id}`), - - // 获取分组列表 - getGroups: (params?: { page?: number; size?: number }) => - request.get>('/snippets/group', { params }), - - // 获取分组下的片段 - getGroupSnippets: (reference: string) => - request.get(`/snippets/group/${reference}`), - - // 重置函数片段(内置函数) - resetFunction: (id: string) => request.delete(`/fn/reset/${id}`), - - // 导入片段 - import: (data: ImportSnippetsData) => - request.post('/snippets/import', { data }), -} diff --git a/apps/admin/src/api/subscribe.ts b/apps/admin/src/api/subscribe.ts deleted file mode 100644 index 57ad6c947..000000000 --- a/apps/admin/src/api/subscribe.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { request } from '~/utils/request' - -export interface Subscriber { - id: string - email: string - cancelToken: string - subscribe: number - verified: boolean - createdAt: string -} - -export interface SubscribeResponse { - data: Subscriber[] - pagination: { - total: number - currentPage: number - totalPage: number - size: number - hasNextPage: boolean - hasPrevPage: boolean - } -} - -export const subscribeApi = { - // 获取订阅状态 - getStatus: () => request.get<{ enable: boolean }>('/subscribe/status'), - - // 获取订阅列表 - getList: (params?: { page?: number; size?: number }) => - request.get('/subscribe', { params }), - - // 取消订阅 (单个,需 cancelToken) - unsubscribe: (params: { email: string; cancelToken: string }) => - request.get('/subscribe/unsubscribe', { params }), - - // 批量取消订阅 - unsubscribeBatch: (params: { emails?: string[]; all?: boolean }) => - request.delete<{ deletedCount: number }>('/subscribe/unsubscribe/batch', { - data: params, - }), -} diff --git a/apps/admin/src/api/system.ts b/apps/admin/src/api/system.ts deleted file mode 100644 index dc1218543..000000000 --- a/apps/admin/src/api/system.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { request } from '~/utils/request' - -export interface AppInfo { - name: string - version: string - hash?: string -} - -export interface InitData { - username: string - password: string - name: string - mail: string - url: string -} - -export interface DebugEventData { - type: string - payload: any -} - -export interface PtyRecord { - id: string - data: any -} - -export interface CreateOwnerData { - username: string - password: string - name?: string - mail: string - url?: string - avatar?: string - introduce?: string -} - -export const systemApi = { - // 获取应用信息 - getAppInfo: () => request.get('/'), - - // 检查是否已初始化(静默错误) - checkInit: async (): Promise<{ isInit: boolean }> => { - try { - return await request.get<{ isInit: boolean }>('/init') - } catch (error: any) { - // 404 或 403 表示已初始化 - if (error?.statusCode === 404 || error?.statusCode === 403) { - return { isInit: true } - } - throw error - } - }, - - // 初始化系统 - init: (data: InitData) => request.post('/init', { data }), - - // 获取初始化默认配置 - getInitDefaultConfigs: () => request.get('/init/configs/default'), - - // 更新初始化配置 - patchInitConfig: (key: string, data: any) => - request.patch(`/init/configs/${key}`, { data }), - - // 从备份恢复 - restoreFromBackup: (formData: FormData, timeout?: number) => - request.post('/init/restore', { data: formData, timeout }), - - // 创建站点主人 - createOwner: (data: CreateOwnerData) => - request.post('/init/owner', { data }), - - // === Debug === - - // 发送调试事件 - sendDebugEvent: (data: DebugEventData) => - request.post('/debug/events', { data }), - - // 执行 Serverless 函数 - executeFunction: (data: { code: string; context?: any }) => - request.post('/debug/function', { data }), - - // === PTY === - - // 获取 PTY 记录 - getPtyRecords: () => request.get('/pty/record'), - - // === 内置函数 === - - // 执行内置函数 - callBuiltInFunction: (name: string, params?: Record) => - request.get(`/fn/built-in/${name}`, { params }), - - // 获取函数类型定义 - getFnTypes: () => request.get('/fn/types'), -} diff --git a/apps/admin/src/api/templates.ts b/apps/admin/src/api/templates.ts deleted file mode 100644 index 518eb4ae1..000000000 --- a/apps/admin/src/api/templates.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { request } from '~/utils/request' - -export interface EmailTemplate { - subject: string - content: string - type: string -} - -export interface UpdateTemplateData { - subject?: string - content?: string -} - -export const templatesApi = { - // 获取邮件模板(后端直接返回模板对象) - getEmailTemplate: (type: string) => - request.get(`/options/email/template`, { - params: { type }, - }), - - // 更新邮件模板 - updateEmailTemplate: (type: string, data: UpdateTemplateData) => - request.put(`/options/email/template`, { data: { ...data, type } }), - - // 删除邮件模板(恢复默认) - deleteEmailTemplate: (params: { type: string }) => - request.delete(`/options/email/template`, { params }), -} diff --git a/apps/admin/src/api/topics.ts b/apps/admin/src/api/topics.ts deleted file mode 100644 index 0bbe08627..000000000 --- a/apps/admin/src/api/topics.ts +++ /dev/null @@ -1,43 +0,0 @@ -import type { PaginateResult } from '~/models/base' -import type { TopicModel } from '~/models/topic' - -import { request } from '~/utils/request' - -export interface GetTopicsParams { - page?: number - size?: number -} - -export interface CreateTopicData { - name: string - slug: string - introduce: string - description?: string - icon?: string -} - -export interface UpdateTopicData extends Partial {} - -export const topicsApi = { - // 获取专栏列表 - getList: (params?: GetTopicsParams) => - request.get>('/topics', { params }), - - // 获取单个专栏 - getById: (id: string) => request.get(`/topics/${id}`), - - // 创建专栏 - create: (data: CreateTopicData) => - request.post('/topics', { data }), - - // 更新专栏 - update: (id: string, data: UpdateTopicData) => - request.put(`/topics/${id}`, { data }), - - // 部分更新专栏 - patch: (id: string, data: Partial) => - request.patch(`/topics/${id}`, { data }), - - // 删除专栏 - delete: (id: string) => request.delete(`/topics/${id}`), -} diff --git a/apps/admin/src/api/user.ts b/apps/admin/src/api/user.ts deleted file mode 100644 index c4c0535aa..000000000 --- a/apps/admin/src/api/user.ts +++ /dev/null @@ -1,131 +0,0 @@ -import type { UserModel } from '~/models/user' - -import { authClient } from '~/utils/authjs/auth' -import { request } from '~/utils/request' - -export interface LoginData { - username: string - password: string -} - -export interface LoginResponse { - token?: string - user?: { - id: string - email: string - name: string - image?: string | null - emailVerified: boolean - createdAt: string | Date - updatedAt: string | Date - role?: 'reader' | 'owner' - } -} - -export interface UpdateOwnerData { - name?: string - username?: string - mail?: string - url?: string - avatar?: string - introduce?: string - socialIds?: Record -} - -export interface Session { - id: string - token: string - ua: string - ip: string - lastActiveAt: string - current?: boolean -} - -export interface AllowLoginResponse { - password: boolean - passkey: boolean - github?: boolean - google?: boolean - [key: string]: boolean | undefined -} - -export const userApi = { - // 获取当前 Owner 信息 - getOwner: () => request.get('/owner'), - - // 检查是否已登录 - checkLogged: () => request.get<{ ok: number }>('/owner/check_logged'), - - // 用户名密码登录(Cookie Session,不返回 JWT) - loginWithPassword: async (data: LoginData) => { - const result = await authClient.signIn.username({ - username: data.username, - password: data.password, - }) - - if (result.error) { - throw new Error(result.error.message || '登录失败') - } - - return result.data as LoginResponse - }, - - // 获取允许的登录方式 - getAllowLogin: () => request.get('/owner/allow-login'), - - // 更新 Owner 信息 - updateOwner: (data: UpdateOwnerData) => - request.patch('/owner', { data }), - - // 登出当前会话 - logout: async () => { - const result = await authClient.signOut() - if (result.error) { - throw new Error(result.error.message || '登出失败') - } - }, - - // 获取会话列表(Better Auth) - getSessions: async () => { - const [sessionsResult, currentResult] = await Promise.all([ - authClient.listSessions(), - authClient.getSession(), - ]) - - if (sessionsResult.error) { - throw new Error(sessionsResult.error.message || '获取会话失败') - } - - const currentToken = currentResult.data?.session?.token - - return (sessionsResult.data || []).map((session: any) => { - const token = session.token || session.id - return { - id: token, - token, - ua: session.userAgent || '', - ip: session.ipAddress || '', - lastActiveAt: new Date( - session.updatedAt || session.createdAt || Date.now(), - ).toISOString(), - current: currentToken ? token === currentToken : false, - } - }) as Session[] - }, - - // 删除指定会话 - deleteSession: async (token: string) => { - const result = await authClient.revokeSession({ token }) - if (result.error) { - throw new Error(result.error.message || '删除会话失败') - } - }, - - // 删除所有其他会话 - deleteAllSessions: async () => { - const result = await authClient.revokeOtherSessions() - if (result.error) { - throw new Error(result.error.message || '删除会话失败') - } - }, -} diff --git a/apps/admin/src/api/webhooks.ts b/apps/admin/src/api/webhooks.ts deleted file mode 100644 index 523c2f017..000000000 --- a/apps/admin/src/api/webhooks.ts +++ /dev/null @@ -1,71 +0,0 @@ -import type { PaginateResult } from '~/models/base' - -import { request } from '~/utils/request' - -export interface WebhookModel { - id: string - url: string - payloadUrl: string - events: string[] - secret?: string - enabled: boolean - scope: number - created: string - updated: string -} - -export interface CreateWebhookData { - url?: string - payloadUrl?: string - events: string[] - secret?: string - enabled?: boolean - scope?: number -} - -export interface UpdateWebhookData extends Partial {} - -export interface WebhookEventRecord { - id: string - event: string - headers: Record - payload: unknown - response: unknown - success: boolean - status: number - hookId: string - timestamp: string -} - -export const webhooksApi = { - // 获取 Webhook 列表 - getList: () => request.get('/webhooks'), - - // 获取可用事件列表 - getEvents: () => request.get('/webhooks/events'), - - // 创建 Webhook - create: (data: CreateWebhookData) => - request.post('/webhooks', { data }), - - // 更新 Webhook - update: (id: string, data: UpdateWebhookData) => - request.patch(`/webhooks/${id}`, { data }), - - // 删除 Webhook - delete: (id: string) => request.delete(`/webhooks/${id}`), - - // 测试 Webhook - test: (id: string, event: string) => - request.post(`/webhooks/${id}/test`, { data: { event } }), - - // 获取 Webhook 推送记录 - getDispatches: (id: string, params?: { page?: number; size?: number }) => - request.get>(`/webhooks/${id}`, { - params: { page: params?.page ?? 1, size: params?.size ?? 20 }, - }), - - // 重新推送 - redispatch: (hookId: string, eventId: string) => - request.post(`/webhooks/${hookId}/redispatch/${eventId}`), -} diff --git a/apps/admin/src/app/api/ai.ts b/apps/admin/src/app/api/ai.ts new file mode 100644 index 000000000..f3b218456 --- /dev/null +++ b/apps/admin/src/app/api/ai.ts @@ -0,0 +1,534 @@ +import { deleteJson, getJson, patchJson, postJson, requestJson } from './http' + +export enum AiQueryType { + Slug = 'slug', + TitleSlug = 'title-slug', +} + +export interface AIWriterGenerateData { + text?: string + title?: string + type: AiQueryType +} + +export interface AIWriterGenerateResponse { + slug?: string + title?: string +} + +export interface ArticleInfo { + id: string + title: string + type: 'Note' | 'Page' | 'Post' | 'Recently' +} + +export interface PaginationInfo { + currentPage?: number + hasNextPage?: boolean + hasPrevPage?: boolean + page?: number + size: number + total: number + totalPage?: number +} + +export interface AISummary { + createdAt: string + hash: string + id: string + lang: string + refId: string + summary: string +} + +export interface GroupedSummaryData { + article: ArticleInfo + summaries: AISummary[] +} + +export interface GroupedSummaryResponse { + data: GroupedSummaryData[] + pagination: PaginationInfo +} + +export interface SummaryByRefResponse { + article: { + document: { title: string } + type: 'Note' | 'Page' | 'Post' | 'Recently' + } + summaries: AISummary[] +} + +export interface AIInsights { + content: string + createdAt: string + hash: string + id: string + isTranslation: boolean + lang: string + refId: string + sourceInsightsId?: string + sourceLang?: string +} + +export interface GroupedInsightsData { + article: ArticleInfo + insights: AIInsights[] +} + +export interface GroupedInsightsResponse { + data: GroupedInsightsData[] + pagination: PaginationInfo +} + +export interface InsightsByRefResponse { + article: { + document: { title: string } + type: 'Note' | 'Page' | 'Post' | 'Recently' + } | null + insights: AIInsights[] +} + +export type AIContentFormat = 'lexical' | 'markdown' | string + +export interface AITranslation { + aiModel?: string + aiProvider?: string + content?: string + contentFormat?: AIContentFormat + createdAt: string + hash: string + id: string + lang: string + refId: string + refType: string + sourceLang: string + subtitle?: string + summary?: string + tags?: string[] + text: string + title: string +} + +export interface GroupedTranslationData { + article: ArticleInfo + translations: AITranslation[] +} + +export interface GroupedTranslationResponse { + data: GroupedTranslationData[] + pagination: PaginationInfo +} + +export interface TranslationByRefResponse { + article: { + document: { title: string } + type: 'Note' | 'Page' | 'Post' | 'Recently' + } + translations: AITranslation[] +} + +export interface ProviderModel { + id: string + name: string +} + +export interface ProviderModelsResponse { + error?: string + models: ProviderModel[] + providerId: string + providerName: string + providerType: string +} + +export interface AITestData { + apiKey?: string + endpoint?: string + model?: string + providerId: string + type: string +} + +export interface AIModelListData { + apiKey?: string + endpoint?: string + providerId: string + type: string +} + +export interface AICommentReviewTestData { + author?: string + text: string +} + +export interface AICommentReviewTestResponse { + isSpam: boolean + reason?: string + score?: number +} + +export type TranslationEntryKeyPath = + | 'category.name' + | 'note.mood' + | 'note.weather' + | 'topic.introduce' + | 'topic.name' + +export interface TranslationEntry { + createdAt: string + id: string + keyPath: TranslationEntryKeyPath + keyType: 'dict' | 'entity' + lang: string + lookupKey: string + sourceText: string + sourceUpdatedAt?: string + translatedText: string +} + +export interface TranslationEntriesResponse { + data: TranslationEntry[] + pagination: { + page: number + size: number + total: number + } +} + +export interface GenerateEntriesResponse { + created: number + skipped: number +} + +export enum AITaskType { + Summary = 'ai:summary', + Translation = 'ai:translation', + TranslationBatch = 'ai:translation:batch', + TranslationAll = 'ai:translation:all', + SlugBackfill = 'ai:slug:backfill', + Insights = 'ai:insights', + InsightsTranslation = 'ai:insights:translation', +} + +export enum AITaskStatus { + Pending = 'pending', + Running = 'running', + Completed = 'completed', + PartialFailed = 'partial_failed', + Failed = 'failed', + Cancelled = 'cancelled', +} + +export interface AITaskLog { + level: 'error' | 'info' | 'warn' + message: string + timestamp: number +} + +export interface SubTaskStats { + completed: number + failed: number + pending: number + running: number + total: number +} + +export interface AITask { + completedAt?: number + completedItems?: number + createdAt: number + error?: string + groupId?: string + id: string + logs: AITaskLog[] + payload: Record + progress?: number + progressMessage?: string + result?: unknown + retryCount: number + startedAt?: number + status: AITaskStatus + subTaskStats?: SubTaskStats + tokensGenerated?: number + totalItems?: number + type: AITaskType + workerId?: string +} + +export interface AITasksResponse { + data: AITask[] + total: number +} + +export interface CreateTaskResponse { + created: boolean + taskId: string +} + +export function testCommentReview(data: AICommentReviewTestData) { + return postJson( + '/ai/comment-review/test', + data, + ) +} + +export function writerGenerate(data: AIWriterGenerateData) { + return postJson( + '/ai/writer/generate', + data, + ) +} + +export function getSummariesGrouped(params?: { + page?: number + search?: string + size?: number +}) { + return getJson('/ai/summaries/grouped', params) +} + +export function getSummaryByRef(refId: string) { + return getJson(`/ai/summaries/ref/${refId}`) +} + +export function deleteSummary(id: string) { + return deleteJson(`/ai/summaries/${id}`) +} + +export function updateSummary(id: string, data: { summary: string }) { + return patchJson(`/ai/summaries/${id}`, data) +} + +export function createSummaryTask(data: { lang?: string; refId: string }) { + return postJson( + '/ai/summaries/task', + data, + ) +} + +export function getInsightsGrouped(params: { + page: number + search?: string + size?: number +}) { + return getJson('/ai/insights/grouped', params) +} + +export function getInsightsByRef(refId: string) { + return getJson(`/ai/insights/ref/${refId}`) +} + +export function deleteInsights(id: string) { + return deleteJson(`/ai/insights/${id}`) +} + +export function updateInsights(id: string, data: { content: string }) { + return patchJson(`/ai/insights/${id}`, data) +} + +export function createInsightsTask(data: { refId: string }) { + return postJson( + '/ai/insights/task', + data, + ) +} + +export function createInsightsTranslationTask(data: { + refId: string + targetLang: string +}) { + return postJson( + '/ai/insights/task/translate', + data, + ) +} + +export function getModels() { + return getJson('/ai/models') +} + +export function getModelList(data: AIModelListData) { + return postJson<{ error?: string; models: ProviderModel[] }, AIModelListData>( + '/ai/models/list', + data, + ) +} + +export function testConfig(data: AITestData) { + return postJson('/ai/test', data) +} + +export function getTranslationsGrouped(params?: { + page?: number + search?: string + size?: number +}) { + return getJson('/ai/translations/grouped', params) +} + +export function getTranslationsByRef(refId: string) { + return getJson(`/ai/translations/ref/${refId}`) +} + +export function deleteTranslation(id: string) { + return deleteJson(`/ai/translations/${id}`) +} + +export function updateTranslation( + id: string, + data: { + content?: string + subtitle?: string + summary?: string + tags?: string[] + text?: string + title?: string + }, +) { + return patchJson(`/ai/translations/${id}`, data) +} + +export function createTranslationTask(data: { + refId: string + targetLanguages?: string[] +}) { + return postJson< + CreateTaskResponse, + { refId: string; targetLanguages?: string[] } + >('/ai/translations/task', data) +} + +export function createTranslationBatchTask(data: { + refIds: string[] + targetLanguages?: string[] +}) { + return postJson< + CreateTaskResponse, + { refIds: string[]; targetLanguages?: string[] } + >('/ai/translations/task/batch', data) +} + +export function createTranslationAllTask(data: { targetLanguages?: string[] }) { + return postJson( + '/ai/translations/task/all', + data, + ) +} + +export interface GetAiTasksParams { + page?: number + size?: number + status?: AITaskStatus + type?: AITaskType +} + +export function getAiTasks(params: GetAiTasksParams = {}) { + return getJson('/ai/tasks', { + page: params.page, + size: params.size, + status: params.status, + type: params.type, + }).then(normalizeTasksResponse) +} + +export function getAiTask(taskId: string) { + return getJson(`/ai/tasks/${taskId}`) +} + +export function retryAiTask(taskId: string) { + return requestJson(`/ai/tasks/${taskId}/retry`, { + method: 'POST', + }) +} + +export function cancelAiTask(taskId: string) { + return requestJson<{ success: boolean }>(`/ai/tasks/${taskId}/cancel`, { + method: 'POST', + }) +} + +export function deleteAiTask(taskId: string) { + return deleteJson<{ success: boolean }>(`/ai/tasks/${taskId}`) +} + +export function deleteAiTasks(params: { + before: number + status?: AITaskStatus + type?: AITaskType +}) { + const searchParams = new URLSearchParams() + searchParams.set('before', String(params.before)) + if (params.status) searchParams.set('status', params.status) + if (params.type) searchParams.set('type', params.type) + + return requestJson<{ deleted: number }>(`/ai/tasks?${searchParams}`, { + method: 'DELETE', + }) +} + +export function getAiTasksByGroupId(groupId: string) { + return getJson(`/ai/tasks/group/${groupId}`) +} + +export function cancelAiTasksByGroupId(groupId: string) { + return deleteJson<{ cancelled: number }>(`/ai/tasks/group/${groupId}`) +} + +export function getTranslationEntries(params?: { + keyPath?: TranslationEntryKeyPath + lang?: string + page?: number + size?: number +}) { + return getJson('/ai/translations/entries', params) +} + +export function generateTranslationEntries(data?: { + keyPaths?: TranslationEntryKeyPath[] + targetLanguages?: string[] +}) { + return postJson< + GenerateEntriesResponse, + { keyPaths?: TranslationEntryKeyPath[]; targetLanguages?: string[] } | null + >('/ai/translations/entries/generate', data ?? null) +} + +export function updateTranslationEntry( + id: string, + data: { translatedText: string }, +) { + return patchJson( + `/ai/translations/entries/${id}`, + data, + ) +} + +export function deleteTranslationEntry(id: string) { + return deleteJson(`/ai/translations/entries/${id}`) +} + +export function getSlugBackfillStatus() { + return getJson<{ + count: number + notes: Array<{ id: string; nid: number; title: string }> + }>('/ai/writer/backfill-slugs/status') +} + +export function createSlugBackfillTask() { + return requestJson('/ai/writer/backfill-slugs', { + method: 'POST', + }) +} + +function normalizeTasksResponse( + response: AITasksResponse | AITask[], +): AITasksResponse { + if (Array.isArray(response)) { + return { + data: response, + total: response.length, + } + } + + return response +} diff --git a/apps/admin/src/app/api/analyze.ts b/apps/admin/src/app/api/analyze.ts new file mode 100644 index 000000000..18d1f7a64 --- /dev/null +++ b/apps/admin/src/app/api/analyze.ts @@ -0,0 +1,85 @@ +import type { UA } from '~/app/models/analyze' +import type { PaginateResult } from '~/app/models/base' + +import { deleteJson, getJson } from './http' + +export type AnalyzeRecord = UA.Root & { + country?: null | string + referer?: null | string +} + +export interface IPAggregate { + months: Array<{ + date: string + key: 'ip' | 'pv' + value: number + }> + paths: Array<{ + count: number + path: string + }> + today: Array<{ + hour: string + key: 'ip' | 'pv' + value: number + }> + todayIps: string[] + total: { + callTime: number + uv: number + } + weeks: Array<{ + day: string + key: 'ip' | 'pv' + value: number + }> +} + +export interface GetAnalyzeParams { + from?: string + page?: number + size?: number + to?: string +} + +export interface TrafficSourceResponse { + categories: Array<{ name: string; value: number }> + details: Array<{ count: number; source: string }> +} + +export interface DeviceDistributionResponse { + browsers: Array<{ name: string; value: number }> + devices: Array<{ name: string; value: number }> + os: Array<{ name: string; value: number }> +} + +export function getAnalyzeList(params: GetAnalyzeParams = {}) { + return getJson>('/analyze', { + from: params.from, + page: params.page, + size: params.size, + to: params.to, + }) +} + +export function getAnalyzeAggregate() { + return getJson('/analyze/aggregate') +} + +export function getTrafficSource(params?: { from?: string; to?: string }) { + return getJson('/analyze/traffic-source', { + from: params?.from, + to: params?.to, + }) +} + +export function getDeviceDistribution(params?: { from?: string; to?: string }) { + return getJson('/analyze/device', { + from: params?.from, + to: params?.to, + }) +} + +export function deleteAllAnalyzeRecords() { + return deleteJson('/analyze') +} diff --git a/apps/admin/src/app/api/backups.ts b/apps/admin/src/app/api/backups.ts new file mode 100644 index 000000000..aa6d2e36a --- /dev/null +++ b/apps/admin/src/app/api/backups.ts @@ -0,0 +1,85 @@ +import { API_URL } from '~/app/constants/env' + +import { deleteJson, patchJson } from './http' + +export interface BackupFile { + createdAt: string + filename: string + size: string +} + +export async function getBackups() { + return requestJson('/backups') +} + +export function createBackup() { + return requestBlob('/backups/new') +} + +export function downloadBackup(filename: string) { + return requestBlob(`/backups/${encodeURIComponent(filename)}`) +} + +export function deleteBackup(filename: string) { + return deleteJson(`/backups/${encodeURIComponent(filename)}`) +} + +export function rollbackBackup(filename: string) { + return patchJson>( + `/backups/rollback/${encodeURIComponent(filename)}`, + {}, + ) +} + +export async function uploadAndRestoreBackup(file: File) { + const formData = new FormData() + formData.append('file', file) + + const response = await fetch(`${API_URL}/backups/rollback`, { + body: formData, + credentials: 'include', + headers: { + 'x-skip-translation': '1', + }, + method: 'POST', + }) + + if (!response.ok) { + throw new Error(response.statusText || 'Upload restore failed') + } +} + +async function requestJson(path: string) { + const response = await fetch(`${API_URL}${path}`, { + credentials: 'include', + headers: { + 'x-skip-translation': '1', + }, + }) + + if (!response.ok) { + throw new Error(response.statusText || 'Request failed') + } + + const data = await response.json() + if (data && typeof data === 'object' && 'data' in data) { + return data.data as TResponse + } + + return data as TResponse +} + +async function requestBlob(path: string) { + const response = await fetch(`${API_URL}${path}`, { + credentials: 'include', + headers: { + 'x-skip-translation': '1', + }, + }) + + if (!response.ok) { + throw new Error(response.statusText || 'Request failed') + } + + return response.blob() +} diff --git a/apps/admin/src/app/api/categories.ts b/apps/admin/src/app/api/categories.ts new file mode 100644 index 000000000..4a5e1a896 --- /dev/null +++ b/apps/admin/src/app/api/categories.ts @@ -0,0 +1,44 @@ +import type { CategoryModel, TagModel } from '~/app/models/category' +import type { PostModel } from '~/app/models/post' + +import { deleteJson, getJson, postJson, putJson } from './http' + +export interface GetCategoriesParams { + type?: 'Category' | 'Tag' | 'tag' +} + +export interface CreateCategoryData { + name: string + slug: string + type?: number +} + +export type UpdateCategoryData = Partial + +export function getCategories(params?: GetCategoriesParams) { + return getJson('/categories', { type: params?.type }) +} + +export function getCategory(id: string) { + return getJson(`/categories/${id}`) +} + +export function createCategory(data: CreateCategoryData) { + return postJson('/categories', data) +} + +export function updateCategory(id: string, data: UpdateCategoryData) { + return putJson(`/categories/${id}`, data) +} + +export function deleteCategory(id: string) { + return deleteJson(`/categories/${id}`) +} + +export function getTags() { + return getJson('/categories', { type: 'tag' }) +} + +export function getPostsByTag(tagName: string) { + return getJson(`/categories/${tagName}`, { tag: 'true' }) +} diff --git a/apps/admin/src/app/api/comments.ts b/apps/admin/src/app/api/comments.ts new file mode 100644 index 000000000..0f28c94c9 --- /dev/null +++ b/apps/admin/src/app/api/comments.ts @@ -0,0 +1,52 @@ +import type { CommentModel, CommentsResponse } from '~/app/models/comment' + +import { deleteJson, getJson, patchJson, postJson } from './http' + +export interface GetCommentsParams { + page: number + size: number + state: number +} + +export interface ReplyCommentData { + text: string +} + +export function getComments(params: GetCommentsParams) { + return getJson('/comments', { + page: params.page, + size: params.size, + state: params.state, + }) +} + +export function replyComment(id: string, text: string) { + return postJson( + `/comments/reader/reply/${id}`, + { text }, + ) +} + +export function updateCommentState(id: string, state: number) { + return patchJson(`/comments/${id}`, { + state, + }) +} + +export function batchUpdateCommentState( + options: + | { currentState: number; all: true; state: number } + | { ids: string[]; state: number }, +) { + return patchJson('/comments/batch/state', options) +} + +export function deleteComment(id: string) { + return deleteJson(`/comments/${id}`) +} + +export function batchDeleteComments( + options: { all: true; state: number } | { ids: string[] }, +) { + return deleteJson('/comments/batch', options) +} diff --git a/apps/admin/src/app/api/cron-tasks.ts b/apps/admin/src/app/api/cron-tasks.ts new file mode 100644 index 000000000..b775edb1c --- /dev/null +++ b/apps/admin/src/app/api/cron-tasks.ts @@ -0,0 +1,128 @@ +import { deleteJson, getJson, requestJson } from './http' + +export enum CronTaskType { + CleanAccessRecord = 'cron:clean-access-record', + CleanCommentUploads = 'cron:clean-comment-uploads', + CleanTempDirectory = 'cron:clean-temp-directory', + DeleteExpiredJWT = 'cron:delete-expired-jwt', + PushToBaiduSearch = 'cron:push-to-baidu-search', + PushToBingSearch = 'cron:push-to-bing-search', + RebuildSearchIndex = 'cron:rebuild-search-index', + ResetIPAccess = 'cron:reset-ip-access', + ResetLikedOrReadArticleRecord = 'cron:reset-liked-or-read', +} + +export enum CronTaskStatus { + Cancelled = 'cancelled', + Completed = 'completed', + Failed = 'failed', + PartialFailed = 'partial_failed', + Pending = 'pending', + Running = 'running', +} + +export interface CronTaskDefinition { + cronExpression: string + description: string + lastDate?: string | null + name: string + nextDate?: string | null + type: CronTaskType +} + +export interface CronTaskLog { + level: 'error' | 'info' | 'warn' + message: string + timestamp: number +} + +export interface CronTask { + completedAt?: number + createdAt: number + error?: string + id: string + logs: CronTaskLog[] + payload: Record + progress?: number + progressMessage?: string + result?: unknown + retryCount: number + startedAt?: number + status: CronTaskStatus + type: CronTaskType + workerId?: string +} + +export interface CronTasksResponse { + data: CronTask[] + total: number +} + +export interface CreateTaskResponse { + created: boolean + taskId: string +} + +export interface CronTaskFilters { + page?: number + size?: number + status?: CronTaskStatus + type?: CronTaskType +} + +export function getCronTaskDefinitions() { + return getJson('/cron-task') +} + +export function getCronTasks(filters?: CronTaskFilters) { + return getJson('/cron-task/tasks', { + page: filters?.page, + size: filters?.size, + status: filters?.status, + type: filters?.type, + }) +} + +export function runCronTask(type: CronTaskType) { + return requestJson(`/cron-task/run/${type}`, { + method: 'POST', + }) +} + +export function cancelCronTask(taskId: string) { + return requestJson<{ success: boolean }>( + `/cron-task/tasks/${taskId}/cancel`, + { + method: 'POST', + }, + ) +} + +export function retryCronTask(taskId: string) { + return requestJson(`/cron-task/tasks/${taskId}/retry`, { + method: 'POST', + }) +} + +export function deleteCronTask(taskId: string) { + return deleteJson<{ success: boolean }>(`/cron-task/tasks/${taskId}`) +} + +export function deleteCronTasks(params: { + before: number + status?: CronTaskStatus + type?: CronTaskType +}) { + const searchParams = new URLSearchParams() + + searchParams.set('before', String(params.before)) + if (params.status) searchParams.set('status', params.status) + if (params.type) searchParams.set('type', params.type) + + return requestJson<{ deleted: number }>( + `/cron-task/tasks?${searchParams.toString()}`, + { + method: 'DELETE', + }, + ) +} diff --git a/apps/admin/src/app/api/drafts.ts b/apps/admin/src/app/api/drafts.ts new file mode 100644 index 000000000..73853b574 --- /dev/null +++ b/apps/admin/src/app/api/drafts.ts @@ -0,0 +1,82 @@ +import type { Image, PaginateResult } from '~/app/models/base' +import type { + DraftHistoryListItem, + DraftModel, + DraftRefType, + TypeSpecificData, +} from '~/app/models/draft' + +import { deleteJson, getJson, postJson, putJson } from './http' + +export type DraftSortOrder = 'asc' | 'desc' + +export interface GetDraftsParams { + hasRef?: boolean + page?: number + refType?: DraftRefType + size?: number + sort_by?: string + sort_order?: DraftSortOrder +} + +export interface CreateDraftData { + content?: string + contentFormat?: 'lexical' | 'markdown' + images?: Image[] + meta?: Record + refId?: string + refType: DraftRefType + text?: string + title?: string + typeSpecificData?: TypeSpecificData +} + +export function getDrafts(params: GetDraftsParams = {}) { + return getJson>('/drafts', { + hasRef: params.hasRef === undefined ? undefined : String(params.hasRef), + page: params.page, + refType: params.refType, + size: params.size, + sort_by: params.sort_by, + sort_order: params.sort_order, + }) +} + +export function getDraftById(id: string) { + return getJson(`/drafts/${id}`) +} + +export function getDraftByRef(refType: DraftRefType, refId: string) { + return getJson(`/drafts/by-ref/${refType}/${refId}`) +} + +export function getNewDrafts(refType: DraftRefType) { + return getJson(`/drafts/by-ref/${refType}/new`) +} + +export function getDraftHistory(id: string) { + return getJson(`/drafts/${id}/history`) +} + +export function getDraftHistoryVersion(id: string, version: number) { + return getJson(`/drafts/${id}/history/${version}`) +} + +export function createDraft(data: CreateDraftData) { + return postJson('/drafts', data) +} + +export function updateDraft(id: string, data: Partial) { + return putJson>(`/drafts/${id}`, data) +} + +export function deleteDraft(id: string) { + return deleteJson<{ success: boolean }>(`/drafts/${id}`) +} + +export function restoreDraftVersion(id: string, version: number) { + return postJson>( + `/drafts/${id}/restore/${version}`, + {}, + ) +} diff --git a/apps/admin/src/app/api/enrichment.ts b/apps/admin/src/app/api/enrichment.ts new file mode 100644 index 000000000..e27043b81 --- /dev/null +++ b/apps/admin/src/app/api/enrichment.ts @@ -0,0 +1,175 @@ +import type { + EnrichmentCaptureJoinedRow, + EnrichmentCaptureListResponse, + EnrichmentCaptureQuota, + EnrichmentImage, + EnrichmentListResponse, + EnrichmentProbeResult, + EnrichmentProviderMeta, + EnrichmentResult, + EnrichmentRow, + EnrichmentRowDetail, + LegacyPager, +} from '~/app/models/enrichment' + +import { deleteJson, getJson, requestJson } from './http' + +const encodeId = (id: string) => encodeURIComponent(id) + +export interface GetEnrichmentListParams { + locale?: string + onlyFailed?: boolean + page?: number + size?: number +} + +export interface GetEnrichmentCapturesParams { + order?: 'asc' | 'desc' + page?: number + size?: number + sort?: 'bytes' | 'created' | 'last_accessed' +} + +export function resolveEnrichment(url: string, lang?: string) { + return getJson('/enrichment/resolve', { lang, url }) +} + +export function getEnrichmentList(params: GetEnrichmentListParams = {}) { + return getJson( + '/enrichment/admin/list', + { + locale: params.locale, + onlyFailed: params.onlyFailed ? 'true' : undefined, + page: params.page, + size: params.size, + }, + ).then((response) => + normalizeListResponse(response, params.page ?? 1, params.size ?? 20), + ) +} + +export function getEnrichmentProviders() { + return getJson('/enrichment/admin/providers') +} + +export function refreshEnrichment( + provider: string, + externalId: string, + locale?: string, +) { + const query = locale ? `?lang=${encodeURIComponent(locale)}` : '' + + return requestJson( + `/enrichment/admin/refresh/${encodeURIComponent(provider)}/${encodeId(externalId)}${query}`, + { method: 'POST' }, + ) +} + +export function invalidateEnrichment( + provider: string, + externalId: string, + locale?: string, +) { + const query = + locale === undefined ? '' : `?lang=${encodeURIComponent(locale)}` + + return deleteJson( + `/enrichment/admin/cache/${encodeURIComponent(provider)}/${encodeId(externalId)}${query}`, + ) +} + +export function getEnrichmentById(id: string) { + return getJson(`/enrichment/admin/by-id/${encodeId(id)}`) +} + +export function getEnrichmentCaptures( + params: GetEnrichmentCapturesParams = {}, +) { + const sort = params.sort ?? 'last_accessed' + const order = params.order ?? 'desc' + + return getJson( + '/enrichment/admin/captures', + { + order, + page: params.page, + size: params.size, + sort, + }, + ).then((response) => + normalizeCaptureResponse(response, params.page ?? 1, params.size ?? 20), + ) +} + +export function getEnrichmentCaptureQuota() { + return getJson('/enrichment/admin/captures/quota') +} + +export function deleteEnrichmentCapture(enrichmentId: string) { + return deleteJson( + `/enrichment/admin/captures/${encodeId(enrichmentId)}`, + ) +} + +export function recaptureEnrichment(enrichmentId: string) { + return requestJson( + `/enrichment/admin/captures/${encodeId(enrichmentId)}/recapture`, + { method: 'POST' }, + ) +} + +export function probeEnrichment(url: string, useCache?: boolean) { + return requestJson('/enrichment/admin/probe', { + body: JSON.stringify({ + url, + ...(useCache === undefined ? {} : { useCache }), + }), + headers: { + 'content-type': 'application/json', + }, + method: 'POST', + }) +} + +function normalizeListResponse( + response: EnrichmentListResponse | EnrichmentRow[], + page: number, + size: number, +): EnrichmentListResponse { + if (Array.isArray(response)) { + return { + data: response, + pagination: fallbackPager(response.length, page, size), + } + } + + return response +} + +function normalizeCaptureResponse( + response: EnrichmentCaptureListResponse | EnrichmentCaptureJoinedRow[], + page: number, + size: number, +): EnrichmentCaptureListResponse { + if (Array.isArray(response)) { + return { + data: response, + pagination: fallbackPager(response.length, page, size), + } + } + + return response +} + +function fallbackPager(total: number, page: number, size: number): LegacyPager { + const totalPage = Math.max(1, Math.ceil(total / size)) + + return { + currentPage: page, + hasNextPage: page < totalPage, + hasPrevPage: page > 1, + size, + total, + totalPage, + } +} diff --git a/apps/admin/src/app/api/files.ts b/apps/admin/src/app/api/files.ts new file mode 100644 index 000000000..cecd3ba17 --- /dev/null +++ b/apps/admin/src/app/api/files.ts @@ -0,0 +1,152 @@ +import { deleteJson, getJson, patchJson, requestJson } from './http' + +export interface FileItem { + created?: number + name: string + url: string +} + +export interface UploadResponse { + name: string + url: string +} + +export interface OrphanFile { + byteSize?: null | number + createdAt: string + detachedAt?: null | string + fileName: string + fileUrl: string + id: string + mimeType?: null | string + readerId?: null | string + refId?: null | string + refType?: null | string + status?: 'active' | 'detached' | 'pending' + uploadedBy?: null | string +} + +export interface FileListPagination { + currentPage: number + hasNextPage: boolean + hasPrevPage: boolean + size: number + total: number + totalPage: number +} + +export interface OrphanListResponse { + data: OrphanFile[] + pagination: FileListPagination +} + +export interface CleanupResult { + deletedCount: number + totalOrphan: number +} + +export interface CommentUploadFile { + byteSize?: number + createdAt: string + detachedAt?: string + fileName: string + fileUrl: string + id: string + mimeType?: string + readerId?: string + refId?: string + refType?: string + status: 'active' | 'detached' | 'pending' +} + +export interface CommentUploadListResponse { + data: CommentUploadFile[] + pagination: FileListPagination +} + +export type FileType = 'avatar' | 'file' | 'icon' | 'image' +export type CommentUploadStatus = '' | 'active' | 'detached' | 'pending' + +export function getFilesByType(type: FileType) { + return getJson(`/files/${type}`) +} + +export function uploadFile(file: File, type?: FileType) { + const formData = new FormData() + formData.append('file', file) + + const query = type ? `?type=${encodeURIComponent(type)}` : '' + + return requestJson(`/files/upload${query}`, { + body: formData, + method: 'POST', + }) +} + +export function updateFile(type: FileType, name: string, file: File) { + const formData = new FormData() + formData.append('file', file) + + return requestJson( + `/files/${type}/${encodeURIComponent(name)}`, + { + body: formData, + method: 'PUT', + }, + ) +} + +export function deleteFileByTypeAndName(type: FileType, name: string) { + return deleteJson(`/files/${type}/${encodeURIComponent(name)}`) +} + +export function renameFile(type: FileType, name: string, newName: string) { + return patchJson( + `/files/${type}/${encodeURIComponent(name)}/rename`, + { name: newName }, + ) +} + +export function getOrphanFiles(page = 1, size = 24) { + return getJson('/files/orphans/list', { page, size }) +} + +export function getOrphanFileCount() { + return getJson<{ count: number }>('/files/orphans/count') +} + +export function cleanupOrphanFiles(maxAgeMinutes = 60) { + return requestJson( + `/files/orphans/cleanup?maxAgeMinutes=${maxAgeMinutes}`, + { method: 'POST' }, + ) +} + +export function batchDeleteOrphanFiles( + options: { all: true } | { ids: string[] }, +) { + return deleteJson<{ deletedCount: number }, typeof options>( + '/files/orphans/batch', + options, + ) +} + +export function getCommentUploads(params: { + page?: number + readerId?: string + refId?: string + size?: number + status?: Exclude +}) { + return getJson('/files/comment-uploads/list', { + page: params.page, + readerId: params.readerId, + refId: params.refId, + size: params.size, + status: params.status, + }) +} + +export function deleteCommentUpload(id: string) { + return deleteJson<{ storageRemoved: boolean }>(`/files/comment-uploads/${id}`) +} diff --git a/apps/admin/src/app/api/http.ts b/apps/admin/src/app/api/http.ts new file mode 100644 index 000000000..f0dc4cca4 --- /dev/null +++ b/apps/admin/src/app/api/http.ts @@ -0,0 +1,143 @@ +import { API_URL } from '~/app/constants/env' + +type ResponseEnvelope = { + data?: T + error?: { message?: string | string[] } + meta?: { + pagination?: unknown + } + message?: string | string[] +} + +export async function postJson( + path: string, + data: TData, +): Promise { + return requestJson(path, { + body: JSON.stringify(data), + headers: { + 'content-type': 'application/json', + }, + method: 'POST', + }) +} + +type QueryValue = Array | boolean | number | string | undefined + +export async function getJson( + path: string, + params?: Record, +): Promise { + return requestJson(withQuery(path, params), { method: 'GET' }) +} + +export async function requestJson( + path: string, + init: RequestInit, +): Promise { + const response = await fetch(`${API_URL}${path}`, { + credentials: 'include', + ...init, + headers: { + 'x-skip-translation': '1', + ...init.headers, + }, + }) + + const responseData = await readResponseData(response) + + if (!response.ok) { + const message = + responseData?.error?.message || + responseData?.message || + response.statusText + + throw new Error( + Array.isArray(message) ? message.join(', ') : message || 'Request failed', + ) + } + + if (responseData && 'data' in responseData) { + if (responseData.meta?.pagination) { + return { + data: responseData.data, + pagination: responseData.meta.pagination, + } as TResponse + } + + return responseData.data as TResponse + } + + return responseData as TResponse +} + +export async function putJson( + path: string, + data: TData, +): Promise { + return requestJson(path, { + body: JSON.stringify(data), + headers: { + 'content-type': 'application/json', + }, + method: 'PUT', + }) +} + +export async function patchJson( + path: string, + data: TData, +): Promise { + return requestJson(path, { + body: JSON.stringify(data), + headers: { + 'content-type': 'application/json', + }, + method: 'PATCH', + }) +} + +export async function deleteJson( + path: string, + data?: TData, +): Promise { + return requestJson(path, { + ...(data === undefined + ? {} + : { + body: JSON.stringify(data), + headers: { + 'content-type': 'application/json', + }, + }), + method: 'DELETE', + }) +} + +function withQuery(path: string, params?: Record) { + if (!params) return path + + const searchParams = new URLSearchParams() + + for (const [key, value] of Object.entries(params)) { + if (value === undefined) continue + if (Array.isArray(value)) { + value.forEach((item) => searchParams.append(key, String(item))) + continue + } + + searchParams.set(key, String(value)) + } + + const query = searchParams.toString() + + return query ? `${path}?${query}` : path +} + +async function readResponseData(response: Response) { + try { + return (await response.json()) as ResponseEnvelope + } catch { + return null + } +} diff --git a/apps/admin/src/app/api/links.ts b/apps/admin/src/app/api/links.ts new file mode 100644 index 000000000..01081416e --- /dev/null +++ b/apps/admin/src/app/api/links.ts @@ -0,0 +1,74 @@ +import type { LinkModel, LinkResponse, LinkStateCount } from '~/app/models/link' + +import { + deleteJson, + getJson, + patchJson, + postJson, + putJson, + requestJson, +} from './http' + +export interface GetLinksParams { + page: number + size: number + state: number +} + +export interface LinkInput { + avatar?: string + description?: string + name: string + state?: number + type?: number + url: string +} + +export function getLinks(params: GetLinksParams) { + return getJson('/links', { + page: params.page, + size: params.size, + state: params.state, + }) +} + +export function getLinkStateCount() { + return getJson('/links/state') +} + +export function createLink(data: LinkInput) { + return postJson('/links', data) +} + +export function updateLink(id: string, data: Partial) { + return putJson>(`/links/${id}`, data) +} + +export function deleteLink(id: string) { + return deleteJson(`/links/${id}`) +} + +export function auditPassLink(id: string) { + return requestJson(`/links/audit/${id}`, { method: 'PATCH' }) +} + +export function auditLinkWithReason( + id: string, + data: { reason: string; state: number }, +) { + return postJson(`/links/audit/reason/${id}`, data) +} + +export function updateLinkState(id: string, state: number) { + return patchJson(`/links/${id}`, { state }) +} + +export function checkLinksHealth() { + return getJson< + Record + >('/links/health') +} + +export function migrateLinkAvatars() { + return requestJson('/links/avatar/migrate', { method: 'POST' }) +} diff --git a/apps/admin/src/app/api/markdown.ts b/apps/admin/src/app/api/markdown.ts new file mode 100644 index 000000000..e5f0f4276 --- /dev/null +++ b/apps/admin/src/app/api/markdown.ts @@ -0,0 +1,49 @@ +import { API_URL } from '~/app/constants/env' + +import { postJson } from './http' + +export interface MarkdownImportData { + content?: string + data?: unknown[] + type?: 'note' | 'page' | 'post' +} + +export interface MarkdownExportParams { + id?: string + show_title?: boolean + slug?: boolean + type?: 'note' | 'page' | 'post' + with_meta_json?: boolean + yaml?: boolean +} + +export function importMarkdown(data: MarkdownImportData) { + return postJson<{ id: string }, MarkdownImportData>('/markdown/import', data) +} + +export async function exportMarkdown(params?: MarkdownExportParams) { + const searchParams = new URLSearchParams() + + if (params) { + for (const [key, value] of Object.entries(params)) { + if (value !== undefined) searchParams.set(key, String(value)) + } + } + + const query = searchParams.toString() + const response = await fetch( + `${API_URL}/markdown/export${query ? `?${query}` : ''}`, + { + credentials: 'include', + headers: { + 'x-skip-translation': '1', + }, + }, + ) + + if (!response.ok) { + throw new Error(response.statusText || 'Export failed') + } + + return response.blob() +} diff --git a/apps/admin/src/app/api/notes.ts b/apps/admin/src/app/api/notes.ts new file mode 100644 index 000000000..737e89dbe --- /dev/null +++ b/apps/admin/src/app/api/notes.ts @@ -0,0 +1,85 @@ +import type { PaginateResult } from '~/app/models/base' +import type { NoteModel } from '~/app/models/note' + +import { deleteJson, getJson, patchJson, postJson, putJson } from './http' + +export type NoteSortKey = + | 'createdAt' + | 'modifiedAt' + | 'mood' + | 'title' + | 'weather' +export type SortOrder = 'asc' | 'desc' + +export interface GetNotesParams { + page?: number + size?: number + sort_by?: NoteSortKey + sort_order?: SortOrder + topicId?: null | string +} + +export interface CreateNoteData { + bookmark?: boolean + content?: string + contentFormat?: 'lexical' | 'markdown' + draftId?: string + isPublished?: boolean + location?: null | string + meta?: Record + mood?: string + password?: null | string + publicAt?: Date | null + slug?: string + text: string + title: string + topicId?: null | string + weather?: string +} + +export interface PatchNoteData { + [key: string]: unknown + slug?: null | string + topicId?: null | string +} + +export function getNotes(params: GetNotesParams = {}) { + return getJson>('/notes', { + page: params.page, + size: params.size, + sort_by: params.sort_by, + sort_order: params.sort_order, + topicId: params.topicId ?? undefined, + }) +} + +export function getNoteById(id: string, params?: { single?: boolean }) { + return getJson(`/notes/${id}`, { + single: params?.single ? 'true' : undefined, + }) +} + +export function createNote(data: CreateNoteData) { + return postJson('/notes', data) +} + +export function updateNote(id: string, data: Partial) { + return putJson>(`/notes/${id}`, data) +} + +export function patchNote(id: string, data: PatchNoteData) { + return patchJson(`/notes/${id}`, data) +} + +export function patchNotePublish(id: string, isPublished: boolean) { + return patchJson( + `/notes/${id}/publish`, + { + isPublished, + }, + ) +} + +export function deleteNote(id: string) { + return deleteJson(`/notes/${id}`) +} diff --git a/apps/admin/src/app/api/options.ts b/apps/admin/src/app/api/options.ts new file mode 100644 index 000000000..d98cb69ad --- /dev/null +++ b/apps/admin/src/app/api/options.ts @@ -0,0 +1,72 @@ +import type { UserModel } from '~/app/models/user' + +import { deleteJson, getJson, patchJson, putJson } from './http' + +export interface SystemOptions { + [key: string]: unknown +} + +export interface UrlOptions { + adminUrl: string + serverUrl: string + webUrl: string + wsUrl: string +} + +export interface EmailTemplateResponse { + props: unknown + template: string +} + +export interface UpdateOwnerData { + avatar?: string + introduce?: string + mail?: string + name?: string + socialIds?: Record + url?: string + username?: string +} + +export function getAllOptions() { + return getJson('/options') +} + +export function getOption(key: string) { + return getJson(`/options/${key}`) +} + +export function getUrlOptions() { + return getJson('/options/url') +} + +export function patchOption(key: string, data: unknown) { + return patchJson(`/options/${key}`, data) +} + +export function getEmailTemplate(type: string) { + return getJson('/options/email/template', { type }) +} + +export function updateEmailTemplate(type: string, source: string) { + return putJson( + `/options/email/template?type=${encodeURIComponent(type)}`, + { + source, + }, + ) +} + +export function deleteEmailTemplate(type: string) { + return deleteJson( + `/options/email/template?type=${encodeURIComponent(type)}`, + ) +} + +export function getOwner() { + return getJson('/owner') +} + +export function updateOwner(data: UpdateOwnerData) { + return patchJson('/owner', data) +} diff --git a/apps/admin/src/app/api/pages.ts b/apps/admin/src/app/api/pages.ts new file mode 100644 index 000000000..9442c45e8 --- /dev/null +++ b/apps/admin/src/app/api/pages.ts @@ -0,0 +1,51 @@ +import type { PaginateResult } from '~/app/models/base' +import type { PageModel } from '~/app/models/page' + +import { deleteJson, getJson, patchJson, postJson, putJson } from './http' + +export interface GetPagesParams { + page?: number + size?: number +} + +export interface CreatePageData { + content?: string + contentFormat?: 'lexical' | 'markdown' + draftId?: string + meta?: Record + order?: number + slug: string + subtitle?: string + text: string + title: string +} + +export function getPages(params: GetPagesParams = {}) { + return getJson>('/pages', { + page: params.page, + size: params.size, + }) +} + +export function getPageById(id: string) { + return getJson(`/pages/${id}`) +} + +export function createPage(data: CreatePageData) { + return postJson('/pages', data) +} + +export function updatePage(id: string, data: Partial) { + return putJson>(`/pages/${id}`, data) +} + +export function deletePage(id: string) { + return deleteJson(`/pages/${id}`) +} + +export function reorderPages(seq: Array<{ id: string; order: number }>) { + return patchJson }>( + '/pages/reorder', + { seq }, + ) +} diff --git a/apps/admin/src/app/api/posts.ts b/apps/admin/src/app/api/posts.ts new file mode 100644 index 000000000..047692dec --- /dev/null +++ b/apps/admin/src/app/api/posts.ts @@ -0,0 +1,76 @@ +import type { PaginateResult } from '~/app/models/base' +import type { PostModel } from '~/app/models/post' + +import { deleteJson, getJson, patchJson, postJson, putJson } from './http' + +export type PostSortKey = 'createdAt' | 'modifiedAt' | 'pinAt' +export type PostSortOrder = 'asc' | 'desc' + +export interface GetPostsParams { + categoryIds?: string[] + page: number + size: number + sort_by?: PostSortKey + sort_order?: PostSortOrder +} + +export interface SearchPostsParams { + keyword: string + page: number + size: number +} + +export interface CreatePostData { + categoryId: string + content?: string + contentFormat?: 'lexical' | 'markdown' + copyright?: boolean + draftId?: string + isPublished?: boolean + meta?: Record + pinOrder?: null | number + relatedId?: string[] + slug?: string + summary?: null | string + tags?: string[] + text: string + title: string +} + +export function getPosts(params: GetPostsParams) { + return getJson>('/posts', { + categoryIds: params.categoryIds, + page: params.page, + size: params.size, + sort_by: params.sort_by, + sort_order: params.sort_order, + }) +} + +export function searchPosts(params: SearchPostsParams) { + return getJson>('/search/post', { + keyword: params.keyword, + page: params.page, + size: params.size, + }) +} + +export function getPostById(id: string) { + return getJson(`/posts/${id}`) +} + +export function createPost(data: CreatePostData) { + return postJson('/posts', data) +} + +export function updatePost(id: string, data: Partial) { + return putJson>(`/posts/${id}`, data) +} + +export function patchPost(id: string, data: Partial) { + return patchJson>(`/posts/${id}`, data) +} + +export function deletePost(id: string) { + return deleteJson(`/posts/${id}`) +} diff --git a/apps/admin/src/app/api/projects.ts b/apps/admin/src/app/api/projects.ts new file mode 100644 index 000000000..afb820771 --- /dev/null +++ b/apps/admin/src/app/api/projects.ts @@ -0,0 +1,34 @@ +import type { ProjectModel, ProjectResponse } from '~/app/models/project' + +import { deleteJson, getJson, postJson, putJson } from './http' + +export interface ProjectInput { + avatar?: string + description: string + docUrl?: string + images?: string[] + name: string + previewUrl?: string + projectUrl?: string + text: string +} + +export function getProjects(params: { page: number; size: number }) { + return getJson('/projects', params) +} + +export function getProject(id: string) { + return getJson(`/projects/${id}`) +} + +export function createProject(data: ProjectInput) { + return postJson('/projects', data) +} + +export function updateProject(id: string, data: Partial) { + return putJson>(`/projects/${id}`, data) +} + +export function deleteProject(id: string) { + return deleteJson(`/projects/${id}`) +} diff --git a/apps/admin/src/app/api/readers.ts b/apps/admin/src/app/api/readers.ts new file mode 100644 index 000000000..6fc67bbe8 --- /dev/null +++ b/apps/admin/src/app/api/readers.ts @@ -0,0 +1,18 @@ +import type { PaginateResult } from '~/app/models/base' + +import { getJson } from './http' + +export interface ReaderModel { + id: string + provider?: string + type?: string + name: string + email: string + image: string + handle?: string + role: 'reader' | 'owner' +} + +export function getReaders(params: { page: number; size: number }) { + return getJson>('/readers', params) +} diff --git a/apps/admin/src/app/api/recently.ts b/apps/admin/src/app/api/recently.ts new file mode 100644 index 000000000..7d75e050b --- /dev/null +++ b/apps/admin/src/app/api/recently.ts @@ -0,0 +1,23 @@ +import type { RecentlyModel } from '~/app/models/recently' + +import { deleteJson, getJson, postJson, putJson } from './http' + +export interface RecentlyInput { + content: string +} + +export function getRecentlyList() { + return getJson('/recently/all') +} + +export function createRecently(data: RecentlyInput) { + return postJson('/recently', data) +} + +export function updateRecently(id: string, data: RecentlyInput) { + return putJson(`/recently/${id}`, data) +} + +export function deleteRecently(id: string) { + return deleteJson(`/recently/${id}`) +} diff --git a/apps/admin/src/app/api/says.ts b/apps/admin/src/app/api/says.ts new file mode 100644 index 000000000..d35faedfe --- /dev/null +++ b/apps/admin/src/app/api/says.ts @@ -0,0 +1,25 @@ +import type { SayModel, SayResponse } from '~/app/models/say' + +import { deleteJson, getJson, postJson, putJson } from './http' + +export interface SayInput { + author?: string + source?: string + text: string +} + +export function getSays(params: { page: number; size: number }) { + return getJson('/says', params) +} + +export function createSay(data: SayInput) { + return postJson('/says', data) +} + +export function updateSay(id: string, data: Partial) { + return putJson>(`/says/${id}`, data) +} + +export function deleteSay(id: string) { + return deleteJson(`/says/${id}`) +} diff --git a/apps/admin/src/app/api/search-index.ts b/apps/admin/src/app/api/search-index.ts new file mode 100644 index 000000000..4037d680a --- /dev/null +++ b/apps/admin/src/app/api/search-index.ts @@ -0,0 +1,83 @@ +import { getJson, requestJson } from './http' + +export interface SearchIndexLegacyPager { + currentPage: number + hasNextPage: boolean + hasPrevPage: boolean + size: number + total: number + totalPage: number +} + +export type SearchIndexRefType = 'note' | 'page' | 'post' + +export interface SearchIndexRebuildResult { + created: number + deleted: number + skipped: number + total: number + updated: number +} + +export interface SearchIndexRebuildOneResult { + rebuilt: number +} + +export interface SearchDocumentAdminRow { + bodyLength: number + createdAt: string + hasPassword: boolean + id: string + isPublished: boolean + lang: string + modifiedAt: string + publicAt: string | null + refId: string + refType: SearchIndexRefType | string + sourceHash: string + title: string + titleLength: number +} + +export interface SearchDocumentAdminListResponse { + data: SearchDocumentAdminRow[] + pagination: SearchIndexLegacyPager +} + +export interface SearchDocumentAdminListParams { + keyword?: string + lang?: string + page?: number + refType?: SearchIndexRefType | string + size?: number +} + +export function rebuildSearchIndex(force = false) { + return requestJson( + force ? '/search/rebuild?force=true' : '/search/rebuild', + { + method: 'POST', + }, + ) +} + +export function rebuildSearchIndexDocument(refType: string, refId: string) { + return requestJson( + `/search/rebuild/${encodeURIComponent(refType)}/${encodeURIComponent(refId)}`, + { + method: 'POST', + }, + ) +} + +export function getSearchIndexDocuments( + params: SearchDocumentAdminListParams = {}, +) { + return getJson('/search/admin/documents', { + keyword: params.keyword, + lang: params.lang, + page: params.page, + refType: params.refType, + size: params.size, + }) +} diff --git a/apps/admin/src/app/api/snippets.ts b/apps/admin/src/app/api/snippets.ts new file mode 100644 index 000000000..6bc8d9550 --- /dev/null +++ b/apps/admin/src/app/api/snippets.ts @@ -0,0 +1,85 @@ +import type { PaginateResult } from '~/app/models/base' +import type { SnippetModel, SnippetType } from '~/app/models/snippet' + +import { deleteJson, getJson, postJson, putJson } from './http' + +export interface GetSnippetsParams { + page?: number + reference?: string + size?: number + type?: SnippetType +} + +export interface CreateSnippetData { + comment?: string + customPath?: string + enable?: boolean + metatype?: string + method?: string + name: string + private?: boolean + raw: string + reference?: string + schema?: string + secret?: Record + type: SnippetType +} + +export interface SnippetGroup { + count: number + reference: string +} + +export interface ImportSnippetsData { + packages?: string[] + snippets: SnippetModel[] +} + +export function getSnippets(params: GetSnippetsParams = {}) { + return getJson>('/snippets', { + page: params.page, + reference: params.reference, + size: params.size, + type: params.type, + }) +} + +export function getSnippetById(id: string) { + return getJson(`/snippets/${id}`) +} + +export function createSnippet(data: CreateSnippetData) { + return postJson('/snippets', data) +} + +export function updateSnippet(id: string, data: Partial) { + return putJson>( + `/snippets/${id}`, + data, + ) +} + +export function deleteSnippet(id: string) { + return deleteJson(`/snippets/${id}`) +} + +export function getSnippetGroups(params?: { page?: number; size?: number }) { + return getJson>('/snippets/group', { + page: params?.page, + size: params?.size, + }) +} + +export function getGroupSnippets(reference: string) { + return getJson( + `/snippets/group/${encodeURIComponent(reference)}`, + ) +} + +export function resetFunctionSnippet(id: string) { + return deleteJson(`/fn/reset/${id}`) +} + +export function importSnippets(data: ImportSnippetsData) { + return postJson('/snippets/import', data) +} diff --git a/apps/admin/src/app/api/subscribe.ts b/apps/admin/src/app/api/subscribe.ts new file mode 100644 index 000000000..d8ef6a9b7 --- /dev/null +++ b/apps/admin/src/app/api/subscribe.ts @@ -0,0 +1,51 @@ +import { deleteJson, getJson, patchJson } from './http' + +export const SubscribePostCreateBit = 1 +export const SubscribeNoteCreateBit = 2 +export const SubscribeSayCreateBit = 4 +export const SubscribeRecentCreateBit = 8 + +export interface Subscriber { + cancelToken: string + createdAt: string + email: string + id: string + subscribe: number + verified: boolean +} + +export interface SubscribeResponse { + data: Subscriber[] + pagination: { + currentPage: number + hasNextPage: boolean + hasPrevPage: boolean + size: number + total: number + totalPage: number + } +} + +export function getSubscribeStatus() { + return getJson<{ enable: boolean }>('/subscribe/status') +} + +export function getSubscribers(params: { page: number; size: number }) { + return getJson('/subscribe', { + page: params.page, + size: params.size, + }) +} + +export function updateSubscribeEnabled(enabled: boolean) { + return patchJson('/options/featureList', { + emailSubscribe: enabled, + }) +} + +export function unsubscribeBatch(params: { all: true } | { emails: string[] }) { + return deleteJson<{ deletedCount: number }, typeof params>( + '/subscribe/unsubscribe/batch', + params, + ) +} diff --git a/apps/admin/src/app/api/system.ts b/apps/admin/src/app/api/system.ts new file mode 100644 index 000000000..c54dea6f2 --- /dev/null +++ b/apps/admin/src/app/api/system.ts @@ -0,0 +1,63 @@ +import { API_URL } from '~/app/constants/env' + +import { getJson, patchJson, postJson, requestJson } from './http' + +export interface CreateOwnerData { + avatar?: string + introduce?: string + mail: string + name?: string + password: string + url?: string + username: string +} + +export interface InitDefaultConfigs { + seo?: { + description?: string + keywords?: string[] + title?: string + } +} + +export async function checkInit() { + try { + const response = await fetch(`${API_URL}/init`, { + credentials: 'include', + headers: { + 'x-skip-translation': '1', + }, + }) + + if (response.status === 404 || response.status === 403) { + return { isInit: true } + } + + if (!response.ok) + throw new Error(response.statusText || 'Init check failed') + + return (await response.json()) as { isInit: boolean } + } catch (error) { + if (error instanceof Error) throw error + throw new Error('Init check failed') + } +} + +export function getInitDefaultConfigs() { + return getJson('/init/configs/default') +} + +export function patchInitConfig(key: string, data: TData) { + return patchJson(`/init/configs/${key}`, data) +} + +export function restoreFromBackup(formData: FormData) { + return requestJson('/init/restore', { + body: formData, + method: 'POST', + }) +} + +export function createOwner(data: CreateOwnerData) { + return postJson('/init/owner', data) +} diff --git a/apps/admin/src/app/api/topics.ts b/apps/admin/src/app/api/topics.ts new file mode 100644 index 000000000..f11b1b298 --- /dev/null +++ b/apps/admin/src/app/api/topics.ts @@ -0,0 +1,60 @@ +import type { PaginateResult } from '~/app/models/base' +import type { NoteModel } from '~/app/models/note' +import type { TopicModel } from '~/app/models/topic' + +import { deleteJson, getJson, patchJson, postJson, putJson } from './http' + +export interface GetTopicsParams { + page?: number + size?: number +} + +export interface CreateTopicData { + description?: string + icon?: string + introduce: string + name: string + slug: string +} + +export type UpdateTopicData = Partial + +export function getTopics(params: GetTopicsParams = {}) { + return getJson>('/topics', { + page: params.page, + size: params.size, + }) +} + +export function getTopic(id: string) { + return getJson(`/topics/${id}`) +} + +export function createTopic(data: CreateTopicData) { + return postJson('/topics', data) +} + +export function updateTopic(id: string, data: UpdateTopicData) { + return putJson(`/topics/${id}`, data) +} + +export function patchTopic(id: string, data: Partial) { + return patchJson>(`/topics/${id}`, data) +} + +export function deleteTopic(id: string) { + return deleteJson(`/topics/${id}`) +} + +export function getNotesByTopic( + topicId: string, + params: { page?: number; size?: number } = {}, +) { + return getJson>>( + `/notes/topics/${topicId}`, + { + page: params.page, + size: params.size, + }, + ) +} diff --git a/apps/admin/src/app/api/webhooks.ts b/apps/admin/src/app/api/webhooks.ts new file mode 100644 index 000000000..63898575b --- /dev/null +++ b/apps/admin/src/app/api/webhooks.ts @@ -0,0 +1,84 @@ +import type { PaginateResult } from '~/app/models/base' + +import { deleteJson, getJson, patchJson, postJson } from './http' + +export interface WebhookModel { + created: string + enabled: boolean + events: string[] + id: string + payloadUrl: string + scope: number + secret?: string + updated: string + url: string +} + +export interface WebhookInput { + enabled?: boolean + events: string[] + payloadUrl?: string + scope?: number + secret?: string + url?: string +} + +export interface WebhookEventRecord { + event: string + headers: Record + hookId: string + id: string + payload: unknown + response: unknown + status: number + success: boolean + timestamp: string +} + +export const EventScope = { + ALL: (1 << 0) | (1 << 1) | (1 << 2), + TO_ADMIN: 1 << 1, + TO_SYSTEM: 1 << 2, + TO_VISITOR: 1 << 0, +} as const + +export function getWebhooks() { + return getJson('/webhooks') +} + +export function getWebhookEvents() { + return getJson('/webhooks/events') +} + +export function createWebhook(data: WebhookInput) { + return postJson('/webhooks', data) +} + +export function updateWebhook(id: string, data: Partial) { + return patchJson>(`/webhooks/${id}`, data) +} + +export function deleteWebhook(id: string) { + return deleteJson(`/webhooks/${id}`) +} + +export function testWebhook(id: string, event: string) { + return postJson(`/webhooks/${id}/test`, { event }) +} + +export function getWebhookDispatches( + id: string, + params: { page: number; size: number }, +) { + return getJson>(`/webhooks/${id}`, { + page: params.page, + size: params.size, + }) +} + +export function redispatchWebhook(hookId: string, eventId: string) { + return postJson>( + `/webhooks/${hookId}/redispatch/${eventId}`, + {}, + ) +} diff --git a/apps/admin/src/constants/env.ts b/apps/admin/src/app/constants/env.ts similarity index 100% rename from apps/admin/src/constants/env.ts rename to apps/admin/src/app/constants/env.ts diff --git a/apps/admin/src/constants/keys.ts b/apps/admin/src/app/constants/keys.ts similarity index 100% rename from apps/admin/src/constants/keys.ts rename to apps/admin/src/app/constants/keys.ts diff --git a/apps/admin/src/app/hooks/use-local-storage-state.ts b/apps/admin/src/app/hooks/use-local-storage-state.ts new file mode 100644 index 000000000..c618f57d6 --- /dev/null +++ b/apps/admin/src/app/hooks/use-local-storage-state.ts @@ -0,0 +1,22 @@ +import { useState } from 'react' + +export function useLocalStorageState(key: string, initialValue: T) { + const [state, setState] = useState(() => { + const rawValue = window.localStorage.getItem(key) + + if (!rawValue) return initialValue + + try { + return JSON.parse(rawValue) as T + } catch { + return rawValue as T + } + }) + + const setStoredState = (nextState: T) => { + setState(nextState) + window.localStorage.setItem(key, JSON.stringify(nextState)) + } + + return [state, setStoredState] as const +} diff --git a/apps/admin/src/models/activity.ts b/apps/admin/src/app/models/activity.ts similarity index 100% rename from apps/admin/src/models/activity.ts rename to apps/admin/src/app/models/activity.ts diff --git a/apps/admin/src/models/ai.ts b/apps/admin/src/app/models/ai.ts similarity index 100% rename from apps/admin/src/models/ai.ts rename to apps/admin/src/app/models/ai.ts diff --git a/apps/admin/src/models/amap.ts b/apps/admin/src/app/models/amap.ts similarity index 100% rename from apps/admin/src/models/amap.ts rename to apps/admin/src/app/models/amap.ts diff --git a/apps/admin/src/models/analyze.ts b/apps/admin/src/app/models/analyze.ts similarity index 100% rename from apps/admin/src/models/analyze.ts rename to apps/admin/src/app/models/analyze.ts diff --git a/apps/admin/src/models/base.ts b/apps/admin/src/app/models/base.ts similarity index 100% rename from apps/admin/src/models/base.ts rename to apps/admin/src/app/models/base.ts diff --git a/apps/admin/src/models/category.ts b/apps/admin/src/app/models/category.ts similarity index 100% rename from apps/admin/src/models/category.ts rename to apps/admin/src/app/models/category.ts diff --git a/apps/admin/src/models/comment.ts b/apps/admin/src/app/models/comment.ts similarity index 100% rename from apps/admin/src/models/comment.ts rename to apps/admin/src/app/models/comment.ts diff --git a/apps/admin/src/models/draft.ts b/apps/admin/src/app/models/draft.ts similarity index 100% rename from apps/admin/src/models/draft.ts rename to apps/admin/src/app/models/draft.ts diff --git a/apps/admin/src/models/enrichment.ts b/apps/admin/src/app/models/enrichment.ts similarity index 100% rename from apps/admin/src/models/enrichment.ts rename to apps/admin/src/app/models/enrichment.ts diff --git a/apps/admin/src/models/link.ts b/apps/admin/src/app/models/link.ts similarity index 100% rename from apps/admin/src/models/link.ts rename to apps/admin/src/app/models/link.ts diff --git a/apps/admin/src/models/meta-preset.ts b/apps/admin/src/app/models/meta-preset.ts similarity index 100% rename from apps/admin/src/models/meta-preset.ts rename to apps/admin/src/app/models/meta-preset.ts diff --git a/apps/admin/src/models/note.ts b/apps/admin/src/app/models/note.ts similarity index 100% rename from apps/admin/src/models/note.ts rename to apps/admin/src/app/models/note.ts diff --git a/apps/admin/src/models/options.ts b/apps/admin/src/app/models/options.ts similarity index 100% rename from apps/admin/src/models/options.ts rename to apps/admin/src/app/models/options.ts diff --git a/apps/admin/src/models/page.ts b/apps/admin/src/app/models/page.ts similarity index 100% rename from apps/admin/src/models/page.ts rename to apps/admin/src/app/models/page.ts diff --git a/apps/admin/src/models/post.ts b/apps/admin/src/app/models/post.ts similarity index 100% rename from apps/admin/src/models/post.ts rename to apps/admin/src/app/models/post.ts diff --git a/apps/admin/src/models/project.ts b/apps/admin/src/app/models/project.ts similarity index 100% rename from apps/admin/src/models/project.ts rename to apps/admin/src/app/models/project.ts diff --git a/apps/admin/src/models/recently.ts b/apps/admin/src/app/models/recently.ts similarity index 100% rename from apps/admin/src/models/recently.ts rename to apps/admin/src/app/models/recently.ts diff --git a/apps/admin/src/models/say.ts b/apps/admin/src/app/models/say.ts similarity index 100% rename from apps/admin/src/models/say.ts rename to apps/admin/src/app/models/say.ts diff --git a/apps/admin/src/models/search-index.ts b/apps/admin/src/app/models/search-index.ts similarity index 100% rename from apps/admin/src/models/search-index.ts rename to apps/admin/src/app/models/search-index.ts diff --git a/apps/admin/src/models/snippet.ts b/apps/admin/src/app/models/snippet.ts similarity index 100% rename from apps/admin/src/models/snippet.ts rename to apps/admin/src/app/models/snippet.ts diff --git a/apps/admin/src/models/stat.ts b/apps/admin/src/app/models/stat.ts similarity index 100% rename from apps/admin/src/models/stat.ts rename to apps/admin/src/app/models/stat.ts diff --git a/apps/admin/src/models/system.ts b/apps/admin/src/app/models/system.ts similarity index 100% rename from apps/admin/src/models/system.ts rename to apps/admin/src/app/models/system.ts diff --git a/apps/admin/src/models/token.ts b/apps/admin/src/app/models/token.ts similarity index 100% rename from apps/admin/src/models/token.ts rename to apps/admin/src/app/models/token.ts diff --git a/apps/admin/src/models/topic.ts b/apps/admin/src/app/models/topic.ts similarity index 100% rename from apps/admin/src/models/topic.ts rename to apps/admin/src/app/models/topic.ts diff --git a/apps/admin/src/models/user.ts b/apps/admin/src/app/models/user.ts similarity index 100% rename from apps/admin/src/models/user.ts rename to apps/admin/src/app/models/user.ts diff --git a/apps/admin/src/app/providers.tsx b/apps/admin/src/app/providers.tsx new file mode 100644 index 000000000..fce8169f5 --- /dev/null +++ b/apps/admin/src/app/providers.tsx @@ -0,0 +1,32 @@ +import { QueryClientProvider } from '@tanstack/react-query' +import { Toaster } from 'sonner' +import type { PropsWithChildren } from 'react' + +import { queryClient } from './query-client' +import { useThemeMode } from './theme' + +export function AppProviders(props: PropsWithChildren) { + const { isDark } = useThemeMode() + + return ( + + {props.children} + + + ) +} diff --git a/apps/admin/src/app/query-client.ts b/apps/admin/src/app/query-client.ts new file mode 100644 index 000000000..5a21aad46 --- /dev/null +++ b/apps/admin/src/app/query-client.ts @@ -0,0 +1,11 @@ +import { QueryClient } from '@tanstack/react-query' + +export const queryClient = new QueryClient({ + defaultOptions: { + queries: { + refetchOnWindowFocus: false, + retry: 1, + staleTime: 1000 * 30, + }, + }, +}) diff --git a/apps/admin/src/app/routes.tsx b/apps/admin/src/app/routes.tsx new file mode 100644 index 000000000..62bff7abe --- /dev/null +++ b/apps/admin/src/app/routes.tsx @@ -0,0 +1,381 @@ +import { + BellOff, + BellRing, + BookOpen, + ChartLine, + Clock, + DatabaseZap, + File, + FileClock, + FileCode2, + FileDown, + Files, + FileText, + Folder, + FolderOpen, + Gauge, + Hash, + Image, + KeyRound, + ListTodo, + MessageSquare, + Pencil, + Quote, + RadioTower, + SearchCheck, + Settings, + Sparkles, + SquareFunction, + Terminal, + Undo2, + UserRound, + Users, + Webhook, +} from 'lucide-react' +import { Navigate, Route, Routes } from 'react-router' +import type { LucideIcon } from 'lucide-react' +import type { ComponentType } from 'react' + +import { AiPage } from './views/ai-page' +import { AnalyzePage } from './views/analyze-page' +import { AuthnDebugPage } from './views/authn-debug-page' +import { BackupPage } from './views/backup-page' +import { CategoriesPage } from './views/categories-page' +import { CommentsPage } from './views/comments-page' +import { CronPage } from './views/cron-page' +import { DashboardPage } from './views/dashboard-page' +import { DraftsPage } from './views/drafts-page' +import { EnrichmentPage } from './views/enrichment-page' +import { EventsDebugPage } from './views/events-debug-page' +import { + CommentImagesPage, + FilesPage, + OrphanFilesPage, +} from './views/files-page' +import { FriendsPage } from './views/friends-page' +import { LoginPage } from './views/login-page' +import { MarkdownPage } from './views/markdown-page' +import { NotesPage } from './views/notes-page' +import { PagesPage } from './views/pages-page' +import { PostsPage } from './views/posts-page' +import { ProjectsPage } from './views/projects-page' +import { ReadersPage } from './views/readers-page' +import { RecentlyPage } from './views/recently-page' +import { SaysPage } from './views/says-page' +import { SearchIndexPage } from './views/search-index-page' +import { ServerlessDebugPage } from './views/serverless-debug-page' +import { SettingsPage } from './views/settings-page' +import { SetupApiPage } from './views/setup-api-page' +import { SetupPage } from './views/setup-page' +import { SnippetsPage } from './views/snippets-page' +import { SubscribePage } from './views/subscribe-page' +import { TemplatePage } from './views/template-page' +import { ToastDebugPage } from './views/toast-debug-page' +import { TopicsPage } from './views/topics-page' +import { WebhooksPage } from './views/webhooks-page' +import { NoteWritePage, PageWritePage, PostWritePage } from './views/write-page' + +export interface AppRoute { + description: string + element: ComponentType + icon: LucideIcon + path: string + title: string +} + +export const appRoutes: AppRoute[] = [ + { + description: 'Runtime status and environment data.', + element: DashboardPage, + icon: Gauge, + path: '/dashboard', + title: 'Dashboard', + }, + { + description: 'Posts, publishing state, search, and article operations.', + element: PostsPage, + icon: FileText, + path: '/posts', + title: 'Posts', + }, + { + description: 'Create and edit posts in the React markdown writing surface.', + element: PostWritePage, + icon: Pencil, + path: '/posts/edit', + title: 'Write Post', + }, + { + description: 'Post categories, tags, and their associated articles.', + element: CategoriesPage, + icon: FolderOpen, + path: '/posts/category', + title: 'Categories', + }, + { + description: 'Notes, publication state, and public note links.', + element: NotesPage, + icon: BookOpen, + path: '/notes', + title: 'Notes', + }, + { + description: 'Create and edit notes in the React markdown writing surface.', + element: NoteWritePage, + icon: Pencil, + path: '/notes/edit', + title: 'Write Note', + }, + { + description: 'Note topics, metadata, and associated note references.', + element: TopicsPage, + icon: Hash, + path: '/notes/topic', + title: 'Topics', + }, + { + description: 'Static pages, ordering metadata, and public page links.', + element: PagesPage, + icon: File, + path: '/pages', + title: 'Pages', + }, + { + description: 'Create and edit static pages in the React writing surface.', + element: PageWritePage, + icon: Pencil, + path: '/pages/edit', + title: 'Write Page', + }, + { + description: 'Autosaved drafts, versions, and content recovery.', + element: DraftsPage, + icon: FileClock, + path: '/drafts', + title: 'Drafts', + }, + { + description: 'Comments, moderation, and reader-facing feedback.', + element: CommentsPage, + icon: MessageSquare, + path: '/comments', + title: 'Comments', + }, + { + description: 'Authenticated readers and provider identities.', + element: ReadersPage, + icon: Users, + path: '/readers', + title: 'Readers', + }, + { + description: 'Short quotes, sayings, and source metadata.', + element: SaysPage, + icon: Quote, + path: '/says', + title: 'Says', + }, + { + description: 'Short-form thoughts, links, and lightweight references.', + element: RecentlyPage, + icon: Clock, + path: '/recently', + title: 'Recently', + }, + { + description: 'Project portfolio entries and publication metadata.', + element: ProjectsPage, + icon: Folder, + path: '/projects', + title: 'Projects', + }, + { + description: 'Friend links, link review, and site health checks.', + element: FriendsPage, + icon: UserRound, + path: '/friends', + title: 'Friends', + }, + { + description: 'Uploaded files, orphan images, and comment image uploads.', + element: FilesPage, + icon: Files, + path: '/files', + title: 'Files', + }, + { + description: 'Uploaded images that are no longer attached to content.', + element: OrphanFilesPage, + icon: Image, + path: '/files/orphans', + title: 'Orphan Images', + }, + { + description: 'Reader comment image uploads and binding status.', + element: CommentImagesPage, + icon: Image, + path: '/files/comment-images', + title: 'Comment Images', + }, + { + description: 'Traffic metrics, paths, IP records, and visitor analysis.', + element: AnalyzePage, + icon: ChartLine, + path: '/analyze', + title: 'Analyze', + }, + { + description: 'AI task surfaces and enrichment workflows.', + element: AiPage, + icon: Sparkles, + path: '/ai', + title: 'AI', + }, + { + description: 'Owner profile, URL data, and system options.', + element: SettingsPage, + icon: Settings, + path: '/setting', + title: 'Settings', + }, + { + description: 'Email subscription status and subscriber management.', + element: SubscribePage, + icon: BellOff, + path: '/extra-features/subscribe', + title: 'Subscribe', + }, + { + description: 'Configuration snippets and serverless function source.', + element: SnippetsPage, + icon: SquareFunction, + path: '/extra-features/snippets', + title: 'Snippets', + }, + { + description: 'Outbound webhook endpoints and dispatch history.', + element: WebhooksPage, + icon: Webhook, + path: '/extra-features/webhooks', + title: 'Webhooks', + }, + { + description: 'Markdown import, parsing preview, and archive export.', + element: MarkdownPage, + icon: FileDown, + path: '/extra-features/markdown', + title: 'Markdown', + }, + { + description: 'Email template source and sample payloads.', + element: TemplatePage, + icon: FileCode2, + path: '/extra-features/assets/template', + title: 'Templates', + }, + { + description: 'Database backup archives and restore operations.', + element: BackupPage, + icon: Undo2, + path: '/maintenance/backup', + title: 'Backups', + }, + { + description: 'Scheduled task definitions, execution status, and logs.', + element: CronPage, + icon: ListTodo, + path: '/maintenance/cron', + title: 'Cron', + }, + { + description: 'Search document rows, rebuild controls, and index metadata.', + element: SearchIndexPage, + icon: SearchCheck, + path: '/maintenance/search-index', + title: 'Search Index', + }, + { + description: 'Cache, screenshots, probes, and derived content assets.', + element: EnrichmentPage, + icon: DatabaseZap, + path: '/enrichment', + title: 'Enrichment', + }, + { + description: 'React Sonner scenarios for status, loading, and actions.', + element: ToastDebugPage, + icon: BellRing, + path: '/debug/toast', + title: 'Toast Lab', + }, + { + description: 'Passkey registration and authentication diagnostics.', + element: AuthnDebugPage, + icon: KeyRound, + path: '/debug/authn', + title: 'Passkey Lab', + }, + { + description: 'Synthetic socket event payloads and dispatch checks.', + element: EventsDebugPage, + icon: RadioTower, + path: '/debug/events', + title: 'Event Lab', + }, + { + description: 'Serverless function execution and response diagnostics.', + element: ServerlessDebugPage, + icon: Terminal, + path: '/debug/serverless', + title: 'Function Lab', + }, +] + +const legacyRouteAliases: Array<{ + element?: ComponentType + from: string + to?: string +}> = [ + { from: '/posts/view', to: '/posts' }, + { from: '/notes/view', to: '/notes' }, + { from: '/pages/list', to: '/pages' }, + { from: '/files/list', to: '/files' }, + { from: '/maintenance/enrichment', to: '/enrichment' }, + { element: AiPage, from: '/ai/summary' }, + { element: AiPage, from: '/ai/insights' }, + { element: AiPage, from: '/ai/translation' }, + { element: AiPage, from: '/ai/translation-entries' }, + { element: AiPage, from: '/ai/tasks' }, + { element: AiPage, from: '/ai/slug-backfill' }, +] + +export function AppRoutes() { + return ( + + } path="/" /> + } path="/login" /> + } path="/setup" /> + } path="/setup-api" /> + {legacyRouteAliases.map((route) => { + if (route.to) { + return ( + } + key={route.from} + path={route.from} + /> + ) + } + + const Element = route.element + return Element ? ( + } key={route.from} path={route.from} /> + ) : null + })} + {appRoutes.map((route) => ( + } key={route.path} path={route.path} /> + ))} + } path="*" /> + + ) +} diff --git a/apps/admin/src/app/shell.tsx b/apps/admin/src/app/shell.tsx new file mode 100644 index 000000000..552f9406b --- /dev/null +++ b/apps/admin/src/app/shell.tsx @@ -0,0 +1,74 @@ +import { NavLink, useLocation } from 'react-router' +import type { PropsWithChildren } from 'react' + +import { appRoutes } from './routes' + +const activeLinkClassName = + 'bg-neutral-950 text-white shadow-sm dark:bg-neutral-50 dark:text-neutral-950' +const inactiveLinkClassName = + 'text-neutral-600 hover:bg-neutral-100 hover:text-neutral-950 dark:text-neutral-300 dark:hover:bg-neutral-900 dark:hover:text-neutral-50' + +export function AdminShell(props: PropsWithChildren) { + const location = useLocation() + const activeRoute = + [...appRoutes] + .sort((a, b) => b.path.length - a.path.length) + .find((route) => location.pathname.startsWith(route.path)) ?? appRoutes[0] + + return ( +
+ + +
+
+
+

{activeRoute.title}

+

+ {activeRoute.description} +

+
+ + React + +
+ +
{props.children}
+
+
+ ) +} diff --git a/apps/admin/src/socket/types.ts b/apps/admin/src/app/socket/types.ts similarity index 80% rename from apps/admin/src/socket/types.ts rename to apps/admin/src/app/socket/types.ts index 803ec331e..0e2f29293 100644 --- a/apps/admin/src/socket/types.ts +++ b/apps/admin/src/app/socket/types.ts @@ -26,13 +26,12 @@ export enum EventTypes { LINK_APPLY = 'LINK_APPLY', DANMAKU_CREATE = 'DANMAKU_CREATE', - // util - CONTENT_REFRESH = 'CONTENT_REFRESH', // 内容更新或重置 页面需要重载 - // for admin + CONTENT_REFRESH = 'CONTENT_REFRESH', + IMAGE_REFRESH = 'IMAGE_REFRESH', IMAGE_FETCH = 'IMAGE_FETCH', ADMIN_NOTIFICATION = 'ADMIN_NOTIFICATION', } -export type NotificationTypes = 'error' | 'warn' | 'success' | 'info' +export type NotificationTypes = 'error' | 'info' | 'success' | 'warn' diff --git a/apps/admin/src/app/theme.ts b/apps/admin/src/app/theme.ts new file mode 100644 index 000000000..97e79807b --- /dev/null +++ b/apps/admin/src/app/theme.ts @@ -0,0 +1,62 @@ +import { useEffect, useMemo, useSyncExternalStore } from 'react' + +export const themeColors = { + primary: '#1a9cf3', + primaryDeep: '#0f7ec4', + primaryShallow: '#4fb5f7', +} as const + +export type ThemeMode = 'dark' | 'light' | 'system' + +export function useThemeMode() { + const query = useMemo( + () => window.matchMedia('(prefers-color-scheme: dark)'), + [], + ) + + const isDark = useSyncExternalStore( + (onStoreChange) => { + query.addEventListener('change', onStoreChange) + + return () => query.removeEventListener('change', onStoreChange) + }, + () => { + const storedTheme = readThemeMode() + + if (storedTheme === 'dark') return true + if (storedTheme === 'light') return false + + return query.matches + }, + () => false, + ) + + useEffect(() => { + document.documentElement.classList.toggle('dark', isDark) + }, [isDark]) + + return { isDark } +} + +export function installThemeTokens() { + document.documentElement.style.setProperty( + '--color-primary', + themeColors.primary, + ) + document.documentElement.style.setProperty( + '--color-primary-shallow', + themeColors.primaryShallow, + ) + document.documentElement.style.setProperty( + '--color-primary-deep', + themeColors.primaryDeep, + ) +} + +function readThemeMode(): ThemeMode { + const storedTheme = localStorage.getItem('theme-mode')?.replace(/^"|"$/g, '') + + if (storedTheme === 'dark' || storedTheme === 'light') return storedTheme + + return 'system' +} diff --git a/apps/admin/src/app/ui/button.tsx b/apps/admin/src/app/ui/button.tsx new file mode 100644 index 000000000..2b2904f6d --- /dev/null +++ b/apps/admin/src/app/ui/button.tsx @@ -0,0 +1,54 @@ +import { Button as BaseButton } from '@base-ui/react/button' +import { Link } from 'react-router' +import type { ButtonProps as BaseButtonProps } from '@base-ui/react/button' +import type { LinkProps } from 'react-router' + +import { cn } from './cn' + +type ButtonVariant = 'primary' | 'subtle' + +export interface ButtonProps extends Omit { + className?: string + variant?: ButtonVariant +} + +export interface ButtonLinkProps extends Omit { + className?: string + variant?: ButtonVariant +} + +const variantClassNames: Record = { + primary: + 'bg-neutral-950 text-white hover:bg-neutral-800 dark:bg-neutral-50 dark:text-neutral-950 dark:hover:bg-neutral-200', + subtle: + 'border border-neutral-200 bg-white text-neutral-700 hover:bg-neutral-50 dark:border-neutral-800 dark:bg-neutral-950 dark:text-neutral-200 dark:hover:bg-neutral-900', +} + +const buttonClassName = + 'inline-flex h-9 items-center justify-center gap-2 rounded px-3 text-sm font-medium outline-none transition-colors focus-visible:ring-2 focus-visible:ring-[var(--color-primary-shallow)] disabled:pointer-events-none disabled:opacity-50' + +export function Button({ + className, + variant = 'primary', + ...props +}: ButtonProps) { + return ( + + ) +} + +export function ButtonLink({ + className, + variant = 'primary', + ...props +}: ButtonLinkProps) { + return ( + + ) +} diff --git a/apps/admin/src/app/ui/checkbox.tsx b/apps/admin/src/app/ui/checkbox.tsx new file mode 100644 index 000000000..9f4573113 --- /dev/null +++ b/apps/admin/src/app/ui/checkbox.tsx @@ -0,0 +1,50 @@ +import { Checkbox as BaseCheckbox } from '@base-ui/react/checkbox' +import { Check, Minus } from 'lucide-react' +import type { MouseEventHandler, ReactNode } from 'react' + +import { cn } from './cn' + +interface CheckboxProps { + 'aria-label'?: string + checked: boolean + className?: string + disabled?: boolean + indeterminate?: boolean + label?: ReactNode + onCheckedChange: (checked: boolean) => void + onClick?: MouseEventHandler +} + +export function Checkbox(props: CheckboxProps) { + const control = ( + + + {props.indeterminate ? ( + + + ) + + if (!props.label) return control + + return ( + + ) +} diff --git a/apps/admin/src/app/ui/cn.ts b/apps/admin/src/app/ui/cn.ts new file mode 100644 index 000000000..bcb0f5397 --- /dev/null +++ b/apps/admin/src/app/ui/cn.ts @@ -0,0 +1,5 @@ +type ClassValue = false | null | string | undefined + +export function cn(...values: ClassValue[]) { + return values.filter(Boolean).join(' ') +} diff --git a/apps/admin/src/app/ui/compact-pagination.tsx b/apps/admin/src/app/ui/compact-pagination.tsx new file mode 100644 index 000000000..41be97c1f --- /dev/null +++ b/apps/admin/src/app/ui/compact-pagination.tsx @@ -0,0 +1,64 @@ +import { ChevronLeft, ChevronRight } from 'lucide-react' + +import { SelectField } from './select' + +export interface CompactPaginationProps { + onPageChange: (page: number) => void + onPageSizeChange: (pageSize: number) => void + page: number + pageCount: number + pageSize: number + pageSizes?: number[] +} + +export function CompactPagination(props: CompactPaginationProps) { + const pageSizes = props.pageSizes ?? [10, 20, 50, 100] + const canPrev = props.page > 1 + const canNext = props.page < props.pageCount + + return ( +
+ + + + + {props.page} + + / + {props.pageCount} + + + + + ({ + label: `${size} / 页`, + value: size, + }))} + popupClassName="text-xs" + triggerClassName="ml-1 h-auto w-auto border-0 bg-transparent px-1.5 py-0.5 text-xs tabular-nums hover:bg-neutral-100 dark:bg-transparent dark:hover:bg-neutral-800" + value={props.pageSize} + /> +
+ ) +} diff --git a/apps/admin/src/app/ui/data-table.tsx b/apps/admin/src/app/ui/data-table.tsx new file mode 100644 index 000000000..72ac51722 --- /dev/null +++ b/apps/admin/src/app/ui/data-table.tsx @@ -0,0 +1,56 @@ +import type { ReactNode } from 'react' + +interface DataTableColumn { + key: keyof T + label: string + render?: (value: T[keyof T], row: T) => ReactNode +} + +interface DataTableProps> { + columns: Array> + rows: T[] +} + +export function DataTable>( + props: DataTableProps, +) { + return ( +
+ + + + {props.columns.map((column) => ( + + ))} + + + + {props.rows.map((row, index) => ( + + {props.columns.map((column) => { + const value = row[column.key] + + return ( + + ) + })} + + ))} + +
+ {column.label} +
+ {column.render ? column.render(value, row) : value} +
+
+ ) +} diff --git a/apps/admin/src/app/ui/metric-card.tsx b/apps/admin/src/app/ui/metric-card.tsx new file mode 100644 index 000000000..cbdea5adb --- /dev/null +++ b/apps/admin/src/app/ui/metric-card.tsx @@ -0,0 +1,24 @@ +import type { LucideIcon } from 'lucide-react' + +interface MetricCardProps { + icon: LucideIcon + label: string + value: string +} + +export function MetricCard(props: MetricCardProps) { + const Icon = props.icon + + return ( +
+
+ ) +} diff --git a/apps/admin/src/app/ui/panel.tsx b/apps/admin/src/app/ui/panel.tsx new file mode 100644 index 000000000..55df43b1f --- /dev/null +++ b/apps/admin/src/app/ui/panel.tsx @@ -0,0 +1,30 @@ +import type { PropsWithChildren, ReactNode } from 'react' + +import { cn } from './cn' + +interface PanelProps extends PropsWithChildren { + className?: string + description?: ReactNode + title: ReactNode +} + +export function Panel(props: PanelProps) { + return ( +
+
+

{props.title}

+ {props.description ? ( +

+ {props.description} +

+ ) : null} +
+ {props.children} +
+ ) +} diff --git a/apps/admin/src/app/ui/select.tsx b/apps/admin/src/app/ui/select.tsx new file mode 100644 index 000000000..8ea21150e --- /dev/null +++ b/apps/admin/src/app/ui/select.tsx @@ -0,0 +1,75 @@ +import { Select as BaseSelect } from '@base-ui/react/select' +import { ChevronDown } from 'lucide-react' +import type { ReactNode } from 'react' + +import { cn } from './cn' + +type SelectValue = number | string + +export interface SelectOption { + label: ReactNode + value: TValue +} + +interface SelectFieldProps { + 'aria-label'?: string + className?: string + disabled?: boolean + id?: string + onValueChange: (value: TValue) => void + options: SelectOption[] + popupClassName?: string + triggerClassName?: string + value: TValue +} + +export function SelectField( + props: SelectFieldProps, +) { + return ( + + disabled={props.disabled} + id={props.id} + items={props.options} + onValueChange={(value) => { + if (value !== null) props.onValueChange(value) + }} + value={props.value} + > + + + + + + + {props.options.map((option) => ( + + {option.label} + + ))} + + + + + ) +} diff --git a/apps/admin/src/app/ui/switch.tsx b/apps/admin/src/app/ui/switch.tsx new file mode 100644 index 000000000..94aa624ce --- /dev/null +++ b/apps/admin/src/app/ui/switch.tsx @@ -0,0 +1,44 @@ +import { Switch as BaseSwitch } from '@base-ui/react/switch' +import type { ReactNode } from 'react' + +import { cn } from './cn' + +interface SwitchProps { + checked: boolean + className?: string + description?: ReactNode + disabled?: boolean + label: ReactNode + onCheckedChange: (checked: boolean) => void +} + +export function Switch(props: SwitchProps) { + return ( + + ) +} diff --git a/apps/admin/src/app/ui/text-field.tsx b/apps/admin/src/app/ui/text-field.tsx new file mode 100644 index 000000000..9ad52d240 --- /dev/null +++ b/apps/admin/src/app/ui/text-field.tsx @@ -0,0 +1,147 @@ +import { Field } from '@base-ui/react/field' +import { Input as BaseInput } from '@base-ui/react/input' +import { forwardRef } from 'react' +import type { + ComponentPropsWithoutRef, + FocusEventHandler, + KeyboardEventHandler, + ReactNode, +} from 'react' + +import { cn } from './cn' + +type TextInputType = ComponentPropsWithoutRef<'input'>['type'] + +interface TextInputProps { + autoComplete?: string + autoFocus?: boolean + className?: string + controlClassName?: string + disabled?: boolean + id?: string + inputMode?: ComponentPropsWithoutRef<'input'>['inputMode'] + label?: ReactNode + labelClassName?: string + list?: string + maxLength?: number + name?: string + onBlur?: FocusEventHandler + onChange: (value: string) => void + onKeyDown?: KeyboardEventHandler + placeholder?: string + required?: boolean + spellCheck?: boolean + type?: TextInputType + value: string +} + +export const TextInput = forwardRef( + function TextInput(props, ref) { + const control = ( + + ) + + if (!props.label) { + return ( + + {control} + + ) + } + + return ( + + + {props.label} + {props.required ? * : null} + + {control} + + ) + }, +) + +interface TextAreaProps { + autoFocus?: boolean + className?: string + controlClassName?: string + disabled?: boolean + label?: ReactNode + labelClassName?: string + maxLength?: number + name?: string + onChange: (value: string) => void + onKeyDown?: KeyboardEventHandler + placeholder?: string + required?: boolean + spellCheck?: boolean + value: string +} + +export const TextArea = forwardRef( + function TextArea(props, ref) { + const control = ( +