events filter
This commit is contained in:
@@ -112,9 +112,9 @@ export default function AnalyticsPage() {
|
|||||||
Tag Analytics ({selectedTagIds.length} selected)
|
Tag Analytics ({selectedTagIds.length} selected)
|
||||||
</h2>
|
</h2>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
{selectedTagIds.map((tagId) => (
|
{selectedTagIds.map((tagName) => (
|
||||||
<div key={tagId} className="p-4 border rounded-md">
|
<div key={tagName} className="p-4 border rounded-md">
|
||||||
<h3 className="font-medium text-gray-800">Tag ID: {tagId}</h3>
|
<h3 className="font-medium text-gray-800">Tag: {tagName}</h3>
|
||||||
<p className="text-gray-500 mt-2">Tag analytics will appear here</p>
|
<p className="text-gray-500 mt-2">Tag analytics will appear here</p>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,50 +1,70 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import type { ApiResponse, EventsQueryParams, EventType } from '@/lib/types';
|
import { getEvents, EventsQueryParams } from '@/lib/analytics';
|
||||||
import {
|
import { ApiResponse } from '@/lib/types';
|
||||||
getEvents,
|
|
||||||
getEventsSummary,
|
|
||||||
getTimeSeriesData,
|
|
||||||
getGeoAnalytics,
|
|
||||||
getDeviceAnalytics
|
|
||||||
} from '@/lib/analytics';
|
|
||||||
|
|
||||||
// 获取事件列表
|
// 获取事件列表
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const searchParams = request.nextUrl.searchParams;
|
const { searchParams } = new URL(request.url);
|
||||||
|
|
||||||
|
// 获取查询参数
|
||||||
|
const page = parseInt(searchParams.get('page') || '1');
|
||||||
|
const pageSize = parseInt(searchParams.get('pageSize') || '20');
|
||||||
|
const eventType = searchParams.get('eventType') || undefined;
|
||||||
|
const linkId = searchParams.get('linkId') || undefined;
|
||||||
|
const linkSlug = searchParams.get('linkSlug') || undefined;
|
||||||
|
const userId = searchParams.get('userId') || undefined;
|
||||||
|
|
||||||
|
// 获取可能存在的多个团队、项目和标签ID
|
||||||
|
const teamIds = searchParams.getAll('teamId');
|
||||||
|
const projectIds = searchParams.getAll('projectId');
|
||||||
|
const tagIds = searchParams.getAll('tagId');
|
||||||
|
|
||||||
|
const startTime = searchParams.get('startTime') || undefined;
|
||||||
|
const endTime = searchParams.get('endTime') || undefined;
|
||||||
|
const sortBy = searchParams.get('sortBy') || undefined;
|
||||||
|
const sortOrder = (searchParams.get('sortOrder') as 'asc' | 'desc') || undefined;
|
||||||
|
|
||||||
|
console.log("API接收到的tagIds:", tagIds); // 添加日志便于调试
|
||||||
|
|
||||||
|
// 获取事件列表
|
||||||
const params: EventsQueryParams = {
|
const params: EventsQueryParams = {
|
||||||
startTime: searchParams.get('startTime') || undefined,
|
page,
|
||||||
endTime: searchParams.get('endTime') || undefined,
|
pageSize,
|
||||||
eventType: searchParams.get('eventType') as EventType || undefined,
|
eventType,
|
||||||
linkId: searchParams.get('linkId') || undefined,
|
linkId,
|
||||||
linkSlug: searchParams.get('linkSlug') || undefined,
|
linkSlug,
|
||||||
userId: searchParams.get('userId') || undefined,
|
userId,
|
||||||
teamId: searchParams.get('teamId') || undefined,
|
teamId: teamIds.length > 0 ? teamIds[0] : undefined,
|
||||||
projectId: searchParams.get('projectId') || undefined,
|
teamIds: teamIds.length > 1 ? teamIds : undefined,
|
||||||
page: searchParams.has('page') ? parseInt(searchParams.get('page')!, 10) : 1,
|
projectId: projectIds.length > 0 ? projectIds[0] : undefined,
|
||||||
pageSize: searchParams.has('pageSize') ? parseInt(searchParams.get('pageSize')!, 10) : 20,
|
projectIds: projectIds.length > 1 ? projectIds : undefined,
|
||||||
sortBy: searchParams.get('sortBy') || undefined,
|
tagIds: tagIds.length > 0 ? tagIds : undefined,
|
||||||
sortOrder: (searchParams.get('sortOrder') as 'asc' | 'desc') || undefined
|
startTime,
|
||||||
|
endTime,
|
||||||
|
sortBy,
|
||||||
|
sortOrder
|
||||||
};
|
};
|
||||||
|
|
||||||
const { events, total } = await getEvents(params);
|
const result = await getEvents(params);
|
||||||
|
|
||||||
const response: ApiResponse<typeof events> = {
|
const response: ApiResponse<typeof result.events> = {
|
||||||
success: true,
|
success: true,
|
||||||
data: events,
|
data: result.events,
|
||||||
meta: {
|
meta: {
|
||||||
total,
|
total: result.total,
|
||||||
page: params.page,
|
page,
|
||||||
pageSize: params.pageSize
|
pageSize
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return NextResponse.json(response);
|
return NextResponse.json(response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('获取事件列表失败:', error);
|
||||||
const response: ApiResponse<null> = {
|
const response: ApiResponse<null> = {
|
||||||
success: false,
|
success: false,
|
||||||
error: error instanceof Error ? error.message : 'Unknown error occurred'
|
data: null,
|
||||||
|
error: error instanceof Error ? error.message : '获取事件列表失败'
|
||||||
};
|
};
|
||||||
return NextResponse.json(response, { status: 500 });
|
return NextResponse.json(response, { status: 500 });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,18 +46,47 @@ export function TagSelector({
|
|||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const selectorRef = useRef<HTMLDivElement>(null);
|
const selectorRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
// Initialize selected tags based on value prop
|
// 标签名称与ID的映射函数
|
||||||
useEffect(() => {
|
const getTagIdByName = (name: string): string | undefined => {
|
||||||
if (value) {
|
const tag = tags.find(t => t.name === name);
|
||||||
if (Array.isArray(value)) {
|
return tag?.id;
|
||||||
setSelectedIds(value);
|
};
|
||||||
} else {
|
|
||||||
setSelectedIds(value ? [value] : []);
|
const getTagNameById = (id: string): string | undefined => {
|
||||||
}
|
const tag = tags.find(t => t.id === id);
|
||||||
} else {
|
return tag?.name;
|
||||||
setSelectedIds([]);
|
};
|
||||||
|
|
||||||
|
// 从标签名称转换为标签ID
|
||||||
|
const nameToId = (nameOrNames: string | string[] | undefined): string[] => {
|
||||||
|
if (!nameOrNames) return [];
|
||||||
|
if (Array.isArray(nameOrNames)) {
|
||||||
|
return nameOrNames
|
||||||
|
.map(name => getTagIdByName(name))
|
||||||
|
.filter((id): id is string => !!id);
|
||||||
}
|
}
|
||||||
}, [value]);
|
const id = getTagIdByName(nameOrNames);
|
||||||
|
return id ? [id] : [];
|
||||||
|
};
|
||||||
|
|
||||||
|
// 从标签ID转换为标签名称
|
||||||
|
const idToName = (idOrIds: string | string[] | undefined): string[] => {
|
||||||
|
if (!idOrIds) return [];
|
||||||
|
if (Array.isArray(idOrIds)) {
|
||||||
|
return idOrIds
|
||||||
|
.map(id => getTagNameById(id))
|
||||||
|
.filter((name): name is string => !!name);
|
||||||
|
}
|
||||||
|
const name = getTagNameById(idOrIds);
|
||||||
|
return name ? [name] : [];
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化已选择的标签 - 从传入的名称转换为ID
|
||||||
|
useEffect(() => {
|
||||||
|
if (tags.length > 0 && value) {
|
||||||
|
setSelectedIds(nameToId(value));
|
||||||
|
}
|
||||||
|
}, [value, tags]);
|
||||||
|
|
||||||
// Add click outside listener to close dropdown
|
// Add click outside listener to close dropdown
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -167,8 +196,10 @@ export function TagSelector({
|
|||||||
|
|
||||||
setSelectedIds(newSelected);
|
setSelectedIds(newSelected);
|
||||||
|
|
||||||
|
// 传递标签名称而不是ID
|
||||||
if (onChange) {
|
if (onChange) {
|
||||||
onChange(multiple ? newSelected : newSelected[0] || '');
|
const tagNames = idToName(newSelected);
|
||||||
|
onChange(multiple ? tagNames : tagNames[0] || '');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -176,8 +207,11 @@ export function TagSelector({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const newSelected = selectedIds.filter(id => id !== tagId);
|
const newSelected = selectedIds.filter(id => id !== tagId);
|
||||||
setSelectedIds(newSelected);
|
setSelectedIds(newSelected);
|
||||||
|
|
||||||
|
// 传递标签名称而不是ID
|
||||||
if (onChange) {
|
if (onChange) {
|
||||||
onChange(multiple ? newSelected : newSelected[0] || '');
|
const tagNames = idToName(newSelected);
|
||||||
|
onChange(multiple ? tagNames : tagNames[0] || '');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -214,6 +248,7 @@ export function TagSelector({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 根据已选择的ID筛选出已选择的标签
|
||||||
const selectedTags = tags.filter(tag => selectedIds.includes(tag.id));
|
const selectedTags = tags.filter(tag => selectedIds.includes(tag.id));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
20
app/page.tsx
20
app/page.tsx
@@ -322,11 +322,11 @@ export default function HomePage() {
|
|||||||
{selectedTagIds.length === 1 ? 'Tag filter:' : 'Tags filter:'}
|
{selectedTagIds.length === 1 ? 'Tag filter:' : 'Tags filter:'}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{selectedTagIds.map(tagId => (
|
{selectedTagIds.map(tagName => (
|
||||||
<span key={tagId} className="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full">
|
<span key={tagName} className="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full">
|
||||||
{tagId}
|
{tagName}
|
||||||
<button
|
<button
|
||||||
onClick={() => setSelectedTagIds(selectedTagIds.filter(id => id !== tagId))}
|
onClick={() => setSelectedTagIds(selectedTagIds.filter(name => name !== tagName))}
|
||||||
className="ml-1 text-blue-600 hover:text-blue-800"
|
className="ml-1 text-blue-600 hover:text-blue-800"
|
||||||
>
|
>
|
||||||
×
|
×
|
||||||
@@ -350,7 +350,7 @@ export default function HomePage() {
|
|||||||
<div className="p-6 border-b border-gray-200">
|
<div className="p-6 border-b border-gray-200">
|
||||||
<h2 className="text-lg font-semibold text-gray-900 mb-4">Recent Events</h2>
|
<h2 className="text-lg font-semibold text-gray-900 mb-4">Recent Events</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="min-w-full divide-y divide-gray-200">
|
<table className="min-w-full divide-y divide-gray-200">
|
||||||
<thead className="bg-gray-50">
|
<thead className="bg-gray-50">
|
||||||
@@ -473,19 +473,19 @@ export default function HomePage() {
|
|||||||
<p className="text-2xl font-semibold text-gray-900">
|
<p className="text-2xl font-semibold text-gray-900">
|
||||||
{typeof summary.totalEvents === 'number' ? summary.totalEvents.toLocaleString() : summary.totalEvents}
|
{typeof summary.totalEvents === 'number' ? summary.totalEvents.toLocaleString() : summary.totalEvents}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-white rounded-lg shadow p-6">
|
<div className="bg-white rounded-lg shadow p-6">
|
||||||
<h3 className="text-sm font-medium text-gray-500">Unique Visitors</h3>
|
<h3 className="text-sm font-medium text-gray-500">Unique Visitors</h3>
|
||||||
<p className="text-2xl font-semibold text-gray-900">
|
<p className="text-2xl font-semibold text-gray-900">
|
||||||
{typeof summary.uniqueVisitors === 'number' ? summary.uniqueVisitors.toLocaleString() : summary.uniqueVisitors}
|
{typeof summary.uniqueVisitors === 'number' ? summary.uniqueVisitors.toLocaleString() : summary.uniqueVisitors}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-white rounded-lg shadow p-6">
|
<div className="bg-white rounded-lg shadow p-6">
|
||||||
<h3 className="text-sm font-medium text-gray-500">Total Conversions</h3>
|
<h3 className="text-sm font-medium text-gray-500">Total Conversions</h3>
|
||||||
<p className="text-2xl font-semibold text-gray-900">
|
<p className="text-2xl font-semibold text-gray-900">
|
||||||
{typeof summary.totalConversions === 'number' ? summary.totalConversions.toLocaleString() : summary.totalConversions}
|
{typeof summary.totalConversions === 'number' ? summary.totalConversions.toLocaleString() : summary.totalConversions}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-white rounded-lg shadow p-6">
|
<div className="bg-white rounded-lg shadow p-6">
|
||||||
<h3 className="text-sm font-medium text-gray-500">Avg. Time Spent</h3>
|
<h3 className="text-sm font-medium text-gray-500">Avg. Time Spent</h3>
|
||||||
<p className="text-2xl font-semibold text-gray-900">
|
<p className="text-2xl font-semibold text-gray-900">
|
||||||
@@ -510,8 +510,8 @@ export default function HomePage() {
|
|||||||
<div className="bg-white rounded-lg shadow p-6 mb-8">
|
<div className="bg-white rounded-lg shadow p-6 mb-8">
|
||||||
<h2 className="text-lg font-semibold text-gray-900 mb-4">Geographic Distribution</h2>
|
<h2 className="text-lg font-semibold text-gray-900 mb-4">Geographic Distribution</h2>
|
||||||
<GeoAnalytics data={geoData} />
|
<GeoAnalytics data={geoData} />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -9,8 +9,8 @@ export enum TimeGranularity {
|
|||||||
MONTH = 'month'
|
MONTH = 'month'
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取事件列表
|
// 事件查询参数类型
|
||||||
export async function getEvents(params: {
|
export interface EventsQueryParams {
|
||||||
startTime?: string;
|
startTime?: string;
|
||||||
endTime?: string;
|
endTime?: string;
|
||||||
eventType?: string;
|
eventType?: string;
|
||||||
@@ -19,11 +19,17 @@ export async function getEvents(params: {
|
|||||||
userId?: string;
|
userId?: string;
|
||||||
teamId?: string;
|
teamId?: string;
|
||||||
projectId?: string;
|
projectId?: string;
|
||||||
|
teamIds?: string[];
|
||||||
|
projectIds?: string[];
|
||||||
|
tagIds?: string[];
|
||||||
page?: number;
|
page?: number;
|
||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
sortBy?: string;
|
sortBy?: string;
|
||||||
sortOrder?: 'asc' | 'desc';
|
sortOrder?: 'asc' | 'desc';
|
||||||
}): Promise<{ events: Event[]; total: number }> {
|
}
|
||||||
|
|
||||||
|
// 获取事件列表
|
||||||
|
export async function getEvents(params: EventsQueryParams): Promise<{ events: Event[]; total: number }> {
|
||||||
const filter = buildFilter(params);
|
const filter = buildFilter(params);
|
||||||
const pagination = buildPagination(params.page, params.pageSize);
|
const pagination = buildPagination(params.page, params.pageSize);
|
||||||
const orderBy = buildOrderBy(params.sortBy, params.sortOrder);
|
const orderBy = buildOrderBy(params.sortBy, params.sortOrder);
|
||||||
|
|||||||
@@ -28,95 +28,108 @@ function buildDateFilter(startTime?: string, endTime?: string): string {
|
|||||||
export function buildFilter(params: Partial<EventsQueryParams>): string {
|
export function buildFilter(params: Partial<EventsQueryParams>): string {
|
||||||
const filters = [];
|
const filters = [];
|
||||||
|
|
||||||
// 时间范围过滤
|
// 添加日期过滤条件
|
||||||
if (params.startTime || params.endTime) {
|
if (params.startTime || params.endTime) {
|
||||||
const dateFilter = buildDateFilter(params.startTime, params.endTime).replace('WHERE ', '');
|
const dateFilter = buildDateFilter(params.startTime, params.endTime);
|
||||||
if (dateFilter) {
|
if (dateFilter) {
|
||||||
filters.push(dateFilter);
|
filters.push(dateFilter.replace('WHERE ', ''));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 事件类型过滤
|
// 添加事件类型过滤条件
|
||||||
if (params.eventType) {
|
if (params.eventType) {
|
||||||
filters.push(`event_type = '${params.eventType}'`);
|
filters.push(`event_type = '${params.eventType}'`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 链接ID过滤
|
// 添加链接ID过滤条件
|
||||||
if (params.linkId) {
|
if (params.linkId) {
|
||||||
filters.push(`link_id = '${params.linkId}'`);
|
filters.push(`link_id = '${params.linkId}'`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 链接短码过滤
|
// 添加链接Slug过滤条件
|
||||||
if (params.linkSlug) {
|
if (params.linkSlug) {
|
||||||
filters.push(`link_slug = '${params.linkSlug}'`);
|
filters.push(`link_slug = '${params.linkSlug}'`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 用户ID过滤
|
// 添加用户ID过滤条件
|
||||||
if (params.userId) {
|
if (params.userId) {
|
||||||
filters.push(`user_id = '${params.userId}'`);
|
filters.push(`user_id = '${params.userId}'`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 团队ID过滤 - 支持多选
|
// 添加团队ID过滤条件
|
||||||
if (params.teamIds && params.teamIds.length > 0) {
|
if (params.teamId) {
|
||||||
const teamValues = params.teamIds.map(id => `'${id}'`).join(', ');
|
|
||||||
filters.push(`team_id IN (${teamValues})`);
|
|
||||||
} else if (params.teamId) {
|
|
||||||
filters.push(`team_id = '${params.teamId}'`);
|
filters.push(`team_id = '${params.teamId}'`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 项目ID过滤 - 支持多选
|
// 处理多个团队ID
|
||||||
if (params.projectIds && params.projectIds.length > 0) {
|
if (params.teamIds && params.teamIds.length > 0) {
|
||||||
const projectValues = params.projectIds.map(id => `'${id}'`).join(', ');
|
filters.push(`team_id IN (${params.teamIds.map(id => `'${id}'`).join(', ')})`);
|
||||||
filters.push(`project_id IN (${projectValues})`);
|
}
|
||||||
} else if (params.projectId) {
|
|
||||||
|
// 添加项目ID过滤条件
|
||||||
|
if (params.projectId) {
|
||||||
filters.push(`project_id = '${params.projectId}'`);
|
filters.push(`project_id = '${params.projectId}'`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 标签ID过滤 - 支持多选
|
// 处理多个项目ID
|
||||||
|
if (params.projectIds && params.projectIds.length > 0) {
|
||||||
|
filters.push(`project_id IN (${params.projectIds.map(id => `'${id}'`).join(', ')})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理标签过滤 - 使用LIKE来匹配标签字符串
|
||||||
if (params.tagIds && params.tagIds.length > 0) {
|
if (params.tagIds && params.tagIds.length > 0) {
|
||||||
// 假设我们在link_tags字段存储标签ID的JSON数组
|
const tagConditions = params.tagIds.map(tag =>
|
||||||
const tagConditions = params.tagIds.map(id => `arrayExists(x -> x = '${id}', JSONExtractArrayRaw(link_tags))`);
|
`link_tags LIKE '%${tag}%'`
|
||||||
|
);
|
||||||
filters.push(`(${tagConditions.join(' OR ')})`);
|
filters.push(`(${tagConditions.join(' OR ')})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : '';
|
return filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 构建分页
|
// 构建分页条件
|
||||||
export function buildPagination(page?: number, pageSize?: number): string {
|
export function buildPagination(page: number = 1, pageSize: number = 20): string {
|
||||||
const limit = pageSize || 20;
|
const offset = (page - 1) * pageSize;
|
||||||
const offset = ((page || 1) - 1) * limit;
|
return `LIMIT ${pageSize} OFFSET ${offset}`;
|
||||||
return `LIMIT ${limit} OFFSET ${offset}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 构建排序
|
// 构建排序条件
|
||||||
export function buildOrderBy(sortBy?: string, sortOrder?: 'asc' | 'desc'): string {
|
export function buildOrderBy(sortBy: string = 'event_time', sortOrder: string = 'desc'): string {
|
||||||
if (!sortBy) {
|
return `ORDER BY ${sortBy} ${sortOrder}`;
|
||||||
return 'ORDER BY event_time DESC';
|
|
||||||
}
|
|
||||||
return `ORDER BY ${sortBy} ${sortOrder || 'desc'}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 执行查询并处理错误
|
// 执行查询
|
||||||
export async function executeQuery<T>(query: string): Promise<T[]> {
|
export async function executeQuery(query: string) {
|
||||||
|
console.log('执行查询:', query); // 查询日志
|
||||||
try {
|
try {
|
||||||
const resultSet = await clickhouse.query({
|
const resultSet = await clickhouse.query({
|
||||||
query,
|
query,
|
||||||
format: 'JSONEachRow'
|
format: 'JSONEachRow',
|
||||||
});
|
});
|
||||||
|
|
||||||
const rows = await resultSet.json<T>();
|
const rows = await resultSet.json();
|
||||||
return Array.isArray(rows) ? rows : [rows];
|
return rows;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('ClickHouse query error:', error);
|
console.error('查询执行错误:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 执行查询并返回单个结果
|
// 执行返回单一结果的查询
|
||||||
export async function executeQuerySingle<T>(query: string): Promise<T | null> {
|
export async function executeQuerySingle(query: string) {
|
||||||
const results = await executeQuery<T>(query);
|
console.log('执行单一结果查询:', query); // 查询日志
|
||||||
return results.length > 0 ? results[0] : null;
|
try {
|
||||||
|
const resultSet = await clickhouse.query({
|
||||||
|
query,
|
||||||
|
format: 'JSONEachRow',
|
||||||
|
});
|
||||||
|
|
||||||
|
const rows = await resultSet.json();
|
||||||
|
return rows.length > 0 ? rows[0] : null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('单一结果查询执行错误:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default clickhouse;
|
export default clickhouse;
|
||||||
10
lib/types.ts
10
lib/types.ts
@@ -24,6 +24,16 @@ export enum DeviceType {
|
|||||||
OTHER = 'other'
|
OTHER = 'other'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 标签类型
|
||||||
|
export interface Tag {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
color?: string;
|
||||||
|
type?: string;
|
||||||
|
attributes?: Record<string, any>;
|
||||||
|
team_id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
// API 响应基础接口
|
// API 响应基础接口
|
||||||
export interface ApiResponse<T> {
|
export interface ApiResponse<T> {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
|
|||||||
Reference in New Issue
Block a user