文案配置化 + 支付开关
小程序 - 页面固定文案全部改读后台配置(购买、登录、资料、首页、分享、介绍页),代码里不再硬编码 - 已拥有课程时在当前页提示,不再跳购买页再提示 - 介绍页正文由代码常量改为读 settings.page.formula.content 服务端 - 课程相关报错文案(不存在 / 已下架 / 课时不存在 / 未购买 / 已过期 / 重复购买 / 未配置支付商品)改读配置,代码保留默认值兜底 - 新增支付总开关 pay.blocked:打开即拦截下单并下发配置里的提示;开关存在数据库里,开发库与生产库各一份(开发库已开,生产库未设) 后台 - 系统配置页按页面分组折叠,默认收起;认不出来的配置归入「其他」 配套数据(不在代码里):对应的 settings 行已写入生产库与开发库
This commit is contained in:
1 parent
c45025fc86
commit
879e74ebda
21 files changed
+325
-103
No files matched your search
@@ -23,6 +23,58 @@ const SWITCHES = [
|
||||
label: '使用开发库时,显示已购买课程',
|
||||
hint: '关(默认):小程序读开发库时,课程不显示「已购」状态,也不返回已购课程列表。开:按实际权益正常显示。生产库不受这个开关影响。',
|
||||
},
|
||||
{
|
||||
key: 'pay.blocked',
|
||||
label: '暂停支付(拦截下单)',
|
||||
hint: '开:所有下单直接返回「支付暂不可用」,不产生订单——临时停售 / 维护时用。关(默认):正常收款。注意:开关存在你当前查看的数据库里,而小程序读哪个库就受哪个库的开关控制,所以开发库和生产库是各设各的(改之前先确认左下角看的是哪个库)。',
|
||||
},
|
||||
];
|
||||
|
||||
/** 服务端返回、小程序原样显示的报错文案。 */
|
||||
const SERVER_TIP_KEYS = new Set([
|
||||
'course.tip.notFoundOrOffline',
|
||||
'course.tip.notFound',
|
||||
'course.tip.offline',
|
||||
'course.tip.lessonNotFound',
|
||||
'course.tip.noEntitlement',
|
||||
'course.tip.expired',
|
||||
'course.tip.alreadyOwned',
|
||||
'course.tip.noProduct',
|
||||
]);
|
||||
|
||||
/**
|
||||
* 配置项分组。数组顺序就是页面上的展示顺序,每组默认折叠。
|
||||
*
|
||||
* 用「前缀 + 例外」来归类:认不出来的会落到最后的「其他」,不会丢。
|
||||
*/
|
||||
const GROUP_RULES: Array<{ title: string; hint?: string; match: (key: string) => boolean }> = [
|
||||
{ title: '首页', match: (k) => k.startsWith('home.') },
|
||||
{ title: '登录页', match: (k) => k.startsWith('login.') },
|
||||
{
|
||||
title: '我的页',
|
||||
match: (k) =>
|
||||
k.startsWith('profile.') && !k.startsWith('profile.form.') && !k.startsWith('profile.confirm.'),
|
||||
},
|
||||
{
|
||||
title: '修改资料页',
|
||||
match: (k) => k.startsWith('profile.form.') || k.startsWith('profile.confirm.'),
|
||||
},
|
||||
{
|
||||
title: '服务端提示',
|
||||
hint: '服务端返回的报错文案,小程序收到后原样显示。改完立即生效,不用发版。',
|
||||
match: (k) => SERVER_TIP_KEYS.has(k),
|
||||
},
|
||||
{ title: '课程与购买页', match: (k) => k.startsWith('course.') },
|
||||
{ title: '账号状态', match: (k) => k.startsWith('account.') },
|
||||
{ title: '应用更新', match: (k) => k.startsWith('app.') },
|
||||
{ title: '分享', match: (k) => k === 'share.title' },
|
||||
{ title: '介绍页', match: (k) => k.startsWith('page.') },
|
||||
{ title: '通用文案', match: (k) => k.startsWith('common.') },
|
||||
{
|
||||
title: '系统',
|
||||
hint: 'system.active_datasource 是小程序当前用的数据源,别在这里改——用左下角「线上小程序数据源」的切换按钮。',
|
||||
match: (k) => k.startsWith('site.') || k.startsWith('system.') || k.startsWith('develop.'),
|
||||
},
|
||||
];
|
||||
|
||||
export default function SettingsPage() {
|
||||
@@ -85,6 +137,17 @@ export default function SettingsPage() {
|
||||
// 开关项已经在上面单独渲染,别再以文本框的形式出现一次
|
||||
const textRows = rows.filter((r) => !SWITCHES.some((s) => s.key === r.key));
|
||||
|
||||
// 按分组归类:只认第一个匹配的规则(否则 course.tip.notFound 会同时出现在两个分组里)。
|
||||
// 认不出来的落到「其他」,保证不会有配置项看不见。
|
||||
const groupOf = (key: string) => GROUP_RULES.find((r) => r.match(key))?.title ?? '其他';
|
||||
const groups = GROUP_RULES.map((rule) => ({
|
||||
title: rule.title,
|
||||
hint: rule.hint,
|
||||
rows: textRows.filter((r) => groupOf(r.key) === rule.title),
|
||||
})).filter((g) => g.rows.length > 0);
|
||||
const rest = textRows.filter((r) => groupOf(r.key) === '其他');
|
||||
if (rest.length > 0) groups.push({ title: '其他', hint: undefined, rows: rest });
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<h1 className="text-lg font-semibold text-gray-900">系统配置</h1>
|
||||
@@ -93,7 +156,8 @@ export default function SettingsPage() {
|
||||
|
||||
<Card title="站点文案与开关">
|
||||
<p className="mb-4 text-xs text-gray-500">
|
||||
小程序首屏显示的标题、我的页面菜单文案、版本号等都在这里。修改后小程序下拉刷新即可生效。
|
||||
小程序里的文案、以及服务端返回给用户的提示都在这里,按页面分组。点标题展开,改完点「保存」。
|
||||
小程序端改完下拉刷新即可生效;服务端提示改完立即生效。
|
||||
</p>
|
||||
|
||||
<div className="mb-5 space-y-4 border-b border-gray-100 pb-5">
|
||||
@@ -132,25 +196,36 @@ export default function SettingsPage() {
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{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>
|
||||
<div className="font-mono text-xs text-gray-400">{row.key}</div>
|
||||
<div className="space-y-2">
|
||||
{groups.map((group) => (
|
||||
<details key={group.title} className="rounded-md border border-gray-200 bg-white">
|
||||
<summary className="cursor-pointer select-none px-3 py-2 text-sm font-medium text-gray-800 hover:bg-gray-50">
|
||||
{group.title}
|
||||
<span className="ml-2 text-xs font-normal text-gray-400">{group.rows.length} 项</span>
|
||||
</summary>
|
||||
<div className="space-y-3 border-t border-gray-100 px-3 py-3">
|
||||
{group.hint && <p className="text-xs text-gray-500">{group.hint}</p>}
|
||||
{group.rows.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>
|
||||
<div className="font-mono text-xs text-gray-400">{row.key}</div>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<Input
|
||||
value={draft[row.key] ?? ''}
|
||||
onChange={(v) => setDraft({ ...draft, [row.key]: v })}
|
||||
/>
|
||||
</div>
|
||||
<Button variant="ghost" onClick={() => save(row)} disabled={savingKey === row.key}>
|
||||
{savingKey === row.key ? '保存中…' : '保存'}
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<Input
|
||||
value={draft[row.key] ?? ''}
|
||||
onChange={(v) => setDraft({ ...draft, [row.key]: v })}
|
||||
/>
|
||||
</div>
|
||||
<Button variant="ghost" onClick={() => save(row)} disabled={savingKey === row.key}>
|
||||
{savingKey === row.key ? '保存中…' : '保存'}
|
||||
</Button>
|
||||
</div>
|
||||
</details>
|
||||
))}
|
||||
{textRows.length === 0 && (
|
||||
{groups.length === 0 && (
|
||||
<div className="py-6 text-center text-sm text-gray-400">暂无配置</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user