218 lines
5.4 KiB
Vue
218 lines
5.4 KiB
Vue
<script setup lang="ts">
|
||
import { ChatDotRound } from '@element-plus/icons-vue'
|
||
import { ref } from 'vue'
|
||
import PageContainer from '@/components/PageContainer.vue'
|
||
import { useAnalysisContext } from '@/composables/useAnalysisContext'
|
||
import { resolveQaAnswer, QA_SAMPLE_QUESTIONS } from '@/lib/qa-resolve'
|
||
|
||
const { context, loading, reload } = useAnalysisContext()
|
||
const question = ref('')
|
||
const answer = ref('')
|
||
const answered = ref(false)
|
||
const lastSource = ref('')
|
||
const samplePicker = ref('')
|
||
let applyingSample = false
|
||
|
||
function submitAnswer() {
|
||
if (!context.value) return
|
||
const r = resolveQaAnswer(question.value, context.value)
|
||
answer.value = r.answer
|
||
lastSource.value = r.source
|
||
answered.value = true
|
||
}
|
||
|
||
function onSampleChange(val: string) {
|
||
if (!val) {
|
||
answer.value = ''
|
||
lastSource.value = ''
|
||
answered.value = false
|
||
return
|
||
}
|
||
applyingSample = true
|
||
question.value = val
|
||
applyingSample = false
|
||
submitAnswer()
|
||
}
|
||
|
||
function onQuestionManualInput() {
|
||
if (applyingSample) return
|
||
samplePicker.value = ''
|
||
}
|
||
|
||
function clearAll() {
|
||
question.value = ''
|
||
answer.value = ''
|
||
answered.value = false
|
||
lastSource.value = ''
|
||
samplePicker.value = ''
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<PageContainer @refresh="reload">
|
||
<div v-loading="loading" class="qa-inner">
|
||
<div class="page-header">
|
||
<div class="header-content">
|
||
<div class="header-left">
|
||
<h2 class="page-title">
|
||
<el-icon><ChatDotRound /></el-icon>
|
||
智能问答
|
||
</h2>
|
||
<p class="page-description">
|
||
左侧可<strong>输入问题</strong>并点击「获取答案」,或通过<strong>示例问题</strong>下拉框选择后,右侧即时展示回答。本页为
|
||
<strong>Demo</strong>:答案由内置规则根据当前加载的 mock 数据即时计算;正式上线将改为调用 AI,并结合数仓与项目内指标口径文档作答。
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="qa-split">
|
||
<el-card class="main-card qa-card qa-left" shadow="never">
|
||
<div class="qa-field">
|
||
<div class="qa-label">示例问题</div>
|
||
<el-select
|
||
v-model="samplePicker"
|
||
class="qa-sample-select"
|
||
placeholder="请选择示例问题…"
|
||
filterable
|
||
clearable
|
||
:disabled="loading"
|
||
@change="onSampleChange"
|
||
>
|
||
<el-option v-for="(s, i) in QA_SAMPLE_QUESTIONS" :key="i" :label="s" :value="s" />
|
||
</el-select>
|
||
<p class="qa-hint">选择后右侧立即显示答案;可清空后换一题。</p>
|
||
</div>
|
||
|
||
<div class="qa-field qa-field-divider">
|
||
<div class="qa-label">自定义提问</div>
|
||
<el-input
|
||
v-model="question"
|
||
type="textarea"
|
||
:rows="5"
|
||
placeholder="例如:为什么11月的AE数量是10月的2倍还多?"
|
||
maxlength="500"
|
||
show-word-limit
|
||
@input="onQuestionManualInput"
|
||
/>
|
||
</div>
|
||
<div class="qa-actions">
|
||
<el-button type="primary" :disabled="!question.trim() || loading" @click="submitAnswer">获取答案</el-button>
|
||
<el-button text :disabled="loading" @click="clearAll">清空</el-button>
|
||
</div>
|
||
</el-card>
|
||
|
||
<el-card class="main-card qa-card qa-answer-card qa-right" shadow="never">
|
||
<template #header>
|
||
<div class="qa-answer-head">
|
||
<span>回答</span>
|
||
<el-tag v-if="answered && lastSource" size="small" type="info" effect="plain">来源:{{ lastSource }}</el-tag>
|
||
</div>
|
||
</template>
|
||
<pre v-if="answered" class="qa-answer-body">{{ answer }}</pre>
|
||
<p v-else class="qa-answer-empty">请从左侧选择示例问题,或输入问题后点击「获取答案」。</p>
|
||
</el-card>
|
||
</div>
|
||
</div>
|
||
</PageContainer>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.qa-inner {
|
||
max-width: 1120px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 16px;
|
||
min-height: 120px;
|
||
}
|
||
|
||
.qa-split {
|
||
display: grid;
|
||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||
gap: 16px;
|
||
align-items: stretch;
|
||
}
|
||
|
||
@media (max-width: 880px) {
|
||
.qa-split {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
|
||
.qa-sample-select {
|
||
width: 100%;
|
||
}
|
||
|
||
.qa-field-divider {
|
||
margin-top: 20px;
|
||
padding-top: 16px;
|
||
border-top: 1px solid var(--el-border-color-lighter);
|
||
}
|
||
|
||
.qa-card {
|
||
border-radius: var(--border-radius-md, 12px);
|
||
}
|
||
|
||
.qa-field {
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.qa-label {
|
||
font-weight: 600;
|
||
margin-bottom: 8px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.qa-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.qa-hint {
|
||
margin: 10px 0 0;
|
||
font-size: 12px;
|
||
color: var(--el-text-color-placeholder);
|
||
}
|
||
|
||
.qa-answer-card {
|
||
border-left: 4px solid var(--customer-board-color, var(--el-color-primary));
|
||
}
|
||
|
||
.qa-answer-head {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
}
|
||
|
||
.qa-answer-body {
|
||
margin: 0;
|
||
white-space: pre-wrap;
|
||
word-break: break-word;
|
||
font-family: inherit;
|
||
font-size: 14px;
|
||
line-height: 1.65;
|
||
color: var(--el-text-color-regular);
|
||
}
|
||
|
||
.qa-answer-empty {
|
||
margin: 0;
|
||
font-size: 14px;
|
||
line-height: 1.65;
|
||
color: var(--el-text-color-placeholder);
|
||
}
|
||
|
||
.qa-left,
|
||
.qa-right {
|
||
min-height: 280px;
|
||
}
|
||
|
||
.qa-right :deep(.el-card__body) {
|
||
display: flex;
|
||
flex-direction: column;
|
||
flex: 1;
|
||
min-height: 0;
|
||
}
|
||
</style>
|