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

5
.gitignore vendored
View File

@@ -17,3 +17,8 @@ __pycache__/
# OS # OS
.DS_Store .DS_Store
Thumbs.db Thumbs.db
# Archives
*.zip
*.tar.gz
*.7z

View File

@@ -5,6 +5,23 @@ import './styles/theme.css'
Vue.config.productionTip = false 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({ new Vue({
router, router,
render: (h) => h(App) render: (h) => h(App)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,7 @@
"host": "127.0.0.1", "host": "127.0.0.1",
"port": 3306, "port": 3306,
"user": "root", "user": "root",
"password": "123456", "password": "rootroot",
"database": "spam_nb_miniapp", "database": "spam_nb_miniapp",
"charset": "utf8mb4", "charset": "utf8mb4",
"admin_init": { "admin_init": {