增加广播栏

This commit is contained in:
tianmo committed 2026-09-30 16:47:27 +08:00
1 parent 85b0e70907
commit d8786eb28a
21 files changed
+516 -59

No files matched your search

+36 -1
View File
@@ -13,6 +13,11 @@ interface Stats {
revenueFen: number;
payEnabled: boolean;
payEnv: number;
payOfferId: string;
/** 管理端当前查看的库 */
dataEnv: string;
/** 线上小程序当前读的库 */
miniProgramEnv: string;
}
export default function DashboardPage() {
@@ -45,11 +50,41 @@ export default function DashboardPage() {
<Metric label="已退款" value={String(stats.refunded)} />
<Metric label="成交金额(元)" value={yuan(stats.revenueFen)} />
<Metric
label="支付能力"
label="支付能力(当前查看的库)"
value={stats.payEnabled ? (stats.payEnv === 1 ? '已配置 · 沙箱' : '已配置 · 现网') : '未配置'}
/>
<Metric label="支付 OfferID" value={stats.payOfferId || '—'} />
<Metric
label="小程序当前读的库"
value={stats.miniProgramEnv === 'develop' ? '开发库' : '生产库'}
/>
</div>
{/*
支付参数跟着数据源走:切到开发库用的是沙箱那一套,不会误扣真钱。
这里把当前生效的组合摊开,避免「以为在测试、其实在收钱」。
*/}
<Card title="当前支付配置">
<ul className="space-y-1 text-sm text-gray-600">
<li>
正在查看:<span className="font-medium">{stats.dataEnv === 'develop' ? '开发库' : '生产库'}</span>
</li>
<li>
支付环境:
<span className={stats.payEnv === 1 ? 'font-medium text-amber-600' : 'font-medium text-green-600'}>
{stats.payEnv === 1 ? '沙箱(不产生真实扣款)' : '现网(真实收款)'}
</span>
</li>
<li>
OfferID:<span className="font-mono">{stats.payOfferId || '—'}</span>
</li>
<li className="pt-1 text-xs text-gray-500">
小程序读的是「{stats.miniProgramEnv === 'develop' ? '开发库' : '生产库'}」,下单就用那个库对应的这套配置。
开发库默认走沙箱;生产库的环境由服务端 WX_PAY_ENV 决定。
</li>
</ul>
</Card>
{!stats.payEnabled && (
<Card title="支付尚未启用">
<p className="text-sm text-gray-600">
@@ -11,6 +11,20 @@ interface SettingRow {
remark: string | null;
}
/**
* 渲染成开关(而不是文本框)的配置项。
*
* 约定:值 '1' = 开,其余(含库里没有这一行)= 关。所以默认就是关,不用先往库里插数据。
* key 要和服务端 src/common/site-flags.ts 里的一致。
*/
const SWITCHES = [
{
key: 'develop.show_owned_courses',
label: '使用开发库时,显示已购买课程',
hint: '关(默认):小程序读开发库时,课程不显示「已购」状态,也不返回已购课程列表。开:按实际权益正常显示。生产库不受这个开关影响。',
},
];
export default function SettingsPage() {
const [rows, setRows] = useState<SettingRow[]>([]);
const [draft, setDraft] = useState<Record<string, string>>({});
@@ -49,6 +63,28 @@ export default function SettingsPage() {
.finally(() => setSavingKey(''));
};
const isOn = (key: string) => (draft[key] ?? '') === '1';
/** 开关一点即存,不用再点「保存」。 */
const toggle = (key: string, label: string) => {
const next = isOn(key) ? '0' : '1';
setDraft({ ...draft, [key]: next });
setSavingKey(key);
setError('');
setNotice('');
api
.upsertSetting(key, next, label)
.then(() => {
setNotice(`已保存:${label} → ${next === '1' ? '开' : '关'}`);
load();
})
.catch((err: { message: string }) => setError(err.message))
.finally(() => setSavingKey(''));
};
// 开关项已经在上面单独渲染,别再以文本框的形式出现一次
const textRows = rows.filter((r) => !SWITCHES.some((s) => s.key === r.key));
return (
<div className="space-y-4">
<h1 className="text-lg font-semibold text-gray-900">系统配置</h1>
@@ -59,8 +95,45 @@ export default function SettingsPage() {
<p className="mb-4 text-xs text-gray-500">
小程序首屏显示的标题、我的页面菜单文案、版本号等都在这里。修改后小程序下拉刷新即可生效。
</p>
<div className="mb-5 space-y-4 border-b border-gray-100 pb-5">
{SWITCHES.map((s) => {
const on = isOn(s.key);
return (
<div key={s.key} className="flex items-start gap-3">
<button
type="button"
role="switch"
aria-checked={on}
disabled={savingKey === s.key}
onClick={() => toggle(s.key, s.label)}
className={`mt-0.5 h-6 w-11 shrink-0 rounded-full transition-colors ${
on ? 'bg-green-600' : 'bg-gray-300'
} ${savingKey === s.key ? 'opacity-50' : ''}`}
>
<span
className={`block h-5 w-5 rounded-full bg-white shadow transition-transform ${
on ? 'translate-x-5' : 'translate-x-0.5'
}`}
/>
</button>
<div className="min-w-0">
<div className="text-sm text-gray-700">
{s.label}
<span className={`ml-2 text-xs ${on ? 'text-green-600' : 'text-gray-400'}`}>
{savingKey === s.key ? '保存中…' : on ? '已开启' : '已关闭'}
</span>
</div>
<div className="mt-0.5 text-xs text-gray-500">{s.hint}</div>
<div className="mt-0.5 font-mono text-xs text-gray-400">{s.key}</div>
</div>
</div>
);
})}
</div>
<div className="space-y-3">
{rows.map((row) => (
{textRows.map((row) => (
<div key={row.key} className="flex items-center gap-3">
<div className="w-56 shrink-0">
<div className="text-sm text-gray-700">{row.remark || row.key}</div>
@@ -77,10 +150,11 @@ export default function SettingsPage() {
</Button>
</div>
))}
{rows.length === 0 && <div className="py-6 text-center text-sm text-gray-400">暂无配置</div>}
{textRows.length === 0 && (
<div className="py-6 text-center text-sm text-gray-400">暂无配置</div>
)}
</div>
</Card>
</div>
);
}