完成了AI问历史记录调试

This commit is contained in:
zhonghua.li
2026-03-31 09:02:25 +08:00
parent 41ca1e5d7d
commit 60da0e2dd5

View File

@@ -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.userNameuserName 为空再用手机号兜底。
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;