Files
yuxingu-Miniprogram-dev/pages/yxg/ask.vue
T
jackyu66gitandCursor b09d82df8d feat: 小程序统一走 Go 后台,咨询与登录切到 /api/v1
去掉协会 WebView 和魔方账密页,微信登录与咨询接口共用同一 token 和拦截器。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-15 00:26:28 +08:00

156 lines
5.4 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<yxg-page title="AI问答">
<text v-if="bootLoading" class="hint">加载中</text>
<text v-else-if="bootError" class="err">{{ bootError }}</text>
<template v-else>
<view class="quota">
<text>今日剩余 {{ quota?.left ?? quota?.remaining ?? '—' }} </text>
<text class="link" @click="yxgGo('/membership')">加购 </text>
</view>
<scroll-view v-if="profiles.length > 1" class="chips" scroll-x>
<view
v-for="p in profiles"
:key="p.id"
class="chip"
:class="{ on: profileId === p.id }"
@click="profileId = p.id"
>
<text>{{ p.display_name || (p.relation === 'self' ? '我' : 'TA') }}</text>
</view>
</scroll-view>
<view v-if="!messages.length" class="scenes">
<view v-for="s in scenes" :key="s.key" class="scene" @click="useScene(s)">
<text>{{ s.label }}</text>
</view>
</view>
<view class="msgs">
<view v-for="m in messages" :key="m.id" class="msg" :class="m.role">
<text>{{ m.content }}</text>
</view>
</view>
<text v-if="sendError" class="err">{{ sendError }}</text>
<view class="composer">
<input class="inp" v-model="draft" placeholder="结合档案聊聊卡住的事…" confirm-type="send" @confirm="send" />
<view class="send" :class="{ off: sending || !draft.trim() }" @click="send"><text>发送</text></view>
</view>
</template>
</yxg-page>
</template>
<script setup>
import YxgPage from '@/components/yxg/yxg-page.vue'
import { yxgApi } from '@/util/yxgApi.js'
import { ensureAccount, pickableArchiveList } from '@/util/yxgAuth.js'
import { yxgGo } from '@/util/yxgNav.js'
const scenes = [
{ key: 'self', label: '我想更了解自己的性格与互动风格', prompt: '我想更了解自己的性格与互动风格。' },
{ key: 'relation', label: '和重要的人相处时该如何沟通?', prompt: '和重要的人相处时,我该如何更好地沟通?' },
{ key: 'emotion', label: '最近情绪有点乱,想梳理一下', prompt: '最近情绪有点乱,我想梳理一下。' },
{ key: 'career', label: '职业选择上可以注意什么?', prompt: '从我的特点看,职业选择上可以注意什么?' },
]
const bootLoading = ref(true)
const bootError = ref('')
const profiles = ref([])
const profileId = ref('')
const threadId = ref('')
const messages = ref([])
const draft = ref('')
const sending = ref(false)
const sendError = ref('')
const quota = ref(null)
async function boot() {
bootLoading.value = true
bootError.value = ''
if (!(await ensureAccount('/ask'))) {
bootLoading.value = false
return
}
try {
const [list, q] = await Promise.all([yxgApi.listProfiles(), yxgApi.getAskQuota()])
profiles.value = pickableArchiveList(list.items || [])
quota.value = q
const self = profiles.value.find((p) => p.relation === 'self')
profileId.value = self?.id || profiles.value[0]?.id || ''
} catch (e) {
bootError.value = e instanceof Error ? e.message : '加载失败'
} finally {
bootLoading.value = false
}
}
async function ensureThread() {
if (threadId.value) return threadId.value
if (!profileId.value) {
sendError.value = '请先完善档案'
yxgGo('/profile')
throw new Error('no profile')
}
const th = await yxgApi.createAskThread({ profile_id: profileId.value })
threadId.value = th.id || th.thread_id
return threadId.value
}
async function send() {
const text = draft.value.trim()
if (!text || sending.value) return
sending.value = true
sendError.value = ''
try {
const id = await ensureThread()
const out = await yxgApi.sendAskMessage(id, text)
draft.value = ''
if (out.user_message) messages.value.push(out.user_message)
if (out.assistant_message) messages.value.push(out.assistant_message)
if (out.quota) quota.value = out.quota
} catch (e) {
sendError.value = e instanceof Error ? e.message : '发送失败'
} finally {
sending.value = false
}
}
function useScene(s) {
draft.value = s.prompt
send()
}
onShow(boot)
</script>
<style>
page { background-color: #ffd1c7; }
</style>
<style scoped>
.hint, .err { display: block; padding: 16px; font-size: 13px; color: #999; }
.err { color: #e54d42; }
.quota {
margin: 8px 16px; padding: 10px 12px; background: #fff; border-radius: 12px;
display: flex; justify-content: space-between; font-size: 13px; color: #666;
}
.link { color: #e54d42; }
.chips { white-space: nowrap; padding: 0 16px 8px; }
.chip {
display: inline-block; margin-right: 8px; padding: 6px 12px; border-radius: 999px;
background: #fff; font-size: 13px; color: #666;
}
.chip.on { background: #ffe4e4; color: #e54d42; }
.scenes { padding: 0 16px; }
.scene {
background: #fff; border-radius: 14px; padding: 12px 14px; margin-bottom: 8px; font-size: 14px; color: #333;
}
.msgs { padding: 8px 16px 80px; }
.msg { padding: 10px 12px; border-radius: 14px; margin-bottom: 8px; font-size: 14px; line-height: 1.55; max-width: 88%; }
.msg.user, .msg.human { margin-left: auto; background: #e54d42; color: #fff; }
.msg.assistant, .msg.ai { background: #fff; color: #333; }
.composer {
position: fixed; left: 0; right: 0; bottom: 0; padding: 10px 12px 24px;
background: #fff9f7; display: flex; gap: 8px; align-items: center;
}
.inp { flex: 1; height: 40px; background: #fff; border-radius: 20px; padding: 0 14px; font-size: 14px; }
.send { padding: 0 14px; height: 40px; border-radius: 20px; background: #e54d42; color: #fff; display: flex; align-items: center; font-size: 14px; }
.send.off { opacity: 0.45; }
</style>