|
@@ -3,8 +3,8 @@
|
|
|
<!-- Top operation bar -->
|
|
<!-- Top operation bar -->
|
|
|
<view class="top-bar">
|
|
<view class="top-bar">
|
|
|
<view class="spacer"></view>
|
|
<view class="spacer"></view>
|
|
|
- <view v-if="userStore.isVip" class="export-btn" @click="exportPdf">
|
|
|
|
|
- 导出PDF
|
|
|
|
|
|
|
+ <view class="collapse-btn" @click="chartCollapsed = !chartCollapsed">
|
|
|
|
|
+ {{ chartCollapsed ? '▾ 展开命盘图' : '▴ 折叠命盘图' }}
|
|
|
</view>
|
|
</view>
|
|
|
</view>
|
|
</view>
|
|
|
|
|
|
|
@@ -51,6 +51,7 @@
|
|
|
<!-- System messages: centered italic text, no bubble -->
|
|
<!-- System messages: centered italic text, no bubble -->
|
|
|
<view v-if="msg.senderType === 'system'" class="msg-system" @longpress.stop="onLongPressMsg(msg)">
|
|
<view v-if="msg.senderType === 'system'" class="msg-system" @longpress.stop="onLongPressMsg(msg)">
|
|
|
<text>{{ msg.content }}</text>
|
|
<text>{{ msg.content }}</text>
|
|
|
|
|
+ <text class="msg-time msg-time--system">{{ formatTime(msg.createdAt) }}</text>
|
|
|
</view>
|
|
</view>
|
|
|
<!-- Practitioner messages: green bubble with label -->
|
|
<!-- Practitioner messages: green bubble with label -->
|
|
|
<view v-else-if="msg.senderType === 'practitioner'" class="msg-row practitioner" @longpress.stop="onLongPressMsg(msg)">
|
|
<view v-else-if="msg.senderType === 'practitioner'" class="msg-row practitioner" @longpress.stop="onLongPressMsg(msg)">
|
|
@@ -58,6 +59,7 @@
|
|
|
<view class="msg-bubble msg-practitioner-bubble">
|
|
<view class="msg-bubble msg-practitioner-bubble">
|
|
|
<text>{{ msg.content }}</text>
|
|
<text>{{ msg.content }}</text>
|
|
|
</view>
|
|
</view>
|
|
|
|
|
+ <text class="msg-time">{{ formatTime(msg.createdAt) }}</text>
|
|
|
</view>
|
|
</view>
|
|
|
<!-- Voice message — playable audio bubble -->
|
|
<!-- Voice message — playable audio bubble -->
|
|
|
<view v-else-if="msg.audioUrl" class="msg-row" :class="msg.role">
|
|
<view v-else-if="msg.audioUrl" class="msg-row" :class="msg.role">
|
|
@@ -68,12 +70,14 @@
|
|
|
</view>
|
|
</view>
|
|
|
<text class="msg-audio__label">语音</text>
|
|
<text class="msg-audio__label">语音</text>
|
|
|
</view>
|
|
</view>
|
|
|
|
|
+ <text class="msg-time">{{ formatTime(msg.createdAt) }}</text>
|
|
|
</view>
|
|
</view>
|
|
|
<!-- Default (AI/User): text bubble -->
|
|
<!-- Default (AI/User): text bubble -->
|
|
|
<view v-else class="msg-row" :class="msg.role">
|
|
<view v-else class="msg-row" :class="msg.role">
|
|
|
<view class="msg-bubble">
|
|
<view class="msg-bubble">
|
|
|
<text>{{ msg.content }}</text>
|
|
<text>{{ msg.content }}</text>
|
|
|
</view>
|
|
</view>
|
|
|
|
|
+ <text class="msg-time">{{ formatTime(msg.createdAt) }}</text>
|
|
|
</view>
|
|
</view>
|
|
|
</view>
|
|
</view>
|
|
|
</scroll-view>
|
|
</scroll-view>
|
|
@@ -244,7 +248,7 @@ import { ref, computed, onMounted } from 'vue'
|
|
|
import { useChartStore } from '@/stores/chart'
|
|
import { useChartStore } from '@/stores/chart'
|
|
|
import { useUserStore } from '@/stores/user'
|
|
import { useUserStore } from '@/stores/user'
|
|
|
import TriangleChart from '@/components/TriangleChart.vue'
|
|
import TriangleChart from '@/components/TriangleChart.vue'
|
|
|
-import { chatApi, consultationApi, profileApi, annotationApi, exportApi, interventionApi, planApi, proposalApi, deliveryApi, chartApi } from '@/utils/api'
|
|
|
|
|
|
|
+import { chatApi, consultationApi, profileApi, annotationApi, exportApi, interventionApi, planApi, proposalApi, deliveryApi } from '@/utils/api'
|
|
|
|
|
|
|
|
const store = useChartStore()
|
|
const store = useChartStore()
|
|
|
const userStore = useUserStore()
|
|
const userStore = useUserStore()
|
|
@@ -342,7 +346,8 @@ onMounted(async () => {
|
|
|
role: m.role === 'ai' ? 'ai' : 'user',
|
|
role: m.role === 'ai' ? 'ai' : 'user',
|
|
|
content: m.content,
|
|
content: m.content,
|
|
|
senderType: m.senderType || null,
|
|
senderType: m.senderType || null,
|
|
|
- interventionId: m.interventionId || null
|
|
|
|
|
|
|
+ interventionId: m.interventionId || null,
|
|
|
|
|
+ createdAt: m.createdAt
|
|
|
}))
|
|
}))
|
|
|
} else if (store.currentRecordId) {
|
|
} else if (store.currentRecordId) {
|
|
|
try {
|
|
try {
|
|
@@ -351,7 +356,8 @@ onMounted(async () => {
|
|
|
role: m.role === 'ai' ? 'ai' : 'user',
|
|
role: m.role === 'ai' ? 'ai' : 'user',
|
|
|
content: m.content,
|
|
content: m.content,
|
|
|
senderType: m.senderType || null,
|
|
senderType: m.senderType || null,
|
|
|
- interventionId: m.interventionId || null
|
|
|
|
|
|
|
+ interventionId: m.interventionId || null,
|
|
|
|
|
+ createdAt: m.createdAt
|
|
|
}))
|
|
}))
|
|
|
const userMsgCount = history.filter(m => m.role === 'user').length
|
|
const userMsgCount = history.filter(m => m.role === 'user').length
|
|
|
usedChats.value = Math.max(usedChats.value, userMsgCount)
|
|
usedChats.value = Math.max(usedChats.value, userMsgCount)
|
|
@@ -367,13 +373,15 @@ onMounted(async () => {
|
|
|
if (messages.value.length === 0) {
|
|
if (messages.value.length === 0) {
|
|
|
messages.value = [{
|
|
messages.value = [{
|
|
|
role: 'ai',
|
|
role: 'ai',
|
|
|
- content: `已为您生成命盘。主性格数字为${store.currentChart.O}。请点击下方问题或输入您想了解的内容。`
|
|
|
|
|
|
|
+ content: `已为您生成命盘。主性格数字为${store.currentChart.O}。请点击下方问题或输入您想了解的内容。`,
|
|
|
|
|
+ createdAt: new Date().toISOString()
|
|
|
}]
|
|
}]
|
|
|
}
|
|
}
|
|
|
} else {
|
|
} else {
|
|
|
messages.value = [{
|
|
messages.value = [{
|
|
|
role: 'ai',
|
|
role: 'ai',
|
|
|
- content: '暂无命盘数据,请先返回首页输入出生信息。'
|
|
|
|
|
|
|
+ content: '暂无命盘数据,请先返回首页输入出生信息。',
|
|
|
|
|
+ createdAt: new Date().toISOString()
|
|
|
}]
|
|
}]
|
|
|
}
|
|
}
|
|
|
})
|
|
})
|
|
@@ -515,7 +523,8 @@ async function uploadVoice(filePath) {
|
|
|
|
|
|
|
|
async function sendVoiceMessage(audioUrl) {
|
|
async function sendVoiceMessage(audioUrl) {
|
|
|
// Add voice message locally
|
|
// Add voice message locally
|
|
|
- messages.value.push({ role: 'user', content: '[语音消息]', audioUrl })
|
|
|
|
|
|
|
+ const now = new Date().toISOString()
|
|
|
|
|
+ messages.value.push({ role: 'user', content: '[语音消息]', audioUrl, createdAt: now })
|
|
|
scrollTop.value = 99999
|
|
scrollTop.value = 99999
|
|
|
loading.value = true
|
|
loading.value = true
|
|
|
|
|
|
|
@@ -526,7 +535,7 @@ async function sendVoiceMessage(audioUrl) {
|
|
|
query: '[语音消息]',
|
|
query: '[语音消息]',
|
|
|
audioUrl
|
|
audioUrl
|
|
|
})
|
|
})
|
|
|
- messages.value.push({ role: 'ai', content: res.reply })
|
|
|
|
|
|
|
+ messages.value.push({ role: 'ai', content: res.reply, createdAt: new Date().toISOString() })
|
|
|
usedChats.value++
|
|
usedChats.value++
|
|
|
}
|
|
}
|
|
|
} catch (e) {
|
|
} catch (e) {
|
|
@@ -534,12 +543,14 @@ async function sendVoiceMessage(audioUrl) {
|
|
|
usedChats.value = maxChats.value
|
|
usedChats.value = maxChats.value
|
|
|
messages.value.push({
|
|
messages.value.push({
|
|
|
role: 'ai',
|
|
role: 'ai',
|
|
|
- content: '今日解读次数已用完,开通会员可享无限次解读。'
|
|
|
|
|
|
|
+ content: '今日解读次数已用完,开通会员可享无限次解读。',
|
|
|
|
|
+ createdAt: new Date().toISOString()
|
|
|
})
|
|
})
|
|
|
} else {
|
|
} else {
|
|
|
messages.value.push({
|
|
messages.value.push({
|
|
|
role: 'ai',
|
|
role: 'ai',
|
|
|
- content: e.message || '解读服务暂时不可用,请稍后再试。'
|
|
|
|
|
|
|
+ content: e.message || '解读服务暂时不可用,请稍后再试。',
|
|
|
|
|
+ createdAt: new Date().toISOString()
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -607,14 +618,15 @@ async function sendQuestion(q) {
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- messages.value.push({ role: 'user', content: q })
|
|
|
|
|
|
|
+ const now = new Date().toISOString()
|
|
|
|
|
+ messages.value.push({ role: 'user', content: q, createdAt: now })
|
|
|
chatInput.value = ''
|
|
chatInput.value = ''
|
|
|
loading.value = true
|
|
loading.value = true
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
if (store.currentRecordId) {
|
|
if (store.currentRecordId) {
|
|
|
const res = await chatApi.send({ chartRecordId: store.currentRecordId, query: q })
|
|
const res = await chatApi.send({ chartRecordId: store.currentRecordId, query: q })
|
|
|
- messages.value.push({ role: 'ai', content: res.reply })
|
|
|
|
|
|
|
+ messages.value.push({ role: 'ai', content: res.reply, createdAt: new Date().toISOString() })
|
|
|
usedChats.value++
|
|
usedChats.value++
|
|
|
} else {
|
|
} else {
|
|
|
messages.value.push({
|
|
messages.value.push({
|
|
@@ -627,12 +639,14 @@ async function sendQuestion(q) {
|
|
|
usedChats.value = maxChats.value
|
|
usedChats.value = maxChats.value
|
|
|
messages.value.push({
|
|
messages.value.push({
|
|
|
role: 'ai',
|
|
role: 'ai',
|
|
|
- content: '今日解读次数已用完,开通会员可享无限次解读。'
|
|
|
|
|
|
|
+ content: '今日解读次数已用完,开通会员可享无限次解读。',
|
|
|
|
|
+ createdAt: new Date().toISOString()
|
|
|
})
|
|
})
|
|
|
} else {
|
|
} else {
|
|
|
messages.value.push({
|
|
messages.value.push({
|
|
|
role: 'ai',
|
|
role: 'ai',
|
|
|
- content: e.message || '解读服务暂时不可用,请稍后再试。'
|
|
|
|
|
|
|
+ content: e.message || '解读服务暂时不可用,请稍后再试。',
|
|
|
|
|
+ createdAt: new Date().toISOString()
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -641,13 +655,22 @@ async function sendQuestion(q) {
|
|
|
scrollTop.value = 99999
|
|
scrollTop.value = 99999
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+function formatTime(isoStr) {
|
|
|
|
|
+ if (!isoStr) return ''
|
|
|
|
|
+ const d = new Date(isoStr)
|
|
|
|
|
+ const now = new Date()
|
|
|
|
|
+ const pad = (n) => String(n).padStart(2, '0')
|
|
|
|
|
+ const hhmm = `${pad(d.getHours())}:${pad(d.getMinutes())}`
|
|
|
|
|
+ // Same day → show time only; older → show date + time
|
|
|
|
|
+ if (d.getFullYear() === now.getFullYear() && d.getMonth() === now.getMonth() && d.getDate() === now.getDate()) {
|
|
|
|
|
+ return hhmm
|
|
|
|
|
+ }
|
|
|
|
|
+ return `${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${hhmm}`
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// US-3.3: Regenerate interpretation via action sheet menu
|
|
// US-3.3: Regenerate interpretation via action sheet menu
|
|
|
function showMenu() {
|
|
function showMenu() {
|
|
|
const items = ['🔄 重新开始']
|
|
const items = ['🔄 重新开始']
|
|
|
- // US-9.1: Academic orientation AI (paid users only)
|
|
|
|
|
- if (userStore.isVipActive) {
|
|
|
|
|
- items.push('🎓 学业方向AI')
|
|
|
|
|
- }
|
|
|
|
|
// US-9.2: Apply for manual plan (only when no active plan request)
|
|
// US-9.2: Apply for manual plan (only when no active plan request)
|
|
|
if (!planRequest.value) {
|
|
if (!planRequest.value) {
|
|
|
items.push('📝 申请人工方案')
|
|
items.push('📝 申请人工方案')
|
|
@@ -659,14 +682,11 @@ function showMenu() {
|
|
|
if (!userStore.isVipActive && shareRemaining.value > 0) {
|
|
if (!userStore.isVipActive && shareRemaining.value > 0) {
|
|
|
items.push('📤 分享给好友')
|
|
items.push('📤 分享给好友')
|
|
|
}
|
|
}
|
|
|
- items.push('❌ 取消')
|
|
|
|
|
uni.showActionSheet({
|
|
uni.showActionSheet({
|
|
|
itemList: items,
|
|
itemList: items,
|
|
|
success: async (res) => {
|
|
success: async (res) => {
|
|
|
if (res.tapIndex === 0) {
|
|
if (res.tapIndex === 0) {
|
|
|
await confirmRegenerate()
|
|
await confirmRegenerate()
|
|
|
- } else if (items[res.tapIndex] === '🎓 学业方向AI') {
|
|
|
|
|
- await handleAcademicOrientation()
|
|
|
|
|
} else if (items[res.tapIndex] === '📝 申请人工方案') {
|
|
} else if (items[res.tapIndex] === '📝 申请人工方案') {
|
|
|
await handlePlanApply()
|
|
await handlePlanApply()
|
|
|
} else if (items[res.tapIndex] === '📄 导出 PDF') {
|
|
} else if (items[res.tapIndex] === '📄 导出 PDF') {
|
|
@@ -708,38 +728,6 @@ async function confirmRegenerate() {
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// US-9.1: Academic orientation AI interpretation
|
|
|
|
|
-async function handleAcademicOrientation() {
|
|
|
|
|
- if (!store.currentRecordId) {
|
|
|
|
|
- uni.showToast({ title: '暂无命盘数据', icon: 'none' })
|
|
|
|
|
- return
|
|
|
|
|
- }
|
|
|
|
|
- try {
|
|
|
|
|
- uni.showLoading({ title: 'AI 分析中...' })
|
|
|
|
|
- const result = await chartApi.academicOrientation(store.currentRecordId)
|
|
|
|
|
- uni.hideLoading()
|
|
|
|
|
- if (result && result.content) {
|
|
|
|
|
- // Append academic orientation as a system message in the chat
|
|
|
|
|
- messages.value.push({
|
|
|
|
|
- senderType: 'system',
|
|
|
|
|
- content: '🎓 学业方向AI分析',
|
|
|
|
|
- id: Date.now()
|
|
|
|
|
- })
|
|
|
|
|
- messages.value.push({
|
|
|
|
|
- senderType: 'ai',
|
|
|
|
|
- content: result.content,
|
|
|
|
|
- id: Date.now() + 1
|
|
|
|
|
- })
|
|
|
|
|
- scrollTop.value = 99999
|
|
|
|
|
- } else {
|
|
|
|
|
- uni.showToast({ title: '暂无学业方向分析结果', icon: 'none' })
|
|
|
|
|
- }
|
|
|
|
|
- } catch (e) {
|
|
|
|
|
- uni.hideLoading()
|
|
|
|
|
- uni.showToast({ title: e.message || '学业方向分析失败', icon: 'none' })
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
// US-9.2: Navigate to plan application page
|
|
// US-9.2: Navigate to plan application page
|
|
|
function handlePlanApply() {
|
|
function handlePlanApply() {
|
|
|
if (!store.currentRecordId) {
|
|
if (!store.currentRecordId) {
|
|
@@ -967,7 +955,7 @@ async function submitCounter() {
|
|
|
.spacer {
|
|
.spacer {
|
|
|
flex: 1;
|
|
flex: 1;
|
|
|
}
|
|
}
|
|
|
-.export-btn {
|
|
|
|
|
|
|
+.collapse-btn {
|
|
|
font-size: 13px;
|
|
font-size: 13px;
|
|
|
color: #fbbf24;
|
|
color: #fbbf24;
|
|
|
padding: 6px 12px;
|
|
padding: 6px 12px;
|
|
@@ -1098,6 +1086,7 @@ async function submitCounter() {
|
|
|
|
|
|
|
|
.msg-row.user {
|
|
.msg-row.user {
|
|
|
justify-content: flex-end;
|
|
justify-content: flex-end;
|
|
|
|
|
+ padding-right: 4px;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.msg-bubble {
|
|
.msg-bubble {
|
|
@@ -1109,12 +1098,15 @@ async function submitCounter() {
|
|
|
color: rgba(255, 255, 255, 0.85);
|
|
color: rgba(255, 255, 255, 0.85);
|
|
|
background: rgba(255, 255, 255, 0.06);
|
|
background: rgba(255, 255, 255, 0.06);
|
|
|
border: 1px solid rgba(255, 255, 255, 0.06);
|
|
border: 1px solid rgba(255, 255, 255, 0.06);
|
|
|
|
|
+ word-break: break-word;
|
|
|
|
|
+ overflow-wrap: break-word;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.msg-row.user .msg-bubble {
|
|
.msg-row.user .msg-bubble {
|
|
|
background: rgba(251, 191, 36, 0.12);
|
|
background: rgba(251, 191, 36, 0.12);
|
|
|
border-color: rgba(251, 191, 36, 0.15);
|
|
border-color: rgba(251, 191, 36, 0.15);
|
|
|
color: #fbbf24;
|
|
color: #fbbf24;
|
|
|
|
|
+ margin-left: auto;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/* US-9.3: Practitioner message bubble */
|
|
/* US-9.3: Practitioner message bubble */
|
|
@@ -1136,6 +1128,22 @@ async function submitCounter() {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/* US-9.3: System message — centered italic text */
|
|
/* US-9.3: System message — centered italic text */
|
|
|
|
|
+.msg-time {
|
|
|
|
|
+ font-size: 11px;
|
|
|
|
|
+ color: rgba(255, 255, 255, 0.25);
|
|
|
|
|
+ margin-top: 4px;
|
|
|
|
|
+ display: block;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.msg-row.user .msg-time {
|
|
|
|
|
+ text-align: right;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.msg-time--system {
|
|
|
|
|
+ margin-top: 2px;
|
|
|
|
|
+ font-style: normal;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
.msg-system {
|
|
.msg-system {
|
|
|
text-align: center;
|
|
text-align: center;
|
|
|
padding: 8px 0;
|
|
padding: 8px 0;
|