feat(report-viewer): add searchable combobox for report selection
Replace native select dropdown with Headless UI Combobox component to enable: - Search/filter functionality for reports - Better UX with keyboard navigation - Improved visual feedback for selected items - Empty state handling for no matches Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
235
package-lock.json
generated
235
package-lock.json
generated
@@ -12,6 +12,7 @@
|
||||
"@aws-sdk/client-s3": "^3.929.0",
|
||||
"@electron-toolkit/preload": "^3.0.2",
|
||||
"@electron-toolkit/utils": "^4.0.0",
|
||||
"@headlessui/react": "^2.2.9",
|
||||
"@tailwindcss/vite": "^4.2.1",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"chromium-bidi": "^15.0.0",
|
||||
@@ -2757,6 +2758,79 @@
|
||||
"integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@floating-ui/core": {
|
||||
"version": "1.7.5",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz",
|
||||
"integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/utils": "^0.2.11"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/dom": {
|
||||
"version": "1.7.6",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz",
|
||||
"integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/core": "^1.7.5",
|
||||
"@floating-ui/utils": "^0.2.11"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/react": {
|
||||
"version": "0.26.28",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.28.tgz",
|
||||
"integrity": "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/react-dom": "^2.1.2",
|
||||
"@floating-ui/utils": "^0.2.8",
|
||||
"tabbable": "^6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/react-dom": {
|
||||
"version": "2.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz",
|
||||
"integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/dom": "^1.7.6"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/utils": {
|
||||
"version": "0.2.11",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz",
|
||||
"integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@headlessui/react": {
|
||||
"version": "2.2.9",
|
||||
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.2.9.tgz",
|
||||
"integrity": "sha512-Mb+Un58gwBn0/yWZfyrCh0TJyurtT+dETj7YHleylHk5od3dv2XqETPGWMyQ5/7sYN7oWdyM1u9MvC0OC8UmzQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/react": "^0.26.16",
|
||||
"@react-aria/focus": "^3.20.2",
|
||||
"@react-aria/interactions": "^3.25.0",
|
||||
"@tanstack/react-virtual": "^3.13.9",
|
||||
"use-sync-external-store": "^1.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18 || ^19 || ^19.0.0-rc",
|
||||
"react-dom": "^18 || ^19 || ^19.0.0-rc"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanfs/core": {
|
||||
"version": "0.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
||||
@@ -3136,6 +3210,103 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-aria/focus": {
|
||||
"version": "3.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.21.5.tgz",
|
||||
"integrity": "sha512-V18fwCyf8zqgJdpLQeDU5ZRNd9TeOfBbhLgmX77Zr5ae9XwaoJ1R3SFJG1wCJX60t34AW+aLZSEEK+saQElf3Q==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@react-aria/interactions": "^3.27.1",
|
||||
"@react-aria/utils": "^3.33.1",
|
||||
"@react-types/shared": "^3.33.1",
|
||||
"@swc/helpers": "^0.5.0",
|
||||
"clsx": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
|
||||
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-aria/interactions": {
|
||||
"version": "3.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.27.1.tgz",
|
||||
"integrity": "sha512-M3wLpTTmDflI0QGNK0PJNUaBXXfeBXue8ZxLMngfc1piHNiH4G5lUvWd9W14XVbqrSCVY8i8DfGrNYpyyZu0tw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@react-aria/ssr": "^3.9.10",
|
||||
"@react-aria/utils": "^3.33.1",
|
||||
"@react-stately/flags": "^3.1.2",
|
||||
"@react-types/shared": "^3.33.1",
|
||||
"@swc/helpers": "^0.5.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
|
||||
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-aria/ssr": {
|
||||
"version": "3.9.10",
|
||||
"resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.10.tgz",
|
||||
"integrity": "sha512-hvTm77Pf+pMBhuBm760Li0BVIO38jv1IBws1xFm1NoL26PU+fe+FMW5+VZWyANR6nYL65joaJKZqOdTQMkO9IQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@swc/helpers": "^0.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-aria/utils": {
|
||||
"version": "3.33.1",
|
||||
"resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.33.1.tgz",
|
||||
"integrity": "sha512-kIx1Sj6bbAT0pdqCegHuPanR9zrLn5zMRiM7LN12rgRf55S19ptd9g3ncahArifYTRkfEU9VIn+q0HjfMqS9/w==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@react-aria/ssr": "^3.9.10",
|
||||
"@react-stately/flags": "^3.1.2",
|
||||
"@react-stately/utils": "^3.11.0",
|
||||
"@react-types/shared": "^3.33.1",
|
||||
"@swc/helpers": "^0.5.0",
|
||||
"clsx": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
|
||||
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-stately/flags": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@react-stately/flags/-/flags-3.1.2.tgz",
|
||||
"integrity": "sha512-2HjFcZx1MyQXoPqcBGALwWWmgFVUk2TuKVIQxCbRq7fPyWXIl6VHcakCLurdtYC2Iks7zizvz0Idv48MQ38DWg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@swc/helpers": "^0.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-stately/utils": {
|
||||
"version": "3.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.11.0.tgz",
|
||||
"integrity": "sha512-8LZpYowJ9eZmmYLpudbo/eclIRnbhWIJZ994ncmlKlouNzKohtM8qTC6B1w1pwUbiwGdUoyzLuQbeaIor5Dvcw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@swc/helpers": "^0.5.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-types/shared": {
|
||||
"version": "3.33.1",
|
||||
"resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.33.1.tgz",
|
||||
"integrity": "sha512-oJHtjvLG43VjwemQDadlR5g/8VepK56B/xKO2XORPHt9zlW6IZs3tZrYlvH29BMvoqC7RtE7E5UjgbnbFtDGag==",
|
||||
"license": "Apache-2.0",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/pluginutils": {
|
||||
"version": "1.0.0-rc.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz",
|
||||
@@ -4236,6 +4407,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@swc/helpers": {
|
||||
"version": "0.5.19",
|
||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.19.tgz",
|
||||
"integrity": "sha512-QamiFeIK3txNjgUTNppE6MiG3p7TdninpZu0E0PbqVh1a9FNLT2FRhisaa4NcaX52XVhA5l7Pk58Ft7Sqi/2sA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@szmarczak/http-timer": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz",
|
||||
@@ -4505,6 +4685,33 @@
|
||||
"vite": "^5.2.0 || ^6 || ^7"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-virtual": {
|
||||
"version": "3.13.23",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.23.tgz",
|
||||
"integrity": "sha512-XnMRnHQ23piOVj2bzJqHrRrLg4r+F86fuBcwteKfbIjJrtGxb4z7tIvPVAe4B+4UVwo9G4Giuz5fmapcrnZ0OQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/virtual-core": "3.13.23"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/virtual-core": {
|
||||
"version": "3.13.23",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.23.tgz",
|
||||
"integrity": "sha512-zSz2Z2HNyLjCplANTDyl3BcdQJc2k1+yyFoKhNRmCr7V7dY8o8q5m8uFTI1/Pg1kL+Hgrz6u3Xo6eFUB7l66cg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@tediousjs/connection-string": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@tediousjs/connection-string/-/connection-string-0.6.0.tgz",
|
||||
@@ -6657,6 +6864,15 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/clsx": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/color": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/color/-/color-5.0.3.tgz",
|
||||
@@ -13336,8 +13552,8 @@
|
||||
"version": "19.2.4",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
|
||||
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.27.0"
|
||||
},
|
||||
@@ -13968,7 +14184,6 @@
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
|
||||
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/semver": {
|
||||
@@ -14680,6 +14895,12 @@
|
||||
"url": "https://opencollective.com/synckit"
|
||||
}
|
||||
},
|
||||
"node_modules/tabbable": {
|
||||
"version": "6.4.0",
|
||||
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz",
|
||||
"integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.1.tgz",
|
||||
@@ -16207,6 +16428,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/use-sync-external-store": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
|
||||
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/utf8-byte-length": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz",
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
"@aws-sdk/client-s3": "^3.929.0",
|
||||
"@electron-toolkit/preload": "^3.0.2",
|
||||
"@electron-toolkit/utils": "^4.0.0",
|
||||
"@headlessui/react": "^2.2.9",
|
||||
"@tailwindcss/vite": "^4.2.1",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"chromium-bidi": "^15.0.0",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { X, FileText, Loader2 } from 'lucide-react'
|
||||
import React, { useEffect, useState, useMemo } from 'react'
|
||||
import { X, FileText, Loader2, ChevronDown } from 'lucide-react'
|
||||
import { Combobox, Transition } from '@headlessui/react'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import rehypeHighlight from 'rehype-highlight'
|
||||
@@ -30,11 +31,12 @@ export const ReportViewerDialog: React.FC<ReportViewerDialogProps> = ({
|
||||
currentUsername
|
||||
}) => {
|
||||
const [reports, setReports] = useState<ReportMetadata[]>([])
|
||||
const [selectedReportKey, setSelectedReportKey] = useState<string>('')
|
||||
const [selectedReport, setSelectedReport] = useState<ReportMetadata | null>(null)
|
||||
const [reportContent, setReportContent] = useState<string>('')
|
||||
const [isLoadingList, setIsLoadingList] = useState<boolean>(false)
|
||||
const [isLoadingContent, setIsLoadingContent] = useState<boolean>(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [query, setQuery] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
@@ -42,7 +44,7 @@ export const ReportViewerDialog: React.FC<ReportViewerDialogProps> = ({
|
||||
} else {
|
||||
// Reset state when closed
|
||||
setReports([])
|
||||
setSelectedReportKey('')
|
||||
setSelectedReport(null)
|
||||
setReportContent('')
|
||||
setError(null)
|
||||
}
|
||||
@@ -71,11 +73,10 @@ export const ReportViewerDialog: React.FC<ReportViewerDialogProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
const handleReportChange = async (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const key = e.target.value
|
||||
setSelectedReportKey(key)
|
||||
const handleReportChange = async (report: ReportMetadata | null) => {
|
||||
setSelectedReport(report)
|
||||
|
||||
if (!key) {
|
||||
if (!report) {
|
||||
setReportContent('')
|
||||
return
|
||||
}
|
||||
@@ -83,7 +84,7 @@ export const ReportViewerDialog: React.FC<ReportViewerDialogProps> = ({
|
||||
setIsLoadingContent(true)
|
||||
setError(null)
|
||||
try {
|
||||
const result = await window.electron.report.download(key)
|
||||
const result = await window.electron.report.download(report.key)
|
||||
if (result.success && result.data) {
|
||||
setReportContent(result.data)
|
||||
} else {
|
||||
@@ -98,6 +99,12 @@ export const ReportViewerDialog: React.FC<ReportViewerDialogProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
// Filter reports based on search query
|
||||
const filteredReports = useMemo(() => {
|
||||
if (!query) return reports
|
||||
return reports.filter((report) => report.filename.toLowerCase().includes(query.toLowerCase()))
|
||||
}, [reports, query])
|
||||
|
||||
if (!isOpen) return null
|
||||
|
||||
// Format date to local string
|
||||
@@ -136,29 +143,82 @@ export const ReportViewerDialog: React.FC<ReportViewerDialogProps> = ({
|
||||
<div className="flex items-center gap-4">
|
||||
<label className="text-sm font-medium text-slate-700 flex-shrink-0">选择报告:</label>
|
||||
<div className="relative flex-1 max-w-2xl">
|
||||
<select
|
||||
value={selectedReportKey}
|
||||
onChange={handleReportChange}
|
||||
disabled={isLoadingList}
|
||||
className="w-full appearance-none bg-slate-50 border border-slate-300 text-slate-700 py-2 pl-3 pr-10 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 disabled:opacity-50 text-sm"
|
||||
>
|
||||
<option value="">请选择要查看的报告...</option>
|
||||
{reports.map((report) => (
|
||||
<option key={report.key} value={report.key}>
|
||||
{isAdmin ? `[${report.username}] ` : ''}
|
||||
{report.filename} ({formatDate(report.lastModified)})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-slate-500">
|
||||
<svg
|
||||
className="fill-current h-4 w-4"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
<Combobox value={selectedReport} onChange={handleReportChange} nullable>
|
||||
<div className="relative">
|
||||
<Combobox.Input
|
||||
className="w-full bg-slate-50 border border-slate-300 text-slate-700 py-2 pl-3 pr-10 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 disabled:opacity-50 text-sm"
|
||||
displayValue={(report: ReportMetadata | null) => {
|
||||
if (!report) return '请选择要查看的报告...'
|
||||
return `${isAdmin ? `[${report.username}] ` : ''}${report.filename} (${formatDate(report.lastModified)})`
|
||||
}}
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
placeholder="请选择要查看的报告..."
|
||||
disabled={isLoadingList}
|
||||
/>
|
||||
<Combobox.Button className="absolute inset-y-0 right-0 flex items-center px-2 text-slate-500 hover:text-slate-700 transition-colors">
|
||||
<ChevronDown size={20} className="h-5 w-5" />
|
||||
</Combobox.Button>
|
||||
</div>
|
||||
<Transition
|
||||
enter="transition duration-100 ease-out"
|
||||
enterFrom="transform scale-95 opacity-0"
|
||||
enterTo="transform scale-100 opacity-100"
|
||||
leave="transition duration-75 ease-out"
|
||||
leaveFrom="transform scale-100 opacity-100"
|
||||
leaveTo="transform scale-95 opacity-0"
|
||||
>
|
||||
<path d="M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z" />
|
||||
</svg>
|
||||
</div>
|
||||
<Combobox.Options className="absolute z-50 w-full mt-1 bg-white border border-slate-200 rounded-lg shadow-lg max-h-60 overflow-auto focus:outline-none">
|
||||
{filteredReports.length === 0 && query ? (
|
||||
<div className="px-4 py-8 text-center text-slate-400 text-sm">
|
||||
未找到匹配的报告
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Empty option to allow clearing selection */}
|
||||
<Combobox.Option
|
||||
value={null}
|
||||
className={({ active }) =>
|
||||
`px-4 py-2.5 text-sm cursor-pointer ${
|
||||
active ? 'bg-slate-100 text-slate-900' : 'text-slate-500 italic'
|
||||
}`
|
||||
}
|
||||
>
|
||||
请选择要查看的报告...
|
||||
</Combobox.Option>
|
||||
{filteredReports.map((report) => (
|
||||
<Combobox.Option
|
||||
key={report.key}
|
||||
value={report}
|
||||
className={({ active, selected }) =>
|
||||
`px-4 py-2.5 text-sm cursor-pointer transition-colors ${
|
||||
selected
|
||||
? 'bg-blue-500 text-white'
|
||||
: active
|
||||
? 'bg-blue-50 text-blue-700'
|
||||
: 'text-slate-900'
|
||||
}`
|
||||
}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="truncate flex-1">
|
||||
{isAdmin ? `[${report.username}] ` : ''}
|
||||
{report.filename}
|
||||
</span>
|
||||
<span
|
||||
className={`ml-2 text-xs ${selected ? 'text-blue-100' : 'text-slate-400'}`}
|
||||
>
|
||||
{formatDate(report.lastModified)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</Combobox.Option>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</Combobox.Options>
|
||||
</Transition>
|
||||
</Combobox>
|
||||
</div>
|
||||
{isLoadingList && <Loader2 size={16} className="text-blue-500 animate-spin" />}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user