各页面增加垃圾信息分类标签显示: - 检测结果页显示分类标签 - 批量识别页和CSV导出增加分类标签列 - 历史记录页显示分类标签 - 管理后台审核页显示分类标签 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
325 lines
9.5 KiB
JavaScript
325 lines
9.5 KiB
JavaScript
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: '已驳回'
|
|
}
|
|
|
|
const CATEGORY_LABELS = {
|
|
fraud: '疑似诈骗',
|
|
harassment: '疑似骚扰',
|
|
advertisement: '疑似广告',
|
|
spam: '疑似垃圾'
|
|
}
|
|
|
|
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,
|
|
category_label: CATEGORY_LABELS[item.category] || ''
|
|
}))
|
|
},
|
|
|
|
normalizeAppeals(rows = []) {
|
|
const baseURL = getApp().globalData.baseURL || 'http://127.0.0.1:5000/api'
|
|
const serverBase = baseURL.replace('/api', '')
|
|
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,
|
|
category_label: CATEGORY_LABELS[item.category] || '',
|
|
appeal_evidence_urls: (item.appeal_evidence_urls || []).map((url) =>
|
|
url.startsWith('http') ? url : `${serverBase}${url}`
|
|
)
|
|
}))
|
|
},
|
|
|
|
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}` })
|
|
const normalizedAppeals = this.normalizeAppeals(data.items || [])
|
|
console.log('normalized appeals:', normalizedAppeals)
|
|
this.setData({
|
|
appeals: normalizedAppeals,
|
|
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()])
|
|
},
|
|
|
|
previewEvidence(e) {
|
|
const url = e.currentTarget.dataset.url
|
|
const item = this.data.appeals.find((a) => a.appeal_evidence_urls && a.appeal_evidence_urls.includes(url))
|
|
const urls = item ? item.appeal_evidence_urls : [url]
|
|
wx.previewImage({
|
|
current: url,
|
|
urls
|
|
})
|
|
}
|
|
})
|