feat: add entitlement plans and topic sessions
This commit is contained in:
@@ -16,6 +16,7 @@ import type {
|
||||
ChatDetail,
|
||||
ChatRecord,
|
||||
DashboardStats,
|
||||
EntitlementPlan,
|
||||
KnowledgeItem,
|
||||
RetrievalLogItem,
|
||||
AttentionItem,
|
||||
@@ -41,6 +42,8 @@ const dashboardFilters = reactive({
|
||||
});
|
||||
const users = ref<AdminUser[]>([]);
|
||||
const userKeyword = ref("");
|
||||
const entitlementPlans = ref<EntitlementPlan[]>([]);
|
||||
const editingEntitlementPlanId = ref<number | null>(null);
|
||||
const models = ref<ModelItem[]>([]);
|
||||
const configs = ref<SystemConfigItem[]>([]);
|
||||
const chats = ref<ChatRecord[]>([]);
|
||||
@@ -88,6 +91,21 @@ const studentImportFile = ref<File | null>(null);
|
||||
const studentImportInput = ref<HTMLInputElement | null>(null);
|
||||
const studentImportResult = ref<UserImportResult | null>(null);
|
||||
|
||||
const entitlementPlanForm = reactive({
|
||||
name: "",
|
||||
planType: "basic" as "basic" | "deep" | "addon" | "teacher",
|
||||
description: "",
|
||||
validityDays: null as number | null,
|
||||
monthlyTopicLimit: 30 as number | null,
|
||||
enableGrowthProfile: 0,
|
||||
enablePeriodicReports: 0,
|
||||
allowHelpCard: 1,
|
||||
allowShareDraft: 1,
|
||||
deductQuota: 1,
|
||||
status: 1,
|
||||
sortOrder: 10,
|
||||
});
|
||||
|
||||
const knowledgeForm = reactive({
|
||||
name: "",
|
||||
feishuSpaceId: "",
|
||||
@@ -278,7 +296,8 @@ async function loadCurrentMenu() {
|
||||
loading.value = true;
|
||||
try {
|
||||
if (activeMenu.value === "dashboard") await loadDashboard();
|
||||
if (activeMenu.value === "users") await loadUsers(pagers.users.page, pagers.users.pageSize);
|
||||
if (activeMenu.value === "users") { await loadEntitlementPlans(); await loadUsers(pagers.users.page, pagers.users.pageSize); }
|
||||
if (activeMenu.value === "entitlements") await loadEntitlementPlans();
|
||||
if (activeMenu.value === "models") models.value = await api.models();
|
||||
if (activeMenu.value === "configs") {
|
||||
configs.value = await api.configs();
|
||||
@@ -298,6 +317,78 @@ async function loadUsers(page = 1, pageSize = pagers.users.pageSize) {
|
||||
Object.assign(pagers.users, { page: result.page, pageSize: result.pageSize, total: result.total });
|
||||
}
|
||||
|
||||
async function loadEntitlementPlans() {
|
||||
entitlementPlans.value = await api.entitlementPlans(true);
|
||||
}
|
||||
|
||||
function planTypeLabel(type: string) {
|
||||
return {
|
||||
basic: "基础版",
|
||||
deep: "深度陪伴版",
|
||||
addon: "高频加购包",
|
||||
teacher: "老师工作版",
|
||||
}[type] || type;
|
||||
}
|
||||
|
||||
async function saveEntitlementPlan() {
|
||||
if (!entitlementPlanForm.name.trim()) {
|
||||
ElMessage.warning("请填写权益版本名称");
|
||||
return;
|
||||
}
|
||||
const payload = { ...entitlementPlanForm };
|
||||
if (editingEntitlementPlanId.value) {
|
||||
await api.updateEntitlementPlan(editingEntitlementPlanId.value, payload);
|
||||
ElMessage.success("权益版本已更新");
|
||||
} else {
|
||||
await api.createEntitlementPlan(payload);
|
||||
ElMessage.success("权益版本已新增");
|
||||
}
|
||||
resetEntitlementPlanForm();
|
||||
await loadEntitlementPlans();
|
||||
}
|
||||
|
||||
function editEntitlementPlan(row: EntitlementPlan) {
|
||||
editingEntitlementPlanId.value = row.id;
|
||||
Object.assign(entitlementPlanForm, {
|
||||
name: row.name,
|
||||
planType: row.planType,
|
||||
description: row.description ?? "",
|
||||
validityDays: row.validityDays ?? null,
|
||||
monthlyTopicLimit: row.monthlyTopicLimit ?? null,
|
||||
enableGrowthProfile: row.enableGrowthProfile ? 1 : 0,
|
||||
enablePeriodicReports: row.enablePeriodicReports ? 1 : 0,
|
||||
allowHelpCard: row.allowHelpCard ? 1 : 0,
|
||||
allowShareDraft: row.allowShareDraft ? 1 : 0,
|
||||
deductQuota: row.deductQuota ? 1 : 0,
|
||||
status: row.status,
|
||||
sortOrder: row.sortOrder,
|
||||
});
|
||||
}
|
||||
|
||||
function resetEntitlementPlanForm() {
|
||||
editingEntitlementPlanId.value = null;
|
||||
Object.assign(entitlementPlanForm, {
|
||||
name: "",
|
||||
planType: "basic",
|
||||
description: "",
|
||||
validityDays: null,
|
||||
monthlyTopicLimit: 30,
|
||||
enableGrowthProfile: 0,
|
||||
enablePeriodicReports: 0,
|
||||
allowHelpCard: 1,
|
||||
allowShareDraft: 1,
|
||||
deductQuota: 1,
|
||||
status: 1,
|
||||
sortOrder: 10,
|
||||
});
|
||||
}
|
||||
|
||||
async function assignUserEntitlement(row: AdminUser, planId: number) {
|
||||
const entitlement = await api.assignUserEntitlement(row.id, { planId });
|
||||
row.entitlement = entitlement;
|
||||
ElMessage.success("用户权益已更新");
|
||||
}
|
||||
|
||||
async function loadRecordTab(tab = recordTab.value) {
|
||||
loading.value = true;
|
||||
try {
|
||||
@@ -866,6 +957,7 @@ function formatRecordDateTime(value: string, boundary: "start" | "end") {
|
||||
<div class="sidebar-title">大本营千问千答</div>
|
||||
<button :class="{ active: activeMenu === 'dashboard' }" @click="switchMenu('dashboard')">数据看板</button>
|
||||
<button :class="{ active: activeMenu === 'users' }" @click="switchMenu('users')">用户管理</button>
|
||||
<button :class="{ active: activeMenu === 'entitlements' }" @click="switchMenu('entitlements')">权益管理</button>
|
||||
<button :class="{ active: activeMenu === 'knowledge' }" @click="switchMenu('knowledge')">知识库管理</button>
|
||||
<button :class="{ active: activeMenu === 'prompt' }" @click="switchMenu('prompt')">Agent 管理</button>
|
||||
<button :class="{ active: activeMenu === 'models' }" @click="switchMenu('models')">模型管理</button>
|
||||
@@ -984,6 +1076,30 @@ 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">
|
||||
<template #default="{ row }">
|
||||
<div class="user-entitlement-cell">
|
||||
<el-select
|
||||
:model-value="row.entitlement?.planId"
|
||||
placeholder="默认基础版"
|
||||
size="small"
|
||||
@change="(planId: number) => assignUserEntitlement(row, planId)"
|
||||
>
|
||||
<el-option
|
||||
v-for="plan in entitlementPlans.filter((item) => item.status === 1)"
|
||||
:key="plan.id"
|
||||
:label="plan.name"
|
||||
:value="plan.id"
|
||||
/>
|
||||
</el-select>
|
||||
<small>
|
||||
{{ row.entitlement?.name || '默认基础版' }}
|
||||
· 本月主题
|
||||
{{ row.entitlement?.monthlyTopicUsed ?? 0 }}/{{ row.entitlement?.monthlyTopicLimit ?? '不限' }}
|
||||
</small>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="lastLoginAt" label="最近登录" width="180" />
|
||||
<el-table-column label="操作" width="160" fixed="right">
|
||||
<template #default="{ row }">
|
||||
@@ -995,6 +1111,92 @@ function formatRecordDateTime(value: string, boundary: "start" | "end") {
|
||||
<AdminPagination :page="pagers.users.page" :page-size="pagers.users.pageSize" :total="pagers.users.total" @change="loadUsers" />
|
||||
</template>
|
||||
|
||||
<template v-if="activeMenu === 'entitlements'">
|
||||
<div class="page-head">
|
||||
<h2>权益管理</h2>
|
||||
<p>配置不同服务版本的本月主题额度和可用能力;学员可在用户管理里直接分配。</p>
|
||||
</div>
|
||||
|
||||
<section class="entitlement-editor">
|
||||
<div class="entitlement-editor-head">
|
||||
<div>
|
||||
<h3>{{ editingEntitlementPlanId ? '编辑权益版本' : '新增权益版本' }}</h3>
|
||||
<p>一期先接入主题额度、成长档案/周期报告开关和转人工卡片/分享草稿能力位。</p>
|
||||
</div>
|
||||
<el-button @click="resetEntitlementPlanForm">清空</el-button>
|
||||
</div>
|
||||
<el-form label-position="top" :model="entitlementPlanForm">
|
||||
<div class="entitlement-form-grid">
|
||||
<el-form-item label="版本名称">
|
||||
<el-input v-model="entitlementPlanForm.name" placeholder="例如:五个月深度陪伴版" />
|
||||
</el-form-item>
|
||||
<el-form-item label="版本类型">
|
||||
<el-select v-model="entitlementPlanForm.planType">
|
||||
<el-option label="基础版" value="basic" />
|
||||
<el-option label="深度陪伴版" value="deep" />
|
||||
<el-option label="高频加购包" value="addon" />
|
||||
<el-option label="老师工作版" value="teacher" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="有效天数">
|
||||
<el-input-number v-model="entitlementPlanForm.validityDays" :min="1" :max="3650" placeholder="留空长期有效" />
|
||||
</el-form-item>
|
||||
<el-form-item label="本月主题额度">
|
||||
<el-input-number v-model="entitlementPlanForm.monthlyTopicLimit" :min="0" :max="99999" placeholder="留空不限" />
|
||||
</el-form-item>
|
||||
<el-form-item label="排序">
|
||||
<el-input-number v-model="entitlementPlanForm.sortOrder" :min="0" :max="9999" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
<el-form-item label="说明">
|
||||
<el-input v-model="entitlementPlanForm.description" type="textarea" :rows="2" placeholder="给后台管理员看的说明,不展示给学员" />
|
||||
</el-form-item>
|
||||
<div class="entitlement-switch-grid">
|
||||
<label><span>成长档案</span><el-switch v-model="entitlementPlanForm.enableGrowthProfile" :active-value="1" :inactive-value="0" /></label>
|
||||
<label><span>周期报告</span><el-switch v-model="entitlementPlanForm.enablePeriodicReports" :active-value="1" :inactive-value="0" /></label>
|
||||
<label><span>允许求助卡片</span><el-switch v-model="entitlementPlanForm.allowHelpCard" :active-value="1" :inactive-value="0" /></label>
|
||||
<label><span>允许分享草稿</span><el-switch v-model="entitlementPlanForm.allowShareDraft" :active-value="1" :inactive-value="0" /></label>
|
||||
<label><span>占用主题额度</span><el-switch v-model="entitlementPlanForm.deductQuota" :active-value="1" :inactive-value="0" /></label>
|
||||
<label><span>启用版本</span><el-switch v-model="entitlementPlanForm.status" :active-value="1" :inactive-value="0" /></label>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<el-button type="primary" @click="saveEntitlementPlan">{{ editingEntitlementPlanId ? '保存权益版本' : '新增权益版本' }}</el-button>
|
||||
<el-button @click="resetEntitlementPlanForm">取消编辑</el-button>
|
||||
</div>
|
||||
</el-form>
|
||||
</section>
|
||||
|
||||
<el-table :data="entitlementPlans" stripe>
|
||||
<el-table-column prop="name" label="版本名称" min-width="180" />
|
||||
<el-table-column label="类型" width="130">
|
||||
<template #default="{ row }">{{ planTypeLabel(row.planType) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="本月主题额度" width="130">
|
||||
<template #default="{ row }">{{ row.monthlyTopicLimit ?? '不限' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="有效期" width="110">
|
||||
<template #default="{ row }">{{ row.validityDays ? `${row.validityDays} 天` : '长期' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="能力" min-width="260">
|
||||
<template #default="{ row }">
|
||||
<div class="entitlement-capabilities">
|
||||
<el-tag v-if="row.enableGrowthProfile" type="success" effect="plain">成长档案</el-tag>
|
||||
<el-tag v-if="row.enablePeriodicReports" type="success" effect="plain">周期报告</el-tag>
|
||||
<el-tag v-if="row.allowHelpCard" effect="plain">求助卡片</el-tag>
|
||||
<el-tag v-if="row.allowShareDraft" effect="plain">分享草稿</el-tag>
|
||||
<el-tag :type="row.deductQuota ? 'warning' : 'info'" effect="plain">{{ row.deductQuota ? '计入额度' : '不计额度' }}</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="90">
|
||||
<template #default="{ row }"><el-tag :type="row.status === 1 ? 'success' : 'info'">{{ row.status === 1 ? '启用' : '停用' }}</el-tag></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="100" fixed="right" align="center">
|
||||
<template #default="{ row }"><el-button size="small" @click="editEntitlementPlan(row)">编辑</el-button></template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</template>
|
||||
|
||||
<template v-if="activeMenu === 'knowledge'">
|
||||
<KnowledgeManagementView @preview="previewKnowledge" />
|
||||
</template>
|
||||
|
||||
@@ -12,6 +12,7 @@ import type {
|
||||
ChatRecord,
|
||||
ChatRecordQuery,
|
||||
DashboardStats,
|
||||
EntitlementPlan,
|
||||
KnowledgeItem,
|
||||
KnowledgeContentSearchItem,
|
||||
KnowledgeDetail,
|
||||
@@ -25,6 +26,7 @@ import type {
|
||||
ModelItem,
|
||||
SystemConfigItem,
|
||||
UserImportResult,
|
||||
UserEntitlementSummary,
|
||||
PageResult,
|
||||
PromptDetail,
|
||||
PromptHistoryItem,
|
||||
@@ -136,6 +138,14 @@ export const api = {
|
||||
updateUser: (id: number, payload: Record<string, unknown>) =>
|
||||
request<AdminUser>(`/admin/user/${id}`, { method: "PUT", body: JSON.stringify(payload) }),
|
||||
deleteUser: (id: number) => request<null>(`/admin/user/${id}`, { method: "DELETE" }),
|
||||
entitlementPlans: (includeDisabled = true) =>
|
||||
request<EntitlementPlan[]>(`/admin/entitlement/plan/list${queryString({ includeDisabled })}`),
|
||||
createEntitlementPlan: (payload: Record<string, unknown>) =>
|
||||
request<EntitlementPlan>("/admin/entitlement/plan", { method: "POST", body: JSON.stringify(payload) }),
|
||||
updateEntitlementPlan: (id: number, payload: Record<string, unknown>) =>
|
||||
request<EntitlementPlan>(`/admin/entitlement/plan/${id}`, { method: "PUT", body: JSON.stringify(payload) }),
|
||||
assignUserEntitlement: (userId: number, payload: { planId: number; effectiveAt?: string | null; expiredAt?: string | null; remark?: string | null }) =>
|
||||
request<UserEntitlementSummary>(`/admin/user/${userId}/entitlement`, { method: "POST", body: JSON.stringify(payload) }),
|
||||
knowledge: (query: Record<string, unknown> = {}) => request<PageResult<KnowledgeItem>>(`/admin/knowledge/list${queryString(query)}`),
|
||||
knowledgeOptions: () => request<KnowledgeItem[]>("/admin/knowledge/options"),
|
||||
resolveKnowledgeNode: (nodeId: string) => request<{ nodeId: string; spaceId: string; sourceTitle: string; name: string; remark: string }>("/admin/knowledge/resolve-node", { method: "POST", body: JSON.stringify({ nodeId }) }),
|
||||
|
||||
@@ -635,6 +635,97 @@ textarea {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.user-entitlement-cell {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.user-entitlement-cell .el-select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.user-entitlement-cell small {
|
||||
overflow: hidden;
|
||||
color: #71817b;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.entitlement-editor {
|
||||
margin-bottom: 16px;
|
||||
padding: 18px;
|
||||
border: 1px solid #dfe8e5;
|
||||
border-radius: 12px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.entitlement-editor-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.entitlement-editor-head h3,
|
||||
.entitlement-editor-head p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.entitlement-editor-head p {
|
||||
margin-top: 6px;
|
||||
color: #71817b;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.entitlement-form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.entitlement-form-grid .el-input-number,
|
||||
.entitlement-form-grid .el-select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.entitlement-switch-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.entitlement-switch-grid label {
|
||||
min-width: 0;
|
||||
min-height: 42px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid #dfe8e5;
|
||||
border-radius: 10px;
|
||||
background: #f8fbfa;
|
||||
color: #40524b;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.entitlement-switch-grid span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.entitlement-capabilities {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.migration-overview {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
|
||||
@@ -61,6 +61,42 @@ export interface AdminUser {
|
||||
expiredAt?: string | null;
|
||||
lastLoginAt?: string | null;
|
||||
createdAt?: string | null;
|
||||
entitlement?: UserEntitlementSummary | null;
|
||||
}
|
||||
|
||||
export interface EntitlementPlan {
|
||||
id: number;
|
||||
name: string;
|
||||
planType: "basic" | "deep" | "addon" | "teacher";
|
||||
description?: string | null;
|
||||
validityDays?: number | null;
|
||||
monthlyTopicLimit?: number | null;
|
||||
enableGrowthProfile: boolean;
|
||||
enablePeriodicReports: boolean;
|
||||
allowHelpCard: boolean;
|
||||
allowShareDraft: boolean;
|
||||
deductQuota: boolean;
|
||||
status: number;
|
||||
sortOrder: number;
|
||||
createdAt?: string | null;
|
||||
updatedAt?: string | null;
|
||||
}
|
||||
|
||||
export interface UserEntitlementSummary {
|
||||
planId?: number | null;
|
||||
name: string;
|
||||
planType: string;
|
||||
monthlyTopicLimit?: number | null;
|
||||
monthlyTopicUsed: number;
|
||||
monthlyTopicRemaining?: number | null;
|
||||
enableGrowthProfile: boolean;
|
||||
enablePeriodicReports: boolean;
|
||||
allowHelpCard: boolean;
|
||||
allowShareDraft: boolean;
|
||||
deductQuota: boolean;
|
||||
effectiveAt?: string | null;
|
||||
expiredAt?: string | null;
|
||||
source: string;
|
||||
}
|
||||
|
||||
export interface UserImportFailure {
|
||||
@@ -315,6 +351,7 @@ export interface ChatRecord {
|
||||
export interface ChatMessageRecord {
|
||||
id: number;
|
||||
sessionId: number;
|
||||
topicSessionId?: number | null;
|
||||
userId: number;
|
||||
role: "user" | "assistant";
|
||||
content: string;
|
||||
|
||||
Reference in New Issue
Block a user