From 4cda624dc7fd9edf796481764def090e671083ca Mon Sep 17 00:00:00 2001 From: Script Generator Date: Fri, 26 Jun 2026 11:18:00 +0800 Subject: [PATCH] =?UTF-8?q?fix(=E8=B4=A6=E5=8F=B7):=20=E5=9B=BA=E5=8C=96?= =?UTF-8?q?=E8=B6=85=E7=BA=A7=E7=AE=A1=E7=90=86=E5=91=98=E8=BA=AB=E4=BB=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/app/admin/members/page.tsx | 31 +++++-- apps/web/components/bug/BugCreateModal.tsx | 6 +- apps/web/components/bug/BugDetailDrawer.tsx | 25 ++++-- apps/web/components/bug/BugRow.tsx | 6 +- apps/web/components/bug/BugTab.tsx | 39 ++++++--- apps/web/lib/auth-user.test.ts | 57 +++++++++++++ apps/web/lib/auth-user.ts | 26 ++++++ apps/web/lib/member-system.test.ts | 84 +++++++++++++++++++ apps/web/lib/member-system.ts | 92 +++++++++++++++++++++ apps/web/lib/members.ts | 1 + apps/web/stores/useAuthStore.ts | 81 +++++++++--------- apps/web/stores/useMemberStore.ts | 36 ++++++-- docs/decisions.md | 15 ++++ 13 files changed, 419 insertions(+), 80 deletions(-) create mode 100644 apps/web/lib/auth-user.test.ts create mode 100644 apps/web/lib/auth-user.ts create mode 100644 apps/web/lib/member-system.test.ts create mode 100644 apps/web/lib/member-system.ts diff --git a/apps/web/app/admin/members/page.tsx b/apps/web/app/admin/members/page.tsx index 32bc66c..938c86a 100644 --- a/apps/web/app/admin/members/page.tsx +++ b/apps/web/app/admin/members/page.tsx @@ -3,8 +3,10 @@ import { useEffect, useMemo, useState } from 'react'; import { Plus, Pencil, Trash2, X, ChevronRight, FolderOpen, Settings } from 'lucide-react'; import { useMemberStore } from '@/stores/useMemberStore'; +import { useAuthStore } from '@/stores/useAuthStore'; import { maskPhone, generatePassword } from '@/lib/members'; import type { Member, Department, PasswordRule } from '@/lib/members'; +import { isSystemAdminMember } from '@/lib/member-system'; import { RouteGuard } from '@/components/auth/Guard'; export default function MembersPage() { @@ -17,6 +19,8 @@ export default function MembersPage() { function MembersPageContent() { const { departments, members, roles, passwordRule, fetchMembers, updatePasswordRule, createDepartment, updateDepartment, deleteDepartment, createMember, updateMember, deleteMember } = useMemberStore(); + const currentUser = useAuthStore((s) => s.user); + const refreshUser = useAuthStore((s) => s.refreshUser); const [activeDeptId, setActiveDeptId] = useState(null); const [showMemberModal, setShowMemberModal] = useState(false); const [editingMember, setEditingMember] = useState(null); @@ -169,7 +173,12 @@ function MembersPageContent() {
- + {!isSystemAdminMember(m) && ( + + )} + {isSystemAdminMember(m) && ( + 系统账号 + )}
@@ -191,8 +200,12 @@ function MembersPageContent() { passwordRule={passwordRule} onClose={() => setShowMemberModal(false)} onSubmit={(data) => { - if (editingMember) updateMember(editingMember.id, data); - else createMember(data as any); + if (editingMember) { + updateMember(editingMember.id, data); + if (editingMember.id === currentUser?.id) void refreshUser(); + } else { + createMember(data as any); + } setShowMemberModal(false); }} /> @@ -239,6 +252,7 @@ function MemberModal({ initial, departments, roles, defaultDeptId, passwordRule, const [phone, setPhone] = useState(initial?.phone ?? ''); const [email, setEmail] = useState(initial?.email ?? ''); const [password] = useState(() => initial?.password ?? generatePassword(passwordRule)); + const isSystemMember = initial ? isSystemAdminMember(initial) : false; const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); @@ -254,21 +268,26 @@ function MemberModal({ initial, departments, roles, defaultDeptId, passwordRule,
+ {isSystemMember && ( +
+ 系统内置超级管理员账号不可删除,姓名、部门、角色固定。 +
+ )}
- setName(e.target.value)} required placeholder="请输入姓名" className="h-9 w-full rounded-lg border border-[var(--line)] bg-[var(--bg-card)] px-3 text-[13px] focus:border-[var(--accent)] focus:outline-none" /> + setName(e.target.value)} required disabled={isSystemMember} placeholder="请输入姓名" className="h-9 w-full rounded-lg border border-[var(--line)] bg-[var(--bg-card)] px-3 text-[13px] focus:border-[var(--accent)] focus:outline-none disabled:bg-zinc-50 disabled:text-[var(--ink-soft)]" />
- setDepartmentId(e.target.value)} required disabled={isSystemMember} className="h-9 w-full rounded-lg border border-[var(--line)] bg-[var(--bg-card)] px-3 text-[13px] focus:border-[var(--accent)] focus:outline-none disabled:bg-zinc-50 disabled:text-[var(--ink-soft)]"> {departments.map((d) => )}
- setRoleId(e.target.value)} required disabled={isSystemMember} className="h-9 w-full rounded-lg border border-[var(--line)] bg-[var(--bg-card)] px-3 text-[13px] focus:border-[var(--accent)] focus:outline-none disabled:bg-zinc-50 disabled:text-[var(--ink-soft)]"> {roles.map((r) => )} diff --git a/apps/web/components/bug/BugCreateModal.tsx b/apps/web/components/bug/BugCreateModal.tsx index 327eeaa..232db0e 100644 --- a/apps/web/components/bug/BugCreateModal.tsx +++ b/apps/web/components/bug/BugCreateModal.tsx @@ -8,6 +8,7 @@ import { useDevTaskStore } from '@/stores/useDevTaskStore'; import { useRequirementStore } from '@/stores/useRequirementStore'; import { useMemberStore } from '@/stores/useMemberStore'; import { useAuthStore } from '@/stores/useAuthStore'; +import { resolveMemberDisplayName } from '@/lib/member-system'; import type { Priority } from '@/lib/derive'; import type { BugSeverity } from '@/lib/bug'; @@ -60,6 +61,7 @@ export function BugCreateModal({ testCaseId, onClose }: Props) { const handleSubmit = () => { if (!canSubmit) return; + const operator = user ? resolveMemberDisplayName(user.id, members) : '系统'; createBug({ versionId: tc?.versionId || '', testCaseId, @@ -68,10 +70,10 @@ export function BugCreateModal({ testCaseId, onClose }: Props) { description: description.trim(), severity, priority, - reportedBy: user?.name || '系统', + reportedBy: operator, assigneeId, images: images.length > 0 ? images : undefined, - }, user?.name || '系统'); + }, operator); onClose(); }; diff --git a/apps/web/components/bug/BugDetailDrawer.tsx b/apps/web/components/bug/BugDetailDrawer.tsx index fdb5b52..03c7e4a 100644 --- a/apps/web/components/bug/BugDetailDrawer.tsx +++ b/apps/web/components/bug/BugDetailDrawer.tsx @@ -10,6 +10,7 @@ import { useMemberStore } from '@/stores/useMemberStore'; import { useAuthStore } from '@/stores/useAuthStore'; import { BUG_ALLOWED_TRANSITIONS, BUG_STATUS_LABEL, BUG_SEVERITY_LABEL, BUG_SEVERITY_COLOR } from '@/lib/bug'; import { formatDateTime } from '@/lib/format'; +import { isMemberReference, resolveMemberDisplayName } from '@/lib/member-system'; import type { BugStatus } from '@/lib/bug'; const LOG_ACTION_LABEL: Record = { @@ -31,7 +32,7 @@ export function BugDetailDrawer({ bugId, onClose, contextLabel }: Props) { const { requirements } = useRequirementStore(); const { members } = useMemberStore(); const user = useAuthStore((s) => s.user); - const operator = user?.name || '系统'; + const operator = user ? resolveMemberDisplayName(user.id, members) : '系统'; const bug = bugs.find((b) => b.id === bugId); if (!bug) return null; @@ -39,6 +40,12 @@ export function BugDetailDrawer({ bugId, onClose, contextLabel }: Props) { const tc = testCases.find((c) => c.id === bug.testCaseId); const requirement = tc ? requirements.find((r) => r.id === tc.requirementId) : null; const nextStatuses = BUG_ALLOWED_TRANSITIONS[bug.status]; + const currentMember = user ? members.find((m) => m.id === user.id) : undefined; + const assigneeName = resolveMemberDisplayName(bug.assigneeId, members); + const reporterName = resolveMemberDisplayName(bug.reportedBy, members); + const isCurrentAssignee = currentMember + ? isMemberReference(bug.assigneeId, currentMember) + : operator === assigneeName; const [resolution, setResolution] = useState(bug.resolution || ''); const [showResolutionInput, setShowResolutionInput] = useState(false); @@ -110,7 +117,7 @@ export function BugDetailDrawer({ bugId, onClose, contextLabel }: Props) { {bug.priority}
- {nextStatuses.length > 0 && !showResolutionInput && operator === bug.assigneeId && ( + {nextStatuses.length > 0 && !showResolutionInput && isCurrentAssignee && (
{nextStatuses.map((s) => ( @@ -125,8 +132,8 @@ export function BugDetailDrawer({ bugId, onClose, contextLabel }: Props) { )}
)} - {nextStatuses.length > 0 && !showResolutionInput && operator !== bug.assigneeId && ( -
当前修复人为 {bug.assigneeId},仅修复人可操作
+ {nextStatuses.length > 0 && !showResolutionInput && !isCurrentAssignee && ( +
当前修复人为 {assigneeName},仅修复人可操作
)} {showResolutionInput && ( @@ -144,7 +151,7 @@ export function BugDetailDrawer({ bugId, onClose, contextLabel }: Props) {
转交给:
setTransferRemark(e.target.value)} placeholder="转交备注(可选)" className="h-8 w-full rounded-lg border border-[var(--line)] px-3 text-[12px] focus:border-[var(--accent)] focus:outline-none" />
@@ -159,8 +166,8 @@ export function BugDetailDrawer({ bugId, onClose, contextLabel }: Props) {
基本信息
-
提交人:{bug.reportedBy}
-
修复人:{bug.assigneeId}
+
提交人:{reporterName}
+
修复人:{assigneeName}
提交:{formatDateTime(bug.createdAt)}
{bug.resolvedAt &&
修复:{formatDateTime(bug.resolvedAt)}
} {bug.closedAt &&
关闭:{formatDateTime(bug.closedAt)}
} @@ -202,10 +209,10 @@ export function BugDetailDrawer({ bugId, onClose, contextLabel }: Props) {
{formatDateTime(log.createdAt)}
- {log.operator} + {resolveMemberDisplayName(log.operator, members)} {LOG_ACTION_LABEL[log.action] || log.action} {log.fromValue && log.toValue && ( - {log.fromValue} → {log.toValue} + {resolveMemberDisplayName(log.fromValue, members)} → {resolveMemberDisplayName(log.toValue, members)} )} {log.remark && ({log.remark})}
diff --git a/apps/web/components/bug/BugRow.tsx b/apps/web/components/bug/BugRow.tsx index 73bed67..e9d2ee1 100644 --- a/apps/web/components/bug/BugRow.tsx +++ b/apps/web/components/bug/BugRow.tsx @@ -4,6 +4,8 @@ import { memo } from 'react'; import { BugStatusBadge } from './BugStatusBadge'; import { BUG_SEVERITY_LABEL, BUG_SEVERITY_COLOR, getBugActualHours } from '@/lib/bug'; import { formatWorkHours } from '@/lib/work-hours'; +import { resolveMemberDisplayName } from '@/lib/member-system'; +import { useMemberStore } from '@/stores/useMemberStore'; import type { Bug } from '@/lib/bug'; interface Props { @@ -21,6 +23,8 @@ const PRIORITY_DOT: Record = { function BugRowImpl({ bug, testCaseNo, onClick }: Props) { const actualHours = getBugActualHours(bug); + const members = useMemberStore((s) => s.members); + const assigneeName = resolveMemberDisplayName(bug.assigneeId, members); return (
@@ -32,7 +36,7 @@ function BugRowImpl({ bug, testCaseNo, onClick }: Props) { {formatWorkHours(actualHours)} )} {testCaseNo && {testCaseNo}} - {bug.assigneeId} + {assigneeName}
); } diff --git a/apps/web/components/bug/BugTab.tsx b/apps/web/components/bug/BugTab.tsx index 39dd482..f3dfeb5 100644 --- a/apps/web/components/bug/BugTab.tsx +++ b/apps/web/components/bug/BugTab.tsx @@ -6,6 +6,7 @@ import { useBugStore } from '@/stores/useBugStore'; import { useTestCaseStore } from '@/stores/useTestCaseStore'; import { useRequirementStore } from '@/stores/useRequirementStore'; import { useAuthStore } from '@/stores/useAuthStore'; +import { useMemberStore } from '@/stores/useMemberStore'; import { BugRow } from './BugRow'; import { BugDetailDrawer } from './BugDetailDrawer'; import { Pagination, usePagination } from '@/components/Pagination'; @@ -13,6 +14,7 @@ import { SearchInput, matchTitleOrNo } from '@/components/SearchInput'; import { useDebouncedValue } from '@/hooks/useDebouncedValue'; import { BUG_STATUS_LABEL, BUG_SEVERITY_LABEL, bugIntervals } from '@/lib/bug'; import { formatWorkHours, calcTwoMetrics } from '@/lib/work-hours'; +import { isMemberReference, resolveMemberDisplayName } from '@/lib/member-system'; import type { BugStatus, BugSeverity } from '@/lib/bug'; interface Props { @@ -25,6 +27,7 @@ export function BugTab({ versionId, requirementIds }: Props) { const { testCases, fetchTestCases } = useTestCaseStore(); const { requirements } = useRequirementStore(); const user = useAuthStore((s) => s.user); + const members = useMemberStore((s) => s.members); useEffect(() => { fetchBugs(); }, [fetchBugs]); useEffect(() => { fetchTestCases(); }, [fetchTestCases]); @@ -48,26 +51,36 @@ export function BugTab({ versionId, requirementIds }: Props) { const [keyword, setKeyword] = useState(''); const debouncedKeyword = useDebouncedValue(keyword, 300); + const [selectedBugId, setSelectedBugId] = useState(null); + + const testCaseMap = useMemo(() => new Map(testCases.map((c) => [c.id, c])), [testCases]); + const currentMember = useMemo(() => members.find((m) => m.id === user?.id), [members, user?.id]); + const currentUserName = currentMember?.name ?? user?.name; + + const assignees = useMemo(() => { + const names = new Set(versionBugs.map((b) => resolveMemberDisplayName(b.assigneeId, members))); + return Array.from(names); + }, [versionBugs, members]); + const filteredBugs = useMemo(() => { let result = versionBugs; - if (filterAssignee) result = result.filter((b) => b.assigneeId === filterAssignee); + if (filterAssignee === '__me') { + if (currentMember) { + result = result.filter((b) => isMemberReference(b.assigneeId, currentMember)); + } else { + result = []; + } + } else if (filterAssignee) { + result = result.filter((b) => resolveMemberDisplayName(b.assigneeId, members) === filterAssignee); + } if (filterStatus) result = result.filter((b) => b.status === filterStatus); if (filterSeverity) result = result.filter((b) => b.severity === filterSeverity); if (debouncedKeyword) result = result.filter((b) => matchTitleOrNo({ title: b.title, no: b.bugNo }, debouncedKeyword)); return result; - }, [versionBugs, filterAssignee, filterStatus, filterSeverity, debouncedKeyword]); + }, [versionBugs, filterAssignee, filterStatus, filterSeverity, debouncedKeyword, currentMember, members]); const { paged, page, setPage, total, pageSize, setPageSize } = usePagination(filteredBugs, 20); - const [selectedBugId, setSelectedBugId] = useState(null); - - const testCaseMap = useMemo(() => new Map(testCases.map((c) => [c.id, c])), [testCases]); - - const assignees = useMemo(() => { - const names = new Set(versionBugs.map((b) => b.assigneeId)); - return Array.from(names); - }, [versionBugs]); - const hasFilter = !!(filterAssignee || filterStatus || filterSeverity || debouncedKeyword); return ( @@ -94,8 +107,8 @@ export function BugTab({ versionId, requirementIds }: Props) { { setKeyword(v); setPage(1); }} placeholder="搜索标题/编号" />