const { request } = require('../../utils/request') Page({ data: { inputText: '', fileName: '', lineCount: 0, loading: false, summary: null, items: [] }, formatPercent(value, digits = 2) { const num = Number(value || 0) return `${(num * 100).toFixed(digits)}%` }, onInput(e) { this.setData({ inputText: e.detail.value || '' }) }, parseLines() { return (this.data.inputText || '') .split('\n') .map((line) => line.trim()) .filter((line) => line.length >= 2) }, chooseFile() { wx.chooseMessageFile({ count: 1, type: 'file', extension: ['txt'], success: (res) => { const file = res.tempFiles[0] const fs = wx.getFileSystemManager() try { const content = fs.readFileSync(file.path, 'utf8') const lines = content .split('\n') .map((line) => line.trim()) .filter((line) => line.length >= 2) this.setData({ inputText: lines.join('\n'), fileName: file.name, lineCount: lines.length }) wx.showToast({ title: `已读取 ${lines.length} 条文本`, icon: 'success' }) } catch (err) { console.error('读取文件失败', err) wx.showToast({ title: '文件读取失败', icon: 'none' }) } }, fail: (err) => { console.error('选择文件失败', err) if (err.errMsg !== 'chooseMessageFile:fail cancel') { wx.showToast({ title: '请选择TXT文件', icon: 'none' }) } } }) }, async submit() { if (this.data.loading) return const items = this.parseLines() if (!items.length) { wx.showToast({ title: '请至少输入一条有效文本', icon: 'none' }) return } this.setData({ loading: true }) try { const data = await request({ url: '/spam/predict/batch', method: 'POST', data: { items } }) const summary = { ...(data.summary || {}), spam_ratio_text: this.formatPercent((data.summary || {}).spam_ratio, 2), blocked_ratio_text: this.formatPercent((data.summary || {}).blocked_ratio, 2) } const normalizedItems = (data.items || []).map((item) => ({ ...item, confidence_text: this.formatPercent(item.confidence, 2) })) this.setData({ summary, items: normalizedItems }) } finally { this.setData({ loading: false }) } }, fillDemo() { this.setData({ inputText: [ '点击链接领取购物补贴,名额有限。', '明天下午三点上线前演练。', '高薪兼职日结,扫码进群。', '测试报告我已经同步到项目群。' ].join('\n') }) }, showTokenWeight(e) { const token = e.currentTarget.dataset.token const weight = e.currentTarget.dataset.weight const weightNum = Number(weight || 0) const direction = weightNum >= 0 ? '倾向垃圾判定' : '倾向正常判定' wx.showModal({ title: '关键词权重', content: `关键词"${token}"\n权重贡献:${weightNum >= 0 ? '+' : ''}${weightNum.toFixed(4)}\n(${direction})`, showCancel: false, confirmText: '关闭' }) }, generateCSV() { const items = this.data.items if (!items.length) return '' const headers = ['文本', '判定结果', '分类标签', '置信度', '垃圾概率', '正常概率', '风险关键词'] const rows = items.map((item) => { const prediction = item.prediction === 'spam' ? '垃圾信息' : '正常信息' const categoryLabel = item.category_label || '' const confidence = item.confidence_text || '0%' const spamProb = this.formatPercent(item.spam_probability, 4) const hamProb = this.formatPercent(item.ham_probability, 4) const tokens = (item.reason_tokens || []).map((t) => t.token || t).join('; ') // CSV 转义:文本中的逗号和换行需要处理 const text = (item.text || '').replace(/"/g, '""') const tokensEscaped = tokens.replace(/"/g, '""') return `"${text}","${prediction}","${categoryLabel}","${confidence}","${spamProb}","${hamProb}","${tokensEscaped}"` }) return [headers.join(','), ...rows].join('\n') }, exportCSV() { const items = this.data.items if (!items.length) { wx.showToast({ title: '暂无识别结果可导出', icon: 'none' }) return } const csvContent = this.generateCSV() const timestamp = new Date().toISOString().slice(0, 19).replace(/[T:]/g, '-') const filename = `batch_detect_${timestamp}.csv` // 写入临时文件 const fs = wx.getFileSystemManager() const tempPath = `${wx.env.USER_DATA_PATH}/${filename}` try { fs.writeFileSync(tempPath, csvContent, 'utf8') wx.showModal({ title: '导出成功', content: `CSV文件已生成,是否打开查看?\n文件名:${filename}`, confirmText: '打开', cancelText: '关闭', success: (res) => { if (res.confirm) { wx.openDocument({ filePath: tempPath, fileType: 'csv', showMenu: true, fail: (err) => { console.error('打开文件失败', err) wx.showToast({ title: '打开失败,请检查文件管理器', icon: 'none' }) } }) } } }) } catch (err) { console.error('写入文件失败', err) wx.showToast({ title: '导出失败', icon: 'none' }) } }, copyCSVToClipboard() { const items = this.data.items if (!items.length) { wx.showToast({ title: '暂无识别结果可复制', icon: 'none' }) return } const csvContent = this.generateCSV() wx.setClipboardData({ data: csvContent, success: () => { wx.showToast({ title: 'CSV内容已复制到剪贴板', icon: 'success' }) } }) } })