feat(ECR-019): ContentSafety FilterRule 只读并 Closed
新增 filter_rules、admin.content_safety.read、列表/详情/试匹配 API 与 admin-h5「安全」页;禁审核写/UGC/真支付。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -243,6 +243,38 @@ export const adminApi = {
|
||||
updated_at: string
|
||||
messages: Array<{ id: string; role: string; content: string; created_at: string }>
|
||||
}>('GET', `/ask/threads/${id}`),
|
||||
filterRules: () =>
|
||||
request<{
|
||||
items: Array<{
|
||||
id: string
|
||||
code: string
|
||||
title: string
|
||||
category: string
|
||||
pattern: string
|
||||
action: string
|
||||
active: boolean
|
||||
system: boolean
|
||||
updated_at: string
|
||||
}>
|
||||
}>('GET', '/content-safety/filter-rules'),
|
||||
filterRule: (id: string) =>
|
||||
request<{
|
||||
id: string
|
||||
code: string
|
||||
title: string
|
||||
category: string
|
||||
pattern: string
|
||||
action: string
|
||||
active: boolean
|
||||
system: boolean
|
||||
updated_at: string
|
||||
}>('GET', `/content-safety/filter-rules/${id}`),
|
||||
evaluateContent: (text: string) =>
|
||||
request<{ matches: Array<{ code: string; title: string; category: string; action: string }> }>(
|
||||
'POST',
|
||||
'/content-safety/evaluate',
|
||||
{ text },
|
||||
),
|
||||
orders: () =>
|
||||
request<{
|
||||
items: Array<{
|
||||
|
||||
@@ -31,6 +31,7 @@ async function onLogout() {
|
||||
<RouterLink to="/plans">套餐</RouterLink>
|
||||
<RouterLink to="/codes">兑换码</RouterLink>
|
||||
<RouterLink to="/ask">问答</RouterLink>
|
||||
<RouterLink to="/safety">安全</RouterLink>
|
||||
<RouterLink to="/orders">订单</RouterLink>
|
||||
<RouterLink to="/audit">审计</RouterLink>
|
||||
</nav>
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { adminApi } from '@/api/client'
|
||||
|
||||
type Rule = Awaited<ReturnType<typeof adminApi.filterRules>>['items'][number]
|
||||
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
const items = ref<Rule[]>([])
|
||||
const sample = ref('我不想活了,求帮助')
|
||||
const matches = ref<Array<{ code: string; title: string; category: string; action: string }>>([])
|
||||
const evalMsg = ref('')
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
try {
|
||||
const res = await adminApi.filterRules()
|
||||
items.value = res.items || []
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : '加载失败'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function runEval() {
|
||||
evalMsg.value = ''
|
||||
try {
|
||||
const res = await adminApi.evaluateContent(sample.value)
|
||||
matches.value = res.matches || []
|
||||
evalMsg.value = matches.value.length ? `命中 ${matches.value.length} 条` : '未命中'
|
||||
} catch (e) {
|
||||
evalMsg.value = e instanceof Error ? e.message : '试匹配失败'
|
||||
matches.value = []
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section>
|
||||
<h1>内容安全</h1>
|
||||
<p class="muted">FilterRule 只读 · 试匹配不写审核工单</p>
|
||||
<p v-if="loading" class="muted">加载中…</p>
|
||||
<p v-else-if="error" class="err">{{ error }}</p>
|
||||
<div v-else class="layout">
|
||||
<div class="card">
|
||||
<h2>过滤规则</h2>
|
||||
<p v-if="!items.length" class="muted">暂无规则</p>
|
||||
<table v-else>
|
||||
<thead>
|
||||
<tr><th>代码</th><th>分类</th><th>动作</th><th>模式</th><th>状态</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="r in items" :key="r.id">
|
||||
<td>{{ r.title }} <code>{{ r.code }}</code></td>
|
||||
<td>{{ r.category }}</td>
|
||||
<td>{{ r.action }}</td>
|
||||
<td>{{ r.pattern }}</td>
|
||||
<td>{{ r.active ? '启用' : '停用' }}{{ r.system ? ' · 系统' : '' }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2>试匹配</h2>
|
||||
<textarea v-model="sample" rows="4" />
|
||||
<div class="row">
|
||||
<button class="btn" type="button" @click="runEval">试匹配</button>
|
||||
<span class="muted">{{ evalMsg }}</span>
|
||||
</div>
|
||||
<ul v-if="matches.length" class="hits">
|
||||
<li v-for="m in matches" :key="m.code">
|
||||
{{ m.title }} · {{ m.category }} · <strong>{{ m.action }}</strong>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
h1 { margin: 0 0 0.35rem; font-size: 1.35rem; }
|
||||
h2 { margin: 0 0 0.6rem; font-size: 1.05rem; }
|
||||
.layout { display: grid; grid-template-columns: 1.2fr 1fr; gap: 1rem; margin-top: 1rem; }
|
||||
textarea {
|
||||
width: 100%;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
padding: 0.55rem 0.7rem;
|
||||
resize: vertical;
|
||||
font: inherit;
|
||||
}
|
||||
.row { display: flex; gap: 0.6rem; align-items: center; margin-top: 0.6rem; }
|
||||
.hits { margin: 0.75rem 0 0; padding-left: 1.1rem; }
|
||||
code { font-size: 0.75rem; color: var(--muted); margin-left: 0.25rem; }
|
||||
@media (max-width: 900px) { .layout { grid-template-columns: 1fr; } }
|
||||
</style>
|
||||
@@ -18,6 +18,7 @@ const router = createRouter({
|
||||
{ path: 'plans', name: 'plans', component: () => import('@/pages/MembershipPlansPage.vue') },
|
||||
{ path: 'codes', name: 'codes', component: () => import('@/pages/RedemptionPage.vue') },
|
||||
{ path: 'ask', name: 'ask', component: () => import('@/pages/AskPage.vue') },
|
||||
{ path: 'safety', name: 'safety', component: () => import('@/pages/SafetyPage.vue') },
|
||||
{ path: 'audit', name: 'audit', component: () => import('@/pages/AuditPage.vue') },
|
||||
],
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user