style(Account UX): resilient binding layout, copyable popovers, pastel header, and custom pay colors

- AccountManagement.js
  - Prevent action button from shifting when account IDs are long by adding gap, min-w-0, and flex-shrink-0; keep buttons in a fixed position.
  - Add copyable Popover for account identifiers (email/GitHub/OIDC/Telegram/LinuxDO) using Typography.Paragraph with copyable; reveal full text on hover.
  - Ensure ellipsis works by rendering the popover trigger as `block max-w-full truncate`.
  - Import Popover and wire up `renderAccountInfo` across all binding rows.

- UserInfoHeader.js
  - Apply unified `with-pastel-balls` background to match PricingVendorIntro.
  - Remove legacy absolute-positioned circles and top gradient bar to avoid visual overlap.

- RechargeCard.jsx
  - Colorize non-Alipay/WeChat/Stripe payment icons using backend `pay_methods[].color`; fallback to `var(--semi-color-text-2)`.
  - Add `showClear` to the redemption code input for quicker clearing.

Notes:
- No linter errors introduced.
- i18n strings and behavior remain unchanged except for improved UX and visual consistency.
This commit is contained in:
t0ng7u
2025-08-17 11:45:55 +08:00
parent bbe381f656
commit 7411c24954
8 changed files with 208 additions and 157 deletions

View File

@@ -26,7 +26,8 @@ import {
Typography,
Avatar,
Tabs,
TabPane
TabPane,
Popover
} from '@douyinfe/semi-ui';
import {
IconMail,
@@ -58,6 +59,30 @@ const AccountManagement = ({
setShowChangePasswordModal,
setShowAccountDeleteModal
}) => {
const renderAccountInfo = (accountId, label) => {
if (!accountId || accountId === '') {
return <span className="text-gray-500">{t('未绑定')}</span>;
}
const popContent = (
<div className="text-xs p-2">
<Typography.Paragraph copyable={{ content: accountId }}>
{accountId}
</Typography.Paragraph>
{label ? (
<div className="mt-1 text-[11px] text-gray-500">{label}</div>
) : null}
</div>
);
return (
<Popover content={popContent} position="top" trigger="hover">
<span className="block max-w-full truncate text-gray-600 hover:text-blue-600 cursor-pointer">
{accountId}
</span>
</Popover>
);
};
return (
<Card className="!rounded-2xl">
{/* 卡片头部 */}
@@ -71,7 +96,7 @@ const AccountManagement = ({
</div>
</div>
<Tabs type="line" defaultActiveKey="binding">
<Tabs type="card" defaultActiveKey="binding">
{/* 账户绑定 Tab */}
<TabPane
tab={
@@ -86,39 +111,38 @@ const AccountManagement = ({
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* 邮箱绑定 */}
<Card className="!rounded-xl">
<div className="flex items-center justify-between">
<div className="flex items-center flex-1">
<div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3">
<div className="flex items-center justify-between gap-3">
<div className="flex items-center flex-1 min-w-0">
<div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3 flex-shrink-0">
<IconMail size="default" className="text-slate-600 dark:text-slate-300" />
</div>
<div className="flex-1 min-w-0">
<div className="font-medium text-gray-900">{t('邮箱')}</div>
<div className="text-sm text-gray-500 truncate">
{userState.user && userState.user.email !== ''
? userState.user.email
: t('未绑定')}
{renderAccountInfo(userState.user?.email, t('邮箱地址'))}
</div>
</div>
</div>
<Button
type="primary"
theme="outline"
size="small"
onClick={() => setShowEmailBindModal(true)}
className="!rounded-lg"
>
{userState.user && userState.user.email !== ''
? t('修改绑定')
: t('绑定')}
</Button>
<div className="flex-shrink-0">
<Button
type="primary"
theme="outline"
size="small"
onClick={() => setShowEmailBindModal(true)}
>
{userState.user && userState.user.email !== ''
? t('修改绑定')
: t('绑定')}
</Button>
</div>
</div>
</Card>
{/* 微信绑定 */}
<Card className="!rounded-xl">
<div className="flex items-center justify-between">
<div className="flex items-center flex-1">
<div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3">
<div className="flex items-center justify-between gap-3">
<div className="flex items-center flex-1 min-w-0">
<div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3 flex-shrink-0">
<SiWechat size={20} className="text-slate-600 dark:text-slate-300" />
</div>
<div className="flex-1 min-w-0">
@@ -130,110 +154,107 @@ const AccountManagement = ({
</div>
</div>
</div>
<Button
type="primary"
theme="outline"
size="small"
disabled={!status.wechat_login}
onClick={() => setShowWeChatBindModal(true)}
className="!rounded-lg"
>
{userState.user && userState.user.wechat_id !== ''
? t('修改绑定')
: status.wechat_login
? t('绑定')
: t('未启用')}
</Button>
<div className="flex-shrink-0">
<Button
type="primary"
theme="outline"
size="small"
disabled={!status.wechat_login}
onClick={() => setShowWeChatBindModal(true)}
>
{userState.user && userState.user.wechat_id !== ''
? t('修改绑定')
: status.wechat_login
? t('绑定')
: t('未启用')}
</Button>
</div>
</div>
</Card>
{/* GitHub绑定 */}
<Card className="!rounded-xl">
<div className="flex items-center justify-between">
<div className="flex items-center flex-1">
<div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3">
<div className="flex items-center justify-between gap-3">
<div className="flex items-center flex-1 min-w-0">
<div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3 flex-shrink-0">
<IconGithubLogo size="default" className="text-slate-600 dark:text-slate-300" />
</div>
<div className="flex-1 min-w-0">
<div className="font-medium text-gray-900">{t('GitHub')}</div>
<div className="text-sm text-gray-500 truncate">
{userState.user && userState.user.github_id !== ''
? userState.user.github_id
: t('未绑定')}
{renderAccountInfo(userState.user?.github_id, t('GitHub ID'))}
</div>
</div>
</div>
<Button
type="primary"
theme="outline"
size="small"
onClick={() => onGitHubOAuthClicked(status.github_client_id)}
disabled={
(userState.user && userState.user.github_id !== '') ||
!status.github_oauth
}
className="!rounded-lg"
>
{status.github_oauth ? t('绑定') : t('未启用')}
</Button>
<div className="flex-shrink-0">
<Button
type="primary"
theme="outline"
size="small"
onClick={() => onGitHubOAuthClicked(status.github_client_id)}
disabled={
(userState.user && userState.user.github_id !== '') ||
!status.github_oauth
}
>
{status.github_oauth ? t('绑定') : t('未启用')}
</Button>
</div>
</div>
</Card>
{/* OIDC绑定 */}
<Card className="!rounded-xl">
<div className="flex items-center justify-between">
<div className="flex items-center flex-1">
<div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3">
<div className="flex items-center justify-between gap-3">
<div className="flex items-center flex-1 min-w-0">
<div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3 flex-shrink-0">
<IconShield size="default" className="text-slate-600 dark:text-slate-300" />
</div>
<div className="flex-1 min-w-0">
<div className="font-medium text-gray-900">{t('OIDC')}</div>
<div className="text-sm text-gray-500 truncate">
{userState.user && userState.user.oidc_id !== ''
? userState.user.oidc_id
: t('未绑定')}
{renderAccountInfo(userState.user?.oidc_id, t('OIDC ID'))}
</div>
</div>
</div>
<Button
type="primary"
theme="outline"
size="small"
onClick={() => onOIDCClicked(
status.oidc_authorization_endpoint,
status.oidc_client_id,
)}
disabled={
(userState.user && userState.user.oidc_id !== '') ||
!status.oidc_enabled
}
className="!rounded-lg"
>
{status.oidc_enabled ? t('绑定') : t('未启用')}
</Button>
<div className="flex-shrink-0">
<Button
type="primary"
theme="outline"
size="small"
onClick={() => onOIDCClicked(
status.oidc_authorization_endpoint,
status.oidc_client_id,
)}
disabled={
(userState.user && userState.user.oidc_id !== '') ||
!status.oidc_enabled
}
>
{status.oidc_enabled ? t('绑定') : t('未启用')}
</Button>
</div>
</div>
</Card>
{/* Telegram绑定 */}
<Card className="!rounded-xl">
<div className="flex items-center justify-between">
<div className="flex items-center flex-1">
<div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3">
<div className="flex items-center justify-between gap-3">
<div className="flex items-center flex-1 min-w-0">
<div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3 flex-shrink-0">
<SiTelegram size={20} className="text-slate-600 dark:text-slate-300" />
</div>
<div className="flex-1 min-w-0">
<div className="font-medium text-gray-900">{t('Telegram')}</div>
<div className="text-sm text-gray-500 truncate">
{userState.user && userState.user.telegram_id !== ''
? userState.user.telegram_id
: t('未绑定')}
{renderAccountInfo(userState.user?.telegram_id, t('Telegram ID'))}
</div>
</div>
</div>
<div className="flex-shrink-0">
{status.telegram_oauth ? (
userState.user.telegram_id !== '' ? (
<Button disabled={true} size="small" className="!rounded-lg">
<Button disabled={true} size="small">
{t('已绑定')}
</Button>
) : (
@@ -245,7 +266,7 @@ const AccountManagement = ({
</div>
)
) : (
<Button disabled={true} size="small" className="!rounded-lg">
<Button disabled={true} size="small">
{t('未启用')}
</Button>
)}
@@ -255,33 +276,32 @@ const AccountManagement = ({
{/* LinuxDO绑定 */}
<Card className="!rounded-xl">
<div className="flex items-center justify-between">
<div className="flex items-center flex-1">
<div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3">
<div className="flex items-center justify-between gap-3">
<div className="flex items-center flex-1 min-w-0">
<div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3 flex-shrink-0">
<SiLinux size={20} className="text-slate-600 dark:text-slate-300" />
</div>
<div className="flex-1 min-w-0">
<div className="font-medium text-gray-900">{t('LinuxDO')}</div>
<div className="text-sm text-gray-500 truncate">
{userState.user && userState.user.linux_do_id !== ''
? userState.user.linux_do_id
: t('未绑定')}
{renderAccountInfo(userState.user?.linux_do_id, t('LinuxDO ID'))}
</div>
</div>
</div>
<Button
type="primary"
theme="outline"
size="small"
onClick={() => onLinuxDOOAuthClicked(status.linuxdo_client_id)}
disabled={
(userState.user && userState.user.linux_do_id !== '') ||
!status.linuxdo_oauth
}
className="!rounded-lg"
>
{status.linuxdo_oauth ? t('绑定') : t('未启用')}
</Button>
<div className="flex-shrink-0">
<Button
type="primary"
theme="outline"
size="small"
onClick={() => onLinuxDOOAuthClicked(status.linuxdo_client_id)}
disabled={
(userState.user && userState.user.linux_do_id !== '') ||
!status.linuxdo_oauth
}
>
{status.linuxdo_oauth ? t('绑定') : t('未启用')}
</Button>
</div>
</div>
</Card>
</div>
@@ -322,7 +342,6 @@ const AccountManagement = ({
value={systemToken}
onClick={handleSystemTokenClick}
size="large"
className="!rounded-lg"
prefix={<IconKey />}
/>
</div>
@@ -333,7 +352,7 @@ const AccountManagement = ({
type="primary"
theme="solid"
onClick={generateAccessToken}
className="!rounded-lg !bg-slate-600 hover:!bg-slate-700 w-full sm:w-auto"
className="!bg-slate-600 hover:!bg-slate-700 w-full sm:w-auto"
icon={<IconKey />}
>
{systemToken ? t('重新生成') : t('生成令牌')}
@@ -361,7 +380,7 @@ const AccountManagement = ({
type="primary"
theme="solid"
onClick={() => setShowChangePasswordModal(true)}
className="!rounded-lg !bg-slate-600 hover:!bg-slate-700 w-full sm:w-auto"
className="!bg-slate-600 hover:!bg-slate-700 w-full sm:w-auto"
icon={<IconLock />}
>
{t('修改密码')}
@@ -392,7 +411,7 @@ const AccountManagement = ({
type="danger"
theme="solid"
onClick={() => setShowAccountDeleteModal(true)}
className="!rounded-lg w-full sm:w-auto !bg-slate-500 hover:!bg-slate-600"
className="w-full sm:w-auto !bg-slate-500 hover:!bg-slate-600"
icon={<IconDelete />}
>
{t('删除账户')}

View File

@@ -106,7 +106,7 @@ const NotificationSettings = ({
onSubmit={handleSubmit}
>
{() => (
<Tabs type="line" defaultActiveKey="notification">
<Tabs type="card" defaultActiveKey="notification">
{/* 通知配置 Tab */}
<TabPane
tab={
@@ -223,11 +223,11 @@ const NotificationSettings = ({
/>
</div>
<div className="text-xs text-gray-500 leading-relaxed">
<div><strong>type:</strong> (quota_exceed: )</div>
<div><strong>title:</strong> </div>
<div><strong>content:</strong> {`{{value}}`} </div>
<div><strong>values:</strong> content</div>
<div><strong>timestamp:</strong> Unix</div>
<div><strong>type:</strong> {t(' (quota_exceed: )')} </div>
<div><strong>title:</strong> {t('')}</div>
<div><strong>content:</strong> {t(' {{value}} ')}</div>
<div><strong>values:</strong> {t('content')}</div>
<div><strong>timestamp:</strong> {t('Unix')}</div>
</div>
</div>
</Form.Slot>

View File

@@ -19,12 +19,10 @@ For commercial licensing, please contact support@quantumnous.com
import React from 'react';
import { Avatar, Card, Tag, Divider, Typography } from '@douyinfe/semi-ui';
import { isRoot, isAdmin, renderQuota } from '../../../../helpers';
import { useTheme } from '../../../../context/Theme';
import { isRoot, isAdmin, renderQuota, stringToColor } from '../../../../helpers';
import { Coins, BarChart2, Users } from 'lucide-react';
const UserInfoHeader = ({ t, userState }) => {
const theme = useTheme();
const getUsername = () => {
if (userState.user) {
@@ -44,34 +42,19 @@ const UserInfoHeader = ({ t, userState }) => {
};
return (
<Card
className="!rounded-2xl !border-0"
style={{
background: theme === 'dark'
? 'linear-gradient(135deg, #1e293b 0%, #334155 50%, #475569 100%)'
: 'linear-gradient(135deg, #f8fafc 0%, #e2e8f0 50%, #cbd5e1 100%)',
position: 'relative'
}}
bodyStyle={{ padding: 0 }}
>
{/* 装饰性背景元素 */}
<div className="absolute inset-0 overflow-hidden">
<div className="absolute -top-10 -right-10 w-40 h-40 bg-slate-400 dark:bg-slate-500 opacity-5 rounded-full"></div>
<div className="absolute -bottom-16 -left-16 w-48 h-48 bg-slate-300 dark:bg-slate-400 opacity-8 rounded-full"></div>
<div className="absolute top-1/2 right-1/4 w-24 h-24 bg-slate-400 dark:bg-slate-500 opacity-6 rounded-full"></div>
</div>
<div className="relative p-4 sm:p-6 md:p-8 text-gray-600 dark:text-gray-300">
<Card className="!rounded-2xl with-pastel-balls">
<div className="relative text-gray-600 dark:text-gray-300">
<div className="flex justify-between items-start mb-4 sm:mb-6">
<div className="flex items-center flex-1 min-w-0">
<Avatar
size='large'
className="mr-3 sm:mr-4 shadow-md flex-shrink-0 bg-slate-500 dark:bg-slate-400"
className="mr-3 sm:mr-4 shadow-md flex-shrink-0"
color={stringToColor(getUsername())}
>
{getAvatarText()}
</Avatar>
<div className="flex-1 min-w-0">
<div className="text-base sm:text-lg font-semibold truncate text-gray-800 dark:text-gray-100">
<div className="text-base text-3xl font-semibold truncate text-gray-800 dark:text-gray-100">
{getUsername()}
</div>
<div className="mt-1 flex flex-wrap gap-1 sm:gap-2">
@@ -113,7 +96,7 @@ const UserInfoHeader = ({ t, userState }) => {
{/* 右上角统计信息Semi UI 卡片) */}
<div className="hidden sm:block flex-shrink-0 ml-2">
<Card size="small" className="!rounded-xl !border-0 shadow-sm" bodyStyle={{ padding: '8px 12px' }}>
<Card size="small" className="!rounded-xl shadow-sm" bodyStyle={{ padding: '8px 12px' }}>
<div className="flex items-center gap-3 lg:gap-4">
<div className="flex items-center justify-end gap-2">
<Coins size={16} className="text-slate-600 dark:text-slate-300" />
@@ -182,11 +165,9 @@ const UserInfoHeader = ({ t, userState }) => {
</div>
</Card>
</div>
<div className="absolute top-0 left-0 w-full h-2 bg-gradient-to-r from-slate-300 via-slate-400 to-slate-500 dark:from-slate-600 dark:via-slate-500 dark:to-slate-400 opacity-40"></div>
</div>
</Card>
);
};
export default UserInfoHeader;
export default UserInfoHeader;