feat: AI 智能查询改为可拖拽缩放的非模态浮窗并支持多行输入
- 统计面板与收支记录的 AI 智能查询由模态弹窗改为非模态浮窗,窗口打开时可继续操作主界面,无遮罩且不自动聚焦 - 新增通用浮窗组件 FloatingPanel,支持标题栏拖拽与右下角缩放,位置尺寸按页面持久化到 localStorage - 新增 AiQueryWindow 组件,输入由单行改为多行文本域,Enter 换行、Ctrl/Cmd+Enter 提交 - 历史记录聚焦时以浮层展示并可按输入过滤,忽略大小写去重、最多保留 200 条 - 提交查询后不关闭窗口,移除取消按钮,统一由右上角关闭按钮关闭 - 修复示例下拉层级低于浮窗被遮挡的问题,并在打开窗口时重置加载状态,避免按钮一直显示加载中 - 补充 close 与 ai.query.submitHint 的中文、英文、日文三语文案
This commit is contained in:
@@ -3,8 +3,8 @@
|
||||
import { ref, onMounted, onBeforeUnmount, computed, h, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useMessage, useDialog, NButton, NSpace, NDataTable, NTag, NModal, NForm, NFormItem, NInput, NInputNumber, NSelect, NDatePicker, NPagination, NCard, NSwitch, NIcon, NPopover, NAutoComplete, NAlert, NDivider, NDropdown, NUpload, NUploadDragger, NTabs, NTabPane } from 'naive-ui'
|
||||
import { EyeOutline, EyeOffOutline, SparklesOutline, BulbOutline, RefreshOutline, CloudUploadOutline, CheckmarkCircleOutline } from '@vicons/ionicons5'
|
||||
import { useMessage, useDialog, NButton, NSpace, NDataTable, NTag, NModal, NForm, NFormItem, NInput, NInputNumber, NSelect, NDatePicker, NPagination, NCard, NSwitch, NIcon, NPopover, NAutoComplete, NAlert, NDivider, NUpload, NUploadDragger, NTabs, NTabPane } from 'naive-ui'
|
||||
import { EyeOutline, EyeOffOutline, SparklesOutline, RefreshOutline, CloudUploadOutline, CheckmarkCircleOutline } from '@vicons/ionicons5'
|
||||
import type { DataTableColumns, UploadFileInfo } from 'naive-ui'
|
||||
import { getTransactionPage, createTransaction, updateTransaction, deleteTransaction, exportTransaction, toggleTransactionStats, toggleRemarkHidden, batchUpdateCategory, batchTags, getChannels, batchDeleteTransactions, batchCreateTransactions } from '../api/transaction'
|
||||
import { getAiConfig, suggestByAi, suggestOneByAi, applyAiSuggestions, queryByAi, parseRecordByAi, parseCategoryByAi, parseTagByAi, parseChannelByAi } from '../api/ai'
|
||||
@@ -21,6 +21,7 @@ import BillImportModal from './BillImportModal.vue'
|
||||
import TransactionDetailModal from '../components/TransactionDetailModal.vue'
|
||||
import TransactionRelationEditorModal from '../components/TransactionRelationEditorModal.vue'
|
||||
import TransactionRelationModal from '../components/TransactionRelationModal.vue'
|
||||
import AiQueryWindow from '../components/AiQueryWindow.vue'
|
||||
import type { RelationMemberDraft } from '../api/transactionGroup'
|
||||
|
||||
const message = useMessage() // 消息提示
|
||||
@@ -187,18 +188,9 @@ const pendingTag = ref<{ name: string; color: string } | null>(null)
|
||||
const aiApplying = ref(false) // AI 建议应用状态
|
||||
const aiSuggestions = ref<AiSuggestion[]>([]) // AI 建议列表
|
||||
const checkedAiRowKeys = ref<number[]>([]) // AI 建议弹窗中已勾选的代表记录ID
|
||||
const aiQueryText = ref('') // AI 自然语言查询输入
|
||||
const aiQuerying = ref(false) // AI 查询解析状态
|
||||
const aiQueryResult = ref<AiQueryResult | null>(null) // AI 查询解析结果
|
||||
const showAiQueryModal = ref(false) // AI 智能查询弹窗显示状态
|
||||
// AI 查询历史存储键
|
||||
const AI_QUERY_HISTORY_KEY = 'finance.transaction.aiQueryHistory'
|
||||
// AI 查询历史(最近使用,最新在前)
|
||||
const aiQueryHistory = useKeywordHistory({
|
||||
storageKey: AI_QUERY_HISTORY_KEY,
|
||||
text: aiQueryText,
|
||||
clearLabel: () => t('ai.query.clearHistory'),
|
||||
})
|
||||
const showAiRecordModal = ref(false) // AI 记账弹窗显示状态
|
||||
const aiRecordText = ref('') // AI 记账输入
|
||||
const aiRecording = ref(false) // AI 记账解析状态
|
||||
@@ -1625,21 +1617,21 @@ function handleAiQueryOpen() {
|
||||
message.warning(t('ai.needConfig'))
|
||||
return
|
||||
}
|
||||
// 打开时清除上一次可能残留的加载状态,避免按钮一直显示加载中
|
||||
aiQuerying.value = false
|
||||
showAiQueryModal.value = true
|
||||
}
|
||||
|
||||
/** 请求 AI 解析自然语言查询 */
|
||||
async function handleAiQuery() {
|
||||
async function handleAiQuery(text: string) {
|
||||
if (!aiEnabled.value) {
|
||||
message.warning(t('ai.needConfig'))
|
||||
return
|
||||
}
|
||||
const text = aiQueryText.value.trim()
|
||||
if (!text) {
|
||||
message.warning(t('ai.query.emptyInput'))
|
||||
return
|
||||
}
|
||||
aiQueryHistory.record()
|
||||
aiQuerying.value = true
|
||||
try {
|
||||
const res: any = await queryByAi(text)
|
||||
@@ -1651,7 +1643,6 @@ async function handleAiQuery() {
|
||||
}
|
||||
// 直接应用解析结果并搜索,无需二次确认
|
||||
applyAiQuery()
|
||||
showAiQueryModal.value = false
|
||||
} catch (e: any) {
|
||||
message.error(e.message || t('ai.query.failed'))
|
||||
} finally {
|
||||
@@ -1697,34 +1688,8 @@ const aiQueryEmpty = computed(() => {
|
||||
&& r.includeInStats == null
|
||||
})
|
||||
|
||||
/** AI 查询示例(从 i18n 拆分为数组) */
|
||||
const queryExamples = computed(() => t('ai.query.examples').split('|'))
|
||||
|
||||
/** AI 查询示例下拉选项 */
|
||||
const exampleOptions = computed(() => queryExamples.value.map((example) => ({ label: example, key: example })))
|
||||
|
||||
/** 选择示例后填入输入框 */
|
||||
function handleExampleSelect(key: string) {
|
||||
aiQueryText.value = key
|
||||
}
|
||||
|
||||
/** 选中历史项:填入并立即发起查询 */
|
||||
function handleAiQuerySelect(value: string) {
|
||||
if (value === aiQueryHistory.clearValue) {
|
||||
aiQueryHistory.clear()
|
||||
return
|
||||
}
|
||||
aiQueryText.value = value
|
||||
handleAiQuery()
|
||||
}
|
||||
|
||||
/** 回车时发起查询(阻止 NAutoComplete 内部选中建议项) */
|
||||
function handleAiQueryKeydown(e: KeyboardEvent) {
|
||||
if (e.key === 'Enter') {
|
||||
e.stopPropagation()
|
||||
handleAiQuery()
|
||||
}
|
||||
}
|
||||
/** AI 查询示例(浮窗示例下拉) */
|
||||
const aiQueryExamples = computed(() => t('ai.query.examples').split('|'))
|
||||
|
||||
/** 将时间戳格式化为 yyyy-MM-dd */
|
||||
function formatDateLabel(ts: number) {
|
||||
@@ -2029,7 +1994,6 @@ async function doExport() {
|
||||
onMounted(() => {
|
||||
loadFilterPrefs()
|
||||
remarkHistory.load()
|
||||
aiQueryHistory.load()
|
||||
loadAiConfig()
|
||||
loadExcludedDisplayStyle()
|
||||
fetchCategories()
|
||||
@@ -2531,42 +2495,19 @@ onBeforeUnmount(() => {
|
||||
</NSpace>
|
||||
</NModal>
|
||||
|
||||
<!-- AI 智能查询弹窗 -->
|
||||
<NModal v-if="aiEnabled" v-model:show="showAiQueryModal" preset="card" :title="t('ai.query.title')" style="width: 520px; max-width: 92vw;">
|
||||
<NSpace vertical :size="12">
|
||||
<NAlert type="info" :show-icon="true">{{ t('ai.query.hint') }}</NAlert>
|
||||
<NAutoComplete
|
||||
:value="aiQueryText"
|
||||
:options="aiQueryHistory.options.value"
|
||||
:get-show="aiQueryHistory.showMenu"
|
||||
:render-label="aiQueryHistory.renderLabel"
|
||||
:input-props="{ onKeydown: handleAiQueryKeydown }"
|
||||
clearable
|
||||
:placeholder="t('ai.query.placeholder')"
|
||||
style="width: 100%;"
|
||||
@update:value="aiQueryHistory.handleInput"
|
||||
@select="handleAiQuerySelect"
|
||||
>
|
||||
<template #prefix>
|
||||
<NIcon :component="SparklesOutline" />
|
||||
</template>
|
||||
</NAutoComplete>
|
||||
<NSpace justify="space-between" align="center">
|
||||
<NDropdown trigger="click" :options="exampleOptions" @select="handleExampleSelect">
|
||||
<NButton text type="primary" size="small">
|
||||
<template #icon>
|
||||
<NIcon :component="BulbOutline" />
|
||||
</template>
|
||||
{{ t('transaction.exampleLabel') }}
|
||||
</NButton>
|
||||
</NDropdown>
|
||||
<NSpace>
|
||||
<NButton @click="showAiQueryModal = false">{{ t('common.cancel') }}</NButton>
|
||||
<NButton type="primary" :loading="aiQuerying" @click="handleAiQuery">{{ t('ai.query.button') }}</NButton>
|
||||
</NSpace>
|
||||
</NSpace>
|
||||
</NSpace>
|
||||
</NModal>
|
||||
<!-- AI 智能查询浮窗(非模态、可拖拽缩放、多行输入) -->
|
||||
<AiQueryWindow
|
||||
v-if="aiEnabled"
|
||||
v-model:show="showAiQueryModal"
|
||||
:title="t('ai.query.title')"
|
||||
:hint="t('ai.query.hint')"
|
||||
:placeholder="t('ai.query.placeholder')"
|
||||
:examples="aiQueryExamples"
|
||||
:loading="aiQuerying"
|
||||
history-key="finance.transaction.aiQueryHistory"
|
||||
storage-key="finance.transaction.aiQueryWindow"
|
||||
@submit="handleAiQuery"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user