feat: add taro tenant admin pages

This commit is contained in:
Codex
2026-06-29 12:15:56 +08:00
parent b79c4259c0
commit 9bd608b27e
18 changed files with 928 additions and 34 deletions

View File

@@ -0,0 +1,175 @@
.admin-page {
min-height: 100vh;
padding: 28px;
background: #f8fafc;
color: #172033;
}
.admin-shell {
display: flex;
flex-direction: column;
gap: 26px;
}
.admin-header {
padding-bottom: 24px;
border-bottom: 1px solid #dbe3ef;
}
.admin-kicker {
display: block;
color: #64748b;
font-size: 22px;
font-weight: 700;
}
.admin-title {
display: block;
margin-top: 8px;
color: #111827;
font-size: 38px;
font-weight: 800;
line-height: 1.2;
}
.admin-subtitle {
display: block;
margin-top: 10px;
color: #64748b;
font-size: 24px;
line-height: 1.45;
}
.admin-tabs,
.admin-actions {
display: flex;
gap: 12px;
overflow-x: auto;
padding-bottom: 6px;
}
.admin-button {
min-width: 132px;
height: 62px;
border: 1px solid #cbd5e1;
border-radius: 8px;
background: #fff;
color: #1e3a8a;
font-size: 23px;
font-weight: 700;
line-height: 62px;
}
.admin-button.primary {
border-color: #2563eb;
background: #2563eb;
color: #fff;
}
.admin-button.active {
border-color: #2563eb;
background: #eff6ff;
color: #1d4ed8;
}
.admin-section {
margin-top: 4px;
}
.admin-section-title {
display: block;
margin-bottom: 14px;
color: #0f172a;
font-size: 29px;
font-weight: 800;
}
.admin-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
}
.admin-grid.three {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.admin-metric {
min-height: 112px;
padding: 18px;
border: 1px solid #e2e8f0;
border-radius: 8px;
background: #fff;
}
.admin-metric-label {
display: block;
color: #64748b;
font-size: 21px;
}
.admin-metric-value {
display: block;
margin-top: 12px;
color: #0f172a;
font-size: 31px;
font-weight: 800;
line-height: 1.2;
}
.admin-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.admin-row {
padding: 20px;
border: 1px solid #e2e8f0;
border-radius: 8px;
background: #fff;
}
.admin-row-main {
display: block;
color: #111827;
font-size: 26px;
font-weight: 760;
line-height: 1.35;
}
.admin-row-meta {
display: block;
margin-top: 8px;
color: #64748b;
font-size: 21px;
line-height: 1.45;
}
.admin-empty {
padding: 32px 22px;
border: 1px dashed #cbd5e1;
border-radius: 8px;
background: #fff;
color: #64748b;
font-size: 23px;
line-height: 1.45;
}
.admin-input {
width: 100%;
height: 66px;
padding: 0 18px;
border: 1px solid #cbd5e1;
border-radius: 8px;
background: #fff;
color: #111827;
font-size: 24px;
}
.admin-error {
display: block;
margin-top: 12px;
color: #be123c;
font-size: 22px;
}

View File

@@ -0,0 +1,3 @@
export default definePageConfig({
navigationBarTitleText: '题库内容',
});

View File

@@ -0,0 +1,75 @@
import { useEffect, useState } from 'react';
import { Text, View } from '@tarojs/components';
import { loadContentEntriesAdmin, loadImportJobs, loadPublicQuestionBanks, type ContentEntryAdminItem, type ImportJobItem, type PublicQuestionBankItem } from '@/services/tenantAdmin';
import '../admin.css';
export default function TenantContentPage() {
const [entries, setEntries] = useState<ContentEntryAdminItem[]>([]);
const [jobs, setJobs] = useState<ImportJobItem[]>([]);
const [publicBanks, setPublicBanks] = useState<PublicQuestionBankItem[]>([]);
const [error, setError] = useState('');
useEffect(() => {
Promise.all([
loadContentEntriesAdmin().catch(() => ({ items: [] })),
loadImportJobs(20).catch(() => ({ items: [] })),
loadPublicQuestionBanks().catch(() => ({ items: [] })),
]).then(([entryPayload, jobPayload, bankPayload]) => {
setEntries(entryPayload.items || []);
setJobs(jobPayload.items || []);
setPublicBanks(bankPayload.items || []);
}).catch(nextError => setError(nextError instanceof Error ? nextError.message : '内容数据加载失败'));
}, []);
return (
<View className='admin-page'>
<View className='admin-shell'>
<View className='admin-header'>
<Text className='admin-kicker'>Content</Text>
<Text className='admin-title'></Text>
<Text className='admin-subtitle'></Text>
</View>
<View className='admin-section'>
<Text className='admin-section-title'></Text>
<View className='admin-list'>
{entries.map(item => (
<View className='admin-row' key={item.id}>
<Text className='admin-row-main'>{item.name}</Text>
<Text className='admin-row-meta'>{item.entryType || 'entry'} · {item.visibility || 'visible'} · {item.isActive === false ? '停用' : '启用'}</Text>
</View>
))}
</View>
{!entries.length ? <View className='admin-empty'></View> : null}
</View>
<View className='admin-section'>
<Text className='admin-section-title'></Text>
<View className='admin-list'>
{jobs.map(item => (
<View className='admin-row' key={item.id}>
<Text className='admin-row-main'>{item.importType || 'import'} · {item.status || 'unknown'}</Text>
<Text className='admin-row-meta'> {item.totalCount || 0} · {item.successCount || 0} · {item.errorCount || 0} · {item.warningCount || 0}</Text>
</View>
))}
</View>
{!jobs.length ? <View className='admin-empty'></View> : null}
</View>
<View className='admin-section'>
<Text className='admin-section-title'></Text>
<View className='admin-list'>
{publicBanks.map((item, index) => (
<View className='admin-row' key={item.grantId || item.sourceQuestionBankId || index}>
<Text className='admin-row-main'>{item.name || '公共题库'}</Text>
<Text className='admin-row-meta'>{item.regionName || '通用'} · {item.grantScope || '授权'} · {item.adoption ? '已采纳' : '可采纳'}</Text>
</View>
))}
</View>
{!publicBanks.length ? <View className='admin-empty'></View> : null}
</View>
{error ? <Text className='admin-error'>{error}</Text> : null}
</View>
</View>
);
}

View File

@@ -0,0 +1,3 @@
export default definePageConfig({
navigationBarTitleText: '租户数据看板',
});

View File

@@ -0,0 +1,76 @@
import { useEffect, useState } from 'react';
import { Button, Text, View } from '@tarojs/components';
import { loadTenantDashboard, type TenantDashboard } from '@/services/tenantAdmin';
import '../admin.css';
function money(cents: unknown) {
return `¥${(Number(cents || 0) / 100).toFixed(2)}`;
}
export default function TenantDashboardPage() {
const [range, setRange] = useState('30d');
const [dashboard, setDashboard] = useState<TenantDashboard | null>(null);
const [error, setError] = useState('');
useEffect(() => {
loadTenantDashboard(range)
.then(payload => setDashboard(payload.item || null))
.catch(nextError => setError(nextError instanceof Error ? nextError.message : '看板加载失败'));
}, [range]);
const cards = dashboard?.cards || {};
const payment = dashboard?.paymentStats || {};
return (
<View className='admin-page'>
<View className='admin-shell'>
<View className='admin-header'>
<Text className='admin-kicker'>Dashboard</Text>
<Text className='admin-title'></Text>
<Text className='admin-subtitle'>使</Text>
</View>
<View className='admin-tabs'>
{['7d', '30d', '90d'].map(item => (
<Button key={item} className={`admin-button ${item === range ? 'active' : ''}`} onClick={() => setRange(item)}>{item}</Button>
))}
</View>
<View className='admin-grid'>
<View className='admin-metric'><Text className='admin-metric-label'></Text><Text className='admin-metric-value'>{money(payment.revenueCentsInRange)}</Text></View>
<View className='admin-metric'><Text className='admin-metric-label'></Text><Text className='admin-metric-value'>{String(payment.paidOrdersInRange || 0)}</Text></View>
<View className='admin-metric'><Text className='admin-metric-label'></Text><Text className='admin-metric-value'>{String(cards.students?.newInRange || 0)}</Text></View>
<View className='admin-metric'><Text className='admin-metric-label'></Text><Text className='admin-metric-value'>{String(cards.learning?.activeStudents || 0)}</Text></View>
<View className='admin-metric'><Text className='admin-metric-label'></Text><Text className='admin-metric-value'>{String(cards.learning?.answers || 0)}</Text></View>
<View className='admin-metric'><Text className='admin-metric-label'></Text><Text className='admin-metric-value'>{`${Math.round(Number(cards.learning?.accuracy || 0) * 100)}%`}</Text></View>
</View>
<View className='admin-section'>
<Text className='admin-section-title'></Text>
<View className='admin-list'>
{(dashboard?.planSales || []).slice(0, 8).map(item => (
<View className='admin-row' key={String(item.planId || item.name)}>
<Text className='admin-row-main'>{String(item.name || '未命名套餐')}</Text>
<Text className='admin-row-meta'>{String(item.paidOrders || 0)} · {money(item.revenueCents)}</Text>
</View>
))}
</View>
</View>
<View className='admin-section'>
<Text className='admin-section-title'></Text>
<View className='admin-list'>
{(dashboard?.recentActivities || []).slice(0, 8).map((item, index) => (
<View className='admin-row' key={`${String(item.activityType)}-${index}`}>
<Text className='admin-row-main'>{String(item.title || item.activityType)}</Text>
<Text className='admin-row-meta'>{String(item.occurredAt || '')}</Text>
</View>
))}
</View>
{!dashboard?.recentActivities?.length ? <View className='admin-empty'></View> : null}
</View>
{error ? <Text className='admin-error'>{error}</Text> : null}
</View>
</View>
);
}

View File

@@ -0,0 +1,3 @@
export default definePageConfig({
navigationBarTitleText: '营销中心',
});

View File

@@ -0,0 +1,100 @@
import { useEffect, useState } from 'react';
import { Text, View } from '@tarojs/components';
import {
loadActivationCodes,
loadCodeBatches,
loadCommissionSummary,
loadCoupons,
loadCrmQueue,
type CodeBatchItem,
type CouponItem,
} from '@/services/tenantAdmin';
import '../admin.css';
function money(cents: unknown) {
return `¥${(Number(cents || 0) / 100).toFixed(2)}`;
}
export default function TenantMarketingPage() {
const [coupons, setCoupons] = useState<CouponItem[]>([]);
const [batches, setBatches] = useState<CodeBatchItem[]>([]);
const [codes, setCodes] = useState<Record<string, unknown>[]>([]);
const [crmQueue, setCrmQueue] = useState<Record<string, unknown>[]>([]);
const [commission, setCommission] = useState<Record<string, unknown> | null>(null);
const [error, setError] = useState('');
useEffect(() => {
Promise.all([
loadCoupons().catch(() => ({ items: [] })),
loadCodeBatches().catch(() => ({ items: [] })),
loadActivationCodes(20).catch(() => ({ items: [] })),
loadCrmQueue(20).catch(() => ({ items: [] })),
loadCommissionSummary().catch(() => ({ item: null })),
]).then(([couponPayload, batchPayload, codePayload, crmPayload, commissionPayload]) => {
setCoupons(couponPayload.items || []);
setBatches(batchPayload.items || []);
setCodes(codePayload.items || []);
setCrmQueue(crmPayload.items || []);
setCommission(commissionPayload.item || null);
}).catch(nextError => setError(nextError instanceof Error ? nextError.message : '营销数据加载失败'));
}, []);
return (
<View className='admin-page'>
<View className='admin-shell'>
<View className='admin-header'>
<Text className='admin-kicker'>Marketing</Text>
<Text className='admin-title'></Text>
<Text className='admin-subtitle'>CRM /</Text>
</View>
<View className='admin-grid'>
<View className='admin-metric'><Text className='admin-metric-label'></Text><Text className='admin-metric-value'>{String(coupons.length)}</Text></View>
<View className='admin-metric'><Text className='admin-metric-label'></Text><Text className='admin-metric-value'>{String(batches.length)}</Text></View>
<View className='admin-metric'><Text className='admin-metric-label'>CRM </Text><Text className='admin-metric-value'>{String(crmQueue.length)}</Text></View>
<View className='admin-metric'><Text className='admin-metric-label'></Text><Text className='admin-metric-value'>{money(commission?.commissionAmountCents)}</Text></View>
</View>
<View className='admin-section'>
<Text className='admin-section-title'></Text>
<View className='admin-list'>
{coupons.map(item => (
<View className='admin-row' key={item.id}>
<Text className='admin-row-main'>{item.code}</Text>
<Text className='admin-row-meta'>{item.status || 'active'} · {item.discountType || 'discount'} {String(item.discountValue ?? '')} · {item.usedCount || 0}/{item.maxUses || '不限'}</Text>
</View>
))}
</View>
{!coupons.length ? <View className='admin-empty'></View> : null}
</View>
<View className='admin-section'>
<Text className='admin-section-title'></Text>
<View className='admin-list'>
{batches.map(item => (
<View className='admin-row' key={item.id}>
<Text className='admin-row-main'>{item.name || item.batchNo || item.id}</Text>
<Text className='admin-row-meta'>{item.status || 'active'} · {item.usedCount || 0}/{item.totalCount || 0} </Text>
</View>
))}
</View>
{!batches.length ? <View className='admin-empty'></View> : null}
</View>
<View className='admin-section'>
<Text className='admin-section-title'></Text>
<View className='admin-list'>
{codes.slice(0, 8).map((item, index) => (
<View className='admin-row' key={String(item.id || index)}>
<Text className='admin-row-main'>{String(item.code || '激活码')}</Text>
<Text className='admin-row-meta'>{String(item.isUsed ? '已使用' : '未使用')} · {String(item.days || 0)} </Text>
</View>
))}
</View>
</View>
{error ? <Text className='admin-error'>{error}</Text> : null}
</View>
</View>
);
}

View File

@@ -0,0 +1,3 @@
export default definePageConfig({
navigationBarTitleText: '租户设置',
});

View File

@@ -0,0 +1,109 @@
import { useEffect, useState } from 'react';
import { Text, View } from '@tarojs/components';
import {
loadAuthProviders,
loadPaymentAccounts,
loadRoleTemplates,
loadTenantDomains,
loadTenantOverview,
type TenantOverview,
} from '@/services/tenantAdmin';
import '../admin.css';
export default function TenantSettingsPage() {
const [overview, setOverview] = useState<TenantOverview | null>(null);
const [domains, setDomains] = useState<Record<string, unknown>[]>([]);
const [payments, setPayments] = useState<Record<string, unknown>[]>([]);
const [authProviders, setAuthProviders] = useState<Record<string, unknown>[]>([]);
const [roles, setRoles] = useState<Record<string, unknown>[]>([]);
const [error, setError] = useState('');
useEffect(() => {
Promise.all([
loadTenantOverview().catch(() => ({ item: null })),
loadTenantDomains().catch(() => ({ items: [] })),
loadPaymentAccounts().catch(() => ({ items: [] })),
loadAuthProviders().catch(() => ({ items: [] })),
loadRoleTemplates().catch(() => ({ items: [] })),
]).then(([overviewPayload, domainPayload, paymentPayload, authPayload, rolePayload]) => {
setOverview(overviewPayload.item || null);
setDomains(domainPayload.items || []);
setPayments(paymentPayload.items || []);
setAuthProviders(authPayload.items || []);
setRoles(rolePayload.items || []);
}).catch(nextError => setError(nextError instanceof Error ? nextError.message : '设置加载失败'));
}, []);
return (
<View className='admin-page'>
<View className='admin-shell'>
<View className='admin-header'>
<Text className='admin-kicker'>Settings</Text>
<Text className='admin-title'></Text>
<Text className='admin-subtitle'> provider </Text>
</View>
<View className='admin-section'>
<Text className='admin-section-title'></Text>
<View className='admin-row'>
<Text className='admin-row-main'>{overview?.brandName || overview?.name || '未配置品牌名'}</Text>
<Text className='admin-row-meta'>{overview?.slogan || overview?.shortName || '暂无标语'}</Text>
<Text className='admin-row-meta'> {overview?.status || '-'} · slug {overview?.slug || '-'}</Text>
</View>
</View>
<View className='admin-section'>
<Text className='admin-section-title'></Text>
<View className='admin-list'>
{domains.map((item, index) => (
<View className='admin-row' key={String(item.id || index)}>
<Text className='admin-row-main'>{String(item.host || '未命名域名')}</Text>
<Text className='admin-row-meta'>{String(item.domainType || 'custom')} · {String(item.status || 'pending')} · {item.isPrimary ? '主域名' : '备用'}</Text>
</View>
))}
</View>
{!domains.length ? <View className='admin-empty'></View> : null}
</View>
<View className='admin-section'>
<Text className='admin-section-title'></Text>
<View className='admin-grid'>
{payments.map((item, index) => (
<View className='admin-metric' key={String(item.id || index)}>
<Text className='admin-metric-label'>{String(item.provider || 'provider')}</Text>
<Text className='admin-metric-value'>{String(item.status || 'disabled')}</Text>
<Text className='admin-row-meta'>{String(item.mode || 'platform_collect')}</Text>
</View>
))}
</View>
</View>
<View className='admin-section'>
<Text className='admin-section-title'></Text>
<View className='admin-list'>
{authProviders.map((item, index) => (
<View className='admin-row' key={String(item.id || index)}>
<Text className='admin-row-main'>{String(item.provider || 'provider')}</Text>
<Text className='admin-row-meta'>{String(item.status || 'disabled')}</Text>
</View>
))}
</View>
</View>
<View className='admin-section'>
<Text className='admin-section-title'></Text>
<View className='admin-list'>
{roles.map((item, index) => (
<View className='admin-row' key={String(item.id || index)}>
<Text className='admin-row-main'>{String(item.name || item.roleKey || '角色模板')}</Text>
<Text className='admin-row-meta'>{String(item.status || 'active')} · </Text>
</View>
))}
</View>
{!roles.length ? <View className='admin-empty'></View> : null}
</View>
{error ? <Text className='admin-error'>{error}</Text> : null}
</View>
</View>
);
}

View File

@@ -0,0 +1,3 @@
export default definePageConfig({
navigationBarTitleText: '学生运营',
});

View File

@@ -0,0 +1,84 @@
import { useEffect, useState } from 'react';
import { Button, Input, Text, View } from '@tarojs/components';
import { loadTenantClasses, loadTenantStudents, type TenantClassItem, type TenantStudentItem } from '@/services/tenantAdmin';
import '../admin.css';
export default function TenantStudentsPage() {
const [keyword, setKeyword] = useState('');
const [classes, setClasses] = useState<TenantClassItem[]>([]);
const [students, setStudents] = useState<TenantStudentItem[]>([]);
const [selectedClassId, setSelectedClassId] = useState('');
const [error, setError] = useState('');
function reload(nextClassId = selectedClassId, nextKeyword = keyword) {
Promise.all([
loadTenantClasses(80).catch(() => ({ items: [] })),
loadTenantStudents({ classId: nextClassId || undefined, keyword: nextKeyword || undefined, limit: 80 }),
]).then(([classPayload, studentPayload]) => {
setClasses(classPayload.items || []);
setStudents(studentPayload.items || []);
}).catch(nextError => setError(nextError instanceof Error ? nextError.message : '学生数据加载失败'));
}
useEffect(() => {
reload('', '');
}, []);
function chooseClass(classId: string) {
setSelectedClassId(classId);
reload(classId, keyword);
}
return (
<View className='admin-page'>
<View className='admin-shell'>
<View className='admin-header'>
<Text className='admin-kicker'>Students</Text>
<Text className='admin-title'></Text>
<Text className='admin-subtitle'></Text>
</View>
<View className='admin-actions'>
<Input className='admin-input' placeholder='手机号、姓名、邮箱' value={keyword} onInput={event => setKeyword(String(event.detail.value || ''))} />
<Button className='admin-button primary' onClick={() => reload(selectedClassId, keyword)}></Button>
</View>
<View className='admin-section'>
<Text className='admin-section-title'></Text>
<View className='admin-tabs'>
<Button className={`admin-button ${selectedClassId ? '' : 'active'}`} onClick={() => chooseClass('')}></Button>
{classes.map(item => (
<Button key={item.id} className={`admin-button ${selectedClassId === item.id ? 'active' : ''}`} onClick={() => chooseClass(item.id)}>
{item.name}
</Button>
))}
</View>
<View className='admin-grid'>
{classes.slice(0, 6).map(item => (
<View className='admin-metric' key={item.id}>
<Text className='admin-metric-label'>{item.code || item.status || '班级'}</Text>
<Text className='admin-metric-value'>{item.name}</Text>
<Text className='admin-row-meta'>{item.studentCount || 0} · {item.teacherCount || 0} </Text>
</View>
))}
</View>
</View>
<View className='admin-section'>
<Text className='admin-section-title'></Text>
<View className='admin-list'>
{students.map(item => (
<View className='admin-row' key={item.userId}>
<Text className='admin-row-main'>{item.name || item.phone || item.email || item.userId}</Text>
<Text className='admin-row-meta'>{item.regionName || '未选地区'}{item.selectedSchoolName ? ` · ${item.selectedSchoolName}` : ''}{item.selectedMajorName ? ` · ${item.selectedMajorName}` : ''}</Text>
<Text className='admin-row-meta'> {item.questionsAnsweredToday || 0} · {item.masteredWordsCount || 0} · {(item.classes || []).length}</Text>
</View>
))}
</View>
{!students.length ? <View className='admin-empty'></View> : null}
</View>
{error ? <Text className='admin-error'>{error}</Text> : null}
</View>
</View>
);
}

View File

@@ -1,45 +1,75 @@
import { useEffect, useState } from 'react';
import { Text, View } from '@tarojs/components';
import { apiRequest, getTenantContext } from '@/services/api';
import './index.css';
interface Dashboard {
cards?: Record<string, unknown>;
trends?: unknown[];
recentActivities?: unknown[];
}
import Taro from '@tarojs/taro';
import { Button, Text, View } from '@tarojs/components';
import { getTenantContext } from '@/services/api';
import { loadTenantDashboard, loadTenantOverview, type TenantDashboard, type TenantOverview } from '@/services/tenantAdmin';
import '../admin.css';
export default function TenantWorkbenchPage() {
const tenant = getTenantContext();
const [dashboard, setDashboard] = useState<Dashboard | null>(null);
const [dashboard, setDashboard] = useState<TenantDashboard | null>(null);
const [overview, setOverview] = useState<TenantOverview | null>(null);
useEffect(() => {
apiRequest<{ item?: Dashboard }>('/api/tenant-admin/dashboard', { query: { timeRange: '30d' } })
.then(payload => setDashboard(payload.item || null))
.catch(() => setDashboard(null));
loadTenantDashboard('30d').then(payload => setDashboard(payload.item || null)).catch(() => setDashboard(null));
loadTenantOverview().then(payload => setOverview(payload.item || null)).catch(() => setOverview(null));
}, []);
const cards = dashboard?.cards || {};
const payment = dashboard?.paymentStats || {};
const modules = [
{ name: '数据看板', path: '/pages/tenant-admin/dashboard/index', meta: '收益、注册、学习、内容' },
{ name: '学生运营', path: '/pages/tenant-admin/students/index', meta: '学生、班级、教师范围' },
{ name: '题库内容', path: '/pages/tenant-admin/content/index', meta: '入口、导入、公共题库' },
{ name: '营销中心', path: '/pages/tenant-admin/marketing/index', meta: '优惠券、激活码、分佣' },
{ name: '租户设置', path: '/pages/tenant-admin/settings/index', meta: '品牌、域名、支付、登录' },
];
return (
<View className='admin-page'>
<View className='admin-header'>
<Text className='admin-title'>{tenant?.branding.brandName || '租户运营后台'}</Text>
<Text className='admin-subtitle'></Text>
</View>
<View className='metric-row'>
{['收益', '注册', '答题', '激活码'].map(label => (
<View className='metric' key={label}>
<Text className='metric-label'>{label}</Text>
<Text className='metric-value'>{dashboard ? '已接入' : '待登录'}</Text>
<View className='admin-shell'>
<View className='admin-header'>
<Text className='admin-kicker'>{overview?.slug || 'Tenant Admin'}</Text>
<Text className='admin-title'>{overview?.brandName || tenant?.branding.brandName || '租户运营后台'}</Text>
<Text className='admin-subtitle'></Text>
</View>
<View className='admin-grid'>
<View className='admin-metric'>
<Text className='admin-metric-label'>30 </Text>
<Text className='admin-metric-value'>¥{((Number(payment.revenueCentsInRange || 0)) / 100).toFixed(2)}</Text>
</View>
))}
</View>
<View className='module-list'>
{['内容管理', '导入复检', '学生运营', '销售分佣', 'CRM 队列', '品牌域名'].map(name => (
<View className='module-row' key={name}>
<Text>{name}</Text>
<Text className='module-status'></Text>
<View className='admin-metric'>
<Text className='admin-metric-label'></Text>
<Text className='admin-metric-value'>{String(cards.students?.total || 0)}</Text>
</View>
))}
<View className='admin-metric'>
<Text className='admin-metric-label'></Text>
<Text className='admin-metric-value'>{String(cards.learning?.answers || 0)}</Text>
</View>
<View className='admin-metric'>
<Text className='admin-metric-label'></Text>
<Text className='admin-metric-value'>{String(cards.content?.questions || 0)}</Text>
</View>
</View>
<View className='admin-section'>
<Text className='admin-section-title'></Text>
<View className='admin-list'>
{modules.map(item => (
<View className='admin-row' key={item.path} onClick={() => Taro.navigateTo({ url: item.path })}>
<Text className='admin-row-main'>{item.name}</Text>
<Text className='admin-row-meta'>{item.meta}</Text>
</View>
))}
</View>
</View>
<View className='admin-actions'>
<Button className='admin-button primary' onClick={() => Taro.navigateTo({ url: '/pages/tenant-admin/content/index' })}></Button>
<Button className='admin-button' onClick={() => Taro.navigateTo({ url: '/pages/tenant-admin/marketing/index' })}></Button>
<Button className='admin-button' onClick={() => Taro.navigateTo({ url: '/pages/tenant-admin/settings/index' })}></Button>
</View>
</View>
</View>
);