Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
c79fd88
chore
HUAHUAI23 Oct 23, 2025
3ceea66
add app config
HUAHUAI23 Oct 23, 2025
2ae824f
chore change env
HUAHUAI23 Oct 23, 2025
9609b23
chore
HUAHUAI23 Oct 23, 2025
2008aca
refactor smart node,rag node
HUAHUAI23 Oct 24, 2025
6f4ebf3
chore
HUAHUAI23 Oct 24, 2025
34e0579
add ticket auto close job
HUAHUAI23 Oct 24, 2025
0b734ce
chore
HUAHUAI23 Oct 24, 2025
72cd354
update i18n
HUAHUAI23 Oct 24, 2025
f488ce7
chore
HUAHUAI23 Oct 24, 2025
cea406d
chore
HUAHUAI23 Oct 24, 2025
0f88905
add knowledge access log
HUAHUAI23 Oct 28, 2025
490b23f
update orm history
HUAHUAI23 Oct 28, 2025
4565780
chore
HUAHUAI23 Oct 28, 2025
e75f715
fix bun run typecheck
HUAHUAI23 Oct 28, 2025
6156b8d
chore
HUAHUAI23 Oct 28, 2025
726aa0c
chore
HUAHUAI23 Oct 28, 2025
d87eaed
chore
HUAHUAI23 Oct 28, 2025
df68988
fix(ci): complete build fixes for all packages
HUAHUAI23 Oct 28, 2025
89173c9
chore
HUAHUAI23 Oct 28, 2025
3c419a7
chore
HUAHUAI23 Oct 28, 2025
ac17db9
chore
HUAHUAI23 Oct 28, 2025
2979519
chore
HUAHUAI23 Oct 28, 2025
12c1e41
chore
HUAHUAI23 Oct 28, 2025
d6d86d8
Merge pull request #48 from HUAHUAI23/fee
HUAHUAI23 Oct 28, 2025
5ca30aa
fix(add):analytics
huanglvjing Oct 27, 2025
171ac6b
fix1027
huanglvjing Oct 27, 2025
9751d54
fix1027_3
huanglvjing Oct 27, 2025
c831c61
fix10_28
huanglvjing Oct 28, 2025
1e83e02
fix1028_2
huanglvjing Oct 28, 2025
ac47ea5
fix1028_6
huanglvjing Oct 28, 2025
9354fa9
Merge main branch into feature/analytics-dashboard
huanglvjing Oct 28, 2025
ecbb631
fix1029_1
huanglvjing Oct 29, 2025
7a527ca
fix1029_2
huanglvjing Oct 29, 2025
c738ae3
fix21
huanglvjing Oct 29, 2025
140751d
fix1030
huanglvjing Oct 30, 2025
7eac12e
fix1030_2
huanglvjing Oct 30, 2025
eece6fd
fix1030_3
huanglvjing Oct 30, 2025
b30b11b
fix1030_5
huanglvjing Oct 30, 2025
d7f9959
fix1030_xiu
huanglvjing Oct 30, 2025
c04abc5
fix103033
huanglvjing Oct 30, 2025
de2da6e
fix1030fix
huanglvjing Oct 30, 2025
c8b37d4
fix1030_7
huanglvjing Oct 30, 2025
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
64 changes: 11 additions & 53 deletions bun.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"ahooks": "^3.8.4",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"echarts": "^6.0.0",
"highlight.js": "^11.11.1",
"i18n": "workspace:*",
"i18next": "^25.1.1",
Expand Down
88 changes: 88 additions & 0 deletions frontend/src/components/common/date-range-picker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { Button, Popover, PopoverContent, PopoverTrigger, Calendar } from "tentix-ui";
import { CalendarIcon } from "lucide-react";
import { cn } from "@lib/utils";
import { useTranslation } from "i18n";

interface DateRangePickerProps {
value?: { from: Date; to: Date };//日期范围
onChange?: (dateRange: { from: Date; to: Date } | undefined) => void;
disabled?: boolean;
placeholder?: string;
className?: string;
formatDate?: (date: Date) => string;
numberOfMonths?: number;
}

//日期范围选择组件
export function DateRangePicker({
value,
onChange,
disabled = false,
placeholder,
className,
formatDate: customFormatDate,
numberOfMonths = 2,
}: DateRangePickerProps) {
const { t } = useTranslation();

//格式化日期
const defaultFormatDate = (date: Date) => {
return date.toLocaleString(undefined, {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
});
};

const formatDate = customFormatDate || defaultFormatDate;

//日期范围选择
const handleDateRangeChange = (newDateRange: { from: Date | undefined; to?: Date | undefined } | undefined) => {
if (newDateRange?.from && newDateRange?.to) {
const range = { from: newDateRange.from, to: newDateRange.to };
onChange?.(range);
} else {
onChange?.(undefined);
}
};

return (
<Popover>
<PopoverTrigger asChild>
<Button
id="date-range-picker"
variant="outline"
className={cn(
"w-[300px] justify-start text-left font-normal h-10",
!value && "text-muted-foreground",
className
)}
disabled={disabled}
>
<CalendarIcon className="mr-2 h-4 w-4" />
{value?.from ? (
value.to ? (
`${formatDate(value.from)} - ${formatDate(value.to)}`
) : (
formatDate(value.from)
)
) : (
<span>{placeholder || t("select")}</span>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="range"
defaultMonth={value?.from}
selected={value}
onSelect={handleDateRangeChange}
numberOfMonths={numberOfMonths}
/>
</PopoverContent>
</Popover>
);
}

65 changes: 65 additions & 0 deletions frontend/src/components/common/echarts-wrapper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { useRef, useEffect } from "react";
import * as echarts from 'echarts';
import type { EChartsOption } from 'echarts';

interface EChartsWrapperProps {
option: EChartsOption;
className?: string;
style?: React.CSSProperties;
renderer?: 'canvas' | 'svg';
enableWindowResize?: boolean;
enableResizeObserver?: boolean;
}

export function EChartsWrapper({
option,
className,
style,
renderer = 'svg',
enableWindowResize = true,
enableResizeObserver = false,
}: EChartsWrapperProps) {
const chartRef = useRef<HTMLDivElement>(null);
const chartInstanceRef = useRef<echarts.ECharts | null>(null);

useEffect(() => {
if (!chartRef.current) return;

// 初始化图表
const chart = echarts.init(chartRef.current, undefined, { renderer });
chartInstanceRef.current = chart;

// 监听窗口大小变化
const handleResize = () => chart.resize();

if (enableWindowResize) {
window.addEventListener('resize', handleResize);
}

let resizeObserver: ResizeObserver | null = null;
if (enableResizeObserver && chartRef.current) {
resizeObserver = new ResizeObserver(handleResize);
resizeObserver.observe(chartRef.current);
}

return () => {
if (enableWindowResize) {
window.removeEventListener('resize', handleResize);
}
if (resizeObserver) {
resizeObserver.disconnect();
}
chart.dispose();
chartInstanceRef.current = null;
};
}, [renderer, enableWindowResize, enableResizeObserver]);

useEffect(() => {
if (chartInstanceRef.current && option) {
chartInstanceRef.current.setOption(option, true);
}
}, [option]);

return <div ref={chartRef} className={className} style={style} />;
}

17 changes: 16 additions & 1 deletion frontend/src/components/staff/sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Link } from "@tanstack/react-router";
import { joinTrans, useTranslation } from "i18n";
import { LayersIcon, Settings, LogOut, Bot } from "lucide-react";
import { LayersIcon, Settings, LogOut, Bot,LineChart } from "lucide-react";
import { Button } from "tentix-ui";
import { useSettingsModal } from "@modal/use-settings-modal";
import { useSealos } from "src/_provider/sealos";
Expand Down Expand Up @@ -50,6 +50,21 @@ export function StaffSidebar() {
</Link>
</Button>
)}
<Button
asChild
variant="ghost"
className="flex flex-col w-[60px] h-auto p-2 justify-center items-center gap-1 rounded-lg text-zinc-500 hover:bg-black/[0.04] hover:text-zinc-500"
>
<Link
to="/staff/analytics"
className="flex flex-col items-center justify-center gap-1 text-center"
>
<LineChart className="!w-6 !h-6" strokeWidth={1.33} />
<span className="text-[11px] leading-4 font-medium tracking-[0.5px] whitespace-nowrap font-['PingFang_SC']">
{t("analytics")}
</span>
</Link>
</Button>
<Button
variant="ghost"
className="flex flex-col w-[60px] h-auto p-2 justify-center items-center gap-1 rounded-lg text-zinc-500 hover:bg-black/[0.04] hover:text-zinc-500"
Expand Down
151 changes: 151 additions & 0 deletions frontend/src/components/staff/tickets-analytics/analytics-filter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import * as React from "react";
import { Button, Checkbox, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "tentix-ui";
import { staffListQueryOptions, useSuspenseQuery } from "@lib/query";
import { useAuth } from "@hook/use-local-user.tsx";
import { useTranslation } from "i18n";
import { DateRangePicker } from "@comp/common/date-range-picker";


interface AnalyticsFilterProps {
onDateRangeChange?: (dateRange: { from: Date; to: Date } | undefined) => void;
onEmployeeChange?: (employeeId: string) => void;
onRefresh?: () => void;
onTodayToggle?: (isToday: boolean) => void;
lastUpdated?: string;
}


//筛选逻辑
export function AnalyticsFilter({
onDateRangeChange,
onEmployeeChange,
onRefresh,
onTodayToggle,
lastUpdated,
}: AnalyticsFilterProps) {
const { t } = useTranslation();
const [dateRange, setDateRange] = React.useState<{ from: Date; to: Date } | undefined>();
const [isTodayChecked, setIsTodayChecked] = React.useState(false);
const [selectedEmployee, setSelectedEmployee] = React.useState("all_staff");

const [initialTime] = React.useState(() =>
new Date().toLocaleTimeString(undefined, {
hour: "2-digit",
minute: "2-digit",
})
);

const displayTime = lastUpdated || initialTime;

const authContext = useAuth();
const currentUser = authContext.user;

//获取员工列表
const { data: staffList } = useSuspenseQuery(staffListQueryOptions());

const employees = React.useMemo(() => {
return staffList?.map(staff => ({
id: staff.id.toString(),
name: staff.name || staff.nickname,
})) || [];
}, [staffList]);

const employeeOptions = React.useMemo(() => {
const defaultOption = { id: "all_staff", name: t("all_staff") };

if (currentUser?.role === "admin") {
return [defaultOption, ...employees];
}

const currentUserOption = {
id: currentUser?.id.toString() || "",
name: currentUser?.name || currentUser?.nickname || t("my"),
};

return [defaultOption, currentUserOption];
}, [employees, currentUser, t]);

//日期范围选择
const handleDateRangeChange = (newDateRange: { from: Date; to: Date } | undefined) => {
setDateRange(newDateRange);
if (newDateRange) {
setIsTodayChecked(false);
}
onDateRangeChange?.(newDateRange);
};

//员工选择
const handleEmployeeChange = (employeeId: string) => {
setSelectedEmployee(employeeId);
onEmployeeChange?.(employeeId);
};

//今天筛选
const handleTodayToggle = (checked: boolean) => {
setIsTodayChecked(checked);
if (checked) {
setDateRange(undefined);
}
onTodayToggle?.(checked);
};

//格式化日期
const formatDate = (date: Date) => {
return date.toLocaleString(undefined, {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
});
};

return (
<div className="flex flex-wrap items-center justify-between py-5 px-6 bg-white rounded-lg border border-zinc-200 gap-4">
<div className="flex flex-wrap items-center space-x-4 gap-2">
<span className="text-sm font-medium text-zinc-700">{t("analytics_filter")}</span>

<div className="flex items-center space-x-2 px-3 py-2 border border-zinc-200 rounded-md">
{/* 今天筛选 */}
<Checkbox
id="today-filter"
checked={isTodayChecked}
onCheckedChange={handleTodayToggle}
/>
<Label htmlFor="today-filter" className="text-sm font-normal text-zinc-700">
{t("today")}
</Label>
</div>

{/* 日期范围选择 */}
<DateRangePicker
value={dateRange}
onChange={handleDateRangeChange}
disabled={isTodayChecked}
formatDate={formatDate}
/>

<Select value={selectedEmployee} onValueChange={handleEmployeeChange}>
<SelectTrigger className="w-[180px] h-10">
<SelectValue placeholder={t("select_employee")} />
</SelectTrigger>
<SelectContent>
{employeeOptions.map((employee) => (
<SelectItem key={employee.id} value={employee.id}>
{employee.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>

{/* 右侧刷新和更新时间区域 */}
<div className="flex items-center space-x-4">
<Button variant="outline" size="sm" onClick={onRefresh}>
{t("reload")}
</Button>
<span className="text-sm text-zinc-500">{t("updated_at")} {displayTime}</span>
</div>
</div>
);
}
Loading