Skip to content

fix: 修复并重构更新日志展示功能,提取为独立组件 UpdateLogModal#479

Merged
ikenxuan merged 1 commit intomainfrom
fix-changeLog
Jun 13, 2025
Merged

fix: 修复并重构更新日志展示功能,提取为独立组件 UpdateLogModal#479
ikenxuan merged 1 commit intomainfrom
fix-changeLog

Conversation

@ikenxuan
Copy link
Collaborator

@ikenxuan ikenxuan commented Jun 13, 2025

好的,这是翻译成简体中文的 pull request 总结:

Sourcery 总结

提取并重构更新日志显示和更新控制到专用组件中,简化仪表板中的版本处理,并通过一致的样式增强 Markdown 渲染。

增强功能:

  • 将更新日志的获取和展示提取到独立的 UpdateLogModal 组件中。
  • 将更新确认和重启逻辑移动到可重用的 UpdateButtons 组件中。
  • 重构“状态”页面以使用新组件,并删除内联模态框和版本获取代码。
  • 添加 getPackageType 并改进 extractUpdateLogs 以过滤和排序跨多个包类型的发布日志。
  • 扩展 Markdown 组件以接受 className 并支持其他元素(prelistrongem),并统一样式。
  • 调整 npmLatest 的状态类型,并删除状态检查中不必要的轮询间隔。
Original summary in English

Summary by Sourcery

Extract and refactor update log display and update controls into dedicated components, simplify version handling in the dashboard, and enhance markdown rendering with consistent styling.

Enhancements:

  • Extract update log fetching and presentation into a standalone UpdateLogModal component.
  • Move update confirmation and restart logic into a reusable UpdateButtons component.
  • Refactor the Status page to use the new components and remove inline modal and version-fetching code.
  • Add getPackageType and improve extractUpdateLogs to filter and sort release logs across multiple package types.
  • Extend Markdown component to accept className and support additional elements (pre, li, strong, em) with unified styling.
  • Adjust state typings for npmLatest and remove unnecessary polling intervals in status checks.

@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Jun 13, 2025

## 审查者指南

此 PR 将更新日志显示重构为一个独立的 UpdateLogModal 组件(包含其自身的获取和 UI),将更新按钮逻辑提取到 UpdateButtons 组件中,增强了 Markdown 渲染器以获得更好的样式和可扩展性,改进了版本库的日志提取算法,并通过删除遗留的内联逻辑和未使用的导入来清理仪表板页面。

#### 顺序图:在模态框中显示更新日志

```mermaid
sequenceDiagram
    actor User
    participant DashboardPage as "DashboardPage (状态组件)"
    participant UpdateLogModal as "UpdateLogModal"
    participant version_ts as "version.ts"
    participant GitHubAPI as "GitHub API (通过 proxyFn)"

    User->>DashboardPage: 点击 '查看更新日志'
    DashboardPage->>DashboardPage: 设置 isChangelogOpen = true
    DashboardPage->>UpdateLogModal: 渲染 (isOpen=true, currentVersion, proxyFn)
    activate UpdateLogModal
    alt 数据尚未加载 OR isOpen 变为 true
        UpdateLogModal->>UpdateLogModal: 设置 setIsLoadingRelease = true
        UpdateLogModal->>GitHubAPI: GET releases.json (通过 proxyFn)
        activate GitHubAPI
        GitHubAPI-->>UpdateLogModal: 返回 GithubRelease[] 数据
        deactivate GitHubAPI
        UpdateLogModal->>version_ts: extractUpdateLogs(data, currentVersion)
        activate version_ts
        version_ts-->>UpdateLogModal: 返回过滤/排序后的 updateLogs
        deactivate version_ts
        UpdateLogModal->>UpdateLogModal: 设置 setIsLoadingRelease = false, 设置 updateLogs
    end
    UpdateLogModal->>User: 显示更新日志
    deactivate UpdateLogModal

类图:更新日志功能的核心组件和实用程序

classDiagram
    direction LR

    class Status {
        <<Component (DashboardPage)>>
        #isChangelogOpen: boolean
        #npmLatest: string | false
        #proxyFn: (url: string) => string
        +handleTooltipClick(): void
    }
    Status "1" --o "1" UpdateLogModal : 显示

    class UpdateLogModal {
        <<Component>>
        +currentVersion: string
        +isOpen: boolean
        +onOpenChange(isOpen: boolean): void
        +npmLatest: string | false
        +proxyFn(url: string): string
        #isLoadingRelease: boolean
        #releaseData: GithubRelease[]
        #updateLogs: GithubRelease[]
        +useEffect(): void
        +handleCloseModal(): void
    }
    UpdateLogModal "1" --o "1" Markdown : 用于渲染
    UpdateLogModal "1" --o "1" UpdateButtons : 嵌入
    UpdateLogModal ..> version_ts : 使用 extractUpdateLogs
    UpdateLogModal ..> GithubRelease : 显示来自的数据

    class UpdateButtons {
        <<Component>>
        +handleCloseModal(): void
        #running: boolean
        +onUpdate(): void
    }

    class Markdown {
        <<Component>>
        +content: string
        +className?: string
    }

    class version_ts {
        <<Utility Module>>
        +getPackageType(tagName: string): string | null
        +extractUpdateLogs(releases: GithubRelease[], currentCoreVersion: string): GithubRelease[]
    }
    version_ts ..> GithubRelease : 处理

    class GithubRelease {
        <<Data Type>>
        +tag_name: string
        +body: string
        +created_at: string
        +published_at: string
        +prerelease: boolean
        +name: string
        +html_url: string
        +author: object
        +assets: object[]
    }
Loading

文件级别更改

变更 详情 文件
将更新日志显示逻辑提取到专用的 UpdateLogModal 组件中
  • 从 Status 组件中删除内联版本获取、状态和模态标记
  • 引入了 UpdateLogModal,它具有自己的 useRequest、加载/错误处理和渲染结构
  • 将 dashboard/index.tsx 中的旧 块替换为 调用
packages/web/src/pages/dashboard/index.tsx
packages/web/src/components/UpdateLogModal.tsx
将更新和关闭按钮流程隔离到 UpdateButtons 组件中
  • 创建 UpdateButtons 以封装更新确认、重启、Toast 通知和加载状态
  • 将 UpdateButtons 集成到 UpdateLogModal 中,并删除了 dashboard/index.tsx 中之前的内联实现
packages/web/src/components/UpdateButtons.tsx
packages/web/src/components/UpdateLogModal.tsx
增强 Markdown 组件以支持自定义样式和包装器
  • 向 Markdown 组件添加了可选的 className prop 和包装器 div
  • 使用其他标签(pre、li、strong、em)扩展了 ReactMarkdown 渲染器,并更新了 Tailwind 类以进行主题设置
packages/web/src/components/Markdown.tsx
改进版本库中的 extractUpdateLogs 以按包和日期进行过滤和排序
  • 添加了 getPackageType 实用程序以按包前缀对版本进行分类
  • 重写了 extractUpdateLogs 以处理当前与最新的核心版本、回退行为和时间范围过滤
  • 确保结果按降序发布顺序排序
packages/web/src/lib/version.ts
通过删除过时的导入和状态逻辑来清理仪表板状态页面
  • 删除了 axios、modal、Markdown、extractUpdateLogs、ScrollShadow、FullScreenLoader、restartRequest 导入
  • 注释掉了状态和机器人请求的 pollingInterval 设置
  • 将 npmLatest 状态类型从 boolean 调整为 string
false

提示和命令

与 Sourcery 互动

  • 触发新的审查: 在 pull request 上评论 @sourcery-ai review
  • 继续讨论: 直接回复 Sourcery 的审查评论。
  • 从审查评论生成 GitHub issue: 通过回复审查评论,要求 Sourcery 从审查评论创建一个 issue。您也可以回复审查评论并使用 @sourcery-ai issue 从中创建一个 issue。
  • 生成 pull request 标题: 在 pull request 标题中的任何位置写入 @sourcery-ai 以随时生成标题。您也可以在 pull request 上评论 @sourcery-ai title 以随时(重新)生成标题。
  • 生成 pull request 摘要: 在 pull request 正文中的任何位置写入 @sourcery-ai summary 以随时在您想要的位置生成 PR 摘要。您也可以在 pull request 上评论 @sourcery-ai summary 以随时(重新)生成摘要。
  • 生成审查者指南: 在 pull request 上评论 @sourcery-ai guide 以随时(重新)生成审查者指南。
  • 解决所有 Sourcery 评论: 在 pull request 上评论 @sourcery-ai resolve 以解决所有 Sourcery 评论。如果您已经解决了所有评论并且不想再看到它们,这将非常有用。
  • 驳回所有 Sourcery 审查: 在 pull request 上评论 @sourcery-ai dismiss 以驳回所有现有的 Sourcery 审查。如果您想从新的审查开始,这将特别有用 - 不要忘记评论 @sourcery-ai review 以触发新的审查!

自定义您的体验

访问您的 仪表板 以:

  • 启用或禁用审查功能,例如 Sourcery 生成的 pull request 摘要、审查者指南等。
  • 更改审查语言。
  • 添加、删除或编辑自定义审查说明。
  • 调整其他审查设置。

获取帮助

```
Original review guide in English

Reviewer's Guide

This PR refactors the update log display into a standalone UpdateLogModal component (with its own fetching and UI), extracts the update button logic into an UpdateButtons component, enhances the Markdown renderer for better styling and extensibility, improves the version library’s log extraction algorithm, and cleans up the dashboard page by removing legacy inline logic and unused imports.

Sequence Diagram: Displaying Update Logs in Modal

sequenceDiagram
    actor User
    participant DashboardPage as "DashboardPage (Status Comp.)"
    participant UpdateLogModal as "UpdateLogModal"
    participant version_ts as "version.ts"
    participant GitHubAPI as "GitHub API (via proxyFn)"

    User->>DashboardPage: Clicks 'View Changelog'
    DashboardPage->>DashboardPage: Sets isChangelogOpen = true
    DashboardPage->>UpdateLogModal: Renders (isOpen=true, currentVersion, proxyFn)
    activate UpdateLogModal
    alt Data not yet loaded OR isOpen becomes true
        UpdateLogModal->>UpdateLogModal: setIsLoadingRelease = true
        UpdateLogModal->>GitHubAPI: GET releases.json (via proxyFn)
        activate GitHubAPI
        GitHubAPI-->>UpdateLogModal: Returns GithubRelease[] data
        deactivate GitHubAPI
        UpdateLogModal->>version_ts: extractUpdateLogs(data, currentVersion)
        activate version_ts
        version_ts-->>UpdateLogModal: Returns filtered/sorted updateLogs
        deactivate version_ts
        UpdateLogModal->>UpdateLogModal: setIsLoadingRelease = false, sets updateLogs
    end
    UpdateLogModal->>User: Displays update logs
    deactivate UpdateLogModal
Loading

Class Diagram: Core Components and Utilities for Update Log Feature

classDiagram
    direction LR

    class Status {
        <<Component (DashboardPage)>>
        #isChangelogOpen: boolean
        #npmLatest: string | false
        #proxyFn: (url: string) => string
        +handleTooltipClick(): void
    }
    Status "1" --o "1" UpdateLogModal : displays

    class UpdateLogModal {
        <<Component>>
        +currentVersion: string
        +isOpen: boolean
        +onOpenChange(isOpen: boolean): void
        +npmLatest: string | false
        +proxyFn(url: string): string
        #isLoadingRelease: boolean
        #releaseData: GithubRelease[]
        #updateLogs: GithubRelease[]
        +useEffect(): void
        +handleCloseModal(): void
    }
    UpdateLogModal "1" --o "1" Markdown : uses for rendering
    UpdateLogModal "1" --o "1" UpdateButtons : embeds
    UpdateLogModal ..> version_ts : uses extractUpdateLogs
    UpdateLogModal ..> GithubRelease : displays data from

    class UpdateButtons {
        <<Component>>
        +handleCloseModal(): void
        #running: boolean
        +onUpdate(): void
    }

    class Markdown {
        <<Component>>
        +content: string
        +className?: string
    }

    class version_ts {
        <<Utility Module>>
        +getPackageType(tagName: string): string | null
        +extractUpdateLogs(releases: GithubRelease[], currentCoreVersion: string): GithubRelease[]
    }
    version_ts ..> GithubRelease : processes

    class GithubRelease {
        <<Data Type>>
        +tag_name: string
        +body: string
        +created_at: string
        +published_at: string
        +prerelease: boolean
        +name: string
        +html_url: string
        +author: object
        +assets: object[]
    }
Loading

File-Level Changes

Change Details Files
Extract update log display logic into a dedicated UpdateLogModal component
  • Removed inline release fetching, state, and modal markup from the Status component
  • Introduced UpdateLogModal with its own useRequest, loading/error handling, and render structure
  • Replaced the old block in dashboard/index.tsx with the invocation
packages/web/src/pages/dashboard/index.tsx
packages/web/src/components/UpdateLogModal.tsx
Isolate update and close button flow into an UpdateButtons component
  • Created UpdateButtons to encapsulate update confirmation, restart, toast notifications, and loading state
  • Integrated UpdateButtons into UpdateLogModal and removed the previous inline implementation in dashboard/index.tsx
packages/web/src/components/UpdateButtons.tsx
packages/web/src/components/UpdateLogModal.tsx
Enhance Markdown component for custom styling and wrapper support
  • Added an optional className prop and wrapper div to the Markdown component
  • Extended ReactMarkdown renderers with additional tags (pre, li, strong, em) and updated Tailwind classes for theming
packages/web/src/components/Markdown.tsx
Improve extractUpdateLogs in version library to filter and sort by package and date
  • Added getPackageType utility to classify releases by package prefix
  • Rewrote extractUpdateLogs to handle current vs latest core version, fallback behavior, and time-range filtering
  • Ensured results are sorted in descending publication order
packages/web/src/lib/version.ts
Clean up dashboard status page by removing obsolete imports and state logic
  • Removed axios, modal, Markdown, extractUpdateLogs, ScrollShadow, FullScreenLoader, restartRequest imports
  • Commented out pollingInterval settings for status and bots requests
  • Adjusted npmLatest state type from boolean to string
false

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@github-actions
Copy link
Contributor

你可以通过以下命令安装该版本:

pnpm add https://pkg.pr.new/node-karin@a6aa8f4 -w

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ikenxuan - 我已经查看了你的更改,它们看起来很棒!

AI代理的提示
请解决此代码审查中的评论:
## 单独评论

### 评论 1
<location> `packages/web/src/pages/dashboard/index.tsx:178` </location>
<code_context>
+    // pollingInterval: 5000,
   })

   const handleTooltipClick = () => {
</code_context>

<issue_to_address>
Modal可能在代理函数准备好之前获取数据

由于fetch现在立即运行,请确保它仅在`proxyFn`初始化之后执行——可以通过检查`proxyFnInitialized`或将此标志传递给`UpdateLogModal`。否则,可能会意外使用默认URL。
</issue_to_address>

### 评论 2
<location> `packages/web/src/components/UpdateLogModal.tsx:31` </location>
<code_context>
+}: UpdateLogModalProps) {
+  const [isLoadingRelease, setIsLoadingRelease] = useState(false)
+
+  const { data: releaseData, error: releaseError, run: fetchRelease } = useRequest(
+    async () => {
+      setIsLoadingRelease(true)
</code_context>

<issue_to_address>
fetchRelease回调关闭初始proxyFn

由于`proxyFn`未包含在依赖项中,因此对其的更新不会反映在`fetchRelease`中。将`proxyFn`添加到`refreshDeps`或在其更改时重新创建请求。
</issue_to_address>

### 评论 3
<location> `packages/web/src/components/UpdateLogModal.tsx:50` </location>
<code_context>
+  )
+
+  // 当模态框打开时自动获取数据
+  useEffect(() => {
+    if (isOpen && !releaseData && !isLoadingRelease) {
+      fetchRelease()
+    }
+  }, [isOpen, releaseData, isLoadingRelease, fetchRelease])
+
+  const updateLogs = releaseData ? extractUpdateLogs(releaseData, currentVersion) : []
</code_context>

<issue_to_address>
获取错误时可能出现无限重试循环

考虑通过检查错误状态或使用标志来避免无限重试,从而防止在失败后重复尝试获取。
</issue_to_address>

### 评论 4
<location> `packages/web/src/components/UpdateLogModal.tsx:58` </location>
<code_context>
+
+  const updateLogs = releaseData ? extractUpdateLogs(releaseData, currentVersion) : []
+
+  const middleVersions = useMemo(() => {
+    return updateLogs || []
+  }, [updateLogs])
+
+  const handleCloseModal = useCallback(() => {
</code_context>

<issue_to_address>
`middleVersions`的冗余useMemo

您可以直接使用`updateLogs`并删除不必要的`useMemo`用于`middleVersions`。
</issue_to_address>

### 评论 5
<location> `packages/web/src/components/UpdateButtons.tsx:72` </location>
<code_context>
+  return (
+    <div className='flex gap-2 ml-auto'>
+      {running && <FullScreenLoader />}
+      <Button
+        ref={window.innerWidth <= 768 ? liquidGlassButtonRef1 : undefined}
+        color='danger'
+        variant='flat'
+        isDisabled={running}
+        onPress={handleCloseModal}
+        className='glass-effect'
+      >
+        关闭
+      </Button>
+      <Button
+        ref={window.innerWidth <= 768 ? liquidGlassButtonRef2 : undefined}
+        color='primary'
+        variant='flat'
+        isDisabled={running}
+        onPress={onUpdate}
+        className='glass-effect'
+      >
</code_context>

<issue_to_address>
直接使用`window.innerWidth`可能会破坏SSR并且不具有反应性

这种方法会导致服务器端渲染期间出现错误,并且不会响应视口更改。在使用之前,使用媒体查询钩子或检查`window`。
</issue_to_address>

### 评论 6
<location> `packages/web/src/lib/version.ts:67` </location>
<code_context>
+ * @param currentCoreVersion 当前 core 版本(纯版本号,如 '1.10.3')
  * @returns 更新日志
  */
 export const extractUpdateLogs = (releases: GithubRelease[], currentCoreVersion: string): GithubRelease[] => {
-  // 找到最新 core 版本
-  releases
</code_context>

<issue_to_address>
考虑重构该函数以使用单次映射进行注释、整合排序和更清晰的链式操作。

```suggestion
// Extract common utilities
const TARGET_PACKAGES = ['core', 'web', 'cli', 'create-karin'] as const;
type PackageType = typeof TARGET_PACKAGES[number];

const getPackageType = (tag: string): PackageType | null => {
  if (tag.startsWith('core-v')) return 'core';
  if (tag.startsWith('web-v'))  return 'web';
  if (tag.startsWith('cli-v'))  return 'cli';
  if (tag.startsWith('create-karin-v') || tag.startsWith('create-v')) return 'create-karin';
  return null;
};

const parseTs = (r: GithubRelease) => Date.parse(r.created_at);
const sortByDateDesc = (a: GithubRelease, b: GithubRelease) =>
  parseTs(b) - parseTs(a);

export const extractUpdateLogs = (
  releases: GithubRelease[],
  currentCoreVersion: string
): GithubRelease[] => {
  // 1) filter & annotate
  const enriched = releases
    .map(r => ({ r, type: getPackageType(r.tag_name), ts: parseTs(r) }))
    .filter(({ type }) => type && TARGET_PACKAGES.includes(type));

  const currentTag = `core-v${currentCoreVersion}`;
  const current = enriched.find(({ r }) => r.tag_name === currentTag);

  // 2) not found → return all sorted
  if (!current) {
    return enriched.map(({ r }) => r).sort(sortByDateDesc);
  }

  // 3) find latest core
  const latestCore = enriched
    .filter(({ type }) => type === 'core')
    .map(({ r }) => r)
    .reduce((best, cur) =>
      compareVersion(cur.tag_name, best.tag_name) > 0 ? cur : best,
      current.r
    );

  // 4) already up-to-date
  if (compareVersion(latestCore.tag_name, currentTag) <= 0) {
    return [];
  }

  // 5) filter by time‐range and sort once
  const fromTs = current.ts;
  const toTs = Date.parse(latestCore.created_at);
  return enriched
    .map(({ r, ts }) => ({ r, ts }))
    .filter(({ r, ts }) =>
      r.tag_name !== currentTag &&
      ts > fromTs &&
      ts <= toTs
    )
    .map(({ r }) => r)
    .sort(sortByDateDesc);
};
```

- 单次传递给`map`→附加`type``ts`,然后对包类型进行一次`filter`- 每次发布调用一次`parseTs`- 将排序合并到`sortByDateDesc`中。
- 链式操作(没有嵌套的`if`/`filter`块)。
- 保留所有原始行为。
</issue_to_address>

### 评论 7
<location> `packages/web/src/components/UpdateLogModal.tsx:120` </location>
<code_context>
+              )
+              : (
+                <>
+                  {middleVersions.map((versionInfo) => (
+                    <div
+                      key={versionInfo.tag_name}
</code_context>

<issue_to_address>
考虑提取内联标签和日期逻辑,以及发布项目渲染,到单独的辅助函数和一个子组件中,以简化主组件的JSX。

```suggestion
内联IIFE/switch和日期格式化为您的JSX添加了很多噪音。将它们提取到小的纯辅助函数和/或一个`<ReleaseItem />`子组件中。例如:

1) helpers/tag.ts
```ts
export type TagInfo = { label: string; color: 'primary'|'warning'|'secondary'|'success' };
export function getTagInfo(tag: string): TagInfo {
  if (tag.includes('web'))    return { label: 'WEB 界面',   color: 'warning' };
  if (tag.includes('cli'))    return { label: '命令行工具', color: 'secondary' };
  if (tag.includes('create')) return { label: '脚手架',     color: 'success' };
  // default to core
  return { label: '本体',       color: 'primary' };
}
```

2) helpers/date.ts
```ts
export function formatDateZh(dateStr: string): string {
  return new Date(dateStr).toLocaleDateString('zh-CN', {
    year: 'numeric', month: 'short', day: 'numeric',
  });
}
```

3) components/ReleaseItem.tsx
```tsx
import { Chip } from '@heroui/chip';
import Markdown from '@/components/Markdown';
import { getTagInfo } from '@/helpers/tag';
import { formatDateZh } from '@/helpers/date';

export function ReleaseItem({ info }) {
  const { label, color } = getTagInfo(info.tag_name);
  return (
    <div className="p-5 …">
      <div className="flex justify-between mb-3">
        <div className="flex gap-2">
          <Chip color={color} …>{label}</Chip>
          <span className="font-mono"> {info.tag_name}</span>
        </div>
        <div className="text-right">
          <div className="text-xs">{formatDateZh(info.published_at)}</div>
          {info.prerelease && <Chip color="warning">预发布</Chip>}
        </div>
      </div>
      {info.name && <h4>…{info.name}</h4>}
      <Markdown content={info.body} className="prose …" />
      {/* …assets + footer… */}
    </div>
  );
}
```

4) 在您的模态JSX中,将内联IIFE和日期逻辑替换为:
```tsx
{middleVersions.map(v => (
  <ReleaseItem key={v.tag_name} info={v} />
))}
```

这保留了所有功能,但将复杂逻辑移出了渲染,使组件更易于阅读。
</issue_to_address>

### 评论 8
<location> `packages/web/src/components/UpdateButtons.tsx:30` </location>
<code_context>
+            if (status === 'ok') {
+              toast.success('更新成功,正在重启......')
+              await restartRequest({ isPm2: true })
+              await new Promise(resolve => {
+                const interval = setInterval(async () => {
+                  try {
</code_context>

<issue_to_address>
考虑将轮询逻辑提取到辅助函数中,并整合重复的钩子以简化组件。

```suggestion
考虑将轮询逻辑提取到可重用的辅助函数中,并将重复的钩子折叠为一个。例如:

// utils/server.ts
export async function waitForServerUp(
  timeout = 2000
): Promise<void> {
  while (true) {
    try {
      await request.serverGet('/api/v1/ping')
      return
    } catch {
      await new Promise((r) => setTimeout(r, timeout))
    }
  }
}

// UpdateButtons.tsx
import { waitForServerUp } from '@/utils/server'

export default function UpdateButtons({ handleCloseModal }: UpdateButtonsProps) {
  const [running, setRunning] = useState(false)
  const dialog = useDialog()

  // single ref usage
  const glassRef = useLiquidGlassButton({
    gaussianBlur: 0,
    scale: 20,
    transparency: 0,
  })

  const onUpdate = () => {
    dialog.confirm({
      title: '更新',
      content: '确认更新吗?',
      onConfirm: async () => {
        setRunning(true)
        try {
          const { status } = await request.serverGet<{ status: 'ok' | 'failed' }>(
            '/api/v1/system/update',
            { timeout: 30000 }
          )
          if (status !== 'ok') throw new Error('更新失败')
          toast.success('更新成功,正在重启…')
          await restartRequest({ isPm2: true })
          await waitForServerUp()
          toast.success('重启成功')
          window.location.reload()
        } catch (err: any) {
          toast.error(err.message || '操作失败')
        } finally {
          setRunning(false)
        }
      },
    })
  }

  const applyRef = window.innerWidth <= 768 ? glassRef : undefined

  return (
    <div className="flex gap-2 ml-auto">
      {running && <FullScreenLoader />}
      <Button
        ref={applyRef}
        color="danger"
        variant="flat"
        isDisabled={running}
        onPress={handleCloseModal}
        className="glass-effect"
      >
        关闭
      </Button>
      <Button
        ref={applyRef}
        color="primary"
        variant="flat"
        isDisabled={running}
        onPress={onUpdate}
        className="glass-effect"
      >
        更新
      </Button>
    </div>
  )
}
```

减少复杂性的步骤:
1. 将轮询拉入`waitForServerUp`以消除内联循环。
2. 将嵌套的`try/catch`合并为一个具有清晰流程的块。
3. 使用单个`useLiquidGlassButton`钩子并有条件地应用其ref。
</issue_to_address>

Sourcery对开源是免费的 - 如果你喜欢我们的评论,请考虑分享它们✨
帮助我更有用!请点击每个评论上的👍或👎,我将使用反馈来改进您的评论。
Original comment in English

Hey @ikenxuan - I've reviewed your changes and they look great!

Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments

### Comment 1
<location> `packages/web/src/pages/dashboard/index.tsx:178` </location>
<code_context>
+    // pollingInterval: 5000,
   })

   const handleTooltipClick = () => {
</code_context>

<issue_to_address>
Modal may fetch before proxy function is ready

Since fetch now runs immediately, ensure it only executes after `proxyFn` is initialized—either by checking `proxyFnInitialized` or passing this flag to `UpdateLogModal`. Otherwise, the default URL may be used unintentionally.
</issue_to_address>

### Comment 2
<location> `packages/web/src/components/UpdateLogModal.tsx:31` </location>
<code_context>
+}: UpdateLogModalProps) {
+  const [isLoadingRelease, setIsLoadingRelease] = useState(false)
+
+  const { data: releaseData, error: releaseError, run: fetchRelease } = useRequest(
+    async () => {
+      setIsLoadingRelease(true)
</code_context>

<issue_to_address>
fetchRelease callback closes over initial proxyFn

Since `proxyFn` is not included in the dependencies, updates to it won't be reflected in `fetchRelease`. Add `proxyFn` to `refreshDeps` or recreate the request when it changes.
</issue_to_address>

### Comment 3
<location> `packages/web/src/components/UpdateLogModal.tsx:50` </location>
<code_context>
+  )
+
+  // 当模态框打开时自动获取数据
+  useEffect(() => {
+    if (isOpen && !releaseData && !isLoadingRelease) {
+      fetchRelease()
+    }
+  }, [isOpen, releaseData, isLoadingRelease, fetchRelease])
+
+  const updateLogs = releaseData ? extractUpdateLogs(releaseData, currentVersion) : []
</code_context>

<issue_to_address>
Potential infinite retry loop on fetch errors

Consider preventing repeated fetch attempts after a failure by checking for an error state or using a flag to avoid infinite retries.
</issue_to_address>

### Comment 4
<location> `packages/web/src/components/UpdateLogModal.tsx:58` </location>
<code_context>
+
+  const updateLogs = releaseData ? extractUpdateLogs(releaseData, currentVersion) : []
+
+  const middleVersions = useMemo(() => {
+    return updateLogs || []
+  }, [updateLogs])
+
+  const handleCloseModal = useCallback(() => {
</code_context>

<issue_to_address>
Redundant useMemo for `middleVersions`

You can use `updateLogs` directly and remove the unnecessary `useMemo` for `middleVersions`.
</issue_to_address>

### Comment 5
<location> `packages/web/src/components/UpdateButtons.tsx:72` </location>
<code_context>
+  return (
+    <div className='flex gap-2 ml-auto'>
+      {running && <FullScreenLoader />}
+      <Button
+        ref={window.innerWidth <= 768 ? liquidGlassButtonRef1 : undefined}
+        color='danger'
+        variant='flat'
+        isDisabled={running}
+        onPress={handleCloseModal}
+        className='glass-effect'
+      >
+        关闭
+      </Button>
+      <Button
+        ref={window.innerWidth <= 768 ? liquidGlassButtonRef2 : undefined}
+        color='primary'
+        variant='flat'
+        isDisabled={running}
+        onPress={onUpdate}
+        className='glass-effect'
+      >
</code_context>

<issue_to_address>
Direct `window.innerWidth` usage may break SSR & is not reactive

This approach will cause errors during server-side rendering and won't respond to viewport changes. Use a media-query hook or check for `window` before accessing it.
</issue_to_address>

### Comment 6
<location> `packages/web/src/lib/version.ts:67` </location>
<code_context>
+ * @param currentCoreVersion 当前 core 版本(纯版本号,如 '1.10.3')
  * @returns 更新日志
  */
 export const extractUpdateLogs = (releases: GithubRelease[], currentCoreVersion: string): GithubRelease[] => {
-  // 找到最新 core 版本
-  releases
</code_context>

<issue_to_address>
Consider refactoring the function to use a single-pass mapping for annotation, consolidated sorting, and clearer chained operations.

```suggestion
// Extract common utilities
const TARGET_PACKAGES = ['core', 'web', 'cli', 'create-karin'] as const;
type PackageType = typeof TARGET_PACKAGES[number];

const getPackageType = (tag: string): PackageType | null => {
  if (tag.startsWith('core-v')) return 'core';
  if (tag.startsWith('web-v'))  return 'web';
  if (tag.startsWith('cli-v'))  return 'cli';
  if (tag.startsWith('create-karin-v') || tag.startsWith('create-v')) return 'create-karin';
  return null;
};

const parseTs = (r: GithubRelease) => Date.parse(r.created_at);
const sortByDateDesc = (a: GithubRelease, b: GithubRelease) =>
  parseTs(b) - parseTs(a);

export const extractUpdateLogs = (
  releases: GithubRelease[],
  currentCoreVersion: string
): GithubRelease[] => {
  // 1) filter & annotate
  const enriched = releases
    .map(r => ({ r, type: getPackageType(r.tag_name), ts: parseTs(r) }))
    .filter(({ type }) => type && TARGET_PACKAGES.includes(type));

  const currentTag = `core-v${currentCoreVersion}`;
  const current = enriched.find(({ r }) => r.tag_name === currentTag);

  // 2) not found → return all sorted
  if (!current) {
    return enriched.map(({ r }) => r).sort(sortByDateDesc);
  }

  // 3) find latest core
  const latestCore = enriched
    .filter(({ type }) => type === 'core')
    .map(({ r }) => r)
    .reduce((best, cur) =>
      compareVersion(cur.tag_name, best.tag_name) > 0 ? cur : best,
      current.r
    );

  // 4) already up-to-date
  if (compareVersion(latestCore.tag_name, currentTag) <= 0) {
    return [];
  }

  // 5) filter by time‐range and sort once
  const fromTs = current.ts;
  const toTs = Date.parse(latestCore.created_at);
  return enriched
    .map(({ r, ts }) => ({ r, ts }))
    .filter(({ r, ts }) =>
      r.tag_name !== currentTag &&
      ts > fromTs &&
      ts <= toTs
    )
    .map(({ r }) => r)
    .sort(sortByDateDesc);
};
```

- Single pass to `map` → attach `type` and `ts`, then one `filter` on package type.  
- One `parseTs` call per release.  
- Consolidated sort into `sortByDateDesc`.  
- Chained operations (no nested `if`/`filter` blocks).  
- Preserves all original behavior.
</issue_to_address>

### Comment 7
<location> `packages/web/src/components/UpdateLogModal.tsx:120` </location>
<code_context>
+              )
+              : (
+                <>
+                  {middleVersions.map((versionInfo) => (
+                    <div
+                      key={versionInfo.tag_name}
</code_context>

<issue_to_address>
Consider extracting the inline tag and date logic, as well as the release item rendering, into separate helper functions and a sub-component to simplify the main component's JSX.

```suggestion
The inline IIFE/switch and date formatting are adding a lot of noise to your JSX. Extract those into small pure helpers and/or a `<ReleaseItem />` sub-component. For example:

1) helpers/tag.ts
```ts
export type TagInfo = { label: string; color: 'primary'|'warning'|'secondary'|'success' };
export function getTagInfo(tag: string): TagInfo {
  if (tag.includes('web'))    return { label: 'WEB 界面',   color: 'warning' };
  if (tag.includes('cli'))    return { label: '命令行工具', color: 'secondary' };
  if (tag.includes('create')) return { label: '脚手架',     color: 'success' };
  // default to core
  return { label: '本体',       color: 'primary' };
}
```

2) helpers/date.ts
```ts
export function formatDateZh(dateStr: string): string {
  return new Date(dateStr).toLocaleDateString('zh-CN', {
    year: 'numeric', month: 'short', day: 'numeric',
  });
}
```

3) components/ReleaseItem.tsx
```tsx
import { Chip } from '@heroui/chip';
import Markdown from '@/components/Markdown';
import { getTagInfo } from '@/helpers/tag';
import { formatDateZh } from '@/helpers/date';

export function ReleaseItem({ info }) {
  const { label, color } = getTagInfo(info.tag_name);
  return (
    <div className="p-5 …">
      <div className="flex justify-between mb-3">
        <div className="flex gap-2">
          <Chip color={color} …>{label}</Chip>
          <span className="font-mono"> {info.tag_name}</span>
        </div>
        <div className="text-right">
          <div className="text-xs">{formatDateZh(info.published_at)}</div>
          {info.prerelease && <Chip color="warning">预发布</Chip>}
        </div>
      </div>
      {info.name && <h4>…{info.name}</h4>}
      <Markdown content={info.body} className="prose …" />
      {/* …assets + footer… */}
    </div>
  );
}
```

4) In your modal JSX, replace the inline IIFE and date logic with:
```tsx
{middleVersions.map(v => (
  <ReleaseItem key={v.tag_name} info={v} />
))}
```

This keeps all functionality but moves the complex logic out of the render, making the component far easier to read.
</issue_to_address>

### Comment 8
<location> `packages/web/src/components/UpdateButtons.tsx:30` </location>
<code_context>
+            if (status === 'ok') {
+              toast.success('更新成功,正在重启......')
+              await restartRequest({ isPm2: true })
+              await new Promise(resolve => {
+                const interval = setInterval(async () => {
+                  try {
</code_context>

<issue_to_address>
Consider extracting the polling logic into a helper function and consolidating duplicate hooks to simplify the component.

```suggestion
Consider extracting the polling logic into a reusable helper and collapsing duplicate hooks into one. For example:

// utils/server.ts
export async function waitForServerUp(
  timeout = 2000
): Promise<void> {
  while (true) {
    try {
      await request.serverGet('/api/v1/ping')
      return
    } catch {
      await new Promise((r) => setTimeout(r, timeout))
    }
  }
}

// UpdateButtons.tsx
import { waitForServerUp } from '@/utils/server'

export default function UpdateButtons({ handleCloseModal }: UpdateButtonsProps) {
  const [running, setRunning] = useState(false)
  const dialog = useDialog()

  // single ref usage
  const glassRef = useLiquidGlassButton({
    gaussianBlur: 0,
    scale: 20,
    transparency: 0,
  })

  const onUpdate = () => {
    dialog.confirm({
      title: '更新',
      content: '确认更新吗?',
      onConfirm: async () => {
        setRunning(true)
        try {
          const { status } = await request.serverGet<{ status: 'ok' | 'failed' }>(
            '/api/v1/system/update',
            { timeout: 30000 }
          )
          if (status !== 'ok') throw new Error('更新失败')
          toast.success('更新成功,正在重启…')
          await restartRequest({ isPm2: true })
          await waitForServerUp()
          toast.success('重启成功')
          window.location.reload()
        } catch (err: any) {
          toast.error(err.message || '操作失败')
        } finally {
          setRunning(false)
        }
      },
    })
  }

  const applyRef = window.innerWidth <= 768 ? glassRef : undefined

  return (
    <div className="flex gap-2 ml-auto">
      {running && <FullScreenLoader />}
      <Button
        ref={applyRef}
        color="danger"
        variant="flat"
        isDisabled={running}
        onPress={handleCloseModal}
        className="glass-effect"
      >
        关闭
      </Button>
      <Button
        ref={applyRef}
        color="primary"
        variant="flat"
        isDisabled={running}
        onPress={onUpdate}
        className="glass-effect"
      >
        更新
      </Button>
    </div>
  )
}
```

Steps to reduce complexity:
1. Pull polling into `waitForServerUp` to eliminate inline loops.  
2. Merge nested `try/catch` into one block with clear flow.  
3. Use a single `useLiquidGlassButton` hook and conditionally apply its ref.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines 182 to 187
if (typeof fn === 'function') {
setProxyFn(fn)
} else {
// 如果不是函数,保持默认的 url => url 函数
console.warn('testGithub 返回的不是函数,使用默认代理函数')
}
setProxyFnInitialized(true)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Modal可能在代理函数准备好之前获取数据

由于fetch现在立即运行,请确保它仅在proxyFn初始化之后执行——可以通过检查proxyFnInitialized或将此标志传递给UpdateLogModal。否则,可能会意外使用默认URL。

Original comment in English

issue (bug_risk): Modal may fetch before proxy function is ready

Since fetch now runs immediately, ensure it only executes after proxyFn is initialized—either by checking proxyFnInitialized or passing this flag to UpdateLogModal. Otherwise, the default URL may be used unintentionally.

}: UpdateLogModalProps) {
const [isLoadingRelease, setIsLoadingRelease] = useState(false)

const { data: releaseData, error: releaseError, run: fetchRelease } = useRequest(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): fetchRelease回调关闭初始proxyFn

由于proxyFn未包含在依赖项中,因此对其的更新不会反映在fetchRelease中。将proxyFn添加到refreshDeps或在其更改时重新创建请求。

Original comment in English

issue (bug_risk): fetchRelease callback closes over initial proxyFn

Since proxyFn is not included in the dependencies, updates to it won't be reflected in fetchRelease. Add proxyFn to refreshDeps or recreate the request when it changes.

Comment on lines +50 to +54
useEffect(() => {
if (isOpen && !releaseData && !isLoadingRelease) {
fetchRelease()
}
}, [isOpen, releaseData, isLoadingRelease, fetchRelease])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): 获取错误时可能出现无限重试循环

考虑通过检查错误状态或使用标志来避免无限重试,从而防止在失败后重复尝试获取。

Original comment in English

issue (bug_risk): Potential infinite retry loop on fetch errors

Consider preventing repeated fetch attempts after a failure by checking for an error state or using a flag to avoid infinite retries.

Comment on lines +58 to +60
const middleVersions = useMemo(() => {
return updateLogs || []
}, [updateLogs])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: middleVersions的冗余useMemo

您可以直接使用updateLogs并删除不必要的useMemo用于middleVersions

Original comment in English

nitpick: Redundant useMemo for middleVersions

You can use updateLogs directly and remove the unnecessary useMemo for middleVersions.

Comment on lines +72 to +81
<Button
ref={window.innerWidth <= 768 ? liquidGlassButtonRef1 : undefined}
color='danger'
variant='flat'
isDisabled={running}
onPress={handleCloseModal}
className='glass-effect'
>
关闭
</Button>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): 直接使用window.innerWidth可能会破坏SSR并且不具有反应性

这种方法会导致服务器端渲染期间出现错误,并且不会响应视口更改。在使用之前,使用媒体查询钩子或检查window

Original comment in English

issue (bug_risk): Direct window.innerWidth usage may break SSR & is not reactive

This approach will cause errors during server-side rendering and won't respond to viewport changes. Use a media-query hook or check for window before accessing it.

* @param currentCoreVersion 当前 core 版本(纯版本号,如 '1.10.3')
* @returns 更新日志
*/
export const extractUpdateLogs = (releases: GithubRelease[], currentCoreVersion: string): GithubRelease[] => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (complexity): 考虑重构该函数以使用单次映射进行注释、整合排序和更清晰的链式操作。

Suggested change
export const extractUpdateLogs = (releases: GithubRelease[], currentCoreVersion: string): GithubRelease[] => {
// Extract common utilities
const TARGET_PACKAGES = ['core', 'web', 'cli', 'create-karin'] as const;
type PackageType = typeof TARGET_PACKAGES[number];
const getPackageType = (tag: string): PackageType | null => {
if (tag.startsWith('core-v')) return 'core';
if (tag.startsWith('web-v')) return 'web';
if (tag.startsWith('cli-v')) return 'cli';
if (tag.startsWith('create-karin-v') || tag.startsWith('create-v')) return 'create-karin';
return null;
};
const parseTs = (r: GithubRelease) => Date.parse(r.created_at);
const sortByDateDesc = (a: GithubRelease, b: GithubRelease) =>
parseTs(b) - parseTs(a);
export const extractUpdateLogs = (
releases: GithubRelease[],
currentCoreVersion: string
): GithubRelease[] => {
// 1) filter & annotate
const enriched = releases
.map(r => ({ r, type: getPackageType(r.tag_name), ts: parseTs(r) }))
.filter(({ type }) => type && TARGET_PACKAGES.includes(type));
const currentTag = `core-v${currentCoreVersion}`;
const current = enriched.find(({ r }) => r.tag_name === currentTag);
// 2) not found → return all sorted
if (!current) {
return enriched.map(({ r }) => r).sort(sortByDateDesc);
}
// 3) find latest core
const latestCore = enriched
.filter(({ type }) => type === 'core')
.map(({ r }) => r)
.reduce((best, cur) =>
compareVersion(cur.tag_name, best.tag_name) > 0 ? cur : best,
current.r
);
// 4) already up-to-date
if (compareVersion(latestCore.tag_name, currentTag) <= 0) {
return [];
}
// 5) filter by time‐range and sort once
const fromTs = current.ts;
const toTs = Date.parse(latestCore.created_at);
return enriched
.map(({ r, ts }) => ({ r, ts }))
.filter(({ r, ts }) =>
r.tag_name !== currentTag &&
ts > fromTs &&
ts <= toTs
)
.map(({ r }) => r)
.sort(sortByDateDesc);
};
  • 单次传递给map→附加typets,然后对包类型进行一次filter
  • 每次发布调用一次parseTs
  • 将排序合并到sortByDateDesc中。
  • 链式操作(没有嵌套的if/filter块)。
  • 保留所有原始行为。
Original comment in English

issue (complexity): Consider refactoring the function to use a single-pass mapping for annotation, consolidated sorting, and clearer chained operations.

Suggested change
export const extractUpdateLogs = (releases: GithubRelease[], currentCoreVersion: string): GithubRelease[] => {
// Extract common utilities
const TARGET_PACKAGES = ['core', 'web', 'cli', 'create-karin'] as const;
type PackageType = typeof TARGET_PACKAGES[number];
const getPackageType = (tag: string): PackageType | null => {
if (tag.startsWith('core-v')) return 'core';
if (tag.startsWith('web-v')) return 'web';
if (tag.startsWith('cli-v')) return 'cli';
if (tag.startsWith('create-karin-v') || tag.startsWith('create-v')) return 'create-karin';
return null;
};
const parseTs = (r: GithubRelease) => Date.parse(r.created_at);
const sortByDateDesc = (a: GithubRelease, b: GithubRelease) =>
parseTs(b) - parseTs(a);
export const extractUpdateLogs = (
releases: GithubRelease[],
currentCoreVersion: string
): GithubRelease[] => {
// 1) filter & annotate
const enriched = releases
.map(r => ({ r, type: getPackageType(r.tag_name), ts: parseTs(r) }))
.filter(({ type }) => type && TARGET_PACKAGES.includes(type));
const currentTag = `core-v${currentCoreVersion}`;
const current = enriched.find(({ r }) => r.tag_name === currentTag);
// 2) not found → return all sorted
if (!current) {
return enriched.map(({ r }) => r).sort(sortByDateDesc);
}
// 3) find latest core
const latestCore = enriched
.filter(({ type }) => type === 'core')
.map(({ r }) => r)
.reduce((best, cur) =>
compareVersion(cur.tag_name, best.tag_name) > 0 ? cur : best,
current.r
);
// 4) already up-to-date
if (compareVersion(latestCore.tag_name, currentTag) <= 0) {
return [];
}
// 5) filter by time‐range and sort once
const fromTs = current.ts;
const toTs = Date.parse(latestCore.created_at);
return enriched
.map(({ r, ts }) => ({ r, ts }))
.filter(({ r, ts }) =>
r.tag_name !== currentTag &&
ts > fromTs &&
ts <= toTs
)
.map(({ r }) => r)
.sort(sortByDateDesc);
};
  • Single pass to map → attach type and ts, then one filter on package type.
  • One parseTs call per release.
  • Consolidated sort into sortByDateDesc.
  • Chained operations (no nested if/filter blocks).
  • Preserves all original behavior.

)
: (
<>
{middleVersions.map((versionInfo) => (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (complexity): 考虑提取内联标签和日期逻辑,以及发布项目渲染,到单独的辅助函数和一个子组件中,以简化主组件的JSX。

Suggested change
{middleVersions.map((versionInfo) => (
内联IIFE/switch和日期格式化为您的JSX添加了很多噪音。将它们提取到小的纯辅助函数和/或一个`<ReleaseItem />`子组件中。例如:
1) helpers/tag.ts
```ts
export type TagInfo = { label: string; color: 'primary'|'warning'|'secondary'|'success' };
export function getTagInfo(tag: string): TagInfo {
if (tag.includes('web')) return { label: 'WEB 界面', color: 'warning' };
if (tag.includes('cli')) return { label: '命令行工具', color: 'secondary' };
if (tag.includes('create')) return { label: '脚手架', color: 'success' };
// default to core
return { label: '本体', color: 'primary' };
}
  1. helpers/date.ts
export function formatDateZh(dateStr: string): string {
  return new Date(dateStr).toLocaleDateString('zh-CN', {
    year: 'numeric', month: 'short', day: 'numeric',
  });
}
  1. components/ReleaseItem.tsx
import { Chip } from '@heroui/chip';
import Markdown from '@/components/Markdown';
import { getTagInfo } from '@/helpers/tag';
import { formatDateZh } from '@/helpers/date';

export function ReleaseItem({ info }) {
  const { label, color } = getTagInfo(info.tag_name);
  return (
    <div className="p-5 …">
      <div className="flex justify-between mb-3">
        <div className="flex gap-2">
          <Chip color={color} >{label}</Chip>
          <span className="font-mono"> {info.tag_name}</span>
        </div>
        <div className="text-right">
          <div className="text-xs">{formatDateZh(info.published_at)}</div>
          {info.prerelease && <Chip color="warning">预发布</Chip>}
        </div>
      </div>
      {info.name && <h4>{info.name}</h4>}
      <Markdown content={info.body} className="prose …" />
      {/* …assets + footer… */}
    </div>
  );
}
  1. 在您的模态JSX中,将内联IIFE和日期逻辑替换为:
{middleVersions.map(v => (
  <ReleaseItem key={v.tag_name} info={v} />
))}

这保留了所有功能,但将复杂逻辑移出了渲染,使组件更易于阅读。

Original comment in English

issue (complexity): Consider extracting the inline tag and date logic, as well as the release item rendering, into separate helper functions and a sub-component to simplify the main component's JSX.

Suggested change
{middleVersions.map((versionInfo) => (
The inline IIFE/switch and date formatting are adding a lot of noise to your JSX. Extract those into small pure helpers and/or a `<ReleaseItem />` sub-component. For example:
1) helpers/tag.ts
```ts
export type TagInfo = { label: string; color: 'primary'|'warning'|'secondary'|'success' };
export function getTagInfo(tag: string): TagInfo {
if (tag.includes('web')) return { label: 'WEB 界面', color: 'warning' };
if (tag.includes('cli')) return { label: '命令行工具', color: 'secondary' };
if (tag.includes('create')) return { label: '脚手架', color: 'success' };
// default to core
return { label: '本体', color: 'primary' };
}
  1. helpers/date.ts
export function formatDateZh(dateStr: string): string {
  return new Date(dateStr).toLocaleDateString('zh-CN', {
    year: 'numeric', month: 'short', day: 'numeric',
  });
}
  1. components/ReleaseItem.tsx
import { Chip } from '@heroui/chip';
import Markdown from '@/components/Markdown';
import { getTagInfo } from '@/helpers/tag';
import { formatDateZh } from '@/helpers/date';

export function ReleaseItem({ info }) {
  const { label, color } = getTagInfo(info.tag_name);
  return (
    <div className="p-5 …">
      <div className="flex justify-between mb-3">
        <div className="flex gap-2">
          <Chip color={color} >{label}</Chip>
          <span className="font-mono"> {info.tag_name}</span>
        </div>
        <div className="text-right">
          <div className="text-xs">{formatDateZh(info.published_at)}</div>
          {info.prerelease && <Chip color="warning">预发布</Chip>}
        </div>
      </div>
      {info.name && <h4>{info.name}</h4>}
      <Markdown content={info.body} className="prose …" />
      {/* …assets + footer… */}
    </div>
  );
}
  1. In your modal JSX, replace the inline IIFE and date logic with:
{middleVersions.map(v => (
  <ReleaseItem key={v.tag_name} info={v} />
))}

This keeps all functionality but moves the complex logic out of the render, making the component far easier to read.

if (status === 'ok') {
toast.success('更新成功,正在重启......')
await restartRequest({ isPm2: true })
await new Promise(resolve => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (complexity): 考虑将轮询逻辑提取到辅助函数中,并整合重复的钩子以简化组件。

Suggested change
await new Promise(resolve => {
考虑将轮询逻辑提取到可重用的辅助函数中,并将重复的钩子折叠为一个。例如:
// utils/server.ts
export async function waitForServerUp(
timeout = 2000
): Promise<void> {
while (true) {
try {
await request.serverGet('/api/v1/ping')
return
} catch {
await new Promise((r) => setTimeout(r, timeout))
}
}
}
// UpdateButtons.tsx
import { waitForServerUp } from '@/utils/server'
export default function UpdateButtons({ handleCloseModal }: UpdateButtonsProps) {
const [running, setRunning] = useState(false)
const dialog = useDialog()
// single ref usage
const glassRef = useLiquidGlassButton({
gaussianBlur: 0,
scale: 20,
transparency: 0,
})
const onUpdate = () => {
dialog.confirm({
title: '更新',
content: '确认更新吗?',
onConfirm: async () => {
setRunning(true)
try {
const { status } = await request.serverGet<{ status: 'ok' | 'failed' }>(
'/api/v1/system/update',
{ timeout: 30000 }
)
if (status !== 'ok') throw new Error('更新失败')
toast.success('更新成功,正在重启…')
await restartRequest({ isPm2: true })
await waitForServerUp()
toast.success('重启成功')
window.location.reload()
} catch (err: any) {
toast.error(err.message || '操作失败')
} finally {
setRunning(false)
}
},
})
}
const applyRef = window.innerWidth <= 768 ? glassRef : undefined
return (
<div className="flex gap-2 ml-auto">
{running && <FullScreenLoader />}
<Button
ref={applyRef}
color="danger"
variant="flat"
isDisabled={running}
onPress={handleCloseModal}
className="glass-effect"
>
关闭
</Button>
<Button
ref={applyRef}
color="primary"
variant="flat"
isDisabled={running}
onPress={onUpdate}
className="glass-effect"
>
更新
</Button>
</div>
)
}

减少复杂性的步骤:

  1. 将轮询拉入waitForServerUp以消除内联循环。
  2. 将嵌套的try/catch合并为一个具有清晰流程的块。
  3. 使用单个useLiquidGlassButton钩子并有条件地应用其ref。
Original comment in English

issue (complexity): Consider extracting the polling logic into a helper function and consolidating duplicate hooks to simplify the component.

Suggested change
await new Promise(resolve => {
Consider extracting the polling logic into a reusable helper and collapsing duplicate hooks into one. For example:
// utils/server.ts
export async function waitForServerUp(
timeout = 2000
): Promise<void> {
while (true) {
try {
await request.serverGet('/api/v1/ping')
return
} catch {
await new Promise((r) => setTimeout(r, timeout))
}
}
}
// UpdateButtons.tsx
import { waitForServerUp } from '@/utils/server'
export default function UpdateButtons({ handleCloseModal }: UpdateButtonsProps) {
const [running, setRunning] = useState(false)
const dialog = useDialog()
// single ref usage
const glassRef = useLiquidGlassButton({
gaussianBlur: 0,
scale: 20,
transparency: 0,
})
const onUpdate = () => {
dialog.confirm({
title: '更新',
content: '确认更新吗?',
onConfirm: async () => {
setRunning(true)
try {
const { status } = await request.serverGet<{ status: 'ok' | 'failed' }>(
'/api/v1/system/update',
{ timeout: 30000 }
)
if (status !== 'ok') throw new Error('更新失败')
toast.success('更新成功,正在重启…')
await restartRequest({ isPm2: true })
await waitForServerUp()
toast.success('重启成功')
window.location.reload()
} catch (err: any) {
toast.error(err.message || '操作失败')
} finally {
setRunning(false)
}
},
})
}
const applyRef = window.innerWidth <= 768 ? glassRef : undefined
return (
<div className="flex gap-2 ml-auto">
{running && <FullScreenLoader />}
<Button
ref={applyRef}
color="danger"
variant="flat"
isDisabled={running}
onPress={handleCloseModal}
className="glass-effect"
>
关闭
</Button>
<Button
ref={applyRef}
color="primary"
variant="flat"
isDisabled={running}
onPress={onUpdate}
className="glass-effect"
>
更新
</Button>
</div>
)
}

Steps to reduce complexity:

  1. Pull polling into waitForServerUp to eliminate inline loops.
  2. Merge nested try/catch into one block with clear flow.
  3. Use a single useLiquidGlassButton hook and conditionally apply its ref.

@ikenxuan ikenxuan merged commit 180ef2d into main Jun 13, 2025
3 checks passed
@ikenxuan ikenxuan deleted the fix-changeLog branch June 13, 2025 12:13
@github-actions github-actions bot mentioned this pull request Jun 13, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant