优化快捷入口与风险项管理展示
This commit is contained in:
parent
6402aaf83e
commit
b5484c8adb
@ -11,6 +11,8 @@ import {
|
||||
Search,
|
||||
} from "@lucide/vue";
|
||||
|
||||
import { ref } from "vue";
|
||||
|
||||
import { featureFlags } from "@/config/features";
|
||||
|
||||
defineProps<{ modelValue: string; loading: boolean; canUpload: boolean }>();
|
||||
@ -31,6 +33,13 @@ const emit = defineEmits<{
|
||||
| "voice",
|
||||
];
|
||||
}>();
|
||||
const textarea = ref<HTMLTextAreaElement | null>(null);
|
||||
|
||||
function focus() {
|
||||
textarea.value?.focus();
|
||||
}
|
||||
|
||||
defineExpose({ focus });
|
||||
|
||||
const functions: Array<{
|
||||
title: string;
|
||||
@ -115,6 +124,7 @@ const functions: Array<{
|
||||
<Camera :size="16" />
|
||||
</button>
|
||||
<textarea
|
||||
ref="textarea"
|
||||
:value="modelValue"
|
||||
rows="1"
|
||||
placeholder="输入产品参数或问题…"
|
||||
@ -145,7 +155,11 @@ const functions: Array<{
|
||||
:key="item.title"
|
||||
class="function-card"
|
||||
type="button"
|
||||
@click="emit('tool', item.tool)"
|
||||
@click="
|
||||
item.tool === 'assessment' || !item.prompt
|
||||
? emit('tool', item.tool)
|
||||
: emit('select', item.prompt)
|
||||
"
|
||||
>
|
||||
<span class="function-dot" :class="`function-dot--${item.tone}`"></span>
|
||||
<component :is="item.icon" :size="13" />
|
||||
|
||||
@ -258,7 +258,6 @@ function generateReport() {
|
||||
<b>{{ standard.riskPoint }}</b
|
||||
><small>{{ standard.rule }} · {{ standard.source }}</small>
|
||||
</div>
|
||||
<strong>{{ standard.status }}</strong>
|
||||
</div>
|
||||
</template>
|
||||
<p v-else class="empty">暂无直接匹配规则</p>
|
||||
@ -317,12 +316,6 @@ function generateReport() {
|
||||
{{ standard.source || "标准文件.xlsx" }}</small
|
||||
>
|
||||
</div>
|
||||
<strong
|
||||
:class="{
|
||||
ok: standard.status === '合规' || standard.status === '打样必测',
|
||||
}"
|
||||
>{{ standard.status }}</strong
|
||||
>
|
||||
</button>
|
||||
<p v-if="!data.standards?.length" class="empty">
|
||||
当前标准库暂无可匹配规则,请补充产品品类、包材或检测指标。
|
||||
@ -696,11 +689,6 @@ function generateReport() {
|
||||
color: var(--color-text-faint);
|
||||
font-size: 9px;
|
||||
}
|
||||
.standard-row > strong {
|
||||
flex: 0 0 auto;
|
||||
color: var(--color-warning);
|
||||
font-size: 10px;
|
||||
}
|
||||
.solution-standard-row {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
@ -753,14 +741,6 @@ function generateReport() {
|
||||
color: var(--color-text-faint);
|
||||
font-size: 10px;
|
||||
}
|
||||
.solution-standard-row > strong {
|
||||
flex: 0 0 auto;
|
||||
color: var(--color-warning);
|
||||
font-size: 11px;
|
||||
}
|
||||
.solution-standard-row > strong.ok {
|
||||
color: var(--color-success);
|
||||
}
|
||||
.suggestion-list {
|
||||
margin: 12px 0 0;
|
||||
padding-left: 18px;
|
||||
|
||||
@ -13,7 +13,10 @@ import { computed } from "vue";
|
||||
|
||||
import { featureFlags } from "@/config/features";
|
||||
|
||||
const emit = defineEmits<{ select: [prompt: string] }>();
|
||||
const emit = defineEmits<{
|
||||
select: [prompt: string];
|
||||
tool: [value: "assessment" | "ocr"];
|
||||
}>();
|
||||
const props = defineProps<{ canUpload: boolean }>();
|
||||
|
||||
const actions = [
|
||||
@ -23,6 +26,7 @@ const actions = [
|
||||
prompt: "帮我评估一款500ml PET瓶洗发水,泵头用祥印的,加工厂博元",
|
||||
icon: Search,
|
||||
tone: "blue",
|
||||
tool: "assessment" as const,
|
||||
},
|
||||
{
|
||||
title: "查供应商",
|
||||
@ -69,6 +73,7 @@ const actions = [
|
||||
icon: Camera,
|
||||
tone: "sky",
|
||||
requiresUpload: true,
|
||||
tool: "ocr" as const,
|
||||
},
|
||||
...(featureFlags.voiceEntry
|
||||
? [
|
||||
@ -90,6 +95,14 @@ const visibleActions = computed(() =>
|
||||
props.canUpload,
|
||||
),
|
||||
);
|
||||
|
||||
function handleAction(action: (typeof actions)[number]) {
|
||||
if ("tool" in action && action.tool) {
|
||||
emit("tool", action.tool);
|
||||
return;
|
||||
}
|
||||
emit("select", action.prompt);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -107,7 +120,7 @@ const visibleActions = computed(() =>
|
||||
:key="action.title"
|
||||
class="welcome-action"
|
||||
type="button"
|
||||
@click="emit('select', action.prompt)"
|
||||
@click="handleAction(action)"
|
||||
>
|
||||
<span class="welcome-action__icon" :class="`tone-${action.tone}`">
|
||||
<component :is="action.icon" :size="16" />
|
||||
|
||||
@ -108,13 +108,6 @@ const standardCategories = computed(() => [
|
||||
(props.result.standardJudgment ?? []).map((item) => item.category),
|
||||
),
|
||||
]);
|
||||
const pendingStandardCount = computed(
|
||||
() =>
|
||||
(props.result.standardJudgment ?? []).filter(
|
||||
(item) => !["合规", "合格", "达标"].includes(item.status),
|
||||
).length,
|
||||
);
|
||||
|
||||
function openStandards(category = "all", keyword?: string) {
|
||||
void router.push({
|
||||
name: "standards",
|
||||
@ -287,18 +280,12 @@ function openSupplier() {
|
||||
{{ standard.source || "标准文件.xlsx" }}</small
|
||||
>
|
||||
</div>
|
||||
<strong :class="{ 'status-action': standard.status !== '待检测' }">{{
|
||||
standard.status
|
||||
}}</strong>
|
||||
</button>
|
||||
<footer>
|
||||
<span class="standards-card__engine-icon">↻</span>
|
||||
<span
|
||||
><b>双引擎融合:</b
|
||||
>历史数据(经验引擎)提供风险优先级与相似案例;标准库(规则引擎)给出量化阈值与检测依据。<template
|
||||
v-if="pendingStandardCount"
|
||||
>{{ pendingStandardCount }} 项待检标准已加入检测任务清单</template
|
||||
></span
|
||||
>历史数据(经验引擎)提供风险优先级与相似案例;标准库(规则引擎)给出量化阈值与检测依据。</span
|
||||
>
|
||||
<span class="standards-card__links">
|
||||
<button
|
||||
@ -895,16 +882,6 @@ h3 {
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.standard-row > strong {
|
||||
flex: 0 0 auto;
|
||||
color: var(--color-warning);
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.standard-row > .status-action {
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.standards-card__empty,
|
||||
.case-empty {
|
||||
margin: 8px 0;
|
||||
|
||||
@ -14,18 +14,18 @@ const { user } = storeToRefs(auth);
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
const navigation = ["对话", "看板", "供应商", "标准库", "报告", "管理"];
|
||||
const navigation = ["对话", "看板", "供应商", "标准库", "报告", "风险项管理"];
|
||||
const navigationRoutes: Record<string, string> = {
|
||||
对话: "assessment",
|
||||
看板: "overview",
|
||||
供应商: "supplier",
|
||||
标准库: "standards",
|
||||
报告: "report",
|
||||
管理: "review",
|
||||
风险项管理: "review",
|
||||
};
|
||||
const visibleNavigation = computed(() =>
|
||||
navigation.filter(
|
||||
(item) => item !== "管理" || auth.hasPermission("risk_item.review"),
|
||||
(item) => item !== "风险项管理" || auth.hasPermission("risk_item.review"),
|
||||
),
|
||||
);
|
||||
|
||||
@ -34,7 +34,7 @@ const routeChunks: Record<string, () => Promise<unknown>> = {
|
||||
供应商: () => import("@/pages/SupplierProfilePage.vue"),
|
||||
标准库: () => import("@/pages/StandardsPage.vue"),
|
||||
报告: () => import("@/pages/ReportPage.vue"),
|
||||
管理: () => import("@/pages/ReviewPage.vue"),
|
||||
风险项管理: () => import("@/pages/ReviewPage.vue"),
|
||||
};
|
||||
const prefetched = new Set<string>();
|
||||
function prefetch(item: string) {
|
||||
@ -67,7 +67,7 @@ function prefetch(item: string) {
|
||||
(item === '供应商' && route.name === 'supplier') ||
|
||||
(item === '标准库' && route.name === 'standards') ||
|
||||
(item === '报告' && route.name === 'report') ||
|
||||
(item === '管理' && route.name === 'review') ||
|
||||
(item === '风险项管理' && route.name === 'review') ||
|
||||
(item === '对话' && route.name === 'assessment'),
|
||||
}"
|
||||
@mouseenter="prefetch(item)"
|
||||
|
||||
@ -264,8 +264,8 @@ onMounted(() => {
|
||||
<div class="admin-page">
|
||||
<header class="admin-header">
|
||||
<div>
|
||||
<p class="eyebrow">ADMIN CONSOLE</p>
|
||||
<h1>管理工作台</h1>
|
||||
<p class="eyebrow">RISK ITEM CONSOLE</p>
|
||||
<h1>风险项管理工作台</h1>
|
||||
<p class="admin-description">风险项录入、审核与数据质量管理</p>
|
||||
</div>
|
||||
<div class="admin-header-actions">
|
||||
@ -692,8 +692,8 @@ onMounted(() => {
|
||||
><ClipboardCheck :size="16"
|
||||
/></span>
|
||||
<div>
|
||||
<h3>风险项录入</h3>
|
||||
<p>表单录入并提交审核</p>
|
||||
<h3>风险项表单录入</h3>
|
||||
<p>填写风险项并提交审核</p>
|
||||
</div>
|
||||
</header>
|
||||
<button
|
||||
@ -701,7 +701,7 @@ onMounted(() => {
|
||||
type="button"
|
||||
@click="router.push({ name: 'risk-item-entry' })"
|
||||
>
|
||||
<Plus :size="14" />打开录入表单
|
||||
<Plus :size="14" />新建风险项
|
||||
</button>
|
||||
</article>
|
||||
<article
|
||||
@ -713,20 +713,20 @@ onMounted(() => {
|
||||
><ImagePlus :size="16"
|
||||
/></span>
|
||||
<div>
|
||||
<h3>OCR 识别录入</h3>
|
||||
<p>上传检测报告、巡检单或异常报告</p>
|
||||
<h3>风险项 OCR 录入</h3>
|
||||
<p>识别报告并生成待审核风险项</p>
|
||||
</div>
|
||||
</header>
|
||||
<button class="tool-link" type="button" @click="ocrOpen = true">
|
||||
<Upload :size="14" />开始 OCR 识别
|
||||
<Upload :size="14" />识别并录入风险项
|
||||
</button>
|
||||
</article>
|
||||
<article v-if="featureFlags.voiceEntry" class="tool-card">
|
||||
<header>
|
||||
<span class="tool-icon tool-icon--purple"><Mic :size="16" /></span>
|
||||
<div>
|
||||
<h3>语音录入</h3>
|
||||
<p>口述异常信息,自动提取字段</p>
|
||||
<h3>风险项语音录入</h3>
|
||||
<p>口述异常并生成待审核风险项</p>
|
||||
</div>
|
||||
</header>
|
||||
<button class="voice-entry" type="button">
|
||||
@ -742,12 +742,12 @@ onMounted(() => {
|
||||
><FileSpreadsheet :size="16"
|
||||
/></span>
|
||||
<div>
|
||||
<h3>Excel 批量导入</h3>
|
||||
<p>按模板批量进入审核队列</p>
|
||||
<h3>风险项 Excel 批量导入</h3>
|
||||
<p>按模板批量生成待审核风险项</p>
|
||||
</div>
|
||||
</header>
|
||||
<button class="tool-link" type="button">
|
||||
<Upload :size="14" />上传 Excel 文件</button
|
||||
<Upload :size="14" />批量导入风险项</button
|
||||
><small class="tool-hint">单次最多 1000 条,自动去重校验</small>
|
||||
</article>
|
||||
<article class="tool-card config-card">
|
||||
|
||||
@ -31,6 +31,7 @@ const withdrawingId = ref<number | null>(null);
|
||||
const prompt = ref("");
|
||||
const ocrOpen = ref(false);
|
||||
const scrollArea = ref<HTMLElement | null>(null);
|
||||
const composer = ref<InstanceType<typeof AssistantComposer> | null>(null);
|
||||
const tool = ref<
|
||||
| "assessment"
|
||||
| "supplier"
|
||||
@ -116,9 +117,11 @@ watch(
|
||||
},
|
||||
);
|
||||
|
||||
function usePrompt(value: string) {
|
||||
prompt.value = value;
|
||||
void submit();
|
||||
async function usePrompt(value: string) {
|
||||
const current = prompt.value.trim();
|
||||
prompt.value = current ? `${current}\n${value}` : value;
|
||||
await nextTick();
|
||||
composer.value?.focus();
|
||||
}
|
||||
|
||||
function openTool(
|
||||
@ -210,14 +213,9 @@ function submitTool() {
|
||||
return;
|
||||
}
|
||||
closeTool();
|
||||
prompt.value =
|
||||
finalValue || `${selectedTitle}:${toolFile.value?.name ?? "附件"}`;
|
||||
const messageType = toolFile.value
|
||||
? selectedTool === "ocr"
|
||||
? "image"
|
||||
: "audio"
|
||||
: "text";
|
||||
void submit(messageType);
|
||||
void usePrompt(
|
||||
finalValue || `${selectedTitle}:${toolFile.value?.name ?? "附件"}`,
|
||||
);
|
||||
}
|
||||
|
||||
function selectFile(event: Event) {
|
||||
@ -434,6 +432,7 @@ async function scrollToBottom(behavior: "smooth" | "auto" = "smooth") {
|
||||
v-if="!conversation?.messages.length"
|
||||
:can-upload="auth.hasPermission('risk_item.submit')"
|
||||
@select="usePrompt"
|
||||
@tool="openTool"
|
||||
/>
|
||||
|
||||
<template
|
||||
@ -533,6 +532,7 @@ async function scrollToBottom(behavior: "smooth" | "auto" = "smooth") {
|
||||
</div>
|
||||
|
||||
<AssistantComposer
|
||||
ref="composer"
|
||||
v-model="prompt"
|
||||
:loading="loading"
|
||||
:can-upload="auth.hasPermission('risk_item.submit')"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user