通用问答
This commit is contained in:
@@ -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(从登录响应中获取)
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user