优化首页逻辑以支持动态标题生成,提升用户体验。更新管理后台资源文件,替换旧的 JavaScript 和 CSS 文件,增强页面性能和样式一致性。同时,调整数据库结构以支持更细粒度的推送状态。
This commit is contained in:
@@ -10,8 +10,8 @@ const DEFAULT_APP_ID = 'wxb8bbb2b10dec74aa'
|
|||||||
const DEFAULT_MCH_ID = '1318592501'
|
const DEFAULT_MCH_ID = '1318592501'
|
||||||
const DEFAULT_WITHDRAW_TMPL_ID = 'u3MbZGPRkrZIk-I7QdpwzFxnO_CeQPaCWF2FkiIablE'
|
const DEFAULT_WITHDRAW_TMPL_ID = 'u3MbZGPRkrZIk-I7QdpwzFxnO_CeQPaCWF2FkiIablE'
|
||||||
// baseUrl 手动切换(注释方式):
|
// baseUrl 手动切换(注释方式):
|
||||||
// const API_BASE_URL = 'http://localhost:8080'
|
const API_BASE_URL = 'http://localhost:8080'
|
||||||
const API_BASE_URL = 'https://soulapi.quwanzhi.com'
|
// const API_BASE_URL = 'https://soulapi.quwanzhi.com'
|
||||||
const CONFIG_CACHE_KEY = 'mpConfigCacheV1'
|
const CONFIG_CACHE_KEY = 'mpConfigCacheV1'
|
||||||
// 与上传版本号对齐;设置页展示优先用 wx.getAccountInfoSync().miniProgram.version(正式版),否则用本字段
|
// 与上传版本号对齐;设置页展示优先用 wx.getAccountInfoSync().miniProgram.version(正式版),否则用本字段
|
||||||
const APP_DISPLAY_VERSION = '1.7.2'
|
const APP_DISPLAY_VERSION = '1.7.2'
|
||||||
|
|||||||
@@ -348,7 +348,7 @@ Page({
|
|||||||
.replace(/\{\{prefix\}\}/g, prefix)
|
.replace(/\{\{prefix\}\}/g, prefix)
|
||||||
.trim() || baseTitle
|
.trim() || baseTitle
|
||||||
} else if (prefix) {
|
} else if (prefix) {
|
||||||
mainTitle = `${prefix} · ${nm}`
|
mainTitle = baseTitle
|
||||||
} else {
|
} else {
|
||||||
mainTitle = `@${nm}`
|
mainTitle = `@${nm}`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ const soulBridge = require('../../utils/soulBridge.js')
|
|||||||
const { trackClick } = require('../../utils/trackClick')
|
const { trackClick } = require('../../utils/trackClick')
|
||||||
const { isSafeImageSrc } = require('../../utils/imageUrl.js')
|
const { isSafeImageSrc } = require('../../utils/imageUrl.js')
|
||||||
const { resolveAvatarWithMbti } = require('../../utils/mbtiAvatar.js')
|
const { resolveAvatarWithMbti } = require('../../utils/mbtiAvatar.js')
|
||||||
|
const mpPagePopups = require('../../utils/mpPagePopups.js')
|
||||||
|
|
||||||
Page({
|
Page({
|
||||||
data: { statusBarHeight: 44, navBarTotalPx: 88, member: null, loading: true, isOwnProfile: false },
|
data: { statusBarHeight: 44, navBarTotalPx: 88, member: null, loading: true, isOwnProfile: false },
|
||||||
@@ -31,15 +32,15 @@ Page({
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 未登录解锁前:先展示后台可配的「链接 vs 解锁」说明,用户确认后再弹出登录引导(mpUi.memberDetailPage)
|
* 未登录解锁前:先展示后台可配的「链接 vs 解锁」说明(mpUi.pagePopupItems:unlockIntroTitle / unlockIntroBody),兼容旧 memberDetailPage
|
||||||
*/
|
*/
|
||||||
_showUnlockIntroThenLogin(afterConfirm) {
|
_showUnlockIntroThenLogin(afterConfirm) {
|
||||||
const mp = app.globalData.configCache?.mpConfig?.mpUi?.memberDetailPage || {}
|
const title =
|
||||||
const title = String(mp.unlockIntroTitle || '解锁与链接说明').trim() || '解锁与链接说明'
|
mpPagePopups.getMemberDetailContent(app, 'unlockIntroTitle') ||
|
||||||
const body = String(
|
'解锁与链接说明'
|
||||||
mp.unlockIntroBody ||
|
const body =
|
||||||
'「链接」用于提交留资,由对方通过获客计划跟进;「解锁」用于复制手机/微信号后自行添加好友。\n\n请确认已了解后再登录。',
|
mpPagePopups.getMemberDetailContent(app, 'unlockIntroBody') ||
|
||||||
).trim()
|
'「链接」用于提交留资,由对方通过获客计划跟进;「解锁」用于复制手机/微信号后自行添加好友。\n\n请确认已了解后再登录。'
|
||||||
wx.showModal({
|
wx.showModal({
|
||||||
title,
|
title,
|
||||||
content: body,
|
content: body,
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ const { checkAndExecute } = require('../../utils/ruleEngine')
|
|||||||
const soulBridge = require('../../utils/soulBridge.js')
|
const soulBridge = require('../../utils/soulBridge.js')
|
||||||
|
|
||||||
const app = getApp()
|
const app = getApp()
|
||||||
|
const mpPagePopups = require('../../utils/mpPagePopups.js')
|
||||||
|
|
||||||
/** 阅读页解析正文用:人物字典 + #标签(与 /config/read-extras 一致) */
|
/** 阅读页解析正文用:人物字典 + #标签(与 /config/read-extras 一致) */
|
||||||
function getContentParseConfig() {
|
function getContentParseConfig() {
|
||||||
@@ -299,10 +300,17 @@ Page({
|
|||||||
const mp = (cfg && cfg.mpConfig) || {}
|
const mp = (cfg && cfg.mpConfig) || {}
|
||||||
const auditMode = !!mp.auditMode
|
const auditMode = !!mp.auditMode
|
||||||
app.globalData.auditMode = auditMode
|
app.globalData.auditMode = auditMode
|
||||||
const rp = (mp.mpUi && mp.mpUi.readPage) || {}
|
const readBeforeLoginHint =
|
||||||
const readBeforeLoginHint = String(rp.beforeLoginHint || '').trim()
|
mpPagePopups.getReadPageContent(app, 'beforeLoginHint') ||
|
||||||
const readSinglePageTitle = String(rp.singlePageTitle || '解锁全文').trim() || '解锁全文'
|
String((mp.mpUi && mp.mpUi.readPage && mp.mpUi.readPage.beforeLoginHint) || '').trim()
|
||||||
const readSinglePageHint = String(rp.singlePagePaywallHint || '').trim()
|
let readSinglePageTitle =
|
||||||
|
mpPagePopups.getReadPageContent(app, 'singlePageTitle') ||
|
||||||
|
String((mp.mpUi && mp.mpUi.readPage && mp.mpUi.readPage.singlePageTitle) || '').trim() ||
|
||||||
|
'解锁全文'
|
||||||
|
readSinglePageTitle = readSinglePageTitle || '解锁全文'
|
||||||
|
const readSinglePageHint =
|
||||||
|
mpPagePopups.getReadPageContent(app, 'singlePagePaywallHint') ||
|
||||||
|
String((mp.mpUi && mp.mpUi.readPage && mp.mpUi.readPage.singlePagePaywallHint) || '').trim()
|
||||||
if (typeof this.setData === 'function') {
|
if (typeof this.setData === 'function') {
|
||||||
this.setData({
|
this.setData({
|
||||||
auditMode,
|
auditMode,
|
||||||
|
|||||||
@@ -23,34 +23,6 @@
|
|||||||
"condition": {
|
"condition": {
|
||||||
"miniprogram": {
|
"miniprogram": {
|
||||||
"list": [
|
"list": [
|
||||||
{
|
|
||||||
"name": "88888888",
|
|
||||||
"pathName": "pages/read/read",
|
|
||||||
"query": "mid=219",
|
|
||||||
"scene": null,
|
|
||||||
"launchMode": "default"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "开发登录",
|
|
||||||
"pathName": "pages/dev-login/dev-login",
|
|
||||||
"query": "",
|
|
||||||
"launchMode": "default",
|
|
||||||
"scene": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "pages/member-detail/member-detail",
|
|
||||||
"pathName": "pages/member-detail/member-detail",
|
|
||||||
"query": "id=ogpTW5cVMxd5afBBtXdvmeMO8aho",
|
|
||||||
"launchMode": "default",
|
|
||||||
"scene": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "pages/my/my",
|
|
||||||
"pathName": "pages/my/my",
|
|
||||||
"query": "",
|
|
||||||
"launchMode": "default",
|
|
||||||
"scene": null
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "个人资料",
|
"name": "个人资料",
|
||||||
"pathName": "pages/avatar-nickname/avatar-nickname",
|
"pathName": "pages/avatar-nickname/avatar-nickname",
|
||||||
@@ -58,13 +30,6 @@
|
|||||||
"launchMode": "default",
|
"launchMode": "default",
|
||||||
"scene": null
|
"scene": null
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "pages/gift-pay/list",
|
|
||||||
"pathName": "pages/gift-pay/list",
|
|
||||||
"query": "",
|
|
||||||
"launchMode": "default",
|
|
||||||
"scene": null
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "代付",
|
"name": "代付",
|
||||||
"pathName": "pages/gift-pay/detail",
|
"pathName": "pages/gift-pay/detail",
|
||||||
@@ -72,26 +37,12 @@
|
|||||||
"launchMode": "default",
|
"launchMode": "default",
|
||||||
"scene": null
|
"scene": null
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "唤醒",
|
|
||||||
"pathName": "pages/read/read",
|
|
||||||
"query": "mid=209",
|
|
||||||
"launchMode": "default",
|
|
||||||
"scene": null
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "pages/my/my",
|
"name": "pages/my/my",
|
||||||
"pathName": "pages/my/my",
|
"pathName": "pages/my/my",
|
||||||
"query": "",
|
"query": "",
|
||||||
"launchMode": "singlePage",
|
"launchMode": "singlePage",
|
||||||
"scene": null
|
"scene": null
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "pages/read/read",
|
|
||||||
"pathName": "pages/read/read",
|
|
||||||
"query": "mid=20",
|
|
||||||
"launchMode": "default",
|
|
||||||
"scene": null
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
68
miniprogram/utils/mpPagePopups.js
Normal file
68
miniprogram/utils/mpPagePopups.js
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
/**
|
||||||
|
* 从 mpConfig.mpUi.pagePopupItems 按 pagePath + key 取文案(管理端 CRUD)。
|
||||||
|
* 兼容旧版 mpUi.memberDetailPage / readPage 字段。
|
||||||
|
*
|
||||||
|
* 当前代码显式引用的键(与 soul-admin 默认种子 / db.defaultMpUi 一致,勿改 key 除非双端同步):
|
||||||
|
* - MEMBER_PATH unlockIntroTitle, unlockIntroBody → member-detail.js _showUnlockIntroThenLogin
|
||||||
|
* - READ_PATH beforeLoginHint, singlePageTitle, singlePagePaywallHint → read.js onLoad
|
||||||
|
*/
|
||||||
|
|
||||||
|
const MEMBER_PATH = '/pages/member-detail/member-detail'
|
||||||
|
const READ_PATH = '/pages/read/read'
|
||||||
|
|
||||||
|
function getList(app) {
|
||||||
|
const mpUi = app.globalData.configCache && app.globalData.configCache.mpConfig
|
||||||
|
? app.globalData.configCache.mpConfig.mpUi
|
||||||
|
: null
|
||||||
|
const list = mpUi && mpUi.pagePopupItems
|
||||||
|
return Array.isArray(list) ? list : []
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {object} app getApp()
|
||||||
|
* @param {string} pagePath 如 /pages/read/read
|
||||||
|
* @param {string} key 英文键
|
||||||
|
* @returns {string} 文案,未配置时返回空串
|
||||||
|
*/
|
||||||
|
function getPagePopupContent(app, pagePath, key) {
|
||||||
|
const list = getList(app)
|
||||||
|
const it = list.find(function (p) {
|
||||||
|
return p && p.pagePath === pagePath && p.key === key
|
||||||
|
})
|
||||||
|
if (it && typeof it.content === 'string') return it.content.trim()
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 带旧版 readPage / memberDetailPage 兜底
|
||||||
|
*/
|
||||||
|
function getReadPageContent(app, key) {
|
||||||
|
const v = getPagePopupContent(app, READ_PATH, key)
|
||||||
|
if (v) return v
|
||||||
|
const rp = (app.globalData.configCache && app.globalData.configCache.mpConfig &&
|
||||||
|
app.globalData.configCache.mpConfig.mpUi &&
|
||||||
|
app.globalData.configCache.mpConfig.mpUi.readPage) || {}
|
||||||
|
if (key === 'beforeLoginHint') return String(rp.beforeLoginHint || '').trim()
|
||||||
|
if (key === 'singlePageTitle') return String(rp.singlePageTitle || '').trim()
|
||||||
|
if (key === 'singlePagePaywallHint') return String(rp.singlePagePaywallHint || '').trim()
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMemberDetailContent(app, key) {
|
||||||
|
const v = getPagePopupContent(app, MEMBER_PATH, key)
|
||||||
|
if (v) return v
|
||||||
|
const md = (app.globalData.configCache && app.globalData.configCache.mpConfig &&
|
||||||
|
app.globalData.configCache.mpConfig.mpUi &&
|
||||||
|
app.globalData.configCache.mpConfig.mpUi.memberDetailPage) || {}
|
||||||
|
if (key === 'unlockIntroTitle') return String(md.unlockIntroTitle || '').trim()
|
||||||
|
if (key === 'unlockIntroBody') return String(md.unlockIntroBody || '').trim()
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getPagePopupContent,
|
||||||
|
getReadPageContent,
|
||||||
|
getMemberDetailContent,
|
||||||
|
MEMBER_PATH,
|
||||||
|
READ_PATH,
|
||||||
|
}
|
||||||
1
soul-admin/dist/assets/index-BHhAT-JW.css
vendored
1
soul-admin/dist/assets/index-BHhAT-JW.css
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
soul-admin/dist/assets/index-BfljfNs2.css
vendored
Normal file
1
soul-admin/dist/assets/index-BfljfNs2.css
vendored
Normal file
File diff suppressed because one or more lines are too long
4
soul-admin/dist/index.html
vendored
4
soul-admin/dist/index.html
vendored
@@ -4,8 +4,8 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>管理后台 - Soul创业派对</title>
|
<title>管理后台 - Soul创业派对</title>
|
||||||
<script type="module" crossorigin src="/assets/index-i0PBc3Gp.js"></script>
|
<script type="module" crossorigin src="/assets/index-BRyXRtx1.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-BHhAT-JW.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-BfljfNs2.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
486
soul-admin/src/pages/settings/MpUiPopupTableSection.tsx
Normal file
486
soul-admin/src/pages/settings/MpUiPopupTableSection.tsx
Normal file
@@ -0,0 +1,486 @@
|
|||||||
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from '@/components/ui/card'
|
||||||
|
import { Label } from '@/components/ui/label'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select'
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog'
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from '@/components/ui/table'
|
||||||
|
import { Pagination } from '@/components/ui/Pagination'
|
||||||
|
import { MessageSquare, RotateCcw, Search, Pencil, Trash2, PlusCircle } from 'lucide-react'
|
||||||
|
import toast from '@/utils/toast'
|
||||||
|
import type { PagePopupItem } from './mpUiCopyConfig'
|
||||||
|
import {
|
||||||
|
DEFAULT_POPUP_PAGE_PATH,
|
||||||
|
POPUP_PAGE_PATH_OPTIONS,
|
||||||
|
displayPageName,
|
||||||
|
isValidPopupKey,
|
||||||
|
newPagePopupId,
|
||||||
|
scopeLabel,
|
||||||
|
seedPagePopupItemsWithIds,
|
||||||
|
summarizePopupRow,
|
||||||
|
} from './mpUiCopyConfig'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
pagePopupItems: PagePopupItem[]
|
||||||
|
setPagePopupItems: React.Dispatch<React.SetStateAction<PagePopupItem[]>>
|
||||||
|
extraKeysCount: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 列表分页(全量仍保存在 mpConfig.mpUi.pagePopupItems,点「保存设置」写库) */
|
||||||
|
const PAGE_SIZE = 10
|
||||||
|
|
||||||
|
const emptyDraft = (): PagePopupItem => ({
|
||||||
|
id: newPagePopupId(),
|
||||||
|
pageName: '',
|
||||||
|
pagePath: DEFAULT_POPUP_PAGE_PATH,
|
||||||
|
scope: 'fullApp',
|
||||||
|
key: '',
|
||||||
|
behavior: '',
|
||||||
|
content: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
export function MpUiPopupTableSection({
|
||||||
|
pagePopupItems,
|
||||||
|
setPagePopupItems,
|
||||||
|
extraKeysCount,
|
||||||
|
}: Props) {
|
||||||
|
const [search, setSearch] = useState('')
|
||||||
|
const [dialogOpen, setDialogOpen] = useState(false)
|
||||||
|
const [dialogMode, setDialogMode] = useState<'add' | 'edit'>('add')
|
||||||
|
const [draft, setDraft] = useState<PagePopupItem>(emptyDraft())
|
||||||
|
const [deleteId, setDeleteId] = useState<string | null>(null)
|
||||||
|
const [tablePage, setTablePage] = useState(1)
|
||||||
|
|
||||||
|
const pathSelectValue = useMemo(() => {
|
||||||
|
const t = draft.pagePath.trim()
|
||||||
|
return POPUP_PAGE_PATH_OPTIONS.some((o) => o.value === t) ? t : '__other__'
|
||||||
|
}, [draft.pagePath])
|
||||||
|
|
||||||
|
const filtered = useMemo(() => {
|
||||||
|
const q = search.trim().toLowerCase()
|
||||||
|
if (!q) return pagePopupItems
|
||||||
|
return pagePopupItems.filter((p) => {
|
||||||
|
const blob = `${p.pageName} ${p.pagePath} ${scopeLabel(p.scope)} ${p.key} ${p.behavior} ${p.content} ${displayPageName(p)}`.toLowerCase()
|
||||||
|
return blob.includes(q)
|
||||||
|
})
|
||||||
|
}, [search, pagePopupItems])
|
||||||
|
|
||||||
|
const totalPages = useMemo(
|
||||||
|
() => Math.max(1, Math.ceil(filtered.length / PAGE_SIZE)),
|
||||||
|
[filtered.length],
|
||||||
|
)
|
||||||
|
|
||||||
|
const pagedRows = useMemo(() => {
|
||||||
|
const start = (tablePage - 1) * PAGE_SIZE
|
||||||
|
return filtered.slice(start, start + PAGE_SIZE)
|
||||||
|
}, [filtered, tablePage])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTablePage(1)
|
||||||
|
}, [search])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTablePage((p) => Math.min(p, totalPages))
|
||||||
|
}, [totalPages])
|
||||||
|
|
||||||
|
const openAdd = () => {
|
||||||
|
setDraft(emptyDraft())
|
||||||
|
setDialogMode('add')
|
||||||
|
setDialogOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const openEdit = (row: PagePopupItem) => {
|
||||||
|
setDraft({
|
||||||
|
...row,
|
||||||
|
scope: row.scope === 'singlePage' ? 'singlePage' : 'fullApp',
|
||||||
|
})
|
||||||
|
setDialogMode('edit')
|
||||||
|
setDialogOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveDialog = () => {
|
||||||
|
const path = draft.pagePath.trim()
|
||||||
|
const key = draft.key.trim()
|
||||||
|
if (!path.startsWith('/')) {
|
||||||
|
toast.error('页面路径须以 / 开头')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!isValidPopupKey(key)) {
|
||||||
|
toast.error('键名须为英文:字母开头,仅字母、数字、下划线')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!draft.behavior.trim()) {
|
||||||
|
toast.error('请填写行为说明')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const dup = pagePopupItems.some(
|
||||||
|
(p) => p.pagePath === path && p.key === key && p.id !== draft.id,
|
||||||
|
)
|
||||||
|
if (dup) {
|
||||||
|
toast.error('同一页面下键名不能重复')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (dialogMode === 'add') {
|
||||||
|
setPagePopupItems((prev) => [...prev, { ...draft, id: draft.id || newPagePopupId() }])
|
||||||
|
toast.success('已添加,请点击右上角「保存设置」提交')
|
||||||
|
} else {
|
||||||
|
setPagePopupItems((prev) => prev.map((p) => (p.id === draft.id ? { ...draft } : p)))
|
||||||
|
toast.success('已更新,请点击「保存设置」提交')
|
||||||
|
}
|
||||||
|
setDialogOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmDelete = () => {
|
||||||
|
if (!deleteId) return
|
||||||
|
setPagePopupItems((prev) => prev.filter((p) => p.id !== deleteId))
|
||||||
|
toast.success('已删除,请点击「保存设置」提交')
|
||||||
|
setDeleteId(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<Card className="bg-[#0f2137] border-gray-700/50 shadow-xl">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-white flex items-center gap-2">
|
||||||
|
<MessageSquare className="w-5 h-5 text-[#38bdac]" />
|
||||||
|
弹窗文案(按页面 + 键)
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription className="text-gray-400">
|
||||||
|
先<strong className="text-gray-300">新增</strong>文案:填写<strong className="text-gray-300">页面名称</strong>、路径、<strong className="text-gray-300">类型</strong>(单页面 / 多页面)、<strong className="text-[#38bdac]">英文键名</strong>、行为说明、文案内容。
|
||||||
|
小程序通过 <code className="text-[#38bdac]/90">pagePath + key</code> 从 <code className="text-[#38bdac]/90">mpConfig.mpUi.pagePopupItems</code> 读取。
|
||||||
|
同一页面可配置多条(不同 key)。下方表格每页展示 {PAGE_SIZE} 条;全部条目随右上角「保存设置」写入数据库,与分页无关。
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||||
|
<p className="text-xs text-gray-500">
|
||||||
|
{extraKeysCount > 0
|
||||||
|
? `另有 ${extraKeysCount} 类其它 mpUi 已保留,保存时一并写回。`
|
||||||
|
: '保存后约 5 分钟内随配置缓存刷新。'}
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="border-gray-600 text-gray-200"
|
||||||
|
title="用与小程序对齐的 5 条标准文案替换当前列表,再点「保存设置」写入数据库。迁移完成后可移除此按钮。"
|
||||||
|
onClick={() => {
|
||||||
|
setPagePopupItems(seedPagePopupItemsWithIds())
|
||||||
|
toast.info('已填入标准 5 条,请点击「保存设置」写入数据库')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RotateCcw className="w-3.5 h-3.5 mr-1.5" />
|
||||||
|
填入默认种子
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
className="bg-[#38bdac] hover:bg-[#2da396] text-white"
|
||||||
|
onClick={openAdd}
|
||||||
|
>
|
||||||
|
<PlusCircle className="w-3.5 h-3.5 mr-1.5" />
|
||||||
|
新增文案
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2 max-w-xl w-full">
|
||||||
|
<Label className="text-gray-400 text-xs flex items-center gap-1.5">
|
||||||
|
<Search className="w-3.5 h-3.5" />
|
||||||
|
筛选(页面名称 / 类型 / 路径 / 键 / 行为 / 正文)
|
||||||
|
</Label>
|
||||||
|
<div className="rounded-md border border-gray-700 bg-[#0a1628] px-3 h-10 flex items-center">
|
||||||
|
<Input
|
||||||
|
className="border-0 bg-transparent text-white h-9 px-0 shadow-none focus-visible:ring-0 placeholder:text-gray-600"
|
||||||
|
placeholder="例如:read、beforeLogin、singlePage…"
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="rounded-lg border border-gray-700/80 overflow-x-auto w-full">
|
||||||
|
<Table className="w-full min-w-[1320px] table-fixed">
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow className="border-gray-700 hover:bg-[#0a1628]/80 bg-[#0a1628]">
|
||||||
|
<TableHead className="text-gray-300 w-[11%] min-w-[108px]">页面名称</TableHead>
|
||||||
|
<TableHead className="text-gray-300 w-[8%] min-w-[88px]">类型</TableHead>
|
||||||
|
<TableHead className="text-gray-300 w-[19%] min-w-[200px]">页面路径</TableHead>
|
||||||
|
<TableHead className="text-gray-300 w-[10%] min-w-[108px]">键名(英文)</TableHead>
|
||||||
|
<TableHead className="text-gray-300 w-[13%] min-w-[108px]">行为</TableHead>
|
||||||
|
<TableHead className="text-gray-300 w-[25%] min-w-[220px]">文案摘要</TableHead>
|
||||||
|
<TableHead className="text-gray-300 text-right w-[14%] min-w-[200px] whitespace-nowrap">
|
||||||
|
操作
|
||||||
|
</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{pagedRows.map((p) => (
|
||||||
|
<TableRow key={p.id} className="border-gray-800 hover:bg-[#0f2137]/90">
|
||||||
|
<TableCell className="text-sm text-gray-200 align-top font-medium">
|
||||||
|
{displayPageName(p)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="align-top">
|
||||||
|
<Badge
|
||||||
|
variant={p.scope === 'singlePage' ? 'outline' : 'secondary'}
|
||||||
|
className={
|
||||||
|
p.scope === 'singlePage'
|
||||||
|
? 'border-amber-500/50 text-amber-200/95 text-[11px]'
|
||||||
|
: 'text-[11px]'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{scopeLabel(p.scope)}
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="font-mono text-xs text-[#38bdac]/95 align-top break-all">
|
||||||
|
{p.pagePath}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="font-mono text-xs text-amber-200/90 align-top break-all">{p.key}</TableCell>
|
||||||
|
<TableCell className="text-xs text-gray-300 align-top">{p.behavior}</TableCell>
|
||||||
|
<TableCell className="text-xs align-top max-w-[220px] min-w-0 py-2">
|
||||||
|
<div
|
||||||
|
className="line-clamp-1 min-w-0 text-gray-400 break-all cursor-default"
|
||||||
|
title={
|
||||||
|
String(p.content ?? '')
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.trim() || '—'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{summarizePopupRow(p)}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="align-middle text-right">
|
||||||
|
<div className="inline-flex flex-nowrap items-center justify-end gap-1 min-w-[168px]">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-8 shrink-0 px-2.5 text-[#38bdac]"
|
||||||
|
onClick={() => openEdit(p)}
|
||||||
|
>
|
||||||
|
<Pencil className="w-3.5 h-3.5 mr-1" />
|
||||||
|
编辑
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-8 shrink-0 px-2.5 text-gray-400 hover:text-red-400"
|
||||||
|
onClick={() => setDeleteId(p.id)}
|
||||||
|
>
|
||||||
|
<Trash2 className="w-3.5 h-3.5 mr-1" />
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
{filtered.length === 0 && (
|
||||||
|
<p className="text-center text-sm text-gray-500 py-8">无数据,请点击「新增文案」或「填入默认种子」</p>
|
||||||
|
)}
|
||||||
|
{filtered.length > 0 && totalPages > 1 && (
|
||||||
|
<Pagination
|
||||||
|
page={tablePage}
|
||||||
|
totalPages={totalPages}
|
||||||
|
total={filtered.length}
|
||||||
|
pageSize={PAGE_SIZE}
|
||||||
|
onPageChange={setTablePage}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{filtered.length > 0 && totalPages <= 1 && (
|
||||||
|
<div className="flex items-center py-3 px-5 border-t border-gray-700/50 text-sm text-gray-400">
|
||||||
|
共 {filtered.length} 条,每页 {PAGE_SIZE} 条
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||||
|
<DialogContent className="bg-[#0f2137] border-gray-700 text-white max-w-2xl w-[min(100vw-2rem,42rem)] max-h-[90vh] overflow-y-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{dialogMode === 'add' ? '新增弹窗文案' : '编辑弹窗文案'}</DialogTitle>
|
||||||
|
<DialogDescription className="text-gray-400">
|
||||||
|
键名在小程序代码中与 <code className="text-[#38bdac]/90">pagePath</code> 联合使用,请与开发约定后勿随意改键。
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-4 py-2">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-gray-300">页面名称</Label>
|
||||||
|
<Input
|
||||||
|
className="bg-[#0a1628] border-gray-600 text-white"
|
||||||
|
placeholder="如:文章详情 / 阅读"
|
||||||
|
value={draft.pageName}
|
||||||
|
onChange={(e) => setDraft((d) => ({ ...d, pageName: e.target.value }))}
|
||||||
|
/>
|
||||||
|
<p className="text-[11px] text-gray-500">便于表格识别;可与路径列对照,留空时对常见路径会自动显示默认名称。</p>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-gray-300">页面路径</Label>
|
||||||
|
<Select
|
||||||
|
value={pathSelectValue}
|
||||||
|
onValueChange={(v) => {
|
||||||
|
if (v === '__other__') {
|
||||||
|
setDraft((d) => {
|
||||||
|
const cur = d.pagePath.trim()
|
||||||
|
const onList = POPUP_PAGE_PATH_OPTIONS.some((o) => o.value === cur)
|
||||||
|
return { ...d, pagePath: onList ? '/pages/' : d.pagePath }
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
setDraft((d) => ({ ...d, pagePath: v }))
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="bg-[#0a1628] border-gray-600 text-white font-mono text-sm w-full">
|
||||||
|
<SelectValue placeholder="选择页面路径" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent className="max-h-[min(60vh,320px)]">
|
||||||
|
{POPUP_PAGE_PATH_OPTIONS.map((opt) => (
|
||||||
|
<SelectItem
|
||||||
|
key={opt.value}
|
||||||
|
value={opt.value}
|
||||||
|
className="focus:bg-[#1a3a4a] focus:text-white font-mono text-xs"
|
||||||
|
>
|
||||||
|
<span className="text-gray-200">{opt.label}</span>
|
||||||
|
<span className="text-gray-500 ml-2">{opt.value}</span>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
<SelectItem value="__other__" className="focus:bg-[#1a3a4a] focus:text-white">
|
||||||
|
自定义路径…
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
{pathSelectValue === '__other__' && (
|
||||||
|
<Input
|
||||||
|
className="bg-[#0a1628] border-gray-600 text-white font-mono text-sm"
|
||||||
|
placeholder="/pages/xxx/xxx"
|
||||||
|
value={draft.pagePath}
|
||||||
|
onChange={(e) => setDraft((d) => ({ ...d, pagePath: e.target.value }))}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<p className="text-[11px] text-gray-500">
|
||||||
|
从列表选择常用页面;若路径未收录(如分包页),选「自定义路径」后手动填写。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-gray-300">类型</Label>
|
||||||
|
<Select
|
||||||
|
value={draft.scope}
|
||||||
|
onValueChange={(v) =>
|
||||||
|
setDraft((d) => ({ ...d, scope: v === 'singlePage' ? 'singlePage' : 'fullApp' }))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="bg-[#0a1628] border-gray-600 text-white">
|
||||||
|
<SelectValue placeholder="选择类型" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="fullApp" className="focus:bg-[#1a3a4a] focus:text-white">
|
||||||
|
多页面(完整小程序)
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="singlePage" className="focus:bg-[#1a3a4a] focus:text-white">
|
||||||
|
单页面(朋友圈预览等)
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<p className="text-[11px] text-gray-500">
|
||||||
|
单页面:微信单页场景(如 1154);多页面:用户进入完整小程序后的页面。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-gray-300">键名(英文,自定义)</Label>
|
||||||
|
<Input
|
||||||
|
className="bg-[#0a1628] border-gray-600 text-white font-mono text-sm"
|
||||||
|
placeholder="beforeLoginHint"
|
||||||
|
value={draft.key}
|
||||||
|
disabled={dialogMode === 'edit'}
|
||||||
|
onChange={(e) =>
|
||||||
|
setDraft((d) => ({ ...d, key: e.target.value.replace(/[^a-zA-Z0-9_]/g, '') }))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{dialogMode === 'edit' && (
|
||||||
|
<p className="text-[11px] text-gray-500">编辑时不可改键名;需改键请删除后新建。</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-gray-300">行为说明</Label>
|
||||||
|
<Input
|
||||||
|
className="bg-[#0a1628] border-gray-600 text-white"
|
||||||
|
placeholder="如:未登录时付费墙上方展示"
|
||||||
|
value={draft.behavior}
|
||||||
|
onChange={(e) => setDraft((d) => ({ ...d, behavior: e.target.value }))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-gray-300">文案内容</Label>
|
||||||
|
<Textarea
|
||||||
|
className="bg-[#0a1628] border-gray-600 text-white min-h-[140px]"
|
||||||
|
placeholder="弹窗正文、提示语等"
|
||||||
|
value={draft.content}
|
||||||
|
onChange={(e) => setDraft((d) => ({ ...d, content: e.target.value }))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" className="border-gray-600" onClick={() => setDialogOpen(false)}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button className="bg-[#38bdac] hover:bg-[#2da396] text-white" onClick={saveDialog}>
|
||||||
|
确定
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<Dialog open={!!deleteId} onOpenChange={(o) => !o && setDeleteId(null)}>
|
||||||
|
<DialogContent className="bg-[#0f2137] border-gray-700 text-white max-w-md">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>删除该条文案?</DialogTitle>
|
||||||
|
<DialogDescription className="text-gray-400">
|
||||||
|
删除后需保存设置才会同步到小程序;若键名已被代码引用,删除后对应位置将走默认兜底。
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" className="border-gray-600" onClick={() => setDeleteId(null)}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button className="bg-red-600 hover:bg-red-700" onClick={confirmDelete}>
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -40,11 +40,15 @@ import {
|
|||||||
EyeOff,
|
EyeOff,
|
||||||
LayoutGrid,
|
LayoutGrid,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
|
MessageSquare,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { get, post } from '@/api/client'
|
import { get, post } from '@/api/client'
|
||||||
import { AuthorSettingsPage } from '@/pages/author-settings/AuthorSettingsPage'
|
import { AuthorSettingsPage } from '@/pages/author-settings/AuthorSettingsPage'
|
||||||
import { AdminUsersPage } from '@/pages/admin-users/AdminUsersPage'
|
import { AdminUsersPage } from '@/pages/admin-users/AdminUsersPage'
|
||||||
import { ApiDocsPage } from '@/pages/api-docs/ApiDocsPage'
|
import { ApiDocsPage } from '@/pages/api-docs/ApiDocsPage'
|
||||||
|
import { MpUiPopupTableSection } from '@/pages/settings/MpUiPopupTableSection'
|
||||||
|
import type { PagePopupItem } from '@/pages/settings/mpUiCopyConfig'
|
||||||
|
import { buildMpUiPayload, splitMpUiForPopupEditor } from '@/pages/settings/mpUiCopyConfig'
|
||||||
|
|
||||||
interface AuthorInfo {
|
interface AuthorInfo {
|
||||||
name?: string
|
name?: string
|
||||||
@@ -76,7 +80,7 @@ interface MpConfig {
|
|||||||
mchId?: string
|
mchId?: string
|
||||||
minWithdraw?: number
|
minWithdraw?: number
|
||||||
auditMode?: boolean
|
auditMode?: boolean
|
||||||
/** 小程序界面文案与跳转,与 soul-api defaultMpUi 结构一致,服务端会与默认值深合并 */
|
/** mpUi:含 pagePopupItems 等;服务端与 defaultMpUi 深合并 */
|
||||||
mpUi?: Record<string, unknown>
|
mpUi?: Record<string, unknown>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,59 +123,10 @@ const defaultFeatures: FeatureConfig = {
|
|||||||
aboutEnabled: true,
|
aboutEnabled: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 与管理端保存后、后端 deepMergeMpUi 的默认结构对齐,供「填入模板」与文档说明 */
|
|
||||||
const MP_UI_TEMPLATE_OBJECT: Record<string, Record<string, string>> = {
|
|
||||||
tabBar: { home: '首页', chapters: '目录', match: '找伙伴', my: '我的' },
|
|
||||||
chaptersPage: {
|
|
||||||
bookTitle: '一场SOUL的创业实验场',
|
|
||||||
bookSubtitle: '来自Soul派对房的真实商业故事',
|
|
||||||
newBadgeText: 'NEW',
|
|
||||||
},
|
|
||||||
// homePage.linkKaruoAvatar:首页「链接卡若」头像 HTTPS,空则小程序用「卡」字占位
|
|
||||||
homePage: {
|
|
||||||
logoTitle: '卡若创业派对',
|
|
||||||
logoSubtitle: '来自派对房的真实故事',
|
|
||||||
linkKaruoText: '点击链接卡若',
|
|
||||||
linkKaruoAvatar: '',
|
|
||||||
pinnedTitlePrefix: '派对会员',
|
|
||||||
pinnedMainTitleTemplate: '',
|
|
||||||
searchPlaceholder: '搜索章节标题或内容...',
|
|
||||||
bannerTag: '推荐',
|
|
||||||
bannerReadMoreText: '点击阅读',
|
|
||||||
superSectionTitle: '超级个体',
|
|
||||||
superSectionLinkText: '获客入口',
|
|
||||||
superSectionLinkPath: '/pages/match/match',
|
|
||||||
pickSectionTitle: '精选推荐',
|
|
||||||
latestSectionTitle: '最新新增',
|
|
||||||
},
|
|
||||||
myPage: {
|
|
||||||
cardLabel: '名片',
|
|
||||||
vipLabelVip: '会员中心',
|
|
||||||
vipLabelGuest: '成为会员',
|
|
||||||
cardPath: '',
|
|
||||||
vipPath: '/pages/vip/vip',
|
|
||||||
readStatLabel: '已读章节',
|
|
||||||
recentReadTitle: '最近阅读',
|
|
||||||
readStatPath: '/pages/reading-records/reading-records?focus=all',
|
|
||||||
recentReadPath: '/pages/reading-records/reading-records?focus=recent',
|
|
||||||
},
|
|
||||||
memberDetailPage: {
|
|
||||||
unlockIntroTitle: '解锁与链接说明',
|
|
||||||
unlockIntroBody:
|
|
||||||
'「链接」用于提交留资,由对方通过获客计划跟进;「解锁」用于复制手机/微信号后自行添加好友。请先阅读说明,确认后再登录。',
|
|
||||||
},
|
|
||||||
readPage: {
|
|
||||||
beforeLoginHint: '试读进度与下方百分比以后台配置为准;登录后可购买解锁全文。',
|
|
||||||
singlePageTitle: '解锁全文',
|
|
||||||
singlePagePaywallHint:
|
|
||||||
'当前为朋友圈单页预览,无法在此登录或付款。请点击底部「前往小程序」进入完整版后再解锁本章。',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const TAB_KEYS = ['system', 'author', 'admin', 'api-docs'] as const
|
const TAB_KEYS = ['system', 'author', 'admin', 'api-docs'] as const
|
||||||
type TabKey = (typeof TAB_KEYS)[number]
|
type TabKey = (typeof TAB_KEYS)[number]
|
||||||
|
|
||||||
const SYSTEM_SECTION_KEYS = ['basic', 'mp', 'oss', 'features'] as const
|
const SYSTEM_SECTION_KEYS = ['basic', 'mp', 'mp-copy', 'oss', 'features'] as const
|
||||||
type SystemSectionKey = (typeof SYSTEM_SECTION_KEYS)[number]
|
type SystemSectionKey = (typeof SYSTEM_SECTION_KEYS)[number]
|
||||||
|
|
||||||
export function SettingsPage() {
|
export function SettingsPage() {
|
||||||
@@ -186,8 +141,10 @@ export function SettingsPage() {
|
|||||||
const [localSettings, setLocalSettings] = useState<LocalSettings>(defaultSettings)
|
const [localSettings, setLocalSettings] = useState<LocalSettings>(defaultSettings)
|
||||||
const [featureConfig, setFeatureConfig] = useState<FeatureConfig>(defaultFeatures)
|
const [featureConfig, setFeatureConfig] = useState<FeatureConfig>(defaultFeatures)
|
||||||
const [mpConfig, setMpConfig] = useState<MpConfig>(defaultMpConfig)
|
const [mpConfig, setMpConfig] = useState<MpConfig>(defaultMpConfig)
|
||||||
const [mpUiJson, setMpUiJson] = useState('{}')
|
/** 不在 pagePopupItems 内的 mpUi 顶层键(tabBar、homePage 等),保存时写回 */
|
||||||
const [chaptersNewBadgeText, setChaptersNewBadgeText] = useState('NEW')
|
const [mpUiExtra, setMpUiExtra] = useState<Record<string, unknown>>({})
|
||||||
|
/** 弹窗文案列表:mpUi.pagePopupItems */
|
||||||
|
const [pagePopupItems, setPagePopupItems] = useState<PagePopupItem[]>([])
|
||||||
const [ossConfig, setOssConfig] = useState<OssConfig>({})
|
const [ossConfig, setOssConfig] = useState<OssConfig>({})
|
||||||
const [isSaving, setIsSaving] = useState(false)
|
const [isSaving, setIsSaving] = useState(false)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
@@ -220,22 +177,9 @@ export function SettingsPage() {
|
|||||||
if (res.mpConfig && typeof res.mpConfig === 'object') {
|
if (res.mpConfig && typeof res.mpConfig === 'object') {
|
||||||
const merged = { ...res.mpConfig } as MpConfig
|
const merged = { ...res.mpConfig } as MpConfig
|
||||||
setMpConfig((prev) => ({ ...prev, ...merged }))
|
setMpConfig((prev) => ({ ...prev, ...merged }))
|
||||||
const raw = merged.mpUi
|
const { extra, pagePopupItems: rows } = splitMpUiForPopupEditor(merged.mpUi)
|
||||||
const rawObj =
|
setMpUiExtra(extra)
|
||||||
raw != null && typeof raw === 'object' && !Array.isArray(raw) ? (raw as Record<string, unknown>) : {}
|
setPagePopupItems(rows)
|
||||||
const chaptersPage =
|
|
||||||
rawObj.chaptersPage && typeof rawObj.chaptersPage === 'object' && !Array.isArray(rawObj.chaptersPage)
|
|
||||||
? (rawObj.chaptersPage as Record<string, unknown>)
|
|
||||||
: {}
|
|
||||||
const badgeRaw = chaptersPage.newBadgeText ?? chaptersPage.sectionNewBadgeText
|
|
||||||
setChaptersNewBadgeText(typeof badgeRaw === 'string' && badgeRaw.trim() ? badgeRaw.trim() : 'NEW')
|
|
||||||
setMpUiJson(
|
|
||||||
JSON.stringify(
|
|
||||||
rawObj,
|
|
||||||
null,
|
|
||||||
2,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
if (res.ossConfig && typeof res.ossConfig === 'object')
|
if (res.ossConfig && typeof res.ossConfig === 'object')
|
||||||
setOssConfig((prev) => ({ ...prev, ...res.ossConfig }))
|
setOssConfig((prev) => ({ ...prev, ...res.ossConfig }))
|
||||||
@@ -317,34 +261,7 @@ export function SettingsPage() {
|
|||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
setIsSaving(true)
|
setIsSaving(true)
|
||||||
try {
|
try {
|
||||||
let mpUi: Record<string, unknown> = {}
|
const mpUi = buildMpUiPayload(pagePopupItems, mpUiExtra)
|
||||||
try {
|
|
||||||
const t = mpUiJson.trim()
|
|
||||||
if (t) {
|
|
||||||
const parsed: unknown = JSON.parse(t)
|
|
||||||
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
||||||
mpUi = parsed as Record<string, unknown>
|
|
||||||
} else {
|
|
||||||
showResult('保存失败', '小程序文案 mpUi 须为 JSON 对象(非数组)', true)
|
|
||||||
setIsSaving(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
showResult('保存失败', '小程序文案 mpUi 不是合法 JSON', true)
|
|
||||||
setIsSaving(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const chaptersPageRaw = mpUi.chaptersPage
|
|
||||||
const chaptersPage =
|
|
||||||
chaptersPageRaw && typeof chaptersPageRaw === 'object' && !Array.isArray(chaptersPageRaw)
|
|
||||||
? { ...(chaptersPageRaw as Record<string, unknown>) }
|
|
||||||
: {}
|
|
||||||
const badgeText = chaptersNewBadgeText.trim()
|
|
||||||
if (badgeText) chaptersPage.newBadgeText = badgeText
|
|
||||||
else delete chaptersPage.newBadgeText
|
|
||||||
delete chaptersPage.sectionNewBadgeText
|
|
||||||
mpUi.chaptersPage = chaptersPage
|
|
||||||
|
|
||||||
const res = await post<{ success?: boolean; error?: string }>('/api/admin/settings', {
|
const res = await post<{ success?: boolean; error?: string }>('/api/admin/settings', {
|
||||||
featureConfig,
|
featureConfig,
|
||||||
@@ -487,6 +404,13 @@ export function SettingsPage() {
|
|||||||
<Smartphone className="w-3.5 h-3.5 mr-1" />
|
<Smartphone className="w-3.5 h-3.5 mr-1" />
|
||||||
小程序与审核
|
小程序与审核
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
|
<TabsTrigger
|
||||||
|
value="mp-copy"
|
||||||
|
className="data-[state=active]:bg-[#38bdac]/20 data-[state=active]:text-[#38bdac] text-gray-400 text-xs"
|
||||||
|
>
|
||||||
|
<MessageSquare className="w-3.5 h-3.5 mr-1" />
|
||||||
|
弹窗文案
|
||||||
|
</TabsTrigger>
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value="oss"
|
value="oss"
|
||||||
className="data-[state=active]:bg-[#38bdac]/20 data-[state=active]:text-[#38bdac] text-gray-400 text-xs"
|
className="data-[state=active]:bg-[#38bdac]/20 data-[state=active]:text-[#38bdac] text-gray-400 text-xs"
|
||||||
@@ -767,46 +691,9 @@ export function SettingsPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<p className="text-xs text-gray-500 pt-2 border-t border-gray-700/50">
|
||||||
<div className="space-y-2 pt-2 border-t border-gray-700/50">
|
弹窗类文案在「弹窗文案」子 Tab 按页面路径 + 英文键维护(pagePopupItems);目录、Tab、首页板块等仍由其它配置决定。
|
||||||
<div className="grid grid-cols-2 gap-4">
|
</p>
|
||||||
<div className="space-y-2">
|
|
||||||
<Label className="text-gray-300">目录 NEW 角标文案</Label>
|
|
||||||
<Input
|
|
||||||
className="bg-[#0a1628] border-gray-700 text-white"
|
|
||||||
placeholder="例如:NEW / 最新 / 刚更新"
|
|
||||||
value={chaptersNewBadgeText}
|
|
||||||
maxLength={8}
|
|
||||||
onChange={(e) => setChaptersNewBadgeText(e.target.value)}
|
|
||||||
/>
|
|
||||||
<p className="text-xs text-gray-500">
|
|
||||||
用于小程序目录页章节角标,保存后约 5 分钟内生效(可与下方 mpUi JSON 同步保存)。
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
||||||
<Label className="text-gray-300">小程序界面文案 mpUi(JSON)</Label>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
className="border-gray-600 text-gray-200"
|
|
||||||
onClick={() => setMpUiJson(JSON.stringify(MP_UI_TEMPLATE_OBJECT, null, 2))}
|
|
||||||
>
|
|
||||||
填入默认模板
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-gray-500">
|
|
||||||
覆盖 Tab 文案、首页/目录标题、我的页名片与阅读记录路径等;仅填需要改的字段也可(与后端默认值深合并)。保存后小程序约 5
|
|
||||||
分钟内通过 config 缓存刷新。
|
|
||||||
</p>
|
|
||||||
<Textarea
|
|
||||||
className="bg-[#0a1628] border-gray-700 text-white font-mono text-sm min-h-[280px]"
|
|
||||||
spellCheck={false}
|
|
||||||
value={mpUiJson}
|
|
||||||
onChange={(e) => setMpUiJson(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
@@ -843,7 +730,15 @@ export function SettingsPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="mp-copy" className="space-y-6 mt-0">
|
||||||
|
<MpUiPopupTableSection
|
||||||
|
pagePopupItems={pagePopupItems}
|
||||||
|
setPagePopupItems={setPagePopupItems}
|
||||||
|
extraKeysCount={Object.keys(mpUiExtra).length}
|
||||||
|
/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="oss" className="space-y-6 mt-0">
|
<TabsContent value="oss" className="space-y-6 mt-0">
|
||||||
|
|||||||
342
soul-admin/src/pages/settings/mpUiCopyConfig.ts
Normal file
342
soul-admin/src/pages/settings/mpUiCopyConfig.ts
Normal file
@@ -0,0 +1,342 @@
|
|||||||
|
/**
|
||||||
|
* 弹窗文案统一为 mpUi.pagePopupItems[]:每页可多条,按英文 key 在小程序内取用。
|
||||||
|
* 兼容旧版 memberDetailPage / readPage / customPagePopups(加载时迁入,保存时不再写出)。
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** 单页面:朋友圈单页预览等;多页面:完整小程序内 */
|
||||||
|
export type PagePopupScope = 'singlePage' | 'fullApp'
|
||||||
|
|
||||||
|
export type PagePopupItem = {
|
||||||
|
id: string
|
||||||
|
/** 页面中文名称(展示用,与路径对应) */
|
||||||
|
pageName: string
|
||||||
|
/** 小程序页面路径,如 /pages/read/read */
|
||||||
|
pagePath: string
|
||||||
|
/**
|
||||||
|
* 展示场景:单页面(scene 1154 等) / 多页面(完整小程序)
|
||||||
|
* 存 JSON 字段 scope,供运营区分与后续小程序按场景过滤
|
||||||
|
*/
|
||||||
|
scope: PagePopupScope
|
||||||
|
/** 英文键,同一 pagePath 下唯一,供代码引用 */
|
||||||
|
key: string
|
||||||
|
/** 行为说明(管理端自用,如「未登录付费墙上方」) */
|
||||||
|
behavior: string
|
||||||
|
/** 展示文案 */
|
||||||
|
content: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const MEMBER_PATH = '/pages/member-detail/member-detail'
|
||||||
|
const READ_PATH = '/pages/read/read'
|
||||||
|
|
||||||
|
/** 常见路径的默认页面名称(未单独填写 pageName 时表格展示用) */
|
||||||
|
export const KNOWN_PAGE_NAMES: Record<string, string> = {
|
||||||
|
[MEMBER_PATH]: '成员详情',
|
||||||
|
[READ_PATH]: '文章详情 / 阅读',
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 弹窗文案「页面路径」下拉的候选项(与 miniprogram/app.json 的 pages 对齐;新增页面时请同步)
|
||||||
|
* value 必须以 / 开头,与小程序 wx.navigateTo 路径一致
|
||||||
|
*/
|
||||||
|
export const POPUP_PAGE_PATH_OPTIONS: { value: string; label: string }[] = [
|
||||||
|
{ value: '/pages/index/index', label: '首页' },
|
||||||
|
{ value: '/pages/chapters/chapters', label: '目录' },
|
||||||
|
{ value: '/pages/match/match', label: '找伙伴' },
|
||||||
|
{ value: '/pages/my/my', label: '我的' },
|
||||||
|
{ value: '/pages/read/read', label: '文章详情 / 阅读' },
|
||||||
|
{ value: '/pages/link-preview/link-preview', label: '链接预览' },
|
||||||
|
{ value: '/pages/agreement/agreement', label: '用户协议' },
|
||||||
|
{ value: '/pages/privacy/privacy', label: '隐私政策' },
|
||||||
|
{ value: '/pages/referral/referral', label: '邀请' },
|
||||||
|
{ value: '/pages/purchases/purchases', label: '购买记录' },
|
||||||
|
{ value: '/pages/reading-records/reading-records', label: '阅读记录' },
|
||||||
|
{ value: '/pages/settings/settings', label: '设置' },
|
||||||
|
{ value: '/pages/search/search', label: '搜索' },
|
||||||
|
{ value: '/pages/addresses/addresses', label: '地址列表' },
|
||||||
|
{ value: '/pages/addresses/edit', label: '编辑地址' },
|
||||||
|
{ value: '/pages/withdraw-records/withdraw-records', label: '提现记录' },
|
||||||
|
{ value: '/pages/wallet/wallet', label: '钱包' },
|
||||||
|
{ value: '/pages/vip/vip', label: '会员中心' },
|
||||||
|
{ value: '/pages/member-detail/member-detail', label: '成员详情' },
|
||||||
|
{ value: '/pages/mentors/mentors', label: '导师列表' },
|
||||||
|
{ value: '/pages/mentor-detail/mentor-detail', label: '导师详情' },
|
||||||
|
{ value: '/pages/profile-show/profile-show', label: '资料展示' },
|
||||||
|
{ value: '/pages/profile-edit/profile-edit', label: '编辑资料' },
|
||||||
|
{ value: '/pages/avatar-nickname/avatar-nickname', label: '头像昵称' },
|
||||||
|
{ value: '/pages/gift-pay/detail', label: '礼物支付 · 详情' },
|
||||||
|
{ value: '/pages/gift-pay/list', label: '礼物支付 · 列表' },
|
||||||
|
{ value: '/pages/gift-pay/redemption-detail', label: '礼物支付 · 兑换详情' },
|
||||||
|
{ value: '/pages/dev-login/dev-login', label: '开发登录' },
|
||||||
|
]
|
||||||
|
|
||||||
|
/** 新增弹窗文案时默认选中的页面路径(阅读页,与种子一致) */
|
||||||
|
export const DEFAULT_POPUP_PAGE_PATH = READ_PATH
|
||||||
|
|
||||||
|
export function displayPageName(p: PagePopupItem): string {
|
||||||
|
const n = String(p.pageName ?? '').trim()
|
||||||
|
if (n) return n
|
||||||
|
return KNOWN_PAGE_NAMES[p.pagePath] || '—'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeScope(raw: unknown): PagePopupScope {
|
||||||
|
const s = String(raw ?? '').trim()
|
||||||
|
if (s === 'singlePage' || s === 'single_page') return 'singlePage'
|
||||||
|
return 'fullApp'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function scopeLabel(scope: PagePopupScope): string {
|
||||||
|
return scope === 'singlePage' ? '单页面' : '多页面'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function newPagePopupId(): string {
|
||||||
|
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') return crypto.randomUUID()
|
||||||
|
return `pp-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 英文键:字母开头,仅字母数字下划线 */
|
||||||
|
export function isValidPopupKey(key: string): boolean {
|
||||||
|
return /^[a-zA-Z][a-zA-Z0-9_]*$/.test(key.trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parsePagePopupItems(raw: unknown): PagePopupItem[] {
|
||||||
|
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return []
|
||||||
|
const v = (raw as Record<string, unknown>).pagePopupItems
|
||||||
|
if (!Array.isArray(v)) return []
|
||||||
|
const out: PagePopupItem[] = []
|
||||||
|
v.forEach((item) => {
|
||||||
|
if (!item || typeof item !== 'object' || Array.isArray(item)) return
|
||||||
|
const o = item as Record<string, unknown>
|
||||||
|
const key = String(o.key ?? '').trim()
|
||||||
|
if (!isValidPopupKey(key)) return
|
||||||
|
const pagePath = String(o.pagePath ?? '').trim()
|
||||||
|
if (!pagePath.startsWith('/')) return
|
||||||
|
out.push({
|
||||||
|
id: typeof o.id === 'string' && o.id.trim() ? o.id.trim() : newPagePopupId(),
|
||||||
|
pageName: String(o.pageName ?? '').trim(),
|
||||||
|
pagePath,
|
||||||
|
scope: normalizeScope(o.scope),
|
||||||
|
key,
|
||||||
|
behavior: String(o.behavior ?? '').trim() || '—',
|
||||||
|
content: String(o.content ?? ''),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 从旧版 mpUi 结构生成 pagePopupItems(仅当尚无 pagePopupItems 时) */
|
||||||
|
export function migrateLegacyPagePopups(raw: unknown): PagePopupItem[] {
|
||||||
|
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return []
|
||||||
|
const obj = raw as Record<string, unknown>
|
||||||
|
const out: PagePopupItem[] = []
|
||||||
|
|
||||||
|
const md = obj.memberDetailPage
|
||||||
|
if (md && typeof md === 'object' && !Array.isArray(md)) {
|
||||||
|
const m = md as Record<string, unknown>
|
||||||
|
if (typeof m.unlockIntroTitle === 'string' && m.unlockIntroTitle.trim()) {
|
||||||
|
out.push({
|
||||||
|
id: newPagePopupId(),
|
||||||
|
pageName: KNOWN_PAGE_NAMES[MEMBER_PATH],
|
||||||
|
pagePath: MEMBER_PATH,
|
||||||
|
scope: 'fullApp',
|
||||||
|
key: 'unlockIntroTitle',
|
||||||
|
behavior: '解锁前说明弹窗 · 标题(wx.showModal title)',
|
||||||
|
content: m.unlockIntroTitle,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (typeof m.unlockIntroBody === 'string' && m.unlockIntroBody.trim()) {
|
||||||
|
out.push({
|
||||||
|
id: newPagePopupId(),
|
||||||
|
pageName: KNOWN_PAGE_NAMES[MEMBER_PATH],
|
||||||
|
pagePath: MEMBER_PATH,
|
||||||
|
scope: 'fullApp',
|
||||||
|
key: 'unlockIntroBody',
|
||||||
|
behavior: '解锁前说明弹窗 · 正文(wx.showModal content)',
|
||||||
|
content: m.unlockIntroBody,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const rp = obj.readPage
|
||||||
|
if (rp && typeof rp === 'object' && !Array.isArray(rp)) {
|
||||||
|
const r = rp as Record<string, unknown>
|
||||||
|
const pairs: [string, string, PagePopupScope][] = [
|
||||||
|
['beforeLoginHint', '未登录时付费墙上方说明', 'fullApp'],
|
||||||
|
['singlePageTitle', '朋友圈单页 · 付费区标题', 'singlePage'],
|
||||||
|
['singlePagePaywallHint', '朋友圈单页 · 付费墙说明', 'singlePage'],
|
||||||
|
]
|
||||||
|
for (const [k, behavior, scope] of pairs) {
|
||||||
|
if (typeof r[k] === 'string' && String(r[k]).trim()) {
|
||||||
|
out.push({
|
||||||
|
id: newPagePopupId(),
|
||||||
|
pageName: KNOWN_PAGE_NAMES[READ_PATH],
|
||||||
|
pagePath: READ_PATH,
|
||||||
|
scope,
|
||||||
|
key: k,
|
||||||
|
behavior,
|
||||||
|
content: String(r[k]),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const custom = obj.customPagePopups
|
||||||
|
if (Array.isArray(custom)) {
|
||||||
|
custom.forEach((c, idx) => {
|
||||||
|
if (!c || typeof c !== 'object' || Array.isArray(c)) return
|
||||||
|
const o = c as Record<string, unknown>
|
||||||
|
const pagePath = String(o.pagePath ?? '').trim()
|
||||||
|
if (!pagePath.startsWith('/')) return
|
||||||
|
const customPageName = String(o.pageName ?? '').trim()
|
||||||
|
const push = (suffix: string, behavior: string, val: unknown, scope: PagePopupScope) => {
|
||||||
|
if (typeof val !== 'string' || !val.trim()) return
|
||||||
|
const k = `c${idx}_${suffix}`
|
||||||
|
if (!isValidPopupKey(k)) return
|
||||||
|
out.push({
|
||||||
|
id: newPagePopupId(),
|
||||||
|
pageName: customPageName || KNOWN_PAGE_NAMES[pagePath] || '',
|
||||||
|
pagePath,
|
||||||
|
scope,
|
||||||
|
key: k,
|
||||||
|
behavior: `${behavior}(旧版 customPagePopups 迁移)`,
|
||||||
|
content: val,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
push('fullModalTitle', '完整端弹窗标题', o.fullModalTitle, 'fullApp')
|
||||||
|
push('fullModalBody', '完整端弹窗正文', o.fullModalBody, 'fullApp')
|
||||||
|
push('singlePageTitle', '单页标题', o.singlePageTitle, 'singlePage')
|
||||||
|
push('singlePageBody', '单页说明', o.singlePageBody, 'singlePage')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
const LEGACY_KEYS = new Set(['memberDetailPage', 'readPage', 'customPagePopups', 'pagePopupItems'])
|
||||||
|
|
||||||
|
/** 其余 mpUi 顶层键原样保留(tabBar、homePage 等) */
|
||||||
|
export function extractMpUiExtra(raw: unknown): Record<string, unknown> {
|
||||||
|
const extra: Record<string, unknown> = {}
|
||||||
|
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return extra
|
||||||
|
const obj = raw as Record<string, unknown>
|
||||||
|
for (const k of Object.keys(obj)) {
|
||||||
|
if (LEGACY_KEYS.has(k)) continue
|
||||||
|
extra[k] = obj[k]
|
||||||
|
}
|
||||||
|
return extra
|
||||||
|
}
|
||||||
|
|
||||||
|
export function splitMpUiForPopupEditor(raw: unknown): {
|
||||||
|
extra: Record<string, unknown>
|
||||||
|
pagePopupItems: PagePopupItem[]
|
||||||
|
} {
|
||||||
|
let items = parsePagePopupItems(raw)
|
||||||
|
if (items.length === 0) items = migrateLegacyPagePopups(raw)
|
||||||
|
const extra = extractMpUiExtra(raw)
|
||||||
|
return { extra, pagePopupItems: dedupePagePopupItems(items) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 同一 pagePath + key 只保留第一条 */
|
||||||
|
function dedupePagePopupItems(items: PagePopupItem[]): PagePopupItem[] {
|
||||||
|
const seen = new Set<string>()
|
||||||
|
const out: PagePopupItem[] = []
|
||||||
|
for (const it of items) {
|
||||||
|
const k = `${it.pagePath}\0${it.key}`
|
||||||
|
if (seen.has(k)) continue
|
||||||
|
seen.add(k)
|
||||||
|
out.push(it)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildMpUiPayload(
|
||||||
|
pagePopupItems: PagePopupItem[],
|
||||||
|
extra: Record<string, unknown>,
|
||||||
|
): Record<string, unknown> {
|
||||||
|
const out: Record<string, unknown> = { ...extra }
|
||||||
|
out.pagePopupItems = pagePopupItems.map((p) => ({
|
||||||
|
id: p.id,
|
||||||
|
pageName: String(p.pageName ?? '').trim(),
|
||||||
|
pagePath: p.pagePath.trim(),
|
||||||
|
scope: p.scope === 'singlePage' ? 'singlePage' : 'fullApp',
|
||||||
|
key: p.key.trim(),
|
||||||
|
behavior: p.behavior.trim() || '—',
|
||||||
|
content: p.content,
|
||||||
|
}))
|
||||||
|
delete out.memberDetailPage
|
||||||
|
delete out.readPage
|
||||||
|
delete out.customPagePopups
|
||||||
|
|
||||||
|
const cp = out.chaptersPage
|
||||||
|
if (cp && typeof cp === 'object' && !Array.isArray(cp)) {
|
||||||
|
const m = (cp as Record<string, unknown>) ?? {}
|
||||||
|
delete m.sectionNewBadgeText
|
||||||
|
const badge = String(m.newBadgeText ?? '').trim()
|
||||||
|
if (badge) m.newBadgeText = badge
|
||||||
|
else delete m.newBadgeText
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
export function summarizePopupRow(p: PagePopupItem): string {
|
||||||
|
const c = String(p.content || '').replace(/\s+/g, ' ').trim()
|
||||||
|
return c.length > 56 ? `${c.slice(0, 56)}…` : c || '—'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认种子:与小程序代码引用一一对应(填入默认 + 保存设置 → 写入 mpConfig.mpUi.pagePopupItems)。
|
||||||
|
*
|
||||||
|
* 对照(唯一来源清单):
|
||||||
|
* - `miniprogram/utils/mpPagePopups.js`:`getMemberDetailContent` / `getReadPageContent` 使用的 pagePath + key
|
||||||
|
* - `/pages/member-detail/member-detail.js` → `_showUnlockIntroThenLogin`(unlockIntro*;正文兜底与下述 content 一致)
|
||||||
|
* - `/pages/read/read.js` → `onLoad` 内 beforeLoginHint / singlePage*(标题兜底「解锁全文」)
|
||||||
|
*
|
||||||
|
* 当前共 5 条,勿随意改 key;改文案请在后台或改此处种子后重新保存。
|
||||||
|
*/
|
||||||
|
export const SEED_PAGE_POPUP_ITEMS: Omit<PagePopupItem, 'id'>[] = [
|
||||||
|
{
|
||||||
|
pageName: KNOWN_PAGE_NAMES[MEMBER_PATH],
|
||||||
|
pagePath: MEMBER_PATH,
|
||||||
|
scope: 'fullApp',
|
||||||
|
key: 'unlockIntroTitle',
|
||||||
|
behavior: '解锁前说明弹窗 · 标题(wx.showModal title)',
|
||||||
|
content: '解锁与链接说明',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pageName: KNOWN_PAGE_NAMES[MEMBER_PATH],
|
||||||
|
pagePath: MEMBER_PATH,
|
||||||
|
scope: 'fullApp',
|
||||||
|
key: 'unlockIntroBody',
|
||||||
|
behavior: '解锁前说明弹窗 · 正文(wx.showModal content)',
|
||||||
|
content:
|
||||||
|
'「链接」用于提交留资,由对方通过获客计划跟进;「解锁」用于复制手机/微信号后自行添加好友。\n\n请确认已了解后再登录。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pageName: KNOWN_PAGE_NAMES[READ_PATH],
|
||||||
|
pagePath: READ_PATH,
|
||||||
|
scope: 'fullApp',
|
||||||
|
key: 'beforeLoginHint',
|
||||||
|
behavior: '未登录时付费墙上方说明',
|
||||||
|
content: '试读进度与下方百分比以后台配置为准;登录后可购买解锁全文。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pageName: KNOWN_PAGE_NAMES[READ_PATH],
|
||||||
|
pagePath: READ_PATH,
|
||||||
|
scope: 'singlePage',
|
||||||
|
key: 'singlePageTitle',
|
||||||
|
behavior: '朋友圈单页 · 付费区标题',
|
||||||
|
content: '解锁全文',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pageName: KNOWN_PAGE_NAMES[READ_PATH],
|
||||||
|
pagePath: READ_PATH,
|
||||||
|
scope: 'singlePage',
|
||||||
|
key: 'singlePagePaywallHint',
|
||||||
|
behavior: '朋友圈单页 · 付费墙说明',
|
||||||
|
content:
|
||||||
|
'当前为朋友圈单页预览,无法在此登录或付款。请点击底部「前往小程序」进入完整版后再解锁本章。',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export function seedPagePopupItemsWithIds(): PagePopupItem[] {
|
||||||
|
return SEED_PAGE_POPUP_ITEMS.map((s) => ({ ...s, id: newPagePopupId() }))
|
||||||
|
}
|
||||||
@@ -1 +1 @@
|
|||||||
{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/auth.ts","./src/api/ckb.ts","./src/api/client.ts","./src/components/rechargealert.tsx","./src/components/richeditor.tsx","./src/components/modules/mbti/mbtiavatarsmanager.tsx","./src/components/modules/user/memberuserselect.tsx","./src/components/modules/user/setvipmodal.tsx","./src/components/modules/user/userdetailmodal.tsx","./src/components/ui/pagination.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/dialog.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/select.tsx","./src/components/ui/slider.tsx","./src/components/ui/switch.tsx","./src/components/ui/table.tsx","./src/components/ui/tabs.tsx","./src/components/ui/textarea.tsx","./src/hooks/usedebounce.ts","./src/layouts/adminlayout.tsx","./src/lib/mbtiavatarprompts.ts","./src/lib/utils.ts","./src/pages/admin-users/adminuserspage.tsx","./src/pages/api-doc/apidocpage.tsx","./src/pages/api-docs/apidocspage.tsx","./src/pages/author-settings/authorsettingspage.tsx","./src/pages/chapters/chapterspage.tsx","./src/pages/content/chaptertree.tsx","./src/pages/content/contentpage.tsx","./src/pages/content/personaddeditmodal.tsx","./src/pages/dashboard/dashboardpage.tsx","./src/pages/distribution/distributionpage.tsx","./src/pages/find-partner/findpartnerpage.tsx","./src/pages/find-partner/tabs/ckbconfigpanel.tsx","./src/pages/find-partner/tabs/ckbstatstab.tsx","./src/pages/find-partner/tabs/findpartnertab.tsx","./src/pages/find-partner/tabs/matchpooltab.tsx","./src/pages/find-partner/tabs/matchrecordstab.tsx","./src/pages/find-partner/tabs/mentorbookingtab.tsx","./src/pages/find-partner/tabs/mentortab.tsx","./src/pages/find-partner/tabs/resourcedockingtab.tsx","./src/pages/find-partner/tabs/teamrecruittab.tsx","./src/pages/linked-mp/linkedmppage.tsx","./src/pages/login/loginpage.tsx","./src/pages/match/matchpage.tsx","./src/pages/match-records/matchrecordspage.tsx","./src/pages/mentor-consultations/mentorconsultationspage.tsx","./src/pages/mentors/mentorspage.tsx","./src/pages/not-found/notfoundpage.tsx","./src/pages/orders/orderspage.tsx","./src/pages/payment/paymentpage.tsx","./src/pages/qrcodes/qrcodespage.tsx","./src/pages/referral-settings/referralsettingspage.tsx","./src/pages/settings/settingspage.tsx","./src/pages/site/sitepage.tsx","./src/pages/users/userspage.tsx","./src/pages/vip-roles/viprolespage.tsx","./src/pages/withdrawals/withdrawalspage.tsx","./src/utils/toast.ts"],"version":"5.6.3"}
|
{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/auth.ts","./src/api/ckb.ts","./src/api/client.ts","./src/components/rechargealert.tsx","./src/components/richeditor.tsx","./src/components/modules/mbti/mbtiavatarsmanager.tsx","./src/components/modules/user/memberuserselect.tsx","./src/components/modules/user/setvipmodal.tsx","./src/components/modules/user/userdetailmodal.tsx","./src/components/ui/pagination.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/dialog.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/select.tsx","./src/components/ui/slider.tsx","./src/components/ui/switch.tsx","./src/components/ui/table.tsx","./src/components/ui/tabs.tsx","./src/components/ui/textarea.tsx","./src/hooks/usedebounce.ts","./src/layouts/adminlayout.tsx","./src/lib/mbtiavatarprompts.ts","./src/lib/utils.ts","./src/pages/admin-users/adminuserspage.tsx","./src/pages/api-doc/apidocpage.tsx","./src/pages/api-docs/apidocspage.tsx","./src/pages/author-settings/authorsettingspage.tsx","./src/pages/chapters/chapterspage.tsx","./src/pages/content/chaptertree.tsx","./src/pages/content/contentpage.tsx","./src/pages/content/personaddeditmodal.tsx","./src/pages/dashboard/dashboardpage.tsx","./src/pages/distribution/distributionpage.tsx","./src/pages/find-partner/findpartnerpage.tsx","./src/pages/find-partner/tabs/ckbconfigpanel.tsx","./src/pages/find-partner/tabs/ckbstatstab.tsx","./src/pages/find-partner/tabs/findpartnertab.tsx","./src/pages/find-partner/tabs/matchpooltab.tsx","./src/pages/find-partner/tabs/matchrecordstab.tsx","./src/pages/find-partner/tabs/mentorbookingtab.tsx","./src/pages/find-partner/tabs/mentortab.tsx","./src/pages/find-partner/tabs/resourcedockingtab.tsx","./src/pages/find-partner/tabs/teamrecruittab.tsx","./src/pages/linked-mp/linkedmppage.tsx","./src/pages/login/loginpage.tsx","./src/pages/match/matchpage.tsx","./src/pages/match-records/matchrecordspage.tsx","./src/pages/mentor-consultations/mentorconsultationspage.tsx","./src/pages/mentors/mentorspage.tsx","./src/pages/not-found/notfoundpage.tsx","./src/pages/orders/orderspage.tsx","./src/pages/payment/paymentpage.tsx","./src/pages/qrcodes/qrcodespage.tsx","./src/pages/referral-settings/referralsettingspage.tsx","./src/pages/settings/mpuipopuptablesection.tsx","./src/pages/settings/settingspage.tsx","./src/pages/settings/mpuicopyconfig.ts","./src/pages/site/sitepage.tsx","./src/pages/users/userspage.tsx","./src/pages/vip-roles/viprolespage.tsx","./src/pages/withdrawals/withdrawalspage.tsx","./src/utils/toast.ts"],"version":"5.6.3"}
|
||||||
@@ -163,14 +163,13 @@ func defaultMpUi() gin.H {
|
|||||||
"readStatPath": "/pages/reading-records/reading-records?focus=all",
|
"readStatPath": "/pages/reading-records/reading-records?focus=all",
|
||||||
"recentReadPath": "/pages/reading-records/reading-records?focus=recent",
|
"recentReadPath": "/pages/reading-records/reading-records?focus=recent",
|
||||||
},
|
},
|
||||||
"memberDetailPage": gin.H{
|
// 弹窗文案:管理端按 pagePath + key 维护,见 mpUi.pagePopupItems(memberDetailPage/readPage 已废弃,由迁移合并)
|
||||||
"unlockIntroTitle": "解锁与链接说明",
|
"pagePopupItems": []interface{}{
|
||||||
"unlockIntroBody": "「链接」用于提交留资,由对方通过获客计划跟进;「解锁」用于复制手机/微信号后自行添加好友。请先阅读说明,确认后再登录。",
|
gin.H{"id": "seed-unlock-title", "pageName": "成员详情", "pagePath": "/pages/member-detail/member-detail", "scope": "fullApp", "key": "unlockIntroTitle", "behavior": "解锁前说明弹窗 · 标题(wx.showModal title)", "content": "解锁与链接说明"},
|
||||||
},
|
gin.H{"id": "seed-unlock-body", "pageName": "成员详情", "pagePath": "/pages/member-detail/member-detail", "scope": "fullApp", "key": "unlockIntroBody", "behavior": "解锁前说明弹窗 · 正文(wx.showModal content)", "content": "「链接」用于提交留资,由对方通过获客计划跟进;「解锁」用于复制手机/微信号后自行添加好友。\n\n请确认已了解后再登录。"},
|
||||||
"readPage": gin.H{
|
gin.H{"id": "seed-read-hint", "pageName": "文章详情 / 阅读", "pagePath": "/pages/read/read", "scope": "fullApp", "key": "beforeLoginHint", "behavior": "未登录时付费墙上方说明", "content": "试读进度与下方百分比以后台配置为准;登录后可购买解锁全文。"},
|
||||||
"beforeLoginHint": "试读进度与下方百分比以后台配置为准;登录后可购买解锁全文。",
|
gin.H{"id": "seed-read-sp-title", "pageName": "文章详情 / 阅读", "pagePath": "/pages/read/read", "scope": "singlePage", "key": "singlePageTitle", "behavior": "朋友圈单页 · 付费区标题", "content": "解锁全文"},
|
||||||
"singlePageTitle": "解锁全文",
|
gin.H{"id": "seed-read-sp-hint", "pageName": "文章详情 / 阅读", "pagePath": "/pages/read/read", "scope": "singlePage", "key": "singlePagePaywallHint", "behavior": "朋友圈单页 · 付费墙说明", "content": "当前为朋友圈单页预览,无法在此登录或付款。请点击底部「前往小程序」进入完整版后再解锁本章。"},
|
||||||
"singlePagePaywallHint": "当前为朋友圈单页预览,无法在此登录或付款。请点击底部「前往小程序」进入完整版后再解锁本章。",
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user