Add linked mini program functionality and enhance link tag handling
- Introduced `navigateToMiniProgramAppIdList` in app.json for mini program navigation. - Updated link tag handling in the read page to support mini program keys and app IDs. - Enhanced content parsing to include app ID and mini program key in link tags. - Added linked mini programs management in the admin panel with API endpoints for CRUD operations. - Improved UI for selecting linked mini programs in the content creation page.
This commit is contained in:
@@ -121,6 +121,8 @@ const LinkTagExtension = Node.create({
|
||||
tagType: { default: 'url', parseHTML: (el: HTMLElement) => el.getAttribute('data-tag-type') || 'url' },
|
||||
tagId: { default: '', parseHTML: (el: HTMLElement) => el.getAttribute('data-tag-id') || '' },
|
||||
pagePath: { default: '', parseHTML: (el: HTMLElement) => el.getAttribute('data-page-path') || '' },
|
||||
appId: { default: '', parseHTML: (el: HTMLElement) => el.getAttribute('data-app-id') || '' },
|
||||
mpKey: { default: '', parseHTML: (el: HTMLElement) => el.getAttribute('data-mp-key') || '' },
|
||||
}
|
||||
},
|
||||
|
||||
@@ -131,6 +133,8 @@ const LinkTagExtension = Node.create({
|
||||
tagType: el.getAttribute('data-tag-type') || 'url',
|
||||
tagId: el.getAttribute('data-tag-id') || '',
|
||||
pagePath: el.getAttribute('data-page-path')|| '',
|
||||
appId: el.getAttribute('data-app-id') || '',
|
||||
mpKey: el.getAttribute('data-mp-key') || '',
|
||||
}) }]
|
||||
},
|
||||
|
||||
@@ -142,6 +146,8 @@ const LinkTagExtension = Node.create({
|
||||
'data-tag-type': node.attrs.tagType,
|
||||
'data-tag-id': node.attrs.tagId,
|
||||
'data-page-path': node.attrs.pagePath,
|
||||
'data-app-id': node.attrs.appId || '',
|
||||
'data-mp-key': node.attrs.mpKey || node.attrs.appId || '',
|
||||
class: 'link-tag-node',
|
||||
}), `#${node.attrs.label}`]
|
||||
},
|
||||
@@ -297,6 +303,8 @@ const RichEditor = forwardRef<RichEditorRef, RichEditorProps>(({
|
||||
tagType: tag.type || 'url',
|
||||
tagId: tag.id || '',
|
||||
pagePath: tag.pagePath || '',
|
||||
appId: tag.appId || '',
|
||||
mpKey: tag.type === 'miniprogram' ? (tag.appId || '') : '',
|
||||
},
|
||||
}).run()
|
||||
}, [editor])
|
||||
|
||||
@@ -446,6 +446,30 @@ export function ContentPage() {
|
||||
}
|
||||
}, [])
|
||||
|
||||
const [linkedMps, setLinkedMps] = useState<{ key: string; name: string; appId: string; path?: string }[]>([])
|
||||
const [mpSearchQuery, setMpSearchQuery] = useState('')
|
||||
const [mpDropdownOpen, setMpDropdownOpen] = useState(false)
|
||||
const mpDropdownRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const loadLinkedMps = useCallback(async () => {
|
||||
try {
|
||||
const res = await get<{ success?: boolean; data?: { key: string; id?: string; name: string; appId: string; path?: string }[] }>(
|
||||
'/api/admin/linked-miniprograms',
|
||||
)
|
||||
if (res?.success && Array.isArray(res.data)) {
|
||||
setLinkedMps(res.data.map((m) => ({ ...m, key: m.key || m.id || '' })))
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}, [])
|
||||
|
||||
const filteredLinkedMps = linkedMps.filter(
|
||||
(m) =>
|
||||
!mpSearchQuery.trim() ||
|
||||
m.name.toLowerCase().includes(mpSearchQuery.toLowerCase()) ||
|
||||
(m.key && m.key.toLowerCase().includes(mpSearchQuery.toLowerCase())) ||
|
||||
m.appId.toLowerCase().includes(mpSearchQuery.toLowerCase()),
|
||||
)
|
||||
|
||||
const handleTogglePin = async (sectionId: string) => {
|
||||
const next = pinnedSectionIds.includes(sectionId)
|
||||
? pinnedSectionIds.filter((id) => id !== sectionId)
|
||||
@@ -487,7 +511,13 @@ export function ContentPage() {
|
||||
} catch { toast.error('保存失败') } finally { setPreviewPercentSaving(false) }
|
||||
}
|
||||
|
||||
useEffect(() => { loadPinnedSections(); loadPreviewPercent(); loadPersons(); loadLinkTags() }, [loadPinnedSections, loadPreviewPercent, loadPersons, loadLinkTags])
|
||||
useEffect(() => {
|
||||
loadPinnedSections()
|
||||
loadPreviewPercent()
|
||||
loadPersons()
|
||||
loadLinkTags()
|
||||
loadLinkedMps()
|
||||
}, [loadPinnedSections, loadPreviewPercent, loadPersons, loadLinkTags, loadLinkedMps])
|
||||
|
||||
const handleShowSectionOrders = async (section: Section & { filePath?: string }) => {
|
||||
setSectionOrdersModal({ section, orders: [] })
|
||||
@@ -2239,15 +2269,15 @@ export function ContentPage() {
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div className="flex gap-2 items-end flex-wrap">
|
||||
<div className="space-y-1">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-400 text-xs">标签ID</Label>
|
||||
<Input className="bg-[#0a1628] border-gray-700 text-white h-8 w-24" placeholder="如 team01" value={newLinkTag.tagId} onChange={e => setNewLinkTag({ ...newLinkTag, tagId: e.target.value })} />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-400 text-xs">显示文字</Label>
|
||||
<Input className="bg-[#0a1628] border-gray-700 text-white h-8 w-28" placeholder="如 神仙团队" value={newLinkTag.label} onChange={e => setNewLinkTag({ ...newLinkTag, label: e.target.value })} />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-400 text-xs">类型</Label>
|
||||
<Select value={newLinkTag.type} onValueChange={v => setNewLinkTag({ ...newLinkTag, type: v as 'url' | 'miniprogram' | 'ckb' })}>
|
||||
<SelectTrigger className="bg-[#0a1628] border-gray-700 text-white h-8 w-24">
|
||||
@@ -2260,17 +2290,67 @@ export function ContentPage() {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-400 text-xs">
|
||||
{newLinkTag.type === 'url' ? 'URL地址' : newLinkTag.type === 'ckb' ? '存客宝计划URL' : '小程序AppID'}
|
||||
{newLinkTag.type === 'url' ? 'URL地址' : newLinkTag.type === 'ckb' ? '存客宝计划URL' : '小程序(选密钥)'}
|
||||
</Label>
|
||||
<Input className="bg-[#0a1628] border-gray-700 text-white h-8 w-44" placeholder={newLinkTag.type === 'url' ? 'https://...' : newLinkTag.type === 'ckb' ? 'https://ckbapi.quwanzhi.com/...' : 'wx...'} value={newLinkTag.type === 'url' || newLinkTag.type === 'ckb' ? newLinkTag.url : newLinkTag.appId} onChange={e => {
|
||||
if (newLinkTag.type === 'url' || newLinkTag.type === 'ckb') setNewLinkTag({ ...newLinkTag, url: e.target.value })
|
||||
else setNewLinkTag({ ...newLinkTag, appId: e.target.value })
|
||||
}} />
|
||||
{newLinkTag.type === 'miniprogram' && linkedMps.length > 0 ? (
|
||||
<div ref={mpDropdownRef} className="relative w-44">
|
||||
<Input
|
||||
className="bg-[#0a1628] border-gray-700 text-white h-8 w-44"
|
||||
placeholder="搜索名称或密钥"
|
||||
value={mpDropdownOpen ? mpSearchQuery : newLinkTag.appId}
|
||||
onChange={(e) => {
|
||||
const v = e.target.value
|
||||
setMpSearchQuery(v)
|
||||
setMpDropdownOpen(true)
|
||||
if (!linkedMps.some((m) => m.key === v)) setNewLinkTag({ ...newLinkTag, appId: v })
|
||||
}}
|
||||
onFocus={() => {
|
||||
setMpSearchQuery(newLinkTag.appId)
|
||||
setMpDropdownOpen(true)
|
||||
}}
|
||||
onBlur={() => setTimeout(() => setMpDropdownOpen(false), 150)}
|
||||
/>
|
||||
{mpDropdownOpen && (
|
||||
<div className="absolute top-full left-0 right-0 mt-1 max-h-48 overflow-y-auto rounded-md border border-gray-700 bg-[#0a1628] shadow-lg z-50">
|
||||
{filteredLinkedMps.length === 0 ? (
|
||||
<div className="px-3 py-2 text-gray-500 text-xs">无匹配,可手动输入密钥</div>
|
||||
) : (
|
||||
filteredLinkedMps.map((m) => (
|
||||
<button
|
||||
key={m.key}
|
||||
type="button"
|
||||
className="w-full px-3 py-2 text-left text-sm text-white hover:bg-[#38bdac]/20 flex flex-col gap-0.5"
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault()
|
||||
setNewLinkTag({ ...newLinkTag, appId: m.key, pagePath: m.path || '' })
|
||||
setMpSearchQuery('')
|
||||
setMpDropdownOpen(false)
|
||||
}}
|
||||
>
|
||||
<span>{m.name}</span>
|
||||
<span className="text-xs text-gray-400 font-mono">{m.key}</span>
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<Input
|
||||
className="bg-[#0a1628] border-gray-700 text-white h-8 w-44"
|
||||
placeholder={newLinkTag.type === 'url' ? 'https://...' : newLinkTag.type === 'ckb' ? 'https://ckbapi.quwanzhi.com/...' : '关联小程序的32位密钥'}
|
||||
value={newLinkTag.type === 'url' || newLinkTag.type === 'ckb' ? newLinkTag.url : newLinkTag.appId}
|
||||
onChange={(e) => {
|
||||
if (newLinkTag.type === 'url' || newLinkTag.type === 'ckb') setNewLinkTag({ ...newLinkTag, url: e.target.value })
|
||||
else setNewLinkTag({ ...newLinkTag, appId: e.target.value })
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{newLinkTag.type === 'miniprogram' && (
|
||||
<div className="space-y-1">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-400 text-xs">页面路径</Label>
|
||||
<Input className="bg-[#0a1628] border-gray-700 text-white h-8 w-36" placeholder="pages/index/index" value={newLinkTag.pagePath} onChange={e => setNewLinkTag({ ...newLinkTag, pagePath: e.target.value })} />
|
||||
</div>
|
||||
@@ -2284,6 +2364,7 @@ export function ContentPage() {
|
||||
return
|
||||
}
|
||||
const payload = { ...newLinkTag }
|
||||
if (payload.type === 'miniprogram') payload.url = ''
|
||||
await post('/api/db/link-tags', payload)
|
||||
setNewLinkTag({ tagId: '', label: '', url: '', type: 'url', appId: '', pagePath: '' })
|
||||
setEditingLinkTagId(null)
|
||||
@@ -2325,7 +2406,9 @@ export function ContentPage() {
|
||||
>
|
||||
{t.type === 'url' ? '网页' : t.type === 'ckb' ? '存客宝' : '小程序'}
|
||||
</Badge>
|
||||
{t.url && (
|
||||
{t.type === 'miniprogram' ? (
|
||||
<span className="text-gray-400 text-xs font-mono">{t.appId} {t.pagePath ? `· ${t.pagePath}` : ''}</span>
|
||||
) : t.url ? (
|
||||
<a
|
||||
href={t.url}
|
||||
target="_blank"
|
||||
@@ -2334,7 +2417,7 @@ export function ContentPage() {
|
||||
>
|
||||
{t.url} <ExternalLink className="w-3 h-3 shrink-0" />
|
||||
</a>
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
|
||||
283
soul-admin/src/pages/linked-mp/LinkedMpPage.tsx
Normal file
283
soul-admin/src/pages/linked-mp/LinkedMpPage.tsx
Normal file
@@ -0,0 +1,283 @@
|
||||
import toast from '@/utils/toast'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import { Smartphone, Plus, Pencil, Trash2 } from 'lucide-react'
|
||||
import { get, post, put, del } from '@/api/client'
|
||||
|
||||
interface LinkedMpItem {
|
||||
key: string
|
||||
id?: string // 兼容旧数据
|
||||
name: string
|
||||
appId: string
|
||||
path?: string
|
||||
sort?: number
|
||||
}
|
||||
|
||||
export function LinkedMpPage() {
|
||||
const [list, setList] = useState<LinkedMpItem[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [modalOpen, setModalOpen] = useState(false)
|
||||
const [editing, setEditing] = useState<LinkedMpItem | null>(null)
|
||||
const [form, setForm] = useState({ name: '', appId: '', path: '', sort: 0 })
|
||||
const [saving, setSaving] = useState(false)
|
||||
|
||||
async function loadList() {
|
||||
setLoading(true)
|
||||
try {
|
||||
const res = await get<{ success?: boolean; data?: LinkedMpItem[] }>(
|
||||
'/api/admin/linked-miniprograms',
|
||||
)
|
||||
if (res?.success && Array.isArray(res.data)) {
|
||||
const sorted = [...res.data].sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0))
|
||||
setList(sorted)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Load linked miniprograms error:', e)
|
||||
toast.error('加载失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadList()
|
||||
}, [])
|
||||
|
||||
function openAdd() {
|
||||
setEditing(null)
|
||||
setForm({ name: '', appId: '', path: '', sort: list.length })
|
||||
setModalOpen(true)
|
||||
}
|
||||
|
||||
function openEdit(item: LinkedMpItem) {
|
||||
setEditing(item)
|
||||
setForm({
|
||||
name: item.name,
|
||||
appId: item.appId,
|
||||
path: item.path ?? '',
|
||||
sort: item.sort ?? 0,
|
||||
})
|
||||
setModalOpen(true)
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
const name = form.name.trim()
|
||||
const appId = form.appId.trim()
|
||||
if (!name || !appId) {
|
||||
toast.error('请填写小程序名称和 AppID')
|
||||
return
|
||||
}
|
||||
setSaving(true)
|
||||
try {
|
||||
if (editing) {
|
||||
const res = await put<{ success?: boolean; error?: string }>(
|
||||
'/api/admin/linked-miniprograms',
|
||||
{ key: editing.key || editing.id, name, appId, path: form.path.trim(), sort: form.sort },
|
||||
)
|
||||
if (res?.success) {
|
||||
toast.success('已更新')
|
||||
setModalOpen(false)
|
||||
loadList()
|
||||
} else {
|
||||
toast.error(res?.error ?? '更新失败')
|
||||
}
|
||||
} else {
|
||||
const res = await post<{ success?: boolean; error?: string }>(
|
||||
'/api/admin/linked-miniprograms',
|
||||
{ name, appId, path: form.path.trim(), sort: form.sort },
|
||||
)
|
||||
if (res?.success) {
|
||||
toast.success('已添加')
|
||||
setModalOpen(false)
|
||||
loadList()
|
||||
} else {
|
||||
toast.error(res?.error ?? '添加失败')
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
toast.error('操作失败')
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(item: LinkedMpItem) {
|
||||
if (!confirm(`确定要删除「${item.name}」吗?`)) return
|
||||
try {
|
||||
const res = await del<{ success?: boolean; error?: string }>(
|
||||
`/api/admin/linked-miniprograms/${item.key || item.id}`,
|
||||
)
|
||||
if (res?.success) {
|
||||
toast.success('已删除')
|
||||
loadList()
|
||||
} else {
|
||||
toast.error(res?.error ?? '删除失败')
|
||||
}
|
||||
} catch (e) {
|
||||
toast.error('删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
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">
|
||||
<Smartphone className="w-5 h-5 text-[#38bdac]" />
|
||||
关联小程序管理
|
||||
</CardTitle>
|
||||
<CardDescription className="text-gray-400">
|
||||
添加后生成 32 位密钥,链接标签选择小程序时存密钥;小程序端点击 #标签 时用密钥查 appId 再跳转。需在 app.json 的 navigateToMiniProgramAppIdList 中配置目标 AppID。
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex justify-end mb-4">
|
||||
<Button
|
||||
onClick={openAdd}
|
||||
className="bg-[#38bdac] hover:bg-[#2da396] text-white"
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
添加关联小程序
|
||||
</Button>
|
||||
</div>
|
||||
{loading ? (
|
||||
<div className="py-12 text-center text-gray-400">加载中...</div>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-[#0a1628] border-gray-700">
|
||||
<TableHead className="text-gray-400">名称</TableHead>
|
||||
<TableHead className="text-gray-400">密钥</TableHead>
|
||||
<TableHead className="text-gray-400">AppID</TableHead>
|
||||
<TableHead className="text-gray-400">路径</TableHead>
|
||||
<TableHead className="text-gray-400 w-24">排序</TableHead>
|
||||
<TableHead className="text-gray-400 w-32">操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{list.map((item) => (
|
||||
<TableRow key={item.key || item.id || item.name} className="border-gray-700/50">
|
||||
<TableCell className="text-white">{item.name}</TableCell>
|
||||
<TableCell className="text-gray-300 font-mono text-xs">{item.key || item.id || '—'}</TableCell>
|
||||
<TableCell className="text-gray-300 font-mono text-sm">{item.appId}</TableCell>
|
||||
<TableCell className="text-gray-400 text-sm">{item.path || '—'}</TableCell>
|
||||
<TableCell className="text-gray-300">{item.sort ?? 0}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-[#38bdac] hover:bg-[#38bdac]/20"
|
||||
onClick={() => openEdit(item)}
|
||||
>
|
||||
<Pencil className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-red-400 hover:bg-red-500/20"
|
||||
onClick={() => handleDelete(item)}
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{list.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} className="text-center py-12 text-gray-500">
|
||||
暂无关联小程序,点击「添加关联小程序」开始配置
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Dialog open={modalOpen} onOpenChange={setModalOpen}>
|
||||
<DialogContent className="bg-[#0f2137] border-gray-700 text-white">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{editing ? '编辑关联小程序' : '添加关联小程序'}</DialogTitle>
|
||||
<DialogDescription className="text-gray-400">
|
||||
填写目标小程序的名称和 AppID,路径可选(为空则打开首页)
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-300">小程序名称</Label>
|
||||
<Input
|
||||
className="bg-[#0a1628] border-gray-700 text-white"
|
||||
placeholder="例如:Soul 创业派对"
|
||||
value={form.name}
|
||||
onChange={(e) => setForm((p) => ({ ...p, name: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-300">AppID</Label>
|
||||
<Input
|
||||
className="bg-[#0a1628] border-gray-700 text-white font-mono"
|
||||
placeholder="例如:wxb8bbb2b10dec74aa"
|
||||
value={form.appId}
|
||||
onChange={(e) => setForm((p) => ({ ...p, appId: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-300">路径(可选)</Label>
|
||||
<Input
|
||||
className="bg-[#0a1628] border-gray-700 text-white"
|
||||
placeholder="例如:pages/index/index"
|
||||
value={form.path}
|
||||
onChange={(e) => setForm((p) => ({ ...p, path: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-gray-300">排序</Label>
|
||||
<Input
|
||||
type="number"
|
||||
className="bg-[#0a1628] border-gray-700 text-white"
|
||||
value={form.sort}
|
||||
onChange={(e) =>
|
||||
setForm((p) => ({ ...p, sort: parseInt(e.target.value, 10) || 0 }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setModalOpen(false)} className="border-gray-600">
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
className="bg-[#38bdac] hover:bg-[#2da396] text-white"
|
||||
>
|
||||
{saving ? '保存中...' : '保存'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useSearchParams } from 'react-router-dom'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import {
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
import { get, post } from '@/api/client'
|
||||
import { AuthorSettingsPage } from '@/pages/author-settings/AuthorSettingsPage'
|
||||
import { AdminUsersPage } from '@/pages/admin-users/AdminUsersPage'
|
||||
import { LinkedMpPage } from '@/pages/linked-mp/LinkedMpPage'
|
||||
|
||||
interface AuthorInfo {
|
||||
name?: string
|
||||
@@ -98,7 +99,7 @@ const defaultFeatures: FeatureConfig = {
|
||||
aboutEnabled: true,
|
||||
}
|
||||
|
||||
const TAB_KEYS = ['system', 'author', 'admin'] as const
|
||||
const TAB_KEYS = ['system', 'author', 'linkedmp', 'admin'] as const
|
||||
type TabKey = (typeof TAB_KEYS)[number]
|
||||
|
||||
export function SettingsPage() {
|
||||
@@ -261,6 +262,13 @@ export function SettingsPage() {
|
||||
<UserCircle className="w-4 h-4 mr-2" />
|
||||
作者详情
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="linkedmp"
|
||||
className="data-[state=active]:bg-[#38bdac]/20 data-[state=active]:text-[#38bdac] text-gray-400 data-[state=active]:font-medium"
|
||||
>
|
||||
<Smartphone className="w-4 h-4 mr-2" />
|
||||
关联小程序
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="admin"
|
||||
className="data-[state=active]:bg-[#38bdac]/20 data-[state=active]:text-[#38bdac] text-gray-400 data-[state=active]:font-medium"
|
||||
@@ -612,6 +620,10 @@ export function SettingsPage() {
|
||||
<AuthorSettingsPage />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="linkedmp" className="mt-0">
|
||||
<LinkedMpPage />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="admin" className="mt-0">
|
||||
<AdminUsersPage />
|
||||
</TabsContent>
|
||||
|
||||
Reference in New Issue
Block a user