mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-13 07:46:54 +08:00
Refactor(UI) : Refactor Frontend: Unified Toasts with Sonner, Introduced Layout System, and Integrated React Router (#872)
This commit is contained in:
30
web/src/lib/clipboard.ts
Normal file
30
web/src/lib/clipboard.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { notify } from './notify'
|
||||
|
||||
/**
|
||||
* 复制文本到剪贴板,并显示轻量提示。
|
||||
*/
|
||||
export async function copyWithToast(text: string, successMsg = '已复制') {
|
||||
try {
|
||||
if (navigator?.clipboard?.writeText) {
|
||||
await navigator.clipboard.writeText(text)
|
||||
} else {
|
||||
// 兼容降级:创建临时文本域执行复制
|
||||
const el = document.createElement('textarea')
|
||||
el.value = text
|
||||
el.style.position = 'fixed'
|
||||
el.style.left = '-9999px'
|
||||
document.body.appendChild(el)
|
||||
el.select()
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(el)
|
||||
}
|
||||
notify.success(successMsg)
|
||||
return true
|
||||
} catch (err) {
|
||||
console.error('Clipboard copy failed:', err)
|
||||
notify.error('复制失败')
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export default { copyWithToast }
|
||||
@@ -8,6 +8,8 @@
|
||||
* - Automatic redirect to login page
|
||||
*/
|
||||
|
||||
import { toast } from 'sonner'
|
||||
|
||||
export class HttpClient {
|
||||
// Singleton flag to prevent duplicate 401 handling
|
||||
private static isHandling401 = false
|
||||
@@ -23,52 +25,7 @@ export class HttpClient {
|
||||
* Show login required notification to user
|
||||
*/
|
||||
private showLoginRequiredNotification(): void {
|
||||
// Create notification element
|
||||
const notification = document.createElement('div')
|
||||
notification.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: linear-gradient(135deg, #F0B90B 0%, #FCD535 100%);
|
||||
color: #0B0E11;
|
||||
padding: 16px 24px;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
z-index: 10000;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
animation: slideDown 0.3s ease-out;
|
||||
`
|
||||
notification.textContent = '⚠️ 登录已过期,请先登录'
|
||||
|
||||
// Add slide down animation
|
||||
const style = document.createElement('style')
|
||||
style.textContent = `
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-50%) translateY(-20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
}
|
||||
`
|
||||
document.head.appendChild(style)
|
||||
|
||||
// Add to page
|
||||
document.body.appendChild(notification)
|
||||
|
||||
// Auto remove after animation
|
||||
setTimeout(() => {
|
||||
notification.style.animation = 'slideDown 0.3s ease-out reverse'
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(notification)
|
||||
document.head.removeChild(style)
|
||||
}, 300)
|
||||
}, 1800)
|
||||
toast.warning('登录已过期,请先登录', { duration: 1800 })
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
87
web/src/lib/notify.tsx
Normal file
87
web/src/lib/notify.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import { toast } from 'sonner'
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
export interface ConfirmOptions {
|
||||
title?: string
|
||||
message?: string
|
||||
okText?: string
|
||||
cancelText?: string
|
||||
}
|
||||
|
||||
// 全局 confirm 函数的引用,将在 ConfirmDialogProvider 中设置
|
||||
let globalConfirm:
|
||||
| ((options: ConfirmOptions & { message: string }) => Promise<boolean>)
|
||||
| null = null
|
||||
|
||||
export function setGlobalConfirm(
|
||||
confirmFn: (options: ConfirmOptions & { message: string }) => Promise<boolean>
|
||||
) {
|
||||
globalConfirm = confirmFn
|
||||
}
|
||||
|
||||
// 确认对话框函数,使用 shadcn AlertDialog
|
||||
export function confirmToast(
|
||||
message: string,
|
||||
options: ConfirmOptions = {}
|
||||
): Promise<boolean> {
|
||||
if (!globalConfirm) {
|
||||
console.error('ConfirmDialogProvider not initialized')
|
||||
return Promise.resolve(false)
|
||||
}
|
||||
|
||||
return globalConfirm({
|
||||
message,
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
// 统一通知封装,避免组件直接依赖 sonner
|
||||
type Message = string | ReactNode
|
||||
|
||||
function message(msg: Message, options?: Parameters<typeof toast>[1]) {
|
||||
return toast(msg as any, options)
|
||||
}
|
||||
|
||||
function success(msg: Message, options?: Parameters<typeof toast.success>[1]) {
|
||||
return toast.success(msg as any, options)
|
||||
}
|
||||
|
||||
function error(msg: Message, options?: Parameters<typeof toast.error>[1]) {
|
||||
return toast.error(msg as any, options)
|
||||
}
|
||||
|
||||
function info(msg: Message, options?: Parameters<typeof toast.info>[1]) {
|
||||
return toast.info?.(msg as any, options) ?? toast(msg as any, options)
|
||||
}
|
||||
|
||||
function warning(msg: Message, options?: Parameters<typeof toast.warning>[1]) {
|
||||
return toast.warning?.(msg as any, options) ?? toast(msg as any, options)
|
||||
}
|
||||
|
||||
function custom(
|
||||
renderer: Parameters<typeof toast.custom>[0],
|
||||
options?: Parameters<typeof toast.custom>[1]
|
||||
) {
|
||||
return toast.custom(renderer, options)
|
||||
}
|
||||
|
||||
function dismiss(id?: string | number) {
|
||||
return toast.dismiss(id as any)
|
||||
}
|
||||
|
||||
function promise<T>(p: Promise<T> | (() => Promise<T>), msgs: any) {
|
||||
return toast.promise<T>(p as any, msgs as any)
|
||||
}
|
||||
|
||||
export const notify = {
|
||||
message,
|
||||
success,
|
||||
error,
|
||||
info,
|
||||
warning,
|
||||
custom,
|
||||
dismiss,
|
||||
promise,
|
||||
}
|
||||
|
||||
export default { confirmToast, notify }
|
||||
28
web/src/lib/text.ts
Normal file
28
web/src/lib/text.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* 文本工具
|
||||
*
|
||||
* stripLeadingIcons: 去掉翻译文案或标题前面用于装饰的 Emoji/符号,
|
||||
* 以便在组件里自行放置图标时不重复显示。
|
||||
*/
|
||||
|
||||
/**
|
||||
* 去掉开头的装饰性 Emoji/符号以及随后的分隔符(空格/冒号/点号等)。
|
||||
*/
|
||||
export function stripLeadingIcons(input: string | undefined | null): string {
|
||||
if (!input) return ''
|
||||
let s = String(input)
|
||||
|
||||
// 1) 去除常见的 Emoji/符号块(箭头、杂项符号、几何图形、表情等)
|
||||
// 覆盖常见范围,兼容性好于使用 Unicode 属性类。
|
||||
s = s.replace(
|
||||
/^[\s\u2190-\u21FF\u2300-\u23FF\u2460-\u24FF\u25A0-\u25FF\u2600-\u27BF\u2B00-\u2BFF\u1F000-\u1FAFF]+/u,
|
||||
''
|
||||
)
|
||||
|
||||
// 2) 去掉开头可能残留的分隔符(空格、连字符、冒号、居中点等)
|
||||
s = s.replace(/^[\s\-:•·]+/, '')
|
||||
|
||||
return s.trim()
|
||||
}
|
||||
|
||||
export default { stripLeadingIcons }
|
||||
Reference in New Issue
Block a user