diff --git a/frontend/admin-web/src/components/features/notifications/UserTagsTab/UserTagsTab.module.scss b/frontend/admin-web/src/components/features/notifications/UserTagsTab/UserTagsTab.module.scss index 77a20989..a48808f3 100644 --- a/frontend/admin-web/src/components/features/notifications/UserTagsTab/UserTagsTab.module.scss +++ b/frontend/admin-web/src/components/features/notifications/UserTagsTab/UserTagsTab.module.scss @@ -343,3 +343,71 @@ justify-content: flex-end; gap: 12px; } + +// 用户ID输入 +.userIdsInput { + font-family: monospace; + font-size: 13px; + line-height: 1.6; + min-height: 120px; +} + +.formHint { + font-size: 12px; + color: #9ca3af; +} + +// 用户列表 +.usersList { + min-height: 200px; +} + +.usersHeader { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 12px; + font-size: 13px; + color: #6b7280; +} + +.usersTable { + width: 100%; + border-collapse: collapse; + font-size: 13px; + + th, + td { + padding: 10px 12px; + text-align: left; + border-bottom: 1px solid #e5e7eb; + } + + th { + font-weight: 500; + color: #6b7280; + background: #f9fafb; + } + + td { + color: #374151; + } + + tr:hover td { + background: #f9fafb; + } +} + +.removeBtn { + padding: 4px 8px; + font-size: 12px; + color: #ef4444; + background: transparent; + border: 1px solid #fecaca; + border-radius: 4px; + cursor: pointer; + + &:hover { + background: #fef2f2; + } +} diff --git a/frontend/admin-web/src/components/features/notifications/UserTagsTab/UserTagsTab.tsx b/frontend/admin-web/src/components/features/notifications/UserTagsTab/UserTagsTab.tsx index 7a97751e..94414462 100644 --- a/frontend/admin-web/src/components/features/notifications/UserTagsTab/UserTagsTab.tsx +++ b/frontend/admin-web/src/components/features/notifications/UserTagsTab/UserTagsTab.tsx @@ -9,10 +9,12 @@ import { UserTag, TagType, TagValueType, + TagUserItem, TAG_TYPE_OPTIONS, TAG_VALUE_TYPE_OPTIONS, TAG_COLORS, } from '@/services/userTagService'; +import { formatDateTime } from '@/utils/formatters'; import styles from './UserTagsTab.module.scss'; /** @@ -51,6 +53,22 @@ export const UserTagsTab = () => { // 删除确认 const [deleteConfirm, setDeleteConfirm] = useState<{ type: 'category' | 'tag'; id: string } | null>(null); + // 分配用户弹窗状态 + const [showAssignModal, setShowAssignModal] = useState(false); + const [assigningTag, setAssigningTag] = useState(null); + const [assignForm, setAssignForm] = useState({ + userIds: '', + value: '', + reason: '', + }); + const [assigning, setAssigning] = useState(false); + + // 查看用户弹窗状态 + const [showUsersModal, setShowUsersModal] = useState(false); + const [viewingTag, setViewingTag] = useState(null); + const [tagUsers, setTagUsers] = useState([]); + const [loadingUsers, setLoadingUsers] = useState(false); + // 加载数据 const loadData = useCallback(async () => { try { @@ -233,6 +251,90 @@ export const UserTagsTab = () => { } }; + // ===================== + // 分配用户操作 + // ===================== + + const handleOpenAssignModal = (tag: UserTag) => { + setAssigningTag(tag); + setAssignForm({ userIds: '', value: '', reason: '' }); + setShowAssignModal(true); + }; + + const handleAssignUsers = async () => { + if (!assigningTag) return; + + const userIds = assignForm.userIds + .split(/[\n,]/) + .map(id => id.trim()) + .filter(Boolean); + + if (userIds.length === 0) { + toast.error('请输入用户ID'); + return; + } + + try { + setAssigning(true); + const result = await userTagService.batchAssignTag({ + tagId: assigningTag.id, + accountSequences: userIds, + value: assignForm.value.trim() || undefined, + reason: assignForm.reason.trim() || undefined, + }); + + if (result.failed > 0) { + toast.warning(`成功: ${result.success}, 失败: ${result.failed}`); + } else { + toast.success(`成功分配给 ${result.success} 个用户`); + } + + setShowAssignModal(false); + loadData(); // 刷新以更新预估用户数 + } catch (err) { + toast.error((err as Error).message || '分配失败'); + } finally { + setAssigning(false); + } + }; + + // ===================== + // 查看用户操作 + // ===================== + + const handleViewUsers = async (tag: UserTag) => { + setViewingTag(tag); + setShowUsersModal(true); + setLoadingUsers(true); + + try { + const response = await userTagService.getTagUsers(tag.id, { limit: 100 }); + setTagUsers(response.items); + } catch (err) { + toast.error((err as Error).message || '加载用户失败'); + } finally { + setLoadingUsers(false); + } + }; + + const handleRemoveUserTag = async (accountSequence: string) => { + if (!viewingTag) return; + + try { + await userTagService.removeTag({ + accountSequence, + tagId: viewingTag.id, + }); + toast.success('已移除用户标签'); + // 刷新用户列表 + const response = await userTagService.getTagUsers(viewingTag.id, { limit: 100 }); + setTagUsers(response.items); + loadData(); // 刷新以更新预估用户数 + } catch (err) { + toast.error((err as Error).message || '移除失败'); + } + }; + // 获取标签类型显示 const getTagTypeLabel = (type: TagType) => { const opt = TAG_TYPE_OPTIONS.find(o => o.value === type); @@ -314,6 +416,8 @@ export const UserTagsTab = () => { {tag.isAdvertisable && 可投放}
+ +
@@ -497,6 +601,127 @@ export const UserTagsTab = () => { : '确定要删除这个标签吗?相关的用户标签关联也会被删除。'}

+ + {/* 分配用户弹窗 */} + setShowAssignModal(false)} + footer={ +
+ + +
+ } + width={560} + > +
+
+ +