合并修改
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# VITE_API_BASE=http://192.168.120.17:8000
|
||||
#VITE_API_BASE=https://apiforeign.minzhong.cn
|
||||
VITE_API_BASE=http://ceshi.apiforeign.minzhong.cn
|
||||
VITE_API_BASE=https://ceshi.apiforeign.minzhongzc.com
|
||||
VITE_USE_MOCK=false
|
||||
# Encryption disabled for dev — enable in production
|
||||
VITE_ENCRYPTION_KEY=
|
||||
Vendored
+19
-19
File diff suppressed because one or more lines are too long
Vendored
+36
-36
@@ -1,37 +1,37 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
|
||||
<title>后台管理</title>
|
||||
<script>
|
||||
(function() {
|
||||
var cached = localStorage.getItem('siteInfo');
|
||||
if (cached) {
|
||||
try {
|
||||
var info = JSON.parse(cached);
|
||||
if (info.siteName) {
|
||||
document.title = info.siteName + ' - 管理后台';
|
||||
}
|
||||
if (info.siteLogo) {
|
||||
var link = document.querySelector('link[rel="icon"]');
|
||||
if (link) {
|
||||
link.href = info.siteLogo;
|
||||
link.type = 'image/png';
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<script type="module" crossorigin src="/assets/index-BLkGzXo7.js"></script>
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
|
||||
<title>后台管理</title>
|
||||
<script>
|
||||
(function() {
|
||||
var cached = localStorage.getItem('siteInfo');
|
||||
if (cached) {
|
||||
try {
|
||||
var info = JSON.parse(cached);
|
||||
if (info.siteName) {
|
||||
document.title = info.siteName + ' - 管理后台';
|
||||
}
|
||||
if (info.siteLogo) {
|
||||
var link = document.querySelector('link[rel="icon"]');
|
||||
if (link) {
|
||||
link.href = info.siteLogo;
|
||||
link.type = 'image/png';
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<script type="module" crossorigin src="/assets/index-MWrMIMSc.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-D7ShJUt4.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>␍
|
||||
</body>
|
||||
</html>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -48,6 +48,8 @@ const AdminSettings: React.FC = () => {
|
||||
setConfigs(data);
|
||||
const formValues: Record<string, any> = {};
|
||||
data.forEach(c => { formValues[c.key] = c.value; });
|
||||
// 预扣积分默认值
|
||||
if (!formValues.optimize_hold_credits) formValues.optimize_hold_credits = '5';
|
||||
formValues.resource_capacity_enabled = capacity.enabled;
|
||||
formValues.resource_capacity_limit_value = capacity.limitValue || '1.000';
|
||||
formValues.resource_capacity_limit_unit = capacity.limitUnit || 'GB';
|
||||
@@ -69,6 +71,18 @@ const AdminSettings: React.FC = () => {
|
||||
await updateSystemConfig(config.id, String(newVal ?? ''));
|
||||
}
|
||||
}
|
||||
// AI创作预扣积分 - 不存在则创建
|
||||
const holdVal = values.optimize_hold_credits;
|
||||
if (holdVal !== undefined && holdVal !== null && holdVal !== '') {
|
||||
const existing = configs.find(c => c.key === 'optimize_hold_credits');
|
||||
if (existing) {
|
||||
if (String(holdVal) !== existing.value) {
|
||||
await updateSystemConfig(existing.id, String(holdVal));
|
||||
}
|
||||
} else {
|
||||
await createSystemConfig('optimize_hold_credits', String(holdVal), '提示词理解预扣积分数量(防止并发超卖)');
|
||||
}
|
||||
}
|
||||
await saveGlobalResourceCapacity({
|
||||
enabled: !!values.resource_capacity_enabled,
|
||||
limitValue: String(values.resource_capacity_limit_value ?? '1.000'),
|
||||
@@ -179,6 +193,7 @@ const AdminSettings: React.FC = () => {
|
||||
'SEO 设置': configs.filter(c => c.key.startsWith('seo_')),
|
||||
'用户积分配置': configs.filter(c => c.key.startsWith('user_') && c.key.includes('credits')),
|
||||
'其他配置': configs.filter(c => c.key === 'operation_manual'),
|
||||
'AI创作配置': configs.filter(c => c.key === 'optimize_hold_credits'),
|
||||
};
|
||||
|
||||
const getFieldDescription = (config: SystemConfig): string => {
|
||||
@@ -194,6 +209,7 @@ const AdminSettings: React.FC = () => {
|
||||
user_login_credits: '用户每日登录赠送的积分数量',
|
||||
user_login_credits_enabled: '是否启用每日登录赠送积分功能',
|
||||
operation_manual: '操作手册链接,前台用户菜单将展示该入口,点击跳转此链接',
|
||||
optimize_hold_credits: 'AI创作时预扣积分数量,用于防止并发超卖。预扣后按实际消耗多退少补',
|
||||
};
|
||||
return descMap[config.key] || config.description || '';
|
||||
};
|
||||
@@ -323,8 +339,8 @@ const AdminSettings: React.FC = () => {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (config.key === 'user_register_credits' || config.key === 'user_login_credits') {
|
||||
return <Input type="number" min={0} placeholder={config.description} size="large" />;
|
||||
if (config.key === 'user_register_credits' || config.key === 'user_login_credits' || config.key === 'optimize_hold_credits') {
|
||||
return <Input type="number" min={1} placeholder={config.description} size="large" />;
|
||||
}
|
||||
return <Input placeholder={config.description} size="large" />;
|
||||
};
|
||||
@@ -384,6 +400,14 @@ const AdminSettings: React.FC = () => {
|
||||
{getFieldComponent(config)}
|
||||
</Form.Item>
|
||||
))}
|
||||
{/* AI创作预扣积分 - 固定显示 */}
|
||||
<Form.Item
|
||||
name="optimize_hold_credits"
|
||||
label={<span style={{ fontWeight: 500 }}>提示词理解预扣积分数量</span>}
|
||||
extra="AI创作时预扣积分数量,用于防止并发超卖。预扣后按实际消耗多退少补"
|
||||
>
|
||||
<Input type="number" min={1} placeholder="默认5" size="large" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
</Form>
|
||||
),
|
||||
|
||||
@@ -342,7 +342,7 @@ async def get_site_info(db: AsyncSession = Depends(get_db)):
|
||||
"""Public endpoint returning site name, logo, agreement and copyright info."""
|
||||
result = await db.execute(
|
||||
select(SystemConfig).where(SystemConfig.key.in_([
|
||||
"site_name", "site_logo", "user_agreement_privacy_url", "site_copyright", "operation_manual", "login_bg_video"
|
||||
"site_name", "site_logo", "user_agreement_privacy_url", "site_copyright", "operation_manual", "login_bg_video", "optimize_hold_credits"
|
||||
]))
|
||||
)
|
||||
configs = result.scalars().all()
|
||||
@@ -366,6 +366,7 @@ async def get_site_info(db: AsyncSession = Depends(get_db)):
|
||||
"site_copyright": info.get("site_copyright", "© 2024 民众智创 版权所有"),
|
||||
"operation_manual": info.get("operation_manual", ""),
|
||||
"login_bg_video": to_full_url(info.get("login_bg_video")) if info.get("login_bg_video") else "",
|
||||
"optimize_hold_credits": int(info.get("optimize_hold_credits") or 5),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -10,11 +10,11 @@ from fastapi.responses import RedirectResponse
|
||||
from sqlalchemy import select, func
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.config import settings
|
||||
from app.dependencies import get_db, get_current_user
|
||||
from app.models.user import User
|
||||
from app.models.project import Project
|
||||
from app.models.generation_record import GenerationRecord
|
||||
from app.models.system_config import SystemConfig
|
||||
from app.schemas.generation import (
|
||||
OptimizeParams,
|
||||
GenerateParams,
|
||||
@@ -32,7 +32,7 @@ from app.services.generation.pipeline.db_lock_service import (
|
||||
DatabaseRowLockBusy,
|
||||
execute_with_lock_timeout,
|
||||
)
|
||||
from app.services.credits import deduct_credits, calc_text_credits
|
||||
from app.services.credits import deduct_credits, add_credits, calc_text_credits
|
||||
from app.services.llm import optimize_prompt
|
||||
from app.services.video_url import generate_temp_url, validate_and_get_record_id, get_video_stream_url
|
||||
from app.services.resource_accounting_service import (
|
||||
@@ -271,6 +271,17 @@ async def optimize(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
record = None
|
||||
# 预扣积分(SELECT FOR UPDATE 锁行 + 检查 + 扣费,防止并发超卖)
|
||||
hold_credits = 5
|
||||
_hold_cfg = await db.execute(select(SystemConfig).where(SystemConfig.key == "optimize_hold_credits").limit(1))
|
||||
_hold_row = _hold_cfg.scalar_one_or_none()
|
||||
if _hold_row and _hold_row.value:
|
||||
try:
|
||||
hold_credits = int(_hold_row.value)
|
||||
except (ValueError, TypeError):
|
||||
hold_credits = 5
|
||||
await deduct_credits(db, current_user.id, hold_credits, "AI创作预扣积分", biz_key=f"optimize_hold:{req.idempotency_key or 'nokey'}")
|
||||
await db.commit()
|
||||
# Validate parameters based on generation type
|
||||
if req.gen_type == GenerationType.video:
|
||||
if req.duration not in DURATIONS:
|
||||
@@ -374,6 +385,13 @@ async def optimize(
|
||||
db, token_usage["input_tokens"], token_usage["output_tokens"],
|
||||
)
|
||||
|
||||
# 积分结算:预扣金额按实际消耗多退少补
|
||||
if text_credits < hold_credits:
|
||||
await add_credits(db, current_user.id, round(hold_credits - text_credits, 2), f"提示词优化积分退还- {project.name}", biz_key=f"optimize_refund:{req.idempotency_key or 'nokey'}")
|
||||
elif text_credits > hold_credits:
|
||||
await deduct_credits(db, current_user.id, round(text_credits - hold_credits, 2), f"提示词优化补扣积分 - {project.name}", biz_key=f"optimize_topup:{req.idempotency_key or 'nokey'}")
|
||||
await db.commit()
|
||||
|
||||
failed_record_id = record.id
|
||||
failed_user_id = current_user.id
|
||||
try:
|
||||
|
||||
@@ -129,7 +129,7 @@ class Settings(BaseSettings):
|
||||
|
||||
CAPTCHA_ENABLED: bool = True
|
||||
|
||||
BASE_URL: str = "http://ceshi.apiforeign.minzhong.cn"
|
||||
BASE_URL: str = "https://ceshi.apiforeign.minzhongzc.com"
|
||||
|
||||
CORS_ORIGINS: list[str] = ["*"]
|
||||
|
||||
|
||||
@@ -185,6 +185,7 @@ async def _seed_data():
|
||||
("user_login_credits_enabled", "false", "启用每日登录赠送积分"),
|
||||
# Operation manual
|
||||
("operation_manual", "", "操作手册链接"),
|
||||
("optimize_hold_credits", "5", "AI创作预扣积分数量(防止并发超卖)"),
|
||||
]
|
||||
for key, value, desc in configs:
|
||||
existing = await db.execute(
|
||||
|
||||
@@ -68,7 +68,7 @@ async def _build_user_content(record: ChatGenerationTask, db: AsyncSession | Non
|
||||
if ref_type == "image":
|
||||
parts.append({"type": "image_url", "image_url": {"url": url}})
|
||||
elif ref_type == "video":
|
||||
parts.append({"type": "video_url", "video_url": {"url": url, "fps": settings.CHATAPI_VIDEO_FPS}})
|
||||
parts.append({"type": "video_url", "video_url": {"url": url}})
|
||||
return parts
|
||||
|
||||
|
||||
|
||||
@@ -742,8 +742,8 @@ def build_user_message(user_content: str, references: list[dict[str, str]], refe
|
||||
content_parts.append({"type": "image_url", "image_url": {"url": ref_url}})
|
||||
log_content_parts.append({"type": "image_url", "image_url": {"url": ref_url}})
|
||||
elif ref_type == "video":
|
||||
content_parts.append({"type": "video_url", "video_url": {"url": ref_url, "fps": reference_video_fps}})
|
||||
log_content_parts.append({"type": "video_url", "video_url": {"url": ref_url, "fps": reference_video_fps}})
|
||||
content_parts.append({"type": "video_url", "video_url": {"url": ref_url}})
|
||||
log_content_parts.append({"type": "video_url", "video_url": {"url": ref_url}})
|
||||
return {"role": "user", "content": content_parts}, {"role": "user", "content": log_content_parts}
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import base64
|
||||
import json
|
||||
import mimetypes
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
@@ -292,22 +290,15 @@ async def _call_openai_compatible(
|
||||
|
||||
def _file_url_or_data_uri(file_url: str, fallback_mime: str) -> str:
|
||||
"""
|
||||
Convert local upload path to base64 data URI.
|
||||
Keep remote http/https/data URLs as-is.
|
||||
远程 URL 保持原样;本地上传路径拼接 BASE_URL 转为完整链接。
|
||||
"""
|
||||
if file_url.startswith(("http://", "https://", "data:")):
|
||||
return file_url
|
||||
|
||||
# Compatible with /uploads/xxx and plain relative paths
|
||||
relative_path = file_url.replace("/uploads/", "", 1).lstrip("/")
|
||||
file_path = os.path.join(settings.UPLOAD_LOCAL_PATH, relative_path)
|
||||
|
||||
mime = mimetypes.guess_type(file_path)[0] or fallback_mime
|
||||
|
||||
with open(file_path, "rb") as f:
|
||||
b64 = base64.b64encode(f.read()).decode()
|
||||
|
||||
return f"data:{mime};base64,{b64}"
|
||||
# 本地上传路径拼接 BASE_URL,例如 /uploads/images/xxx.png → https://domain.com/uploads/images/xxx.png
|
||||
base = settings.BASE_URL.rstrip("/")
|
||||
path = file_url if file_url.startswith("/") else f"/{file_url}"
|
||||
return f"{base}{path}"
|
||||
|
||||
if image_urls or video_urls:
|
||||
user_message, log_user_message = await _build_multimodal_content(
|
||||
|
||||
@@ -212,9 +212,9 @@ def build_resource_signed_url(
|
||||
|
||||
http://www.test6.com/generation/video/a.mp4?from=history&exp=1780000000&sign=xxxx
|
||||
"""
|
||||
if not resource_url:
|
||||
if not resource_url or not resource_url.startswith("/generate/"):
|
||||
return resource_url
|
||||
|
||||
|
||||
seconds = _get_sign_expire_seconds(expire_seconds)
|
||||
current_ts = _to_int(now_ts, int(time.time())) if now_ts is not None else int(time.time())
|
||||
|
||||
|
||||
@@ -88,7 +88,6 @@ async def build_user_message(user_text: str, video_url: str, db=None) -> tuple[d
|
||||
"type": "video_url",
|
||||
"video_url": {
|
||||
"url": real_url,
|
||||
"fps": _video_fps(),
|
||||
},
|
||||
},
|
||||
{"type": "text", "text": user_text},
|
||||
@@ -98,7 +97,6 @@ async def build_user_message(user_text: str, video_url: str, db=None) -> tuple[d
|
||||
"type": "video_url",
|
||||
"video_url": {
|
||||
"url": video_url,
|
||||
"fps": _video_fps(),
|
||||
},
|
||||
},
|
||||
{"type": "text", "text": user_text},
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
|
||||
# VITE_API_BASE=http://192.168.120.17:8000
|
||||
#VITE_API_BASE=https://apiforeign.minzhong.cn
|
||||
VITE_API_BASE=http://ceshi.apiforeign.minzhong.cn
|
||||
#VITE_API_BASE=https://apiforeign.minzhongzc.com
|
||||
VITE_API_BASE=https://ceshi.apiforeign.minzhongzc.com
|
||||
VITE_USE_MOCK=false
|
||||
# Encryption disabled for dev — enable in production
|
||||
VITE_ENCRYPTION_KEY=
|
||||
|
||||
+613
File diff suppressed because one or more lines are too long
Vendored
+2
-1
@@ -28,7 +28,8 @@
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<script type="module" crossorigin src="/assets/index-Dej_BHGv.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-mLPAhw5a.js"></script>
|
||||
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-Bsz_Xon-.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -3893,6 +3893,12 @@ const AIChatPage: React.FC = () => {
|
||||
}}>
|
||||
{selectedRatio === 'auto' ? '智能' : selectedRatio} · {selectedResolution === '1K' ? '1K标清' : selectedResolution === '2K' ? '2K高清' : '4K超清'} · {width}×{height}
|
||||
</Text>
|
||||
{multiGenerationEnabled && (
|
||||
<>
|
||||
<span style={{ width: 1, height: 14, background: '#E7EAF0' }} />
|
||||
<span style={{ fontSize: 13, fontWeight: 600, color: '#8b5cf6' }}>{generationCount}</span>
|
||||
</>
|
||||
)}
|
||||
<CaretDownOutlined style={{ fontSize: 10, color: '#8b5cf6', marginLeft: 'auto' }} />
|
||||
</button>
|
||||
|
||||
@@ -4153,6 +4159,57 @@ const AIChatPage: React.FC = () => {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 生成数量 */}
|
||||
{multiGenerationEnabled && (
|
||||
<div style={{ marginTop: 16, paddingTop: 16, borderTop: '1px solid #f0f0f5' }}>
|
||||
<Text style={{
|
||||
display: 'block',
|
||||
marginBottom: 8,
|
||||
fontSize: 12,
|
||||
fontWeight: 500,
|
||||
color: '#667085',
|
||||
}}>
|
||||
生成数量
|
||||
</Text>
|
||||
<div style={{ display: 'flex', gap: 6 }}>
|
||||
{Array.from({ length: effectiveMaxGenerationCount }, (_, i) => i + 1).map((num) => (
|
||||
<button
|
||||
key={num}
|
||||
onClick={() => setGenerationCount(num)}
|
||||
disabled={effectiveMaxGenerationCount <= 1}
|
||||
style={{
|
||||
flex: 1,
|
||||
height: 42,
|
||||
borderRadius: 6,
|
||||
border: generationCount === num
|
||||
? '2px solid #8b5cf6'
|
||||
: '1px solid #E7EAF0',
|
||||
backgroundColor: generationCount === num
|
||||
? '#8b5cf6'
|
||||
: '#FFFFFF',
|
||||
cursor: effectiveMaxGenerationCount <= 1 ? 'not-allowed' : 'pointer',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
transition: 'all 0.2s',
|
||||
opacity: effectiveMaxGenerationCount <= 1 ? 0.5 : 1,
|
||||
}}
|
||||
>
|
||||
<span style={{
|
||||
fontSize: 14,
|
||||
fontWeight: 600,
|
||||
color: generationCount === num
|
||||
? '#fff'
|
||||
: '#475467',
|
||||
}}>
|
||||
{num}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -4201,6 +4258,12 @@ const AIChatPage: React.FC = () => {
|
||||
<span style={{ fontSize: 13, fontWeight: 600, color: '#2f3440' }}>{videoResolution?.toUpperCase?.() || videoResolution}</span>
|
||||
<span style={{ width: 1, height: 14, background: '#E7EAF0' }} />
|
||||
<span style={{ fontSize: 13, fontWeight: 600, color: '#2f3440' }}>{videoDuration}秒</span>
|
||||
{multiGenerationEnabled && (
|
||||
<>
|
||||
<span style={{ width: 1, height: 14, background: '#E7EAF0' }} />
|
||||
<span style={{ fontSize: 13, fontWeight: 600, color: '#8b5cf6' }}>{generationCount}份</span>
|
||||
</>
|
||||
)}
|
||||
<CaretDownOutlined style={{ fontSize: 10, color: '#667085', marginLeft: 'auto' }} />
|
||||
</button>
|
||||
|
||||
@@ -4351,17 +4414,34 @@ const AIChatPage: React.FC = () => {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
padding: '4px 12px',
|
||||
backgroundColor: '#F6F7FA',
|
||||
borderRadius: 6,
|
||||
}}>
|
||||
<span style={{ fontSize: 14, fontWeight: 600, color: '#667085' }}>
|
||||
{videoDuration}
|
||||
</span>
|
||||
<input
|
||||
type="number"
|
||||
min={Math.min(...engineOptions.durations)}
|
||||
max={Math.max(...engineOptions.durations)}
|
||||
value={videoDuration}
|
||||
onChange={(e) => {
|
||||
const v = Number(e.target.value);
|
||||
if (!isNaN(v)) {
|
||||
const clamped = Math.min(Math.max(v, Math.min(...engineOptions.durations)), Math.max(...engineOptions.durations));
|
||||
setVideoDuration(clamped);
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
width: 56,
|
||||
height: 32,
|
||||
textAlign: 'center',
|
||||
border: '1px solid #E7EAF0',
|
||||
borderRadius: 6,
|
||||
fontSize: 14,
|
||||
fontWeight: 600,
|
||||
color: '#2f3440',
|
||||
outline: 'none',
|
||||
}}
|
||||
/>
|
||||
<span style={{ fontSize: 12, color: '#98a2b3' }}>秒</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* 选择分辨率 */}
|
||||
@@ -4410,6 +4490,57 @@ const AIChatPage: React.FC = () => {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 生成数量 */}
|
||||
{multiGenerationEnabled && (
|
||||
<div style={{ marginTop: 16, paddingTop: 16, borderTop: '1px solid #f0f0f5' }}>
|
||||
<Text style={{
|
||||
display: 'block',
|
||||
marginBottom: 8,
|
||||
fontSize: 12,
|
||||
fontWeight: 500,
|
||||
color: '#667085',
|
||||
}}>
|
||||
生成数量
|
||||
</Text>
|
||||
<div style={{ display: 'flex', gap: 6 }}>
|
||||
{Array.from({ length: effectiveMaxGenerationCount }, (_, i) => i + 1).map((num) => (
|
||||
<button
|
||||
key={num}
|
||||
onClick={() => setGenerationCount(num)}
|
||||
disabled={effectiveMaxGenerationCount <= 1}
|
||||
style={{
|
||||
flex: 1,
|
||||
height: 42,
|
||||
borderRadius: 6,
|
||||
border: generationCount === num
|
||||
? '2px solid #8b5cf6'
|
||||
: '1px solid #E7EAF0',
|
||||
backgroundColor: generationCount === num
|
||||
? '#8b5cf6'
|
||||
: '#FFFFFF',
|
||||
cursor: effectiveMaxGenerationCount <= 1 ? 'not-allowed' : 'pointer',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
transition: 'all 0.2s',
|
||||
opacity: effectiveMaxGenerationCount <= 1 ? 0.5 : 1,
|
||||
}}
|
||||
>
|
||||
<span style={{
|
||||
fontSize: 14,
|
||||
fontWeight: 600,
|
||||
color: generationCount === num
|
||||
? '#fff'
|
||||
: '#475467',
|
||||
}}>
|
||||
{num}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -4417,23 +4548,6 @@ const AIChatPage: React.FC = () => {
|
||||
</Space>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, flexShrink: 0 }}>
|
||||
{multiGenerationEnabled && (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6, whiteSpace: 'nowrap', height: 34, padding: '0 8px 0 12px', borderRadius: 11, background: 'rgba(255, 255, 255, 0.92)', border: '1px solid rgba(231, 234, 240, 0.92)', boxShadow: '0 4px 12px rgba(47, 52, 64, 0.04)' }}>
|
||||
<Text style={{ fontSize: 13, color: '#667085', fontWeight: 600 }}>生成数量:</Text>
|
||||
<Select
|
||||
value={generationCount}
|
||||
onChange={(value) => setGenerationCount(Number(value || 1))}
|
||||
disabled={effectiveMaxGenerationCount <= 1}
|
||||
size="small"
|
||||
variant="borderless"
|
||||
style={{ width: 68 }}
|
||||
options={Array.from({ length: effectiveMaxGenerationCount }, (_, index) => ({
|
||||
value: index + 1,
|
||||
label: `${index + 1}份`,
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6, color: '#667085', whiteSpace: 'nowrap', height: 34, padding: '0 12px', borderRadius: 11, background: 'rgba(255, 255, 255, 0.92)', border: '1px solid rgba(231, 234, 240, 0.92)', boxShadow: '0 4px 12px rgba(47, 52, 64, 0.04)' }}>
|
||||
<Text style={{ fontSize: 13, color: '#667085', fontWeight: 600 }}>预估积分:</Text>
|
||||
<Text style={{ fontSize: 13, color: '#2f3440', fontWeight: 800 }}>{getEstimatedCredits()}</Text>
|
||||
|
||||
Reference in New Issue
Block a user