- {linkTags.map(t => (
+ {linkTags.map((t) => (
-
#{t.label}
-
+
+
{t.type === 'url' ? '网页' : t.type === 'ckb' ? '存客宝' : '小程序'}
-
- {t.url}
-
+ {t.url && (
+
+ {t.url}
+
+ )}
+
+
+
+
-
))}
{linkTags.length === 0 &&
暂无链接标签,添加后可在编辑器中使用 #标签 跳转
}
diff --git a/soul-admin/src/pages/users/UsersPage.tsx b/soul-admin/src/pages/users/UsersPage.tsx
index 589e9894..106d4ea4 100644
--- a/soul-admin/src/pages/users/UsersPage.tsx
+++ b/soul-admin/src/pages/users/UsersPage.tsx
@@ -1,4 +1,4 @@
-import toast from '@/utils/toast'
+import toast from '@/utils/toast'
import { useState, useEffect, useCallback } from 'react'
import { Card, CardContent } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
@@ -43,6 +43,7 @@ import {
ChevronDown,
ChevronUp,
Crown,
+ Tag,
} from 'lucide-react'
import { UserDetailModal } from '@/components/modules/user/UserDetailModal'
import { Pagination } from '@/components/ui/Pagination'
@@ -81,11 +82,12 @@ interface UserRule {
createdAt?: string
}
-interface VipRole {
- id: number
+interface VipMember {
+ id: string
name: string
- sort: number
- createdAt?: string
+ avatar?: string | null
+ vipRole?: string | null
+ vipSort?: number | null
}
// 用户旅程阶段定义
@@ -144,12 +146,11 @@ export function UsersPage() {
const [editingRule, setEditingRule] = useState
(null)
const [ruleForm, setRuleForm] = useState({ title: '', description: '', trigger: '', sort: 0, enabled: true })
- // ===== VIP 角色 =====
- const [vipRoles, setVipRoles] = useState([])
- const [vipRolesLoading, setVipRolesLoading] = useState(false)
- const [showVipRoleModal, setShowVipRoleModal] = useState(false)
- const [editingVipRole, setEditingVipRole] = useState(null)
- const [vipRoleForm, setVipRoleForm] = useState({ name: '', sort: 0 })
+ // ===== 超级个体(VIP 用户列表) =====
+ const [vipMembers, setVipMembers] = useState([])
+ const [vipMembersLoading, setVipMembersLoading] = useState(false)
+ const [draggingVipId, setDraggingVipId] = useState(null)
+ const [dragOverVipId, setDragOverVipId] = useState(null)
// ===== 用户旅程总览 =====
const [journeyStats, setJourneyStats] = useState>({})
@@ -313,36 +314,169 @@ export function UsersPage() {
try { await put('/api/db/user-rules', { id: rule.id, enabled: !rule.enabled }); loadRules() } catch { }
}
- // ===== VIP 角色 =====
- const loadVipRoles = useCallback(async () => {
- setVipRolesLoading(true)
+ // ===== 超级个体(VIP 用户列表) =====
+ const loadVipMembers = useCallback(async () => {
+ setVipMembersLoading(true)
try {
- const data = await get<{ success?: boolean; roles?: VipRole[] }>('/api/db/vip-roles')
- if (data?.success) setVipRoles(data.roles || [])
- } catch { } finally { setVipRolesLoading(false) }
+ const data = await get<{ success?: boolean; data?: VipMember[]; error?: string }>(
+ '/api/db/vip-members?limit=500',
+ )
+ if (data?.success && data.data) {
+ const list = [...data.data].map((m, idx) => ({
+ ...m,
+ vipSort: typeof (m as any).vipSort === 'number' ? (m as any).vipSort : idx + 1,
+ }))
+ list.sort((a, b) => (a.vipSort ?? 999999) - (b.vipSort ?? 999999))
+ setVipMembers(list)
+ } else if (data && data.error) {
+ toast.error(data.error)
+ }
+ } catch {
+ toast.error('加载超级个体列表失败')
+ } finally {
+ setVipMembersLoading(false)
+ }
}, [])
- async function handleSaveVipRole() {
- if (!vipRoleForm.name) { toast.error('请填写角色名称'); return }
- setIsSaving(true)
- try {
- if (editingVipRole) {
- const data = await put<{ success?: boolean; error?: string }>('/api/db/vip-roles', { id: editingVipRole.id, ...vipRoleForm })
- if (!data?.success) { toast.error('更新失败'); return }
- } else {
- const data = await post<{ success?: boolean; error?: string }>('/api/db/vip-roles', vipRoleForm)
- if (!data?.success) { toast.error('创建失败'); return }
- }
- setShowVipRoleModal(false); loadVipRoles()
- } catch { toast.error('保存失败') } finally { setIsSaving(false) }
+ const [showVipRoleModal, setShowVipRoleModal] = useState(false)
+ const [vipRoleModalMember, setVipRoleModalMember] = useState(null)
+ const [vipRoleInput, setVipRoleInput] = useState('')
+ const [vipRoleSaving, setVipRoleSaving] = useState(false)
+
+ const VIP_ROLE_PRESETS = ['创业者', '资源整合者', '技术达人', '投资人', '产品经理', '流量操盘手']
+
+ const openVipRoleModal = (member: VipMember) => {
+ setVipRoleModalMember(member)
+ setVipRoleInput(member.vipRole || '')
+ setShowVipRoleModal(true)
}
- async function handleDeleteVipRole(id: number) {
- if (!confirm('确定删除?')) return
+ const handleSetVipRole = async (value: string) => {
+ const trimmed = value.trim()
+ if (!vipRoleModalMember) return
+ if (!trimmed) {
+ toast.error('请选择或输入标签')
+ return
+ }
+ setVipRoleSaving(true)
try {
- const data = await del<{ success?: boolean }>(`/api/db/vip-roles?id=${id}`)
- if (data?.success) loadVipRoles()
- } catch { }
+ const res = await put<{ success?: boolean; error?: string }>('/api/db/users', {
+ id: vipRoleModalMember.id,
+ vipRole: trimmed,
+ })
+ if (!res?.success) {
+ toast.error(res?.error || '更新超级个体标签失败')
+ return
+ }
+ toast.success('已更新超级个体标签')
+ setShowVipRoleModal(false)
+ setVipRoleModalMember(null)
+ await loadVipMembers()
+ } catch {
+ toast.error('更新超级个体标签失败')
+ } finally {
+ setVipRoleSaving(false)
+ }
+ }
+
+ const [showVipSortModal, setShowVipSortModal] = useState(false)
+ const [vipSortModalMember, setVipSortModalMember] = useState(null)
+ const [vipSortInput, setVipSortInput] = useState('')
+ const [vipSortSaving, setVipSortSaving] = useState(false)
+
+ const openVipSortModal = (member: VipMember) => {
+ setVipSortModalMember(member)
+ setVipSortInput(member.vipSort != null ? String(member.vipSort) : '')
+ setShowVipSortModal(true)
+ }
+
+ const handleSetVipSort = async () => {
+ if (!vipSortModalMember) return
+ const num = Number(vipSortInput)
+ if (!Number.isFinite(num)) {
+ toast.error('请输入有效的数字序号')
+ return
+ }
+ setVipSortSaving(true)
+ try {
+ const res = await put<{ success?: boolean; error?: string }>('/api/db/users', {
+ id: vipSortModalMember.id,
+ vipSort: num,
+ })
+ if (!res?.success) {
+ toast.error(res?.error || '更新排序序号失败')
+ return
+ }
+ toast.success('已更新排序序号')
+ setShowVipSortModal(false)
+ setVipSortModalMember(null)
+ await loadVipMembers()
+ } catch {
+ toast.error('更新排序序号失败')
+ } finally {
+ setVipSortSaving(false)
+ }
+ }
+
+ const handleVipRowDragStart = (e: React.DragEvent, id: string) => {
+ e.dataTransfer.effectAllowed = 'move'
+ e.dataTransfer.setData('text/plain', id)
+ setDraggingVipId(id)
+ }
+
+ const handleVipRowDragOver = (e: React.DragEvent, id: string) => {
+ e.preventDefault()
+ if (dragOverVipId !== id) setDragOverVipId(id)
+ }
+
+ const handleVipRowDragEnd = () => {
+ setDraggingVipId(null)
+ setDragOverVipId(null)
+ }
+
+ const handleVipRowDrop = async (e: React.DragEvent, targetId: string) => {
+ e.preventDefault()
+ const fromId = e.dataTransfer.getData('text/plain') || draggingVipId
+ setDraggingVipId(null)
+ setDragOverVipId(null)
+ if (!fromId || fromId === targetId) return
+
+ const fromMember = vipMembers.find((m) => m.id === fromId)
+ const targetMember = vipMembers.find((m) => m.id === targetId)
+ if (!fromMember || !targetMember) return
+
+ const fromSort = fromMember.vipSort ?? vipMembers.findIndex((m) => m.id === fromId) + 1
+ const targetSort = targetMember.vipSort ?? vipMembers.findIndex((m) => m.id === targetId) + 1
+
+ // 本地先交换顺序,提升交互流畅度
+ setVipMembers((prev) => {
+ const list = [...prev]
+ const fromIdx = list.findIndex((m) => m.id === fromId)
+ const toIdx = list.findIndex((m) => m.id === targetId)
+ if (fromIdx === -1 || toIdx === -1) return prev
+ const next = [...list]
+ const [m1, m2] = [next[fromIdx], next[toIdx]]
+ next[fromIdx] = { ...m2, vipSort: fromSort }
+ next[toIdx] = { ...m1, vipSort: targetSort }
+ return next
+ })
+
+ try {
+ const [res1, res2] = await Promise.all([
+ put<{ success?: boolean; error?: string }>('/api/db/users', { id: fromId, vipSort: targetSort }),
+ put<{ success?: boolean; error?: string }>('/api/db/users', { id: targetId, vipSort: fromSort }),
+ ])
+ if (!res1?.success || !res2?.success) {
+ toast.error(res1?.error || res2?.error || '更新排序失败')
+ await loadVipMembers()
+ return
+ }
+ toast.success('已更新排序')
+ await loadVipMembers()
+ } catch {
+ toast.error('更新排序失败')
+ await loadVipMembers()
+ }
}
// ===== 用户旅程总览 =====
@@ -381,8 +515,8 @@ export function UsersPage() {
规则配置
-
- VIP 角色
+
+ 超级个体列表
@@ -707,46 +841,136 @@ export function UsersPage() {
)}
- {/* ===== VIP 角色 ===== */}
+ {/* ===== 超级个体列表(VIP 用户) ===== */}
-
管理用户 VIP 角色分类,这些角色将在用户详情和会员展示中使用
+
+
+ 展示当前所有有效的超级个体(VIP 用户),用于检查会员信息与排序值。
+
+
+ 提示:按住任意一行即可拖拽排序,释放后将同步更新小程序展示顺序。
+
+
-
-
- {vipRolesLoading ? (
-
- ) : vipRoles.length === 0 ? (
+ {vipMembersLoading ? (
+
+
+ 加载中...
+
+ ) : vipMembers.length === 0 ? (
-
暂无 VIP 角色
-
{ setEditingVipRole(null); setVipRoleForm({ name: '', sort: 0 }); setShowVipRoleModal(true) }} className="bg-[#38bdac] hover:bg-[#2da396] text-white"> 添加第一个角色
+
当前没有有效的超级个体用户。
) : (
-
- {vipRoles.map((role) => (
-
-
-
-
- {role.name}
-
-
- { setEditingVipRole(role); setVipRoleForm({ name: role.name, sort: role.sort }); setShowVipRoleModal(true) }} className="text-gray-500 hover:text-[#38bdac]">
- handleDeleteVipRole(role.id)} className="text-gray-500 hover:text-red-400">
-
-
-
排序: {role.sort}
-
- ))}
-
+
+
+
+
+
+ 序号
+ 成员
+ 超级个体标签
+ 排序值
+ 操作
+
+
+
+ {vipMembers.map((m, index) => {
+ const isDragging = draggingVipId === m.id
+ const isOver = dragOverVipId === m.id
+ return (
+ handleVipRowDragStart(e, m.id)}
+ onDragOver={(e) => handleVipRowDragOver(e, m.id)}
+ onDrop={(e) => handleVipRowDrop(e, m.id)}
+ onDragEnd={handleVipRowDragEnd}
+ className={`border-gray-700/50 cursor-grab active:cursor-grabbing select-none ${
+ isDragging ? 'opacity-60' : ''
+ } ${isOver ? 'bg-[#38bdac]/10' : ''}`}
+ >
+ {index + 1}
+
+
+ {m.avatar ? (
+ // eslint-disable-next-line jsx-a11y/alt-text
+

+ ) : (
+
+ {m.name?.[0] || '创'}
+
+ )}
+
+
+
+
+ {m.vipRole || (未设置超级个体标签)}
+
+
+ {m.vipSort ?? index + 1}
+
+
+
+
openVipRoleModal(m)}
+ title="设置超级个体标签"
+ >
+
+
+
{
+ setSelectedUserIdForDetail(m.id)
+ setShowDetailModal(true)
+ }}
+ title="编辑资料"
+ >
+
+
+
openVipSortModal(m)}
+ title="设置排序序号"
+ >
+
+
+
+
+
+ )})}
+
+
+
+
)}
@@ -754,6 +978,81 @@ export function UsersPage() {
{/* ===== 弹框组件 ===== */}
{/* 添加/编辑用户 */}
+ {/* 设置排序 */}
+
+
+ {/* 设置超级个体标签 */}
+
+
- {/* 添加/编辑 VIP 角色 */}
-
-
{/* 绑定关系 */}