feat: admin-web新增feedback工具和配置优化
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -17,3 +17,8 @@ __pycache__/
|
|||||||
# OS
|
# OS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|
||||||
|
# Archives
|
||||||
|
*.zip
|
||||||
|
*.tar.gz
|
||||||
|
*.7z
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
Reference in New Issue
Block a user