refactor(Phase 2): extract components + AppHeader
New components: - PricingModal: standalone product pricing modal with lazy rule loading - PointsCard: reusable points balance + today consumed + action buttons - AppHeader: unified page header with title, back button, and right actions Profile.tsx: - Use PricingModal + PointsCard (380 lines -> ~200 lines) - Remove embedded pricing table logic (~80 lines) - Remove inline points display logic (~40 lines) Pages updated with AppHeader: - Profile: '我的账户' (no back button) - UsageDetail: '积分明细' + back button - SystemUpdate: '系统更新' + back button - AboutUs: '关于我们' + back button CSS: - Add app-header, app-header-left, app-header-title, app-header-right classes - Remove margin-bottom from page-back-btn (handled by app-header)
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
interface AppHeaderProps {
|
||||
title: string;
|
||||
showBack?: boolean;
|
||||
onBack?: () => void;
|
||||
rightActions?: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function AppHeader({ title, showBack, onBack, rightActions }: AppHeaderProps) {
|
||||
return (
|
||||
<div className="app-header">
|
||||
<div className="app-header-left">
|
||||
{showBack && onBack && (
|
||||
<button className="page-back-btn" onClick={onBack}>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<polyline points="15 18 9 12 15 6" />
|
||||
</svg>
|
||||
返回
|
||||
</button>
|
||||
)}
|
||||
<h2 className="app-header-title">{title}</h2>
|
||||
</div>
|
||||
{rightActions && <div className="app-header-right">{rightActions}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
interface PointsCardProps {
|
||||
balance: number;
|
||||
todayConsumed: number;
|
||||
onRecharge: () => void;
|
||||
onViewDetail: () => void;
|
||||
onViewPricing: () => void;
|
||||
}
|
||||
|
||||
export default function PointsCard({
|
||||
balance,
|
||||
todayConsumed,
|
||||
onRecharge,
|
||||
onViewDetail,
|
||||
onViewPricing,
|
||||
}: PointsCardProps) {
|
||||
return (
|
||||
<div className="profile-points-section">
|
||||
<div className="profile-points-grid">
|
||||
<div className="profile-points-card">
|
||||
<div className="profile-points-label">剩余积分</div>
|
||||
<div className="profile-points-value-row">
|
||||
<span className="profile-points-value primary">{balance}</span>
|
||||
<span className="profile-points-unit">分</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="profile-points-card">
|
||||
<div className="profile-points-label">今日消耗</div>
|
||||
<div className="profile-points-value-row">
|
||||
<span className="profile-points-value danger">{todayConsumed}</span>
|
||||
<span className="profile-points-unit">分</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="profile-points-actions-row">
|
||||
<button className="btn btn-primary btn-sm" onClick={onRecharge}>
|
||||
积分充值
|
||||
</button>
|
||||
<button className="btn btn-ghost btn-sm" onClick={onViewDetail}>
|
||||
充值明细
|
||||
</button>
|
||||
<button className="btn btn-ghost btn-sm" onClick={onViewPricing}>
|
||||
产品定价
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { pointsApi, type PointRule } from '../../api/modules/points';
|
||||
import Modal from '../Modal/Modal';
|
||||
|
||||
const SOURCE_TYPE_LABELS: Record<string, string> = {
|
||||
script: '脚本生成',
|
||||
polish: '文案润色',
|
||||
title: '标题生成',
|
||||
tts: '配音合成',
|
||||
voice_clone: '声音复刻',
|
||||
video: '视频生成',
|
||||
compose: '压制成片',
|
||||
subtitle_burn: '字幕烧录',
|
||||
cover_design: '封面设计',
|
||||
caption: '字幕生成',
|
||||
};
|
||||
|
||||
const ClockIcon = () => (
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<circle cx="12" cy="12" r="10" /><polyline points="12 6 12 12 16 14" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
const InfoIcon = () => (
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<circle cx="12" cy="12" r="10" /><line x1="12" y1="16" x2="12" y2="12" /><line x1="12" y1="8" x2="12.01" y2="8" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
interface PricingModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export default function PricingModal({ open, onClose }: PricingModalProps) {
|
||||
const [rules, setRules] = useState<PointRule[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || rules.length > 0) return;
|
||||
setLoading(true);
|
||||
pointsApi.getRules()
|
||||
.then(setRules)
|
||||
.catch((e) => console.error('[PricingModal] 获取积分规则失败:', e))
|
||||
.finally(() => setLoading(false));
|
||||
}, [open, rules.length]);
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose} width="600px">
|
||||
{loading ? (
|
||||
<div className="profile-pricing-loading">
|
||||
加载中...
|
||||
</div>
|
||||
) : (
|
||||
<div className="profile-pricing-body">
|
||||
{/* 表格 */}
|
||||
<div className="profile-pricing-table">
|
||||
<div className="profile-pricing-header">
|
||||
<span>功能名称</span>
|
||||
<span>计费方式</span>
|
||||
<span>积分消耗</span>
|
||||
</div>
|
||||
{rules
|
||||
.filter((rule) => rule.mode !== 'free')
|
||||
.map((rule) => (
|
||||
<div key={rule.sourceType} className="profile-pricing-row">
|
||||
<span className="profile-pricing-name">
|
||||
{SOURCE_TYPE_LABELS[rule.sourceType] || rule.sourceType}
|
||||
</span>
|
||||
<span className="profile-pricing-mode">
|
||||
<span className={`profile-pricing-tag ${rule.mode}`}>
|
||||
{rule.mode === 'fixed' ? '固定' : '按时长'}
|
||||
</span>
|
||||
</span>
|
||||
<span className="profile-pricing-points">
|
||||
{rule.mode === 'fixed'
|
||||
? `${rule.points} 积分/次`
|
||||
: `${rule.unit} ${rule.pointsPerUnit} 积分`}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
{rules.filter((rule) => rule.mode !== 'free').length === 0 && (
|
||||
<div className="profile-pricing-empty">暂无计费项目</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 按时长细则 */}
|
||||
{rules.some((r) => r.mode === 'duration') && (
|
||||
<div className="profile-pricing-detail-section">
|
||||
<div className="profile-pricing-detail-title">
|
||||
<ClockIcon />
|
||||
按时长计费细则
|
||||
</div>
|
||||
<div className="profile-pricing-detail-list">
|
||||
{rules
|
||||
.filter((r) => r.mode === 'duration')
|
||||
.map((rule) => {
|
||||
const contentType =
|
||||
rule.sourceType === 'tts' ? '音频'
|
||||
: rule.sourceType === 'video' ? '视频'
|
||||
: '内容';
|
||||
const unitNum = (rule.unit?.match(/\d+/) || ['1'])[0];
|
||||
return (
|
||||
<div key={rule.sourceType} className="profile-pricing-detail-row">
|
||||
<strong>{SOURCE_TYPE_LABELS[rule.sourceType] || rule.sourceType}</strong>
|
||||
<span>
|
||||
按生成{contentType}的实际时长计费,{rule.unit}消耗 {rule.pointsPerUnit} 积分,不足{unitNum}秒按{unitNum}秒计。
|
||||
{rule.sourceType === 'video' ? '使用系统素材每个空镜额外消耗 2 积分。' : ''}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 说明 */}
|
||||
<div className="profile-pricing-info-section">
|
||||
<div className="profile-pricing-info-title">
|
||||
<InfoIcon />
|
||||
说明
|
||||
</div>
|
||||
<ul className="profile-pricing-info-list">
|
||||
<li>固定计费:每次使用消耗固定积分,与输入内容长度无关。</li>
|
||||
<li>按时长计费:根据实际生成内容的时长按单位消耗。</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -768,7 +768,6 @@
|
||||
cursor: pointer;
|
||||
border-radius: var(--radius-md);
|
||||
transition: all var(--transition-fast);
|
||||
margin-bottom: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.page-back-btn:hover {
|
||||
@@ -780,6 +779,33 @@
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ── AppHeader ── */
|
||||
.app-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing-md);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.app-header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.app-header-title {
|
||||
font-size: var(--font-xl);
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.app-header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.profile-edit-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -2,9 +2,11 @@ import { useEffect, useState } from 'react';
|
||||
import { useNavigation } from '../../contexts/NavigationContext';
|
||||
import { useAuthStore } from '../../store';
|
||||
import { client } from '../../api/client';
|
||||
import { pointsApi, type PointBalance, type PointRule } from '../../api/modules/points';
|
||||
import { pointsApi, type PointBalance } from '../../api/modules/points';
|
||||
import RechargeModal from '../../components/RechargeModal/RechargeModal';
|
||||
import Modal from '../../components/Modal/Modal';
|
||||
import PricingModal from '../../components/PricingModal/PricingModal';
|
||||
import PointsCard from '../../components/PointsCard/PointsCard';
|
||||
import AppHeader from '../../components/Layout/AppHeader';
|
||||
import '../ContentManagement/ContentManagement.css';
|
||||
|
||||
interface UserProfile {
|
||||
@@ -14,19 +16,6 @@ interface UserProfile {
|
||||
avatar: string;
|
||||
}
|
||||
|
||||
const SOURCE_TYPE_LABELS: Record<string, string> = {
|
||||
script: '脚本生成',
|
||||
polish: '文案润色',
|
||||
title: '标题生成',
|
||||
tts: '配音合成',
|
||||
voice_clone: '声音复刻',
|
||||
video: '视频生成',
|
||||
compose: '压制成片',
|
||||
subtitle_burn: '字幕烧录',
|
||||
cover_design: '封面设计',
|
||||
caption: '字幕生成',
|
||||
};
|
||||
|
||||
function maskMobile(mobile: string): string {
|
||||
if (!mobile || mobile.length !== 11) { return mobile; }
|
||||
return `${mobile.slice(0, 3)}****${mobile.slice(7)}`;
|
||||
@@ -56,12 +45,6 @@ const ChevronRightIcon = ({ className }: { className?: string }) => (
|
||||
</svg>
|
||||
);
|
||||
|
||||
const ClockIcon = () => (
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<circle cx="12" cy="12" r="10" /><polyline points="12 6 12 12 16 14" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
const EditIcon = () => (
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z" />
|
||||
@@ -77,8 +60,6 @@ export default function Profile() {
|
||||
const [todayConsumed, setTodayConsumed] = useState(0);
|
||||
const [showRechargeModal, setShowRechargeModal] = useState(false);
|
||||
const [showPricingModal, setShowPricingModal] = useState(false);
|
||||
const [pricingRules, setPricingRules] = useState<PointRule[]>([]);
|
||||
const [pricingLoading, setPricingLoading] = useState(false);
|
||||
|
||||
// 昵称编辑状态
|
||||
const [nickname, setNickname] = useState('');
|
||||
@@ -113,19 +94,8 @@ export default function Profile() {
|
||||
loadData();
|
||||
};
|
||||
|
||||
const handleOpenPricing = async () => {
|
||||
const handleOpenPricing = () => {
|
||||
setShowPricingModal(true);
|
||||
if (pricingRules.length === 0) {
|
||||
setPricingLoading(true);
|
||||
try {
|
||||
const rules = await pointsApi.getRules();
|
||||
setPricingRules(rules);
|
||||
} catch (e) {
|
||||
console.error('[Profile] 获取积分规则失败:', e);
|
||||
} finally {
|
||||
setPricingLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveNickname = async () => {
|
||||
@@ -165,9 +135,7 @@ export default function Profile() {
|
||||
|
||||
return (
|
||||
<div className="settings-page">
|
||||
<div className="settings-section">
|
||||
<h2>我的账户</h2>
|
||||
</div>
|
||||
<AppHeader title="我的账户" />
|
||||
|
||||
{/* 个人信息 + 积分 */}
|
||||
<div className="card profile-card-flat">
|
||||
@@ -229,42 +197,16 @@ export default function Profile() {
|
||||
<div className="profile-divider" />
|
||||
|
||||
{/* 积分统计 */}
|
||||
<div className="profile-points-section">
|
||||
<div className="profile-points-grid">
|
||||
<div className="profile-points-card">
|
||||
<div className="profile-points-label">剩余积分</div>
|
||||
<div className="profile-points-value-row">
|
||||
<span className="profile-points-value primary">{balance?.balance ?? 0}</span>
|
||||
<span className="profile-points-unit">分</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="profile-points-card">
|
||||
<div className="profile-points-label">今日消耗</div>
|
||||
<div className="profile-points-value-row">
|
||||
<span className="profile-points-value danger">{todayConsumed}</span>
|
||||
<span className="profile-points-unit">分</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="profile-points-actions-row">
|
||||
<button className="btn btn-primary btn-sm" onClick={() => setShowRechargeModal(true)}>
|
||||
积分充值
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-ghost btn-sm"
|
||||
onClick={() => {
|
||||
localStorage.setItem('usage-detail-initial-tab', 'recharge');
|
||||
navigate('usage-detail');
|
||||
}}
|
||||
>
|
||||
充值明细
|
||||
</button>
|
||||
<button className="btn btn-ghost btn-sm" onClick={handleOpenPricing}>
|
||||
产品定价
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<PointsCard
|
||||
balance={balance?.balance ?? 0}
|
||||
todayConsumed={todayConsumed}
|
||||
onRecharge={() => setShowRechargeModal(true)}
|
||||
onViewDetail={() => {
|
||||
localStorage.setItem('usage-detail-initial-tab', 'recharge');
|
||||
navigate('usage-detail');
|
||||
}}
|
||||
onViewPricing={handleOpenPricing}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 设置入口 */}
|
||||
@@ -293,88 +235,7 @@ export default function Profile() {
|
||||
onRechargeSuccess={handleRechargeSuccess}
|
||||
/>
|
||||
|
||||
<Modal open={showPricingModal} onClose={() => setShowPricingModal(false)} width="600px">
|
||||
{pricingLoading ? (
|
||||
<div className="profile-pricing-loading">
|
||||
加载中...
|
||||
</div>
|
||||
) : (
|
||||
<div className="profile-pricing-body">
|
||||
{/* 表格 */}
|
||||
<div className="profile-pricing-table">
|
||||
<div className="profile-pricing-header">
|
||||
<span>功能名称</span>
|
||||
<span>计费方式</span>
|
||||
<span>积分消耗</span>
|
||||
</div>
|
||||
{pricingRules
|
||||
.filter((rule) => rule.mode !== 'free')
|
||||
.map((rule) => (
|
||||
<div key={rule.sourceType} className="profile-pricing-row">
|
||||
<span className="profile-pricing-name">
|
||||
{SOURCE_TYPE_LABELS[rule.sourceType] || rule.sourceType}
|
||||
</span>
|
||||
<span className="profile-pricing-mode">
|
||||
<span className={`profile-pricing-tag ${rule.mode}`}>
|
||||
{rule.mode === 'fixed' ? '固定' : '按时长'}
|
||||
</span>
|
||||
</span>
|
||||
<span className="profile-pricing-points">
|
||||
{rule.mode === 'fixed'
|
||||
? `${rule.points} 积分/次`
|
||||
: `${rule.unit} ${rule.pointsPerUnit} 积分`}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
{pricingRules.filter((rule) => rule.mode !== 'free').length === 0 && (
|
||||
<div className="profile-pricing-empty">暂无计费项目</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 按时长细则 */}
|
||||
{pricingRules.some((r) => r.mode === 'duration') && (
|
||||
<div className="profile-pricing-detail-section">
|
||||
<div className="profile-pricing-detail-title">
|
||||
<ClockIcon />
|
||||
按时长计费细则
|
||||
</div>
|
||||
<div className="profile-pricing-detail-list">
|
||||
{pricingRules
|
||||
.filter((r) => r.mode === 'duration')
|
||||
.map((rule) => {
|
||||
const contentType =
|
||||
rule.sourceType === 'tts' ? '音频'
|
||||
: rule.sourceType === 'video' ? '视频'
|
||||
: '内容';
|
||||
const unitNum = (rule.unit?.match(/\d+/) || ['1'])[0];
|
||||
return (
|
||||
<div key={rule.sourceType} className="profile-pricing-detail-row">
|
||||
<strong>{SOURCE_TYPE_LABELS[rule.sourceType] || rule.sourceType}</strong>
|
||||
<span>
|
||||
按生成{contentType}的实际时长计费,{rule.unit}消耗 {rule.pointsPerUnit} 积分,不足{unitNum}秒按{unitNum}秒计。
|
||||
{rule.sourceType === 'video' ? '使用系统素材每个空镜额外消耗 2 积分。' : ''}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 说明 */}
|
||||
<div className="profile-pricing-info-section">
|
||||
<div className="profile-pricing-info-title">
|
||||
<InfoIcon />
|
||||
说明
|
||||
</div>
|
||||
<ul className="profile-pricing-info-list">
|
||||
<li>固定计费:每次使用消耗固定积分,与输入内容长度无关。</li>
|
||||
<li>按时长计费:根据实际生成内容的时长按单位消耗。</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
<PricingModal open={showPricingModal} onClose={() => setShowPricingModal(false)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useState, useCallback, useRef } from 'react';
|
||||
import { useNavigation } from '../../contexts/NavigationContext';
|
||||
import AppHeader from '../../components/Layout/AppHeader';
|
||||
import { pointsApi, type PointTransaction } from '../../api/modules/points';
|
||||
import DateRangePicker from '../../components/DatePicker/DateRangePicker';
|
||||
import '../ContentManagement/ContentManagement.css';
|
||||
@@ -365,14 +366,8 @@ export default function UsageDetail() {
|
||||
|
||||
return (
|
||||
<div className="settings-page">
|
||||
<button className="page-back-btn" onClick={() => navigate('profile')}>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<polyline points="15 18 9 12 15 6" />
|
||||
</svg>
|
||||
返回
|
||||
</button>
|
||||
<AppHeader title="积分明细" showBack onBack={() => navigate('profile')} />
|
||||
<div className="settings-section">
|
||||
<h2>积分明细</h2>
|
||||
|
||||
{/* Tab 切换 */}
|
||||
<div
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useRef, useCallback } from 'react';
|
||||
import '../ContentManagement/ContentManagement.css';
|
||||
import AppHeader from '../../components/Layout/AppHeader';
|
||||
import EnvironmentSwitchModal from '../../components/Modal/EnvironmentSwitchModal';
|
||||
import { useNavigation } from '../../contexts/NavigationContext';
|
||||
import { saveAppConfig } from '../../api/modules/config';
|
||||
@@ -56,14 +57,8 @@ export default function AboutUs() {
|
||||
|
||||
return (
|
||||
<div className="settings-page">
|
||||
<button className="page-back-btn" onClick={() => navigate('profile')}>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<polyline points="15 18 9 12 15 6" />
|
||||
</svg>
|
||||
返回
|
||||
</button>
|
||||
<AppHeader title="关于我们" showBack onBack={() => navigate('profile')} />
|
||||
<div className="settings-section">
|
||||
<h2>关于我们</h2>
|
||||
<div className="card" style={{ padding: 0, overflow: 'hidden' }}>
|
||||
<div className="settings-row">
|
||||
<span className="settings-row-label">应用名称</span>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useNavigation } from '../../contexts/NavigationContext';
|
||||
import AppHeader from '../../components/Layout/AppHeader';
|
||||
import { check, type Update, type DownloadEvent } from '@tauri-apps/plugin-updater';
|
||||
import { relaunch } from '@tauri-apps/plugin-process';
|
||||
import '../ContentManagement/ContentManagement.css';
|
||||
@@ -109,14 +110,8 @@ export default function SystemUpdate() {
|
||||
|
||||
return (
|
||||
<div className="settings-page">
|
||||
<button className="page-back-btn" onClick={() => navigate('profile')}>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<polyline points="15 18 9 12 15 6" />
|
||||
</svg>
|
||||
返回
|
||||
</button>
|
||||
<AppHeader title="系统更新" showBack onBack={() => navigate('profile')} />
|
||||
<div className="settings-section">
|
||||
<h2>系统更新</h2>
|
||||
<div className="card" style={{ padding: 0, overflow: 'hidden' }}>
|
||||
{/* 当前版本 */}
|
||||
<div className="settings-row">
|
||||
|
||||
Reference in New Issue
Block a user