feat: 完成权益到期与续期闭环
This commit is contained in:
@@ -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<number | null>(null);
|
||||
const userKeyword = ref("");
|
||||
const userEntitlementFilters = reactive({ planId: undefined as number | undefined, status: "all" });
|
||||
const selectedUsers = ref<AdminUser[]>([]);
|
||||
const renewDialogOpen = ref(false);
|
||||
const renewTargets = ref<AdminUser[]>([]);
|
||||
const renewSubmitting = ref(false);
|
||||
const entitlementPlans = ref<EntitlementPlan[]>([]);
|
||||
const models = ref<ModelItem[]>([]);
|
||||
const configs = ref<SystemConfigItem[]>([]);
|
||||
@@ -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") {
|
||||
<template v-if="activeMenu === 'users'">
|
||||
<div class="page-head inline">
|
||||
<div><h2>用户管理</h2><p>维护学员名单;只有名单内启用学员可以登录用户端。</p></div>
|
||||
<el-input v-model="userKeyword" placeholder="手机号或姓名" clearable @change="loadUsers(1)" />
|
||||
</div>
|
||||
<section class="student-tools">
|
||||
<div class="student-tool-panel">
|
||||
@@ -1154,7 +1239,27 @@ function formatRecordDateTime(value: string, boundary: "start" | "end") {
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<el-table :data="users" stripe>
|
||||
<section class="user-list-toolbar">
|
||||
<el-input v-model="userKeyword" placeholder="手机号或姓名" clearable @keyup.enter="loadUsers(1)" />
|
||||
<el-select v-model="userEntitlementFilters.planId" placeholder="全部权益版本" clearable>
|
||||
<el-option v-for="plan in entitlementPlans.filter((item) => item.status === 1)" :key="plan.id" :label="plan.name" :value="plan.id" />
|
||||
</el-select>
|
||||
<el-select v-model="userEntitlementFilters.status" aria-label="权益状态">
|
||||
<el-option label="全部权益状态" value="all" />
|
||||
<el-option label="生效中" value="active" />
|
||||
<el-option label="7 天内到期" value="expiring7" />
|
||||
<el-option label="8-30 天内到期" value="expiring30" />
|
||||
<el-option label="已到期" value="expired" />
|
||||
<el-option label="使用默认权益" value="default" />
|
||||
</el-select>
|
||||
<el-button type="primary" @click="loadUsers(1)">筛选</el-button>
|
||||
<el-button @click="resetUserFilters">重置</el-button>
|
||||
<el-button class="batch-renew-button" :disabled="!selectedUsers.length" @click="openRenewDialog(selectedUsers)">
|
||||
批量续期{{ selectedUsers.length ? `(${selectedUsers.length})` : '' }}
|
||||
</el-button>
|
||||
</section>
|
||||
<el-table :data="users" stripe @selection-change="handleUserSelectionChange">
|
||||
<el-table-column type="selection" width="48" />
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="phone" label="手机号" width="150" />
|
||||
<el-table-column prop="name" label="姓名" width="140" />
|
||||
@@ -1166,7 +1271,7 @@ function formatRecordDateTime(value: string, boundary: "start" | "end") {
|
||||
<template #default="{ row }"><el-input-number v-model="row.dailyChatLimit" :min="0" size="small" /></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="dailyChatUsed" label="已用" width="90" />
|
||||
<el-table-column label="权益版本" min-width="260">
|
||||
<el-table-column label="权益版本" min-width="300">
|
||||
<template #default="{ row }">
|
||||
<div class="user-entitlement-cell">
|
||||
<el-select
|
||||
@@ -1184,9 +1289,12 @@ function formatRecordDateTime(value: string, boundary: "start" | "end") {
|
||||
</el-select>
|
||||
<small>
|
||||
{{ row.entitlement?.name || '默认基础版' }}
|
||||
· 本月主题
|
||||
{{ row.entitlement?.monthlyTopicUsed ?? 0 }}/{{ row.entitlement?.monthlyTopicLimit ?? '不限' }}
|
||||
· {{ row.entitlement?.lifecycleStatus === 'expired_fallback' ? `原 ${row.entitlement?.previousPlanName || '权益'} ${formatEntitlementDate(row.entitlement?.previousExpiredAt)} 到期` : `有效至 ${formatEntitlementDate(row.entitlement?.expiredAt)}` }}
|
||||
</small>
|
||||
<div class="entitlement-meta-row">
|
||||
<el-tag size="small" :type="entitlementStatusType(row)">{{ entitlementStatusLabel(row) }}</el-tag>
|
||||
<span>本月主题 {{ row.entitlement?.monthlyTopicUsed ?? 0 }}/{{ row.entitlement?.monthlyTopicLimit ?? '不限' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -1200,6 +1308,7 @@ function formatRecordDateTime(value: string, boundary: "start" | "end") {
|
||||
<el-button size="small">更多</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="openRenewDialog([row])">权益续期</el-dropdown-item>
|
||||
<el-dropdown-item @click="deleteUser(row)">删除用户</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
@@ -1209,6 +1318,12 @@ function formatRecordDateTime(value: string, boundary: "start" | "end") {
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<AdminPagination :page="pagers.users.page" :page-size="pagers.users.pageSize" :total="pagers.users.total" @change="loadUsers" />
|
||||
<UserEntitlementRenewDialog
|
||||
v-model="renewDialogOpen"
|
||||
:users="renewTargets"
|
||||
:submitting="renewSubmitting"
|
||||
@submit="submitEntitlementRenew"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-if="activeMenu === 'entitlements'">
|
||||
|
||||
Reference in New Issue
Block a user