1. 功能概述
FastbuildAI的"新建套餐"功能是充值管理模块的核心功能,允许管理员创建和管理用户充值套餐。该功能位于用户管理菜单下的充值管理页面,提供了完整的套餐配置界面。
1.1 业务流程
- 管理员登录: 使用管理员账号登录FastbuildAI后台管理系统
- 导航到充值管理: 点击"用户管理" → "充值管理"菜单
- 新建套餐: 点击"新建套餐"按钮添加新的充值规则
- 配置套餐信息: 输入充值金额、赠送电力值、售价、标签等信息
- 保存套餐: 点击右下角"保存"按钮提交配置
1.2 功能特性
- 动态表格管理: 支持动态添加和删除充值套餐行
- 实时数据验证: 表单字段实时验证和错误提示
- 权限控制: 基于用户权限控制功能访问
- 多语言支持: 完整的国际化文本配置
- 状态管理: 智能检测数据变更状态
2. 页面路由与菜单结构
2.1 菜单配置
前端配置文件 : apps/web/core/i18n/zh/console-menu.json
json
{
"userManagement": {
"title": "用户管理",
"userList": "用户列表",
"userInfo": "用户信息",
"userRecharge": "充值管理"
}
}
后端菜单配置文件 : apps/server/src/core/database/install/menu.json
json
{
"name": "console-menu.userManagement.userRecharge",
"code": "user-user-recharge",
"path": "user-recharge",
"icon": "",
"component": "/console/user/user-recharge/index",
"permissionCode": "recharge-config:getConfig",
"sort": 100,
"isHidden": 0,
"type": 2,
"sourceType": 1,
"pluginPackName": null,
"children": [
{
"name": "console-common.save",
"code": "user-user-recharge-save",
"path": "",
"icon": "",
"component": "",
"permissionCode": "recharge-config:setConfig",
"sort": 0,
"isHidden": 1,
"type": 2,
"sourceType": 1,
"pluginPackName": null
}
]
}
2.2 路由结构
基于Nuxt.js的文件系统路由,充值管理页面的路由结构:
apps/web/app/console/user/user-recharge/
├── index.vue # 充值管理主页面
访问路径 : /console/user/user-recharge
2.3 菜单层级
控制台首页
└── 用户管理 (userManagement)
├── 用户列表 (userList)
├── 用户信息 (userInfo)
└── 充值管理 (userRecharge) ← 目标页面
3. 核心组件分析
3.1 主组件结构
文件位置 : apps/web/app/console/user/user-recharge/index.vue
这是一个完整的Vue 3 Composition API组件,使用TypeScript和Nuxt UI组件库构建。
3.1.1 Script Setup部分
vue
<script setup lang="ts">
import { useMessage } from "@fastbuildai/ui";
import type { TableColumn } from "@nuxt/ui";
import { useI18n } from "vue-i18n";
import type { RechargeConfigData, RechargeRule } from "@/models/package-management";
import { apiGetRechargeRules, saveRechargeRules } from "@/services/console/package-management";
const { t } = useI18n();
const toast = useMessage();
// 响应式数据状态
const rechargeStatus = ref(true);
const changeValue = ref(false);
const rechargeRules = ref<RechargeRule[]>([]);
const rechargeExplain = ref("");
// 表格列配置
const columns: TableColumn<RechargeRule>[] = [
{
id: "move",
header: "#",
size: 40,
enableSorting: false,
enableHiding: false,
},
{
id: "rechargeValue",
accessorKey: "rechargeValue",
header: t("console-marketing.packageManagement.tab.rechargeValue"),
},
{
id: "freeQuantity",
accessorKey: "freeQuantity",
header: t("console-marketing.packageManagement.tab.freeQuantity"),
},
{
id: "price",
accessorKey: "price",
header: t("console-marketing.packageManagement.tab.price"),
},
{
id: "label",
accessorKey: "label",
header: t("console-marketing.packageManagement.tab.label"),
},
{
id: "action",
header: t("console-marketing.packageManagement.tab.action"),
size: 40,
enableSorting: false,
enableHiding: false,
},
];
const oldData = ref<RechargeConfigData>();
// 获取充值规则数据
const getRechargeRules = async () => {
const data = await apiGetRechargeRules();
oldData.value = data;
rechargeRules.value = data.rechargeRule.map((item) => ({ ...item }));
rechargeStatus.value = data.rechargeStatus;
rechargeExplain.value = data.rechargeExplain;
};
// 添加新的充值套餐行
const addRow = () => {
const newRow = ref<RechargeRule>({
givePower: 0,
label: "",
power: 0,
sellPrice: 0,
});
rechargeRules.value.push(newRow.value);
};
// 删除充值套餐行
const deleteRow = (row: RechargeRule) => {
rechargeRules.value = rechargeRules.value.filter((item) => item !== row);
};
// 保存充值规则配置
const saveRules = async () => {
try {
await saveRechargeRules({
rechargeRule: rechargeRules.value,
rechargeStatus: rechargeStatus.value,
rechargeExplain: rechargeExplain.value,
});
getRechargeRules();
toast.success(t("console-marketing.packageManagement.saveSuccess"));
} catch (error) {
console.error(error);
}
};
watch(rechargeStatus, () => {
if (rechargeStatus.value !== oldData.value?.rechargeStatus) {
changeValue.value = true;
} else {
changeValue.value = false;
}
});
watch(rechargeExplain, () => {
if (rechargeExplain.value !== oldData.value?.rechargeExplain) {
changeValue.value = true;
} else {
changeValue.value = false;
}
});
// 判断充值规则是否变化
const isEqual = (arr1: RechargeRule[], arr2: RechargeRule[] | undefined) => {
if (!arr2) return false;
if (arr1.length !== arr2.length) return false;
return arr1.every((item, index) => {
if (
item?.power !== arr2[index]?.power ||
item?.givePower !== arr2[index]?.givePower ||
item?.sellPrice !== arr2[index]?.sellPrice ||
item?.label !== arr2[index]?.label
) {
return false;
} else {
return true;
}
});
};
watch(
rechargeRules,
() => {
if (!isEqual(rechargeRules.value, oldData.value?.rechargeRule)) {
changeValue.value = true;
} else {
changeValue.value = false;
}
},
{ deep: true },
);
onMounted(() => {
getRechargeRules();
});
</script>
3.1.2 Template部分
vue
<template>
<div class="my-px space-y-4 pb-6">
<div class="flex flex-col gap-6">
<!-- 功能状态开关 -->
<div class="">
<div>
<div class="mb-4 flex flex-col gap-1">
<div class="text-secondary-foreground text-md font-bold">
{{ t("console-marketing.packageManagement.statusTitle") }}
</div>
<div class="text-muted-foreground text-xs">
{{ t("console-marketing.packageManagement.statusDescription") }}
</div>
</div>
<USwitch v-model="rechargeStatus" />
</div>
</div>
<!-- 充值说明配置 -->
<div v-if="rechargeStatus">
<div class="mb-4 flex flex-col gap-1">
<div class="text-secondary-foreground text-md font-bold">
{{ t("console-marketing.packageManagement.rechargeInstructionsTitle") }}
</div>
<div class="text-muted-foreground text-xs">
{{ t("console-marketing.packageManagement.rechargeInstructionsDescription") }}
</div>
</div>
<div class="w-full text-sm">
<UTextarea
class="w-full"
v-model="rechargeExplain"
:rows="6"
placeholder="请输入套餐充值说明..."
/>
</div>
</div>
<!-- 充值规则表格 -->
<div class="flex-1">
<div class="flex w-full items-center justify-between">
<div class="text-secondary-foreground text-md font-bold">
{{ t("console-marketing.packageManagement.rechargeRulesTitle") }}
</div>
<div class="flex items-center justify-between gap-2 px-4">
<UButton
color="primary"
variant="outline"
icon="tabler:plus"
:ui="{ leadingIcon: 'size-4' }"
@click="addRow"
>
{{ t("console-marketing.packageManagement.button.new") }}
</UButton>
</div>
</div>
<!-- 数据表格 -->
<div class="mt-4">
<UTable
ref="table"
:data="rechargeRules"
:columns="columns"
:ui="{
base: 'table-fixed border-separate border-spacing-0',
thead: '[&>tr]:bg-elevated/50 [&>tr]:after:content-none',
tbody: '[&>tr]:last:[&>td]:border-b-0',
th: 'py-2 first:rounded-l-lg last:rounded-r-lg border-y border-default first:border-l last:border-r',
td: 'border-b border-default',
tr: '[&:has(>td[colspan])]:hidden',
}"
>
<!-- 序号列 -->
<template #move-cell="{ row }">
{{ row.index + 1 }}
</template>
<!-- 充值数量输入 -->
<template #rechargeValue-cell="{ row }">
<UInput v-model="row.original.power" type="number" />
</template>
<!-- 赠送数量输入 -->
<template #freeQuantity-cell="{ row }">
<UInput v-model="row.original.givePower" type="number" />
</template>
<!-- 价格输入(带单位) -->
<template #price-cell="{ row }">
<UInput
v-model="row.original.sellPrice"
type="number"
min="0"
step="0.01"
:ui="{
trailing: 'bg-muted-foreground/15 pl-2 rounded-tr-lg rounded-br-lg',
}"
>
<template #trailing>
<span>{{ t("console-marketing.packageManagement.tab.priceUnit") }}</span>
</template>
</UInput>
</template>
<!-- 标签输入 -->
<template #label-cell="{ row }">
<UInput v-model="row.original.label" />
</template>
<!-- 操作按钮 -->
<template #action-cell="{ row }">
<UButton
icon="tabler:trash"
color="error"
variant="ghost"
@click="deleteRow(row.original)"
/>
</template>
</UTable>
</div>
</div>
<!-- 保存按钮 -->
<div class="flex justify-end">
<AccessControl :codes="['recharge-config:setConfig']">
<UButton
color="primary"
:disabled="!changeValue"
:ui="{ base: 'w-16 flex justify-center items-center' }"
@click="saveRules"
>
{{ t("console-marketing.packageManagement.button.save") }}
</UButton>
</AccessControl>
</div>
</div>
</div>
</template>
3.2 数据模型定义
文件位置 : apps/web/models/package-management.d.ts
typescript
/**
* 套餐充值配置响应接口
*/
export interface RechargeConfigData {
/**
* 充值说明
*/
rechargeExplain: string;
/**
* 充值规则
*/
rechargeRule: RechargeRule[];
/**
* 充值开关:true-开启;false-关闭
*/
rechargeStatus: boolean;
}
/**
* 充值规则接口
*/
export interface RechargeRule {
/**
* 赠送数量
*/
givePower: number;
/**
* 规则ID(可选)
*/
id?: string;
/**
* 标签
*/
label: string;
/**
* 充值数量
*/
power: number;
/**
* 售价
*/
sellPrice: string | number;
}
3.3 API接口服务
文件位置 : apps/web/services/console/package-management.ts
typescript
// ==================== 套餐管理相关 API ====================
import type { RechargeConfigData } from "@/models/package-management";
/**
* 获取套餐充值配置
*/
export const apiGetRechargeRules = (): Promise<RechargeConfigData> => {
return useConsoleGet("/recharge-config");
};
/**
* 保存套餐充值配置
*/
export const saveRechargeRules = (data: RechargeConfigData): Promise<void> => {
return useConsolePost("/recharge-config", data);
};
4. 用户交互流程详解
4.1 页面初始化流程
页面加载 组件挂载 onMounted 调用 getRechargeRules API 获取服务器数据 更新响应式状态 备份原始数据 oldData 渲染页面内容 用户可以开始操作
4.2 新建套餐流程
用户点击新建套餐按钮 调用 addRow 方法 创建新的 RechargeRule 对象 添加到 rechargeRules 数组 触发响应式更新 表格自动渲染新行 用户可以编辑新行数据
4.3 数据保存流程
是 否 是 否 用户修改数据 触发 watch 监听 检测数据变更 数据是否变更? 启用保存按钮 禁用保存按钮 用户点击保存 调用 saveRules 方法 发送 API 请求 保存成功? 显示成功提示 显示错误信息 重新获取数据 重置变更状态
4.4 表单验证机制
组件实现了多层次的数据验证:
- 输入类型验证 : 使用
type="number"确保数值输入 - 最小值限制 : 价格字段设置
min="0"防止负数 - 精度控制 : 价格字段使用
step="0.01"支持小数 - 深度比较 : 使用
isEqual函数精确检测数据变更
4.5 错误处理和用户反馈
typescript
// 保存操作的错误处理
const saveRules = async () => {
try {
await saveRechargeRules({
rechargeRule: rechargeRules.value,
rechargeStatus: rechargeStatus.value,
rechargeExplain: rechargeExplain.value,
});
getRechargeRules();
toast.success(t("console-marketing.packageManagement.saveSuccess"));
} catch (error) {
console.error(error);
// 这里可以添加错误提示
toast.error(t("console-marketing.packageManagement.saveFailed"));
}
};
5. UI组件详解
5.1 USwitch 开关组件
vue
<USwitch v-model="rechargeStatus" />
功能 : 控制充值功能的启用/禁用状态
特点:
- 双向数据绑定
- 响应式状态更新
- 自动触发变更检测
5.2 UTextarea 文本域组件
vue
<UTextarea
class="w-full"
v-model="rechargeExplain"
:rows="6"
placeholder="请输入套餐充值说明..."
/>
功能 : 输入充值说明文本
特点:
- 多行文本输入
- 固定行数显示
- 占位符提示
5.3 UTable 表格组件
表格组件是页面的核心,具有以下特性:
5.3.1 表格配置
typescript
const columns: TableColumn<RechargeRule>[] = [
{
id: "move",
header: "#",
size: 40,
enableSorting: false,
enableHiding: false,
},
// ... 其他列配置
];
5.3.2 自定义单元格模板
vue
<!-- 价格输入单元格 -->
<template #price-cell="{ row }">
<UInput
v-model="row.original.sellPrice"
type="number"
min="0"
step="0.01"
:ui="{
trailing: 'bg-muted-foreground/15 pl-2 rounded-tr-lg rounded-br-lg',
}"
>
<template #trailing>
<span>{{ t("console-marketing.packageManagement.tab.priceUnit") }}</span>
</template>
</UInput>
</template>
5.4 UInput 输入组件
支持多种输入类型:
- 数值输入 :
type="number" - 文本输入: 默认类型
- 带单位显示 : 使用
trailing插槽
5.5 UButton 按钮组件
vue
<!-- 新建套餐按钮 -->
<UButton
color="primary"
variant="outline"
icon="tabler:plus"
:ui="{ leadingIcon: 'size-4' }"
@click="addRow"
>
{{ t("console-marketing.packageManagement.button.new") }}
</UButton>
<!-- 删除按钮 -->
<UButton
icon="tabler:trash"
color="error"
variant="ghost"
@click="deleteRow(row.original)"
/>
<!-- 保存按钮 -->
<UButton
color="primary"
:disabled="!changeValue"
:ui="{ base: 'w-16 flex justify-center items-center' }"
@click="saveRules"
>
{{ t("console-marketing.packageManagement.button.save") }}
</UButton>
6. 权限控制机制
6.1 AccessControl 组件
文件位置 : common/components/access-control.vue
vue
<AccessControl :codes="['recharge-config:setConfig']">
<UButton
color="primary"
:disabled="!changeValue"
@click="saveRules"
>
{{ t("console-marketing.packageManagement.button.save") }}
</UButton>
</AccessControl>
权限控制逻辑:
- 检查用户权限数组中是否包含指定权限码
- Root用户拥有所有权限
- 权限不足时隐藏或禁用相关功能
6.2 权限码说明
recharge-config:setConfig: 充值配置设置权限- 只有拥有此权限的用户才能看到保存按钮
- 确保数据安全和操作权限控制
7. 国际化支持
7.1 文本配置文件
中文配置 (core/i18n/zh/console-marketing.json):
json
{
"packageManagement": {
"rechargeInstructionsTitle": "充值说明",
"saveSuccess": "保存成功",
"saveFailed": "保存失败",
"statusTitle": "功能状态",
"statusDescription": "启用后用户可以访问充值功能",
"rechargeRulesTitle": "充值规则",
"tab": {
"rechargeValue": "充值数量",
"freeQuantity": "赠送数量",
"price": "价格",
"label": "标签",
"action": "操作",
"priceUnit": "元"
},
"button": {
"save": "保存",
"new": "新建套餐"
}
}
}
英文配置 (core/i18n/en/console-marketing.json):
json
{
"packageManagement": {
"rechargeInstructionsTitle": "Recharge Policy",
"saveSuccess": "Saved",
"saveFailed": "Save failed",
"statusTitle": "Feature Status",
"statusDescription": "Enable for user access to recharge",
"rechargeRulesTitle": "Recharge Rules",
"tab": {
"rechargeValue": "Amount",
"freeQuantity": "Bonus",
"price": "Price",
"label": "Label",
"action": "Actions",
"priceUnit": "¥"
},
"button": {
"save": "Save",
"new": "New Package"
}
}
}
7.2 使用方式
typescript
// 在组件中使用国际化
const { t } = useI18n();
// 获取翻译文本
const title = t("console-marketing.packageManagement.rechargeInstructionsTitle");
支持的语言:
- 中文 (zh)
- 英文 (en)
- 日文 (jp)
8. 状态管理与数据流
8.1 响应式状态变量
typescript
// 核心状态变量
const rechargeStatus = ref(true); // 充值功能开关
const changeValue = ref(false); // 数据变更标志
const rechargeRules = ref<RechargeRule[]>([]); // 充值规则数组
const rechargeExplain = ref(""); // 充值说明文本
const oldData = ref<RechargeConfigData>(); // 原始数据备份
8.2 数据变更监听
8.2.1 简单值监听
typescript
// 监听充值状态变更
watch(rechargeStatus, () => {
if (rechargeStatus.value !== oldData.value?.rechargeStatus) {
changeValue.value = true;
} else {
changeValue.value = false;
}
});
// 监听充值说明变更
watch(rechargeExplain, () => {
if (rechargeExplain.value !== oldData.value?.rechargeExplain) {
changeValue.value = true;
} else {
changeValue.value = false;
}
});
8.2.2 深度对象监听
typescript
// 深度比较函数
const isEqual = (arr1: RechargeRule[], arr2: RechargeRule[] | undefined) => {
if (!arr2) return false;
if (arr1.length !== arr2.length) return false;
return arr1.every((item, index) => {
if (
item?.power !== arr2[index]?.power ||
item?.givePower !== arr2[index]?.givePower ||
item?.sellPrice !== arr2[index]?.sellPrice ||
item?.label !== arr2[index]?.label
) {
return false;
} else {
return true;
}
});
};
// 深度监听充值规则数组
watch(
rechargeRules,
() => {
if (!isEqual(rechargeRules.value, oldData.value?.rechargeRule)) {
changeValue.value = true;
} else {
changeValue.value = false;
}
},
{ deep: true },
);
8.3 数据流向图
API数据 oldData备份 响应式状态 用户界面 用户操作 状态更新 Watch监听 变更检测 UI反馈 保存操作 API调用
8.4 生命周期管理
typescript
// 组件挂载时初始化数据
onMounted(() => {
getRechargeRules();
});
// 数据获取函数
const getRechargeRules = async () => {
const data = await apiGetRechargeRules();
oldData.value = data; // 备份原始数据
rechargeRules.value = data.rechargeRule.map((item) => ({ ...item })); // 深拷贝
rechargeStatus.value = data.rechargeStatus;
rechargeExplain.value = data.rechargeExplain;
};
9. 表单验证与用户交互
9.1 输入验证机制
9.1.1 数值输入验证
vue
<!-- 充值数量输入 -->
<UInput v-model="row.original.power" type="number" />
<!-- 赠送数量输入 -->
<UInput v-model="row.original.givePower" type="number" />
<!-- 价格输入(带最小值和步长限制) -->
<UInput
v-model="row.original.sellPrice"
type="number"
min="0"
step="0.01"
/>
验证特性:
type="number": 限制只能输入数字min="0": 防止输入负数step="0.01": 支持小数点后两位
9.1.2 实时验证反馈
typescript
// 数据变更时实时检测
watch(rechargeRules, () => {
if (!isEqual(rechargeRules.value, oldData.value?.rechargeRule)) {
changeValue.value = true; // 启用保存按钮
} else {
changeValue.value = false; // 禁用保存按钮
}
}, { deep: true });
9.2 用户反馈机制
9.2.1 成功反馈
typescript
// 保存成功提示
toast.success(t("console-marketing.packageManagement.saveSuccess"));
9.2.2 错误处理
typescript
try {
await saveRechargeRules(data);
// 成功处理
} catch (error) {
console.error(error);
// 错误处理
}
9.2.3 按钮状态控制
vue
<UButton
color="primary"
:disabled="!changeValue" <!-- 根据数据变更状态控制按钮可用性 -->
@click="saveRules"
>
{{ t("console-marketing.packageManagement.button.save") }}
</UButton>
9.3 改进建议
9.3.1 删除确认对话框
typescript
// 建议使用模态确认对话框
const deleteRow = async (row: RechargeRule) => {
const confirmed = await useModal().confirm({
title: "确认删除",
content: "确定要删除这个充值套餐吗?此操作不可撤销。",
confirmText: "删除",
cancelText: "取消"
});
if (confirmed) {
rechargeRules.value = rechargeRules.value.filter((item) => item !== row);
}
};
9.3.2 表单验证增强
typescript
// 可以添加更严格的验证规则
const validateRule = (rule: RechargeRule): boolean => {
if (rule.power <= 0) {
toast.error("充值数量必须大于0");
return false;
}
if (rule.sellPrice <= 0) {
toast.error("售价必须大于0");
return false;
}
if (!rule.label.trim()) {
toast.error("标签不能为空");
return false;
}
return true;
};
10. 技术架构总结
10.1 技术栈
- 前端框架: Vue 3 + Nuxt.js
- UI组件库: Nuxt UI (基于Tailwind CSS)
- 状态管理: Vue 3 Composition API + Pinia
- 类型系统: TypeScript
- 国际化: Vue I18n
- HTTP客户端: Nuxt内置的$fetch
- 权限控制: 自定义AccessControl组件
10.2 架构特点
10.2.1 组件化设计
- 单文件组件: 使用Vue SFC格式,逻辑、模板、样式集中管理
- 组合式API: 采用Composition API提高代码复用性
- 类型安全: 全面使用TypeScript确保类型安全
10.2.2 响应式数据流
- 双向绑定: 表单数据与状态自动同步
- 深度监听: 复杂对象变更的精确检测
- 状态管理: 清晰的数据流向和状态变更
10.2.3 用户体验优化
- 实时反馈: 数据变更即时反映在UI上
- 权限控制: 基于用户权限的功能访问控制
- 国际化: 完整的多语言支持
10.2.4 可维护性
- 模块化: 清晰的文件组织和模块划分
- 类型定义: 完整的TypeScript类型定义
- 代码复用: 通用组件和工具函数的抽象
10.3 设计模式
10.3.1 MVVM模式
View (Template) ↔ ViewModel (Script) ↔ Model (API/Store)
10.3.2 组件通信
- Props Down: 父组件向子组件传递数据
- Events Up: 子组件向父组件发送事件
- Provide/Inject: 跨层级组件通信
10.3.3 状态管理模式
State → View → Action → State
10.4 性能优化
10.4.1 响应式优化
- ref vs reactive: 合理选择响应式API
- 深度监听: 仅在必要时使用deep watch
- 计算属性: 缓存复杂计算结果
10.4.2 组件优化
- 懒加载: 按需加载组件和模块
- 虚拟滚动: 大数据量表格的性能优化
- 防抖节流: 用户输入的性能优化
10.5 安全性考虑
10.5.1 权限控制
- 前端权限: AccessControl组件控制UI显示
- 后端验证: API层面的权限验证
- 角色管理: 基于角色的访问控制
10.5.2 数据验证
- 客户端验证: 表单数据的前端验证
- 服务端验证: API接口的后端验证
- 类型安全: TypeScript类型检查
10.6 扩展性设计
10.6.1 组件扩展
- 插槽机制: 支持内容自定义
- 属性配置: 灵活的组件配置
- 事件系统: 完整的事件通信
10.6.2 功能扩展
- 模块化: 新功能模块的独立开发
- 插件系统: 支持第三方插件扩展
- 配置化: 通过配置文件控制功能
FastbuildAI的新建套餐功能展现了现代Vue.js应用的最佳实践,通过Vue 3 + Nuxt.js + TypeScript的技术组合,实现了一个功能完善、用户体验优秀、可维护强劲的前端管理界面。该实现不仅满足了当前的业务需求,也为未来的功能扩展提供了良好的技术基础。