feat(admin): 富文本与内容管理页调整;链接标签模型与 db 同步;更新 dist
Made-with: Cursor
This commit is contained in:
File diff suppressed because one or more lines are too long
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 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>
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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 或微信 AppID;AppSecret 仅存服务端(不下发小程序),供后续开放接口与台账使用。
|
||||
</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 已锁定
|
||||
// 留空则后端自动生成;自定义时与库一致:1~50 字符,勿含 #、逗号、换行
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user