feat: admin-web新增feedback工具和配置优化

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
刘正航
2026-05-14 21:15:27 +08:00
parent 829599bc17
commit b8acc8be43
11 changed files with 94 additions and 15 deletions

View File

@@ -5,6 +5,23 @@ import './styles/theme.css'
Vue.config.productionTip = false
Vue.config.errorHandler = (err, vm, info) => {
console.error('[vue error]', info, err)
}
Vue.config.warnHandler = (msg, vm, trace) => {
console.warn('[vue warn]', msg, trace)
}
window.addEventListener('unhandledrejection', (event) => {
console.warn('[unhandled rejection]', event.reason)
event.preventDefault()
})
window.addEventListener('error', (event) => {
console.error('[window error]', event.error || event.message)
})
new Vue({
router,
render: (h) => h(App)

View File

@@ -57,3 +57,42 @@ export function previewImage(urls, current) {
if (!urls || !urls.length) return
ensurePreview().open({ urls, current })
}
export async function copyText(text) {
if (text === undefined || text === null) return false
const value = String(text)
if (navigator.clipboard && window.isSecureContext) {
try {
await navigator.clipboard.writeText(value)
return true
} catch (err) {
console.warn('[copyText] navigator.clipboard 不可用,回退到 execCommand', err)
}
}
try {
const textarea = document.createElement('textarea')
textarea.value = value
textarea.setAttribute('readonly', '')
textarea.style.position = 'fixed'
textarea.style.top = '0'
textarea.style.left = '0'
textarea.style.width = '1px'
textarea.style.height = '1px'
textarea.style.opacity = '0'
textarea.style.pointerEvents = 'none'
document.body.appendChild(textarea)
textarea.focus()
textarea.select()
textarea.setSelectionRange(0, value.length)
const ok = document.execCommand('copy')
document.body.removeChild(textarea)
return !!ok
} catch (err) {
console.error('[copyText] execCommand 也失败了', err)
return false
}
}

View File

@@ -136,7 +136,7 @@
<script>
import { request } from '@/utils/request'
import { toast, confirm } from '@/utils/feedback'
import { toast, confirm, copyText } from '@/utils/feedback'
export default {
name: 'BatchView',
@@ -176,7 +176,10 @@ export default {
this.lineCount = lines.length
toast(`已读取 ${lines.length} 条文本`, 'success')
}
reader.onerror = () => toast('文件读取失败', 'error')
reader.onerror = (e) => {
console.error('[batch] 文件读取失败', e)
toast('文件读取失败', 'error')
}
reader.readAsText(file, 'utf-8')
e.target.value = ''
},
@@ -267,6 +270,7 @@ export default {
}, 0)
toast('导出成功', 'success')
} catch (err) {
console.error('[batch] 导出失败', err)
toast('导出失败', 'error')
} finally {
this.exporting = false
@@ -278,10 +282,11 @@ export default {
return
}
const csv = this.generateCSV()
try {
await navigator.clipboard.writeText(csv)
const ok = await copyText(csv)
if (ok) {
toast('CSV 内容已复制到剪贴板', 'success')
} catch (err) {
} else {
console.error('[batch] 复制失败CSV 内容打印到控制台供手动复制\n' + csv)
toast('复制失败,请手动选择文本', 'error')
}
}

View File

@@ -168,6 +168,8 @@ export default {
detect_spam_probability_text: this.formatPercent((result.detect || {}).spam_probability, 2)
}
toast(result.publish_allowed ? '发布成功' : '已拦截,可申诉', result.publish_allowed ? 'success' : 'error')
} catch (err) {
console.error('[detect] 发布失败', err)
} finally {
this.loading = false
}

View File

@@ -118,11 +118,14 @@ export default {
async bootstrap() {
this.loading = true
try {
const [, modelInfo] = await Promise.all([
refreshUser(),
request({ url: '/spam/model/info' })
])
this.modelInfo = modelInfo
await refreshUser()
const modelInfo = await request({ url: '/spam/model/info' }).catch((err) => {
console.warn('[home] 模型信息加载失败', err)
return null
})
if (modelInfo) this.modelInfo = modelInfo
} catch (err) {
console.warn('[home] bootstrap 异常', err)
} finally {
this.loading = false
}

View File

@@ -147,7 +147,7 @@
<script>
import { request } from '@/utils/request'
import { toast } from '@/utils/feedback'
import { toast, copyText } from '@/utils/feedback'
export default {
name: 'AdminDashboard',
@@ -256,10 +256,12 @@ export default {
`【近 7 日趋势】`,
(report.spam_trend || []).slice(-7).map((t) => `${t.label}: 拦截${t.blocked}条,发布${t.published}`).join('\n')
]
try {
await navigator.clipboard.writeText(lines.join('\n'))
const content = lines.join('\n')
const ok = await copyText(content)
if (ok) {
toast('报告已复制到剪贴板', 'success')
} catch (err) {
} else {
console.error('[dashboard] 复制失败,报告内容打印到控制台供手动复制\n' + content)
toast('复制失败,请手动选择', 'error')
}
}

View File

@@ -161,6 +161,7 @@ export default {
try {
items = JSON.parse(this.importText)
} catch (err) {
console.error('[samples] JSON 格式错误', err)
toast('JSON 格式错误', 'error')
return
}

View File

@@ -179,6 +179,7 @@ export default {
try {
items = JSON.parse(this.importText)
} catch (err) {
console.error('[users] JSON 格式错误', err)
toast('JSON 格式错误', 'error')
return
}

View File

@@ -7,6 +7,10 @@ module.exports = defineConfig({
port: 8080,
host: '0.0.0.0',
historyApiFallback: true,
client: {
overlay: false,
logging: 'warn'
},
proxy: {
'/api': {
target: 'http://127.0.0.1:5000',