| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315 |
- 'use client'
- import type { FC } from 'react'
- import { useState } from 'react'
- import {
- RiFileHistoryLine,
- RiFilterLine,
- RiRefreshLine,
- } from '@remixicon/react'
- import Modal from '@/app/components/base/modal'
- import Button from '@/app/components/base/button'
- import { DocumentOperationType } from '@/models/operation-log'
- import type { OperationLog, OperationLogQueryParams } from '@/models/operation-log'
- import { useDocumentOperationLogs } from '@/service/knowledge/use-operation-log'
- import Loading from '@/app/components/base/loading'
- type OperationLogsModalProps = {
- isOpen: boolean
- onClose: () => void
- datasetId: string
- documentId: string
- documentName: string
- }
- const OperationLogsModal: FC<OperationLogsModalProps> = ({
- isOpen,
- onClose,
- datasetId,
- documentId,
- documentName,
- }) => {
- const [params, setParams] = useState<OperationLogQueryParams>({
- page: 1,
- per_page: 20,
- })
- const { data, isLoading, refetch } = useDocumentOperationLogs(
- datasetId,
- documentId,
- params,
- )
- const [actionFilter, setActionFilter] = useState<string>('')
- const handleFilterByAction = (action: string) => {
- const newAction = action === actionFilter ? '' : action
- setActionFilter(newAction)
- setParams({
- ...params,
- action: newAction || undefined,
- page: 1,
- })
- }
- const handlePageChange = (page: number) => {
- setParams({ ...params, page })
- }
- const getOperationTypeLabel = (action: string) => {
- const labels: Record<string, string> = {
- [DocumentOperationType.DOCUMENT_ENABLE]: '启用',
- [DocumentOperationType.DOCUMENT_DISABLE]: '禁用',
- [DocumentOperationType.DOCUMENT_ARCHIVE]: '归档',
- [DocumentOperationType.DOCUMENT_UNARCHIVE]: '取消归档',
- [DocumentOperationType.DOCUMENT_BATCH_ENABLE]: '批量启用',
- [DocumentOperationType.DOCUMENT_BATCH_DISABLE]: '批量禁用',
- [DocumentOperationType.DOCUMENT_BATCH_ARCHIVE]: '批量归档',
- [DocumentOperationType.DOCUMENT_BATCH_UNARCHIVE]: '批量取消归档',
- [DocumentOperationType.DOCUMENT_SCHEDULE_CREATE]: '创建定时任务',
- [DocumentOperationType.DOCUMENT_SCHEDULE_CANCEL]: '取消定时任务',
- [DocumentOperationType.DOCUMENT_SCHEDULE_EXECUTE]: '执行定时任务',
- [DocumentOperationType.DOCUMENT_CREATE]: '创建',
- [DocumentOperationType.DOCUMENT_UPDATE]: '更新',
- [DocumentOperationType.DOCUMENT_DELETE]: '删除',
- [DocumentOperationType.DOCUMENT_RENAME]: '重命名',
- }
- return labels[action] || action
- }
- const getOperationTypeBadge = (action: string) => {
- const config: Record<string, { bg: string; text: string }> = {
- [DocumentOperationType.DOCUMENT_ENABLE]: { bg: 'bg-util-colors-teal-teal-100', text: 'text-util-colors-teal-teal-600' },
- [DocumentOperationType.DOCUMENT_DISABLE]: { bg: 'bg-util-colors-orange-orange-100', text: 'text-util-colors-orange-orange-600' },
- [DocumentOperationType.DOCUMENT_ARCHIVE]: { bg: 'bg-util-colors-gray-gray-100', text: 'text-util-colors-gray-gray-600' },
- [DocumentOperationType.DOCUMENT_SCHEDULE_CREATE]: { bg: 'bg-util-colors-blue-blue-100', text: 'text-util-colors-blue-blue-600' },
- [DocumentOperationType.DOCUMENT_SCHEDULE_EXECUTE]: { bg: 'bg-util-colors-purple-purple-100', text: 'text-util-colors-purple-purple-600' },
- }
- const style = config[action] || { bg: 'bg-util-colors-gray-gray-100', text: 'text-util-colors-gray-gray-600' }
- return (
- <span className={`inline-flex items-center px-2 py-0.5 rounded-md text-xs font-medium ${style.bg} ${style.text}`}>
- {getOperationTypeLabel(action)}
- </span>
- )
- }
- const renderChangesSummary = (log: OperationLog) => {
- const { changes, metadata } = log.content
- if (!changes && !metadata)
- return null
- return (
- <div className="mt-2 space-y-1 text-xs">
- {changes && (
- <div className="flex gap-4">
- {changes.old && (
- <div className="flex-1">
- <span className="text-text-quaternary">修改前:</span>
- <code className="ml-1 px-1 py-0.5 bg-background-section-burn rounded text-text-tertiary">
- {JSON.stringify(changes.old)}
- </code>
- </div>
- )}
- {changes.new && (
- <div className="flex-1">
- <span className="text-text-quaternary">修改后:</span>
- <code className="ml-1 px-1 py-0.5 bg-background-section-burn rounded text-text-tertiary">
- {JSON.stringify(changes.new)}
- </code>
- </div>
- )}
- </div>
- )}
- {metadata?.scheduled && (
- <div className="text-text-quaternary">
- <span className="inline-flex items-center gap-1">
- <svg className="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
- <path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clipRule="evenodd" />
- </svg>
- 定时执行
- </span>
- {metadata.schedule_id && (
- <span className="ml-2 text-text-quaternary">
- ID: {metadata.schedule_id.substring(0, 16)}...
- </span>
- )}
- </div>
- )}
- {metadata?.batch && (
- <div className="text-text-quaternary">
- 批量操作: {metadata.batch_size} 个文档
- </div>
- )}
- </div>
- )
- }
- const logs = data?.logs || []
- const hasLogs = logs.length > 0
- // Quick action filters
- const quickFilters = [
- DocumentOperationType.DOCUMENT_ENABLE,
- DocumentOperationType.DOCUMENT_DISABLE,
- DocumentOperationType.DOCUMENT_ARCHIVE,
- DocumentOperationType.DOCUMENT_SCHEDULE_CREATE,
- DocumentOperationType.DOCUMENT_SCHEDULE_EXECUTE,
- ]
- return (
- <Modal
- isShow={isOpen}
- onClose={onClose}
- title={
- <div className="flex items-center gap-2">
- <RiFileHistoryLine className="w-5 h-5" />
- <span>操作历史</span>
- </div>
- }
- className="!w-[800px] !max-w-[90vw]"
- >
- <div className="py-4">
- {/* Document Name */}
- <div className="mb-4">
- <div className="text-sm text-text-tertiary">
- 文档: <span className="text-text-secondary font-medium">{documentName}</span>
- </div>
- </div>
- {/* Toolbar */}
- <div className="mb-4 space-y-3">
- {/* Quick Filters */}
- <div className="flex items-center gap-2 flex-wrap">
- <RiFilterLine className="w-4 h-4 text-text-tertiary" />
- <span className="text-xs text-text-quaternary">快速筛选:</span>
- <div className="flex items-center gap-1.5 flex-wrap">
- {quickFilters.map(action => (
- <button
- key={action}
- type="button"
- onClick={() => handleFilterByAction(action)}
- className={`px-2 py-1 rounded text-xs transition-colors ${
- actionFilter === action
- ? 'bg-components-button-primary-bg text-components-button-primary-text'
- : 'bg-background-section-burn text-text-tertiary hover:bg-background-default-hover'
- }`}
- >
- {getOperationTypeLabel(action)}
- </button>
- ))}
- {actionFilter && (
- <button
- type="button"
- onClick={() => handleFilterByAction('')}
- className="px-2 py-1 text-xs text-text-accent hover:underline"
- >
- 清除
- </button>
- )}
- </div>
- </div>
- {/* Refresh Button */}
- <div className="flex justify-end">
- <Button
- size="small"
- onClick={() => refetch()}
- className="gap-1"
- >
- <RiRefreshLine className="w-4 h-4" />
- 刷新
- </Button>
- </div>
- </div>
- {/* Logs List */}
- <div className="border border-divider-subtle rounded-lg overflow-hidden">
- {isLoading
- ? (
- <div className="flex items-center justify-center py-12">
- <Loading type="area" />
- </div>
- )
- : !hasLogs
- ? (
- <div className="text-center py-12 text-text-quaternary">
- <RiFileHistoryLine className="w-12 h-12 mx-auto mb-3 opacity-50" />
- <div className="text-sm">暂无操作历史</div>
- </div>
- )
- : (
- <div className="divide-y divide-divider-subtle max-h-[500px] overflow-y-auto">
- {logs.map((log: OperationLog) => (
- <div
- key={log.id}
- className="p-4 hover:bg-background-default-hover transition-colors"
- >
- <div className="flex items-start justify-between gap-4">
- <div className="flex-1 min-w-0">
- {/* Operation Type */}
- <div className="mb-2">
- {getOperationTypeBadge(log.action)}
- </div>
- {/* Timestamp */}
- <div className="text-sm text-text-secondary mb-1">
- {new Date(log.created_at).toLocaleString('zh-CN')}
- </div>
- {/* IP Address */}
- <div className="text-xs text-text-quaternary">
- 来自: {log.created_ip}
- </div>
- {/* Changes Summary */}
- {renderChangesSummary(log)}
- </div>
- </div>
- </div>
- ))}
- </div>
- )}
- </div>
- {/* Pagination */}
- {data && data.total > params.per_page! && (
- <div className="mt-4 flex items-center justify-between text-sm">
- <div className="text-text-quaternary">
- 显示 {(params.page! - 1) * params.per_page! + 1}-{Math.min(params.page! * params.per_page!, data.total)} 条,共 {data.total} 条
- </div>
- <div className="flex items-center gap-2">
- <Button
- size="small"
- disabled={params.page === 1}
- onClick={() => handlePageChange(params.page! - 1)}
- >
- 上一页
- </Button>
- <span className="text-text-tertiary">
- {params.page} / {data.pages}
- </span>
- <Button
- size="small"
- disabled={params.page === data.pages}
- onClick={() => handlePageChange(params.page! + 1)}
- >
- 下一页
- </Button>
- </div>
- </div>
- )}
- </div>
- {/* Footer */}
- <div className="flex justify-end pt-4 border-t border-divider-subtle">
- <Button onClick={onClose}>
- 关闭
- </Button>
- </div>
- </Modal>
- )
- }
- export default OperationLogsModal
|