Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
190 changes: 144 additions & 46 deletions frontend/src/components/staff/tickets-analytics/hot-issues-analysis.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Badge, cn } from "tentix-ui";
import { TrendingUp, TrendingDown, Minus, Sparkles, BarChartBig } from "lucide-react";
import { hotIssuesQueryOptions, useSuspenseQuery } from "@lib/query";
import { TrendingUp, TrendingDown, Minus, Sparkles, BarChartBig, AlertCircle } from "lucide-react";
import { hotIssuesQueryOptions } from "@lib/analytics-query";
import { useQuery } from "@tanstack/react-query";
import { useTranslation } from "i18n";

interface HotIssue {
Expand Down Expand Up @@ -47,14 +48,95 @@ interface HotIssuesAnalysisProps {
//热点问题分析
export function HotIssuesAnalysis({ filterParams, isLoading: externalLoading }: HotIssuesAnalysisProps) {
const { t } = useTranslation();
const { data: rawData } = useSuspenseQuery(hotIssuesQueryOptions(filterParams));
const { data: rawData, isLoading: queryLoading, error, isError } = useQuery(
hotIssuesQueryOptions(filterParams)
);

const isLoading = externalLoading || queryLoading;

//检查响应
// 错误状态-不阻塞其他组件
if (isError) {
console.error('Hot issues API error:', error);
const isTimeout = error instanceof Error && (
error.message.includes("timeout") ||
error.message.includes("超时") ||
error.message.includes("timed out")
);

return (
<div className="w-full bg-white rounded-lg border border-orange-200 shadow-sm">
<div className="flex h-16 p-6 justify-between items-center flex-shrink-0 border-b border-orange-200">
<h2 className="text-xl text-zinc-900 flex items-center gap-2">
{t("hot_issues_analysis")}
</h2>
</div>
<div className="p-8 flex flex-col items-center justify-center gap-4 min-h-[300px]">
<AlertCircle className="h-12 w-12 text-orange-500" />
<div className="text-center max-w-md">
<p className="text-base font-medium text-zinc-900 mb-2">
{isTimeout
? (t("request_timeout"))
: (t("data_loading_failed"))
}
</p>
<p className="text-sm text-zinc-600 mb-4">
{isTimeout
? (t("request_timeout_message"))
: (error instanceof Error ? error.message : (t("network_error_message")))
}
</p>
<button
onClick={() => window.location.reload()}
className="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors text-sm"
>
{t("retry")}
</button>
</div>
</div>
</div>
);
}

//加载中或数据为空
if (!rawData || isLoading) {
return (
<div className="w-full">
<div className="bg-white border border-zinc-200 rounded-lg p-4 shadow-sm">
<div className="animate-pulse">
<div className="h-6 bg-zinc-200 rounded w-48 mb-2"></div>
<div className="h-4 bg-zinc-200 rounded w-32"></div>
</div>
</div>
<div className="bg-white border-l border-r border-b border-zinc-200 rounded-b-lg p-6">
<div className="animate-pulse space-y-4">
{[...Array(5)].map((_, i) => (
<div key={i} className="h-16 bg-zinc-200 rounded"></div>
))}
</div>
</div>
</div>
);
}

//检查响应格式错误
if ('message' in rawData) {
console.error('Hot issues API error:', rawData.message);
return (
<div className="w-full p-4 bg-red-50 border border-red-200 rounded-lg">
<p className="text-red-600">Error loading hot issues: {rawData.message}</p>
<div className="w-full bg-white rounded-lg border border-orange-200">
<div className="flex h-16 p-6 justify-between items-center flex-shrink-0 border-b border-orange-200">
<h2 className="text-xl text-zinc-900 flex items-center gap-2">
{t("hot_issues_analysis")}
</h2>
</div>
<div className="p-8 flex flex-col items-center justify-center gap-4 min-h-[300px]">
<AlertCircle className="h-12 w-12 text-orange-500" />
<div className="text-center">
<p className="text-base font-medium text-zinc-900 mb-2">
{t("data_error")}
</p>
<p className="text-sm text-zinc-600">{rawData.message}</p>
</div>
</div>
</div>
);
}
Expand All @@ -68,8 +150,6 @@ export function HotIssuesAnalysis({ filterParams, isLoading: externalLoading }:
timeRange: filterParams?.isToday ? t("today") : t("last_7_days"),
aiInsights: rawData.aiInsights
};

const isLoading = externalLoading;
//计算标签问题数量最大值
const maxCount = Math.max(...data.tagStats.map(s => s.count), 0);
let dynamicYAxisMax = 100;
Expand Down Expand Up @@ -330,46 +410,64 @@ export function HotIssuesAnalysis({ filterParams, isLoading: externalLoading }:
{t("intelligent_analysis_insights")}
<Sparkles className="h-5 w-5 text-blue-500" />
</h2>
{/* 关键发现 */}
<div className="flex flex-col gap-[10px]">
<h3 className="text-base text-zinc-900 flex items-center gap-2">
{t("key_findings")}
</h3>
<ul className="flex flex-col gap-0">
{(data.aiInsights?.keyFindings || []).map((finding, index) => (
<li key={index} className="flex items-center gap-2 text-sm font-normal leading-normal text-zinc-600">
<span className="text-zinc-600 text-lg">•</span>
<span>{finding}</span>
</li>
))}
</ul>
</div>

{/* 如果 AI 洞察不可用,显示友好的错误提示 */}
{!data.aiInsights ? (
<div className="w-full bg-orange-50 border border-orange-200 rounded-lg p-6 flex items-start gap-3">
<AlertCircle className="h-5 w-5 text-orange-500 flex-shrink-0 mt-0.5" />
<div className="flex-1">
<p className="text-sm font-medium text-orange-900 mb-1">
{t("ai_insights_unavailable")}
</p>
<p className="text-sm text-orange-700">
{t("ai_insights_timeout_message")}
</p>
</div>
</div>
) : (
<>
{/* 关键发现 */}
<div className="flex flex-col gap-[10px]">
<h3 className="text-base text-zinc-900 flex items-center gap-2">
{t("key_findings")}
</h3>
<ul className="flex flex-col gap-0">
{(data.aiInsights.keyFindings || []).map((finding, index) => (
<li key={index} className="flex items-center gap-2 text-sm font-normal leading-normal text-zinc-600">
<span className="text-zinc-600 text-lg">•</span>
<span>{finding}</span>
</li>
))}
</ul>
</div>

{/* 改进建议 */}
<div className="flex flex-col gap-[10px]">
<h3 className="text-base text-zinc-900 flex items-center gap-2">
{t("improvement_suggestions")}
</h3>
<ul className="flex flex-col gap-0">
{(data.aiInsights?.improvements || []).map((improvement, index) => (
<li key={index} className="flex items-center gap-2 text-sm font-normal leading-normal text-zinc-600">
<span className="text-zinc-600 text-lg">•</span>
<span>{improvement}</span>
</li>
))}
</ul>
</div>
{/* 改进建议 */}
<div className="flex flex-col gap-[10px]">
<h3 className="text-base text-zinc-900 flex items-center gap-2">
{t("improvement_suggestions")}
</h3>
<ul className="flex flex-col gap-0">
{(data.aiInsights.improvements || []).map((improvement, index) => (
<li key={index} className="flex items-center gap-2 text-sm font-normal leading-normal text-zinc-600">
<span className="text-zinc-600 text-lg">•</span>
<span>{improvement}</span>
</li>
))}
</ul>
</div>

{/* 数据驱动策略 */}
<div className="border-2 border-dashed border-gray-300 rounded-lg p-4 bg-gray-50 w-full">
<h3 className="text-base text-zinc-900 flex items-center gap-2">
<BarChartBig className="h-4 w-4 text-blue-500" />
{t("data_driven_strategy")}
</h3>
<p className="text-sm font-normal leading-normal text-zinc-900">
{data.aiInsights?.strategy || t("analyzing_data")}
</p>
</div>
{/* 数据驱动策略 */}
<div className="border-2 border-dashed border-gray-300 rounded-lg p-4 bg-gray-50 w-full">
<h3 className="text-base text-zinc-900 flex items-center gap-2">
<BarChartBig className="h-4 w-4 text-blue-500" />
{t("data_driven_strategy")}
</h3>
<p className="text-sm font-normal leading-normal text-zinc-900">
{data.aiInsights.strategy || t("analyzing_data")}
</p>
</div>
</>
)}
</div>
</div>
</div>
Expand Down
40 changes: 20 additions & 20 deletions frontend/src/components/staff/tickets-analytics/module-analysis.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ interface ModuleAnalysisProps {
export function ModuleAnalysis({ filterParams, isLoading: externalLoading }: ModuleAnalysisProps) {
const { t, i18n } = useTranslation();
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
const [mousePosition, setMousePosition] = useState<{ x: number; y: number }>({ x: 0, y: 0 });
const { data: rawData } = useSuspenseQuery(moduleAnalysisQueryOptions(filterParams));
const ticketModules = useTicketModules();

Expand Down Expand Up @@ -86,31 +87,36 @@ export function ModuleAnalysis({ filterParams, isLoading: externalLoading }: Mod
const bgColor = blueColors[Math.min(index, blueColors.length - 1)];
const isHovered = hoveredIndex === index;

const isTopItem = index < 1;

return (
<div
key={index}
className="relative cursor-pointer"
onMouseEnter={() => setHoveredIndex(index)}
onMouseLeave={() => setHoveredIndex(null)}
className="relative"
onMouseMove={(e) => {
const rect = e.currentTarget.getBoundingClientRect();
setMousePosition({
x: e.clientX - rect.left,
y: e.clientY - rect.top,
});
}}
>
{/* 条形容器 */}
<div className="relative h-12 rounded-lg overflow-visible">
{/* 条形背景 */}
<div
className={`absolute top-0 left-0 h-full transition-all duration-500 ease-out rounded-lg z-10 ${
className={`absolute top-0 left-0 h-full transition-all duration-500 ease-out rounded-lg z-10 cursor-pointer ${
isHovered ? 'opacity-90 scale-x-[1.02]' : 'opacity-100 scale-x-100'
}`}
style={{
width: `${Math.max(widthPercentage * 0.8, 15)}%`,
backgroundColor: bgColor,
}}
onMouseEnter={() => setHoveredIndex(index)}
onMouseLeave={() => setHoveredIndex(null)}
/>

{/* 模块名称 */}
<div
className="absolute top-0 left-0 h-full flex items-center px-4 z-20"
className="absolute top-0 left-0 h-full flex items-center px-4 z-20 pointer-events-none"
style={{
width: `${Math.max(widthPercentage * 0.8, 15)}%`,
}}
Expand All @@ -126,7 +132,7 @@ export function ModuleAnalysis({ filterParams, isLoading: externalLoading }: Mod

{/* 数值和百分比 - 显示在颜色条形旁边 */}
<div
className="absolute top-1/2 left-0 transform -translate-y-1/2 ml-3 z-30"
className="absolute top-1/2 left-0 transform -translate-y-1/2 ml-3 z-30 pointer-events-none"
style={{
left: `${Math.max(widthPercentage * 0.8, 15)}%`,
}}
Expand All @@ -137,12 +143,14 @@ export function ModuleAnalysis({ filterParams, isLoading: externalLoading }: Mod
</div>
</div>

{/* Hover 提示框*/}
{/* 提示框-跟鼠标 */}
{isHovered && (
<div
className={`absolute left-1/2 transform -translate-x-1/2 bg-white text-gray-700 w-[240px] p-4 flex flex-col justify-center items-start gap-2 rounded-xl border border-zinc-200 z-[1000] pointer-events-none shadow-[0_2px_8px_-2px_rgba(0,0,0,0.08)] ${
isTopItem ? "top-[calc(100%+10px)]" : "-top-12"
}`}
className="absolute bg-white text-gray-700 w-[240px] p-4 flex flex-col justify-center items-start gap-2 rounded-xl border border-zinc-200 z-[1000] pointer-events-none shadow-[0_4px_12px_rgba(0,0,0,0.12)]"
style={{
left: `${mousePosition.x + 15}px`,
top: `${mousePosition.y - 30}px`,
}}
>
<div className="flex items-center gap-2 w-full">
{/* 蓝色小竖线 */}
Expand All @@ -154,14 +162,6 @@ export function ModuleAnalysis({ filterParams, isLoading: externalLoading }: Mod
</span>
</div>
</div>
{/* 箭头指示器 */}
<div
className={`absolute left-1/2 transform -translate-x-1/2 w-0 h-0 border-l-4 border-r-4 ${
isTopItem
? 'top-[-4px] border-b-4 border-b-white'
: 'bottom-[-4px] border-t-4 border-t-white'
}`}
/>
</div>
)}
</div>
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/lib/analytics-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ export const hotIssuesQueryOptions = (filterParams?: {
return data;
},
...analyticsQueryConfig,
staleTime: 30 * 60 * 1000,
gcTime: 30 * 60 * 1000,
});

export const ratingAnalysisQueryOptions = (filterParams?: {
Expand Down
Loading