@quantabit/report-sdk 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +104 -0
- package/dist/index.cjs +813 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.esm.js +794 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/styles.css +1 -0
- package/package.json +83 -0
- package/types/index.d.ts +60 -0
|
@@ -0,0 +1,794 @@
|
|
|
1
|
+
import React, { useState, useEffect, useCallback, useContext, createContext } from 'react';
|
|
2
|
+
import { BaseApiClient } from '@quantabit/sdk-config';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Report SDK - API 客户端
|
|
6
|
+
* 举报系统后端接口封装
|
|
7
|
+
*
|
|
8
|
+
* 使用 BaseApiClient 基类简化代码
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 举报 API 客户端
|
|
14
|
+
*/
|
|
15
|
+
class ReportApiClient extends BaseApiClient {
|
|
16
|
+
constructor(config = {}) {
|
|
17
|
+
super('/report', config);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// ============ 举报提交 ============
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 提交举报
|
|
24
|
+
* @param {Object} data - 举报数据
|
|
25
|
+
*/
|
|
26
|
+
async submit(data) {
|
|
27
|
+
return this.post('/', data);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 举报用户
|
|
32
|
+
* @param {string} userId - 被举报用户 ID
|
|
33
|
+
* @param {Object} data - 举报信息
|
|
34
|
+
*/
|
|
35
|
+
async reportUser(userId, data) {
|
|
36
|
+
return this.post('/user', {
|
|
37
|
+
target_id: userId,
|
|
38
|
+
...data
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 举报内容
|
|
44
|
+
* @param {string} contentId - 内容 ID
|
|
45
|
+
* @param {string} contentType - 内容类型
|
|
46
|
+
* @param {Object} data - 举报信息
|
|
47
|
+
*/
|
|
48
|
+
async reportContent(contentId, contentType, data) {
|
|
49
|
+
return this.post('/content', {
|
|
50
|
+
content_id: contentId,
|
|
51
|
+
content_type: contentType,
|
|
52
|
+
...data
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 举报评论
|
|
58
|
+
* @param {string} commentId - 评论 ID
|
|
59
|
+
* @param {Object} data - 举报信息
|
|
60
|
+
*/
|
|
61
|
+
async reportComment(commentId, data) {
|
|
62
|
+
return this.post('/comment', {
|
|
63
|
+
comment_id: commentId,
|
|
64
|
+
...data
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ============ 举报查询 ============
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 获取我的举报列表
|
|
72
|
+
* @param {Object} params - 查询参数
|
|
73
|
+
*/
|
|
74
|
+
async getMyReports(params = {}) {
|
|
75
|
+
return this.get('/my', params);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 获取举报详情
|
|
80
|
+
* @param {string} reportId - 举报 ID
|
|
81
|
+
*/
|
|
82
|
+
async getReport(reportId) {
|
|
83
|
+
return this.get(`/${reportId}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 获取举报处理进度
|
|
88
|
+
* @param {string} reportId - 举报 ID
|
|
89
|
+
*/
|
|
90
|
+
async getReportProgress(reportId) {
|
|
91
|
+
return this.get(`/${reportId}/progress`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 撤回举报
|
|
96
|
+
* @param {string} reportId - 举报 ID
|
|
97
|
+
*/
|
|
98
|
+
async withdraw(reportId) {
|
|
99
|
+
return this.post(`/${reportId}/withdraw`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ============ 举报类型 ============
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* 获取举报类型列表
|
|
106
|
+
* @param {string} category - 分类
|
|
107
|
+
*/
|
|
108
|
+
async getReportTypes(category = null) {
|
|
109
|
+
const params = category ? {
|
|
110
|
+
category
|
|
111
|
+
} : {};
|
|
112
|
+
return this.get('/types', params);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 获取举报类型分类
|
|
117
|
+
*/
|
|
118
|
+
async getReportCategories() {
|
|
119
|
+
return this.get('/categories');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ============ 管理员操作 ============
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* 获取待处理举报(管理员)
|
|
126
|
+
* @param {Object} params - 查询参数
|
|
127
|
+
*/
|
|
128
|
+
async getPendingReports(params = {}) {
|
|
129
|
+
return this.get('/admin/pending', params);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* 处理举报(管理员)
|
|
134
|
+
* @param {string} reportId - 举报 ID
|
|
135
|
+
* @param {Object} decision - 处理决定
|
|
136
|
+
*/
|
|
137
|
+
async handleReport(reportId, decision) {
|
|
138
|
+
return this.post(`/admin/${reportId}/handle`, decision);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* 批量处理举报(管理员)
|
|
143
|
+
* @param {string[]} reportIds - 举报 ID 列表
|
|
144
|
+
* @param {Object} decision - 处理决定
|
|
145
|
+
*/
|
|
146
|
+
async batchHandle(reportIds, decision) {
|
|
147
|
+
return this.post('/admin/batch-handle', {
|
|
148
|
+
report_ids: reportIds,
|
|
149
|
+
...decision
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* 分配举报(管理员)
|
|
155
|
+
* @param {string} reportId - 举报 ID
|
|
156
|
+
* @param {string} handlerId - 处理人 ID
|
|
157
|
+
*/
|
|
158
|
+
async assignReport(reportId, handlerId) {
|
|
159
|
+
return this.post(`/admin/${reportId}/assign`, {
|
|
160
|
+
handler_id: handlerId
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ============ 统计 ============
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* 获取举报统计(管理员)
|
|
168
|
+
* @param {Object} params - 统计参数
|
|
169
|
+
*/
|
|
170
|
+
async getStats(params = {}) {
|
|
171
|
+
return this.get('/admin/stats', params);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* 获取举报趋势(管理员)
|
|
176
|
+
* @param {Object} params - 查询参数
|
|
177
|
+
*/
|
|
178
|
+
async getTrend(params = {}) {
|
|
179
|
+
return this.get('/admin/trend', params);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// 创建默认实例
|
|
184
|
+
const reportApi = new ReportApiClient();
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Report SDK - 国际化 i18n
|
|
188
|
+
*/
|
|
189
|
+
|
|
190
|
+
const SUPPORTED_LANGUAGES = ['en', 'zh', 'ja', 'ko'];
|
|
191
|
+
const messages = {
|
|
192
|
+
zh: {
|
|
193
|
+
loading: '加载中...',
|
|
194
|
+
report: '举报',
|
|
195
|
+
reports: '举报记录',
|
|
196
|
+
// 举报类型
|
|
197
|
+
typeContent: '内容违规',
|
|
198
|
+
typeUser: '用户违规',
|
|
199
|
+
typeSpam: '垃圾信息',
|
|
200
|
+
typeFraud: '欺诈诈骗',
|
|
201
|
+
typeHarassment: '骚扰辱骂',
|
|
202
|
+
typeViolence: '暴力内容',
|
|
203
|
+
typePorn: '色情内容',
|
|
204
|
+
typeInfringement: '侵权假冒',
|
|
205
|
+
typeOther: '其他',
|
|
206
|
+
// 举报状态
|
|
207
|
+
statusPending: '待处理',
|
|
208
|
+
statusProcessing: '处理中',
|
|
209
|
+
statusApproved: '已通过',
|
|
210
|
+
statusRejected: '已驳回',
|
|
211
|
+
statusClosed: '已关闭',
|
|
212
|
+
// 表单
|
|
213
|
+
selectType: '选择举报类型',
|
|
214
|
+
describe: '问题描述',
|
|
215
|
+
describePlaceholder: '请详细描述您遇到的问题...',
|
|
216
|
+
uploadEvidence: '上传证据',
|
|
217
|
+
uploadHint: '支持图片、视频、截图',
|
|
218
|
+
contact: '联系方式(选填)',
|
|
219
|
+
// 操作
|
|
220
|
+
submit: '提交举报',
|
|
221
|
+
cancel: '取消',
|
|
222
|
+
viewDetail: '查看详情',
|
|
223
|
+
// 结果
|
|
224
|
+
submitSuccess: '举报提交成功',
|
|
225
|
+
submitSuccessHint: '我们会尽快处理您的举报',
|
|
226
|
+
// 审核(管理端)
|
|
227
|
+
review: '审核',
|
|
228
|
+
approve: '通过(已处理)',
|
|
229
|
+
reject: '驳回',
|
|
230
|
+
reviewNote: '审核备注',
|
|
231
|
+
// 提示
|
|
232
|
+
noReports: '暂无举报记录',
|
|
233
|
+
thanksForReport: '感谢您的反馈'
|
|
234
|
+
},
|
|
235
|
+
en: {
|
|
236
|
+
loading: 'Loading...',
|
|
237
|
+
report: 'Report',
|
|
238
|
+
reports: 'Reports',
|
|
239
|
+
typeContent: 'Content Violation',
|
|
240
|
+
typeUser: 'User Violation',
|
|
241
|
+
typeSpam: 'Spam',
|
|
242
|
+
typeFraud: 'Fraud',
|
|
243
|
+
typeHarassment: 'Harassment',
|
|
244
|
+
typeViolence: 'Violence',
|
|
245
|
+
typePorn: 'Adult Content',
|
|
246
|
+
typeInfringement: 'Infringement',
|
|
247
|
+
typeOther: 'Other',
|
|
248
|
+
statusPending: 'Pending',
|
|
249
|
+
statusProcessing: 'Processing',
|
|
250
|
+
statusApproved: 'Approved',
|
|
251
|
+
statusRejected: 'Rejected',
|
|
252
|
+
statusClosed: 'Closed',
|
|
253
|
+
selectType: 'Select Report Type',
|
|
254
|
+
describe: 'Description',
|
|
255
|
+
describePlaceholder: 'Please describe the issue...',
|
|
256
|
+
uploadEvidence: 'Upload Evidence',
|
|
257
|
+
uploadHint: 'Images, videos, or screenshots',
|
|
258
|
+
contact: 'Contact (Optional)',
|
|
259
|
+
submit: 'Submit Report',
|
|
260
|
+
cancel: 'Cancel',
|
|
261
|
+
viewDetail: 'View Details',
|
|
262
|
+
submitSuccess: 'Report Submitted',
|
|
263
|
+
submitSuccessHint: 'We will review your report shortly',
|
|
264
|
+
review: 'Review',
|
|
265
|
+
approve: 'Approve',
|
|
266
|
+
reject: 'Reject',
|
|
267
|
+
reviewNote: 'Review Note',
|
|
268
|
+
noReports: 'No reports',
|
|
269
|
+
thanksForReport: 'Thank you for your feedback'
|
|
270
|
+
},
|
|
271
|
+
ja: {
|
|
272
|
+
loading: '読み込み中...',
|
|
273
|
+
report: '通報',
|
|
274
|
+
reports: '通報履歴',
|
|
275
|
+
typeContent: 'コンテンツ違反',
|
|
276
|
+
typeUser: 'ユーザー違反',
|
|
277
|
+
typeSpam: 'スパム',
|
|
278
|
+
typeFraud: '詐欺',
|
|
279
|
+
typeHarassment: '嫌がらせ',
|
|
280
|
+
typeViolence: '暴力',
|
|
281
|
+
typePorn: 'アダルトコンテンツ',
|
|
282
|
+
typeInfringement: '権利侵害',
|
|
283
|
+
typeOther: 'その他',
|
|
284
|
+
statusPending: '保留中',
|
|
285
|
+
statusProcessing: '処理中',
|
|
286
|
+
statusApproved: '承認済み',
|
|
287
|
+
statusRejected: '拒否されました',
|
|
288
|
+
statusClosed: 'クローズ',
|
|
289
|
+
selectType: '通報タイプを選択',
|
|
290
|
+
describe: '説明',
|
|
291
|
+
describePlaceholder: '問題を説明してください...',
|
|
292
|
+
uploadEvidence: '証拠をアップロード',
|
|
293
|
+
uploadHint: '画像、ビデオ、またはスクリーンショット',
|
|
294
|
+
contact: '連絡先(任意)',
|
|
295
|
+
submit: '通報を送信',
|
|
296
|
+
cancel: 'キャンセル',
|
|
297
|
+
viewDetail: '詳細を見る',
|
|
298
|
+
submitSuccess: '通報を送信しました',
|
|
299
|
+
submitSuccessHint: '通報をすぐに確認します',
|
|
300
|
+
review: '審査',
|
|
301
|
+
approve: '承認',
|
|
302
|
+
reject: '拒否',
|
|
303
|
+
reviewNote: '審査メモ',
|
|
304
|
+
noReports: '通報はありません',
|
|
305
|
+
thanksForReport: 'フィードバックをありがとうございます'
|
|
306
|
+
},
|
|
307
|
+
ko: {
|
|
308
|
+
loading: '로딩 중...',
|
|
309
|
+
report: '신고',
|
|
310
|
+
reports: '신고 내역',
|
|
311
|
+
typeContent: '콘텐츠 위반',
|
|
312
|
+
typeUser: '사용자 위반',
|
|
313
|
+
typeSpam: '스팸',
|
|
314
|
+
typeFraud: '사기',
|
|
315
|
+
typeHarassment: '괴롭힘',
|
|
316
|
+
typeViolence: '폭력',
|
|
317
|
+
typePorn: '음란물',
|
|
318
|
+
typeInfringement: '권리 침해',
|
|
319
|
+
typeOther: '기타',
|
|
320
|
+
statusPending: '대기 중',
|
|
321
|
+
statusProcessing: '처리 중',
|
|
322
|
+
statusApproved: '승인됨',
|
|
323
|
+
statusRejected: '거부됨',
|
|
324
|
+
statusClosed: '닫힘',
|
|
325
|
+
selectType: '신고 유형 선택',
|
|
326
|
+
describe: '설명',
|
|
327
|
+
describePlaceholder: '문제를 설명해주세요...',
|
|
328
|
+
uploadEvidence: '증거 업로드',
|
|
329
|
+
uploadHint: '이미지, 동영상 또는 스크린샷',
|
|
330
|
+
contact: '연락처 (선택 사항)',
|
|
331
|
+
submit: '신고 제출',
|
|
332
|
+
cancel: '취소',
|
|
333
|
+
viewDetail: '자세히 보기',
|
|
334
|
+
submitSuccess: '신고가 제출되었습니다',
|
|
335
|
+
submitSuccessHint: '곧 신고 내용을 확인하겠습니다',
|
|
336
|
+
review: '검토',
|
|
337
|
+
approve: '승인',
|
|
338
|
+
reject: '거부',
|
|
339
|
+
reviewNote: '검토 메모',
|
|
340
|
+
noReports: '신고 내역 없음',
|
|
341
|
+
thanksForReport: '피드백을 보내주셔서 감사합니다'
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
let currentLanguage = 'zh';
|
|
345
|
+
function setLanguage(lang) {
|
|
346
|
+
if (SUPPORTED_LANGUAGES.includes(lang)) {
|
|
347
|
+
currentLanguage = lang;
|
|
348
|
+
if (typeof window !== 'undefined') {
|
|
349
|
+
localStorage.setItem('qbit_did_report_language', lang);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
function getLanguage() {
|
|
354
|
+
if (typeof window !== 'undefined') {
|
|
355
|
+
const saved = localStorage.getItem('qbit_did_report_language');
|
|
356
|
+
if (saved && SUPPORTED_LANGUAGES.includes(saved)) {
|
|
357
|
+
currentLanguage = saved;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return currentLanguage;
|
|
361
|
+
}
|
|
362
|
+
function t(key, params) {
|
|
363
|
+
const msgs = messages[getLanguage()] || messages.zh;
|
|
364
|
+
let text = msgs[key] || key;
|
|
365
|
+
if (params) {
|
|
366
|
+
Object.keys(params).forEach(k => {
|
|
367
|
+
text = text.replace(new RegExp(`\\{${k}\\}`, 'g'), params[k]);
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
return text;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Report SDK - Context Provider
|
|
375
|
+
*/
|
|
376
|
+
|
|
377
|
+
const ReportContext = /*#__PURE__*/createContext(null);
|
|
378
|
+
function ReportProvider({
|
|
379
|
+
children,
|
|
380
|
+
apiUrl,
|
|
381
|
+
token,
|
|
382
|
+
language = 'zh',
|
|
383
|
+
onSubmit,
|
|
384
|
+
onError
|
|
385
|
+
}) {
|
|
386
|
+
const [myReports, setMyReports] = useState([]);
|
|
387
|
+
const [pendingReports, setPendingReports] = useState([]);
|
|
388
|
+
const [stats, setStats] = useState(null);
|
|
389
|
+
const [loading, setLoading] = useState(false);
|
|
390
|
+
const [error, setError] = useState(null);
|
|
391
|
+
useEffect(() => {
|
|
392
|
+
if (apiUrl) reportApi.setBaseUrl(apiUrl);
|
|
393
|
+
if (token) reportApi.setToken(token);
|
|
394
|
+
if (language) setLanguage(language);
|
|
395
|
+
}, [apiUrl, token, language]);
|
|
396
|
+
const handleError = useCallback(err => {
|
|
397
|
+
setError(err.message);
|
|
398
|
+
onError?.(err);
|
|
399
|
+
}, [onError]);
|
|
400
|
+
|
|
401
|
+
// ============ 用户操作 ============
|
|
402
|
+
|
|
403
|
+
const submit = useCallback(async data => {
|
|
404
|
+
setLoading(true);
|
|
405
|
+
setError(null);
|
|
406
|
+
try {
|
|
407
|
+
const result = await reportApi.submit(data);
|
|
408
|
+
onSubmit?.(result);
|
|
409
|
+
return result;
|
|
410
|
+
} catch (err) {
|
|
411
|
+
handleError(err);
|
|
412
|
+
throw err;
|
|
413
|
+
} finally {
|
|
414
|
+
setLoading(false);
|
|
415
|
+
}
|
|
416
|
+
}, [handleError, onSubmit]);
|
|
417
|
+
const loadMyReports = useCallback(async (params = {}) => {
|
|
418
|
+
setLoading(true);
|
|
419
|
+
try {
|
|
420
|
+
const result = await reportApi.getMyReports(params);
|
|
421
|
+
const items = result.items || result.data || result || [];
|
|
422
|
+
setMyReports(items);
|
|
423
|
+
return items;
|
|
424
|
+
} catch (err) {
|
|
425
|
+
handleError(err);
|
|
426
|
+
} finally {
|
|
427
|
+
setLoading(false);
|
|
428
|
+
}
|
|
429
|
+
}, [handleError]);
|
|
430
|
+
|
|
431
|
+
// ============ 管理操作 ============
|
|
432
|
+
|
|
433
|
+
const loadPendingReports = useCallback(async (params = {}) => {
|
|
434
|
+
setLoading(true);
|
|
435
|
+
try {
|
|
436
|
+
const result = await reportApi.getPendingList(params);
|
|
437
|
+
const items = result.items || result.data || result || [];
|
|
438
|
+
setPendingReports(items);
|
|
439
|
+
return items;
|
|
440
|
+
} catch (err) {
|
|
441
|
+
handleError(err);
|
|
442
|
+
} finally {
|
|
443
|
+
setLoading(false);
|
|
444
|
+
}
|
|
445
|
+
}, [handleError]);
|
|
446
|
+
const handleReport = useCallback(async (reportId, data) => {
|
|
447
|
+
setLoading(true);
|
|
448
|
+
try {
|
|
449
|
+
const result = await reportApi.handle(reportId, data);
|
|
450
|
+
await loadPendingReports();
|
|
451
|
+
return result;
|
|
452
|
+
} catch (err) {
|
|
453
|
+
handleError(err);
|
|
454
|
+
throw err;
|
|
455
|
+
} finally {
|
|
456
|
+
setLoading(false);
|
|
457
|
+
}
|
|
458
|
+
}, [handleError, loadPendingReports]);
|
|
459
|
+
const loadStats = useCallback(async () => {
|
|
460
|
+
try {
|
|
461
|
+
const result = await reportApi.getStats();
|
|
462
|
+
setStats(result);
|
|
463
|
+
return result;
|
|
464
|
+
} catch (err) {
|
|
465
|
+
handleError(err);
|
|
466
|
+
}
|
|
467
|
+
}, [handleError]);
|
|
468
|
+
const value = {
|
|
469
|
+
myReports,
|
|
470
|
+
pendingReports,
|
|
471
|
+
stats,
|
|
472
|
+
loading,
|
|
473
|
+
error,
|
|
474
|
+
submit,
|
|
475
|
+
loadMyReports,
|
|
476
|
+
loadPendingReports,
|
|
477
|
+
handleReport,
|
|
478
|
+
loadStats,
|
|
479
|
+
api: reportApi
|
|
480
|
+
};
|
|
481
|
+
return /*#__PURE__*/React.createElement(ReportContext.Provider, {
|
|
482
|
+
value: value
|
|
483
|
+
}, children);
|
|
484
|
+
}
|
|
485
|
+
function useReport() {
|
|
486
|
+
const context = useContext(ReportContext);
|
|
487
|
+
if (!context) {
|
|
488
|
+
throw new Error('useReport must be used within a ReportProvider');
|
|
489
|
+
}
|
|
490
|
+
return context;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Report SDK - 类型定义
|
|
495
|
+
*/
|
|
496
|
+
|
|
497
|
+
// 举报类型
|
|
498
|
+
const ReportType = {
|
|
499
|
+
CONTENT: 'content',
|
|
500
|
+
// 内容违规
|
|
501
|
+
USER: 'user',
|
|
502
|
+
// 用户违规
|
|
503
|
+
SPAM: 'spam',
|
|
504
|
+
// 垃圾信息
|
|
505
|
+
FRAUD: 'fraud',
|
|
506
|
+
// 欺诈
|
|
507
|
+
HARASSMENT: 'harassment',
|
|
508
|
+
// 骚扰
|
|
509
|
+
VIOLENCE: 'violence',
|
|
510
|
+
// 暴力
|
|
511
|
+
PORN: 'porn',
|
|
512
|
+
// 色情
|
|
513
|
+
INFRINGEMENT: 'infringement',
|
|
514
|
+
// 侵权
|
|
515
|
+
OTHER: 'other' // 其他
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
// 举报目标类型
|
|
519
|
+
const TargetType = {
|
|
520
|
+
POST: 'post',
|
|
521
|
+
// 帖子
|
|
522
|
+
COMMENT: 'comment',
|
|
523
|
+
// 评论
|
|
524
|
+
USER: 'user',
|
|
525
|
+
// 用户
|
|
526
|
+
MESSAGE: 'message',
|
|
527
|
+
// 消息
|
|
528
|
+
GROUP: 'group',
|
|
529
|
+
// 群组
|
|
530
|
+
PRODUCT: 'product' // 商品
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
// 举报状态
|
|
534
|
+
const ReportStatus = {
|
|
535
|
+
PENDING: 'pending',
|
|
536
|
+
// 待处理
|
|
537
|
+
PROCESSING: 'processing',
|
|
538
|
+
// 处理中
|
|
539
|
+
APPROVED: 'approved',
|
|
540
|
+
// 已通过(举报成立)
|
|
541
|
+
REJECTED: 'rejected',
|
|
542
|
+
// 已驳回
|
|
543
|
+
CLOSED: 'closed' // 已关闭
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
// 处理结果
|
|
547
|
+
const HandleResult = {
|
|
548
|
+
WARNING: 'warning',
|
|
549
|
+
// 警告
|
|
550
|
+
DELETE: 'delete',
|
|
551
|
+
// 删除内容
|
|
552
|
+
MUTE: 'mute',
|
|
553
|
+
// 禁言
|
|
554
|
+
BAN: 'ban',
|
|
555
|
+
// 封禁
|
|
556
|
+
NONE: 'none' // 无处理
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* 举报类型配置
|
|
561
|
+
*/
|
|
562
|
+
const ReportTypeConfig = {
|
|
563
|
+
[ReportType.SPAM]: {
|
|
564
|
+
icon: '🗑️',
|
|
565
|
+
color: '#faad14'
|
|
566
|
+
},
|
|
567
|
+
[ReportType.FRAUD]: {
|
|
568
|
+
icon: '⚠️',
|
|
569
|
+
color: '#ff4d4f'
|
|
570
|
+
},
|
|
571
|
+
[ReportType.HARASSMENT]: {
|
|
572
|
+
icon: '😡',
|
|
573
|
+
color: '#eb2f96'
|
|
574
|
+
},
|
|
575
|
+
[ReportType.VIOLENCE]: {
|
|
576
|
+
icon: '⚔️',
|
|
577
|
+
color: '#cf1322'
|
|
578
|
+
},
|
|
579
|
+
[ReportType.PORN]: {
|
|
580
|
+
icon: '🔞',
|
|
581
|
+
color: '#722ed1'
|
|
582
|
+
},
|
|
583
|
+
[ReportType.INFRINGEMENT]: {
|
|
584
|
+
icon: '©️',
|
|
585
|
+
color: '#1890ff'
|
|
586
|
+
},
|
|
587
|
+
[ReportType.CONTENT]: {
|
|
588
|
+
icon: '📝',
|
|
589
|
+
color: '#fa8c16'
|
|
590
|
+
},
|
|
591
|
+
[ReportType.USER]: {
|
|
592
|
+
icon: '👤',
|
|
593
|
+
color: '#52c41a'
|
|
594
|
+
},
|
|
595
|
+
[ReportType.OTHER]: {
|
|
596
|
+
icon: '❓',
|
|
597
|
+
color: '#8c8c8c'
|
|
598
|
+
}
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* 创建默认举报
|
|
603
|
+
*/
|
|
604
|
+
function createDefaultReport(overrides = {}) {
|
|
605
|
+
return {
|
|
606
|
+
id: null,
|
|
607
|
+
reporter_did: null,
|
|
608
|
+
target_type: TargetType.POST,
|
|
609
|
+
target_id: null,
|
|
610
|
+
report_type: ReportType.OTHER,
|
|
611
|
+
description: '',
|
|
612
|
+
evidence_urls: [],
|
|
613
|
+
// 证据截图
|
|
614
|
+
contact: '',
|
|
615
|
+
status: ReportStatus.PENDING,
|
|
616
|
+
handler_did: null,
|
|
617
|
+
// 处理人
|
|
618
|
+
handle_result: null,
|
|
619
|
+
// 处理结果
|
|
620
|
+
handle_note: '',
|
|
621
|
+
// 处理备注
|
|
622
|
+
created_at: null,
|
|
623
|
+
handled_at: null,
|
|
624
|
+
...overrides
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* 获取举报类型列表
|
|
630
|
+
*/
|
|
631
|
+
function getReportTypes() {
|
|
632
|
+
return Object.values(ReportType).map(type => ({
|
|
633
|
+
value: type,
|
|
634
|
+
...ReportTypeConfig[type]
|
|
635
|
+
}));
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Report SDK - ReportForm 组件
|
|
640
|
+
* 举报表单
|
|
641
|
+
*/
|
|
642
|
+
|
|
643
|
+
function ReportForm({
|
|
644
|
+
targetType,
|
|
645
|
+
targetId,
|
|
646
|
+
onSuccess,
|
|
647
|
+
onCancel,
|
|
648
|
+
className = ''
|
|
649
|
+
}) {
|
|
650
|
+
const {
|
|
651
|
+
submit,
|
|
652
|
+
loading
|
|
653
|
+
} = useReport();
|
|
654
|
+
const [reportType, setReportType] = useState('');
|
|
655
|
+
const [description, setDescription] = useState('');
|
|
656
|
+
const [evidenceUrls, setEvidenceUrls] = useState([]);
|
|
657
|
+
const [contact, setContact] = useState('');
|
|
658
|
+
const [submitted, setSubmitted] = useState(false);
|
|
659
|
+
const [error, setError] = useState(null);
|
|
660
|
+
const reportTypes = getReportTypes();
|
|
661
|
+
const handleSubmit = async e => {
|
|
662
|
+
e.preventDefault();
|
|
663
|
+
setError(null);
|
|
664
|
+
if (!reportType) {
|
|
665
|
+
setError('请选择举报类型');
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
if (!description.trim()) {
|
|
669
|
+
setError('请填写问题描述');
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
try {
|
|
673
|
+
const result = await submit({
|
|
674
|
+
target_type: targetType,
|
|
675
|
+
target_id: targetId,
|
|
676
|
+
report_type: reportType,
|
|
677
|
+
description: description.trim(),
|
|
678
|
+
evidence_urls: evidenceUrls,
|
|
679
|
+
contact
|
|
680
|
+
});
|
|
681
|
+
setSubmitted(true);
|
|
682
|
+
onSuccess?.(result);
|
|
683
|
+
} catch (err) {
|
|
684
|
+
setError(err.message);
|
|
685
|
+
}
|
|
686
|
+
};
|
|
687
|
+
if (submitted) {
|
|
688
|
+
return /*#__PURE__*/React.createElement("div", {
|
|
689
|
+
className: `report-form report-success ${className}`
|
|
690
|
+
}, /*#__PURE__*/React.createElement("div", {
|
|
691
|
+
className: "success-icon"
|
|
692
|
+
}, "\u2713"), /*#__PURE__*/React.createElement("h3", null, t('submitSuccess')), /*#__PURE__*/React.createElement("p", null, t('submitSuccessHint')), /*#__PURE__*/React.createElement("button", {
|
|
693
|
+
className: "btn-close",
|
|
694
|
+
onClick: onCancel
|
|
695
|
+
}, "\u5173\u95ED"));
|
|
696
|
+
}
|
|
697
|
+
return /*#__PURE__*/React.createElement("div", {
|
|
698
|
+
className: `report-form ${className}`
|
|
699
|
+
}, /*#__PURE__*/React.createElement("h3", {
|
|
700
|
+
className: "report-form-title"
|
|
701
|
+
}, t('report')), /*#__PURE__*/React.createElement("form", {
|
|
702
|
+
onSubmit: handleSubmit
|
|
703
|
+
}, /*#__PURE__*/React.createElement("div", {
|
|
704
|
+
className: "form-group"
|
|
705
|
+
}, /*#__PURE__*/React.createElement("label", null, t('selectType')), /*#__PURE__*/React.createElement("div", {
|
|
706
|
+
className: "report-type-grid"
|
|
707
|
+
}, reportTypes.map(type => {
|
|
708
|
+
const config = ReportTypeConfig[type.value];
|
|
709
|
+
return /*#__PURE__*/React.createElement("button", {
|
|
710
|
+
key: type.value,
|
|
711
|
+
type: "button",
|
|
712
|
+
className: `report-type-item ${reportType === type.value ? 'selected' : ''}`,
|
|
713
|
+
onClick: () => setReportType(type.value)
|
|
714
|
+
}, /*#__PURE__*/React.createElement("span", {
|
|
715
|
+
className: "type-icon"
|
|
716
|
+
}, config.icon), /*#__PURE__*/React.createElement("span", {
|
|
717
|
+
className: "type-label"
|
|
718
|
+
}, t(`type${type.value.charAt(0).toUpperCase() + type.value.slice(1)}`)));
|
|
719
|
+
}))), /*#__PURE__*/React.createElement("div", {
|
|
720
|
+
className: "form-group"
|
|
721
|
+
}, /*#__PURE__*/React.createElement("label", null, t('describe')), /*#__PURE__*/React.createElement("textarea", {
|
|
722
|
+
value: description,
|
|
723
|
+
onChange: e => setDescription(e.target.value),
|
|
724
|
+
placeholder: t('describePlaceholder'),
|
|
725
|
+
rows: 4,
|
|
726
|
+
maxLength: 500
|
|
727
|
+
}), /*#__PURE__*/React.createElement("span", {
|
|
728
|
+
className: "char-count"
|
|
729
|
+
}, description.length, "/500")), /*#__PURE__*/React.createElement("div", {
|
|
730
|
+
className: "form-group"
|
|
731
|
+
}, /*#__PURE__*/React.createElement("label", null, t('contact')), /*#__PURE__*/React.createElement("input", {
|
|
732
|
+
type: "text",
|
|
733
|
+
value: contact,
|
|
734
|
+
onChange: e => setContact(e.target.value),
|
|
735
|
+
placeholder: "\u90AE\u7BB1\u6216\u624B\u673A\u53F7"
|
|
736
|
+
})), error && /*#__PURE__*/React.createElement("div", {
|
|
737
|
+
className: "form-error"
|
|
738
|
+
}, error), /*#__PURE__*/React.createElement("div", {
|
|
739
|
+
className: "form-actions"
|
|
740
|
+
}, /*#__PURE__*/React.createElement("button", {
|
|
741
|
+
type: "button",
|
|
742
|
+
className: "btn-cancel",
|
|
743
|
+
onClick: onCancel
|
|
744
|
+
}, t('cancel')), /*#__PURE__*/React.createElement("button", {
|
|
745
|
+
type: "submit",
|
|
746
|
+
className: "btn-submit",
|
|
747
|
+
disabled: loading
|
|
748
|
+
}, loading ? t('loading') : t('submit')))));
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/**
|
|
752
|
+
* Report SDK - ReportButton 组件
|
|
753
|
+
* 举报按钮(点击弹出表单)
|
|
754
|
+
*/
|
|
755
|
+
|
|
756
|
+
function ReportButton({
|
|
757
|
+
targetType,
|
|
758
|
+
targetId,
|
|
759
|
+
onSuccess,
|
|
760
|
+
iconOnly = false,
|
|
761
|
+
className = ''
|
|
762
|
+
}) {
|
|
763
|
+
const [showForm, setShowForm] = useState(false);
|
|
764
|
+
const handleSuccess = result => {
|
|
765
|
+
setShowForm(false);
|
|
766
|
+
onSuccess?.(result);
|
|
767
|
+
};
|
|
768
|
+
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("button", {
|
|
769
|
+
className: `report-button ${iconOnly ? 'icon-only' : ''} ${className}`,
|
|
770
|
+
onClick: () => setShowForm(true),
|
|
771
|
+
title: t('report')
|
|
772
|
+
}, /*#__PURE__*/React.createElement("span", {
|
|
773
|
+
className: "report-icon"
|
|
774
|
+
}, "\uD83D\uDEA8"), !iconOnly && /*#__PURE__*/React.createElement("span", {
|
|
775
|
+
className: "report-text"
|
|
776
|
+
}, t('report'))), showForm && /*#__PURE__*/React.createElement("div", {
|
|
777
|
+
className: "report-modal-overlay",
|
|
778
|
+
onClick: () => setShowForm(false)
|
|
779
|
+
}, /*#__PURE__*/React.createElement("div", {
|
|
780
|
+
className: "report-modal",
|
|
781
|
+
onClick: e => e.stopPropagation()
|
|
782
|
+
}, /*#__PURE__*/React.createElement("button", {
|
|
783
|
+
className: "modal-close",
|
|
784
|
+
onClick: () => setShowForm(false)
|
|
785
|
+
}, "\u2715"), /*#__PURE__*/React.createElement(ReportForm, {
|
|
786
|
+
targetType: targetType,
|
|
787
|
+
targetId: targetId,
|
|
788
|
+
onSuccess: handleSuccess,
|
|
789
|
+
onCancel: () => setShowForm(false)
|
|
790
|
+
}))));
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
export { HandleResult, ReportApiClient, ReportButton, ReportForm, ReportProvider, ReportStatus, ReportType, ReportTypeConfig, SUPPORTED_LANGUAGES, TargetType, createDefaultReport, getLanguage, getReportTypes, messages, reportApi, setLanguage, t, useReport };
|
|
794
|
+
//# sourceMappingURL=index.esm.js.map
|