Compare commits
2 Commits
b0dbd088e7
...
a6f7172ec4
| Author | SHA1 | Date | |
|---|---|---|---|
| a6f7172ec4 | |||
| 8054b0235d |
@@ -6,11 +6,20 @@ export async function GET(request: NextRequest) {
|
|||||||
try {
|
try {
|
||||||
const searchParams = request.nextUrl.searchParams;
|
const searchParams = request.nextUrl.searchParams;
|
||||||
|
|
||||||
|
// 获取团队、项目和标签筛选参数
|
||||||
|
const teamIds = searchParams.getAll('teamId');
|
||||||
|
const projectIds = searchParams.getAll('projectId');
|
||||||
|
const tagIds = searchParams.getAll('tagId');
|
||||||
|
|
||||||
const data = await getGeoAnalytics({
|
const data = await getGeoAnalytics({
|
||||||
startTime: searchParams.get('startTime') || undefined,
|
startTime: searchParams.get('startTime') || undefined,
|
||||||
endTime: searchParams.get('endTime') || undefined,
|
endTime: searchParams.get('endTime') || undefined,
|
||||||
linkId: searchParams.get('linkId') || undefined,
|
linkId: searchParams.get('linkId') || undefined,
|
||||||
groupBy: (searchParams.get('groupBy') || 'country') as 'country' | 'city'
|
groupBy: (searchParams.get('groupBy') || 'country') as 'country' | 'city',
|
||||||
|
// 添加团队、项目和标签筛选
|
||||||
|
teamIds: teamIds.length > 0 ? teamIds : undefined,
|
||||||
|
projectIds: projectIds.length > 0 ? projectIds : undefined,
|
||||||
|
tagIds: tagIds.length > 0 ? tagIds : undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
const response: ApiResponse<typeof data> = {
|
const response: ApiResponse<typeof data> = {
|
||||||
|
|||||||
@@ -35,10 +35,8 @@ export async function GET(request: NextRequest) {
|
|||||||
linkId,
|
linkId,
|
||||||
linkSlug,
|
linkSlug,
|
||||||
userId,
|
userId,
|
||||||
teamId: teamIds.length > 0 ? teamIds[0] : undefined,
|
teamIds: teamIds.length > 0 ? teamIds : undefined,
|
||||||
teamIds: teamIds.length > 1 ? teamIds : undefined,
|
projectIds: projectIds.length > 0 ? projectIds : undefined,
|
||||||
projectId: projectIds.length > 0 ? projectIds[0] : undefined,
|
|
||||||
projectIds: projectIds.length > 1 ? projectIds : undefined,
|
|
||||||
tagIds: tagIds.length > 0 ? tagIds : undefined,
|
tagIds: tagIds.length > 0 ? tagIds : undefined,
|
||||||
startTime,
|
startTime,
|
||||||
endTime,
|
endTime,
|
||||||
|
|||||||
@@ -15,11 +15,20 @@ export async function GET(request: NextRequest) {
|
|||||||
}, { status: 400 });
|
}, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取团队、项目和标签筛选参数
|
||||||
|
const teamIds = searchParams.getAll('teamId');
|
||||||
|
const projectIds = searchParams.getAll('projectId');
|
||||||
|
const tagIds = searchParams.getAll('tagId');
|
||||||
|
|
||||||
const data = await getTimeSeriesData({
|
const data = await getTimeSeriesData({
|
||||||
startTime,
|
startTime,
|
||||||
endTime,
|
endTime,
|
||||||
linkId: searchParams.get('linkId') || undefined,
|
linkId: searchParams.get('linkId') || undefined,
|
||||||
granularity: (searchParams.get('granularity') || 'day') as 'hour' | 'day' | 'week' | 'month'
|
granularity: (searchParams.get('granularity') || 'day') as 'hour' | 'day' | 'week' | 'month',
|
||||||
|
// 添加团队、项目和标签筛选
|
||||||
|
teamIds: teamIds.length > 0 ? teamIds : undefined,
|
||||||
|
projectIds: projectIds.length > 0 ? projectIds : undefined,
|
||||||
|
tagIds: tagIds.length > 0 ? tagIds : undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
const response: ApiResponse<typeof data> = {
|
const response: ApiResponse<typeof data> = {
|
||||||
|
|||||||
@@ -35,12 +35,14 @@ export function ProjectSelector({
|
|||||||
className,
|
className,
|
||||||
multiple = false,
|
multiple = false,
|
||||||
teamId,
|
teamId,
|
||||||
|
teamIds,
|
||||||
}: {
|
}: {
|
||||||
value?: string | string[];
|
value?: string | string[];
|
||||||
onChange?: (projectId: string | string[]) => void;
|
onChange?: (projectId: string | string[]) => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
multiple?: boolean;
|
multiple?: boolean;
|
||||||
teamId?: string; // Optional team ID to filter projects by team
|
teamId?: string; // Optional team ID to filter projects by team
|
||||||
|
teamIds?: string[]; // Optional array of team IDs to filter projects by multiple teams
|
||||||
}) {
|
}) {
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
@@ -49,6 +51,16 @@ export function ProjectSelector({
|
|||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const selectorRef = useRef<HTMLDivElement>(null);
|
const selectorRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// Normalize team IDs to ensure we're always working with an array
|
||||||
|
const effectiveTeamIds = React.useMemo(() => {
|
||||||
|
if (teamIds && teamIds.length > 0) {
|
||||||
|
return teamIds;
|
||||||
|
} else if (teamId) {
|
||||||
|
return [teamId];
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}, [teamId, teamIds]);
|
||||||
|
|
||||||
// Initialize selected projects based on value prop
|
// Initialize selected projects based on value prop
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (value) {
|
if (value) {
|
||||||
@@ -90,38 +102,59 @@ export function ProjectSelector({
|
|||||||
try {
|
try {
|
||||||
const supabase = getSupabaseClient();
|
const supabase = getSupabaseClient();
|
||||||
|
|
||||||
let projectsQuery;
|
if (effectiveTeamIds && effectiveTeamIds.length > 0) {
|
||||||
|
// If team IDs are provided, get projects for those teams
|
||||||
if (teamId) {
|
const { data: projectsData, error: projectsError } = await supabase
|
||||||
// 如果提供了teamId,获取该团队的项目
|
|
||||||
projectsQuery = supabase
|
|
||||||
.from('team_projects')
|
.from('team_projects')
|
||||||
.select('project_id, projects:project_id(*), teams:team_id(name)')
|
.select('project_id, projects:project_id(*), teams:team_id(name)')
|
||||||
.eq('team_id', teamId)
|
.in('team_id', effectiveTeamIds)
|
||||||
.is('projects.deleted_at', null);
|
.is('projects.deleted_at', null);
|
||||||
|
|
||||||
|
if (projectsError) throw projectsError;
|
||||||
|
|
||||||
|
if (!projectsData || projectsData.length === 0) {
|
||||||
|
if (isMounted) setProjects([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract projects from response with team info
|
||||||
|
if (isMounted) {
|
||||||
|
const projectList: Project[] = [];
|
||||||
|
|
||||||
|
for (const item of projectsData as ProjectWithTeam[]) {
|
||||||
|
if (item.projects && typeof item.projects === 'object' && 'id' in item.projects && 'name' in item.projects) {
|
||||||
|
const project = item.projects as Project;
|
||||||
|
if (item.teams && 'name' in item.teams) {
|
||||||
|
project.team_name = item.teams.name;
|
||||||
|
}
|
||||||
|
// Avoid duplicate projects from different teams
|
||||||
|
if (!projectList.some(p => p.id === project.id)) {
|
||||||
|
projectList.push(project);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setProjects(projectList);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// 否则,获取用户所属的所有项目及其所属团队
|
// If no team IDs, get all user's projects
|
||||||
projectsQuery = supabase
|
const { data: projectsData, error: projectsError } = await supabase
|
||||||
.from('user_projects')
|
.from('user_projects')
|
||||||
.select('project_id, projects:project_id(*)')
|
.select('project_id, projects:project_id(*)')
|
||||||
.eq('user_id', userId)
|
.eq('user_id', userId)
|
||||||
.is('projects.deleted_at', null);
|
.is('projects.deleted_at', null);
|
||||||
}
|
|
||||||
|
|
||||||
const { data: projectsData, error: projectsError } = await projectsQuery;
|
if (projectsError) throw projectsError;
|
||||||
|
|
||||||
if (projectsError) throw projectsError;
|
if (!projectsData || projectsData.length === 0) {
|
||||||
|
if (isMounted) setProjects([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!projectsData || projectsData.length === 0) {
|
// Fetch team info for these projects
|
||||||
if (isMounted) setProjects([]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果没有提供teamId,需要单独获取每个项目对应的团队信息
|
|
||||||
if (!teamId && projectsData.length > 0) {
|
|
||||||
const projectIds = projectsData.map(item => item.project_id);
|
const projectIds = projectsData.map(item => item.project_id);
|
||||||
|
|
||||||
// 获取项目所属的团队信息
|
// Get team info for each project
|
||||||
const { data: teamProjectsData, error: teamProjectsError } = await supabase
|
const { data: teamProjectsData, error: teamProjectsError } = await supabase
|
||||||
.from('team_projects')
|
.from('team_projects')
|
||||||
.select('project_id, teams:team_id(name)')
|
.select('project_id, teams:team_id(name)')
|
||||||
@@ -129,7 +162,7 @@ export function ProjectSelector({
|
|||||||
|
|
||||||
if (teamProjectsError) throw teamProjectsError;
|
if (teamProjectsError) throw teamProjectsError;
|
||||||
|
|
||||||
// 创建项目ID到团队名称的映射
|
// Create project ID to team name mapping
|
||||||
const projectTeamMap: Record<string, string> = {};
|
const projectTeamMap: Record<string, string> = {};
|
||||||
if (teamProjectsData) {
|
if (teamProjectsData) {
|
||||||
teamProjectsData.forEach(item => {
|
teamProjectsData.forEach(item => {
|
||||||
@@ -139,7 +172,7 @@ export function ProjectSelector({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 提取项目数据,并添加团队名称
|
// Extract projects with team names
|
||||||
if (isMounted && projectsData) {
|
if (isMounted && projectsData) {
|
||||||
const projectList: Project[] = [];
|
const projectList: Project[] = [];
|
||||||
|
|
||||||
@@ -151,23 +184,6 @@ export function ProjectSelector({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setProjects(projectList);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 如果提供了teamId,直接从查询结果中提取项目和团队信息
|
|
||||||
if (isMounted && projectsData) {
|
|
||||||
const projectList: Project[] = [];
|
|
||||||
|
|
||||||
for (const item of projectsData as ProjectWithTeam[]) {
|
|
||||||
if (item.projects && typeof item.projects === 'object' && 'id' in item.projects && 'name' in item.projects) {
|
|
||||||
const project = item.projects as Project;
|
|
||||||
if (item.teams && 'name' in item.teams) {
|
|
||||||
project.team_name = item.teams.name;
|
|
||||||
}
|
|
||||||
projectList.push(project);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setProjects(projectList);
|
setProjects(projectList);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -203,7 +219,7 @@ export function ProjectSelector({
|
|||||||
isMounted = false;
|
isMounted = false;
|
||||||
subscription.unsubscribe();
|
subscription.unsubscribe();
|
||||||
};
|
};
|
||||||
}, [teamId]);
|
}, [effectiveTeamIds]);
|
||||||
|
|
||||||
const handleToggle = () => {
|
const handleToggle = () => {
|
||||||
if (!loading && !error && projects.length > 0) {
|
if (!loading && !error && projects.length > 0) {
|
||||||
|
|||||||
@@ -30,14 +30,14 @@ export function TagSelector({
|
|||||||
className,
|
className,
|
||||||
multiple = false,
|
multiple = false,
|
||||||
teamId,
|
teamId,
|
||||||
tagType,
|
teamIds,
|
||||||
}: {
|
}: {
|
||||||
value?: string | string[];
|
value?: string | string[];
|
||||||
onChange?: (tagId: string | string[]) => void;
|
onChange?: (tagIds: string | string[]) => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
multiple?: boolean;
|
multiple?: boolean;
|
||||||
teamId?: string; // Optional team ID to filter tags by team
|
teamId?: string; // Optional single team ID
|
||||||
tagType?: string; // Optional tag type for filtering
|
teamIds?: string[]; // Optional array of team IDs
|
||||||
}) {
|
}) {
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
@@ -46,6 +46,16 @@ export function TagSelector({
|
|||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const selectorRef = useRef<HTMLDivElement>(null);
|
const selectorRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// Normalize team IDs to ensure we're always working with an array
|
||||||
|
const effectiveTeamIds = React.useMemo(() => {
|
||||||
|
if (teamIds && teamIds.length > 0) {
|
||||||
|
return teamIds;
|
||||||
|
} else if (teamId) {
|
||||||
|
return [teamId];
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}, [teamId, teamIds]);
|
||||||
|
|
||||||
// 标签名称与ID的映射函数
|
// 标签名称与ID的映射函数
|
||||||
const getTagIdByName = (name: string): string | undefined => {
|
const getTagIdByName = (name: string): string | undefined => {
|
||||||
const tag = tags.find(t => t.name === name);
|
const tag = tags.find(t => t.name === name);
|
||||||
@@ -119,13 +129,8 @@ export function TagSelector({
|
|||||||
let query = supabase.from('tags').select('*').is('deleted_at', null);
|
let query = supabase.from('tags').select('*').is('deleted_at', null);
|
||||||
|
|
||||||
// Filter by team if teamId is provided
|
// Filter by team if teamId is provided
|
||||||
if (teamId) {
|
if (effectiveTeamIds) {
|
||||||
query = query.eq('team_id', teamId);
|
query = query.in('team_id', effectiveTeamIds);
|
||||||
}
|
|
||||||
|
|
||||||
// Filter by tag type if provided
|
|
||||||
if (tagType) {
|
|
||||||
query = query.eq('type', tagType);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data: tagsData, error: tagsError } = await query;
|
const { data: tagsData, error: tagsError } = await query;
|
||||||
@@ -170,7 +175,7 @@ export function TagSelector({
|
|||||||
isMounted = false;
|
isMounted = false;
|
||||||
subscription.unsubscribe();
|
subscription.unsubscribe();
|
||||||
};
|
};
|
||||||
}, [teamId, tagType]);
|
}, [effectiveTeamIds]);
|
||||||
|
|
||||||
const handleToggle = () => {
|
const handleToggle = () => {
|
||||||
if (!loading && !error && tags.length > 0) {
|
if (!loading && !error && tags.length > 0) {
|
||||||
|
|||||||
17
app/page.tsx
17
app/page.tsx
@@ -248,7 +248,18 @@ export default function HomePage() {
|
|||||||
<div className="flex flex-col gap-4 md:flex-row md:items-center">
|
<div className="flex flex-col gap-4 md:flex-row md:items-center">
|
||||||
<TeamSelector
|
<TeamSelector
|
||||||
value={selectedTeamIds}
|
value={selectedTeamIds}
|
||||||
onChange={(value) => setSelectedTeamIds(Array.isArray(value) ? value : [value])}
|
onChange={(value) => {
|
||||||
|
const newTeamIds = Array.isArray(value) ? value : [value];
|
||||||
|
|
||||||
|
// Check if team selection has changed
|
||||||
|
if (JSON.stringify(newTeamIds) !== JSON.stringify(selectedTeamIds)) {
|
||||||
|
// Clear project selection when team changes
|
||||||
|
setSelectedProjectIds([]);
|
||||||
|
|
||||||
|
// Update team selection
|
||||||
|
setSelectedTeamIds(newTeamIds);
|
||||||
|
}
|
||||||
|
}}
|
||||||
className="w-[250px]"
|
className="w-[250px]"
|
||||||
multiple={true}
|
multiple={true}
|
||||||
/>
|
/>
|
||||||
@@ -257,14 +268,14 @@ export default function HomePage() {
|
|||||||
onChange={(value) => setSelectedProjectIds(Array.isArray(value) ? value : [value])}
|
onChange={(value) => setSelectedProjectIds(Array.isArray(value) ? value : [value])}
|
||||||
className="w-[250px]"
|
className="w-[250px]"
|
||||||
multiple={true}
|
multiple={true}
|
||||||
teamId={selectedTeamIds.length === 1 ? selectedTeamIds[0] : undefined}
|
teamIds={selectedTeamIds.length > 0 ? selectedTeamIds : undefined}
|
||||||
/>
|
/>
|
||||||
<TagSelector
|
<TagSelector
|
||||||
value={selectedTagIds}
|
value={selectedTagIds}
|
||||||
onChange={(value) => setSelectedTagIds(Array.isArray(value) ? value : [value])}
|
onChange={(value) => setSelectedTagIds(Array.isArray(value) ? value : [value])}
|
||||||
className="w-[250px]"
|
className="w-[250px]"
|
||||||
multiple={true}
|
multiple={true}
|
||||||
teamId={selectedTeamIds.length === 1 ? selectedTeamIds[0] : undefined}
|
teamIds={selectedTeamIds.length > 0 ? selectedTeamIds : undefined}
|
||||||
/>
|
/>
|
||||||
<DateRangePicker
|
<DateRangePicker
|
||||||
value={dateRange}
|
value={dateRange}
|
||||||
|
|||||||
@@ -181,6 +181,9 @@ export async function getTimeSeriesData(params: {
|
|||||||
endTime: string;
|
endTime: string;
|
||||||
linkId?: string;
|
linkId?: string;
|
||||||
granularity: 'hour' | 'day' | 'week' | 'month';
|
granularity: 'hour' | 'day' | 'week' | 'month';
|
||||||
|
teamIds?: string[];
|
||||||
|
projectIds?: string[];
|
||||||
|
tagIds?: string[];
|
||||||
}): Promise<TimeSeriesData[]> {
|
}): Promise<TimeSeriesData[]> {
|
||||||
const filter = buildFilter(params);
|
const filter = buildFilter(params);
|
||||||
|
|
||||||
@@ -213,9 +216,12 @@ export async function getGeoAnalytics(params: {
|
|||||||
endTime?: string;
|
endTime?: string;
|
||||||
linkId?: string;
|
linkId?: string;
|
||||||
groupBy?: 'country' | 'city';
|
groupBy?: 'country' | 'city';
|
||||||
|
teamIds?: string[];
|
||||||
|
projectIds?: string[];
|
||||||
|
tagIds?: string[];
|
||||||
}): Promise<GeoData[]> {
|
}): Promise<GeoData[]> {
|
||||||
const filter = buildFilter(params);
|
const filter = buildFilter(params);
|
||||||
const groupByField = 'ip_address'; // 暂时按 IP 地址分组
|
const groupByField = params.groupBy === 'city' ? 'city' : 'country';
|
||||||
|
|
||||||
const query = `
|
const query = `
|
||||||
SELECT
|
SELECT
|
||||||
@@ -228,7 +234,7 @@ export async function getGeoAnalytics(params: {
|
|||||||
GROUP BY ${groupByField}
|
GROUP BY ${groupByField}
|
||||||
HAVING location != ''
|
HAVING location != ''
|
||||||
ORDER BY visits DESC
|
ORDER BY visits DESC
|
||||||
LIMIT 10
|
LIMIT 20
|
||||||
`;
|
`;
|
||||||
|
|
||||||
return executeQuery<GeoData>(query);
|
return executeQuery<GeoData>(query);
|
||||||
|
|||||||
Reference in New Issue
Block a user