11a85bfee7
- BGM 本地上传改用 Tauri open 对话框,修复 path 为空导致混音失效 - Rust 端放宽 BGM 路径验证(系统文件选择器选取的文件),加路径遍历防护 - BGM 混音失败时 toast 提示,不再静默忽略 - 我的作品页增加导出功能 - 封面形象卡片样式统一为 works-card 体系 - 关闭 uvicorn access log(Dockerfile + 3 个 compose) - ESLint 全绿:关掉 prop-types/incompatible-library,修复 curly/exhaustive-deps/any/unused-vars - .gitignore 排除 *.exe 构建产物
123 lines
4.5 KiB
TypeScript
123 lines
4.5 KiB
TypeScript
import { useEffect, useState } from 'react';
|
||
import { pointsApi, type PointRule } from '../../api/modules/points';
|
||
import Modal from '../Modal/Modal';
|
||
|
||
const SOURCE_TYPE_LABELS: Record<string, string> = {
|
||
script: '脚本生成',
|
||
polish: '文案润色',
|
||
title: '标题生成',
|
||
tts: '配音合成',
|
||
voice_clone: '声音复刻',
|
||
video: '视频生成',
|
||
compose: '压制成片',
|
||
subtitle_burn: '字幕烧录',
|
||
cover_design: '封面设计',
|
||
cover_avatar: '封面人物形象',
|
||
caption: '字幕生成',
|
||
};
|
||
|
||
const ClockIcon = () => (
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||
<circle cx="12" cy="12" r="10" /><polyline points="12 6 12 12 16 14" />
|
||
</svg>
|
||
);
|
||
|
||
interface PricingModalProps {
|
||
open: boolean;
|
||
onClose: () => void;
|
||
}
|
||
|
||
export default function PricingModal({ open, onClose }: PricingModalProps) {
|
||
const [rules, setRules] = useState<PointRule[]>([]);
|
||
const [loading, setLoading] = useState(false);
|
||
|
||
useEffect(() => {
|
||
if (!open || rules.length > 0) {return;}
|
||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||
setLoading(true);
|
||
pointsApi.getRules()
|
||
.then(setRules)
|
||
.catch((e) => console.error('[PricingModal] 获取积分规则失败:', e))
|
||
.finally(() => setLoading(false));
|
||
}, [open, rules.length]);
|
||
|
||
return (
|
||
<Modal open={open} onClose={onClose} width="600px" maxHeight="none">
|
||
{loading ? (
|
||
<div className="profile-pricing-loading">
|
||
加载中...
|
||
</div>
|
||
) : (
|
||
<div className="profile-pricing-body">
|
||
{/* 表格 */}
|
||
<div className="profile-pricing-table">
|
||
<div className="profile-pricing-header">
|
||
<span>功能名称</span>
|
||
<span>计费方式</span>
|
||
<span>积分消耗</span>
|
||
</div>
|
||
{rules
|
||
.filter((rule) => rule.mode !== 'free')
|
||
.map((rule) => (
|
||
<div key={rule.sourceType} className="profile-pricing-row">
|
||
<span className="profile-pricing-name">
|
||
{SOURCE_TYPE_LABELS[rule.sourceType] || rule.sourceType}
|
||
</span>
|
||
<span className="profile-pricing-mode">
|
||
<span className={`profile-pricing-tag ${rule.mode}`}>
|
||
{rule.mode === 'fixed' ? '固定' : '按时长'}
|
||
</span>
|
||
</span>
|
||
<span className="profile-pricing-points">
|
||
{rule.mode === 'fixed'
|
||
? `${rule.points} 积分/次`
|
||
: `${rule.unit} ${rule.pointsPerUnit} 积分`}
|
||
</span>
|
||
</div>
|
||
))}
|
||
{rules.filter((rule) => rule.mode !== 'free').length === 0 && (
|
||
<div className="profile-pricing-empty">暂无计费项目</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* 按时长细则 */}
|
||
{rules.some((r) => r.mode === 'duration') && (
|
||
<div className="profile-pricing-detail-section">
|
||
<div className="profile-pricing-detail-title">
|
||
<ClockIcon />
|
||
按时长计费细则
|
||
</div>
|
||
<div className="profile-pricing-detail-list">
|
||
{rules
|
||
.filter((r) => r.mode === 'duration')
|
||
.map((rule) => {
|
||
const contentType =
|
||
rule.sourceType === 'tts' ? '音频'
|
||
: rule.sourceType === 'video' ? '视频'
|
||
: '内容';
|
||
const unitNum = (rule.unit?.match(/\d+/) || ['1'])[0];
|
||
return (
|
||
<div key={rule.sourceType} className="profile-pricing-detail-row">
|
||
<strong>{SOURCE_TYPE_LABELS[rule.sourceType] || rule.sourceType}</strong>
|
||
<span>
|
||
按生成{contentType}的实际时长计费,{rule.unit}消耗 {rule.pointsPerUnit} 积分,不足{unitNum}秒按{unitNum}秒计。
|
||
{rule.sourceType === 'video' && (
|
||
<>
|
||
<br />
|
||
使用系统素材每个空镜额外消耗 2 积分。
|
||
</>
|
||
)}
|
||
</span>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
</div>
|
||
)}
|
||
</Modal>
|
||
);
|
||
}
|