diff --git a/dashboard/src/routes/resource/expression.tsx b/dashboard/src/routes/resource/expression.tsx deleted file mode 100644 index dbb0795c..00000000 --- a/dashboard/src/routes/resource/expression.tsx +++ /dev/null @@ -1,1211 +0,0 @@ -import { MessageSquare, Search, Edit, Trash2, Eye, Plus, Clock, Hash, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, Info, CheckCircle2, XCircle, Circle, ClipboardCheck } from 'lucide-react' -import { useState, useEffect } from 'react' -import { cn } from '@/lib/utils' -import { Button } from '@/components/ui/button' -import { Input } from '@/components/ui/input' -import { Label } from '@/components/ui/label' -import { ScrollArea } from '@/components/ui/scroll-area' -import { useToast } from '@/hooks/use-toast' -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from '@/components/ui/table' -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog' -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from '@/components/ui/alert-dialog' -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '@/components/ui/select' -import { Checkbox } from '@/components/ui/checkbox' -import { Switch } from '@/components/ui/switch' -import { Alert, AlertDescription } from '@/components/ui/alert' -import type { Expression, ExpressionCreateRequest, ExpressionUpdateRequest, ChatInfo } from '@/types/expression' -import { getExpressionList, getExpressionDetail, createExpression, updateExpression, deleteExpression, batchDeleteExpressions, getExpressionStats, getChatList, getReviewStats } from '@/lib/expression-api' -import { ExpressionReviewer } from '@/components/expression-reviewer' - -export function ExpressionManagementPage() { - const [expressions, setExpressions] = useState([]) - const [loading, setLoading] = useState(true) - const [total, setTotal] = useState(0) - const [page, setPage] = useState(1) - const [pageSize, setPageSize] = useState(20) - const [search, setSearch] = useState('') - const [selectedExpression, setSelectedExpression] = useState(null) - const [isDetailDialogOpen, setIsDetailDialogOpen] = useState(false) - const [isEditDialogOpen, setIsEditDialogOpen] = useState(false) - const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false) - const [deleteConfirmExpression, setDeleteConfirmExpression] = useState(null) - const [selectedIds, setSelectedIds] = useState>(new Set()) - const [isBatchDeleteDialogOpen, setIsBatchDeleteDialogOpen] = useState(false) - const [jumpToPage, setJumpToPage] = useState('') - const [stats, setStats] = useState({ total: 0, recent_7days: 0, chat_count: 0, top_chats: {} as Record }) - const [chatList, setChatList] = useState([]) - const [chatNameMap, setChatNameMap] = useState>(new Map()) - const [isReviewerOpen, setIsReviewerOpen] = useState(false) - const [uncheckedCount, setUncheckedCount] = useState(0) - const { toast } = useToast() - - // 加载表达方式列表 - const loadExpressions = async () => { - try { - setLoading(true) - const result = await getExpressionList({ - page, - page_size: pageSize, - search: search || undefined, - }) - if (result.success) { - setExpressions(result.data.data) - setTotal(result.data.total) - } else { - toast({ - title: '加载失败', - description: result.error, - variant: 'destructive', - }) - } - } catch (error) { - toast({ - title: '加载失败', - description: error instanceof Error ? error.message : '无法加载表达方式', - variant: 'destructive', - }) - } finally { - setLoading(false) - } - } - - // 加载统计数据 - const loadStats = async () => { - try { - const result = await getExpressionStats() - if (result.success) { - setStats(result.data) - } else { - console.error('加载统计数据失败:', result.error) - } - } catch (error) { - console.error('加载统计数据失败:', error) - } - } - - // 加载审核统计 - const loadReviewStats = async () => { - try { - const result = await getReviewStats() - if (result.success) { - setUncheckedCount(result.data.unchecked) - } - } catch (error) { - console.error('加载审核统计失败:', error) - } - } - - // 加载聚天列表 - const loadChatList = async () => { - try { - const result = await getChatList() - if (result.success) { - setChatList(result.data) - // 构建聚天ID到名称的映射 - const nameMap = new Map() - result.data.forEach((chat: ChatInfo) => { - nameMap.set(chat.chat_id, chat.chat_name) - }) - setChatNameMap(nameMap) - } - } catch (error) { - console.error('加载聚天列表失败:', error) - } - } - - // 获取聊天名称(支持Unicode字符完整显示) - const getChatName = (chatId: string): string => { - return chatNameMap.get(chatId) || chatId - } - - // 初始加载 - useEffect(() => { - loadExpressions() - loadReviewStats() - loadStats() - loadChatList() - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [page, pageSize, search]) - - // 查看详情 - const handleViewDetail = async (expression: Expression) => { - try { - const result = await getExpressionDetail(expression.id) - if (result.success) { - setSelectedExpression(result.data) - setIsDetailDialogOpen(true) - } else { - toast({ - title: '加载详情失败', - description: result.error, - variant: 'destructive', - }) - } - } catch (error) { - toast({ - title: '加载详情失败', - description: error instanceof Error ? error.message : '无法加载表达方式详情', - variant: 'destructive', - }) - } - } - - // 编辑表达方式 - const handleEdit = (expression: Expression) => { - setSelectedExpression(expression) - setIsEditDialogOpen(true) - } - - // 删除表达方式 - const handleDelete = async (expression: Expression) => { - try { - const result = await deleteExpression(expression.id) - if (result.success) { - toast({ - title: '删除成功', - description: `已删除表达方式: ${expression.situation}`, - }) - setDeleteConfirmExpression(null) - loadExpressions() - loadStats() - } else { - toast({ - title: '删除失败', - description: result.error, - variant: 'destructive', - }) - } - } catch (error) { - toast({ - title: '删除失败', - description: error instanceof Error ? error.message : '无法删除表达方式', - variant: 'destructive', - }) - } - } - - // 切换单个选择 - const toggleSelect = (id: number) => { - const newSelected = new Set(selectedIds) - if (newSelected.has(id)) { - newSelected.delete(id) - } else { - newSelected.add(id) - } - setSelectedIds(newSelected) - } - - // 全选/取消全选 - const toggleSelectAll = () => { - if (selectedIds.size === expressions.length && expressions.length > 0) { - setSelectedIds(new Set()) - } else { - setSelectedIds(new Set(expressions.map(e => e.id))) - } - } - - // 批量删除 - const handleBatchDelete = async () => { - try { - const result = await batchDeleteExpressions(Array.from(selectedIds)) - if (result.success) { - toast({ - title: '批量删除成功', - description: `已删除 ${selectedIds.size} 个表达方式`, - }) - setSelectedIds(new Set()) - setIsBatchDeleteDialogOpen(false) - loadExpressions() - loadStats() - } else { - toast({ - title: '批量删除失败', - description: result.error, - variant: 'destructive', - }) - } - } catch (error) { - toast({ - title: '批量删除失败', - description: error instanceof Error ? error.message : '无法批量删除表达方式', - variant: 'destructive', - }) - } - } - - // 页面跳转 - const handleJumpToPage = () => { - const targetPage = parseInt(jumpToPage) - const totalPages = Math.ceil(total / pageSize) - if (targetPage >= 1 && targetPage <= totalPages) { - setPage(targetPage) - setJumpToPage('') - } else { - toast({ - title: '无效的页码', - description: `请输入1-${totalPages}之间的页码`, - variant: 'destructive', - }) - } - } - - return ( -
- {/* 页面标题 */} -
-
-
-

- - 表达方式管理 -

-

- 管理麦麦的表达方式和话术模板 -

-
-
- - -
-
-
- - -
- - {/* 统计卡片 */} -
-
-
总数量
-
{stats.total}
-
-
-
近7天新增
-
{stats.recent_7days}
-
-
-
关联聊天数
-
{stats.chat_count}
-
-
- - {/* 搜索和批量操作 */} -
- -
-
- - setSearch(e.target.value)} - className="pl-9" - /> -
-
- - {/* 批量操作工具栏 */} -
-
- {selectedIds.size > 0 && ( - 已选择 {selectedIds.size} 个表达方式 - )} -
-
- - - {selectedIds.size > 0 && ( - <> - - - - )} -
-
-
- - {/* 表达方式列表 */} -
- {/* 桌面端表格视图 */} -
- - - - - 0} - onCheckedChange={toggleSelectAll} - /> - - 情境 - 风格 - 聊天 - 操作 - - - - {loading ? ( - - - 加载中... - - - ) : expressions.length === 0 ? ( - - - 暂无数据 - - - ) : ( - expressions.map((expression) => ( - - - toggleSelect(expression.id)} - /> - - - {expression.situation} - - {expression.style} - - - {getChatName(expression.chat_id)} - - - -
- - - -
-
-
- )) - )} -
-
-
- - {/* 移动端卡片视图 */} -
- {loading ? ( -
- 加载中... -
- ) : expressions.length === 0 ? ( -
- 暂无数据 -
- ) : ( - expressions.map((expression) => ( -
- {/* 复选框和情境 */} -
- toggleSelect(expression.id)} - className="mt-1" - /> -
-
-
情境
-

- {expression.situation} -

-
-
-
风格
-

- {expression.style} -

-
-
-
- - {/* 聊天名称 */} -
-
聊天
-

- {getChatName(expression.chat_id)} -

-
- - {/* 操作按钮 */} -
- - - -
-
- )) - )} -
- - {/* 分页 - 增强版 */} - {total > 0 && ( -
-
- 共 {total} 条记录,第 {page} / {Math.ceil(total / pageSize)} 页 -
-
- {/* 首页 */} - - - {/* 上一页 */} - - - {/* 页码跳转 */} -
- setJumpToPage(e.target.value)} - onKeyDown={(e) => e.key === 'Enter' && handleJumpToPage()} - placeholder={page.toString()} - className="w-16 h-8 text-center" - min={1} - max={Math.ceil(total / pageSize)} - /> - -
- - {/* 下一页 */} - - - {/* 末页 */} - -
-
- )} -
- -
-
- - {/* 详情对话框 */} - - - {/* 创建对话框 */} - { - loadExpressions() - loadStats() - setIsCreateDialogOpen(false) - }} - /> - - {/* 编辑对话框 */} - { - loadExpressions() - loadStats() - setIsEditDialogOpen(false) - }} - /> - - {/* 删除确认对话框 */} - setDeleteConfirmExpression(null)} - > - - - 确认删除 - - 确定要删除表达方式 "{deleteConfirmExpression?.situation}" 吗? - 此操作不可撤销。 - - - - 取消 - deleteConfirmExpression && handleDelete(deleteConfirmExpression)} - className="bg-destructive text-destructive-foreground hover:bg-destructive/90" - > - 删除 - - - - - - {/* 批量删除确认对话框 */} - - - {/* 表达方式审核器 */} - { - setIsReviewerOpen(open) - if (!open) { - // 关闭审核器时刷新列表和统计 - loadExpressions() - loadStats() - loadReviewStats() - } - }} - /> -
- ) -} - -// 表达方式详情对话框 -function ExpressionDetailDialog({ - expression, - open, - onOpenChange, - chatNameMap, -}: { - expression: Expression | null - open: boolean - onOpenChange: (open: boolean) => void - chatNameMap: Map -}) { - if (!expression) return null - - const formatTime = (timestamp: number | null) => { - if (!timestamp) return '-' - return new Date(timestamp * 1000).toLocaleString('zh-CN') - } - - const getChatName = (chatId: string): string => { - return chatNameMap.get(chatId) || chatId - } - - return ( - - - - 表达方式详情 - - 查看表达方式的完整信息 - - - -
-
- - - - -
- -
- -
- - {/* 状态标记 */} -
- -
-
-
- {expression.checked ? ( - - ) : ( - - )} -
-
-

已检查

-

- {expression.checked ? "已通过审核" : "未审核"} -

-
-
-
-
- {expression.rejected ? ( - - ) : ( - - )} -
-
-

已拒绝

-

- {expression.rejected ? "不会被使用" : "正常"} -

-
-
-
-
-
- - - - -
-
- ) -} - -// 信息项组件 -function InfoItem({ - icon: Icon, - label, - value, - mono = false, -}: { - icon?: typeof Hash - label: string - value: string | null | undefined - mono?: boolean -}) { - return ( -
- -
- {value || '-'} -
-
- ) -} - -// 表达方式创建对话框 -function ExpressionCreateDialog({ - open, - onOpenChange, - chatList, - onSuccess, -}: { - open: boolean - onOpenChange: (open: boolean) => void - chatList: ChatInfo[] - onSuccess: () => void -}) { - const [formData, setFormData] = useState({ - situation: '', - style: '', - chat_id: '', - }) - const [saving, setSaving] = useState(false) - const { toast } = useToast() - - const handleCreate = async () => { - if (!formData.situation || !formData.style || !formData.chat_id) { - toast({ - title: '验证失败', - description: '请填写必填字段:情境、风格和聚天', - variant: 'destructive', - }) - return - } - - try { - setSaving(true) - const result = await createExpression(formData) - if (result.success) { - toast({ - title: '创建成功', - description: '表达方式已创建', - }) - // 重置表单 - setFormData({ - situation: '', - style: '', - chat_id: '', - }) - onSuccess() - } else { - toast({ - title: '创建失败', - description: result.error, - variant: 'destructive', - }) - } - } catch (error) { - toast({ - title: '创建失败', - description: error instanceof Error ? error.message : '无法创建表达方式', - variant: 'destructive', - }) - } finally { - setSaving(false) - } - } - - return ( - - - - 新增表达方式 - - 创建新的表达方式记录 - - - -
-
-
- - setFormData({ ...formData, situation: e.target.value })} - placeholder="描述使用场景" - /> -
-
- - setFormData({ ...formData, style: e.target.value })} - placeholder="描述表达风格" - /> -
-
- -
- - -
-
- - - - - -
-
- ) -} - -// 表达方式编辑对话框 -function ExpressionEditDialog({ - expression, - open, - onOpenChange, - chatList, - onSuccess, -}: { - expression: Expression | null - open: boolean - onOpenChange: (open: boolean) => void - chatList: ChatInfo[] - onSuccess: () => void -}) { - const [formData, setFormData] = useState({}) - const [saving, setSaving] = useState(false) - const { toast } = useToast() - - useEffect(() => { - if (expression) { - setFormData({ - situation: expression.situation, - style: expression.style, - chat_id: expression.chat_id, - checked: expression.checked, - rejected: expression.rejected, - }) - } - }, [expression]) - - const handleSave = async () => { - if (!expression) return - - try { - setSaving(true) - const result = await updateExpression(expression.id, formData) - if (result.success) { - toast({ - title: '保存成功', - description: '表达方式已更新', - }) - onSuccess() - } else { - toast({ - title: '保存失败', - description: result.error, - variant: 'destructive', - }) - } - } catch (error) { - toast({ - title: '保存失败', - description: error instanceof Error ? error.message : '无法更新表达方式', - variant: 'destructive', - }) - } finally { - setSaving(false) - } - } - - if (!expression) return null - - return ( - - - - 编辑表达方式 - - 修改表达方式的信息 - - - -
-
-
- - setFormData({ ...formData, situation: e.target.value })} - placeholder="描述使用场景" - /> -
-
- - setFormData({ ...formData, style: e.target.value })} - placeholder="描述表达风格" - /> -
-
- -
- - -
- - {/* 状态标记 */} - - - -
-

状态标记说明:

-

• 已检查:表示该表达方式已通过审核(可由AI自动检查或人工审核)

-

• 已拒绝:表示该表达方式被标记为不合适,将永远不会被使用

-

- 根据配置中"仅使用已审核通过的表达方式"设置:
- • 开启时:只有通过审核(已检查)的项目会被使用
- • 关闭时:未审核的项目也会被使用 -

-
-
-
- -
-
-
- -

- 已通过审核 -

-
- setFormData({ ...formData, checked })} - /> -
- -
-
- -

- 不会被使用 -

-
- setFormData({ ...formData, rejected })} - /> -
-
-
- - - - - -
-
- ) -} - -// 批量删除确认对话框 -function BatchDeleteConfirmDialog({ - open, - onOpenChange, - onConfirm, - count, -}: { - open: boolean - onOpenChange: (open: boolean) => void - onConfirm: () => void - count: number -}) { - return ( - - - - 确认批量删除 - - 您即将删除 {count} 个表达方式,此操作无法撤销。确定要继续吗? - - - - 取消 - - 确认删除 - - - - - ) -} diff --git a/dashboard/src/routes/resource/expression/ExpressionDialogs.tsx b/dashboard/src/routes/resource/expression/ExpressionDialogs.tsx new file mode 100644 index 00000000..15f3c955 --- /dev/null +++ b/dashboard/src/routes/resource/expression/ExpressionDialogs.tsx @@ -0,0 +1,561 @@ +import { CheckCircle2, Circle, Clock, Hash, Info, XCircle } from 'lucide-react' +import { useEffect, useState } from 'react' + +import { Alert, AlertDescription } from '@/components/ui/alert' +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '@/components/ui/alert-dialog' +import { Button } from '@/components/ui/button' +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' +import { Switch } from '@/components/ui/switch' +import { useToast } from '@/hooks/use-toast' +import { cn } from '@/lib/utils' + +import { createExpression, updateExpression } from '@/lib/expression-api' + +import type { Expression, ExpressionCreateRequest, ExpressionUpdateRequest, ChatInfo } from '@/types/expression' + +/** + * 表达方式详情对话框 + */ +export function ExpressionDetailDialog({ + expression, + open, + onOpenChange, + chatNameMap, +}: { + expression: Expression | null + open: boolean + onOpenChange: (open: boolean) => void + chatNameMap: Map +}) { + if (!expression) return null + + const formatTime = (timestamp: number | null) => { + if (!timestamp) return '-' + return new Date(timestamp * 1000).toLocaleString('zh-CN') + } + + const getChatName = (chatId: string): string => { + return chatNameMap.get(chatId) || chatId + } + + return ( + + + + 表达方式详情 + + 查看表达方式的完整信息 + + + +
+
+ + + + +
+ +
+ +
+ + {/* 状态标记 */} +
+ +
+
+
+ {expression.checked ? ( + + ) : ( + + )} +
+
+

已检查

+

+ {expression.checked ? "已通过审核" : "未审核"} +

+
+
+
+
+ {expression.rejected ? ( + + ) : ( + + )} +
+
+

已拒绝

+

+ {expression.rejected ? "不会被使用" : "正常"} +

+
+
+
+
+
+ + + + +
+
+ ) +} + +/** + * 信息项组件 + */ +function InfoItem({ + icon: Icon, + label, + value, + mono = false, +}: { + icon?: typeof Hash + label: string + value: string | null | undefined + mono?: boolean +}) { + return ( +
+ +
+ {value || '-'} +
+
+ ) +} + +/** + * 表达方式创建对话框 + */ +export function ExpressionCreateDialog({ + open, + onOpenChange, + chatList, + onSuccess, +}: { + open: boolean + onOpenChange: (open: boolean) => void + chatList: ChatInfo[] + onSuccess: () => void +}) { + const [formData, setFormData] = useState({ + situation: '', + style: '', + chat_id: '', + }) + const [saving, setSaving] = useState(false) + const { toast } = useToast() + + const handleCreate = async () => { + if (!formData.situation || !formData.style || !formData.chat_id) { + toast({ + title: '验证失败', + description: '请填写必填字段:情境、风格和聚天', + variant: 'destructive', + }) + return + } + + try { + setSaving(true) + const result = await createExpression(formData) + if (result.success) { + toast({ + title: '创建成功', + description: '表达方式已创建', + }) + setFormData({ + situation: '', + style: '', + chat_id: '', + }) + onSuccess() + } else { + toast({ + title: '创建失败', + description: result.error, + variant: 'destructive', + }) + } + } catch (error) { + toast({ + title: '创建失败', + description: error instanceof Error ? error.message : '无法创建表达方式', + variant: 'destructive', + }) + } finally { + setSaving(false) + } + } + + return ( + + + + 新增表达方式 + + 创建新的表达方式记录 + + + +
+
+
+ + setFormData({ ...formData, situation: e.target.value })} + placeholder="描述使用场景" + /> +
+
+ + setFormData({ ...formData, style: e.target.value })} + placeholder="描述表达风格" + /> +
+
+ +
+ + +
+
+ + + + + +
+
+ ) +} + +/** + * 表达方式编辑对话框 + */ +export function ExpressionEditDialog({ + expression, + open, + onOpenChange, + chatList, + onSuccess, +}: { + expression: Expression | null + open: boolean + onOpenChange: (open: boolean) => void + chatList: ChatInfo[] + onSuccess: () => void +}) { + const [formData, setFormData] = useState({}) + const [saving, setSaving] = useState(false) + const { toast } = useToast() + + useEffect(() => { + if (expression) { + setFormData({ + situation: expression.situation, + style: expression.style, + chat_id: expression.chat_id, + checked: expression.checked, + rejected: expression.rejected, + }) + } + }, [expression]) + + const handleSave = async () => { + if (!expression) return + + try { + setSaving(true) + const result = await updateExpression(expression.id, formData) + if (result.success) { + toast({ + title: '保存成功', + description: '表达方式已更新', + }) + onSuccess() + } else { + toast({ + title: '保存失败', + description: result.error, + variant: 'destructive', + }) + } + } catch (error) { + toast({ + title: '保存失败', + description: error instanceof Error ? error.message : '无法更新表达方式', + variant: 'destructive', + }) + } finally { + setSaving(false) + } + } + + if (!expression) return null + + return ( + + + + 编辑表达方式 + + 修改表达方式的信息 + + + +
+
+
+ + setFormData({ ...formData, situation: e.target.value })} + placeholder="描述使用场景" + /> +
+
+ + setFormData({ ...formData, style: e.target.value })} + placeholder="描述表达风格" + /> +
+
+ +
+ + +
+ + {/* 状态标记 */} + + + +
+

状态标记说明:

+

• 已检查:表示该表达方式已通过审核(可由AI自动检查或人工审核)

+

• 已拒绝:表示该表达方式被标记为不合适,将永远不会被使用

+

+ 根据配置中"仅使用已审核通过的表达方式"设置:
+ • 开启时:只有通过审核(已检查)的项目会被使用
+ • 关闭时:未审核的项目也会被使用 +

+
+
+
+ +
+
+
+ +

+ 已通过审核 +

+
+ setFormData({ ...formData, checked })} + /> +
+ +
+
+ +

+ 不会被使用 +

+
+ setFormData({ ...formData, rejected })} + /> +
+
+
+ + + + + +
+
+ ) +} + +/** + * 批量删除确认对话框 + */ +export function BatchDeleteConfirmDialog({ + open, + onOpenChange, + onConfirm, + count, +}: { + open: boolean + onOpenChange: (open: boolean) => void + onConfirm: () => void + count: number +}) { + return ( + + + + 确认批量删除 + + 您即将删除 {count} 个表达方式,此操作无法撤销。确定要继续吗? + + + + 取消 + + 确认删除 + + + + + ) +} + +/** + * 单个删除确认对话框 + */ +export function DeleteConfirmDialog({ + expression, + open, + onOpenChange, + onConfirm, +}: { + expression: Expression | null + open: boolean + onOpenChange: (open: boolean) => void + onConfirm: () => Promise +}) { + return ( + + + + 确认删除 + + 确定要删除表达方式 "{expression?.situation}" 吗? + 此操作不可撤销。 + + + + 取消 + + 删除 + + + + + ) +} diff --git a/dashboard/src/routes/resource/expression/ExpressionList.tsx b/dashboard/src/routes/resource/expression/ExpressionList.tsx new file mode 100644 index 00000000..b39d748d --- /dev/null +++ b/dashboard/src/routes/resource/expression/ExpressionList.tsx @@ -0,0 +1,361 @@ +import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, Edit, Eye, Trash2 } from 'lucide-react' +import { useState } from 'react' +import { Button } from '@/components/ui/button' +import { Checkbox } from '@/components/ui/checkbox' +import { Input } from '@/components/ui/input' +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table' +import { useToast } from '@/hooks/use-toast' + +import type { Expression } from '@/types/expression' + +/** + * 表达方式列表组件(桌面端Table + 移动端Card视图 + 分页) + */ +export function ExpressionList({ + expressions, + loading, + total, + page, + pageSize, + selectedIds, + chatNameMap, + onEdit, + onViewDetail, + onDelete, + onToggleSelect, + onToggleSelectAll, + onPageChange, + onJumpToPage, +}: { + expressions: Expression[] + loading: boolean + total: number + page: number + pageSize: number + selectedIds: Set + chatNameMap: Map + onEdit: (expression: Expression) => void + onViewDetail: (expression: Expression) => void + onDelete: (expression: Expression) => void + onToggleSelect: (id: number) => void + onToggleSelectAll: () => void + onPageChange: (newPage: number) => void + onJumpToPage: (targetPage: string) => void +}) { + const { toast } = useToast() + + const getChatName = (chatId: string): string => { + return chatNameMap.get(chatId) || chatId + } + + const totalPages = Math.ceil(total / pageSize) + + const handleJumpToPage = (jumpToPage: string) => { + const targetPage = parseInt(jumpToPage) + if (targetPage >= 1 && targetPage <= totalPages) { + onJumpToPage(jumpToPage) + } else { + toast({ + title: '无效的页码', + description: `请输入1-${totalPages}之间的页码`, + variant: 'destructive', + }) + } + } + + return ( +
+ {/* 桌面端表格视图 */} +
+ + + + + 0} + onCheckedChange={onToggleSelectAll} + /> + + 情境 + 风格 + 聊天 + 操作 + + + + {loading ? ( + + + 加载中... + + + ) : expressions.length === 0 ? ( + + + 暂无数据 + + + ) : ( + expressions.map((expression) => ( + + + onToggleSelect(expression.id)} + /> + + + {expression.situation} + + {expression.style} + + + {getChatName(expression.chat_id)} + + + +
+ + + +
+
+
+ )) + )} +
+
+
+ + {/* 移动端卡片视图 */} +
+ {loading ? ( +
+ 加载中... +
+ ) : expressions.length === 0 ? ( +
+ 暂无数据 +
+ ) : ( + expressions.map((expression) => ( +
+ {/* 复选框和情境 */} +
+ onToggleSelect(expression.id)} + className="mt-1" + /> +
+
+
情境
+

+ {expression.situation} +

+
+
+
风格
+

+ {expression.style} +

+
+
+
+ + {/* 聊天名称 */} +
+
聊天
+

+ {getChatName(expression.chat_id)} +

+
+ + {/* 操作按钮 */} +
+ + + +
+
+ )) + )} +
+ + {/* 分页 */} + {total > 0 && ( + + )} +
+ ) +} + +/** + * 分页组件 + */ +function Pagination({ + total, + page, + pageSize, + onPageChange, + onJumpToPage, +}: { + total: number + page: number + pageSize: number + onPageChange: (newPage: number) => void + onJumpToPage: (targetPage: string) => void +}) { + const [jumpToPage, setJumpToPage] = useState('') + const totalPages = Math.ceil(total / pageSize) + + const handleJump = () => { + if (jumpToPage) { + onJumpToPage(jumpToPage) + setJumpToPage('') + } + } + + return ( +
+
+ 共 {total} 条记录,第 {page} / {totalPages} 页 +
+
+ {/* 首页 */} + + + {/* 上一页 */} + + + {/* 页码跳转 */} +
+ setJumpToPage(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && handleJump()} + placeholder={page.toString()} + className="w-16 h-8 text-center" + min={1} + max={totalPages} + /> + +
+ + {/* 下一页 */} + + + {/* 末页 */} + +
+
+ ) +} + diff --git a/dashboard/src/routes/resource/expression/index.ts b/dashboard/src/routes/resource/expression/index.ts new file mode 100644 index 00000000..983a46ac --- /dev/null +++ b/dashboard/src/routes/resource/expression/index.ts @@ -0,0 +1 @@ +export { ExpressionManagementPage } from './index.tsx' diff --git a/dashboard/src/routes/resource/expression/index.tsx b/dashboard/src/routes/resource/expression/index.tsx new file mode 100644 index 00000000..a7515a66 --- /dev/null +++ b/dashboard/src/routes/resource/expression/index.tsx @@ -0,0 +1,467 @@ +import { ClipboardCheck, MessageSquare, Plus, Search, Trash2 } from 'lucide-react' +import { useEffect, useState } from 'react' + +import { Button } from '@/components/ui/button' +import { ExpressionReviewer } from '@/components/expression-reviewer' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { ScrollArea } from '@/components/ui/scroll-area' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' +import { useToast } from '@/hooks/use-toast' + +import { + batchDeleteExpressions, + deleteExpression, + getChatList, + getExpressionDetail, + getExpressionList, + getExpressionStats, + getReviewStats, +} from '@/lib/expression-api' + +import { + BatchDeleteConfirmDialog, + DeleteConfirmDialog, + ExpressionCreateDialog, + ExpressionDetailDialog, + ExpressionEditDialog, +} from './ExpressionDialogs' +import { ExpressionList } from './ExpressionList' + +import type { ChatInfo, Expression } from '@/types/expression' +import type { StatsData } from './types' + +/** + * 表达方式管理主页面 + */ +export function ExpressionManagementPage() { + const [expressions, setExpressions] = useState([]) + const [loading, setLoading] = useState(true) + const [total, setTotal] = useState(0) + const [page, setPage] = useState(1) + const [pageSize, setPageSize] = useState(20) + const [search, setSearch] = useState('') + const [selectedExpression, setSelectedExpression] = useState(null) + const [isDetailDialogOpen, setIsDetailDialogOpen] = useState(false) + const [isEditDialogOpen, setIsEditDialogOpen] = useState(false) + const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false) + const [deleteConfirmExpression, setDeleteConfirmExpression] = useState(null) + const [selectedIds, setSelectedIds] = useState>(new Set()) + const [isBatchDeleteDialogOpen, setIsBatchDeleteDialogOpen] = useState(false) + const [stats, setStats] = useState({ total: 0, recent_7days: 0, chat_count: 0, top_chats: {} }) + const [chatList, setChatList] = useState([]) + const [chatNameMap, setChatNameMap] = useState>(new Map()) + const [isReviewerOpen, setIsReviewerOpen] = useState(false) + const [uncheckedCount, setUncheckedCount] = useState(0) + const { toast } = useToast() + + // 加载表达方式列表 + const loadExpressions = async () => { + try { + setLoading(true) + const result = await getExpressionList({ + page, + page_size: pageSize, + search: search || undefined, + }) + if (result.success) { + setExpressions(result.data.data) + setTotal(result.data.total) + } else { + toast({ + title: '加载失败', + description: result.error, + variant: 'destructive', + }) + } + } catch (error) { + toast({ + title: '加载失败', + description: error instanceof Error ? error.message : '无法加载表达方式', + variant: 'destructive', + }) + } finally { + setLoading(false) + } + } + + // 加载统计数据 + const loadStats = async () => { + try { + const result = await getExpressionStats() + if (result.success) { + setStats(result.data) + } else { + console.error('加载统计数据失败:', result.error) + } + } catch (error) { + console.error('加载统计数据失败:', error) + } + } + + // 加载审核统计 + const loadReviewStats = async () => { + try { + const result = await getReviewStats() + if (result.success) { + setUncheckedCount(result.data.unchecked) + } + } catch (error) { + console.error('加载审核统计失败:', error) + } + } + + // 加载聚天列表 + const loadChatList = async () => { + try { + const result = await getChatList() + if (result.success) { + setChatList(result.data) + const nameMap = new Map() + result.data.forEach((chat: ChatInfo) => { + nameMap.set(chat.chat_id, chat.chat_name) + }) + setChatNameMap(nameMap) + } + } catch (error) { + console.error('加载聚天列表失败:', error) + } + } + + // 初始加载 + useEffect(() => { + loadExpressions() + loadReviewStats() + loadStats() + loadChatList() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [page, pageSize, search]) + + // 查看详情 + const handleViewDetail = async (expression: Expression) => { + try { + const result = await getExpressionDetail(expression.id) + if (result.success) { + setSelectedExpression(result.data) + setIsDetailDialogOpen(true) + } else { + toast({ + title: '加载详情失败', + description: result.error, + variant: 'destructive', + }) + } + } catch (error) { + toast({ + title: '加载详情失败', + description: error instanceof Error ? error.message : '无法加载表达方式详情', + variant: 'destructive', + }) + } + } + + // 编辑表达方式 + const handleEdit = (expression: Expression) => { + setSelectedExpression(expression) + setIsEditDialogOpen(true) + } + + // 删除表达方式 + const handleDelete = async () => { + if (!deleteConfirmExpression) return + try { + const result = await deleteExpression(deleteConfirmExpression.id) + if (result.success) { + toast({ + title: '删除成功', + description: `已删除表达方式: ${deleteConfirmExpression.situation}`, + }) + setDeleteConfirmExpression(null) + loadExpressions() + loadStats() + } else { + toast({ + title: '删除失败', + description: result.error, + variant: 'destructive', + }) + } + } catch (error) { + toast({ + title: '删除失败', + description: error instanceof Error ? error.message : '无法删除表达方式', + variant: 'destructive', + }) + } + } + + // 切换单个选择 + const toggleSelect = (id: number) => { + const newSelected = new Set(selectedIds) + if (newSelected.has(id)) { + newSelected.delete(id) + } else { + newSelected.add(id) + } + setSelectedIds(newSelected) + } + + // 全选/取消全选 + const toggleSelectAll = () => { + if (selectedIds.size === expressions.length && expressions.length > 0) { + setSelectedIds(new Set()) + } else { + setSelectedIds(new Set(expressions.map(e => e.id))) + } + } + + // 批量删除 + const handleBatchDelete = async () => { + try { + const result = await batchDeleteExpressions(Array.from(selectedIds)) + if (result.success) { + toast({ + title: '批量删除成功', + description: `已删除 ${selectedIds.size} 个表达方式`, + }) + setSelectedIds(new Set()) + setIsBatchDeleteDialogOpen(false) + loadExpressions() + loadStats() + } else { + toast({ + title: '批量删除失败', + description: result.error, + variant: 'destructive', + }) + } + } catch (error) { + toast({ + title: '批量删除失败', + description: error instanceof Error ? error.message : '无法批量删除表达方式', + variant: 'destructive', + }) + } + } + + // 页面跳转 + const handleJumpToPage = (jumpToPage: string) => { + const targetPage = parseInt(jumpToPage) + const totalPages = Math.ceil(total / pageSize) + if (targetPage >= 1 && targetPage <= totalPages) { + setPage(targetPage) + } + } + + return ( +
+ {/* 页面标题 */} +
+
+
+

+ + 表达方式管理 +

+

+ 管理麦麦的表达方式和话术模板 +

+
+
+ + +
+
+
+ + +
+ + {/* 统计卡片 */} +
+
+
总数量
+
{stats.total}
+
+
+
近7天新增
+
{stats.recent_7days}
+
+
+
关联聊天数
+
{stats.chat_count}
+
+
+ + {/* 搜索和批量操作 */} +
+ +
+
+ + setSearch(e.target.value)} + className="pl-9" + /> +
+
+ + {/* 批量操作工具栏 */} +
+
+ {selectedIds.size > 0 && ( + 已选择 {selectedIds.size} 个表达方式 + )} +
+
+ + + {selectedIds.size > 0 && ( + <> + + + + )} +
+
+
+ + {/* 表达方式列表 */} + setDeleteConfirmExpression(expression)} + onToggleSelect={toggleSelect} + onToggleSelectAll={toggleSelectAll} + onPageChange={setPage} + onJumpToPage={handleJumpToPage} + /> + +
+
+ + {/* 详情对话框 */} + + + {/* 创建对话框 */} + { + loadExpressions() + loadStats() + setIsCreateDialogOpen(false) + }} + /> + + {/* 编辑对话框 */} + { + loadExpressions() + loadStats() + setIsEditDialogOpen(false) + }} + /> + + {/* 删除确认对话框 */} + setDeleteConfirmExpression(null)} + onConfirm={handleDelete} + /> + + {/* 批量删除确认对话框 */} + + + {/* 表达方式审核器 */} + { + setIsReviewerOpen(open) + if (!open) { + loadExpressions() + loadStats() + loadReviewStats() + } + }} + /> +
+ ) +} diff --git a/dashboard/src/routes/resource/expression/types.ts b/dashboard/src/routes/resource/expression/types.ts new file mode 100644 index 00000000..a1de7dbb --- /dev/null +++ b/dashboard/src/routes/resource/expression/types.ts @@ -0,0 +1,47 @@ +/** + * 表达方式管理页面内部类型定义 + */ + +import type { Expression } from '@/types/expression' + +/** + * 删除确认状态 + */ +export interface DeleteConfirmState { + expression: Expression | null + isOpen: boolean +} + +/** + * 统计数据 + */ +export interface StatsData { + total: number + recent_7days: number + chat_count: number + top_chats: Record +} + +/** + * 页面状态 + */ +export interface PageState { + expressions: Expression[] + loading: boolean + total: number + page: number + pageSize: number + search: string + selectedExpression: Expression | null + isDetailDialogOpen: boolean + isEditDialogOpen: boolean + isCreateDialogOpen: boolean + deleteConfirmExpression: Expression | null + selectedIds: Set + isBatchDeleteDialogOpen: boolean + jumpToPage: string + stats: StatsData + chatNameMap: Map + isReviewerOpen: boolean + uncheckedCount: number +}