refactor(plugins): 将插件列表重构为 Element Plus 组件体系 - #14
Conversation
- 使用 el-card/el-table/el-pagination 替代自定义布局 - 新增 PluginDetailDrawer 抽屉组件展示插件详情 - 统一使用 TypeScript <script setup> 语法 - 优化暗色模式自动检测与切换逻辑 - 调整样式,移除冗余的动画与交互效果
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
✅ Deploy Preview for zippy-cactus-2e87a5 ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
✅ Deploy Preview for node-karin ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
💤 Files with no reviewable changes (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughThe plugin browser is rewritten with typed Vue 3 Composition API components and Element Plus controls. Search and pagination use reactive state, plugin cards emit detail events, and a drawer fetches and renders plugin README content with install and homepage actions. ChangesPlugin Browser Redesign
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant SearchBar
participant PluginList
participant PluginCard
participant PluginDetailDrawer
participant GitHubProxy
SearchBar->>PluginList: emit search parameters
PluginList->>PluginList: filter and paginate plugins
PluginList->>PluginCard: render paginated plugin
PluginCard->>PluginList: emit openDetail
PluginList->>PluginDetailDrawer: set plugin and open drawer
PluginDetailDrawer->>GitHubProxy: fetch README through proxy
GitHubProxy-->>PluginDetailDrawer: return README text
PluginDetailDrawer->>PluginDetailDrawer: parse and render Markdown
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 9
🧹 Nitpick comments (11)
docs/.vuepress/theme/components/PluginDetailDrawer.vue (5)
26-26: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
watchis imported but unused.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/.vuepress/theme/components/PluginDetailDrawer.vue` at line 26, Remove the unused watch import from the Vue import declaration in PluginDetailDrawer.vue, while preserving the existing onMounted and ref imports.
50-59: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
navigator.clipboardis undefined on non-secure origins. Thetrywill throw a TypeError and hit the error branch, so behavior degrades gracefully — but drop the redundantconsole.logon success.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/.vuepress/theme/components/PluginDetailDrawer.vue` around lines 50 - 59, Remove the redundant success-path console.log from copyInstallCommand while preserving the clipboard write and success ElMessage behavior; leave the existing catch and error notification unchanged.
94-111: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winNo request timeout and
datamay not be a string.
axios.getwithouttimeoutcan hang the loading state indefinitely if the proxy stalls. Also forceresponseType: 'text'— a README served as JSON would makemarked.parsereceive an object.♻️ Proposed change
- const { data } = await axios.get(proxiedUrl) + const { data } = await axios.get<string>(proxiedUrl, { + timeout: 10000, + responseType: 'text', + transformResponse: [(d) => d] + })🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/.vuepress/theme/components/PluginDetailDrawer.vue` around lines 94 - 111, Update fetchReadme to configure the axios.get request with an explicit finite timeout and responseType set to text before passing data to marked.parse. Preserve the existing loading, error, and cleanup behavior.
132-141: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUnscoped
.el-drawerglobal styles leak to every drawer in the app.header-class/body-classare already unique — target.drawer-header/.drawer-bodydirectly, or scope under a component-specific class passed viaclassonel-drawer.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/.vuepress/theme/components/PluginDetailDrawer.vue` around lines 132 - 141, Update the styles in PluginDetailDrawer so they no longer globally affect every .el-drawer; target the unique drawer-header and drawer-body classes directly, or scope them beneath a component-specific class applied to this el-drawer while preserving the existing margin and padding rules.
121-143: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueThree
<style>blocks with a trailing empty scoped one. Remove line 143; consider merging the global block into the first file-level styles for clarity.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/.vuepress/theme/components/PluginDetailDrawer.vue` around lines 121 - 143, Remove the trailing empty scoped <style> block from PluginDetailDrawer.vue. Leave the existing readme-content and global el-drawer styles unchanged; merging them is optional and not required.docs/.vuepress/theme/components/PluginCard.vue (2)
59-61: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick winAdd
noopenertowindow.open. Without it the opened page gets awindow.openerreference (reverse tabnabbing) on older browsers.const jumpTo = (url: string) => { - window.open(url, '_blank') + window.open(url, '_blank', 'noopener,noreferrer') }Same applies to
jumpToinPluginDetailDrawer.vue.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/.vuepress/theme/components/PluginCard.vue` around lines 59 - 61, Update the jumpTo functions in PluginCard.vue and PluginDetailDrawer.vue to pass noopener in the window.open feature options, preventing the opened page from retaining a window.opener reference while preserving the existing URL and new-tab behavior.
5-9: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winCard header / author / body are click handlers on plain
divs — not keyboard reachable. Considerel-link/buttonsemantics ortabindex+@keyup.enterfor the primary actions.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/.vuepress/theme/components/PluginCard.vue` around lines 5 - 9, The PluginCard header click target around replaceName and jumpTo is a plain div and is not keyboard accessible. Replace it with an appropriate interactive element such as el-link or button, or add keyboard focusability and Enter-key handling while preserving the existing jumpTo(plugin.home) behavior.docs/.vuepress/theme/components/PluginList.vue (3)
30-30: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
showPlugin!non-null assertion is safe only because of thev-if. Prefer gating on the value itself so the assertion isn't needed.- <PluginDetailDrawer v-if="drawerVisible" v-model:drawer-visible="drawerVisible" :plugin="showPlugin!" /> + <PluginDetailDrawer v-if="drawerVisible && showPlugin" v-model:drawer-visible="drawerVisible" :plugin="showPlugin" />🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/.vuepress/theme/components/PluginList.vue` at line 30, Update the PluginDetailDrawer rendering in PluginList to gate directly on showPlugin rather than drawerVisible, allowing the plugin prop to be passed without the non-null assertion. Preserve the drawer visibility behavior through the existing v-model binding.
45-50: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
useToggle(isDark)is a no-op here. It returns a toggle function but never invokes it; the preceding assignment already setsisDark. Drop the branch.♻️ Proposed cleanup
const checkDark = () => { isDark.value = document.documentElement.getAttribute('data-theme') === 'dark' - if (isDark.value) { - useToggle(isDark) - } }Also remove
useTogglefrom the@vueuse/coreimport.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/.vuepress/theme/components/PluginList.vue` around lines 45 - 50, Remove the redundant if branch and useToggle(isDark) call from checkDark, since isDark.value is already assigned directly. Also remove the unused useToggle import from `@vueuse/core`.
98-103: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDebug
console.logand unusederrorMsg.
errorMsgis set but never rendered — load failures show an empty grid with no feedback. Either surface it in the template or drop it, and remove the console log.♻️ Proposed cleanup
} finally { loading.value = false - console.log(allPlugins.value) }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/.vuepress/theme/components/PluginList.vue` around lines 98 - 103, Update the PluginList component’s error handling so load failures are surfaced through the template using errorMsg, or remove errorMsg entirely if no user-facing message is desired; also remove the debug console.log from the finally block while preserving loading.value cleanup.docs/.vuepress/theme/components/SearchBar.vue (1)
70-71: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRedundant computed wrapper.
props.authorOptionsis already reactive in the template;authorOptionscomputed only shadows the prop name.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/.vuepress/theme/components/SearchBar.vue` around lines 70 - 71, Remove the redundant authorOptions computed wrapper and update the template or component references to use props.authorOptions directly. Preserve the existing author options behavior and remove any now-unused computed import or related code.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/.vuepress/theme/components/PluginCard.vue`:
- Around line 5-9: Stop click propagation on the author homepage link and the
corresponding author target so activating either does not trigger the parent
`.card-body` openDetail handler; update the relevant click bindings around
`PluginCard` while preserving homepage navigation.
- Around line 19-23: Update the author rendering in PluginCard so it safely
handles an empty plugin.author array before accessing author[0].home or
author[0].name. Preserve the existing clickable author display when an author
exists, and render an appropriate empty-safe result when it does not.
In `@docs/.vuepress/theme/components/PluginDetailDrawer.vue`:
- Around line 62-73: Update getRawReadmeUrl to return an empty or established
sentinel value for non-GitHub URLs instead of returning homeUrl, including npmjs
plugin homes. Update the drawer’s README loading/rendering flow to detect that
sentinel and display the “无 README” state without fetching or rendering the HTML
page.
- Around line 12-14: Move the npm-type conditional from the el-button to the
surrounding el-tooltip in PluginDetailDrawer, so the tooltip itself is only
rendered when plugin.type is npm while preserving the existing button and
copyInstallCommand behavior.
- Line 19: Sanitize the rendered Markdown before assigning it to readmeHtml,
covering both remote and local README content, then continue using the sanitized
value in the v-html binding. Add and use an established sanitizer dependency
such as DOMPurify or sanitize-html, ensuring raw HTML, scripts, and event
handlers cannot reach PluginDetailDrawer’s rendered output.
In `@docs/.vuepress/theme/components/PluginList.vue`:
- Around line 57-67: Extract the duplicated Plugin type into a shared module,
such as docs/.vuepress/theme/types/plugin.ts, preserving its existing fields.
Remove the local type and import the shared Plugin type in
docs/.vuepress/theme/components/PluginList.vue#L57-L67,
docs/.vuepress/theme/components/PluginCard.vue#L34-L44 for Props['plugin'], and
docs/.vuepress/theme/components/PluginDetailDrawer.vue#L31-L41.
- Around line 4-29: Update the empty-state rendering in the PluginList template
so el-empty is shown only after a successful load, not when the plugin fetch
fails. Use the component’s existing loading and error state, or add the
necessary error-state distinction in the fetch flow, while preserving the empty
result for successful responses with no plugins.
- Around line 35-41: Update the component imports in PluginList.vue to use the
exact case of the filenames: SearchBar.vue, PluginCard.vue, and
PluginDetailDrawer.vue. Leave the remaining imports unchanged.
- Line 136: Update the author matching expression in the keyword search to
compare a normalized author name against the lowercased keyword using substring
matching rather than exact equality. Preserve optional-author handling and
ensure the "作者" search matches regardless of name casing.
---
Nitpick comments:
In `@docs/.vuepress/theme/components/PluginCard.vue`:
- Around line 59-61: Update the jumpTo functions in PluginCard.vue and
PluginDetailDrawer.vue to pass noopener in the window.open feature options,
preventing the opened page from retaining a window.opener reference while
preserving the existing URL and new-tab behavior.
- Around line 5-9: The PluginCard header click target around replaceName and
jumpTo is a plain div and is not keyboard accessible. Replace it with an
appropriate interactive element such as el-link or button, or add keyboard
focusability and Enter-key handling while preserving the existing
jumpTo(plugin.home) behavior.
In `@docs/.vuepress/theme/components/PluginDetailDrawer.vue`:
- Line 26: Remove the unused watch import from the Vue import declaration in
PluginDetailDrawer.vue, while preserving the existing onMounted and ref imports.
- Around line 50-59: Remove the redundant success-path console.log from
copyInstallCommand while preserving the clipboard write and success ElMessage
behavior; leave the existing catch and error notification unchanged.
- Around line 94-111: Update fetchReadme to configure the axios.get request with
an explicit finite timeout and responseType set to text before passing data to
marked.parse. Preserve the existing loading, error, and cleanup behavior.
- Around line 132-141: Update the styles in PluginDetailDrawer so they no longer
globally affect every .el-drawer; target the unique drawer-header and
drawer-body classes directly, or scope them beneath a component-specific class
applied to this el-drawer while preserving the existing margin and padding
rules.
- Around line 121-143: Remove the trailing empty scoped <style> block from
PluginDetailDrawer.vue. Leave the existing readme-content and global el-drawer
styles unchanged; merging them is optional and not required.
In `@docs/.vuepress/theme/components/PluginList.vue`:
- Line 30: Update the PluginDetailDrawer rendering in PluginList to gate
directly on showPlugin rather than drawerVisible, allowing the plugin prop to be
passed without the non-null assertion. Preserve the drawer visibility behavior
through the existing v-model binding.
- Around line 45-50: Remove the redundant if branch and useToggle(isDark) call
from checkDark, since isDark.value is already assigned directly. Also remove the
unused useToggle import from `@vueuse/core`.
- Around line 98-103: Update the PluginList component’s error handling so load
failures are surfaced through the template using errorMsg, or remove errorMsg
entirely if no user-facing message is desired; also remove the debug console.log
from the finally block while preserving loading.value cleanup.
In `@docs/.vuepress/theme/components/SearchBar.vue`:
- Around line 70-71: Remove the redundant authorOptions computed wrapper and
update the template or component references to use props.authorOptions directly.
Preserve the existing author options behavior and remove any now-unused computed
import or related code.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: ca3a9c61-6aae-45ca-9e91-e66d89a3ca10
📒 Files selected for processing (5)
docs/.vuepress/client.tsdocs/.vuepress/theme/components/PluginCard.vuedocs/.vuepress/theme/components/PluginDetailDrawer.vuedocs/.vuepress/theme/components/PluginList.vuedocs/.vuepress/theme/components/SearchBar.vue
| <el-divider style="margin: 12px 0" /> | ||
| <div v-if="readmeLoading" style="text-align: center; padding: 24px">加载 README 中...</div> | ||
| <div v-else-if="readmeError" style="text-align: center; padding: 24px; color: #999">{{ readmeError }}</div> | ||
| <div v-else class="readme-content" v-html="readmeHtml"></div> |
There was a problem hiding this comment.
🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
marked v15 sanitize option removed DOMPurify recommendation
💡 Result:
The sanitize and sanitizer options were removed from the Marked library in version 8.0.0 [1][2]. Marked explicitly does not sanitize output HTML, as this is not considered the responsibility of the markdown parser [3]. Instead, the official recommendation is to sanitize the resulting HTML output using a dedicated third-party library [4][5]. DOMPurify is the most frequently recommended option, though other libraries like sanitize-html and insane are also cited [1][6]. You can implement this sanitization in a few ways: 1. Wrapping the output: const sanitizedHtml = DOMPurify.sanitize(marked.parse(markdownString)); [4][5] 2. Using an extension hook: Marked supports a postprocess hook that can be used to automatically sanitize all output [7][8]. marked.use({ hooks: { postprocess(html) { return DOMPurify.sanitize(html); } } }); [7][8]
Citations:
- 1: https://marked.js.org/using_advanced
- 2: markedjs/marked@22ebdb2
- 3: Sanitize and sanitizer markedjs/marked#1232
- 4: https://marked.js.org/
- 5: https://github.com/markedjs/marked?tab=readme-ov-file
- 6: https://github.com/markedjs/marked/blob/v18.0.0/docs/INDEX.md
- 7: https://github.com/markedjs/marked/blob/0ccf4769/docs/USING_PRO.md
- 8: Add optional
sanitizeroption for easier secure rendering with DOMPurify markedjs/marked#3943
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "## locate file"
fd -a 'PluginDetailDrawer\.vue$' . || true
echo "## file excerpt"
file="$(fd 'PluginDetailDrawer\.vue$' . | head -n 1)"
if [ -n "${file:-}" ]; then
wc -l "$file"
cat -n "$file"
fi
echo "## package manifests"
fd '(^|/)package\.json$|package-lock\.json$|pnpm-lock\.yaml$|yarn\.lock$|bun\.lockb?|bun\.lock$' . -d 4 -x sh -c 'echo "--- $1"; sed -n "1,220p" "$1" | rg -n "marked|vuepress|dependencies|devDependencies|scripts" -C 2 || true' sh {}Repository: KarinJS/karin-docs
Length of output: 4958
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "## tracked matching files"
git ls-files | rg '(^|/)PluginDetailDrawer\.vue$|PluginDetailDrawer' || true
echo "## search for v-html and marked/readmeHtml"
rg -n "v-html|readmeHtml|marked\.parse|PluginDetailDrawer" docs . --glob '!node_modules' --glob '!dist' || true
echo "## package-lock snippets if present"
if [ -f package.json ]; then cat -n package.json | sed -n '1,80p'; fiRepository: KarinJS/karin-docs
Length of output: 2338
Sanitize Markdown before injecting it with v-html.
marked does not sanitize output, so README content can render raw HTML/<script>/event handlers. Sanitize before assigning to readmeHtml, e.g. with DOMPurify/sanitize-html, and add the sanitizer as a dependency; apply this whether the content is remote or local.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/.vuepress/theme/components/PluginDetailDrawer.vue` at line 19, Sanitize
the rendered Markdown before assigning it to readmeHtml, covering both remote
and local README content, then continue using the sanitized value in the v-html
binding. Add and use an established sanitizer dependency such as DOMPurify or
sanitize-html, ensuring raw HTML, scripts, and event handlers cannot reach
PluginDetailDrawer’s rendered output.
- 将 Plugin 类型定义集中提取到 test-url.ts 工具模块,消除组件内的重复类型声明 - 修复搜索关键词匹配逻辑,支持作者名称的模糊大小写搜索 - 修复加载失败时错误提示与骨架屏状态冲突的问题,统一错误处理流程 - 调整组件文件名大小写以符合 Vue 规范 - 优化 README 获取逻辑,提前处理无法获取的情况
There was a problem hiding this comment.
🧹 Nitpick comments (1)
docs/.vuepress/theme/utils/test-url.ts (1)
99-100: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick winSeparate the raw API payload type from the normalized
Plugintype.
PluginList.vuetypes each rawdata.pluginsitem asPlugin, but this definition requiresauthorto already be an array while the same loader explicitly handles non-array values. This weakens TypeScript’s protection against schema drift. KeepPluginas the normalized UI type and introduce a separate payload type (or runtime schema validation) for the API response, then map it intoPlugin.This is based on the normalization in
docs/.vuepress/theme/components/PluginList.vueand array consumption indocs/.vuepress/theme/components/PluginCard.vue.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/.vuepress/theme/utils/test-url.ts` around lines 99 - 100, Separate the raw API plugin payload type from the normalized Plugin type in test-url.ts. Update the PluginList.vue loader to type data.plugins with the payload shape that permits non-array author values, then normalize each item into the existing Plugin shape before passing it to PluginCard.vue. Keep Plugin.author as the array consumed by the UI.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@docs/.vuepress/theme/utils/test-url.ts`:
- Around line 99-100: Separate the raw API plugin payload type from the
normalized Plugin type in test-url.ts. Update the PluginList.vue loader to type
data.plugins with the payload shape that permits non-array author values, then
normalize each item into the existing Plugin shape before passing it to
PluginCard.vue. Keep Plugin.author as the array consumed by the UI.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 1668db4f-872b-4957-842c-21e45e756f8c
📒 Files selected for processing (4)
docs/.vuepress/theme/components/PluginCard.vuedocs/.vuepress/theme/components/PluginDetailDrawer.vuedocs/.vuepress/theme/components/PluginList.vuedocs/.vuepress/theme/utils/test-url.ts
🚧 Files skipped from review as they are similar to previous changes (3)
- docs/.vuepress/theme/components/PluginCard.vue
- docs/.vuepress/theme/components/PluginDetailDrawer.vue
- docs/.vuepress/theme/components/PluginList.vue
Summary by CodeRabbit