feat: 操作按钮展示积分消耗提示
后端: - 新增 GET /points/rules 返回积分计费规则列表 前端: - 各操作按钮添加积分消耗提示: - 固定积分: 生成脚本(5)/润色(1)/标题生成(1)/字幕烧录(2)/封面设计(2)/压制成片(5) - 按秒计费: 配音合成/视频生成 显示'按秒计费'
This commit is contained in:
@@ -19,3 +19,4 @@ __pycache__/
|
||||
# Environment
|
||||
.env
|
||||
.env.local
|
||||
test_kick.sh
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user