feat(admin): 富文本与内容管理页调整;链接标签模型与 db 同步;更新 dist

Made-with: Cursor
This commit is contained in:
卡若
2026-03-24 12:29:46 +08:00
parent 70b83fdb25
commit f069b71374
8 changed files with 272 additions and 153 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -4,8 +4,8 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>管理后台 - Soul创业派对</title>
<script type="module" crossorigin src="/assets/index-Dv-LWSbq.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DXojA1Za.css">
<script type="module" crossorigin src="/assets/index-Dz0mx7au.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-qjssBjc3.css">
</head>
<body>
<div id="root"></div>

View File

@@ -37,9 +37,11 @@ export interface LinkTagItem {
label: string
aliases?: string
url: string
type: 'url' | 'miniprogram' | 'ckb'
type: 'url' | 'miniprogram' | 'ckb' | 'wxlink'
appId?: string
pagePath?: string
/** 管理端列表用:库内是否已存目标小程序 AppSecret接口不下发明文 */
hasAppSecret?: boolean
}
/** 插入附件 HTML 时转义,防 XSS */

View File

@@ -260,8 +260,9 @@ export function ContentPage() {
label: '',
aliases: '',
url: '',
type: 'url' as 'url' | 'miniprogram' | 'ckb',
type: 'url' as 'url' | 'miniprogram' | 'ckb' | 'wxlink',
appId: '',
appSecret: '',
pagePath: '',
})
const [linkTagSaving, setLinkTagSaving] = useState(false)
@@ -537,7 +538,15 @@ export function ContentPage() {
try {
const data = await get<{
success?: boolean
linkTags?: { tagId: string; label: string; url: string; type: string; appId?: string; pagePath?: string }[]
linkTags?: {
tagId: string
label: string
url: string
type: string
appId?: string
pagePath?: string
hasAppSecret?: boolean
}[]
}>('/api/db/link-tags')
if (data?.success && data.linkTags) {
setLinkTags(
@@ -545,9 +554,10 @@ export function ContentPage() {
id: t.tagId,
label: t.label,
url: t.url,
type: (t.type || 'url') as 'url' | 'miniprogram' | 'ckb',
type: (t.type || 'url') as 'url' | 'miniprogram' | 'ckb' | 'wxlink',
appId: t.appId || '',
pagePath: t.pagePath || '',
hasAppSecret: !!t.hasAppSecret,
})),
)
}
@@ -608,7 +618,16 @@ export function ContentPage() {
if (s) qs.set('search', s)
const data = await get<{
success?: boolean
linkTags?: { tagId: string; label: string; aliases?: string; url: string; type: string; appId?: string; pagePath?: string }[]
linkTags?: {
tagId: string
label: string
aliases?: string
url: string
type: string
appId?: string
pagePath?: string
hasAppSecret?: boolean
}[]
total?: number
page?: number
pageSize?: number
@@ -622,9 +641,10 @@ export function ContentPage() {
label: t.label,
aliases: t.aliases || '',
url: t.url,
type: (t.type || 'url') as 'url' | 'miniprogram' | 'ckb',
type: (t.type || 'url') as 'url' | 'miniprogram' | 'ckb' | 'wxlink',
appId: t.appId || '',
pagePath: t.pagePath || '',
hasAppSecret: !!t.hasAppSecret,
})),
)
setLinkTagTotal(typeof data.total === 'number' ? data.total : 0)
@@ -2810,7 +2830,7 @@ export function ContentPage() {
className="bg-amber-500 hover:bg-amber-600 text-white h-8"
onClick={() => {
setLinkTagEditing(null)
setLinkTagForm({ tagId: '', label: '', aliases: '', url: '', type: 'url', appId: '', pagePath: '' })
setLinkTagForm({ tagId: '', label: '', aliases: '', url: '', type: 'url', appId: '', appSecret: '', pagePath: '' })
setMpSearchQuery('')
setMpDropdownOpen(false)
setLinkTagModalOpen(true)
@@ -2862,6 +2882,7 @@ export function ContentPage() {
url: t.url,
type: t.type,
appId: t.appId ?? '',
appSecret: '',
pagePath: t.pagePath ?? '',
})
setMpSearchQuery(t.appId ?? '')
@@ -2882,12 +2903,12 @@ export function ContentPage() {
className={`text-[10px] ${
t.type === 'ckb'
? 'bg-green-500/20 text-green-300 border-green-500/30'
: t.type === 'miniprogram'
: t.type === 'miniprogram' || t.type === 'wxlink'
? 'bg-[#38bdac]/20 text-[#38bdac] border-[#38bdac]/30'
: 'bg-gray-700 text-gray-300'
}`}
>
{t.type === 'url' ? '网页' : t.type === 'ckb' ? '存客宝' : '小程序'}
{t.type === 'url' ? '网页' : t.type === 'ckb' ? '存客宝' : t.type === 'wxlink' ? '小程序链接' : '小程序'}
</Badge>
</td>
<td className="px-3 py-2 text-gray-300">
@@ -2903,6 +2924,18 @@ export function ContentPage() {
)
})()}
{t.pagePath && <div className="text-xs text-gray-500 font-mono">{t.pagePath}</div>}
<div
className={`text-xs ${t.hasAppSecret ? 'text-emerald-400/90' : 'text-amber-500/80'}`}
>
AppSecret{t.hasAppSecret ? '已保存(仅服务端)' : '未配置'}
</div>
</div>
) : t.type === 'wxlink' ? (
<div className="space-y-0.5">
<div className="text-xs text-[#38bdac] truncate max-w-[420px] font-mono" title={t.url}>
{t.url || '—'}
</div>
<div className="text-[11px] text-gray-500"> web-view </div>
</div>
) : t.url ? (
<a
@@ -2932,6 +2965,7 @@ export function ContentPage() {
url: t.url,
type: t.type,
appId: t.appId ?? '',
appSecret: '',
pagePath: t.pagePath ?? '',
})
setMpSearchQuery(t.appId ?? '')
@@ -2996,7 +3030,7 @@ export function ContentPage() {
<DialogHeader className="gap-1">
<DialogTitle className="text-base">{linkTagEditing ? '编辑链接标签' : '添加链接标签'}</DialogTitle>
<DialogDescription className="text-gray-400 text-xs">
#
# mpKey AppIDAppSecret 使
</DialogDescription>
</DialogHeader>
@@ -3006,7 +3040,7 @@ export function ContentPage() {
<Label className="text-gray-300 text-sm">ID</Label>
<Input
className="bg-[#0a1628] border-gray-700 text-white h-8 text-sm font-mono"
placeholder="留空自动生成;或填 12位数字 / z开头12位"
placeholder="留空自动生成;或自定义短 ID如 kr最长 50 字符"
value={linkTagForm.tagId}
disabled={!!linkTagEditing}
onChange={(e) => setLinkTagForm((p) => ({ ...p, tagId: e.target.value }))}
@@ -3038,7 +3072,7 @@ export function ContentPage() {
<Select
value={linkTagForm.type}
onValueChange={(v) =>
setLinkTagForm((p) => ({ ...p, type: v as 'url' | 'miniprogram' | 'ckb' }))
setLinkTagForm((p) => ({ ...p, type: v as 'url' | 'miniprogram' | 'ckb' | 'wxlink' }))
}
>
<SelectTrigger className="bg-[#0a1628] border-gray-700 text-white h-8">
@@ -3046,7 +3080,8 @@ export function ContentPage() {
</SelectTrigger>
<SelectContent className="bg-[#0f2137] border-gray-700 text-white">
<SelectItem value="url"></SelectItem>
<SelectItem value="miniprogram"></SelectItem>
<SelectItem value="miniprogram">API跳转</SelectItem>
<SelectItem value="wxlink"></SelectItem>
<SelectItem value="ckb"></SelectItem>
</SelectContent>
</Select>
@@ -3057,9 +3092,18 @@ export function ContentPage() {
? 'URL地址'
: linkTagForm.type === 'ckb'
? '存客宝计划URL'
: '小程序(选密钥)'}
: linkTagForm.type === 'wxlink'
? '小程序链接'
: '小程序 mpKey / 微信 AppID'}
</Label>
{linkTagForm.type === 'miniprogram' && linkedMps.length > 0 ? (
{linkTagForm.type === 'wxlink' ? (
<Input
className="bg-[#0a1628] border-gray-700 text-white h-8 text-sm"
placeholder="粘贴小程序右上角 ... → 复制链接 得到的 URL"
value={linkTagForm.url}
onChange={(e) => setLinkTagForm((p) => ({ ...p, url: e.target.value }))}
/>
) : linkTagForm.type === 'miniprogram' && linkedMps.length > 0 ? (
<div ref={mpDropdownRef} className="relative">
<Input
className="bg-[#0a1628] border-gray-700 text-white h-8 text-sm"
@@ -3110,7 +3154,7 @@ export function ContentPage() {
? 'https://...'
: linkTagForm.type === 'ckb'
? 'https://ckbapi.quwanzhi.com/...'
: '关联小程序的32位密钥'
: '关联配置的 key或直接填 wx 开头的 AppID'
}
value={linkTagForm.type === 'url' || linkTagForm.type === 'ckb' ? linkTagForm.url : linkTagForm.appId}
onChange={(e) => {
@@ -3123,16 +3167,38 @@ export function ContentPage() {
</div>
</div>
{linkTagForm.type === 'wxlink' && (
<p className="text-[11px] text-amber-400/80 leading-snug px-0.5">
... web-view
</p>
)}
{linkTagForm.type === 'miniprogram' && (
<div className="space-y-1">
<Label className="text-gray-300 text-sm"></Label>
<Input
className="bg-[#0a1628] border-gray-700 text-white h-8 text-sm font-mono"
placeholder="pages/index/index"
value={linkTagForm.pagePath}
onChange={(e) => setLinkTagForm((p) => ({ ...p, pagePath: e.target.value }))}
/>
</div>
<>
<div className="space-y-1">
<Label className="text-gray-300 text-sm"></Label>
<Input
className="bg-[#0a1628] border-gray-700 text-white h-8 text-sm font-mono"
placeholder="pages/index/index"
value={linkTagForm.pagePath}
onChange={(e) => setLinkTagForm((p) => ({ ...p, pagePath: e.target.value }))}
/>
</div>
<div className="space-y-1">
<Label className="text-gray-300 text-sm">AppSecret · </Label>
<Input
type="password"
autoComplete="new-password"
className="bg-[#0a1628] border-gray-700 text-white h-8 text-sm font-mono"
placeholder={linkTagEditing?.hasAppSecret ? '已保存密钥,留空不改;填写则覆盖' : '粘贴目标小程序 AppSecret'}
value={linkTagForm.appSecret}
onChange={(e) => setLinkTagForm((p) => ({ ...p, appSecret: e.target.value }))}
/>
<p className="text-[11px] text-gray-500 leading-snug">
AppID
</p>
</div>
</>
)}
</div>
@@ -3149,13 +3215,18 @@ export function ContentPage() {
url: linkTagForm.url.trim(),
type: linkTagForm.type,
appId: linkTagForm.appId.trim(),
appSecret: linkTagForm.appSecret.trim(),
pagePath: linkTagForm.pagePath.trim(),
}
// 新增:允许留空后端自动生成;编辑tagId 已锁定
// 留空后端自动生成;自定义时与库一致150 字符,勿含 #、逗号、换行
if (payload.tagId) {
const ok = /^\d{12}$/.test(payload.tagId) || /^z[a-z0-9]{11}$/.test(payload.tagId)
if (!ok) {
toast.error('标签ID需为12位数字或 z 开头的12位z+11位小写字母数字')
const id = payload.tagId
if ([...id].length > 50) {
toast.error('标签ID 最长 50 个字符')
return
}
if (/[#,\n\r\t]/.test(id)) {
toast.error('标签ID 不能含 #、逗号或换行')
return
}
}
@@ -3164,6 +3235,7 @@ export function ContentPage() {
return
}
if (payload.type === 'miniprogram') payload.url = ''
if (payload.type === 'wxlink') { payload.appId = ''; payload.pagePath = '' }
setLinkTagSaving(true)
try {
const res = await post<{ success?: boolean; error?: string }>('/api/db/link-tags', payload)