去掉协会 WebView 和魔方账密页,微信登录与咨询接口共用同一 token 和拦截器。 Co-authored-by: Cursor <cursoragent@cursor.com>
156 lines
5.4 KiB
Vue
156 lines
5.4 KiB
Vue
<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>
|