Files
meijiaka-zy/tauri-app/src/components/PricingModal/PricingModal.tsx
T
小鱼开发 11a85bfee7 fix: 修复 BGM 本地上传、封面形象样式、ESLint 清零、access log 关闭
- 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 构建产物
2026-05-27 18:37:33 +08:00

123 lines
4.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
}