feat: 操作按钮展示积分消耗提示

后端:
- 新增 GET /points/rules 返回积分计费规则列表

前端:
- 各操作按钮添加积分消耗提示:
  - 固定积分: 生成脚本(5)/润色(1)/标题生成(1)/字幕烧录(2)/封面设计(2)/压制成片(5)
  - 按秒计费: 配音合成/视频生成 显示'按秒计费'
This commit is contained in:
小鱼开发
2026-05-13 14:36:05 +08:00
parent 86486fa4d5
commit 4579fa78d4
11 changed files with 64 additions and 13 deletions
+1
View File
@@ -19,3 +19,4 @@ __pycache__/
# Environment
.env
.env.local
test_kick.sh
+33
View File
@@ -451,6 +451,39 @@ async def get_chargeable_types(
return success_response(data=types, message="获取扣费业务类型成功")
# ── 积分规则查询 ──────────────────────────────────────
@router.get("/rules", response_model=ApiResponse[list[dict]])
async def get_points_rules(
current_user: User = Depends(get_current_user),
):
"""
获取积分计费规则列表。
返回所有业务的积分消耗规则,供前端在操作按钮上展示积分提示。
"""
rules = []
for source_type, cfg in point_service.POINTS_CONFIG.items():
if source_type.startswith("_"):
continue
rule = {
"source_type": source_type,
"mode": cfg["mode"],
}
if cfg["mode"] == "fixed":
rule["points"] = cfg["points"]
elif cfg["mode"] == "duration":
rule["min_points"] = cfg.get("min_points", 1)
if "divisor" in cfg:
rule["unit"] = f"{cfg['divisor']}"
rule["points_per_unit"] = 1
if "multiplier" in cfg:
rule["unit"] = "每秒"
rule["points_per_unit"] = cfg["multiplier"]
rules.append(rule)
return success_response(data=rules, message="获取积分规则成功")
# ── 积分预估查询 ──────────────────────────────────────
@router.get("/cost")
+17
View File
@@ -61,7 +61,24 @@ interface PointTransactionList {
// ── API 方法 ──────────────────────────────────────────
export interface PointRule {
sourceType: string;
mode: 'fixed' | 'duration' | 'free';
points?: number;
minPoints?: number;
unit?: string;
pointsPerUnit?: number;
}
export const pointsApi = {
/**
* 获取积分计费规则列表
* GET /points/rules
*/
getRules: async (): Promise<PointRule[]> => {
return client.get<PointRule[]>('/points/rules');
},
/**
* 获取当前用户积分余额
* GET /points/balance
+1 -1
View File
@@ -53,7 +53,7 @@ export default function Modal({
? { justifyContent: 'center' }
: title
? undefined
: { justifyContent: 'flex-end', borderBottom: 'none', paddingBottom: 0 }
: { justifyContent: 'flex-end', borderBottom: 'none', padding: '0 var(--spacing-lg)' }
}
>
{title && (
@@ -335,7 +335,7 @@ export default function VoiceMaterialLibrary() {
<Modal
open={uploadModalOpen}
onClose={() => setUploadModalOpen(false)}
title="上传音频样本"
title=""
width="480px"
>
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
@@ -402,7 +402,7 @@ export default function VoiceMaterialLibrary() {
onClick={handleUpload}
disabled={!uploadName.trim() || !selectedFile}
>
</button>
</div>
</div>
@@ -415,7 +415,7 @@ export default function CoverDesign() {
onClick={handleGenerate}
style={{ marginTop: 'auto', flexShrink: 0 }}
>
2
</button>
</div>
@@ -370,7 +370,7 @@ export default function ScriptCreation() {
>
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2" />
</svg>
{generated ? '重新生成脚本' : '生成脚本'}
{generated ? '重新生成脚本5积分)' : '生成脚本5积分)'}
</>
)}
</button>
@@ -619,7 +619,7 @@ export default function SubtitleBurning() {
onClick={handleBurn}
disabled={!alignment?.utterances?.length}
>
2
</button>
<button
className={`btn burn-btn ${previewMode === 'result' ? 'btn-primary' : 'btn-secondary'}`}
@@ -635,7 +635,7 @@ export default function SubtitleBurning() {
onClick={handleBurn}
disabled={isBurning || !alignment?.utterances?.length}
>
{isBurning ? '压制中...' : '压制字幕'}
{isBurning ? '压制中...' : '压制字幕2积分)'}
</button>
)}
</div>
@@ -367,7 +367,7 @@ export default function VideoCompose() {
>
<polygon points="5 3 19 12 5 21 5 3" />
</svg>
5
</>
)}
</button>
@@ -413,7 +413,7 @@ export default function VideoCompose() {
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
5
</button>
</div>
)}
@@ -1162,7 +1162,7 @@ export default function VideoGeneration() {
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M23 4v6h-6M1 20v-6h6M3.51 9a9 9 0 0114.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0020.49 15" />
</svg>
</>
)}
</button>
@@ -1199,7 +1199,7 @@ export default function VideoGeneration() {
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polygon points="5 3 19 12 5 21 5 3" />
</svg>
</>
)}
</button>
@@ -501,12 +501,12 @@ export default function VoiceSynthesis() {
<div className="voice-generate-wrap">
{!hasGeneratedAudio ? (
<button className="btn btn-primary generate-btn" onClick={handleGenerate} disabled={isGenerating || !mergedText.trim()}>
{isGenerating ? '合成中...' : '合成配音'}
{isGenerating ? '合成中...' : '合成配音(按秒计费)'}
</button>
) : (
<div className="voice-generate-btns">
<button className="btn btn-secondary generate-btn" onClick={handleGenerate} disabled={isGenerating || !mergedText.trim()}>
{isGenerating ? '合成中...' : '重新生成'}
{isGenerating ? '合成中...' : '重新生成(按秒计费)'}
</button>
<button
className="btn btn-primary generate-btn"