feat: 邮箱注册验证 + 9项功能修复与优化
【认证系统】 - 新增邮箱验证码注册/登录流程 (sendEmailCode / verifyEmail / forgotPassword / resetPassword) - register-email 现在需要验证码 - 服务端新增 email_verification_codes 表 + patch-email-verification.js - App.tsx 登录后 emailVerified 检查提醒 - keyServerClient token 显式传递修复 401 错误 【电商模块】 - 自动推进: 策划完成后自动生成分镜图/视频 - 模特图选项 (性别/年龄/种族/体型/场景) 注入 AI 提示词 - 任务持久化指纹修复 (图片数量替代 blob URL) - 新增「视频换装」入口 (happyhorse-1.0-video-edit) 【剧本评分】 - 新增 .docx/.doc Word 文档支持 (ZIP解压+XML提取) - 历史记录支持点击查看/恢复评测结果 【画布】 - ReactFlow 节点禁止内置拖拽避免冲突 - 连接线拖拽弹窗优化 (预览线不消失, 弹窗跟踪鼠标) 【页面修复】 - 首页轮播图改为 aspect-ratio: 16/9 解决尺寸问题 - 资产库新增悬停删除按钮 - scriptEvalClient 改用服务端 /api/ai/chat 端点 - TokenUsagePage 未登录跳过 API 调用
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { keyServerClient } from "../api/keyServerClient";
|
||||
|
||||
interface ClientErrorItem {
|
||||
export interface ClientErrorItem {
|
||||
id: number;
|
||||
message: string;
|
||||
stack?: string;
|
||||
|
||||
@@ -22,6 +22,7 @@ import NotificationCenter from "./NotificationCenter";
|
||||
import { RechargeModal } from "./RechargeModal/RechargeModal";
|
||||
import { AnimatedPanel } from "./AnimatedPanel";
|
||||
import AdminMonitor from "./AdminMonitor";
|
||||
import CookieConsentBanner from "./CookieConsentBanner";
|
||||
|
||||
interface AppShellProps {
|
||||
activeView: WebViewKey;
|
||||
@@ -40,6 +41,7 @@ interface AppShellProps {
|
||||
}
|
||||
|
||||
const BRAND_LOGO_URL = "https://stringtest.oss-cn-hangzhou.aliyuncs.com/logo.png";
|
||||
const CLIENT_ERROR_MONITOR_ENABLED = import.meta.env.VITE_ENABLE_CLIENT_ERROR_MONITOR === "1";
|
||||
|
||||
function formatBalance(cents: number): string {
|
||||
const value = Math.max(0, cents) / 100;
|
||||
@@ -344,8 +346,8 @@ function AppShell({
|
||||
<dd>15155073618</dd>
|
||||
</dl>
|
||||
<div className="info-popover__links">
|
||||
<a href="#" onClick={(e) => { e.preventDefault(); setInfoOpen(false); }}>用户协议</a>
|
||||
<a href="#" onClick={(e) => { e.preventDefault(); setInfoOpen(false); }}>隐私政策</a>
|
||||
<a href="#/userAgreement" onClick={() => setInfoOpen(false)}>用户协议</a>
|
||||
<a href="#/privacyPolicy" onClick={() => setInfoOpen(false)}>隐私政策</a>
|
||||
</div>
|
||||
</AnimatedPanel>
|
||||
</div>
|
||||
@@ -356,7 +358,7 @@ function AppShell({
|
||||
onClick={() => setRechargeOpen(true)}
|
||||
>
|
||||
<WalletOutlined />
|
||||
{displayedBalanceLabel}
|
||||
<span className="member-button__label">{displayedBalanceLabel}</span>
|
||||
</button>
|
||||
<div className="profile-popover-anchor" ref={profileRef}>
|
||||
<button
|
||||
@@ -471,8 +473,9 @@ function AppShell({
|
||||
<div className="web-shell__page">{children}</div>
|
||||
</main>
|
||||
</div>
|
||||
{session?.user.role === "admin" ? <AdminMonitor /> : null}
|
||||
{CLIENT_ERROR_MONITOR_ENABLED && session?.user.role === "admin" ? <AdminMonitor /> : null}
|
||||
<RechargeModal open={rechargeOpen} onClose={() => setRechargeOpen(false)} currentBalance={displayedBalanceCents} />
|
||||
<CookieConsentBanner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -27,7 +27,6 @@ const NAV_ORDER: string[] = [
|
||||
"avatarConsole",
|
||||
"characterMix",
|
||||
"agent",
|
||||
"settings",
|
||||
"login",
|
||||
"profile",
|
||||
"report",
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { CheckCircleOutlined, CloseOutlined, CrownOutlined, RocketOutlined } from "@ant-design/icons";
|
||||
import { useMemo, useState, type ReactNode } from "react";
|
||||
import { keyServerClient, type RechargeOrderResult } from "../../api/keyServerClient";
|
||||
import { toast } from "../toast/toastStore";
|
||||
|
||||
type RechargeAudience = "personal" | "enterprise";
|
||||
type PaymentMethod = "wechat" | "alipay" | "bank";
|
||||
|
||||
interface MembershipPlan {
|
||||
id: string;
|
||||
@@ -107,6 +110,12 @@ const rechargeRules = [
|
||||
"退费规则:充值积分到账后不支持退换、折现,仅限平台内消费",
|
||||
];
|
||||
|
||||
const paymentMethods: Array<{ id: PaymentMethod; label: string; hint: string }> = [
|
||||
{ id: "wechat", label: "微信支付", hint: "生成支付链接或二维码" },
|
||||
{ id: "alipay", label: "支付宝", hint: "生成支付链接或二维码" },
|
||||
{ id: "bank", label: "对公转账", hint: "企业客户可联系客服确认" },
|
||||
];
|
||||
|
||||
interface RechargeModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
@@ -116,14 +125,43 @@ interface RechargeModalProps {
|
||||
export function RechargeModal({ open, onClose, currentBalance }: RechargeModalProps) {
|
||||
const [activeAudience, setActiveAudience] = useState<RechargeAudience>("personal");
|
||||
const [selectedPlanIds, setSelectedPlanIds] = useState<Record<RechargeAudience, string>>(defaultSelectedPlanIds);
|
||||
const [paymentMethod, setPaymentMethod] = useState<PaymentMethod>("wechat");
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [order, setOrder] = useState<RechargeOrderResult | null>(null);
|
||||
const visiblePlans = useMemo(() => membershipPlans.filter((plan) => plan.audience === activeAudience), [activeAudience]);
|
||||
const selectedPlanId = selectedPlanIds[activeAudience];
|
||||
const selectedPlan = membershipPlans.find((plan) => plan.id === selectedPlanId) ?? visiblePlans[0];
|
||||
|
||||
const handlePlanSelect = (plan: MembershipPlan) => {
|
||||
setSelectedPlanIds((current) => ({
|
||||
...current,
|
||||
[plan.audience]: plan.id,
|
||||
}));
|
||||
setOrder(null);
|
||||
};
|
||||
|
||||
const handleCreateOrder = async () => {
|
||||
if (!selectedPlan || submitting) return;
|
||||
|
||||
setSubmitting(true);
|
||||
try {
|
||||
const nextOrder = await keyServerClient.createRechargeOrder({ planId: selectedPlan.id, paymentMethod });
|
||||
setOrder(nextOrder);
|
||||
if (nextOrder.payUrl) {
|
||||
window.open(nextOrder.payUrl, "_blank", "noopener,noreferrer");
|
||||
}
|
||||
toast.success("充值订单已创建");
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "订单创建失败,请联系客服处理。";
|
||||
toast.error(message);
|
||||
setOrder({
|
||||
orderId: `support-${Date.now()}`,
|
||||
status: "manual-review",
|
||||
message: "支付接口暂不可用,请通过页面联系方式联系客服完成充值。",
|
||||
});
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!open) return null;
|
||||
@@ -224,6 +262,44 @@ export function RechargeModal({ open, onClose, currentBalance }: RechargeModalPr
|
||||
))}
|
||||
</ol>
|
||||
</footer>
|
||||
|
||||
<section className="recharge-modal__checkout" aria-label="支付方式">
|
||||
<div>
|
||||
<span className="recharge-modal__checkout-eyebrow">支付确认</span>
|
||||
<h3>{selectedPlan.name} · {selectedPlan.period}</h3>
|
||||
<p>{selectedPlan.price},{selectedPlan.grant}</p>
|
||||
</div>
|
||||
<div className="recharge-modal__payment-methods" role="radiogroup" aria-label="选择支付方式">
|
||||
{paymentMethods.map((method) => (
|
||||
<button
|
||||
key={method.id}
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={paymentMethod === method.id}
|
||||
className={paymentMethod === method.id ? "is-active" : ""}
|
||||
onClick={() => {
|
||||
setPaymentMethod(method.id);
|
||||
setOrder(null);
|
||||
}}
|
||||
>
|
||||
<strong>{method.label}</strong>
|
||||
<span>{method.hint}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<button type="button" className="recharge-modal__pay" onClick={() => void handleCreateOrder()} disabled={submitting}>
|
||||
{submitting ? "创建订单中..." : "立即充值"}
|
||||
</button>
|
||||
{order ? (
|
||||
<div className="recharge-modal__order" role="status">
|
||||
<strong>订单号:{order.orderId}</strong>
|
||||
<span>状态:{order.status}</span>
|
||||
{order.qrCodeUrl ? <img src={order.qrCodeUrl} alt="支付二维码" /> : null}
|
||||
{order.payUrl ? <a href={order.payUrl} target="_blank" rel="noreferrer">打开支付链接</a> : null}
|
||||
<p>{order.message || "支付完成后积分将自动入账,如长时间未到账请联系客服。"}</p>
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user