refactor(fam-ui): 重构第三阶段 - Streamlit 换成 Vue3+Vite+Tailwind 完全重写
范围变更:原计划是给 Streamlit 界面做视觉美化,用户中途要求换新框架完全重新实现。 最终方案:Vue 3 + Vite + Tailwind CSS v4,本地 npm run build 出静态文件,NAS 不装 Node.js,由 fam-core 的 Flask 直接提供(send_from_directory),原来独立跑在 :8501 的 Streamlit 进程整个退休,前端和 API 合并到 fam-core 的 :8000 一个进程。 fam-core 新增只读 API(ui_api.py):全部包装 db_layer.py 里已有的查询函数,没有 新写查询逻辑(除了下面两处真实缺口)。新增 static_app.py 做 SPA 静态文件服务 (assets 直出 + 非 API 路径回退 index.html 供前端路由接管),app.py 注册顺序上 必须排在其他 /api/* 蓝图之后。 顺带补的两个功能缺口(旧 Streamlit 版本自己绕开 db_layer 写了裸 SQL 才有的功能, db_layer 本身不支持): - get_chat_history 补 offset 参数(分页) - 新增 get_attention_events(统计图表页"关注事件"表格) fam-ui 完全重写为 Vue 3 项目:7 个页面 1:1 迁移功能(事件时间轴/AI对话/对话历史/ 人物管理/统计图表/模型统计/服务状态),深色主题设计系统(Inter+JetBrains Mono 字体、蓝紫渐变强调色、语义色 token)。用本地 npm run dev 代理到真实 NAS 数据做 了完整联调,过程中发现并修了两个真 bug: - URLSearchParams 把 undefined 转成字符串 "undefined" 传给后端,导致日期筛选失效 - MariaDB SUM() 返回 Decimal,Flask 默认序列化成字符串,前端字符串拼接出乱码数字 (ui_api.py::_ser 统一转 int/float 修复) 移动端 H5 补了响应式:原来的固定宽度侧边栏 + flex-wrap 导航在手机宽度下会把每个 按钮挤到文字逐字换行;改成 lg 以上桌面侧边栏、lg 以下移动端顶栏 + 横向可滑动导航 胶囊;人物管理页命名表单(输入框+命名按钮+合并下拉)在窄屏下改为纵向堆叠。 已部署 NAS 并验证:curl 确认 index.html/assets/深层路由/API 路由全部 200; Browser 工具在桌面宽度和手机宽度下把 7 个页面点了一遍(含真实提问一次 AI 对话、 人物头像/事件缩略图加载、命名合并表单),旧 Streamlit 进程已停止。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
103
fam-ui/src/views/Chat.vue
Normal file
103
fam-ui/src/views/Chat.vue
Normal file
@@ -0,0 +1,103 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { api } from '../api.js'
|
||||
import PageHeader from '../components/PageHeader.vue'
|
||||
|
||||
const named = ref([])
|
||||
const queriedPerson = ref('')
|
||||
const queriedDate = ref(new Date().toISOString().slice(0, 10))
|
||||
const userQuestion = ref('')
|
||||
const selectedQuick = ref('自定义')
|
||||
const loading = ref(false)
|
||||
const errorMsg = ref('')
|
||||
const result = ref(null)
|
||||
|
||||
const quickQuestions = computed(() => [
|
||||
`${queriedPerson.value}今天干嘛了?`,
|
||||
`${queriedPerson.value}有没有发生什么需要注意的事情?`,
|
||||
`今天${queriedPerson.value}的活动时间线是什么?`,
|
||||
])
|
||||
|
||||
function applyQuick() {
|
||||
if (selectedQuick.value !== '自定义') userQuestion.value = selectedQuick.value
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const data = await api.namedMembers()
|
||||
named.value = data.members
|
||||
if (named.value.length) queriedPerson.value = named.value[0]
|
||||
} catch { /* 忽略:下拉留空即可 */ }
|
||||
})
|
||||
|
||||
async function ask() {
|
||||
errorMsg.value = ''
|
||||
result.value = null
|
||||
if (!userQuestion.value.trim()) { errorMsg.value = '请输入问题'; return }
|
||||
if (!queriedPerson.value.trim()) { errorMsg.value = '请输入查询人物'; return }
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await api.chatAsk(userQuestion.value, queriedPerson.value, queriedDate.value)
|
||||
result.value = { question: userQuestion.value, answer: data.answer, contextSummary: data.context_summary }
|
||||
} catch (e) {
|
||||
errorMsg.value = e.message
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PageHeader icon="💬" title="AI 对话" sub="基于同步事件上下文的智能问答" />
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label class="mb-1.5 block text-xs font-medium text-text-dim">查询人物</label>
|
||||
<input v-model="queriedPerson" class="w-full rounded-lg border border-border bg-panel px-3 py-2 text-sm text-text outline-none focus:border-accent focus:ring-2 focus:ring-accent/15" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1.5 block text-xs font-medium text-text-dim">查询日期</label>
|
||||
<input type="date" v-model="queriedDate" class="w-full rounded-lg border border-border bg-panel px-3 py-2 text-sm text-text outline-none focus:border-accent focus:ring-2 focus:ring-accent/15" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="named.length" class="mt-4">
|
||||
<label class="mb-1.5 block text-xs font-medium text-text-dim">快捷选择成员</label>
|
||||
<select v-model="queriedPerson" class="w-full rounded-lg border border-border bg-panel px-3 py-2 text-sm text-text-dim">
|
||||
<option value="">(不选)</option>
|
||||
<option v-for="n in named" :key="n" :value="n">{{ n }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<label class="mb-1.5 block text-xs font-medium text-text-dim">快捷提问</label>
|
||||
<select v-model="selectedQuick" @change="applyQuick" class="w-full rounded-lg border border-border bg-panel px-3 py-2 text-sm text-text-dim">
|
||||
<option>自定义</option>
|
||||
<option v-for="q in quickQuestions" :key="q" :value="q">{{ q }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<label class="mb-1.5 block text-xs font-medium text-text-dim">你的问题</label>
|
||||
<textarea v-model="userQuestion" rows="3" class="w-full rounded-lg border border-border bg-panel px-3 py-2 text-sm text-text outline-none focus:border-accent focus:ring-2 focus:ring-accent/15"></textarea>
|
||||
</div>
|
||||
|
||||
<button :disabled="loading" @click="ask"
|
||||
class="mt-4 rounded-xl bg-gradient-to-br from-accent to-accent-2 px-5 py-2.5 text-sm font-semibold text-white shadow-[0_4px_16px_-2px_rgba(91,140,255,.4)] disabled:opacity-50">
|
||||
{{ loading ? 'AI 正在思考…' : '提问' }}
|
||||
</button>
|
||||
|
||||
<p v-if="errorMsg" class="mt-3 text-sm text-danger">{{ errorMsg }}</p>
|
||||
|
||||
<div v-if="result" class="mt-6 space-y-3.5">
|
||||
<div class="rounded-2xl border border-accent/30 bg-accent/15 p-4 text-sm leading-relaxed shadow-[var(--shadow-card)]">
|
||||
<div class="mb-1.5 text-[11px] font-semibold uppercase tracking-wide text-text-mute">❓ 提问</div>
|
||||
{{ result.question }}
|
||||
</div>
|
||||
<div class="rounded-2xl border border-border bg-panel-2 p-4 text-sm leading-relaxed shadow-[var(--shadow-card)]">
|
||||
<div class="mb-1.5 text-[11px] font-semibold uppercase tracking-wide text-text-mute">🤖 回答</div>
|
||||
<div class="whitespace-pre-wrap">{{ result.answer }}</div>
|
||||
</div>
|
||||
<p v-if="result.contextSummary" class="text-xs text-text-faint">上下文: {{ result.contextSummary }}</p>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user