完成了AI问历史记录调试
This commit is contained in:
@@ -3,17 +3,55 @@
|
||||
<!-- 顶部导航(参考示例:左侧菜单,标题,新对话,右侧加号) -->
|
||||
<view class="qa-header" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||||
<view class="qa-header-bar">
|
||||
<view class="qa-header-left" @click="goBack">
|
||||
<view class="qa-header-left" @click="openMainList">
|
||||
<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">
|
||||
<view class="qa-header-right" @click="startNewSession">
|
||||
<text class="qa-header-plus">+</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- AiQaMain 列表面板 -->
|
||||
<view v-if="showMainList" class="main-list-mask" @click="closeMainList"></view>
|
||||
<view v-if="showMainList" class="main-list-drawer">
|
||||
<view class="main-list-drawer-header">
|
||||
<text class="main-list-drawer-title">对话列表</text>
|
||||
<view class="main-list-drawer-close" @click.stop="closeMainList">
|
||||
<text>关闭</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<scroll-view class="main-list-scroll" scroll-y="true">
|
||||
<view v-if="mainListLoading" class="main-list-loading">加载中...</view>
|
||||
|
||||
<view v-else>
|
||||
<view
|
||||
v-for="g in groupedAiQaMainList"
|
||||
:key="g.key"
|
||||
class="main-list-group"
|
||||
>
|
||||
<view class="main-list-group-title">{{ g.label }}</view>
|
||||
|
||||
<view
|
||||
v-for="item in g.items"
|
||||
:key="getMainItemKey(item)"
|
||||
class="main-list-item"
|
||||
@click.stop="selectMainItem(item)"
|
||||
>
|
||||
<text class="main-list-item-text">{{ getMainItemTitle(item) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="!aiQaMainList || aiQaMainList.length === 0" class="main-list-empty">
|
||||
暂无数据
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 中间主体区域:图标 + 提示文案 -->
|
||||
<view class="qa-main" v-if="chatMessages.length === 0">
|
||||
<view class="qa-main-icon-wrapper">
|
||||
@@ -68,8 +106,37 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { post } from '@/common/request.js'
|
||||
import { post, get } from '@/common/request.js'
|
||||
import { getApiUrl } from '@/common/config.js'
|
||||
import { store } from '@/common/store.js'
|
||||
|
||||
function firstNonEmptyTrimmed(...parts) {
|
||||
for (const p of parts) {
|
||||
if (p === undefined || p === null) continue;
|
||||
const s = String(p).trim();
|
||||
if (s) return s;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function getSubmitUserContext() {
|
||||
try {
|
||||
const login = uni.getStorageSync('backend-login-response') || {};
|
||||
const ui = store.userInfo || {};
|
||||
const loginAccount = firstNonEmptyTrimmed(login.userName, ui.username);
|
||||
const userName = firstNonEmptyTrimmed(
|
||||
login.realName,
|
||||
login.name,
|
||||
login.nickName,
|
||||
ui.nickname,
|
||||
login.email,
|
||||
ui.email
|
||||
);
|
||||
return { loginAccount, userName };
|
||||
} catch (e) {
|
||||
return { loginAccount: '', userName: '' };
|
||||
}
|
||||
}
|
||||
|
||||
function getSystemInfo() {
|
||||
try {
|
||||
@@ -86,6 +153,64 @@ function getSystemInfo() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AiQaItem.answerText 可能是 JsonNode(对象)或字符串形式的 JSON。
|
||||
* 这里尽量兼容后端返回的字段名,并返回“可展示”的答案内容(对象/字符串都允许,展示时再 normalizeText)。
|
||||
*/
|
||||
function extractAnswerFromAnswerText(answerText) {
|
||||
if (answerText === undefined || answerText === null) return '';
|
||||
|
||||
// 字符串:尝试解析 JSON,否则当作普通文本
|
||||
if (typeof answerText === 'string') {
|
||||
const s = answerText.trim();
|
||||
if (!s) return '';
|
||||
if (
|
||||
(s.startsWith('{') && s.endsWith('}')) ||
|
||||
(s.startsWith('[') && s.endsWith(']'))
|
||||
) {
|
||||
try {
|
||||
const parsed = JSON.parse(s);
|
||||
return extractAnswerFromAnswerText(parsed);
|
||||
} catch (e) {
|
||||
// ignore parse error
|
||||
}
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
// 对象:优先取常见答案字段,否则原样返回
|
||||
if (typeof answerText === 'object') {
|
||||
return (
|
||||
answerText.answer ??
|
||||
answerText.content ??
|
||||
answerText.response ??
|
||||
answerText.result ??
|
||||
answerText.message ??
|
||||
answerText
|
||||
);
|
||||
}
|
||||
|
||||
return String(answerText);
|
||||
}
|
||||
|
||||
/**
|
||||
* 历史记录里有时会把“问题类型: xxx / 用户问题: xxx”拼在一起展示。
|
||||
* 这里仅做展示清洗:去掉这些标签,保留最终问题内容。
|
||||
*/
|
||||
function stripQuestionTypePrefix(text) {
|
||||
const raw = text === undefined || text === null ? '' : String(text);
|
||||
let s = raw.trim();
|
||||
if (!s) return '';
|
||||
|
||||
// 去掉“问题类型:xxx”(支持中英文冒号、空格/换行容忍)
|
||||
s = s.replace(/^\s*问题类型\s*[::]\s*[^\r\n]*\s*/g, '');
|
||||
// 去掉“用户问题:xxx”中的“用户问题”标签(如果存在)
|
||||
s = s.replace(/^\s*用户问题\s*[::]\s*/g, '');
|
||||
|
||||
// 有些内容可能是两行:问题类型 + 用户问题,处理完后再次 trim
|
||||
return s.trim();
|
||||
}
|
||||
|
||||
export default {
|
||||
data() {
|
||||
const systemInfo = getSystemInfo();
|
||||
@@ -95,13 +220,232 @@ export default {
|
||||
message: '',
|
||||
sending: false,
|
||||
chatMessages: [],
|
||||
sessionParentId: ''
|
||||
sessionParentId: '',
|
||||
|
||||
// AiQaMain 列表面板状态
|
||||
showMainList: false,
|
||||
mainListLoading: false,
|
||||
aiQaMainList: []
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
groupedAiQaMainList() {
|
||||
const groups = [
|
||||
{ key: 'today', label: '今天', items: [] },
|
||||
{ key: 'yesterday', label: '昨天', items: [] },
|
||||
{ key: 'seven', label: '7天内', items: [] },
|
||||
{ key: 'thirty', label: '30天内', items: [] },
|
||||
{ key: 'older', label: '更早', items: [] }
|
||||
];
|
||||
|
||||
const MS_DAY = 24 * 60 * 60 * 1000;
|
||||
const now = new Date();
|
||||
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
|
||||
|
||||
const parseDateToMs = (v) => {
|
||||
if (v === undefined || v === null) return null;
|
||||
const raw = String(v).trim();
|
||||
if (!raw) return null;
|
||||
|
||||
// 数字时间戳(秒/毫秒兜底)
|
||||
if (/^\d+$/.test(raw)) {
|
||||
const n = Number(raw);
|
||||
if (!n) return null;
|
||||
// 10 位通常是秒
|
||||
const ms = raw.length === 10 ? n * 1000 : n;
|
||||
if (ms > 0) return ms;
|
||||
}
|
||||
|
||||
// yyyy-MM-dd 或 yyyy-MM-dd HH:mm:ss
|
||||
const m = raw.match(/^(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2})(?::(\d{2}))?)?/);
|
||||
if (m) {
|
||||
const y = Number(m[1]);
|
||||
const mo = Number(m[2]) - 1;
|
||||
const d = Number(m[3]);
|
||||
const hh = m[4] ? Number(m[4]) : 0;
|
||||
const mi = m[5] ? Number(m[5]) : 0;
|
||||
const ss = m[6] ? Number(m[6]) : 0;
|
||||
return new Date(y, mo, d, hh, mi, ss).getTime();
|
||||
}
|
||||
|
||||
const dt = new Date(raw);
|
||||
const t = dt.getTime();
|
||||
return Number.isNaN(t) ? null : t;
|
||||
};
|
||||
|
||||
const list = Array.isArray(this.aiQaMainList) ? this.aiQaMainList : [];
|
||||
list.forEach((item) => {
|
||||
const timeMs =
|
||||
parseDateToMs(item.createTime) ||
|
||||
parseDateToMs(item.gmtCreate) ||
|
||||
parseDateToMs(item.create_time) ||
|
||||
parseDateToMs(item.time) ||
|
||||
null;
|
||||
if (!timeMs) return;
|
||||
|
||||
const diffDays = Math.floor((todayStart - timeMs) / MS_DAY);
|
||||
const idx = diffDays === 0 ? 0 : diffDays === 1 ? 1 : diffDays <= 6 ? 2 : diffDays <= 29 ? 3 : 4;
|
||||
groups[idx].items.push(item);
|
||||
});
|
||||
|
||||
// 过滤空组,并保持顺序
|
||||
return groups.filter((g) => g.items.length > 0);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
goBack() {
|
||||
uni.navigateBack();
|
||||
},
|
||||
openMainList() {
|
||||
this.showMainList = true;
|
||||
this.fetchAiQaMainList(true);
|
||||
},
|
||||
closeMainList() {
|
||||
this.showMainList = false;
|
||||
},
|
||||
async fetchAiQaMainList(force = false) {
|
||||
if (this.mainListLoading) return;
|
||||
if (!force && Array.isArray(this.aiQaMainList) && this.aiQaMainList.length > 0) return;
|
||||
|
||||
this.mainListLoading = true;
|
||||
try {
|
||||
const login = uni.getStorageSync('backend-login-response') || {};
|
||||
const salesId = login.userId ? String(login.userId).trim() : '';
|
||||
// 后端要求:登录账号传到 salesPhone(用于匹配 AiQaMain.salesPhone)
|
||||
// 这里优先取 login.userName;userName 为空再用手机号兜底。
|
||||
const salesPhone = firstNonEmptyTrimmed(login.userName, login.phone);
|
||||
|
||||
// salesName 暂不参与过滤(避免误筛导致列表为空)
|
||||
const salesName = '';
|
||||
|
||||
const url = getApiUrl('/api/aiQaMain/list');
|
||||
const res = await get(url, {
|
||||
current: 1,
|
||||
size: 50,
|
||||
salesId,
|
||||
salesPhone,
|
||||
salesName
|
||||
});
|
||||
|
||||
const body = res && res.data ? res.data : null;
|
||||
const success = body && body.success === true;
|
||||
if (!success) {
|
||||
const msg = body?.message || '请求失败';
|
||||
uni.showToast({ title: msg, icon: 'none' });
|
||||
this.aiQaMainList = [];
|
||||
return;
|
||||
}
|
||||
|
||||
this.aiQaMainList = body.data || [];
|
||||
} catch (e) {
|
||||
console.error('[general_qa] fetchAiQaMainList error:', e);
|
||||
uni.showToast({ title: '加载失败', icon: 'none' });
|
||||
this.aiQaMainList = [];
|
||||
} finally {
|
||||
this.mainListLoading = false;
|
||||
}
|
||||
},
|
||||
getMainItemKey(item) {
|
||||
const key = firstNonEmptyTrimmed(
|
||||
item.id,
|
||||
item.mainId,
|
||||
item.parentId,
|
||||
item.qaMainId,
|
||||
item.questionId
|
||||
);
|
||||
return key || JSON.stringify(item);
|
||||
},
|
||||
getMainItemTitle(item) {
|
||||
return firstNonEmptyTrimmed(
|
||||
item.questionTitle,
|
||||
item.title,
|
||||
item.questionContent,
|
||||
item.question,
|
||||
item.content,
|
||||
item.summary,
|
||||
item.name,
|
||||
item.id
|
||||
);
|
||||
},
|
||||
async selectMainItem(item) {
|
||||
const parentId = firstNonEmptyTrimmed(
|
||||
item.id, // AiQaMain.id(优先)
|
||||
item.parentId, // 兜底:如果后端把字段直接展开了
|
||||
item.mainId,
|
||||
item.qaMainId,
|
||||
item.sessionParentId
|
||||
);
|
||||
|
||||
uni.hideLoading();
|
||||
this.mainListLoading = false;
|
||||
this.showMainList = false;
|
||||
this.chatMessages = [];
|
||||
this.message = '';
|
||||
this.sending = false;
|
||||
this.sessionParentId = parentId ? String(parentId) : '';
|
||||
|
||||
if (!this.sessionParentId) return;
|
||||
|
||||
// 拉取该会话下的历史对话明细,并恢复到聊天框
|
||||
try {
|
||||
uni.showLoading({ title: '加载历史...', mask: true });
|
||||
const url = getApiUrl('/api/aiQaItem/listByParentId');
|
||||
const res = await get(url, { parentId: this.sessionParentId });
|
||||
|
||||
uni.hideLoading();
|
||||
const body = res && res.data ? res.data : null;
|
||||
const success = body && body.success === true;
|
||||
if (!success) {
|
||||
const msg = body?.message || '查询失败';
|
||||
uni.showToast({ title: msg, icon: 'none' });
|
||||
return;
|
||||
}
|
||||
|
||||
const list = Array.isArray(body.data) ? body.data : [];
|
||||
|
||||
// 后端按 createTime desc 返回,聊天展示一般按时间正序
|
||||
const ordered = list.slice().reverse();
|
||||
const restored = [];
|
||||
|
||||
ordered.forEach((qItem) => {
|
||||
const questionText = firstNonEmptyTrimmed(
|
||||
qItem.questSrc,
|
||||
qItem.quest_src,
|
||||
qItem.questionContent,
|
||||
qItem.question,
|
||||
qItem.title
|
||||
);
|
||||
|
||||
if (questionText) {
|
||||
const cleaned = stripQuestionTypePrefix(this.normalizeText(questionText));
|
||||
if (cleaned) {
|
||||
restored.push({ role: 'user', text: cleaned });
|
||||
}
|
||||
}
|
||||
|
||||
const answerText = extractAnswerFromAnswerText(
|
||||
qItem.answerText ?? qItem.answer_text
|
||||
);
|
||||
|
||||
if (answerText !== undefined && answerText !== null && String(answerText).trim()) {
|
||||
restored.push({ role: 'ai', text: this.normalizeText(answerText) });
|
||||
}
|
||||
});
|
||||
|
||||
this.chatMessages = restored;
|
||||
} catch (e) {
|
||||
uni.hideLoading();
|
||||
console.error('[general_qa] listByParentId error:', e);
|
||||
uni.showToast({ title: '加载失败,请重试', icon: 'none' });
|
||||
}
|
||||
},
|
||||
startNewSession() {
|
||||
uni.hideLoading();
|
||||
this.chatMessages = [];
|
||||
this.sessionParentId = '';
|
||||
this.message = '';
|
||||
this.sending = false;
|
||||
},
|
||||
copyText(text) {
|
||||
const content = (text || '').toString();
|
||||
if (!content.trim()) return;
|
||||
@@ -148,9 +492,12 @@ export default {
|
||||
const questionType = 'general_qa';
|
||||
|
||||
const url = getApiUrl('/api/aiQaItem/askAI');
|
||||
const { loginAccount, userName } = getSubmitUserContext();
|
||||
const payload = {
|
||||
questionType,
|
||||
questionContent: text
|
||||
questionContent: text,
|
||||
loginAccount,
|
||||
userName
|
||||
};
|
||||
// parentId:第一次请求不传;后端返回后再带上
|
||||
if (this.sessionParentId) {
|
||||
@@ -369,6 +716,15 @@ export default {
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
/* 复制按钮在角上绝对定位;气泡已有左右 padding,再给正文加一点边距避免与 44rpx 按钮重叠 */
|
||||
.qa-msg--ai .qa-msg-text {
|
||||
padding-left: 40rpx;
|
||||
}
|
||||
|
||||
.qa-msg--user .qa-msg-text {
|
||||
padding-right: 40rpx;
|
||||
}
|
||||
|
||||
.qa-input-wrapper {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
@@ -381,6 +737,86 @@ export default {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* AiQaMain 列表面板 */
|
||||
.main-list-mask {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.35);
|
||||
z-index: 1999;
|
||||
}
|
||||
|
||||
.main-list-drawer {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 560rpx;
|
||||
background-color: #FFFFFF;
|
||||
z-index: 2000;
|
||||
border-right: 1rpx solid #F0F0F0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.main-list-drawer-header {
|
||||
padding: 22rpx 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1rpx solid #F0F0F0;
|
||||
}
|
||||
|
||||
.main-list-drawer-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.main-list-drawer-close {
|
||||
font-size: 28rpx;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.main-list-scroll {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.main-list-loading,
|
||||
.main-list-empty {
|
||||
padding: 30rpx 24rpx;
|
||||
font-size: 28rpx;
|
||||
color: #888888;
|
||||
}
|
||||
|
||||
.main-list-group {
|
||||
padding: 18rpx 24rpx;
|
||||
}
|
||||
|
||||
.main-list-group-title {
|
||||
font-size: 28rpx;
|
||||
color: #333333;
|
||||
font-weight: 600;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.main-list-item {
|
||||
background-color: #F7F7FA;
|
||||
border: 1rpx solid #EFEFF4;
|
||||
border-radius: 12rpx;
|
||||
padding: 18rpx 16rpx;
|
||||
margin-bottom: 14rpx;
|
||||
}
|
||||
|
||||
.main-list-item-text {
|
||||
font-size: 26rpx;
|
||||
color: #333333;
|
||||
line-height: 1.4;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.qa-input-row {
|
||||
flex-direction: row;
|
||||
display: flex;
|
||||
|
||||
Reference in New Issue
Block a user