From bc1ac95c8bddb5006ad0d89fc483d7ec77df7c72 Mon Sep 17 00:00:00 2001 From: FEnjoyit <1196525839@qq.com> Date: Thu, 10 Sep 2026 15:00:37 +0800 Subject: [PATCH 1/7] =?UTF-8?q?fix(api):=20=E6=98=8E=E7=A1=AE=E6=88=90?= =?UTF-8?q?=E5=91=98=E4=BB=BB=E6=9C=9F=E7=94=9F=E5=91=BD=E5=91=A8=E6=9C=9F?= =?UTF-8?q?=E7=BA=A6=E6=9D=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/openapi.yaml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/api/openapi.yaml b/api/openapi.yaml index 340a4483..cd77ce9f 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -1036,7 +1036,7 @@ paths: /api/v1/clubs/{clubId}/members: get: summary: 查询社团成员与干部任期记录 - description: 系统管理员、社团管理员、本社团负责人、干部、成员和指导老师可以只读查看成员及任期;默认仅返回当前有效记录,可选择包含历史记录,并可按届、部门或小组筛选。社团管理员不参与内部任期维护。 + description: 系统管理员、社团管理员、本社团负责人、干部、成员和指导老师可以只读查看成员及任期;默认返回状态有效且尚未到期的当前或未来任期,可选择包含历史记录,并可按届、部门或小组筛选。社团管理员不参与内部任期维护。 operationId: getClubMembers parameters: - $ref: "#/components/parameters/Page" @@ -1304,7 +1304,7 @@ paths: /api/v1/clubs/{clubId}/members/terms: post: summary: 新增社团成员或干部任期 - description: 系统管理员、本社团负责人或指导老师可以为成员新增职位任期;新增任期时可关闭该成员原有效任期以保留历史记录。 + description: 系统管理员、本社团负责人或指导老师可以为成员新增职位任期;新增任期时可关闭该成员此前开始且与新任期重叠的有效任期以保留历史记录。同一成员在同一社团不得重复创建同名任期,选择关闭旧任期时也不得用更早或同日起始的新记录覆盖已有任期。 operationId: createClubMemberTerm parameters: - name: clubId @@ -1332,7 +1332,7 @@ paths: "404": description: 当前用户、社团或目标成员不存在 "409": - description: 社团状态不允许维护 + description: 社团状态不允许维护,或目标成员存在重复、同日起始或逆序重叠任期 /api/v1/clubs/{clubId}/members/{memberId}: patch: @@ -6639,7 +6639,7 @@ components: properties: code: type: string - pattern: '^(VALIDATION_ERROR|UNAUTHORIZED|FORBIDDEN|NOT_FOUND|CONFLICT|PAYLOAD_TOO_LARGE|RATE_LIMITED|SERVICE_UNAVAILABLE|INTERNAL_ERROR|REQUEST_FAILED)$' + pattern: "^(VALIDATION_ERROR|UNAUTHORIZED|FORBIDDEN|NOT_FOUND|CONFLICT|PAYLOAD_TOO_LARGE|RATE_LIMITED|SERVICE_UNAVAILABLE|INTERNAL_ERROR|REQUEST_FAILED)$" description: >- 固定的 HTTP 错误类别:VALIDATION_ERROR、UNAUTHORIZED、FORBIDDEN、NOT_FOUND、CONFLICT、 PAYLOAD_TOO_LARGE、RATE_LIMITED、SERVICE_UNAVAILABLE、INTERNAL_ERROR、REQUEST_FAILED。 @@ -7319,7 +7319,8 @@ components: description: 该话题的回复列表;回复对象的该字段为空数组。 items: $ref: "#/components/schemas/ForumPost" - required: [id, clubId, userId, content, isTop, postStatus, createdAt, replies] + required: + [id, clubId, userId, content, isTop, postStatus, createdAt, replies] CreateForumPostRequest: type: object From cf61f8e0dd4ac829ac5831e040b0e4e46bf9426a Mon Sep 17 00:00:00 2001 From: FEnjoyit <199337229+FEnjoyit@users.noreply.github.com> Date: Thu, 10 Sep 2026 07:01:50 +0000 Subject: [PATCH 2/7] =?UTF-8?q?chore(api):=20=E4=BB=8E=20openapi.yaml=20?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E7=94=9F=E6=88=90=20API=20=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/api/apis/DefaultApi.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/api/apis/DefaultApi.ts b/frontend/src/api/apis/DefaultApi.ts index fd0b072b..7b655271 100644 --- a/frontend/src/api/apis/DefaultApi.ts +++ b/frontend/src/api/apis/DefaultApi.ts @@ -4462,7 +4462,7 @@ export class DefaultApi extends runtime.BaseAPI { } /** - * 系统管理员、本社团负责人或指导老师可以为成员新增职位任期;新增任期时可关闭该成员原有效任期以保留历史记录。 + * 系统管理员、本社团负责人或指导老师可以为成员新增职位任期;新增任期时可关闭该成员此前开始且与新任期重叠的有效任期以保留历史记录。同一成员在同一社团不得重复创建同名任期,选择关闭旧任期时也不得用更早或同日起始的新记录覆盖已有任期。 * 新增社团成员或干部任期 */ async createClubMemberTermRaw( @@ -4478,7 +4478,7 @@ export class DefaultApi extends runtime.BaseAPI { } /** - * 系统管理员、本社团负责人或指导老师可以为成员新增职位任期;新增任期时可关闭该成员原有效任期以保留历史记录。 + * 系统管理员、本社团负责人或指导老师可以为成员新增职位任期;新增任期时可关闭该成员此前开始且与新任期重叠的有效任期以保留历史记录。同一成员在同一社团不得重复创建同名任期,选择关闭旧任期时也不得用更早或同日起始的新记录覆盖已有任期。 * 新增社团成员或干部任期 */ async createClubMemberTerm( @@ -7569,7 +7569,7 @@ export class DefaultApi extends runtime.BaseAPI { } /** - * 系统管理员、社团管理员、本社团负责人、干部、成员和指导老师可以只读查看成员及任期;默认仅返回当前有效记录,可选择包含历史记录,并可按届、部门或小组筛选。社团管理员不参与内部任期维护。 + * 系统管理员、社团管理员、本社团负责人、干部、成员和指导老师可以只读查看成员及任期;默认返回状态有效且尚未到期的当前或未来任期,可选择包含历史记录,并可按届、部门或小组筛选。社团管理员不参与内部任期维护。 * 查询社团成员与干部任期记录 */ async getClubMembersRaw( @@ -7585,7 +7585,7 @@ export class DefaultApi extends runtime.BaseAPI { } /** - * 系统管理员、社团管理员、本社团负责人、干部、成员和指导老师可以只读查看成员及任期;默认仅返回当前有效记录,可选择包含历史记录,并可按届、部门或小组筛选。社团管理员不参与内部任期维护。 + * 系统管理员、社团管理员、本社团负责人、干部、成员和指导老师可以只读查看成员及任期;默认返回状态有效且尚未到期的当前或未来任期,可选择包含历史记录,并可按届、部门或小组筛选。社团管理员不参与内部任期维护。 * 查询社团成员与干部任期记录 */ async getClubMembers( From d6c955cec66e4b341f5b502f3af138fef52b1c60 Mon Sep 17 00:00:00 2001 From: FEnjoyit <1196525839@qq.com> Date: Thu, 10 Sep 2026 15:02:10 +0800 Subject: [PATCH 3/7] =?UTF-8?q?fix(club):=20=E9=98=BB=E6=AD=A2=E5=BC=82?= =?UTF-8?q?=E5=B8=B8=E6=88=90=E5=91=98=E4=BB=BB=E6=9C=9F=E8=A6=86=E7=9B=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend.Tests/ClubMemberTermEndpointTests.cs | 225 +++++++++++++++++++ backend/Controllers/ClubsController.cs | 29 ++- 2 files changed, 249 insertions(+), 5 deletions(-) create mode 100644 backend.Tests/ClubMemberTermEndpointTests.cs diff --git a/backend.Tests/ClubMemberTermEndpointTests.cs b/backend.Tests/ClubMemberTermEndpointTests.cs new file mode 100644 index 00000000..ad2facd4 --- /dev/null +++ b/backend.Tests/ClubMemberTermEndpointTests.cs @@ -0,0 +1,225 @@ +using System.Net; +using System.Net.Http.Headers; +using System.Net.Http.Json; +using System.Text.Json; +using ClubHub.Api.Data; +using ClubHub.Api.Data.Entities; +using ClubHub.Api.Services; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace ClubHub.Api.Tests; + +public sealed class ClubMemberTermEndpointTests +{ + [Fact] + public async Task GetMembers_DefaultQueryIncludesFutureActiveTerm() + { + await using var factory = new ClubHubWebApplicationFactory(); + var dates = FutureTermDates(2); + using var client = await SeedClubAsync(factory, dates.Start, dates.End, "未来任期"); + + using var response = await client.GetAsync("/api/v1/clubs/830002/members"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + using var document = JsonDocument.Parse(await response.Content.ReadAsStringAsync()); + var member = Assert.Single(document.RootElement.EnumerateArray()); + Assert.Equal("active", member.GetProperty("memberStatus").GetString()); + Assert.Equal("未来任期", member.GetProperty("termName").GetString()); + Assert.False(member.GetProperty("isCurrent").GetBoolean()); + } + + [Fact] + public async Task CreateMemberTerm_DuplicateTermReturnsConflictWithoutEndingExistingTerm() + { + await using var factory = new ClubHubWebApplicationFactory(); + var dates = FutureTermDates(2); + using var client = await SeedClubAsync(factory, dates.Start, dates.End, "2030-2031学年"); + + using var response = await CreateTermAsync( + client, + "2030-2031学年", + dates.Start, + dates.End); + + Assert.Equal(HttpStatusCode.Conflict, response.StatusCode); + await AssertExistingTermUnchangedAsync(factory, dates.Start, dates.End); + } + + [Fact] + public async Task CreateMemberTerm_EarlierThanExistingActiveTermReturnsConflict() + { + await using var factory = new ClubHubWebApplicationFactory(); + var existingDates = FutureTermDates(3); + var requestedDates = FutureTermDates(2); + using var client = await SeedClubAsync( + factory, + existingDates.Start, + existingDates.End, + "较晚任期"); + + using var response = await CreateTermAsync( + client, + "较早任期", + requestedDates.Start, + requestedDates.End); + + Assert.Equal(HttpStatusCode.Conflict, response.StatusCode); + await AssertExistingTermUnchangedAsync(factory, existingDates.Start, existingDates.End); + } + + [Fact] + public async Task CreateMemberTerm_SameStartWithDifferentNameReturnsConflict() + { + await using var factory = new ClubHubWebApplicationFactory(); + var dates = FutureTermDates(2); + using var client = await SeedClubAsync(factory, dates.Start, dates.End, "原任期"); + + using var response = await CreateTermAsync( + client, + "同日起始的新任期", + dates.Start, + dates.End); + + Assert.Equal(HttpStatusCode.Conflict, response.StatusCode); + await AssertExistingTermUnchangedAsync(factory, dates.Start, dates.End); + } + + [Fact] + public async Task CreateMemberTerm_LaterTermClosesEarlierOverlappingTerm() + { + await using var factory = new ClubHubWebApplicationFactory(); + var existingStart = new DateTime(DateTime.UtcNow.Year, 7, 1); + var requestedDates = FutureTermDates(2); + var existingEnd = requestedDates.End; + using var client = await SeedClubAsync( + factory, + existingStart, + existingEnd, + "原任期"); + + using var response = await CreateTermAsync( + client, + "新任期", + requestedDates.Start, + requestedDates.End); + + Assert.Equal(HttpStatusCode.Created, response.StatusCode); + await using var scope = factory.Services.CreateAsyncScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var terms = await db.ClubMembers + .Where(member => member.ClubId == 830002 && member.UserId == 830003) + .OrderBy(member => member.TermStart) + .ToListAsync(); + Assert.Equal(2, terms.Count); + Assert.Equal("ended", terms[0].MemberStatus); + Assert.Equal(requestedDates.Start.AddDays(-1), terms[0].TermEnd); + Assert.Equal("active", terms[1].MemberStatus); + Assert.Equal(requestedDates.Start, terms[1].TermStart); + Assert.Equal(requestedDates.End, terms[1].TermEnd); + } + + private static async Task SeedClubAsync( + ClubHubWebApplicationFactory factory, + DateTime existingStart, + DateTime existingEnd, + string existingTermName) + { + await using var scope = factory.Services.CreateAsyncScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var now = DateTime.UtcNow; + var principal = new User + { + UserId = 830001, + Username = "member-term-principal", + PasswordHash = "unused", + RealName = "任期测试负责人", + AccountStatus = "normal", + CreatedAt = now + }; + db.Users.AddRange( + principal, + new User + { + UserId = 830003, + Username = "member-term-target", + PasswordHash = "unused", + RealName = "任期测试成员", + AccountStatus = "normal", + CreatedAt = now + }); + db.Clubs.Add(new Club + { + ClubId = 830002, + ClubName = "任期回归测试社团", + PresidentUserId = principal.UserId, + AuditStatus = "approved", + ClubStatus = "active", + CreatedAt = now + }); + db.ClubMembers.Add(new ClubMember + { + MemberId = 830004, + ClubId = 830002, + UserId = 830003, + PositionName = "成员", + TermName = existingTermName, + TermStart = existingStart, + TermEnd = existingEnd, + MemberStatus = "active", + JoinAt = now, + ContributionScore = 0 + }); + await db.SaveChangesAsync(); + + var token = scope.ServiceProvider + .GetRequiredService() + .CreateToken(principal); + var client = factory.CreateClient(); + client.DefaultRequestHeaders.Authorization = + new AuthenticationHeaderValue("Bearer", token); + return client; + } + + private static async Task CreateTermAsync( + HttpClient client, + string termName, + DateTime termStart, + DateTime termEnd) + { + return await client.PostAsJsonAsync( + "/api/v1/clubs/830002/members/terms", + new + { + userId = 830003, + positionName = "成员", + termName, + termStart, + termEnd, + memberStatus = "active", + contributionScore = 0, + closeCurrentTerm = true + }); + } + + private static async Task AssertExistingTermUnchangedAsync( + ClubHubWebApplicationFactory factory, + DateTime expectedStart, + DateTime expectedEnd) + { + await using var scope = factory.Services.CreateAsyncScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var term = Assert.Single(await db.ClubMembers + .Where(member => member.ClubId == 830002 && member.UserId == 830003) + .ToListAsync()); + Assert.Equal("active", term.MemberStatus); + Assert.Equal(expectedStart, term.TermStart); + Assert.Equal(expectedEnd, term.TermEnd); + } + + private static (DateTime Start, DateTime End) FutureTermDates(int yearsAhead) + { + var startYear = DateTime.UtcNow.Year + yearsAhead; + return (new DateTime(startYear, 7, 1), new DateTime(startYear + 1, 6, 30)); + } +} diff --git a/backend/Controllers/ClubsController.cs b/backend/Controllers/ClubsController.cs index f97f2f4d..f99a2bd9 100644 --- a/backend/Controllers/ClubsController.cs +++ b/backend/Controllers/ClubsController.cs @@ -1050,7 +1050,6 @@ public async Task GetMembers( { query = query.Where(cm => (cm.MemberStatus == null || cm.MemberStatus == MemberActive) && - (cm.TermStart == null || cm.TermStart <= today) && (cm.TermEnd == null || cm.TermEnd >= today)); } @@ -1185,6 +1184,18 @@ public async Task CreateMemberTerm(int clubId, [FromBody] CreateC var termStart = req.TermStart.Date; var termEnd = req.TermEnd?.Date; var memberStatus = ToMemberStatus(req.MemberStatus) ?? MemberActive; + var termName = req.TermName.Trim(); + var normalizedTermName = termName.ToUpperInvariant(); + + var hasDuplicateTerm = await _db.ClubMembers.AnyAsync(cm => + cm.ClubId == clubId && + cm.UserId == req.UserId && + cm.TermName != null && + cm.TermName.Trim().ToUpper() == normalizedTermName); + if (hasDuplicateTerm) + { + return Conflict(new { message = "该成员在本社团已存在同名任期,请编辑原记录。" }); + } if (req.CloseCurrentTerm ?? true) { @@ -1197,12 +1208,20 @@ public async Task CreateMemberTerm(int clubId, [FromBody] CreateC (cm.TermEnd == null || cm.TermEnd >= termStart)) .ToListAsync(); + if (activeTerms.Any(activeTerm => + activeTerm.TermStart is not null && + activeTerm.TermStart.Value.Date >= termStart)) + { + return Conflict(new + { + message = "已有有效任期与新任期同日或更晚开始,请调整日期或编辑原记录。" + }); + } + foreach (var activeTerm in activeTerms) { activeTerm.MemberStatus = MemberEnded; - activeTerm.TermEnd = activeTerm.TermStart is not null && closeDate < activeTerm.TermStart.Value.Date - ? activeTerm.TermStart.Value.Date - : closeDate; + activeTerm.TermEnd = closeDate; } } @@ -1211,7 +1230,7 @@ public async Task CreateMemberTerm(int clubId, [FromBody] CreateC ClubId = clubId, UserId = req.UserId, PositionName = req.PositionName.Trim(), - TermName = req.TermName.Trim(), + TermName = termName, TermStart = termStart, TermEnd = termEnd, MemberStatus = memberStatus, From 985d142ee108a818acdd71be36a2e870664fa5b0 Mon Sep 17 00:00:00 2001 From: FEnjoyit <1196525839@qq.com> Date: Thu, 10 Sep 2026 15:04:08 +0800 Subject: [PATCH 4/7] =?UTF-8?q?fix(frontend):=20=E4=BF=AE=E6=AD=A3?= =?UTF-8?q?=E6=88=90=E5=91=98=E4=BB=BB=E6=9C=9F=E4=B8=8E=E6=8D=A2=E5=B1=8A?= =?UTF-8?q?=E5=90=8D=E5=86=8C=E4=BA=A4=E4=BA=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/defenseBusinessRules.test.ts | 14 +++ frontend/src/views/ClubList.vue | 125 +++++++--------------- 2 files changed, 54 insertions(+), 85 deletions(-) diff --git a/frontend/src/defenseBusinessRules.test.ts b/frontend/src/defenseBusinessRules.test.ts index f0e9485b..eab1eb7d 100644 --- a/frontend/src/defenseBusinessRules.test.ts +++ b/frontend/src/defenseBusinessRules.test.ts @@ -56,4 +56,18 @@ describe("答辩业务入口规则", () => { expect(clubListSource).toContain('message: "请填写材料链接或归档编号"'); expect(clubListSource.match(/label="材料链接或归档编号"/g)).toHaveLength(4); }); + + it("keeps academic-year choices stable and shows future member terms after refresh", () => { + expect(clubListSource).toContain( + "Array.from({ length: 101 }, (_, index) => academicTermOption(2000 + index))", + ); + expect(clubListSource).toContain("activeOrFutureClubMembers.value"); + expect(clubListSource).toContain("{ label: '当前与待生效', value: 'current' }"); + expect(clubListSource).toContain("sortMemberRows(filteredRows, memberSortMode.value)"); + expect(clubListSource).toContain("组织信息待完善"); + expect(clubListSource).not.toContain("!member.groupName?.trim()"); + expect(clubListSource).not.toContain("待补资料"); + expect(clubListSource).not.toContain("manualAcademicTermOptions"); + expect(clubListSource).not.toContain("新增学年"); + }); }); diff --git a/frontend/src/views/ClubList.vue b/frontend/src/views/ClubList.vue index a11ebf1b..a225ea2e 100644 --- a/frontend/src/views/ClubList.vue +++ b/frontend/src/views/ClubList.vue @@ -267,7 +267,6 @@ const clubs = ref([]); const applications = ref([]); const clubMembers = ref([]); const clubDepartments = ref([]); -const manualAcademicTermOptions = ref([]); const loading = ref(true); const usersLoading = ref(true); const dialogUsersLoading = ref(false); @@ -315,10 +314,9 @@ const memberFilters = reactive({ termName: "", departmentId: undefined as number | undefined, groupId: undefined as number | undefined, - unassignedOnly: false, + incompleteOnly: false, }); const memberSortMode = ref("organization"); -const newAcademicTermStartYear = ref(academicYearStart(new Date()) + 3); const applicationDialogVisible = ref(false); const applicationFormRef = ref(); @@ -637,7 +635,6 @@ const selectedDepartmentManagerScopes = computed(() => const canCreateMemberDepartment = computed( () => isOrganizationWorkspace.value && canManageSelectedClub.value, ); -const canCreateAcademicTerm = computed(() => canManageSelectedClub.value); const canCreateMemberGroup = computed( () => isOrganizationWorkspace.value && groupCreateDepartmentOptions.value.length > 0, ); @@ -704,10 +701,7 @@ const selectedBatchPositionRows = computed(() => selectedMemberRows.value.filter((row) => canBatchUpdateMemberTerm(row)), ); const academicTermOptions = computed(() => { - const currentYear = academicYearStart(new Date()); - const baseTerms = [currentYear - 1, currentYear, currentYear + 1, currentYear + 2].map((year) => - academicTermOption(year), - ); + const baseTerms = Array.from({ length: 101 }, (_, index) => academicTermOption(2000 + index)); const memberTerms = clubMembers.value .filter((member) => member.termName && member.termStart && member.termEnd) .map((member) => ({ @@ -716,11 +710,7 @@ const academicTermOptions = computed(() => { termEnd: dateOnly(member.termEnd), })); - return uniqueAcademicTermOptions([ - ...baseTerms, - ...manualAcademicTermOptions.value, - ...memberTerms, - ]); + return uniqueAcademicTermOptions([...baseTerms, ...memberTerms]); }); const memberTermSelectOptions = computed(() => { const options = [...academicTermOptions.value]; @@ -741,6 +731,13 @@ const memberTermFilterOptions = computed(() => ]), ); const currentClubMembers = computed(() => clubMembers.value.filter((member) => member.isCurrent)); +const activeOrFutureClubMembers = computed(() => + clubMembers.value.filter( + (member) => + isActiveStatus(member.memberStatus) && + (!dateOnly(member.termEnd) || dateOnly(member.termEnd) >= todayDateOnly()), + ), +); const currentActiveClubMembers = computed(() => currentClubMembers.value.filter((member) => isActiveStatus(member.memberStatus)), ); @@ -773,13 +770,13 @@ const organizationGroupMembers = computed(() => { }); const memberTableRows = computed(() => { const rows = ( - memberWorkspaceMode.value === "history" ? clubMembers.value : currentClubMembers.value + memberWorkspaceMode.value === "history" ? clubMembers.value : activeOrFutureClubMembers.value ).filter( (member) => (memberWorkspaceMode.value !== "history" || !memberFilters.termName || member.termName === memberFilters.termName) && - (!memberFilters.unassignedOnly || isMemberUnassigned(member)), + (!memberFilters.incompleteOnly || hasIncompleteMemberOrganization(member)), ); return sortMemberRows(rows, memberSortMode.value); @@ -796,19 +793,21 @@ const transitionSourceRows = computed(() => { } }); - return Array.from(rows.values()).sort((left, right) => - transitionRecordSortKey(right).localeCompare(transitionRecordSortKey(left)), + const filteredRows = Array.from(rows.values()).filter( + (member) => !memberFilters.incompleteOnly || hasIncompleteMemberOrganization(member), ); + return sortMemberRows(filteredRows, memberSortMode.value); }); const memberGroupSummary = computed(() => { - const rows = memberTableRows.value; + const rows = + memberWorkspaceMode.value === "transition" ? transitionSourceRows.value : memberTableRows.value; const currentRows = rows.filter((member) => member.isCurrent); return { total: rows.length, current: currentRows.length, departments: uniqueTextOptions(rows.map((member) => member.departmentName)).length, groups: uniqueTextOptions(rows.map((member) => member.groupName)).length, - unassigned: rows.filter(isMemberUnassigned).length, + incomplete: rows.filter(hasIncompleteMemberOrganization).length, }; }); const organizationSummary = computed(() => { @@ -1988,29 +1987,6 @@ async function submitMemberTerm() { } } -function addAcademicTermOption() { - if (!canCreateAcademicTerm.value) return; - - const startYear = Number(newAcademicTermStartYear.value); - if (!Number.isInteger(startYear) || startYear < 2000 || startYear > 2100) { - ElMessage.warning("请输入 2000-2100 之间的学年起始年份。"); - return; - } - - const term = academicTermOption(startYear); - if (academicTermOptions.value.some((option) => option.label === term.label)) { - ElMessage.info("该任期已存在。"); - return; - } - - manualAcademicTermOptions.value = uniqueAcademicTermOptions([ - ...manualAcademicTermOptions.value, - term, - ]); - newAcademicTermStartYear.value = startYear + 1; - ElMessage.success("任期已加入可选项"); -} - function resetFilters() { filters.auditStatus = ""; filters.keyword = ""; @@ -2022,11 +1998,11 @@ function clearMemberFilters() { memberFilters.termName = ""; memberFilters.departmentId = undefined; memberFilters.groupId = undefined; - memberFilters.unassignedOnly = false; + memberFilters.incompleteOnly = false; void loadMembers(); } -function toggleUnassignedMemberFilter(value: string | number | boolean) { +function toggleIncompleteMemberFilter(value: string | number | boolean) { if (!value) return; memberFilters.departmentId = undefined; @@ -2976,10 +2952,8 @@ function uniqueTextOptions(values: Array) { ).sort((left, right) => left.localeCompare(right, "zh-CN")); } -function isMemberUnassigned(member: ClubMemberRecord) { - return ( - !member.departmentName?.trim() || !member.groupName?.trim() || !member.positionName?.trim() - ); +function hasIncompleteMemberOrganization(member: ClubMemberRecord) { + return !member.departmentName?.trim() || !member.positionName?.trim(); } watch(currentUserId, () => { @@ -2990,7 +2964,7 @@ watch(selectedClubId, () => { memberFilters.departmentId = undefined; memberFilters.groupId = undefined; memberFilters.termName = ""; - memberFilters.unassignedOnly = false; + memberFilters.incompleteOnly = false; resetDepartmentForm(); resetGroupForm(); organizationTreeCollapsed.value = false; @@ -4019,7 +3993,7 @@ onUnmounted(() => { { v-model="memberFilters.departmentId" class="filter-item" clearable - :disabled="memberFilters.unassignedOnly" + :disabled="memberFilters.incompleteOnly" placeholder="按部门筛选" @change="handleMemberFilterDepartmentChange" > @@ -4069,7 +4043,7 @@ onUnmounted(() => { v-model="memberFilters.groupId" class="filter-item" clearable - :disabled="memberFilters.unassignedOnly" + :disabled="memberFilters.incompleteOnly" placeholder="按小组筛选" > { /> - 待补资料 + 组织信息待完善 清除筛选 {
- 当前列表 {{ memberGroupSummary.total }} 条 - 有效任期 {{ memberGroupSummary.current }} 条 + + {{ memberWorkspaceMode === "transition" ? "待换届" : "当前列表" }} + {{ memberGroupSummary.total }} 条 + + + 当前任期 {{ memberGroupSummary.current }} 条 + 部门 {{ memberGroupSummary.departments }} 个 小组 {{ memberGroupSummary.groups }} 个 - 待补资料 {{ memberGroupSummary.unassigned }} 条 -
- - - 新增学年 - -
+ 组织信息待完善 {{ memberGroupSummary.incomplete }} 条
@@ -4154,8 +4113,8 @@ onUnmounted(() => { :empty-text=" memberWorkspaceMode === 'current' ? canManageSelectedClub - ? '暂无当前名册,可新增成员任期' - : '暂无当前有效成员任期' + ? '暂无当前或待生效任期,可新增成员任期' + : '暂无当前或待生效成员任期' : '暂无历史任期记录' " row-key="memberId" @@ -4206,7 +4165,7 @@ onUnmounted(() => { - +