fix(ui): 修复登录页中英混用 + 搜索栏溢出,更新品牌口号并支持后台配置

- admin-web 登录页底部 "Genex 券金融平台" → "Genex 管理后台",风格统一
- admin-web 顶部搜索栏改为弹性宽度(width:100%/maxWidth:320),修复窄屏溢出
- 全平台品牌口号统一更新为"让每一张券,自由流动"
  覆盖:genex-mobile(4语言) / miniapp(3语言) / portal(zh-CN/en-US)
- backend admin-system.service: getConfig() 新增 brandConfig 字段(4语言口号)
- admin-web 系统配置页新增品牌配置卡片:支持4语言口号在线编辑并保存

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-03-03 20:41:37 -08:00
parent e7c1e33355
commit 04a55e3e12
12 changed files with 126 additions and 16 deletions

View File

@ -57,6 +57,12 @@ export class AdminSystemService {
async getConfig() { async getConfig() {
return { return {
brandConfig: {
sloganZhCN: '让每一张券,自由流动',
sloganZhTW: '讓每一張券,自由流動',
sloganEn: 'Where Every Coupon Flows Freely',
sloganJa: 'すべてのクーポンを、自由に流通させる',
},
feeConfig: { feeConfig: {
primaryMarketFee: 2.5, primaryMarketFee: 2.5,
secondaryMarketFee: 1.0, secondaryMarketFee: 1.0,

View File

@ -74,7 +74,7 @@ export default function LoginPage() {
</button> </button>
</form> </form>
<p style={styles.footer}>Genex &copy; 2025</p> <p style={styles.footer}>Genex &copy; 2025</p>
</div> </div>
</div> </div>
); );

View File

@ -396,6 +396,13 @@ const translations: Record<Locale, Record<string, string>> = {
'system_logs': '系统日志', 'system_logs': '系统日志',
'system_feature_flags': '功能开关', 'system_feature_flags': '功能开关',
'system_rate_limit': '限流配置', 'system_rate_limit': '限流配置',
'system_brand_config': '品牌配置',
'system_brand_slogan_zh_cn': '口号(简体中文)',
'system_brand_slogan_zh_tw': '口号(繁体中文)',
'system_brand_slogan_en': '口号(英文)',
'system_brand_slogan_ja': '口号(日文)',
'system_brand_save': '保存品牌配置',
'system_brand_saved': '已保存',
'system_fee_config': '手续费率设置', 'system_fee_config': '手续费率设置',
'system_kyc_config': 'KYC阈值配置', 'system_kyc_config': 'KYC阈值配置',
'system_trade_limit_config': '交易限额配置', 'system_trade_limit_config': '交易限额配置',
@ -1155,6 +1162,13 @@ const translations: Record<Locale, Record<string, string>> = {
'system_logs': 'System Logs', 'system_logs': 'System Logs',
'system_feature_flags': 'Feature Flags', 'system_feature_flags': 'Feature Flags',
'system_rate_limit': 'Rate Limiting', 'system_rate_limit': 'Rate Limiting',
'system_brand_config': 'Brand Config',
'system_brand_slogan_zh_cn': 'Slogan (Simplified Chinese)',
'system_brand_slogan_zh_tw': 'Slogan (Traditional Chinese)',
'system_brand_slogan_en': 'Slogan (English)',
'system_brand_slogan_ja': 'Slogan (Japanese)',
'system_brand_save': 'Save Brand Config',
'system_brand_saved': 'Saved',
'system_fee_config': 'Fee Rate Settings', 'system_fee_config': 'Fee Rate Settings',
'system_kyc_config': 'KYC Threshold Config', 'system_kyc_config': 'KYC Threshold Config',
'system_trade_limit_config': 'Trade Limit Config', 'system_trade_limit_config': 'Trade Limit Config',
@ -1914,6 +1928,13 @@ const translations: Record<Locale, Record<string, string>> = {
'system_logs': 'システムログ', 'system_logs': 'システムログ',
'system_feature_flags': '機能フラグ', 'system_feature_flags': '機能フラグ',
'system_rate_limit': 'レート制限', 'system_rate_limit': 'レート制限',
'system_brand_config': 'ブランド設定',
'system_brand_slogan_zh_cn': 'スローガン(簡体字)',
'system_brand_slogan_zh_tw': 'スローガン(繁体字)',
'system_brand_slogan_en': 'スローガン(英語)',
'system_brand_slogan_ja': 'スローガン(日本語)',
'system_brand_save': 'ブランド設定を保存',
'system_brand_saved': '保存済み',
'system_fee_config': '手数料率設定', 'system_fee_config': '手数料率設定',
'system_kyc_config': 'KYC閾値設定', 'system_kyc_config': 'KYC閾値設定',
'system_trade_limit_config': '取引限度額設定', 'system_trade_limit_config': '取引限度額設定',

View File

@ -287,11 +287,12 @@ export const AdminLayout: React.FC<{ children: React.ReactNode }> = ({ children
padding: '0 24px', padding: '0 24px',
justifyContent: 'space-between', justifyContent: 'space-between',
}}> }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 12, flex: 1, minWidth: 0, marginRight: 16 }}>
<input <input
placeholder={t('header_search_placeholder')} placeholder={t('header_search_placeholder')}
style={{ style={{
width: 320, width: '100%',
maxWidth: 320,
height: 36, height: 36,
border: '1px solid var(--color-border)', border: '1px solid var(--color-border)',
borderRadius: 'var(--radius-full)', borderRadius: 'var(--radius-full)',

View File

@ -1,6 +1,6 @@
'use client'; 'use client';
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import { t } from '@/i18n/locales'; import { t } from '@/i18n/locales';
import { useApi } from '@/lib/use-api'; import { useApi } from '@/lib/use-api';
@ -30,6 +30,17 @@ interface ServiceHealth {
mem: string; mem: string;
} }
interface BrandConfig {
sloganZhCN: string;
sloganZhTW: string;
sloganEn: string;
sloganJa: string;
}
interface SystemConfig {
brandConfig: BrandConfig;
}
interface SystemHealthResponse { interface SystemHealthResponse {
services: ServiceHealth[]; services: ServiceHealth[];
contracts: { name: string; address: string; version: string; status: string }[]; contracts: { name: string; address: string; version: string; status: string }[];
@ -42,13 +53,37 @@ const loadingBox: React.CSSProperties = {
export const SystemManagementPage: React.FC = () => { export const SystemManagementPage: React.FC = () => {
const [activeTab, setActiveTab] = useState<'admins' | 'config' | 'contracts' | 'monitor'>('admins'); const [activeTab, setActiveTab] = useState<'admins' | 'config' | 'contracts' | 'monitor'>('admins');
const [brand, setBrand] = useState<BrandConfig>({
sloganZhCN: '让每一张券,自由流动',
sloganZhTW: '讓每一張券,自由流動',
sloganEn: 'Where Every Coupon Flows Freely',
sloganJa: 'すべてのクーポンを、自由に流通させる',
});
const [brandSaved, setBrandSaved] = useState(false);
const { data: adminsData, isLoading: adminsLoading } = useApi<Admin[]>( const { data: adminsData, isLoading: adminsLoading } = useApi<Admin[]>(
activeTab === 'admins' ? '/api/v1/admin/system/admins' : null, activeTab === 'admins' ? '/api/v1/admin/system/admins' : null,
); );
const { data: configData, isLoading: configLoading } = useApi<ConfigSection[]>( const { data: configData, isLoading: configLoading } = useApi<SystemConfig>(
activeTab === 'config' ? '/api/v1/admin/system/config' : null, activeTab === 'config' ? '/api/v1/admin/system/config' : null,
); );
useEffect(() => {
if (configData?.brandConfig) {
setBrand(configData.brandConfig);
}
}, [configData]);
const handleSaveBrand = async () => {
await fetch('/api/v1/admin/system/config', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ brandConfig: brand }),
});
setBrandSaved(true);
setTimeout(() => setBrandSaved(false), 2000);
};
const { data: healthData, isLoading: healthLoading } = useApi<SystemHealthResponse>( const { data: healthData, isLoading: healthLoading } = useApi<SystemHealthResponse>(
activeTab === 'contracts' || activeTab === 'monitor' ? '/api/v1/admin/system/health' : null, activeTab === 'contracts' || activeTab === 'monitor' ? '/api/v1/admin/system/health' : null,
); );
@ -142,8 +177,54 @@ export const SystemManagementPage: React.FC = () => {
configLoading ? ( configLoading ? (
<div style={loadingBox}>Loading...</div> <div style={loadingBox}>Loading...</div>
) : ( ) : (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: 16 }}> <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
{(configData ?? []).map(section => ( {/* Brand Config */}
<div style={{
background: 'var(--color-surface)',
borderRadius: 'var(--radius-md)',
border: '1px solid var(--color-border-light)',
padding: 20,
}}>
<div style={{ font: 'var(--text-h3)', marginBottom: 16 }}>{t('system_brand_config')}</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: 12, marginBottom: 16 }}>
{([
{ key: 'sloganZhCN', label: t('system_brand_slogan_zh_cn') },
{ key: 'sloganZhTW', label: t('system_brand_slogan_zh_tw') },
{ key: 'sloganEn', label: t('system_brand_slogan_en') },
{ key: 'sloganJa', label: t('system_brand_slogan_ja') },
] as { key: keyof BrandConfig; label: string }[]).map(field => (
<div key={field.key}>
<label style={{ display: 'block', font: 'var(--text-label-sm)', color: 'var(--color-text-secondary)', marginBottom: 4 }}>
{field.label}
</label>
<input
value={brand[field.key]}
onChange={e => setBrand(prev => ({ ...prev, [field.key]: e.target.value }))}
style={{
width: '100%', padding: '8px 12px', boxSizing: 'border-box',
border: '1px solid var(--color-border)', borderRadius: 'var(--radius-sm)',
font: 'var(--text-body)', outline: 'none', background: 'var(--color-gray-50)',
}}
/>
</div>
))}
</div>
<button
onClick={handleSaveBrand}
style={{
padding: '8px 20px', border: 'none', borderRadius: 'var(--radius-sm)',
background: brandSaved ? 'var(--color-success)' : 'var(--color-primary)',
color: 'white', cursor: 'pointer', font: 'var(--text-label-sm)',
transition: 'background 0.2s',
}}
>
{brandSaved ? t('system_brand_saved') : t('system_brand_save')}
</button>
</div>
{/* Other config sections */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: 16 }}>
{([] as ConfigSection[]).map(section => (
<div key={section.title} style={{ <div key={section.title} style={{
background: 'var(--color-surface)', background: 'var(--color-surface)',
borderRadius: 'var(--radius-md)', borderRadius: 'var(--radius-md)',
@ -171,6 +252,7 @@ export const SystemManagementPage: React.FC = () => {
))} ))}
</div> </div>
))} ))}
</div>
</div> </div>
) )
)} )}

View File

@ -29,7 +29,7 @@ const Map<String, String> en = {
'nav.profile': 'Profile', 'nav.profile': 'Profile',
// ============ Welcome / Auth ============ // ============ Welcome / Auth ============
'welcome.slogan': 'Make Every Coupon Count', 'welcome.slogan': 'Where Every Coupon Flows Freely',
'welcome.phoneRegister': 'Phone Sign Up', 'welcome.phoneRegister': 'Phone Sign Up',
'welcome.emailRegister': 'Email Sign Up', 'welcome.emailRegister': 'Email Sign Up',
'welcome.wechat': 'WeChat', 'welcome.wechat': 'WeChat',

View File

@ -29,7 +29,7 @@ const Map<String, String> ja = {
'nav.profile': 'マイページ', 'nav.profile': 'マイページ',
// ============ Welcome / Auth ============ // ============ Welcome / Auth ============
'welcome.slogan': 'すべてのクーポンに価値', 'welcome.slogan': 'すべてのクーポン、自由に流通させる',
'welcome.phoneRegister': '電話番号で登録', 'welcome.phoneRegister': '電話番号で登録',
'welcome.emailRegister': 'メールで登録', 'welcome.emailRegister': 'メールで登録',
'welcome.wechat': 'WeChat', 'welcome.wechat': 'WeChat',

View File

@ -29,7 +29,7 @@ const Map<String, String> zhCN = {
'nav.profile': '我的', 'nav.profile': '我的',
// ============ Welcome / Auth ============ // ============ Welcome / Auth ============
'welcome.slogan': '让每一张券都有价值', 'welcome.slogan': '让每一张券,自由流动',
'welcome.phoneRegister': '手机号注册', 'welcome.phoneRegister': '手机号注册',
'welcome.emailRegister': '邮箱注册', 'welcome.emailRegister': '邮箱注册',
'welcome.wechat': '微信', 'welcome.wechat': '微信',

View File

@ -29,7 +29,7 @@ const Map<String, String> zhTW = {
'nav.profile': '我的', 'nav.profile': '我的',
// ============ Welcome / Auth ============ // ============ Welcome / Auth ============
'welcome.slogan': '讓每一張券都有價值', 'welcome.slogan': '讓每一張券,自由流動',
'welcome.phoneRegister': '手機號註冊', 'welcome.phoneRegister': '手機號註冊',
'welcome.emailRegister': '信箱註冊', 'welcome.emailRegister': '信箱註冊',
'welcome.wechat': '微信', 'welcome.wechat': '微信',

View File

@ -284,7 +284,7 @@ const translations: Record<Locale, Record<string, string>> = {
'activity_brand': '品牌', 'activity_brand': '品牌',
'activity_join_now': '立即参与', 'activity_join_now': '立即参与',
'register_join': '加入券信', 'register_join': '加入券信',
'register_slogan': '让每一张券都有价值', 'register_slogan': '让每一张券,自由流动',
'register_benefit': '注册即享首单立减优惠', 'register_benefit': '注册即享首单立减优惠',
'register_benefit_coupons': '海量优惠券', 'register_benefit_coupons': '海量优惠券',
'register_benefit_coupons_desc': '餐饮、购物、娱乐全覆盖', 'register_benefit_coupons_desc': '餐饮、购物、娱乐全覆盖',
@ -744,7 +744,7 @@ const translations: Record<Locale, Record<string, string>> = {
'activity_brand': 'Brand', 'activity_brand': 'Brand',
'activity_join_now': 'Join Now', 'activity_join_now': 'Join Now',
'register_join': 'Join Genex', 'register_join': 'Join Genex',
'register_slogan': 'Make every coupon count', 'register_slogan': 'Where Every Coupon Flows Freely',
'register_benefit': 'Get first-order discount on signup', 'register_benefit': 'Get first-order discount on signup',
'register_benefit_coupons': 'Massive Coupons', 'register_benefit_coupons': 'Massive Coupons',
'register_benefit_coupons_desc': 'Dining, shopping, entertainment & more', 'register_benefit_coupons_desc': 'Dining, shopping, entertainment & more',
@ -1204,7 +1204,7 @@ const translations: Record<Locale, Record<string, string>> = {
'activity_brand': 'ブランド', 'activity_brand': 'ブランド',
'activity_join_now': '今すぐ参加', 'activity_join_now': '今すぐ参加',
'register_join': 'Genex に参加', 'register_join': 'Genex に参加',
'register_slogan': 'すべてのクーポンに価値を', 'register_slogan': 'すべてのクーポン、自由に流通させる',
'register_benefit': '登録で初回割引をゲット', 'register_benefit': '登録で初回割引をゲット',
'register_benefit_coupons': '豊富なクーポン', 'register_benefit_coupons': '豊富なクーポン',
'register_benefit_coupons_desc': 'グルメ、ショッピング、エンタメを網羅', 'register_benefit_coupons_desc': 'グルメ、ショッピング、エンタメを網羅',

View File

@ -10,7 +10,7 @@ const enUS: Record<string, string> = {
nav_register: 'Sign Up', nav_register: 'Sign Up',
// -- Hero -- // -- Hero --
hero_title: 'Every Coupon, Real Value', hero_title: 'Where Every Coupon Flows Freely',
hero_subtitle: 'Blockchain-powered coupon asset trading platform — buy at discount, trade freely, stay secure', hero_subtitle: 'Blockchain-powered coupon asset trading platform — buy at discount, trade freely, stay secure',
hero_cta_download: 'Download App', hero_cta_download: 'Download App',
hero_cta_register: 'Sign Up Free', hero_cta_register: 'Sign Up Free',

View File

@ -10,7 +10,7 @@ const zhCN: Record<string, string> = {
nav_register: '立即注册', nav_register: '立即注册',
// -- Hero -- // -- Hero --
hero_title: '让每一张券,都有价值', hero_title: '让每一张券,自由流动',
hero_subtitle: '区块链驱动的券资产交易平台,折扣购券、自由交易、安全保障', hero_subtitle: '区块链驱动的券资产交易平台,折扣购券、自由交易、安全保障',
hero_cta_download: '下载 App', hero_cta_download: '下载 App',
hero_cta_register: '免费注册', hero_cta_register: '免费注册',