dev #1
@ -1,6 +1,6 @@
|
||||
import { http } from "@/api/http";
|
||||
import type { ApiResponse } from "@/types/api";
|
||||
import type { Conversation, ConversationSummary } from "@/types/conversation";
|
||||
import type { ApiResponse } from "@/types/api";
|
||||
|
||||
export interface ConversationStreamEvent {
|
||||
type: "start" | "semantic" | "delta" | "done" | "error";
|
||||
@ -22,6 +22,10 @@ export function createConversation(title?: string) {
|
||||
});
|
||||
}
|
||||
|
||||
export function directConversationQuery(kind: "supplier" | "material" | "cases" | "solution", query: string) {
|
||||
return http.post<ApiResponse<Record<string, unknown>>>("/v1/conversations/direct-query", { kind, query });
|
||||
}
|
||||
|
||||
export function fetchConversation(id: number, offset = 0) {
|
||||
return http.get<ApiResponse<Conversation>>(`/v1/conversations/${id}`, {
|
||||
params: { limit: 20, offset },
|
||||
|
||||
@ -47,6 +47,7 @@ const functions: Array<{
|
||||
icon: typeof Search;
|
||||
tone: string;
|
||||
prompt?: string;
|
||||
isNew?: boolean;
|
||||
tool:
|
||||
"assessment" | "supplier" | "material" | "cases" | "solution" | "review";
|
||||
}> = [
|
||||
@ -89,6 +90,7 @@ const functions: Array<{
|
||||
tone: "purple",
|
||||
prompt: "帮我生成一个新品风险预防方案",
|
||||
tool: "solution",
|
||||
isNew: true,
|
||||
},
|
||||
{
|
||||
title: "审核队列",
|
||||
@ -96,6 +98,7 @@ const functions: Array<{
|
||||
icon: ClipboardCheck,
|
||||
tone: "red",
|
||||
tool: "review",
|
||||
isNew: true,
|
||||
},
|
||||
];
|
||||
</script>
|
||||
@ -155,14 +158,11 @@ const functions: Array<{
|
||||
:key="item.title"
|
||||
class="function-card"
|
||||
type="button"
|
||||
@click="
|
||||
item.tool === 'assessment' || !item.prompt
|
||||
? emit('tool', item.tool)
|
||||
: emit('select', item.prompt)
|
||||
"
|
||||
@click="emit('tool', item.tool)"
|
||||
>
|
||||
<span class="function-dot" :class="`function-dot--${item.tone}`"></span>
|
||||
<component :is="item.icon" :size="13" />
|
||||
<span v-if="item.isNew" class="function-new">新</span>
|
||||
<span
|
||||
><strong>{{ item.title }}</strong
|
||||
><small>{{ item.description }}</small></span
|
||||
@ -253,8 +253,8 @@ const functions: Array<{
|
||||
.function-grid {
|
||||
display: grid;
|
||||
max-width: var(--content-width);
|
||||
margin: 8px auto 0;
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
margin: 10px auto 0;
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
@ -264,18 +264,22 @@ const functions: Array<{
|
||||
min-height: 44px;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 9px;
|
||||
padding: 7px 8px;
|
||||
border-radius: 8px;
|
||||
padding: 8px 10px;
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text-secondary);
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s, box-shadow 0.15s, transform 0.15s;
|
||||
}
|
||||
|
||||
.function-card:hover {
|
||||
border-color: var(--color-border-strong);
|
||||
box-shadow: var(--shadow-sm);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.function-card > span:last-child {
|
||||
@ -292,12 +296,26 @@ const functions: Array<{
|
||||
|
||||
.function-card strong {
|
||||
color: var(--color-text);
|
||||
font-size: 10px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.function-card small {
|
||||
color: var(--color-text-faint);
|
||||
font-size: 9px;
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.function-new {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
border-radius: 3px;
|
||||
padding: 0 4px;
|
||||
background: var(--color-accent-subtle);
|
||||
color: var(--color-accent);
|
||||
font-size: 8px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.function-dot {
|
||||
|
||||
@ -7,8 +7,10 @@ import {
|
||||
Building2,
|
||||
CheckCircle2,
|
||||
ClipboardCheck,
|
||||
ChevronDown,
|
||||
PackageSearch,
|
||||
Ruler,
|
||||
Search,
|
||||
Sparkles,
|
||||
} from "@lucide/vue";
|
||||
import { computed, ref } from "vue";
|
||||
@ -24,6 +26,7 @@ const props = defineProps<{
|
||||
const router = useRouter();
|
||||
const emit = defineEmits<{
|
||||
generateReport: [card: ReportCard];
|
||||
openAssessment: [];
|
||||
}>();
|
||||
interface CardData extends AssistantResponseCard {
|
||||
name?: string;
|
||||
@ -42,7 +45,6 @@ interface CardData extends AssistantResponseCard {
|
||||
suggestions?: string[];
|
||||
note?: string;
|
||||
detectedValues?: Array<{ name: string; value: string }>;
|
||||
matchedCount?: number;
|
||||
standards?: Array<{
|
||||
category: string;
|
||||
riskPoint: string;
|
||||
@ -50,7 +52,17 @@ interface CardData extends AssistantResponseCard {
|
||||
source?: string | null;
|
||||
status: string;
|
||||
}>;
|
||||
cases?: Array<{
|
||||
caseId?: number | null;
|
||||
date?: string | null;
|
||||
product?: string | null;
|
||||
problem?: string | null;
|
||||
solution?: string | null;
|
||||
similarity?: number | null;
|
||||
}>;
|
||||
matchedCount?: number;
|
||||
confidence?: string;
|
||||
thinkingSummary?: string[];
|
||||
analysisBase?: string;
|
||||
risks?: Array<{ level: string; title: string; detail: string }>;
|
||||
similarCases?: Array<{
|
||||
@ -79,6 +91,7 @@ interface CardData extends AssistantResponseCard {
|
||||
const data = computed(() => props.card as CardData);
|
||||
type SolutionCase = NonNullable<CardData["similarCases"]>[number];
|
||||
const selectedCase = ref<SolutionCase | null>(null);
|
||||
const thinkingExpanded = ref(false);
|
||||
const standardCategories = computed(() =>
|
||||
[...new Set((data.value.standards ?? []).map((item) => item.category))].join(
|
||||
" / ",
|
||||
@ -135,6 +148,7 @@ function generateReport() {
|
||||
<PackageSearch v-else-if="card.type === 'material'" :size="16" />
|
||||
<BookOpenCheck v-else-if="card.type === 'standard'" :size="16" />
|
||||
<ClipboardCheck v-else-if="card.type === 'compliance'" :size="16" />
|
||||
<Search v-else-if="card.type === 'cases'" :size="22" />
|
||||
<Bot v-else :size="16" />
|
||||
</span>
|
||||
<div>
|
||||
@ -151,11 +165,17 @@ function generateReport() {
|
||||
<small v-else-if="card.type === 'solution'"
|
||||
>AI 推理生成 · 置信度:{{ data.confidence }}</small
|
||||
>
|
||||
<small v-else-if="card.type === 'cases'"
|
||||
>匹配 {{ data.matchedCount ?? data.cases?.length ?? 0 }} 条相似历史案例</small
|
||||
>
|
||||
<small v-else>标准库规则匹配 · 待品控确认</small>
|
||||
</div>
|
||||
<span v-if="card.type === 'solution'" class="solution-confidence"
|
||||
>置信度:{{ data.confidence || "中" }}</span
|
||||
>
|
||||
<span v-else-if="card.type === 'cases'" class="case-semantic-badge"
|
||||
>语义检索</span
|
||||
>
|
||||
</header>
|
||||
|
||||
<template v-if="card.type === 'supplier'">
|
||||
@ -273,7 +293,61 @@ function generateReport() {
|
||||
<p v-if="data.note" class="card-note">{{ data.note }}</p>
|
||||
</template>
|
||||
|
||||
<template v-else-if="card.type === 'cases'">
|
||||
<section class="case-results">
|
||||
<h4>相似案例 TOP {{ Math.min(3, data.cases?.length ?? 0) }}</h4>
|
||||
<template v-if="data.cases?.length">
|
||||
<button
|
||||
v-for="(item, index) in data.cases.slice(0, 3)"
|
||||
:key="`${item.caseId}-${index}`"
|
||||
class="case-result-row"
|
||||
type="button"
|
||||
title="查看案例详情"
|
||||
@click="openSimilarCase(item)"
|
||||
>
|
||||
<span class="case-result-rank">TOP {{ index + 1 }}</span>
|
||||
<span class="case-result-text"
|
||||
>{{ item.date || "日期未知" }}|{{ item.product || "未命名产品" }}|{{
|
||||
item.problem || "异常问题未填写"
|
||||
}} → {{ item.solution || "处理方案未填写" }}</span
|
||||
>
|
||||
<em v-if="item.similarity != null"
|
||||
>相似度 {{ Math.round(item.similarity * 100) }}%</em
|
||||
>
|
||||
</button>
|
||||
</template>
|
||||
<p v-else class="empty">暂无匹配案例</p>
|
||||
</section>
|
||||
<section v-if="data.suggestions?.length" class="case-suggestions">
|
||||
<h4>💡 处理建议</h4>
|
||||
<ol>
|
||||
<li v-for="suggestion in data.suggestions" :key="suggestion">{{ suggestion }}</li>
|
||||
</ol>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<section
|
||||
v-if="data.thinkingSummary?.length"
|
||||
class="solution-thinking"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="solution-thinking__header"
|
||||
@click="thinkingExpanded = !thinkingExpanded"
|
||||
>
|
||||
<span class="solution-thinking__title">思考过程</span>
|
||||
<span class="solution-thinking__status">已完成</span>
|
||||
<ChevronDown
|
||||
:size="13"
|
||||
class="solution-thinking__arrow"
|
||||
:class="{ expanded: thinkingExpanded }"
|
||||
/>
|
||||
</button>
|
||||
<ol v-if="thinkingExpanded" class="solution-thinking__steps">
|
||||
<li v-for="step in data.thinkingSummary" :key="step">{{ step }}</li>
|
||||
</ol>
|
||||
</section>
|
||||
<div class="solution-base">
|
||||
<b>分析基础:</b
|
||||
>{{ data.analysisBase || "基于相似品类、相似物料与标准库规则生成。" }}
|
||||
@ -393,9 +467,20 @@ function generateReport() {
|
||||
<CheckCircle2 :size="14" />追加到报告
|
||||
</button>
|
||||
<button
|
||||
v-if="card.type === 'cases'"
|
||||
type="button"
|
||||
:disabled="card.type !== 'solution'"
|
||||
:title="card.type === 'solution' ? '跳转判定标准库' : '功能开发中'"
|
||||
class="response-actions__primary"
|
||||
@click="emit('openAssessment')"
|
||||
>
|
||||
评估关联风险
|
||||
</button>
|
||||
<button v-if="card.type === 'cases'" type="button" @click="openStandardLibrary">
|
||||
查看标准
|
||||
</button>
|
||||
<button
|
||||
v-if="card.type === 'solution'"
|
||||
type="button"
|
||||
title="跳转判定标准库"
|
||||
@click="openStandardLibrary"
|
||||
>
|
||||
查看标准详情
|
||||
@ -523,6 +608,9 @@ function generateReport() {
|
||||
.response-card--material {
|
||||
border-left: 3px solid var(--color-warning);
|
||||
}
|
||||
.response-card--cases {
|
||||
border-color: var(--color-border);
|
||||
}
|
||||
.response-card--standard,
|
||||
.response-card--compliance {
|
||||
border-left: 3px solid var(--color-accent);
|
||||
@ -571,6 +659,35 @@ function generateReport() {
|
||||
background: linear-gradient(135deg, var(--color-accent), var(--color-purple));
|
||||
color: #fff;
|
||||
}
|
||||
.response-card--cases .response-card__icon {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
color: #111827;
|
||||
background: transparent;
|
||||
}
|
||||
.response-card--cases .response-card__header {
|
||||
position: relative;
|
||||
align-items: flex-start;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
.response-card--cases .response-card__header h3 {
|
||||
color: var(--color-warning);
|
||||
font-size: 14px;
|
||||
}
|
||||
.response-card--cases .response-card__header small {
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
.case-semantic-badge {
|
||||
margin-left: auto;
|
||||
border-radius: 999px;
|
||||
padding: 4px 9px;
|
||||
background: #eff6ff;
|
||||
color: #2563eb;
|
||||
font-size: 10px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.response-card--solution .response-card__header {
|
||||
margin-bottom: 14px;
|
||||
border-bottom: 0;
|
||||
@ -735,6 +852,77 @@ function generateReport() {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.case-result-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
margin-top: 5px;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
padding: 7px 9px;
|
||||
background: #f5f5f4;
|
||||
color: var(--color-text-secondary);
|
||||
font: inherit;
|
||||
line-height: 1.4;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
.case-result-row:hover {
|
||||
background: #e7e5e4;
|
||||
}
|
||||
|
||||
.case-result-rank {
|
||||
flex: 0 0 auto;
|
||||
border-radius: 999px;
|
||||
padding: 2px 6px;
|
||||
background: #eff6ff;
|
||||
color: #2563eb;
|
||||
font-size: 9px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.case-result-row > div {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.case-result-text {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
font-size: 11px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.case-result-row em {
|
||||
flex: 0 0 auto;
|
||||
color: var(--color-accent);
|
||||
font-size: 10px;
|
||||
font-style: normal;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.case-suggestions {
|
||||
margin-top: 15px;
|
||||
border: 1px solid #86efac;
|
||||
border-radius: 7px;
|
||||
padding: 12px 14px;
|
||||
background: #f0fdf4;
|
||||
color: #166534;
|
||||
}
|
||||
.case-suggestions h4 {
|
||||
margin: 0 0 7px;
|
||||
color: #166534;
|
||||
font-size: 12px;
|
||||
}
|
||||
.case-suggestions ol {
|
||||
margin: 0;
|
||||
padding-left: 17px;
|
||||
}
|
||||
.case-suggestions li {
|
||||
margin: 4px 0;
|
||||
font-size: 11px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
.solution-standard-row > div b {
|
||||
color: var(--color-text-secondary);
|
||||
font-weight: 500;
|
||||
@ -796,6 +984,52 @@ function generateReport() {
|
||||
font-size: 12px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
.solution-thinking {
|
||||
margin-top: 12px;
|
||||
border: 1px solid var(--color-border-light);
|
||||
border-radius: 7px;
|
||||
background: var(--color-bg-warm);
|
||||
}
|
||||
.solution-thinking__header {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
border: 0;
|
||||
padding: 8px 10px;
|
||||
background: transparent;
|
||||
color: var(--color-text-secondary);
|
||||
font: inherit;
|
||||
font-size: 11px;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
.solution-thinking__title {
|
||||
font-weight: 650;
|
||||
}
|
||||
.solution-thinking__status {
|
||||
color: var(--color-success);
|
||||
font-size: 10px;
|
||||
}
|
||||
.solution-thinking__arrow {
|
||||
margin-left: auto;
|
||||
color: var(--color-text-faint);
|
||||
transition: transform 0.18s ease;
|
||||
}
|
||||
.solution-thinking__arrow.expanded {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
.solution-thinking__steps {
|
||||
margin: 0;
|
||||
border-top: 1px solid var(--color-border-light);
|
||||
padding: 7px 12px 8px 27px;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 10px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.solution-thinking__steps li {
|
||||
margin: 2px 0;
|
||||
}
|
||||
.solution-standards {
|
||||
margin-bottom: 10px;
|
||||
border: 1px solid #bfdbfe;
|
||||
|
||||
@ -9,10 +9,12 @@ import AssistantWelcome from "@/components/assistant/AssistantWelcome.vue";
|
||||
import RiskAnalysisCard from "@/components/assistant/RiskAnalysisCard.vue";
|
||||
import AssistantResponseCard from "@/components/assistant/AssistantResponseCard.vue";
|
||||
import OcrRecognitionDialog from "@/components/ocr/OcrRecognitionDialog.vue";
|
||||
import { directConversationQuery } from "@/api/conversations";
|
||||
import { useConversationStore } from "@/stores/conversation.store";
|
||||
import { useAuthStore } from "@/stores/auth.store";
|
||||
import { storeToRefs } from "pinia";
|
||||
import type { ReportCard, RiskAssessmentResult } from "@/types/risk";
|
||||
import type { AssistantResponseCard as AssistantResponseCardData } from "@/types/conversation";
|
||||
import {
|
||||
readConversationReport,
|
||||
writeConversationReport,
|
||||
@ -46,6 +48,10 @@ const tool = ref<
|
||||
const toolInput = ref("");
|
||||
const toolError = ref("");
|
||||
const toolFile = ref<File | null>(null);
|
||||
const directResult = ref<Record<string, unknown> | null>(null);
|
||||
const directLoading = ref(false);
|
||||
const directUserPrompt = ref("");
|
||||
const directResponseCard = ref<AssistantResponseCardData | null>(null);
|
||||
const expandedReplies = ref(new Set<number>());
|
||||
const primaryReportReady = ref(false);
|
||||
const assessmentFields = ref({
|
||||
@ -96,9 +102,15 @@ const toolPlaceholder = computed(() => {
|
||||
return "请输入内容";
|
||||
});
|
||||
const toolSubmitLabel = computed(() =>
|
||||
["assessment", "material", "cases", "solution"].includes(tool.value ?? "")
|
||||
? "开始分析"
|
||||
: "带入对话",
|
||||
["supplier", "material", "cases"].includes(tool.value ?? "")
|
||||
? directLoading.value
|
||||
? "查询中…"
|
||||
: "查询"
|
||||
: tool.value === "solution"
|
||||
? directLoading.value
|
||||
? "生成中…"
|
||||
: "生成方案"
|
||||
: "提交",
|
||||
);
|
||||
|
||||
const loadingText = computed(() =>
|
||||
@ -186,7 +198,11 @@ function openTool(
|
||||
return;
|
||||
}
|
||||
if (value === "supplier") {
|
||||
router.push({ name: "supplier" });
|
||||
tool.value = "supplier";
|
||||
toolInput.value = "";
|
||||
toolFile.value = null;
|
||||
toolError.value = "";
|
||||
directResult.value = null;
|
||||
return;
|
||||
}
|
||||
if (value === "ocr") {
|
||||
@ -197,6 +213,7 @@ function openTool(
|
||||
toolInput.value = "";
|
||||
toolFile.value = null;
|
||||
toolError.value = "";
|
||||
directResult.value = null;
|
||||
assessmentFields.value = {
|
||||
category: "",
|
||||
specification: "",
|
||||
@ -210,9 +227,32 @@ function openTool(
|
||||
function closeTool() {
|
||||
tool.value = null;
|
||||
toolError.value = "";
|
||||
directResult.value = null;
|
||||
}
|
||||
|
||||
function submitTool() {
|
||||
async function submitDirectTool(kind: "supplier" | "material" | "cases" | "solution", query: string) {
|
||||
directLoading.value = true;
|
||||
toolError.value = "";
|
||||
try {
|
||||
const response = await directConversationQuery(kind, query);
|
||||
directResult.value = response.data.data ?? null;
|
||||
if (response.data.data) {
|
||||
directUserPrompt.value = query;
|
||||
directResponseCard.value = response.data.data as AssistantResponseCardData;
|
||||
closeTool();
|
||||
await nextTick();
|
||||
await scrollToBottom();
|
||||
}
|
||||
} catch (reason) {
|
||||
toolError.value = axios.isAxiosError(reason)
|
||||
? String(reason.response?.data?.message || "查询失败")
|
||||
: reason instanceof Error ? reason.message : "查询失败";
|
||||
} finally {
|
||||
directLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function submitTool() {
|
||||
const selectedTool = tool.value;
|
||||
const selectedTitle = toolTitle.value;
|
||||
if (selectedTool === "assessment") {
|
||||
@ -237,31 +277,33 @@ function submitTool() {
|
||||
toolError.value = "请输入要查询的物料名称";
|
||||
return;
|
||||
}
|
||||
toolInput.value = `查物料风险:${value}`;
|
||||
toolInput.value = value;
|
||||
} else if (selectedTool === "cases") {
|
||||
const value = toolInput.value.trim();
|
||||
if (!value) {
|
||||
toolError.value = "请输入要检索的异常现象或关键词";
|
||||
return;
|
||||
}
|
||||
toolInput.value = `搜索相似案例:${value}`;
|
||||
toolInput.value = value;
|
||||
} else if (selectedTool === "solution") {
|
||||
const value = toolInput.value.trim();
|
||||
if (!value) {
|
||||
toolError.value = "请输入需要生成方案的产品或风险问题";
|
||||
return;
|
||||
}
|
||||
toolInput.value = `生成 AI 案例推理方案:${value}`;
|
||||
toolInput.value = value;
|
||||
}
|
||||
const finalValue = toolInput.value.trim();
|
||||
if (!finalValue && !toolFile.value) {
|
||||
toolError.value = "请先填写必填内容";
|
||||
return;
|
||||
}
|
||||
if (["supplier", "material", "cases", "solution"].includes(selectedTool ?? "")) {
|
||||
await submitDirectTool(selectedTool as "supplier" | "material" | "cases" | "solution", finalValue);
|
||||
return;
|
||||
}
|
||||
closeTool();
|
||||
void usePrompt(
|
||||
finalValue || `${selectedTitle}:${toolFile.value?.name ?? "附件"}`,
|
||||
);
|
||||
void usePrompt(finalValue || `${selectedTitle}:${toolFile.value?.name ?? "附件"}`);
|
||||
}
|
||||
|
||||
function selectFile(event: Event) {
|
||||
@ -588,6 +630,7 @@ async function scrollToBottom(behavior: "smooth" | "auto" = "smooth") {
|
||||
:card="message.responseCard"
|
||||
:can-append-report="primaryReportReady"
|
||||
@generate-report="openSolutionReport"
|
||||
@open-assessment="openTool('assessment')"
|
||||
/>
|
||||
<time class="message-time">{{
|
||||
formatMessageTime(message.createdAt)
|
||||
@ -596,6 +639,24 @@ async function scrollToBottom(behavior: "smooth" | "auto" = "smooth") {
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="directResponseCard">
|
||||
<div class="message message--user">
|
||||
<div class="message-avatar">{{ auth.user?.name?.slice(0, 1) ?? "我" }}</div>
|
||||
<div class="user-bubble">{{ directUserPrompt }}</div>
|
||||
</div>
|
||||
<div class="message message--assistant">
|
||||
<div class="message-avatar">蝶</div>
|
||||
<div class="message-body">
|
||||
<AssistantResponseCard
|
||||
:card="directResponseCard"
|
||||
:can-append-report="primaryReportReady"
|
||||
@generate-report="openSolutionReport"
|
||||
@open-assessment="openTool('assessment')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-if="loading" class="message message--assistant">
|
||||
<div class="message-avatar">蝶</div>
|
||||
<div class="message-body">
|
||||
@ -686,11 +747,49 @@ async function scrollToBottom(behavior: "smooth" | "auto" = "smooth") {
|
||||
/>
|
||||
</label>
|
||||
<p v-if="toolError" class="dialog-error">{{ toolError }}</p>
|
||||
<section v-if="directResult" class="direct-result-card">
|
||||
<template v-if="tool === 'supplier'">
|
||||
<span class="direct-tier">{{ directResult.tier || "新供应商" }}</span>
|
||||
<div class="direct-lines">
|
||||
<p>近 3 年异常 <b>N={{ directResult.anomalyCount ?? 0 }}</b>(代理合作频次口径)</p>
|
||||
<p>基础分 <b>{{ directResult.supplierBaseScore ?? 70 }}</b> · α <b>×{{ Number(directResult.frequencyCoefficient ?? 1).toFixed(2) }}</b></p>
|
||||
<p>供应商得分 S供 = <b>{{ directResult.supplierScore ?? 70 }}</b> 分</p>
|
||||
</div>
|
||||
<p v-if="Array.isArray(directResult.topProblems) && directResult.topProblems.length">
|
||||
高频异常:{{ (directResult.topProblems as Array<{name: string; count: number}>).map((item) => `${item.name}(${item.count} 条)`).join("、") }}
|
||||
</p>
|
||||
<p v-if="Array.isArray(directResult.suggestions)">建议:{{ (directResult.suggestions as string[]).join(" + ") }}</p>
|
||||
</template>
|
||||
<template v-else-if="tool === 'material'">
|
||||
<strong>{{ directResult.title || "物料风险分析" }}</strong>
|
||||
<p>物料:<b>{{ directResult.material || directResult.query }}</b> · 历史异常 {{ directResult.anomalyCount ?? 0 }} 条</p>
|
||||
<p v-if="Array.isArray(directResult.frequentProblems) && directResult.frequentProblems.length">
|
||||
高频问题:{{ (directResult.frequentProblems as Array<{name: string; count: number}>).map((item) => `${item.name}(${item.count} 条)`).join("、") }}
|
||||
</p>
|
||||
<p v-if="Array.isArray(directResult.suggestions)">建议:{{ (directResult.suggestions as string[]).join(";") }}</p>
|
||||
</template>
|
||||
<template v-else-if="tool === 'cases'">
|
||||
<strong>相似案例检索</strong>
|
||||
<p>关键词:{{ directResult.query }} · 匹配 {{ directResult.matchedCount ?? 0 }} 条</p>
|
||||
<ul v-if="Array.isArray(directResult.cases)" class="direct-case-list">
|
||||
<li v-for="(item, index) in (directResult.cases as Array<Record<string, unknown>>).slice(0, 5)" :key="index">
|
||||
<b>{{ item.product || "未命名产品" }}</b>:{{ item.problem || item.description || "异常描述未填写" }}
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
<template v-else>
|
||||
<strong>{{ directResult.title || "AI 方案生成" }}</strong>
|
||||
<p>{{ directResult.analysisBase || "已结合历史案例与标准库生成分析方案。" }}</p>
|
||||
<ol v-if="Array.isArray(directResult.suggestions)" class="direct-suggestion-list">
|
||||
<li v-for="item in directResult.suggestions as string[]" :key="item">{{ item }}</li>
|
||||
</ol>
|
||||
</template>
|
||||
</section>
|
||||
<div class="dialog-actions">
|
||||
<button type="button" class="dialog-secondary" @click="closeTool">
|
||||
取消
|
||||
</button>
|
||||
<button type="submit" class="dialog-primary">
|
||||
<button type="submit" class="dialog-primary" :disabled="directLoading">
|
||||
{{ toolSubmitLabel }}
|
||||
</button>
|
||||
</div>
|
||||
@ -1070,6 +1169,52 @@ async function scrollToBottom(behavior: "smooth" | "auto" = "smooth") {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.direct-result-card {
|
||||
margin-top: 14px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 9px;
|
||||
padding: 16px;
|
||||
background: var(--color-bg-warm);
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 13px;
|
||||
line-height: 1.75;
|
||||
}
|
||||
|
||||
.direct-result-card p {
|
||||
margin: 3px 0;
|
||||
}
|
||||
|
||||
.direct-result-card strong {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: var(--color-text);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.direct-tier {
|
||||
display: inline-block !important;
|
||||
border-radius: 999px;
|
||||
padding: 3px 10px;
|
||||
background: var(--color-warning-soft);
|
||||
color: #92400e !important;
|
||||
font-size: 11px !important;
|
||||
}
|
||||
|
||||
.direct-lines {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.direct-result-card ul,
|
||||
.direct-result-card ol {
|
||||
margin: 8px 0 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.direct-case-list li,
|
||||
.direct-suggestion-list li {
|
||||
margin: 3px 0;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.conversation-column {
|
||||
padding-inline: 12px;
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import type { RiskAssessmentResult, RiskLevel } from "@/types/risk";
|
||||
|
||||
export interface AssistantResponseCard {
|
||||
type: "supplier" | "material" | "standard" | "compliance" | "solution";
|
||||
type: "supplier" | "material" | "standard" | "compliance" | "solution" | "cases";
|
||||
title: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user