const { request } = require('../../utils/request') const INTERCEPT_STATUS_OPTIONS = [ { value: '', label: '全部发布状态' }, { value: 'blocked', label: '已拦截' }, { value: 'published', label: '已发布' } ] const INTERCEPT_REVIEW_STATUS_OPTIONS = [ { value: '', label: '全部复核状态' }, { value: 'pending', label: '待复核' }, { value: 'confirmed_spam', label: '确认垃圾' }, { value: 'approved_ham', label: '误判放行' }, { value: 'none', label: '未进入复核' } ] const APPEAL_STATUS_OPTIONS = [ { value: 'pending', label: '待处理申诉' }, { value: '', label: '全部申诉' }, { value: 'approved', label: '已通过申诉' }, { value: 'rejected', label: '已驳回申诉' } ] const REVIEW_STATUS_TEXT = { none: '无', pending: '待复核', confirmed_spam: '确认垃圾', approved_ham: '误判放行' } const APPEAL_STATUS_TEXT = { none: '无', pending: '待处理', approved: '已通过', rejected: '已驳回' } function buildPager(total, page, pageSize) { const totalValue = Number(total || 0) const totalPages = Math.max(Math.ceil(totalValue / pageSize), 1) return { page, pageSize, total: totalValue, totalPages, hasPrev: page > 1, hasNext: page < totalPages } } Page({ data: { loading: false, thresholdInput: '0.75', interceptKeyword: '', interceptStatusOptions: INTERCEPT_STATUS_OPTIONS, interceptStatusIndex: 1, interceptReviewStatusOptions: INTERCEPT_REVIEW_STATUS_OPTIONS, interceptReviewStatusIndex: 1, intercepts: [], interceptPager: buildPager(0, 1, 10), appealKeyword: '', appealStatusOptions: APPEAL_STATUS_OPTIONS, appealStatusIndex: 0, appeals: [], appealPager: buildPager(0, 1, 10), reviewNoteMap: {}, appealNoteMap: {} }, formatPercent(value, digits = 2) { const num = Number(value || 0) return `${(num * 100).toFixed(digits)}%` }, onShow() { this.bootstrap() }, onPullDownRefresh() { this.bootstrap(true) }, async bootstrap(fromPullDown = false) { this.setData({ loading: true }) try { await Promise.all([this.fetchThreshold(), this.fetchIntercepts(), this.fetchAppeals()]) } finally { this.setData({ loading: false }) if (fromPullDown) wx.stopPullDownRefresh() } }, async fetchThreshold() { const data = await request({ url: '/admin/detection/threshold' }) this.setData({ thresholdInput: String(data.spam_threshold || 0.75) }) }, normalizeIntercepts(rows = []) { return rows.map((item) => ({ ...item, created_text: (item.created_at || '').replace('T', ' ').slice(0, 19), review_status_text: REVIEW_STATUS_TEXT[item.manual_review_status] || item.manual_review_status, spam_probability_text: this.formatPercent(item.spam_probability, 2), appeal_status_text: APPEAL_STATUS_TEXT[item.appeal_status] || item.appeal_status })) }, normalizeAppeals(rows = []) { return rows.map((item) => ({ ...item, created_text: (item.created_at || '').replace('T', ' ').slice(0, 19), appeal_status_text: APPEAL_STATUS_TEXT[item.appeal_status] || item.appeal_status })) }, async fetchIntercepts() { const { interceptPager, interceptKeyword, interceptStatusOptions, interceptStatusIndex, interceptReviewStatusOptions, interceptReviewStatusIndex } = this.data const status = interceptStatusOptions[interceptStatusIndex].value const reviewStatus = interceptReviewStatusOptions[interceptReviewStatusIndex].value const query = [ `keyword=${encodeURIComponent((interceptKeyword || '').trim())}`, `status=${encodeURIComponent(status)}`, `review_status=${encodeURIComponent(reviewStatus)}`, `page=${interceptPager.page}`, `page_size=${interceptPager.pageSize}` ].join('&') const data = await request({ url: `/admin/intercepts?${query}` }) this.setData({ intercepts: this.normalizeIntercepts(data.items || []), interceptPager: buildPager(data.total || 0, interceptPager.page, interceptPager.pageSize) }) }, async fetchAppeals() { const { appealPager, appealKeyword, appealStatusOptions, appealStatusIndex } = this.data const status = appealStatusOptions[appealStatusIndex].value const query = [ `keyword=${encodeURIComponent((appealKeyword || '').trim())}`, `status=${encodeURIComponent(status)}`, `page=${appealPager.page}`, `page_size=${appealPager.pageSize}` ].join('&') const data = await request({ url: `/admin/appeals?${query}` }) this.setData({ appeals: this.normalizeAppeals(data.items || []), appealPager: buildPager(data.total || 0, appealPager.page, appealPager.pageSize) }) }, onInput(e) { const field = e.currentTarget.dataset.field this.setData({ [field]: e.detail.value || '' }) }, onInterceptStatusChange(e) { this.setData({ interceptStatusIndex: Number(e.detail.value || 0) }) }, onInterceptReviewStatusChange(e) { this.setData({ interceptReviewStatusIndex: Number(e.detail.value || 0) }) }, onAppealStatusChange(e) { this.setData({ appealStatusIndex: Number(e.detail.value || 0) }) }, applyInterceptFilters() { this.setData({ interceptPager: { ...this.data.interceptPager, page: 1 } }) this.fetchIntercepts() }, clearInterceptFilters() { this.setData({ interceptKeyword: '', interceptStatusIndex: 1, interceptReviewStatusIndex: 1, interceptPager: { ...this.data.interceptPager, page: 1 } }) this.fetchIntercepts() }, applyAppealFilters() { this.setData({ appealPager: { ...this.data.appealPager, page: 1 } }) this.fetchAppeals() }, clearAppealFilters() { this.setData({ appealKeyword: '', appealStatusIndex: 0, appealPager: { ...this.data.appealPager, page: 1 } }) this.fetchAppeals() }, prevInterceptPage() { if (!this.data.interceptPager.hasPrev) return this.setData({ interceptPager: { ...this.data.interceptPager, page: this.data.interceptPager.page - 1 } }) this.fetchIntercepts() }, nextInterceptPage() { if (!this.data.interceptPager.hasNext) return this.setData({ interceptPager: { ...this.data.interceptPager, page: this.data.interceptPager.page + 1 } }) this.fetchIntercepts() }, prevAppealPage() { if (!this.data.appealPager.hasPrev) return this.setData({ appealPager: { ...this.data.appealPager, page: this.data.appealPager.page - 1 } }) this.fetchAppeals() }, nextAppealPage() { if (!this.data.appealPager.hasNext) return this.setData({ appealPager: { ...this.data.appealPager, page: this.data.appealPager.page + 1 } }) this.fetchAppeals() }, onReviewNoteInput(e) { const id = e.currentTarget.dataset.id this.setData({ [`reviewNoteMap.${id}`]: e.detail.value || '' }) }, onAppealNoteInput(e) { const id = e.currentTarget.dataset.id this.setData({ [`appealNoteMap.${id}`]: e.detail.value || '' }) }, async saveThreshold() { const value = Number(this.data.thresholdInput) if (Number.isNaN(value) || value <= 0 || value >= 1) { wx.showToast({ title: '阈值需在 0 到 1 之间', icon: 'none' }) return } await request({ url: '/admin/detection/threshold', method: 'PUT', data: { spam_threshold: value } }) wx.showToast({ title: '阈值更新成功', icon: 'success' }) this.fetchThreshold() }, async reviewIntercept(e) { const id = Number(e.currentTarget.dataset.id) const decision = e.currentTarget.dataset.decision const note = (this.data.reviewNoteMap[id] || '').trim() await request({ url: `/admin/intercepts/${id}/review`, method: 'PUT', data: { decision, note: note || (decision === 'spam' ? '人工复核确认为垃圾信息' : '人工复核后解除拦截') } }) wx.showToast({ title: '复核完成', icon: 'success' }) this.setData({ [`reviewNoteMap.${id}`]: '' }) await Promise.all([this.fetchIntercepts(), this.fetchAppeals()]) }, async processAppeal(e) { const id = Number(e.currentTarget.dataset.id) const action = e.currentTarget.dataset.action const note = (this.data.appealNoteMap[id] || '').trim() await request({ url: `/admin/appeals/${id}/process`, method: 'PUT', data: { action, note: note || (action === 'approve' ? '申诉通过,解除拦截' : '申诉驳回,维持拦截') } }) wx.showToast({ title: '申诉处理完成', icon: 'success' }) this.setData({ [`appealNoteMap.${id}`]: '' }) await Promise.all([this.fetchIntercepts(), this.fetchAppeals()]) } })