Compare commits
14
Commits
049bbb0d24
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e06a739cdd | ||
|
|
fe57416493 | ||
|
|
c47fac821c | ||
|
|
a4417e8976 | ||
|
|
3194356859 | ||
|
|
978475f0cd | ||
|
|
5be40ead52 | ||
|
|
7fc58e62ce | ||
|
|
5eed7686c9 | ||
|
|
4a2355a902 | ||
|
|
7ab5c9a5c9 | ||
|
|
556ea4dbd2 | ||
|
|
abb479c469 | ||
|
|
4db879199c |
Vendored
+1
-1
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-CTeXmoRP.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-B0W8S7jH.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-D3fwIbOp.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>␍
|
||||
</body>
|
||||
</html>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { Table, Button, Space, Typography, message, Card, DatePicker, Select } from 'antd';
|
||||
import dayjs from 'dayjs';
|
||||
import { BankOutlined, SearchOutlined } from '@ant-design/icons';
|
||||
import { queryBankTransactions, listBankAccounts } from '../api';
|
||||
import { formatDate } from '../utils/formatDate';
|
||||
@@ -8,19 +9,22 @@ import type { BankAccount } from '../types';
|
||||
const { RangePicker } = DatePicker;
|
||||
|
||||
interface TransactionRecord {
|
||||
counterAcctNo?: string;
|
||||
cnterName?: string;
|
||||
cnterBankName?: string;
|
||||
acctNo?: string;
|
||||
transAmt?: number | string;
|
||||
dcFlag?: number;
|
||||
dcFlagLabel?: string;
|
||||
transTimeStr?: string;
|
||||
digestCode?: string;
|
||||
id?: string;
|
||||
accountId?: string;
|
||||
accountNo?: string;
|
||||
transactionNo?: string;
|
||||
transactionTime?: string;
|
||||
transactionAmount?: string;
|
||||
balanceDirection?: string;
|
||||
balanceAfter?: string;
|
||||
counterpartyName?: string;
|
||||
counterpartyAccount?: string;
|
||||
counterpartyBank?: string;
|
||||
remark?: string;
|
||||
balance?: number | string;
|
||||
tellerSeqno?: string;
|
||||
transferId?: string;
|
||||
digestCode?: string;
|
||||
syncBatch?: string;
|
||||
isSynced?: boolean;
|
||||
createdAt?: string;
|
||||
}
|
||||
|
||||
const AdminBankTransactions: React.FC = () => {
|
||||
@@ -45,7 +49,18 @@ const AdminBankTransactions: React.FC = () => {
|
||||
const loadAccounts = async () => {
|
||||
try {
|
||||
const res = await listBankAccounts();
|
||||
setAccounts(res.items || []);
|
||||
const items = res.items || [];
|
||||
setAccounts(items);
|
||||
|
||||
// 默认选中默认账户(或第一个账户)
|
||||
const defaultAccount = items.find(a => a.isDefault) || items[0];
|
||||
if (defaultAccount) {
|
||||
setSelectedAccountId(defaultAccount.id);
|
||||
}
|
||||
|
||||
// 默认日期范围:今天
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
setDateRange([today, today]);
|
||||
} catch (err: any) {
|
||||
message.error(err?.message || '加载银行账户失败');
|
||||
}
|
||||
@@ -95,72 +110,72 @@ const AdminBankTransactions: React.FC = () => {
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '收款账号',
|
||||
dataIndex: 'counterAcctNo',
|
||||
title: '交易流水号',
|
||||
dataIndex: 'transactionNo',
|
||||
width: 180,
|
||||
render: (v: string) => v || '-',
|
||||
},
|
||||
{
|
||||
title: '收款户名',
|
||||
dataIndex: 'cnterName',
|
||||
width: 120,
|
||||
render: (v: string) => v || '-',
|
||||
},
|
||||
{
|
||||
title: '收款开户行',
|
||||
dataIndex: 'cnterBankName',
|
||||
width: 150,
|
||||
render: (v: string) => v || '-',
|
||||
},
|
||||
{
|
||||
title: '我方付款账号',
|
||||
dataIndex: 'acctNo',
|
||||
width: 180,
|
||||
render: (v: string) => v || '-',
|
||||
},
|
||||
{
|
||||
title: '打款金额',
|
||||
dataIndex: 'transAmt',
|
||||
width: 120,
|
||||
align: 'right' as const,
|
||||
render: (v: number | string) => v != null ? Number(v).toFixed(2) : '-',
|
||||
},
|
||||
{
|
||||
title: '借贷方向',
|
||||
dataIndex: 'dcFlagLabel',
|
||||
width: 100,
|
||||
render: (v: string, r: TransactionRecord) => v || (r.dcFlag === 0 ? '借/出金' : r.dcFlag === 1 ? '贷/入金' : '-'),
|
||||
},
|
||||
{
|
||||
title: '打款时间',
|
||||
dataIndex: 'transTimeStr',
|
||||
title: '交易时间',
|
||||
dataIndex: 'transactionTime',
|
||||
width: 160,
|
||||
render: (v: string) => v ? formatDate(v) : '-',
|
||||
},
|
||||
{
|
||||
title: '摘要',
|
||||
title: '交易金额',
|
||||
dataIndex: 'transactionAmount',
|
||||
width: 120,
|
||||
align: 'right' as const,
|
||||
render: (v: string) => v != null ? Number(v).toFixed(2) : '-',
|
||||
},
|
||||
{
|
||||
title: '借贷方向',
|
||||
dataIndex: 'balanceDirection',
|
||||
width: 100,
|
||||
render: (v: string) => v === 'DR' ? '借/出金' : v === 'CR' ? '贷/入金' : '-',
|
||||
},
|
||||
{
|
||||
title: '交易后余额',
|
||||
dataIndex: 'balanceAfter',
|
||||
width: 120,
|
||||
align: 'right' as const,
|
||||
render: (v: string) => v != null ? Number(v).toFixed(2) : '-',
|
||||
},
|
||||
{
|
||||
title: '对方户名',
|
||||
dataIndex: 'counterpartyName',
|
||||
width: 120,
|
||||
render: (v: string) => v || '-',
|
||||
},
|
||||
{
|
||||
title: '对方账号',
|
||||
dataIndex: 'counterpartyAccount',
|
||||
width: 180,
|
||||
render: (v: string) => v || '-',
|
||||
},
|
||||
{
|
||||
title: '对方开户行',
|
||||
dataIndex: 'counterpartyBank',
|
||||
width: 150,
|
||||
render: (v: string) => v || '-',
|
||||
},
|
||||
{
|
||||
title: '摘要码',
|
||||
dataIndex: 'digestCode',
|
||||
width: 100,
|
||||
render: (v: string) => v || '-',
|
||||
},
|
||||
{
|
||||
title: '用途/备注',
|
||||
title: '备注',
|
||||
dataIndex: 'remark',
|
||||
width: 150,
|
||||
render: (v: string) => v || '-',
|
||||
},
|
||||
{
|
||||
title: '账户余额',
|
||||
dataIndex: 'balance',
|
||||
width: 120,
|
||||
align: 'right' as const,
|
||||
render: (v: number | string) => v != null ? Number(v).toFixed(2) : '-',
|
||||
},
|
||||
{
|
||||
title: '流水号',
|
||||
dataIndex: 'tellerSeqno',
|
||||
width: 150,
|
||||
render: (v: string) => v || '-',
|
||||
title: '同步时间',
|
||||
dataIndex: 'createdAt',
|
||||
width: 160,
|
||||
render: (v: string) => v ? formatDate(v) : '-',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -191,10 +206,11 @@ const AdminBankTransactions: React.FC = () => {
|
||||
allowClear
|
||||
options={accounts.map(a => ({
|
||||
value: a.id,
|
||||
label: `${a.bankName} - ${a.accountNo} (${a.accountName})`,
|
||||
label: `${a.accountNo} - ${a.accountName}(${a.bankName})`,
|
||||
}))}
|
||||
/>
|
||||
<RangePicker
|
||||
value={dateRange[0] && dateRange[1] ? [dayjs(dateRange[0]), dayjs(dateRange[1])] : undefined}
|
||||
onChange={(dates, dateStrings) => {
|
||||
if (Array.isArray(dateStrings)) {
|
||||
setDateRange([dateStrings[0] || null, dateStrings[1] || null]);
|
||||
@@ -225,7 +241,7 @@ const AdminBankTransactions: React.FC = () => {
|
||||
|
||||
<Card variant="outlined" style={{ borderRadius: 12, border: '1px solid #f0f0f5' }}>
|
||||
<Table
|
||||
rowKey={(r, i) => `${r.tellerSeqno || ''}-${r.transferId || ''}-${i}`}
|
||||
rowKey={(r) => r.id || `${r.transactionNo || ''}-${Math.random()}`}
|
||||
columns={columns}
|
||||
dataSource={data}
|
||||
loading={loading}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
"""银行流水表加 created_at 字段
|
||||
|
||||
Revision ID: 20260814_bank_tx_ca
|
||||
Revises: 20260814_bank_tx
|
||||
Create Date: 2026-08-14 16:00:00.000000
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
# revision identifiers, use by Alembic.
|
||||
revision = '20260814_bank_tx_ca'
|
||||
down_revision = 'e7f2527691bb'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.add_column(
|
||||
'bank_transactions',
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
|
||||
)
|
||||
op.create_index('ix_bank_transactions_created_at', 'bank_transactions', ['created_at'])
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_index('ix_bank_transactions_created_at', table_name='bank_transactions')
|
||||
op.drop_column('bank_transactions', 'created_at')
|
||||
@@ -1,11 +1,15 @@
|
||||
"""银行账户管理与交易查询后台路由。"""
|
||||
|
||||
from datetime import datetime, time
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Path, Query
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.dependencies import get_admin_user, get_db
|
||||
from app.models.bank_account import BankAccount
|
||||
from app.models.bank_transaction import BankTransaction
|
||||
from app.models.user import User
|
||||
from app.services.bank.service import query_transactions_with_log
|
||||
from app.utils.id_gen import generate_id
|
||||
@@ -155,19 +159,64 @@ async def list_transactions(
|
||||
admin: User = Depends(get_admin_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""查询银行交易流水(带接口请求记录到文件日志)。"""
|
||||
"""查询本地已同步的银行交易流水。"""
|
||||
result = await db.execute(select(BankAccount).where(BankAccount.id == account_id))
|
||||
account = result.scalar_one_or_none()
|
||||
if account is None:
|
||||
raise HTTPException(status_code=404, detail="银行账户不存在")
|
||||
|
||||
data = await query_transactions_with_log(
|
||||
admin.id,
|
||||
acct_no=account.account_no,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
dc_flag=dc_flag,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
# 构建查询条件(日期字符串转为 datetime,避免 PostgreSQL 类型错误)
|
||||
conditions = [BankTransaction.account_id == account_id]
|
||||
if start_date:
|
||||
start_dt = datetime.strptime(start_date, "%Y-%m-%d")
|
||||
conditions.append(BankTransaction.transaction_time >= start_dt)
|
||||
if end_date:
|
||||
end_dt = datetime.strptime(end_date, "%Y-%m-%d").replace(
|
||||
hour=23, minute=59, second=59
|
||||
)
|
||||
conditions.append(BankTransaction.transaction_time <= end_dt)
|
||||
if dc_flag is not None:
|
||||
direction = "DR" if dc_flag == 0 else "CR"
|
||||
conditions.append(BankTransaction.balance_direction == direction)
|
||||
|
||||
# 查总数
|
||||
count_result = await db.execute(
|
||||
select(sa.func.count()).where(*conditions)
|
||||
)
|
||||
return data
|
||||
total = count_result.scalar() or 0
|
||||
|
||||
# 查分页数据
|
||||
offset = (page - 1) * page_size
|
||||
data_result = await db.execute(
|
||||
select(BankTransaction)
|
||||
.where(*conditions)
|
||||
.order_by(BankTransaction.transaction_time.desc())
|
||||
.offset(offset)
|
||||
.limit(page_size)
|
||||
)
|
||||
transactions = data_result.scalars().all()
|
||||
|
||||
return {
|
||||
"items": [
|
||||
{
|
||||
"id": t.id,
|
||||
"account_id": t.account_id,
|
||||
"account_no": t.account_no,
|
||||
"transaction_no": t.transaction_no,
|
||||
"transaction_time": t.transaction_time.isoformat() if t.transaction_time else None,
|
||||
"transaction_amount": t.transaction_amount,
|
||||
"balance_direction": t.balance_direction,
|
||||
"balance_after": t.balance_after,
|
||||
"counterparty_name": t.counterparty_name,
|
||||
"counterparty_account": t.counterparty_account,
|
||||
"counterparty_bank": t.counterparty_bank,
|
||||
"remark": t.remark,
|
||||
"digest_code": t.digest_code,
|
||||
"sync_batch": t.sync_batch,
|
||||
"is_synced": t.is_synced,
|
||||
"created_at": t.created_at.isoformat() if t.created_at else None,
|
||||
}
|
||||
for t in transactions
|
||||
],
|
||||
"total": total,
|
||||
}
|
||||
|
||||
@@ -148,9 +148,10 @@ async def run_task(
|
||||
if task is None:
|
||||
raise HTTPException(status_code=404, detail="任务不存在")
|
||||
|
||||
from app.tasks.celery_app import RECOVERY_QUEUE
|
||||
from app.tasks.scheduled_tasks import execute_scheduled_task
|
||||
|
||||
execute_scheduled_task.apply_async(args=[task_id])
|
||||
execute_scheduled_task.apply_async(args=[task_id], queue=RECOVERY_QUEUE)
|
||||
return {"message": "任务已提交执行"}
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""银行交易流水模型。"""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import Boolean, DateTime, Index, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
@@ -15,6 +17,14 @@ class BankTransaction(Base):
|
||||
account_id: Mapped[str] = mapped_column(String(32), index=True, nullable=False, comment="银行账户ID")
|
||||
account_no: Mapped[str] = mapped_column(String(64), index=True, nullable=False, comment="银行账号")
|
||||
|
||||
# 记录创建时间
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True),
|
||||
default=lambda: datetime.now(timezone.utc),
|
||||
nullable=False,
|
||||
comment="记录创建时间",
|
||||
)
|
||||
|
||||
# 交易信息
|
||||
transaction_no: Mapped[str | None] = mapped_column(String(128), unique=True, nullable=True, comment="交易流水号(唯一)")
|
||||
transaction_time: Mapped[DateTime | None] = mapped_column(DateTime(timezone=True), index=True, nullable=True, comment="交易时间")
|
||||
|
||||
@@ -34,7 +34,7 @@ logger = logging.getLogger("video_gen")
|
||||
_PAGE_SIZE = 100
|
||||
|
||||
|
||||
def sync_bank_transactions(
|
||||
async def sync_bank_transactions(
|
||||
url: str = "",
|
||||
api_key: str = "",
|
||||
acct_no: str = "",
|
||||
@@ -85,24 +85,16 @@ def sync_bank_transactions(
|
||||
"errors": [],
|
||||
}
|
||||
|
||||
import asyncio
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
try:
|
||||
loop.run_until_complete(
|
||||
_do_sync(
|
||||
url=url,
|
||||
api_key=api_key,
|
||||
acct_no=acct_no,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
dc_flag=dc_flag,
|
||||
sync_batch=sync_batch,
|
||||
stats=stats,
|
||||
)
|
||||
)
|
||||
finally:
|
||||
loop.close()
|
||||
await _do_sync(
|
||||
url=url,
|
||||
api_key=api_key,
|
||||
acct_no=acct_no,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
dc_flag=dc_flag,
|
||||
sync_batch=sync_batch,
|
||||
stats=stats,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"银行流水同步完成: batch=%s, acct=%s, 翻页=%d, 获取=%d, 新增=%d, 重复=%d",
|
||||
|
||||
@@ -31,28 +31,28 @@ from app.tasks.celery_app import celery_app
|
||||
logger = logging.getLogger("video_gen")
|
||||
|
||||
|
||||
def _update_task_status(task_id: str, status: str, error_msg: str | None = None) -> None:
|
||||
async def _update_task_status(task_id: str, status: str, error_msg: str | None = None) -> None:
|
||||
"""更新任务最后执行状态。"""
|
||||
|
||||
async def _do():
|
||||
async with async_session() as db:
|
||||
result = await db.execute(select(ScheduledTask).where(ScheduledTask.id == task_id))
|
||||
task = result.scalar_one_or_none()
|
||||
if task is None:
|
||||
return
|
||||
task.last_run_at = datetime.now(timezone.utc).isoformat()
|
||||
task.last_status = status
|
||||
task.last_error = error_msg
|
||||
await db.commit()
|
||||
|
||||
run_async(_do())
|
||||
async with async_session() as db:
|
||||
result = await db.execute(select(ScheduledTask).where(ScheduledTask.id == task_id))
|
||||
task = result.scalar_one_or_none()
|
||||
if task is None:
|
||||
return
|
||||
task.last_run_at = datetime.now(timezone.utc).isoformat()
|
||||
task.last_status = status
|
||||
task.last_error = error_msg
|
||||
await db.commit()
|
||||
|
||||
|
||||
def _execute_internal_method(config: str | None) -> dict:
|
||||
async def _execute_internal_method(config: str | None) -> dict:
|
||||
"""执行内部方法调用。
|
||||
|
||||
配置 JSON 中 module 和 function 指定要调用的函数,其余字段作为 kwargs 传入。
|
||||
支持同步函数和异步函数(async def)。
|
||||
"""
|
||||
import importlib
|
||||
from inspect import iscoroutinefunction
|
||||
|
||||
cfg = json.loads(config or "{}")
|
||||
module_path = cfg.pop("module", "").strip()
|
||||
function_name = cfg.pop("function", "").strip()
|
||||
@@ -60,8 +60,6 @@ def _execute_internal_method(config: str | None) -> dict:
|
||||
if not module_path or not function_name:
|
||||
raise ValueError("内部方法需要指定 module 和 function")
|
||||
|
||||
import importlib
|
||||
|
||||
module = importlib.import_module(module_path)
|
||||
func = getattr(module, function_name, None)
|
||||
if func is None or not callable(func):
|
||||
@@ -69,7 +67,11 @@ def _execute_internal_method(config: str | None) -> dict:
|
||||
|
||||
# 剩余字段作为 kwargs 传给函数
|
||||
start = time.monotonic()
|
||||
result = func(**cfg)
|
||||
if iscoroutinefunction(func):
|
||||
# 异步函数:直接 await
|
||||
result = await func(**cfg)
|
||||
else:
|
||||
result = func(**cfg)
|
||||
duration_ms = int((time.monotonic() - start) * 1000)
|
||||
return {
|
||||
"duration_ms": duration_ms,
|
||||
@@ -91,16 +93,15 @@ def execute_scheduled_task(self, task_id: str):
|
||||
if not task.is_active:
|
||||
logger.info("定时任务已禁用,跳过执行: %s", task_id)
|
||||
return
|
||||
|
||||
task_config = task.config
|
||||
|
||||
try:
|
||||
exec_result = _execute_internal_method(task_config)
|
||||
_update_task_status(task_id, "success")
|
||||
exec_result = await _execute_internal_method(task_config)
|
||||
await _update_task_status(task_id, "success")
|
||||
logger.info("定时任务执行成功: %s -> %s", task_id, exec_result)
|
||||
except Exception as e:
|
||||
error_msg = str(e)
|
||||
_update_task_status(task_id, "error", error_msg)
|
||||
await _update_task_status(task_id, "error", error_msg)
|
||||
logger.exception("定时任务执行失败: %s", task_id)
|
||||
|
||||
run_async(_run())
|
||||
|
||||
+111
-111
File diff suppressed because one or more lines are too long
Vendored
+35
-35
@@ -1,36 +1,36 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==" id="favicon" />
|
||||
<script>
|
||||
// 立即从 localStorage 设置 favicon,避免闪烁
|
||||
(function() {
|
||||
try {
|
||||
var cached = localStorage.getItem('siteInfo');
|
||||
if (cached) {
|
||||
var info = JSON.parse(cached);
|
||||
if (info.siteLogo) {
|
||||
var link = document.getElementById('favicon');
|
||||
link.href = info.siteLogo;
|
||||
link.type = 'image/png';
|
||||
}
|
||||
if (info.siteName) {
|
||||
document.title = info.siteName;
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
})();
|
||||
</script>
|
||||
<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 type="module" crossorigin src="/assets/index-BMPNcnJK.js"></script>
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==" id="favicon" />
|
||||
<script>
|
||||
// 立即从 localStorage 设置 favicon,避免闪烁
|
||||
(function() {
|
||||
try {
|
||||
var cached = localStorage.getItem('siteInfo');
|
||||
if (cached) {
|
||||
var info = JSON.parse(cached);
|
||||
if (info.siteLogo) {
|
||||
var link = document.getElementById('favicon');
|
||||
link.href = info.siteLogo;
|
||||
link.type = 'image/png';
|
||||
}
|
||||
if (info.siteName) {
|
||||
document.title = info.siteName;
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
})();
|
||||
</script>
|
||||
<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 type="module" crossorigin src="/assets/index-Bc_ErQq7.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DSYnuUvx.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>␍
|
||||
</body>
|
||||
</html>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -574,6 +574,20 @@ export async function setDefaultInvoiceHeader(id: string): Promise<any> {
|
||||
return api.put(`/invoice-headers/${id}/set-default`);
|
||||
}
|
||||
|
||||
// ── Bank Accounts ──────────────────────────────────────────
|
||||
|
||||
export interface BankAccountInfo {
|
||||
id: string;
|
||||
accountName: string;
|
||||
bankName: string;
|
||||
accountNo: string;
|
||||
description: string | null;
|
||||
}
|
||||
|
||||
export async function getDefaultBankAccount(): Promise<{ hasAccount: boolean; account: BankAccountInfo | null }> {
|
||||
return api.get('/bank/default-account');
|
||||
}
|
||||
|
||||
export async function getCreditRatios(): Promise<any[]> {
|
||||
return api.get('/credits/ratios');
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ import {
|
||||
CrownFilled,
|
||||
FireOutlined,
|
||||
BankFilled,
|
||||
BankOutlined,
|
||||
CheckCircleFilled,
|
||||
WechatOutlined,
|
||||
AlipayCircleOutlined,
|
||||
@@ -60,7 +61,7 @@ import {
|
||||
} from '@ant-design/icons';
|
||||
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useAuthStore } from '../../store/useAuthStore';
|
||||
import { getMenuConfigs, getCreditProductCatalog, getPaymentMethods, createRechargeOrder, getPaymentOrder, cancelPaymentOrder, getSiteInfo, getUnreadCount, createContactRequest, getUser, changePassword, changeUsername } from '../../api';
|
||||
import { getMenuConfigs, getCreditProductCatalog, getPaymentMethods, createRechargeOrder, getPaymentOrder, cancelPaymentOrder, getSiteInfo, getUnreadCount, createContactRequest, getUser, changePassword, changeUsername, getDefaultBankAccount } from '../../api';
|
||||
import type { CreditProduct, CreditProductCatalog } from '../../types';
|
||||
import NotificationPopup from '../NotificationPopup';
|
||||
import ActivityBanner from './ActivityBanner';
|
||||
@@ -397,7 +398,7 @@ const FAQ_ITEMS = [
|
||||
label: '发票申请与联系方式',
|
||||
children: (
|
||||
<div style={{ color: '#64748b', lineHeight: 1.8, fontSize: 13 }}>
|
||||
<p style={{ margin: 0 }}>发票可在「订阅与开票」→「购买记录」中自助申请。如需企业合作,请联系 bd@liblib.ai;其他问题欢迎前往"帮助中心"查询。</p>
|
||||
<p style={{ margin: 0 }}>发票可在「订阅与开票」→「购买记录」中自助申请。如需企业合作,请联系客服;其他问题欢迎前往"帮助中心"查询。</p>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
@@ -495,6 +496,24 @@ const AppLayout: React.FC = () => {
|
||||
const [pendingProduct, setPendingProduct] = useState<{ id: string; product: CreditProduct; } | null>(null);
|
||||
const [purchaseQuantity, setPurchaseQuantity] = useState(1);
|
||||
const [qrRevealed, setQrRevealed] = useState(false);
|
||||
const [paymentTab, setPaymentTab] = useState<'alipay' | 'corporate'>('alipay');
|
||||
const [corporateTransferForm] = Form.useForm();
|
||||
const [corporateSubmitting, setCorporateSubmitting] = useState(false);
|
||||
const [bankAccountInfo, setBankAccountInfo] = useState<{ hasAccount: boolean; account: any } | null>(null);
|
||||
const [bankAccountLoading, setBankAccountLoading] = useState(false);
|
||||
|
||||
const fetchBankAccount = useCallback(async () => {
|
||||
if (bankAccountInfo || bankAccountLoading) return;
|
||||
setBankAccountLoading(true);
|
||||
try {
|
||||
const data = await getDefaultBankAccount();
|
||||
setBankAccountInfo(data);
|
||||
} catch {
|
||||
setBankAccountInfo({ hasAccount: false, account: null });
|
||||
} finally {
|
||||
setBankAccountLoading(false);
|
||||
}
|
||||
}, [bankAccountInfo, bankAccountLoading]);
|
||||
|
||||
const PENDING_ORDER_KEY = 'pending_payment_order';
|
||||
const [unreadCount, setUnreadCount] = useState(0);
|
||||
@@ -886,6 +905,8 @@ const AppLayout: React.FC = () => {
|
||||
setPendingProduct({ id: productId, product });
|
||||
setPurchaseQuantity(product.productType === 'team_subscription' ? 2 : 1);
|
||||
setQrRevealed(false);
|
||||
setPaymentTab('alipay');
|
||||
corporateTransferForm.resetFields();
|
||||
setCurrentPaymentInfo(null);
|
||||
setCreditsModalOpen(false);
|
||||
setQrCodeModalOpen(true);
|
||||
@@ -944,6 +965,50 @@ const AppLayout: React.FC = () => {
|
||||
}
|
||||
}, [pendingProduct, paymentMethod, purchaseQuantity]);
|
||||
|
||||
const submitCorporateTransfer = useCallback(async () => {
|
||||
if (!pendingProduct) return;
|
||||
try {
|
||||
const values: any = await corporateTransferForm.validateFields();
|
||||
setCorporateSubmitting(true);
|
||||
const { id: productId, product } = pendingProduct;
|
||||
const order = await createRechargeOrder(productId, 'corporate');
|
||||
const finalPrice = Number(order.amount ?? product.currentPrice ?? product.price ?? 0);
|
||||
const credits = Number(product.grantCredits || product.monthlyGrantCredits || 0);
|
||||
const originalPrice = product.regularPrice;
|
||||
const cycleMap: Record<string, string> = { monthly: '月套餐', quarterly: '季套餐', yearly: '年套餐' };
|
||||
const isSubscription = !!product.billingCycle;
|
||||
const periodLabel = product.billingCycle ? cycleMap[product.billingCycle] : '';
|
||||
const tierName = product.name || '积分充值';
|
||||
const displayTitle = isSubscription ? `${tierName} ${periodLabel}` : tierName;
|
||||
const paymentInfo = {
|
||||
price: finalPrice,
|
||||
credits,
|
||||
qrCode: '',
|
||||
method: 'corporate',
|
||||
title: displayTitle,
|
||||
originalPrice: originalPrice ? Number(originalPrice) : undefined,
|
||||
tierName,
|
||||
periodLabel,
|
||||
isSubscription,
|
||||
};
|
||||
void values;
|
||||
setCurrentPaymentInfo(paymentInfo);
|
||||
message.success('已提交对公转账信息,客服将在1-3个工作日内审核到账');
|
||||
setQrCodeModalOpen(false);
|
||||
setPendingProduct(null);
|
||||
corporateTransferForm.resetFields();
|
||||
await useAuthStore.getState().refreshUser();
|
||||
await loadCreditCatalog();
|
||||
} catch (err: any) {
|
||||
if (err?.errorFields) {
|
||||
return;
|
||||
}
|
||||
message.error(err?.message || '提交失败,请重试');
|
||||
} finally {
|
||||
setCorporateSubmitting(false);
|
||||
}
|
||||
}, [pendingProduct, corporateTransferForm]);
|
||||
|
||||
const startPolling = useCallback((orderNo: string, timeoutSeconds: number = 180) => {
|
||||
stopPolling();
|
||||
setCountdown(timeoutSeconds);
|
||||
@@ -1921,9 +1986,11 @@ const AppLayout: React.FC = () => {
|
||||
setCurrentPaymentInfo(null);
|
||||
setPendingProduct(null);
|
||||
setQrRevealed(false);
|
||||
setPaymentTab('alipay');
|
||||
corporateTransferForm.resetFields();
|
||||
}}
|
||||
footer={null}
|
||||
width={760}
|
||||
width={'50%'}
|
||||
closable
|
||||
title={
|
||||
<Typography.Title level={5} style={{ margin: 0 }}>
|
||||
@@ -1998,31 +2065,150 @@ const AppLayout: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 右侧:在线支付方式。后台线下成交不在客户端创建。 */}
|
||||
<div style={{ width: 300, padding: 20, background: '#fff', display: 'flex', flexDirection: 'column' }}>
|
||||
<Typography.Text strong style={{ marginBottom: 12 }}>选择支付方式</Typography.Text>
|
||||
<div style={{ display: 'flex', gap: 8, marginBottom: 18 }}>
|
||||
{enabledMethods.alipay && <Button type={paymentMethod === 'alipay' ? 'primary' : 'default'} onClick={() => { if (!qrRevealed) setPaymentMethod('alipay'); }} icon={<AlipayCircleOutlined />}>支付宝</Button>}
|
||||
{enabledMethods.wechat && <Button type={paymentMethod === 'wechat' ? 'primary' : 'default'} onClick={() => { if (!qrRevealed) setPaymentMethod('wechat'); }} icon={<WechatOutlined />}>微信支付</Button>}
|
||||
{/* 右侧:支付方式 */}
|
||||
<div style={{ width: '50%', padding: 20, background: '#fff', display: 'flex', flexDirection: 'column' }}>
|
||||
{/* Tab 切换 */}
|
||||
<div style={{ display: 'flex', background: '#f3f4f6', borderRadius: 10, padding: 4, marginBottom: 20 }}>
|
||||
<div
|
||||
onClick={() => setPaymentTab('alipay')}
|
||||
style={{
|
||||
flex: 1, textAlign: 'center', padding: '8px 0', borderRadius: 8,
|
||||
cursor: 'pointer', fontSize: 14, fontWeight: 600,
|
||||
background: paymentTab === 'alipay' ? '#fff' : 'transparent',
|
||||
color: paymentTab === 'alipay' ? '#1677ff' : '#64748b',
|
||||
boxShadow: paymentTab === 'alipay' ? '0 2px 8px rgba(0,0,0,0.08)' : 'none',
|
||||
transition: 'all 0.2s',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6,
|
||||
}}
|
||||
>
|
||||
<AlipayCircleOutlined style={{ color: paymentTab === 'alipay' ? '#1677ff' : '#94a3b8' }} />
|
||||
支付宝支付
|
||||
</div>
|
||||
<div
|
||||
onClick={() => { setPaymentTab('corporate'); fetchBankAccount(); }}
|
||||
style={{
|
||||
flex: 1, textAlign: 'center', padding: '8px 0', borderRadius: 8,
|
||||
cursor: 'pointer', fontSize: 14, fontWeight: 600,
|
||||
background: paymentTab === 'corporate' ? '#fff' : 'transparent',
|
||||
color: paymentTab === 'corporate' ? '#6366f1' : '#64748b',
|
||||
boxShadow: paymentTab === 'corporate' ? '0 2px 8px rgba(0,0,0,0.08)' : 'none',
|
||||
transition: 'all 0.2s',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6,
|
||||
}}
|
||||
>
|
||||
<BankOutlined style={{ color: paymentTab === 'corporate' ? '#6366f1' : '#94a3b8' }} />
|
||||
对公转账
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<div style={{ marginBottom: 12, color: '#64748b' }}>{paymentMethod === 'wechat' ? '请用微信扫码支付' : '请用支付宝扫码支付'}</div>
|
||||
<div style={{ position: 'relative', width: 180, height: 180, borderRadius: 12, border: '1px solid #f0f0f0', display: 'flex', alignItems: 'center', justifyContent: 'center', background: '#fff' }}>
|
||||
{qrRevealed && currentPaymentInfo?.qrCode ? <QRCodeSVG value={currentPaymentInfo.qrCode} size={150} level="M" /> : (
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<div style={{ fontSize: 13, color: '#64748b', marginBottom: 12 }}>确认商品和金额后生成二维码</div>
|
||||
<Button type="primary" loading={paying} onClick={confirmPayment}>确认支付</Button>
|
||||
|
||||
{/* 支付宝 Tab */}
|
||||
{paymentTab === 'alipay' && (
|
||||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 12 }}>
|
||||
<AlipayCircleOutlined style={{ fontSize: 18, color: '#1677ff' }} />
|
||||
<span style={{ fontSize: 14, color: '#64748b' }}>请用支付宝扫码支付</span>
|
||||
</div>
|
||||
<div style={{ position: 'relative', width: 180, height: 180, borderRadius: 12, border: '1px solid #f0f0f0', display: 'flex', alignItems: 'center', justifyContent: 'center', background: '#fff' }}>
|
||||
{qrRevealed && currentPaymentInfo?.qrCode ? (
|
||||
<QRCodeSVG value={currentPaymentInfo.qrCode} size={150} level="M" />
|
||||
) : (
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<div style={{ fontSize: 13, color: '#64748b', marginBottom: 12 }}>确认商品和金额后<br/>生成二维码</div>
|
||||
<Button type="primary" loading={paying} onClick={confirmPayment} style={{ borderRadius: 8, height: 36, fontWeight: 600 }}>
|
||||
确认使用支付宝支付
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{qrRevealed && (
|
||||
<div style={{ marginTop: 12, fontSize: 12, color: '#94a3b8' }}>
|
||||
{countdown} 秒后二维码失效
|
||||
</div>
|
||||
)}
|
||||
{qrRevealed && (
|
||||
<Button
|
||||
danger
|
||||
style={{ marginTop: 14, borderRadius: 8 }}
|
||||
onClick={async () => {
|
||||
stopPolling();
|
||||
if (currentOrderNoRef.current) { try { await cancelPaymentOrder(currentOrderNoRef.current); } catch { } currentOrderNoRef.current = null; }
|
||||
localStorage.removeItem(PENDING_ORDER_KEY); setQrCodeModalOpen(false); setCurrentPaymentInfo(null); setPendingProduct(null); setQrRevealed(false); message.info('已取消支付');
|
||||
}}
|
||||
>
|
||||
取消转账
|
||||
</Button>
|
||||
)}
|
||||
<div style={{ marginTop: 8, fontSize: 11, color: '#94a3b8', textAlign: 'center' }}>
|
||||
开通即代表同意<br/>《用户服务协议》
|
||||
</div>
|
||||
</div>
|
||||
{qrRevealed && <div style={{ marginTop: 12, fontSize: 12, color: '#94a3b8' }}>{countdown} 秒后二维码失效</div>}
|
||||
{qrRevealed && <Button danger style={{ marginTop: 14 }} onClick={async () => {
|
||||
stopPolling();
|
||||
if (currentOrderNoRef.current) { try { await cancelPaymentOrder(currentOrderNoRef.current); } catch { } currentOrderNoRef.current = null; }
|
||||
localStorage.removeItem(PENDING_ORDER_KEY); setQrCodeModalOpen(false); setCurrentPaymentInfo(null); setPendingProduct(null); setQrRevealed(false); message.info('已取消支付');
|
||||
}}>取消支付</Button>}
|
||||
</div>
|
||||
<div style={{ marginTop: 8, fontSize: 11, color: '#94a3b8', textAlign: 'center' }}>线上订单仅支持已启用的支付宝/微信支付;后台线下成交请联系客服处理。</div>
|
||||
)}
|
||||
|
||||
{/* 对公转账 Tab */}
|
||||
{paymentTab === 'corporate' && (
|
||||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', overflowY: 'auto' }}>
|
||||
{/* 收款账户信息 */}
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Typography.Text style={{ fontSize: 12, color: '#64748b', marginBottom: 8, display: 'block', fontWeight: 600 }}>收款账户信息</Typography.Text>
|
||||
<div style={{ padding: 12, background: '#f8fafc', borderRadius: 8, border: '1px solid #e2e8f0', minHeight: 80, display: 'flex', alignItems: 'center' }}>
|
||||
{bankAccountLoading ? (
|
||||
<div style={{ width: '100%', textAlign: 'center', color: '#94a3b8', fontSize: 12 }}>加载中...</div>
|
||||
) : bankAccountInfo?.hasAccount && bankAccountInfo?.account ? (
|
||||
<div style={{ width: '100%' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '4px 0', fontSize: 12, marginBottom: 4 }}>
|
||||
<span style={{ color: '#64748b' }}>账户名称</span>
|
||||
<span style={{ color: '#1a1a2e', fontWeight: 500 }}>{bankAccountInfo.account.accountName}</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '4px 0', fontSize: 12, marginBottom: 4 }}>
|
||||
<span style={{ color: '#64748b' }}>开户银行</span>
|
||||
<span style={{ color: '#1a1a2e', fontWeight: 500, display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
{/* <BankFilled style={{ color: '#c8161d', fontSize: 14 }} /> */}
|
||||
{bankAccountInfo.account.bankName}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '4px 0', fontSize: 12 }}>
|
||||
<span style={{ color: '#64748b' }}>账号</span>
|
||||
<span style={{ color: '#1a1a2e', fontWeight: 500, fontFamily: 'monospace' }}>{bankAccountInfo.account.accountNo}</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ width: '100%', textAlign: 'center', color: '#94a3b8', fontSize: 12 }}>暂无收款账户信息</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 打款信息填写 */}
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<Typography.Text style={{ fontSize: 12, color: '#64748b', marginBottom: 4, display: 'block', fontWeight: 600 }}>填写您的打款信息</Typography.Text>
|
||||
<Typography.Text style={{ fontSize: 11, color: '#ef4444', marginBottom: 8, display: 'block' }}>注:打款金额需与所标注金额一致,不然无法开通,银行信息需正确填写</Typography.Text>
|
||||
<Form form={corporateTransferForm} layout="vertical" size="small" requiredMark={false}>
|
||||
<Form.Item name="accountName" label="账户名称" rules={[{ required: true, message: '请输入账户名称' }]} style={{ marginBottom: 10 }}>
|
||||
<Input placeholder="请输入付款方账户名称" />
|
||||
</Form.Item>
|
||||
<Form.Item name="bankName" label="开户银行" rules={[{ required: true, message: '请输入开户银行' }]} style={{ marginBottom: 10 }}>
|
||||
<Input placeholder="请输入开户银行" prefix={<BankOutlined style={{ color: '#94a3b8' }} />} />
|
||||
</Form.Item>
|
||||
<Form.Item name="bankAccount" label="账号" rules={[{ required: true, message: '请输入账号' }]} style={{ marginBottom: 8 }}>
|
||||
<Input placeholder="请输入付款方账号" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="primary"
|
||||
block
|
||||
size="large"
|
||||
loading={corporateSubmitting}
|
||||
onClick={submitCorporateTransfer}
|
||||
style={{ borderRadius: 8, background: 'linear-gradient(135deg, #6366f1, #8b5cf6)', border: 'none', fontWeight: 600, height: 38, fontSize: 14 }}
|
||||
>
|
||||
提交对公转账
|
||||
</Button>
|
||||
<div style={{ marginTop: 8, fontSize: 11, color: '#94a3b8', textAlign: 'center' }}>
|
||||
提交后客服将在 12 个小时日内审核到账,如遇问题,请联系客服。
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
@@ -964,13 +964,15 @@ const GenerateConver: React.FC = () => {
|
||||
|
||||
{/* 上传视频 */}
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<p style={{ margin: 0, fontSize: 14, fontWeight: 600, color: '#1e293b', marginBottom: 10 }}>
|
||||
上传复刻视频
|
||||
<span style={{ color: '#f94444' }}>(必选)</span>
|
||||
<p style={{ fontSize: 12, color: '#64748b', fontWeight: 400, }}>
|
||||
<div style={{ marginBottom: 10 }}>
|
||||
<p style={{ margin: 0, fontSize: 14, fontWeight: 600, color: '#1e293b' }}>
|
||||
上传复刻视频
|
||||
<span style={{ color: '#f94444' }}>(必选)</span>
|
||||
</p>
|
||||
<p style={{ fontSize: 12, color: '#64748b', fontWeight: 400, margin: '4px 0 0' }}>
|
||||
当前引擎最多可上传 {currentMaxDuration} 秒
|
||||
</p>
|
||||
</p>
|
||||
</div>
|
||||
{videoUrl ? (
|
||||
<div style={{ position: 'relative' }}>
|
||||
<video
|
||||
|
||||
Reference in New Issue
Block a user