✨ feat(vscode): 工具栏发布按钮与下拉菜单

Claude-Session: https://claude.ai/code/session_01HTMgwaE3woqM5yvsHiuSAa
This commit is contained in:
cruldra committed 2026-08-31 23:43:07 +08:00
1 parent b363e08ef9
commit d04048148a
6 files changed
+120 -121

No files matched your search

@@ -1,4 +1,6 @@
import { GitCommit, GitMerge, ListPlus, Loader2, Lock, RefreshCw, Settings, Unlock, User, Users } from 'lucide-react'
import type { ReleaseAction, ReleaseStatus } from '../hooks/useIssues'
import { GitCommit, ListPlus, Loader2, Lock, RefreshCw, Rocket, Settings, Unlock, User, Users } from 'lucide-react'
import { useEffect, useRef, useState } from 'react'
interface Props {
onRefresh: () => void
@@ -15,24 +17,9 @@ interface Props {
*/
hasChanges: boolean
onCommit: () => void
/**
* Commits the remote auto-build branch is behind the remote dev branch.
* Combined with `branchSyncDisabled` to compute the button's disabled
* state.
*/
branchSyncBehind: number
branchSyncRunning: boolean
/**
* True when sync is structurally unavailable (same-branch / fetch fail
* / not a repo / …). Disables the button regardless of `branchSyncBehind`.
*/
branchSyncDisabled: boolean
/**
* Hover tooltip, already includes branch names + behind count or the
* unavailable reason.
*/
branchSyncTitle: string
onSyncBranch: () => void
/** 最近一次发布状态;首次消息到达前为 undefined,按钮显示「正在检查」并禁用。 */
release: ReleaseStatus | undefined
onRelease: (action: ReleaseAction) => void
/**
* Whether the workspace's `.env*` files are currently chmod-locked (444).
* Icon flips between Lock and Unlock based on this.
@@ -63,6 +50,19 @@ interface Props {
onImportYouTrack: () => void
}
const MENU_ITEM_CLASS = 'w-full px-3 py-1 text-left hover:bg-[var(--vscode-menu-selectionBackground)] hover:text-[var(--vscode-menu-selectionForeground)] disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:bg-transparent disabled:hover:text-inherit'
function releaseTitle(release: ReleaseStatus | undefined): string {
if (!release)
return '正在检查发布状态…'
if (release.unavailable)
return release.unavailable
if (!release.current)
return '尚无发布点 · 点击切第一个发布分支'
const lead = release.unreleased > 0 ? `${release.devBranch} 领先 ${release.unreleased} 个提交` : '已全部发布'
return `正式服发布点 ${release.current} · ${lead}`
}
export function PanelHeader({
onRefresh,
scope,
@@ -71,11 +71,8 @@ export function PanelHeader({
commitRunning,
hasChanges,
onCommit,
branchSyncBehind,
branchSyncRunning,
branchSyncDisabled,
branchSyncTitle,
onSyncBranch,
release,
onRelease,
envLocked,
envFileCount,
envLockRunning,
@@ -85,27 +82,80 @@ export function PanelHeader({
onImportYouTrack,
}: Props) {
const showCommit = hasChanges || commitRunning
// Sync button is *always* rendered so the user can see status (vs the
// commit button which auto-hides on a clean tree). Disabled when nothing
// to push, unavailable, or already syncing.
const syncDisabled = branchSyncRunning || branchSyncDisabled || branchSyncBehind <= 0
// Env-lock button is always visible; disabled while a chmod batch is in
// flight or when the workspace has no .env files to act on.
const envLockDisabled = envLockRunning || envFileCount === 0
const [releaseMenuOpen, setReleaseMenuOpen] = useState(false)
const menuRef = useRef<HTMLDivElement>(null)
useEffect(() => {
if (!releaseMenuOpen)
return
const onDown = (e: MouseEvent): void => {
if (!menuRef.current?.contains(e.target as Node))
setReleaseMenuOpen(false)
}
document.addEventListener('mousedown', onDown)
return () => document.removeEventListener('mousedown', onDown)
}, [releaseMenuOpen])
const title = releaseTitle(release)
// 发布按钮常驻,让用户随时看到发布点状态;结构性不可用或动作进行中禁用。
const releaseDisabled = !release || !!release.unavailable || release.running
const unreleased = release?.unreleased ?? 0
function pickRelease(action: ReleaseAction): void {
setReleaseMenuOpen(false)
onRelease(action)
}
return (
<div className="flex h-8 shrink-0 items-center justify-end gap-1 border-b border-[var(--vscode-panel-border)] px-2">
<button
type="button"
onClick={onSyncBranch}
disabled={syncDisabled}
title={branchSyncTitle}
aria-label={branchSyncTitle}
className="flex h-6 w-6 items-center justify-center rounded hover:bg-[var(--vscode-toolbar-hoverBackground)] disabled:cursor-not-allowed disabled:opacity-50"
>
{branchSyncRunning
? <Loader2 size={14} className="animate-spin" />
: <GitMerge size={14} />}
</button>
<div ref={menuRef} className="relative">
<button
type="button"
onClick={() => setReleaseMenuOpen(open => !open)}
disabled={releaseDisabled}
title={title}
aria-label={title}
aria-haspopup="menu"
aria-expanded={releaseMenuOpen}
className="relative flex h-6 w-6 items-center justify-center rounded hover:bg-[var(--vscode-toolbar-hoverBackground)] disabled:cursor-not-allowed disabled:opacity-50"
>
{release?.running
? <Loader2 size={14} className="animate-spin" />
: <Rocket size={14} />}
{unreleased > 0 && (
<span className="absolute -right-1 -top-1 rounded-full bg-[var(--vscode-badge-background)] px-1 text-[9px] leading-3 text-[var(--vscode-badge-foreground)]">
{unreleased > 99 ? '99+' : unreleased}
</span>
)}
</button>
{releaseMenuOpen && release && (
<div
role="menu"
className="absolute right-0 top-7 z-20 min-w-44 rounded border border-[var(--vscode-menu-border)] bg-[var(--vscode-menu-background)] py-1 text-xs text-[var(--vscode-menu-foreground)] shadow"
>
<button type="button" role="menuitem" className={MENU_ITEM_CLASS} onClick={() => pickRelease('cut')}>
{`切新发布(${release.nextBranch})`}
</button>
<button
type="button"
role="menuitem"
className={MENU_ITEM_CLASS}
disabled={!release.current}
onClick={() => pickRelease('cherry-pick')}
>
{release.current ? `摘修复到 ${release.current}` : '摘修复(尚无发布分支)'}
</button>
{release.jenkinsConfigured && (
<button type="button" role="menuitem" className={MENU_ITEM_CLASS} onClick={() => pickRelease('deploy')}>
部署到正式服…
</button>
)}
</div>
)}
</div>
<button
type="button"
onClick={onToggleEnvLock}