通用问答

This commit is contained in:
zhonghua.li
2026-03-31 06:55:31 +08:00
parent d14ef1ef05
commit 41ca1e5d7d
2 changed files with 379 additions and 106 deletions

View File

@@ -135,16 +135,18 @@ export function request(options = {}) {
} }
// 每次请求都自动添加 token登录接口除外 // 每次请求都自动添加 token登录接口除外
// 确保所有后端请求都携带 token 进行身份验证 // 并在需要 token 却不存在时,直接拦截请求并跳转登录页
if (needToken && !isLoginApi(fullUrl)) { if (needToken && !isLoginApi(fullUrl)) {
const token = getToken() const token = getToken()
if (token) { if (!token) {
// 每次请求都添加 token 到 Authorization header console.warn('[Request] token 为空,拦截请求并跳转到登录页面')
requestHeader['Authorization'] = `Bearer ${token}` redirectToLogin()
} else { // 直接抛错中断后续逻辑(包括 uni.request
// 如果没有 token移除可能存在的旧 Authorization header return Promise.reject(new Error('未登录或登录已失效,请先登录'))
delete requestHeader['Authorization']
} }
// 每次请求都添加 token 到 Authorization header
requestHeader['Authorization'] = `Bearer ${token}`
} }
// 添加 roleName 和 scenario 到 header从登录响应中获取 // 添加 roleName 和 scenario 到 header从登录响应中获取

View File

@@ -1,50 +1,201 @@
<template> <template>
<view class="qa-page"> <view class="qa-page">
<view class="page-header"> <!-- 顶部导航参考示例左侧菜单标题新对话右侧加号 -->
<view class="nav-bar"> <view class="qa-header" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="nav-left" @click="goBack"> <view class="qa-header-bar">
<text class="back-icon"></text> <view class="qa-header-left" @click="goBack">
<view class="qa-header-menu-line"></view>
<view class="qa-header-menu-line qa-header-menu-line-short"></view>
</view>
<text class="qa-header-title">新对话</text>
<view class="qa-header-right">
<text class="qa-header-plus"></text>
</view> </view>
<text class="nav-title">通用问答</text>
</view> </view>
</view> </view>
<scroll-view class="content-scroll" scroll-y="true"> <!-- 中间主体区域图标 + 提示文案 -->
<view class="intro-card"> <view class="qa-main" v-if="chatMessages.length === 0">
<text class="intro-title">说明</text> <view class="qa-main-icon-wrapper">
<text class="intro-body">面向未归入具体场景的日常咨询产品基础介绍服务范围预约流程交货周期等先澄清客户真正想知道什么再分点回答避免一次抛出过多信息</text> <text class="qa-main-icon">Q</text>
</view> </view>
<text class="qa-main-text" selectable="true">今天有什么可以帮到你</text>
</view>
<view class="section"> <!-- 对话内容 -->
<text class="section-title">可优先确认</text> <scroll-view v-else class="qa-chat" scroll-y="true">
<view class="bullet" v-for="(t, i) in points" :key="i"> <view class="qa-chat-inner">
<text class="bullet-dot"></text> <view
<text class="bullet-text">{{ t }}</text> v-for="(m, i) in chatMessages"
:key="i"
class="qa-msg"
:class="m.role === 'user' ? 'qa-msg--user' : 'qa-msg--ai'"
>
<view class="qa-msg-actions" :class="m.role === 'ai' ? 'qa-msg-actions--left' : 'qa-msg-actions--right'">
<view class="qa-copy-btn" @click.stop="copyText(m.text)">
<text class="qa-copy-btn-icon"></text>
</view>
</view>
<text class="qa-msg-text" selectable="true">{{ m.text }}</text>
</view> </view>
</view> </view>
<view class="tip-card">
<text class="tip-label">应对关键</text>
<text class="tip-body">先倾听再结构化回答复杂问题拆成结论 + 理由 + 下一步并反问一句确认是否讲清楚</text>
</view>
</scroll-view> </scroll-view>
<!-- 底部输入区域 -->
<view class="qa-input-wrapper" :style="{ paddingBottom: safeBottom + 'px' }">
<view class="qa-input-row">
<view class="qa-input-box">
<textarea
class="qa-textarea"
v-model="message"
placeholder="发送消息或按住说话"
placeholder-class="qa-input-placeholder"
:auto-height="true"
:maxlength="-1"
></textarea>
</view>
<view class="qa-input-send" @click="onSend">
<uni-icons type="top" size="18" color="#fff"></uni-icons>
</view>
</view>
<view class="qa-input-actions">
<view class="qa-chip" @click="goBack">
<uni-icons type="left" size="18" color="#666"></uni-icons>
</view>
</view>
</view>
</view> </view>
</template> </template>
<script> <script>
import { post } from '@/common/request.js'
import { getApiUrl } from '@/common/config.js'
function getSystemInfo() {
try {
const info = uni.getSystemInfoSync();
return {
statusBarHeight: info.statusBarHeight || 20,
safeAreaInsets: info.safeAreaInsets || { bottom: 0 }
};
} catch (e) {
return {
statusBarHeight: 20,
safeAreaInsets: { bottom: 0 }
};
}
}
export default { export default {
data() { data() {
const systemInfo = getSystemInfo();
return { return {
points: [ statusBarHeight: systemInfo.statusBarHeight,
'客户当前处在哪个环节(了解 / 对比 / 即将下单)', safeBottom: systemInfo.safeAreaInsets.bottom || 0,
'是否有明确使用场景或限制条件(空间、预算、时间)', message: '',
'是否需要书面资料或样品/现场勘查' sending: false,
] chatMessages: [],
sessionParentId: ''
}; };
}, },
methods: { methods: {
goBack() { goBack() {
uni.navigateBack(); uni.navigateBack();
},
copyText(text) {
const content = (text || '').toString();
if (!content.trim()) return;
uni.setClipboardData({
data: content,
success: () => {
uni.showToast({ title: '已复制', icon: 'success' });
},
fail: () => {
uni.showToast({ title: '复制失败', icon: 'none' });
}
});
},
normalizeText(value) {
if (value === undefined || value === null) return '';
if (typeof value === 'string') {
// 兼容后端把换行转义成 \\n 的情况
return value.replace(/\\n/g, '\n');
}
try {
return JSON.stringify(value, null, 2);
} catch (e) {
return String(value);
}
},
onSend() {
const text = (this.message || '').trim();
if (!text) {
uni.showToast({ title: '请输入消息', icon: 'none' });
return;
}
if (this.sending) return;
this.sending = true;
// 角色消息先入栈,减少等待感
const userMsg = { role: 'user', text };
this.chatMessages.push(userMsg);
this.message = '';
uni.showLoading({ title: '生成中...', mask: true });
// 该页面对应“通用问答”
const questionType = 'general_qa';
const url = getApiUrl('/api/aiQaItem/askAI');
const payload = {
questionType,
questionContent: text
};
// parentId第一次请求不传后端返回后再带上
if (this.sessionParentId) {
payload.parentId = this.sessionParentId;
}
post(url, payload)
.then((res) => {
uni.hideLoading();
const body = res && res.data ? res.data : null;
const success = body && body.success === true;
if (!success) {
const msg = body?.message || '请求失败';
this.chatMessages.push({ role: 'ai', text: msg });
return;
}
const aiData = body.data || {};
// 若后端返回 parentId则保存并用于后续请求
const returnedParentId = aiData.parentId;
if (returnedParentId !== undefined && returnedParentId !== null && String(returnedParentId).trim()) {
this.sessionParentId = String(returnedParentId).trim();
}
// 兼容不同字段名:优先取常见答案字段,否则兜底 JSON
const rawAnswer =
aiData.answer ??
aiData.content ??
aiData.response ??
aiData.result ??
aiData.message ??
aiData;
this.chatMessages.push({ role: 'ai', text: this.normalizeText(rawAnswer) });
})
.catch((err) => {
uni.hideLoading();
console.error('[general_qa] askAI error:', err);
this.chatMessages.push({ role: 'ai', text: err?.errMsg || '调用失败,请重试' });
})
.finally(() => {
this.sending = false;
});
} }
} }
}; };
@@ -52,122 +203,242 @@ export default {
<style scoped> <style scoped>
.qa-page { .qa-page {
flex: 1;
min-height: 100vh; min-height: 100vh;
background: #f8f9fa; background-color: #F5F5F5;
display: flex;
flex-direction: column;
} }
.page-header { .qa-header {
background: #ffffff; background-color: #FFFFFF;
border-bottom: 1rpx solid #e9ecef; border-bottom: 1rpx solid #EDEDED;
} }
.nav-bar { .qa-header-bar {
height: 88rpx;
padding: 0 32rpx;
display: flex; display: flex;
align-items: center; align-items: center;
height: 88rpx; justify-content: space-between;
padding: 0 30rpx;
} }
.nav-left { .qa-header-left {
width: 60rpx; width: 60rpx;
height: 60rpx; height: 60rpx;
justify-content: center;
align-items: flex-start;
display: flex;
flex-direction: column;
}
.qa-header-menu-line {
width: 26rpx;
height: 4rpx;
border-radius: 4rpx;
background-color: #333333;
margin-bottom: 6rpx;
}
.qa-header-menu-line-short {
width: 16rpx;
}
.qa-header-title {
flex: 1;
text-align: center;
font-size: 30rpx;
font-weight: 500;
color: #333333;
}
.qa-header-right {
width: 60rpx;
height: 60rpx;
align-items: center;
justify-content: center;
display: flex;
}
.qa-header-plus {
font-size: 38rpx;
color: #333333;
}
.qa-main {
flex: 1;
align-items: center;
justify-content: center;
display: flex;
flex-direction: column;
padding-bottom: 200rpx; /* 给底部固定输入框留空间 */
}
.qa-main-icon-wrapper {
width: 120rpx;
height: 120rpx;
border-radius: 60rpx;
background-color: #FFFFFF;
align-items: center;
justify-content: center;
display: flex;
margin-bottom: 32rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.06);
}
.qa-main-icon {
font-size: 60rpx;
color: #2A68FF;
}
.qa-main-text {
font-size: 30rpx;
color: #333333;
}
.qa-chat {
flex: 1;
width: 100%;
padding-bottom: 200rpx; /* 给底部固定输入框留空间 */
}
.qa-chat-inner {
padding: 24rpx 24rpx 12rpx;
}
.qa-msg {
margin-bottom: 20rpx;
max-width: 88%;
position: relative;
}
.qa-msg-actions {
position: absolute;
top: 8rpx;
z-index: 2;
}
.qa-msg-actions--left {
left: 8rpx;
}
.qa-msg-actions--right {
right: 8rpx;
}
.qa-copy-btn {
width: 44rpx;
height: 44rpx;
border-radius: 22rpx;
background-color: #F4F6FA;
border: 1rpx solid #E5EAF3;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
margin-right: 20rpx;
} }
.back-icon { .qa-copy-btn-icon {
font-size: 36rpx; font-size: 22rpx;
color: #333; color: #8a8a8a;
line-height: 1;
} }
.nav-title { .qa-msg--user {
flex: 1; margin-left: auto;
text-align: center; background-color: #EAF2FF;
font-size: 32rpx; border: 1rpx solid #D7E6FF;
font-weight: bold;
color: #333;
} }
.content-scroll { .qa-msg--ai {
height: calc(100vh - 88rpx); margin-right: auto;
background-color: #FFFFFF;
border: 1rpx solid #F0F0F0;
} }
.intro-card { .qa-msg--user,
margin: 24rpx 30rpx; .qa-msg--ai {
padding: 28rpx; padding: 18rpx 20rpx;
background: #fff;
border-radius: 16rpx; border-radius: 16rpx;
} }
.intro-title { .qa-msg-text {
display: block;
font-size: 28rpx;
font-weight: 600;
color: #333;
margin-bottom: 16rpx;
}
.intro-body {
font-size: 26rpx; font-size: 26rpx;
color: #555; color: #333333;
line-height: 1.6; line-height: 1.6;
word-break: break-word;
user-select: text;
} }
.section { .qa-input-wrapper {
margin: 0 30rpx 24rpx; position: fixed;
padding: 28rpx; left: 0;
background: #fff; right: 0;
border-radius: 16rpx; bottom: 0;
z-index: 1000;
padding: 12rpx 20rpx 12rpx;
background-color: #FFFFFF;
box-shadow: 0 -4rpx 12rpx rgba(0, 0, 0, 0.04);
box-sizing: border-box;
} }
.section-title { .qa-input-row {
display: block; flex-direction: row;
font-size: 28rpx;
font-weight: 600;
color: #333;
margin-bottom: 20rpx;
}
.bullet {
display: flex; display: flex;
margin-bottom: 12rpx; align-items: center;
} }
.bullet-dot { .qa-input-box {
margin-right: 12rpx;
color: #007AFF;
font-size: 28rpx;
line-height: 1.5;
}
.bullet-text {
flex: 1; flex: 1;
font-size: 26rpx; min-height: 80rpx;
color: #555; border-radius: 40rpx;
line-height: 1.5; background-color: #F5F5F5;
padding: 0 32rpx;
display: flex;
align-items: center;
} }
.tip-card { .qa-textarea {
margin: 0 30rpx 40rpx; width: 100%;
padding: 28rpx; min-height: 88rpx; /* 默认两行2*44rpx */
background: #f0f7ff; max-height: 176rpx; /* 最大四行4*44rpx */
border-radius: 16rpx; line-height: 44rpx;
border: 1rpx solid #d0e3fc; font-size: 26rpx;
color: #333333;
padding: 18rpx 0;
box-sizing: border-box;
overflow-y: auto;
} }
.tip-label { .qa-input-placeholder {
display: block;
font-size: 26rpx; font-size: 26rpx;
font-weight: 600; color: #999999;
color: #007AFF;
margin-bottom: 12rpx;
} }
.tip-body { .qa-input-send {
font-size: 26rpx; width: 80rpx;
color: #333; height: 80rpx;
line-height: 1.6; border-radius: 40rpx;
margin-left: 16rpx;
background-color: #2A68FF;
align-items: center;
justify-content: center;
display: flex;
} }
.qa-input-actions {
margin-top: 12rpx;
flex-direction: row;
display: flex;
}
.qa-chip {
padding: 8rpx 20rpx;
border-radius: 999rpx;
background-color: #F5F5F5;
margin-right: 16rpx;
display: flex;
align-items: center;
justify-content: center;
}
</style> </style>