From 5644f3f11bb06bb7b41a557b7e55fcac0fc70046 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8C=97=E6=9E=B3?= <7854742+wang_rumeng@user.noreply.gitee.com> Date: Tue, 30 Sep 2025 16:07:18 +0800 Subject: [PATCH 01/22] =?UTF-8?q?=E4=BC=98=E5=8C=96=E9=98=9F=E5=88=97?= =?UTF-8?q?=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/movie_queue.ts | 29 ++- components/QueueBox/H5QueueNotication.tsx | 167 +++++++++++++++ components/layout/H5TopBar.tsx | 5 +- components/layout/top-bar.tsx | 6 +- utils/notifications.tsx | 249 +++++++++++----------- 5 files changed, 323 insertions(+), 133 deletions(-) create mode 100644 components/QueueBox/H5QueueNotication.tsx diff --git a/api/movie_queue.ts b/api/movie_queue.ts index 273da29..a20d442 100644 --- a/api/movie_queue.ts +++ b/api/movie_queue.ts @@ -1,5 +1,5 @@ import { ApiResponse } from './common'; -import { showQueueNotification } from '../components/QueueBox/QueueNotification2'; +import { showH5QueueNotification } from '../components/QueueBox/H5QueueNotication'; import { notification } from 'antd'; /** 队列状态枚举 */ @@ -66,6 +66,7 @@ export async function withQueuePolling( let attempts = 0; let lastNotificationPosition: number | undefined; let isCancelled = false; + let closeModal: (() => void) | null = null; // 生成唯一的轮询ID const pollId = Math.random().toString(36).substring(7); @@ -73,7 +74,8 @@ export async function withQueuePolling( // 创建取消函数 const cancel = () => { isCancelled = true; - notification.destroy(); // 关闭通知 + try { closeModal?.(); } catch {} + notification.destroy(); // 兼容旧弹层 onCancel?.(); cancelTokens.delete(pollId); }; @@ -100,7 +102,19 @@ export async function withQueuePolling( // 如果位置发生变化,更新通知 if ((position !== lastNotificationPosition || status === QueueStatus.PROCESS) && position !== undefined && waiting !== undefined) { - showQueueNotification(position, waiting, status, cancel); + // 打开或更新 H5 弹窗(仅允许 Cancel 关闭,Refresh 触发刷新) + try { closeModal?.(); } catch {} + closeModal = showH5QueueNotification( + position, + waiting, + status, + cancel, + async () => { + // 触发一次立刻刷新:重置 attempts 的等待,直接递归调用 poll() + // 不关闭弹窗,由 showH5QueueNotification 保持打开 + attempts = Math.max(0, attempts - 1); + } + ); lastNotificationPosition = position; } @@ -120,15 +134,18 @@ export async function withQueuePolling( // 如果状态为ready,结束轮询 if (response.code !== 202 && response.data) { - notification.destroy(); // 关闭通知 + try { closeModal?.(); } catch {} + notification.destroy(); // 兼容旧弹层 onSuccess?.(response.data); return response; } - notification.destroy(); // 关闭通知 + try { closeModal?.(); } catch {} + notification.destroy(); // 兼容旧弹层 return response; } catch (error) { - notification.destroy(); // 关闭通知 + try { closeModal?.(); } catch {} + notification.destroy(); // 兼容旧弹层 if (error instanceof Error) { onError?.(error); } diff --git a/components/QueueBox/H5QueueNotication.tsx b/components/QueueBox/H5QueueNotication.tsx new file mode 100644 index 0000000..4fd062a --- /dev/null +++ b/components/QueueBox/H5QueueNotication.tsx @@ -0,0 +1,167 @@ +'use client'; + +import React, { useEffect, useRef } from 'react'; +import { createRoot, Root } from 'react-dom/client'; + +type QueueStatus = 'waiting' | 'process' | string; + +interface H5QueueNotificationProps { + position: number; + estimatedMinutes: number; + status: QueueStatus; + onCancel?: () => void; + onRefresh?: () => void; + onClose?: () => void; +} + +function H5QueueNotificationModal(props: H5QueueNotificationProps) { + const { position, estimatedMinutes, status, onCancel, onClose } = props; + const containerRef = useRef(null); + + // 禁用 ESC 关闭,保留空 effect 结构便于未来拓展 + useEffect(() => { + return () => {}; + }, []); + + useEffect(() => { + // Prevent background scroll on mobile while modal is open + const originalOverflow = document.body.style.overflow; + document.body.style.overflow = 'hidden'; + return () => { + document.body.style.overflow = originalOverflow; + }; + }, []); + + const message = status === 'process' + ? 'Your work is being produced. Please wait until it is completed before creating a new work.' + : `Your work is waiting for production at the ${position} position`; + + return ( +
+
+ {/* 去除右上角关闭按钮,避免除取消以外的关闭路径 */} + +
+
+ 🎬 +
+

Queue Reminder

+
+ +
+

+ {message} +

+ + {status !== 'process' && ( +
+
+
Position
+
#{position}
+
+
+
ETA
+
~{estimatedMinutes} min
+
+
+ )} +
+ +
+ + {status !== 'process' && ( + + )} +
+
+
+ ); +} + +/** + * Opens a lightweight H5-styled dark modal queue notification. + * @param {number} position - Current queue position. + * @param {number} estimatedMinutes - Estimated waiting minutes. + * @param {QueueStatus} status - Queue status: 'waiting' | 'process' | string. + * @param {() => void} onCancel - Callback when user confirms cancel. + * @returns {() => void} - Close function to dismiss the modal programmatically. + */ +export function showH5QueueNotification( + position: number, + estimatedMinutes: number, + status: QueueStatus, + onCancel?: () => void, + onRefresh?: () => void +): () => void { + if (typeof window === 'undefined' || typeof document === 'undefined') { + return () => {}; + } + + const mount = document.createElement('div'); + mount.setAttribute('data-alt', 'h5-queue-root'); + document.body.appendChild(mount); + + let root: Root | null = null; + try { + root = createRoot(mount); + } catch { + // Fallback cleanup if root creation fails + document.body.removeChild(mount); + return () => {}; + } + + const close = () => { + try { + root?.unmount(); + } finally { + if (mount.parentNode) { + mount.parentNode.removeChild(mount); + } + } + }; + + root.render( + + ); + + return close; +} + + diff --git a/components/layout/H5TopBar.tsx b/components/layout/H5TopBar.tsx index b92adc8..d458d66 100644 --- a/components/layout/H5TopBar.tsx +++ b/components/layout/H5TopBar.tsx @@ -172,7 +172,10 @@ export default function H5TopBar({ onSelectHomeTab }: H5TopBarProps) { const handleCustomAmountBuy = async () => { const amount = parseInt(customAmount); - if (isNaN(amount) || amount <= 0) return; + if (isNaN(amount) || amount < 50) { + window.msg.warning("Minimum purchase is 50 credits."); + return; + } await handleBuyTokens(amount); setCustomAmount(""); }; diff --git a/components/layout/top-bar.tsx b/components/layout/top-bar.tsx index b79a0fe..9120912 100644 --- a/components/layout/top-bar.tsx +++ b/components/layout/top-bar.tsx @@ -113,11 +113,7 @@ export function TopBar({ collapsed, isDesktop=true }: { collapsed: boolean, isDe const handleCustomAmountBuy = async () => { const amount = parseInt(customAmount); if (isNaN(amount) || amount < 50) { - showInsufficientPointsNotification({ - current_balance: credits, - required_tokens: 50, - message: "Minimum purchase is 50 credits.", - }); + window.msg.warning("Minimum purchase is 50 credits."); return; } await handleBuyTokens(amount); diff --git a/utils/notifications.tsx b/utils/notifications.tsx index f19b0a3..a89b27a 100644 --- a/utils/notifications.tsx +++ b/utils/notifications.tsx @@ -1,131 +1,138 @@ -import { notification } from 'antd'; -import { useRouter } from 'next/router'; +"use client"; -type NotificationType = 'success' | 'info' | 'warning' | 'error'; +import React, { useEffect, useRef } from 'react'; +import { createRoot, Root } from 'react-dom/client'; +import { BadgeCent } from 'lucide-react'; -const darkGlassStyle = { - background: 'rgba(30, 32, 40, 0.95)', - backdropFilter: 'blur(10px)', - WebkitBackdropFilter: 'blur(10px)', - border: '1px solid rgba(255, 255, 255, 0.08)', - borderRadius: '8px', - boxShadow: '0 4px 16px rgba(0, 0, 0, 0.4)', - padding: '12px 16px', -}; - -const messageStyle = { - fontSize: '13px', - fontWeight: 500, - color: '#ffffff', - marginBottom: '6px', - display: 'flex', - alignItems: 'center', - gap: '6px', -}; - -const iconStyle = { - color: '#F6B266', // 警告图标颜色 - background: 'rgba(246, 178, 102, 0.15)', - padding: '4px', - borderRadius: '6px', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', -}; - -const descriptionStyle = { - fontSize: '12px', - color: 'rgba(255, 255, 255, 0.65)', - marginBottom: '12px', - lineHeight: '1.5', -}; - -const btnStyle = { - color: 'rgb(250 173 20 / 90%)', - background: 'transparent', - border: 'none', - cursor: 'pointer', - padding: 0, - fontSize: '12px', - fontWeight: 500, - textDecoration: 'underline', - textUnderlineOffset: '2px', - textDecorationColor: 'rgb(250 173 20 / 60%)', - transition: 'all 0.2s ease', -}; - -/** - * 显示积分不足通知 - * @description 在右上角显示一个带有充值链接的积分不足提醒 - */ -const INSUFFICIENT_POINTS_KEY = 'insufficient-points-notification'; - -export const showInsufficientPointsNotification = (detail?: { +interface InsufficientDetail { current_balance?: number; required_tokens?: number; message?: string; -}) => { - // 先关闭现有的通知 - notification.destroy(INSUFFICIENT_POINTS_KEY); - - // 每次都生成新的 key - const uniqueKey = `${INSUFFICIENT_POINTS_KEY}-${Date.now()}`; +} - notification.warning({ - key: uniqueKey, - message: null, - description: ( -
-

- Insufficient credits reminder -

-

- {detail?.message || 'Your credits are insufficient, please upgrade to continue.'} - {detail?.current_balance !== undefined && detail?.required_tokens !== undefined && ( - <> -
- - Current balance: {detail.current_balance} / Required: {detail.required_tokens} - - - )} -

- -
- ), - duration: 0, - placement: 'topRight', - style: darkGlassStyle, - className: 'dark-glass-notification', - closeIcon: ( - - ), - }); -}; +
+
+ +
+

+ {titleText} +

+
+ +
+

+ {descriptionText} +

+ + {(typeof detail?.current_balance !== 'undefined') && (typeof detail?.required_tokens !== 'undefined') && ( +
+
+
Current balance
+
{detail?.current_balance}
+
+
+
Required
+
{detail?.required_tokens}
+
+
+ )} +
+ +
+ + +
+ + + ); +} /** - * 全局配置通知样式 + * Opens an H5-styled modal to notify insufficient credits. + * @param {InsufficientDetail} detail - optional detail including balances and message. */ -notification.config({ - maxCount: 3, // 最多同时显示3个通知 -}); +export function showInsufficientPointsNotification(detail?: InsufficientDetail): void { + if (typeof window === 'undefined' || typeof document === 'undefined') { + return; + } + + const mount = document.createElement('div'); + mount.setAttribute('data-alt', 'insufficient-modal-root'); + document.body.appendChild(mount); + + let root: Root | null = null; + try { + root = createRoot(mount); + } catch { + if (mount.parentNode) { + mount.parentNode.removeChild(mount); + } + return; + } + + const close = () => { + try { + root?.unmount(); + } finally { + if (mount.parentNode) { + mount.parentNode.removeChild(mount); + } + } + }; + + root.render( + + ); +} + From 41ebf2c447a4f7bff28abd0debb3ccf4904a34e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8C=97=E6=9E=B3?= <7854742+wang_rumeng@user.noreply.gitee.com> Date: Tue, 30 Sep 2025 16:24:53 +0800 Subject: [PATCH 02/22] =?UTF-8?q?=E5=8C=BA=E5=88=86=E7=A7=BB=E5=8A=A8?= =?UTF-8?q?=E7=AB=AF=E8=BE=93=E5=85=A5=E6=A1=86=EF=BC=8C=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E7=B1=BB=E5=90=8D=EF=BC=8C=E5=8E=BB=E8=AE=BE=E7=BD=AE=E5=AD=97?= =?UTF-8?q?=E4=BD=93=E5=A4=A7=E5=B0=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/globals.css | 2 +- app/signup/page.tsx | 7 ++++--- components/ChatInputBox/ChatInputBox.tsx | 2 +- components/ChatInputBox/H5PhotoStoryDrawer.tsx | 2 +- components/ChatInputBox/H5TemplateDrawer.tsx | 8 ++++---- components/SmartChatBox/InputBar.tsx | 2 +- components/layout/H5TopBar.tsx | 2 +- components/pages/login.tsx | 9 +++++---- 8 files changed, 18 insertions(+), 16 deletions(-) diff --git a/app/globals.css b/app/globals.css index 0e918bf..32576a1 100644 --- a/app/globals.css +++ b/app/globals.css @@ -307,6 +307,6 @@ body { } } -textarea, input { +.mobile-textarea, .mobile-input { font-size: 16px !important; } \ No newline at end of file diff --git a/app/signup/page.tsx b/app/signup/page.tsx index 33808c6..157a35d 100644 --- a/app/signup/page.tsx +++ b/app/signup/page.tsx @@ -9,6 +9,7 @@ import { GoogleLoginButton } from "@/components/ui/google-login-button"; import { Eye, EyeOff, Mail, PartyPopper } from "lucide-react"; import { isGoogleLoginEnabled } from "@/lib/server-config"; import { fetchSettingByCode } from "@/api/serversetting"; +import { useDeviceType } from "@/hooks/useDeviceType"; export default function SignupPage() { const [name, setName] = useState(""); @@ -31,7 +32,7 @@ export default function SignupPage() { const [showGoogleLogin, setShowGoogleLogin] = useState(false); const [showRedirectModal, setShowRedirectModal] = useState(false); const router = useRouter(); - + const { isMobile } = useDeviceType(); // Handle scroll indicator for small screens and load SSO config React.useEffect(() => { try { @@ -448,7 +449,7 @@ export default function SignupPage() { onFocus={() => setEmailFocused(true)} onBlur={() => setEmailFocused(false)} required - className="w-full px-4 py-3 rounded-lg bg-black/30 border border-white/20 text-white placeholder-gray-400 focus:outline-none focus:ring-1 focus:border-custom-blue/80" + className={`w-full px-4 py-3 rounded-lg bg-black/30 border border-white/20 text-white placeholder-gray-400 focus:outline-none focus:ring-1 focus:border-custom-blue/80 ${isMobile ? 'mobile-input' : ''}`} /> @@ -473,7 +474,7 @@ export default function SignupPage() { required className={`w-full px-4 py-3 pr-12 rounded-lg bg-black/30 border border-white/20 text-white placeholder-gray-400 focus:outline-none focus:ring-1 focus:border-custom-blue/80 ${ passwordError ? "border-red-500/50" : "border-white/20" - }`} + } ${isMobile ? 'mobile-input' : ''}`} />