新增 crisis_policies、admin.crisis.read、列表/试匹配 API 与 admin-h5「危机」页;禁 CrisisEvent 写/UGC/真支付。 Co-authored-by: Cursor <cursoragent@cursor.com>
104 lines
3.2 KiB
Vue
104 lines
3.2 KiB
Vue
<script setup lang="ts">
|
|
import { onMounted, ref } from 'vue'
|
|
import { adminApi } from '@/api/client'
|
|
|
|
type Policy = Awaited<ReturnType<typeof adminApi.crisisPolicies>>['items'][number]
|
|
|
|
const loading = ref(false)
|
|
const error = ref('')
|
|
const items = ref<Policy[]>([])
|
|
const sample = ref('真的不想活了,怎么办')
|
|
const matches = ref<
|
|
Array<{ code: string; title: string; severity: string; action: string; helpline_text?: string }>
|
|
>([])
|
|
const evalMsg = ref('')
|
|
|
|
async function load() {
|
|
loading.value = true
|
|
error.value = ''
|
|
try {
|
|
const res = await adminApi.crisisPolicies()
|
|
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.evaluateCrisis(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">CrisisPolicy 只读 · 试匹配不写 CrisisEvent · 非医疗诊断</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></tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="p in items" :key="p.id">
|
|
<td>{{ p.title }} <code>{{ p.code }}</code></td>
|
|
<td>{{ p.severity }}</td>
|
|
<td>{{ p.action }}</td>
|
|
<td>{{ p.pattern }}</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">
|
|
<strong>{{ m.title }}</strong> · {{ m.severity }} · {{ m.action }}
|
|
<p v-if="m.helpline_text" class="help">{{ m.helpline_text }}</p>
|
|
</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; }
|
|
.help { margin: 0.35rem 0 0; color: var(--muted); font-size: 0.85rem; }
|
|
code { font-size: 0.75rem; color: var(--muted); margin-left: 0.25rem; }
|
|
@media (max-width: 900px) { .layout { grid-template-columns: 1fr; } }
|
|
</style>
|