diff --git a/ai_knowledge_base_v2/apps/admin-web/src/App.vue b/ai_knowledge_base_v2/apps/admin-web/src/App.vue index bfcaa92..10d60d7 100644 --- a/ai_knowledge_base_v2/apps/admin-web/src/App.vue +++ b/ai_knowledge_base_v2/apps/admin-web/src/App.vue @@ -8,6 +8,7 @@ import AgentManagementView from "./components/AgentManagementView.vue"; import EntitlementManagementView from "./components/EntitlementManagementView.vue"; import KnowledgeManagementView from "./components/KnowledgeManagementView.vue"; import TableRowActions from "./components/TableRowActions.vue"; +import UserEntitlementRenewDialog from "./components/UserEntitlementRenewDialog.vue"; import { systemSettingDefinitions, systemSettingSections, type SystemSettingValue } from "./config/systemSettings"; import { api, clearToken, getToken, saveToken } from "./services/api"; import type { @@ -49,6 +50,11 @@ const userDetailLoading = ref(false); const userReportGenerating = ref(""); const userSettlementRetrying = ref(null); const userKeyword = ref(""); +const userEntitlementFilters = reactive({ planId: undefined as number | undefined, status: "all" }); +const selectedUsers = ref([]); +const renewDialogOpen = ref(false); +const renewTargets = ref([]); +const renewSubmitting = ref(false); const entitlementPlans = ref([]); const models = ref([]); const configs = ref([]); @@ -326,11 +332,91 @@ async function loadCurrentMenu() { } async function loadUsers(page = 1, pageSize = pagers.users.pageSize) { - const result = await api.users({ keyword: userKeyword.value, page, pageSize }); + const result = await api.users({ + keyword: userKeyword.value, + planId: userEntitlementFilters.planId, + entitlementStatus: userEntitlementFilters.status, + page, + pageSize, + }); users.value = result.items; + selectedUsers.value = []; Object.assign(pagers.users, { page: result.page, pageSize: result.pageSize, total: result.total }); } +function resetUserFilters() { + userKeyword.value = ""; + userEntitlementFilters.planId = undefined; + userEntitlementFilters.status = "all"; + void loadUsers(1); +} + +function handleUserSelectionChange(rows: AdminUser[]) { + selectedUsers.value = rows; +} + +function openRenewDialog(targets: AdminUser[]) { + if (!targets.length) return ElMessage.warning("请先选择需要续期的用户"); + renewTargets.value = targets; + renewDialogOpen.value = true; +} + +async function submitEntitlementRenew(payload: { extensionDays: number; remark: string }) { + if (!renewTargets.value.length || renewSubmitting.value) return; + renewSubmitting.value = true; + const idempotencyKey = typeof crypto.randomUUID === "function" + ? crypto.randomUUID() + : `renew-${Date.now()}-${Math.random().toString(36).slice(2)}`; + try { + if (renewTargets.value.length === 1) { + await api.renewUserEntitlement(renewTargets.value[0].id, { ...payload, idempotencyKey }); + ElMessage.success("权益续期成功"); + } else { + const result = await api.batchRenewUserEntitlement({ + userIds: renewTargets.value.map((user) => user.id), + ...payload, + idempotencyKey, + }); + if (result.failureCount) { + const firstFailure = result.failed[0]; + ElMessage.warning(`成功 ${result.successCount} 人,失败 ${result.failureCount} 人${firstFailure ? `;${firstFailure.name || firstFailure.userId}:${firstFailure.reason}` : ""}`); + } else { + ElMessage.success(`已为 ${result.successCount} 位用户续期`); + } + } + renewDialogOpen.value = false; + await loadUsers(pagers.users.page, pagers.users.pageSize); + } catch (error) { + ElMessage.error(error instanceof Error ? error.message : "权益续期失败"); + } finally { + renewSubmitting.value = false; + } +} + +function entitlementStatusLabel(row: AdminUser) { + const status = row.entitlement?.lifecycleStatus; + if (status === "expired_fallback") return "已到期,已降级"; + if (status === "expiring_7") return "7 天内到期"; + if (status === "expiring_30") return "30 天内到期"; + if (status === "long_term") return "长期有效"; + if (status === "default") return "默认权益"; + return "生效中"; +} + +function entitlementStatusType(row: AdminUser) { + const status = row.entitlement?.lifecycleStatus; + if (status === "expired_fallback") return "danger"; + if (status === "expiring_7" || status === "expiring_30") return "warning"; + if (status === "active" || status === "long_term") return "success"; + return "info"; +} + +function formatEntitlementDate(value?: string | null) { + if (!value) return "长期"; + const date = new Date(value); + return Number.isNaN(date.getTime()) ? value : date.toLocaleDateString("zh-CN"); +} + async function loadEntitlementPlans() { entitlementPlans.value = await api.entitlementPlans(true); } @@ -1098,7 +1184,6 @@ function formatRecordDateTime(value: string, boundary: "start" | "end") {