feat: 优化权益管理并完成一期验收

This commit is contained in:
2026-07-31 18:49:48 +08:00
parent b6aff78aac
commit a3946013f5
14 changed files with 740 additions and 284 deletions

View File

@@ -0,0 +1,576 @@
<script setup lang="ts">
import { ElMessage } from "element-plus";
import { computed, nextTick, reactive, ref } from "vue";
import { api } from "../services/api";
import type { EntitlementPlan } from "../types/api";
const props = defineProps<{
plans: EntitlementPlan[];
}>();
const emit = defineEmits<{
refreshed: [];
}>();
const editorRef = ref<HTMLElement | null>(null);
const editingPlanId = ref<number | null>(null);
const saving = ref(false);
const form = reactive({
name: "",
planType: "basic" as "basic" | "deep" | "addon",
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 enabledCount = computed(() => props.plans.filter((plan) => plan.status === 1).length);
const capabilityOptions = [
{
key: "enableGrowthProfile" as const,
title: "成长档案",
description: "持续沉淀学员主题、变化与成长记录",
},
{
key: "enablePeriodicReports" as const,
title: "周期报告",
description: "允许生成周报、月报和阶段报告",
},
{
key: "allowHelpCard" as const,
title: "求助卡片",
description: "允许学员整理内容并生成求助卡片",
},
{
key: "allowShareDraft" as const,
title: "分享草稿",
description: "允许将对话内容整理为可分享草稿",
},
{
key: "deductQuota" as const,
title: "占用主题额度",
description: "新建主题时计入本月可用额度",
},
];
function planTypeLabel(type: string) {
return {
basic: "基础版",
deep: "深度陪伴版",
addon: "高频加购包",
}[type] || type;
}
function resetForm() {
editingPlanId.value = null;
Object.assign(form, {
name: "",
planType: "basic",
description: "",
validityDays: null,
monthlyTopicLimit: 30,
enableGrowthProfile: 0,
enablePeriodicReports: 0,
allowHelpCard: 1,
allowShareDraft: 1,
deductQuota: 1,
status: 1,
sortOrder: 10,
});
}
async function savePlan() {
if (!form.name.trim()) {
ElMessage.warning("请填写权益版本名称");
return;
}
saving.value = true;
try {
const payload = { ...form, name: form.name.trim(), description: form.description.trim() };
if (editingPlanId.value) {
await api.updateEntitlementPlan(editingPlanId.value, payload);
ElMessage.success("权益版本已更新");
} else {
await api.createEntitlementPlan(payload);
ElMessage.success("权益版本已新增");
}
resetForm();
emit("refreshed");
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : "权益版本保存失败");
} finally {
saving.value = false;
}
}
async function editPlan(plan: EntitlementPlan) {
editingPlanId.value = plan.id;
Object.assign(form, {
name: plan.name,
planType: plan.planType,
description: plan.description ?? "",
validityDays: plan.validityDays ?? null,
monthlyTopicLimit: plan.monthlyTopicLimit ?? null,
enableGrowthProfile: plan.enableGrowthProfile ? 1 : 0,
enablePeriodicReports: plan.enablePeriodicReports ? 1 : 0,
allowHelpCard: plan.allowHelpCard ? 1 : 0,
allowShareDraft: plan.allowShareDraft ? 1 : 0,
deductQuota: plan.deductQuota ? 1 : 0,
status: plan.status,
sortOrder: plan.sortOrder,
});
await nextTick();
editorRef.value?.scrollIntoView({ behavior: "smooth", block: "start" });
}
</script>
<template>
<div class="page-head entitlement-page-head">
<div>
<h2>权益管理</h2>
<p>配置不同服务版本的主题额度与可用能力保存后可在用户管理中直接分配</p>
</div>
<div class="entitlement-summary" aria-label="权益版本统计">
<span><strong>{{ plans.length }}</strong> 个版本</span>
<span><strong>{{ enabledCount }}</strong> 个启用</span>
</div>
</div>
<section ref="editorRef" class="entitlement-editor">
<div class="entitlement-editor-head">
<div>
<div class="entitlement-editor-title-row">
<h3>{{ editingPlanId ? "编辑权益版本" : "新增权益版本" }}</h3>
<span class="editor-mode">{{ editingPlanId ? "编辑中" : "新建" }}</span>
</div>
<p>先设置版本和额度再决定学员可使用的产品能力</p>
</div>
<el-button v-if="editingPlanId" @click="resetForm">退出编辑</el-button>
<el-button v-else @click="resetForm">重置表单</el-button>
</div>
<el-form label-position="top" :model="form" @submit.prevent="savePlan">
<div class="entitlement-section-label">
<strong>基础配置</strong>
<span>名称和说明仅用于后台识别不展示给学员</span>
</div>
<div class="entitlement-basic-grid">
<el-form-item class="field-name" label="版本名称" required>
<el-input v-model="form.name" maxlength="50" placeholder="例如:五个月深度陪伴版" />
</el-form-item>
<el-form-item label="版本类型">
<el-select v-model="form.planType">
<el-option label="基础版" value="basic" />
<el-option label="深度陪伴版" value="deep" />
<el-option label="高频加购包" value="addon" />
</el-select>
</el-form-item>
<el-form-item label="版本状态">
<div class="status-switch-field">
<div>
<strong>{{ form.status === 1 ? "启用" : "停用" }}</strong>
<small>{{ form.status === 1 ? "可分配给学员" : "保留配置,不再分配" }}</small>
</div>
<el-switch v-model="form.status" :active-value="1" :inactive-value="0" />
</div>
</el-form-item>
<el-form-item label="有效天数">
<el-input-number v-model="form.validityDays" :min="1" :max="3650" placeholder="留空即长期" />
<div class="field-help">不填写表示长期有效</div>
</el-form-item>
<el-form-item label="每月主题额度">
<el-input-number v-model="form.monthlyTopicLimit" :min="0" :max="99999" placeholder="留空即不限" />
<div class="field-help">不填写表示不限制主题数</div>
</el-form-item>
<el-form-item label="列表排序">
<el-input-number v-model="form.sortOrder" :min="0" :max="9999" />
<div class="field-help">数字越小在列表中越靠前</div>
</el-form-item>
<el-form-item class="field-description" label="后台说明">
<el-input v-model="form.description" type="textarea" :rows="3" maxlength="500" show-word-limit placeholder="补充适用人群、售卖规则或内部说明" />
</el-form-item>
</div>
<div class="entitlement-section-label capability-heading">
<strong>能力权限</strong>
<span>关闭后前后端都会按权益规则限制对应能力</span>
</div>
<div class="entitlement-switch-grid">
<label v-for="item in capabilityOptions" :key="item.key" class="capability-switch-card">
<span>
<strong>{{ item.title }}</strong>
<small>{{ item.description }}</small>
</span>
<el-switch v-model="form[item.key]" :active-value="1" :inactive-value="0" />
</label>
</div>
<div class="entitlement-form-actions">
<el-button type="primary" native-type="submit" :loading="saving">
{{ editingPlanId ? "保存修改" : "新增权益版本" }}
</el-button>
<el-button v-if="editingPlanId" @click="resetForm">取消编辑</el-button>
</div>
</el-form>
</section>
<section class="entitlement-list-panel">
<div class="entitlement-list-head">
<div>
<h3>已有权益版本</h3>
<p>停用版本不会再出现在可分配列表中已保存的数据仍会保留</p>
</div>
</div>
<el-table :data="plans" stripe>
<el-table-column label="版本" min-width="210">
<template #default="{ row }">
<div class="plan-name-cell">
<strong>{{ row.name }}</strong>
<small>{{ row.description || "暂无后台说明" }}</small>
</div>
</template>
</el-table-column>
<el-table-column label="类型" width="130">
<template #default="{ row }">{{ planTypeLabel(row.planType) }}</template>
</el-table-column>
<el-table-column label="主题额度" width="110" align="center">
<template #default="{ row }">{{ row.monthlyTopicLimit ?? "不限" }}</template>
</el-table-column>
<el-table-column label="有效期" width="110" align="center">
<template #default="{ row }">{{ row.validityDays ? `${row.validityDays}` : "长期" }}</template>
</el-table-column>
<el-table-column label="能力" min-width="360">
<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" align="center">
<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="112" fixed="right" align="center">
<template #default="{ row }"><el-button size="small" @click="editPlan(row)">编辑</el-button></template>
</el-table-column>
</el-table>
</section>
</template>
<style scoped>
.entitlement-page-head {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 24px;
}
.entitlement-summary {
display: flex;
flex: 0 0 auto;
gap: 8px;
}
.entitlement-summary span {
padding: 8px 12px;
border: 1px solid #dfe8e5;
border-radius: 999px;
background: #fff;
color: #71817b;
font-size: 13px;
}
.entitlement-summary strong {
color: #1f8d68;
}
.entitlement-editor,
.entitlement-list-panel {
border: 1px solid #dfe8e5;
border-radius: 14px;
background: #fff;
box-shadow: 0 8px 26px rgb(31 65 54 / 4%);
}
.entitlement-editor {
scroll-margin-top: 20px;
padding: 22px;
}
.entitlement-editor-head,
.entitlement-editor-title-row,
.entitlement-list-head {
display: flex;
align-items: center;
}
.entitlement-editor-head {
justify-content: space-between;
gap: 18px;
padding-bottom: 18px;
border-bottom: 1px solid #edf2f0;
}
.entitlement-editor-head h3,
.entitlement-editor-head p,
.entitlement-list-head h3,
.entitlement-list-head p {
margin: 0;
}
.entitlement-editor-head h3,
.entitlement-list-head h3 {
color: #263a34;
font-size: 18px;
}
.entitlement-editor-head p,
.entitlement-list-head p {
margin-top: 5px;
color: #71817b;
font-size: 13px;
line-height: 1.55;
}
.entitlement-editor-title-row {
gap: 10px;
}
.editor-mode {
padding: 3px 8px;
border-radius: 999px;
background: #edf7f3;
color: #218361;
font-size: 12px;
}
.entitlement-section-label {
display: flex;
align-items: baseline;
gap: 10px;
margin: 20px 0 12px;
}
.entitlement-section-label strong {
color: #263a34;
font-size: 15px;
}
.entitlement-section-label span {
color: #8a9893;
font-size: 12px;
}
.entitlement-basic-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0 16px;
}
.field-name {
grid-column: span 2;
}
.field-description {
grid-column: 1 / -1;
}
.entitlement-basic-grid :deep(.el-select),
.entitlement-basic-grid :deep(.el-input-number) {
width: 100%;
}
.entitlement-basic-grid :deep(.el-form-item) {
margin-bottom: 17px;
}
.status-switch-field {
width: 100%;
min-height: 40px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
box-sizing: border-box;
padding: 5px 12px;
border: 1px solid #dcdfe6;
border-radius: 4px;
}
.status-switch-field div,
.plan-name-cell,
.capability-switch-card > span {
min-width: 0;
}
.status-switch-field strong,
.status-switch-field small,
.plan-name-cell strong,
.plan-name-cell small,
.capability-switch-card strong,
.capability-switch-card small {
display: block;
}
.status-switch-field strong {
color: #40524b;
font-size: 13px;
line-height: 1.2;
}
.status-switch-field small,
.field-help {
color: #98a49f;
font-size: 11px;
}
.field-help {
margin-top: 5px;
line-height: 1.4;
}
.capability-heading {
margin-top: 5px;
}
.entitlement-switch-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 10px;
}
.capability-switch-card {
min-height: 64px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
box-sizing: border-box;
padding: 11px 13px;
border: 1px solid #dfe8e5;
border-radius: 10px;
background: #f8fbfa;
cursor: pointer;
transition: border-color 0.2s ease, background-color 0.2s ease;
}
.capability-switch-card:hover {
border-color: #b8d9cd;
background: #f4faf7;
}
.capability-switch-card strong {
color: #40524b;
font-size: 13px;
line-height: 1.3;
}
.capability-switch-card small {
margin-top: 4px;
color: #82918c;
font-size: 11px;
line-height: 1.35;
}
.capability-switch-card :deep(.el-switch) {
flex: 0 0 auto;
}
.entitlement-form-actions {
display: flex;
gap: 10px;
margin-top: 20px;
}
.entitlement-form-actions :deep(.el-button) {
min-width: 112px;
}
.entitlement-list-panel {
margin-top: 16px;
overflow: hidden;
}
.entitlement-list-head {
min-height: 74px;
box-sizing: border-box;
padding: 14px 20px;
border-bottom: 1px solid #edf2f0;
}
.plan-name-cell strong {
overflow: hidden;
color: #344942;
text-overflow: ellipsis;
white-space: nowrap;
}
.plan-name-cell small {
overflow: hidden;
margin-top: 4px;
color: #86948f;
font-size: 12px;
text-overflow: ellipsis;
white-space: nowrap;
}
.entitlement-capabilities {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
@media (max-width: 1180px) {
.entitlement-basic-grid,
.entitlement-switch-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.field-name {
grid-column: 1 / -1;
}
}
@media (max-width: 760px) {
.entitlement-page-head {
align-items: flex-start;
flex-direction: column;
}
.entitlement-editor {
padding: 16px;
}
.entitlement-editor-head {
align-items: flex-start;
}
.entitlement-basic-grid,
.entitlement-switch-grid {
grid-template-columns: 1fr;
}
.field-name,
.field-description {
grid-column: auto;
}
.entitlement-section-label {
align-items: flex-start;
flex-direction: column;
gap: 3px;
}
}
</style>