forked from wangziqi/gongxue-base
feat: add feedback operations report
This commit is contained in:
@@ -17,6 +17,7 @@ import {
|
||||
loadCoupons,
|
||||
loadCrmConfig,
|
||||
loadCrmQueue,
|
||||
loadFeedbackReport,
|
||||
loadPointActivityClaims,
|
||||
loadPointActivityTasks,
|
||||
loadPointExchangeItems,
|
||||
@@ -43,6 +44,7 @@ import {
|
||||
type CouponReport,
|
||||
type CrmConfigItem,
|
||||
type CrmQueueItem,
|
||||
type FeedbackReport,
|
||||
type PointActivityTaskItem,
|
||||
type PointExchangeItem,
|
||||
type PointsRiskReport,
|
||||
@@ -307,6 +309,7 @@ export default function TenantMarketingPage() {
|
||||
const [pointExchangeItems, setPointExchangeItems] = useState<PointExchangeItem[]>([]);
|
||||
const [pointExchangeOrders, setPointExchangeOrders] = useState<Record<string, unknown>[]>([]);
|
||||
const [pointsRiskReport, setPointsRiskReport] = useState<PointsRiskReport | null>(null);
|
||||
const [feedbackReport, setFeedbackReport] = useState<FeedbackReport | null>(null);
|
||||
const [crmForm, setCrmForm] = useState({
|
||||
enabled: false,
|
||||
url: '',
|
||||
@@ -378,6 +381,7 @@ export default function TenantMarketingPage() {
|
||||
pointExchangePayload,
|
||||
pointExchangeOrderPayload,
|
||||
pointsRiskPayload,
|
||||
feedbackReportPayload,
|
||||
] = await Promise.all([
|
||||
loadCoupons({ status: couponFilter.status || undefined, campaignName: couponFilter.campaignName || undefined }).catch(() => ({ items: [] })),
|
||||
loadCouponReport({
|
||||
@@ -411,6 +415,7 @@ export default function TenantMarketingPage() {
|
||||
limit: 30,
|
||||
}).catch(() => ({ items: [] })),
|
||||
loadPointsRiskReport({ timeRange: '30d', limit: 20 }).catch(() => ({ item: null })),
|
||||
loadFeedbackReport({ timeRange: '30d', limit: 20 }).catch(() => ({ item: null })),
|
||||
]);
|
||||
const nextCrm = crmConfigPayload.item || null;
|
||||
const nextSettings = commissionSettingsPayload.item || null;
|
||||
@@ -433,6 +438,7 @@ export default function TenantMarketingPage() {
|
||||
setPointExchangeItems(pointExchangePayload.items || []);
|
||||
setPointExchangeOrders(pointExchangeOrderPayload.items || []);
|
||||
setPointsRiskReport(pointsRiskPayload.item || null);
|
||||
setFeedbackReport(feedbackReportPayload.item || null);
|
||||
if (nextCrm) {
|
||||
setCrmForm({
|
||||
enabled: nextCrm.enabled === true,
|
||||
@@ -610,7 +616,7 @@ export default function TenantMarketingPage() {
|
||||
setBusy('points');
|
||||
setError('');
|
||||
try {
|
||||
const [taskPayload, claimPayload, exchangePayload, orderPayload, riskPayload] = await Promise.all([
|
||||
const [taskPayload, claimPayload, exchangePayload, orderPayload, riskPayload, feedbackReportPayload] = await Promise.all([
|
||||
loadPointActivityTasks({ status: nextFilter.taskStatus || undefined, limit: 100 }),
|
||||
loadPointActivityClaims({
|
||||
taskId: nextFilter.selectedTaskId || undefined,
|
||||
@@ -625,12 +631,14 @@ export default function TenantMarketingPage() {
|
||||
limit: 50,
|
||||
}).catch(() => ({ items: [] })),
|
||||
loadPointsRiskReport({ timeRange: '30d', limit: 20 }).catch(() => ({ item: null })),
|
||||
loadFeedbackReport({ timeRange: '30d', limit: 20 }).catch(() => ({ item: null })),
|
||||
]);
|
||||
setPointTasks(taskPayload.items || []);
|
||||
setPointClaims(claimPayload.items || []);
|
||||
setPointExchangeItems(exchangePayload.items || []);
|
||||
setPointExchangeOrders(orderPayload.items || []);
|
||||
setPointsRiskReport(riskPayload.item || null);
|
||||
setFeedbackReport(feedbackReportPayload.item || null);
|
||||
} catch (nextError) {
|
||||
setError(nextError instanceof Error ? nextError.message : '积分运营数据加载失败');
|
||||
} finally {
|
||||
@@ -980,6 +988,39 @@ export default function TenantMarketingPage() {
|
||||
<View className='admin-metric'><Text className='admin-metric-label'>预计佣金</Text><Text className='admin-metric-value'>{money(commission?.commissionAmountCents)}</Text></View>
|
||||
</View>
|
||||
|
||||
<View className='admin-section'>
|
||||
<Text className='admin-section-title'>反馈运营摘要</Text>
|
||||
<View className='admin-grid'>
|
||||
<View className='admin-metric'><Text className='admin-metric-label'>30 天反馈</Text><Text className='admin-metric-value'>{String(feedbackReport?.summary?.total || 0)}</Text></View>
|
||||
<View className='admin-metric'><Text className='admin-metric-label'>待处理积压</Text><Text className='admin-metric-value'>{String(feedbackReport?.summary?.pendingBacklog || 0)}</Text></View>
|
||||
<View className='admin-metric'><Text className='admin-metric-label'>处理率</Text><Text className='admin-metric-value'>{percentLabel(feedbackReport?.summary?.handledRate)}</Text></View>
|
||||
<View className='admin-metric'><Text className='admin-metric-label'>奖励积分</Text><Text className='admin-metric-value'>{String(feedbackReport?.summary?.rewardPoints || 0)}</Text></View>
|
||||
</View>
|
||||
<View className='admin-grid'>
|
||||
<View className='admin-metric'><Text className='admin-metric-label'>高优先级</Text><Text className='admin-metric-value'>{String(feedbackReport?.summary?.highPriority || 0)}</Text></View>
|
||||
<View className='admin-metric'><Text className='admin-metric-label'>已解决</Text><Text className='admin-metric-value'>{String((feedbackReport?.summary?.resolved || 0) + (feedbackReport?.summary?.closed || 0))}</Text></View>
|
||||
<View className='admin-metric'><Text className='admin-metric-label'>平均处理</Text><Text className='admin-metric-value'>{String(feedbackReport?.summary?.avgHandleHours || 0)}h</Text></View>
|
||||
<View className='admin-metric'><Text className='admin-metric-label'>采纳率</Text><Text className='admin-metric-value'>{percentLabel(feedbackReport?.summary?.resolutionRate)}</Text></View>
|
||||
</View>
|
||||
<View className='admin-list'>
|
||||
{(feedbackReport?.recentUnhandled || []).slice(0, 4).map((item, index) => (
|
||||
<View className='admin-row' key={String(item.id || index)}>
|
||||
<Text className='admin-row-main'>{String(item.title || item.type || '待处理反馈')} · {String(item.priority || 'normal')}</Text>
|
||||
<Text className='admin-row-meta'>{String(item.userName || item.userPhone || item.userId || '学生')} · {String(item.status || '-')} · 已等待 {String(item.ageHours || 0)}h</Text>
|
||||
</View>
|
||||
))}
|
||||
{(feedbackReport?.topQuestions || []).slice(0, 4).map((item, index) => (
|
||||
<View className='admin-row' key={String(item.questionId || index)}>
|
||||
<Text className='admin-row-main'>{String(item.questionPreview || item.questionId || '高频反馈题目')}</Text>
|
||||
<Text className='admin-row-meta'>{String(item.questionType || '题目')} · 反馈 {String(item.reportCount || 0)} 次 · 高优先级 {String(item.highPriority || 0)}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
{!(feedbackReport?.recentUnhandled?.length || feedbackReport?.topQuestions?.length) ? (
|
||||
<View className='admin-empty'>当前 30 天暂无反馈运营风险项,或当前角色没有 `feedback:read` 权限。</View>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
<View className='admin-section'>
|
||||
<Text className='admin-section-title'>积分任务与兑换</Text>
|
||||
<View className='admin-grid'>
|
||||
|
||||
@@ -606,6 +606,40 @@ export interface PointsRiskReport {
|
||||
dailyTrend?: Array<Record<string, unknown>>;
|
||||
}
|
||||
|
||||
export interface FeedbackReport {
|
||||
range?: {
|
||||
timeRange?: '7d' | '30d' | '90d';
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
};
|
||||
summary?: {
|
||||
total?: number;
|
||||
newInRange?: number;
|
||||
pending?: number;
|
||||
accepted?: number;
|
||||
rejected?: number;
|
||||
resolved?: number;
|
||||
closed?: number;
|
||||
highPriority?: number;
|
||||
pendingBacklog?: number;
|
||||
handledInRange?: number;
|
||||
rewardedReports?: number;
|
||||
rewardPoints?: number;
|
||||
resolutionRate?: number;
|
||||
handledRate?: number;
|
||||
rejectionRate?: number;
|
||||
avgHandleHours?: number;
|
||||
};
|
||||
byStatus?: Array<Record<string, unknown>>;
|
||||
byType?: Array<Record<string, unknown>>;
|
||||
byCategory?: Array<Record<string, unknown>>;
|
||||
byPriority?: Array<Record<string, unknown>>;
|
||||
dailyTrend?: Array<Record<string, unknown>>;
|
||||
topQuestions?: Array<Record<string, unknown>>;
|
||||
topHandlers?: Array<Record<string, unknown>>;
|
||||
recentUnhandled?: Array<Record<string, unknown>>;
|
||||
}
|
||||
|
||||
export interface CodeBatchItem {
|
||||
id: string;
|
||||
name?: string | null;
|
||||
@@ -1282,6 +1316,15 @@ export async function loadPointsRiskReport(query: {
|
||||
});
|
||||
}
|
||||
|
||||
export async function loadFeedbackReport(query: {
|
||||
timeRange?: '7d' | '30d' | '90d';
|
||||
limit?: number;
|
||||
} = {}) {
|
||||
return apiRequest<{ item?: FeedbackReport }>('/api/tenant-admin/feedbacks/report', {
|
||||
query: { ...query, limit: query.limit || 20 },
|
||||
});
|
||||
}
|
||||
|
||||
export async function loadCodeBatches() {
|
||||
return apiRequest<{ items?: CodeBatchItem[] }>('/api/tenant-admin/code-batches');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user