Compare commits

...

8 Commits

Author SHA1 Message Date
wangdongbo
1ae7b23641 Merge branch 'dev-2.491'
# Conflicts:
#	pages.json
#	pages/archive/edit-archive.vue
#	pages/login/login.vue
#	utils/api.js
2026-08-21 14:25:40 +08:00
wangdongbo
fdf9428f36 no message 2026-08-14 10:13:46 +08:00
wangdongbo
3770881c92 ai咨询助手调整 2026-08-13 09:47:38 +08:00
wangdongbo
bbf9e81f1f no message 2026-08-11 09:21:20 +08:00
wangdongbo
68c6db7422 咨询助手优化 2026-08-10 14:00:23 +08:00
wangdongbo
dace2bb2da ai咨询服务优化 2026-08-07 18:15:14 +08:00
wangdongbo
1654b4267e no message 2026-08-07 10:10:32 +08:00
wangdongbo
ea9535be7b 咨询助理功能开发 2026-08-07 09:35:21 +08:00
13 changed files with 469 additions and 35 deletions

View File

@ -156,6 +156,33 @@
}
]
},
{
"root": "pages/ai-consult",
"name": "ai-consult",
"pages": [
{
"path": "list",
"style": {
"navigationBarTitleText": "咨询记录",
"disableScroll": true
}
},
{
"path": "chat",
"style": {
"navigationBarTitleText": "AI咨询助理",
"disableScroll": true
}
},
{
"path": "entry",
"style": {
"navigationBarTitleText": "AI咨询助理",
"disableScroll": true
}
}
]
},
{
"root": "pages/experience-coupon",
"name": "experience-coupon",

158
pages/ai-consult/chat.vue Normal file
View File

@ -0,0 +1,158 @@
<template>
<view class="chat-page">
<scroll-view scroll-y class="chat-content" :scroll-into-view="bottomId">
<view class="message-list">
<view v-for="(item, index) in messages" :key="item._id" :id="`message-${item._id}`" class="message-item" :class="`message-${item.sender}`">
<view v-if="shouldShowTime(item, index)" class="time-divider">{{ formatMessageTime(item.createTime) }}</view>
<evaluation-card v-if="item.messageType === 'consult_ended' && (item.rateId || session?.rateId)" class="rate-card" :extension="{ rateId: item.rateId || session.rateId }" :corp-id="corpId" :doctor-info="rateAssistantInfo" />
<view v-else-if="item.sender === 'system' && item.messageType !== 'risk_notice'" class="system-message" :class="{ warn: item.messageType === 'sensitive_notice' }">{{ displayContent(item) }}</view>
<view v-else-if="item.messageType !== 'risk_notice'" class="message-content">
<view v-if="item.sender === 'assistant'" class="avatar assistant-avatar">AI</view>
<view class="message-bubble-container">
<view v-if="item.sender === 'assistant'" class="username-label">{{ session?.assistantName || 'AI咨询助理' }}</view>
<view class="message-bubble" :class="{ pending: item._sendStatus === 'pending' }">
<text class="message-text">{{ displayContent(item) }}</text>
</view>
<text v-if="item._sendStatus === 'failed'" class="send-status">发送失败请重试</text>
</view>
<view v-if="item.sender === 'patient'" class="avatar patient-avatar"></view>
</view>
</view>
<view v-if="waitingAi" class="message-item message-assistant">
<view class="message-content"><view class="avatar assistant-avatar">AI</view><view class="message-bubble-container"><view class="username-label">{{ session?.assistantName || 'AI咨询助理' }}</view><view class="message-bubble thinking"><view class="thinking-dot"></view><view class="thinking-dot"></view><view class="thinking-dot"></view></view></view></view>
</view>
<view id="bottom" />
</view>
</scroll-view>
<view v-if="active" class="input-section">
<textarea v-model="content" class="text-input" maxlength="1000" auto-height confirm-type="send" :show-confirm-bar="false" placeholder="请输入消息" @confirm="send" />
<button class="send-btn" :disabled="!content.trim()" @click="send">发送</button>
</view>
<view v-else class="ended">本次AI咨询已结束</view>
</view>
</template>
<script setup>
import { ref, nextTick, computed } from 'vue';
import { onLoad, onShow } from '@dcloudio/uni-app';
import api from '@/utils/api';
import useAccountStore from '@/store/account';
import { storeToRefs } from 'pinia';
import EvaluationCard from '@/pages/message/components/special-message/evaluation.vue';
const { openid } = storeToRefs(useAccountStore());
const corpId = ref('');
const sessionId = ref('');
const session = ref(null);
const messages = ref([]);
const content = ref('');
const pendingRequestCount = ref(0);
const waitingAi = ref(false);
const bottomId = ref('');
const active = ref(false);
const rateAssistantInfo = computed(() => ({ name: session.value?.assistantName || 'AI咨询助理', title: 'AI咨询助理', department: session.value?.teamName || '', avatar: '' }));
function displayContent(item) {
const value = item?.content || '';
if (item?.sender !== 'assistant' || typeof value !== 'string') return value;
const contents = [];
const matcher = /"streamContent"\s*:\s*"((?:\\.|[^"\\])*)"/g;
let match;
while ((match = matcher.exec(value))) {
try {
const text = JSON.parse(`"${match[1]}"`);
if (text) contents.push(text);
} catch (error) {
//
}
}
return contents.join('') || value;
}
function formatMessageTime(value) {
const date = new Date(Number(value));
if (Number.isNaN(date.getTime())) return '';
return `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;
}
function shouldShowTime(item, index) {
if (index === 0) return Boolean(item?.createTime);
return Number(item?.createTime) - Number(messages.value[index - 1]?.createTime) > 5 * 60 * 1000;
}
async function scrollToBottom() {
bottomId.value = '';
await nextTick();
bottomId.value = 'bottom';
}
async function detail() {
const res = await api('getAiConsultSessionDetail', { corpId: corpId.value, sessionId: sessionId.value, miniAppId: openid.value || uni.getStorageSync('openid') }, false);
if (!res?.success) return uni.showToast({ title: res?.message || '加载失败', icon: 'none' });
session.value = res.data.session;
messages.value = res.data.messages || [];
active.value = session.value.status === 'active';
uni.setNavigationBarTitle({ title: session.value.assistantName || 'AI咨询助理' });
await scrollToBottom();
}
async function send() {
if (!active.value || !content.value.trim()) return;
const value = content.value.trim();
const localMessage = { _id: `local-${Date.now()}`, sender: 'patient', content: value, messageType: 'text', createTime: Date.now(), _sendStatus: 'pending' };
messages.value.push(localMessage);
content.value = '';
pendingRequestCount.value += 1;
waitingAi.value = true;
await scrollToBottom();
try {
const res = await api('sendAiConsultMessage', { corpId: corpId.value, sessionId: sessionId.value, customerId: session.value.customerId, miniAppId: openid.value || uni.getStorageSync('openid'), content: value }, false);
if (!res?.success) {
localMessage._sendStatus = 'failed';
uni.showToast({ title: res?.message || '发送失败', icon: 'none' });
return;
}
await detail();
} catch (error) {
localMessage._sendStatus = 'failed';
uni.showToast({ title: '发送失败,请重试', icon: 'none' });
} finally {
pendingRequestCount.value -= 1;
waitingAi.value = pendingRequestCount.value > 0;
await scrollToBottom();
}
}
onLoad(opts => { corpId.value = opts.corpId; sessionId.value = opts.sessionId; detail(); });
onShow(detail);
</script>
<style scoped>
.chat-page { height: 100vh; display: flex; flex-direction: column; background: #f5f7fb; overflow: hidden; }
.chat-content { flex: 1; height: 0; box-sizing: border-box; }
.message-list { padding: 28rpx 24rpx 40rpx; }
.message-item { margin-bottom: 28rpx; }
.message-content { display: flex; align-items: flex-start; gap: 16rpx; }
.message-patient .message-content { justify-content: flex-end; }
.avatar { width: 72rpx; height: 72rpx; flex: 0 0 72rpx; display: flex; align-items: center; justify-content: center; border-radius: 50%; font-size: 24rpx; font-weight: 600; }
.assistant-avatar { color: #fff; background: linear-gradient(135deg, #4d93ff, #0877f1); }
.patient-avatar { color: #0877f1; background: #e2efff; }
.message-bubble-container { max-width: 74%; min-width: 0; }
.message-patient .message-bubble-container { display: flex; flex-direction: column; align-items: flex-end; }
.username-label { margin: 2rpx 0 10rpx; color: #8a919f; font-size: 24rpx; line-height: 32rpx; }
.message-bubble { padding: 18rpx 22rpx; border-radius: 8rpx 22rpx 22rpx; background: #fff; box-shadow: 0 4rpx 14rpx rgba(17, 48, 89, .06); }
.message-patient .message-bubble { color: #fff; border-radius: 22rpx 8rpx 22rpx 22rpx; background: #0877f1; box-shadow: 0 4rpx 14rpx rgba(8, 119, 241, .18); }
.message-bubble.pending { opacity: .72; }
.message-text { color: inherit; font-size: 30rpx; line-height: 46rpx; white-space: pre-wrap; word-break: break-word; }
.time-divider { margin: 2rpx 0 22rpx; color: #a0a6b0; text-align: center; font-size: 23rpx; }
.system-message { display: inline-block; margin: 0 auto; padding: 10rpx 20rpx; color: #79808c; background: #e9ecf1; border-radius: 22rpx; font-size: 24rpx; line-height: 36rpx; }
.message-system { text-align: center; }
.system-message.warn { color: #d84256; background: #fff0f1; }
.send-status { margin-top: 8rpx; color: #e34d59; font-size: 23rpx; }
.thinking { display: flex; align-items: center; gap: 8rpx; min-width: 84rpx; padding: 26rpx 28rpx; }
.thinking-dot { width: 10rpx; height: 10rpx; border-radius: 50%; background: #7f8998; animation: thinking 1.2s infinite ease-in-out; }
.thinking-dot:nth-child(2) { animation-delay: .16s; }
.thinking-dot:nth-child(3) { animation-delay: .32s; }
@keyframes thinking { 0%, 60%, 100% { opacity: .35; transform: translateY(0); } 30% { opacity: 1; transform: translateY(-7rpx); } }
.input-section { display: flex; align-items: flex-end; gap: 16rpx; padding: 16rpx 24rpx calc(16rpx + env(safe-area-inset-bottom)); background: #fff; border-top: 1rpx solid #edf0f4; }
.text-input { flex: 1; max-height: 180rpx; padding: 16rpx 20rpx; box-sizing: border-box; color: #283242; background: #f3f5f8; border-radius: 12rpx; font-size: 29rpx; line-height: 40rpx; }
.send-btn { flex: 0 0 auto; height: 72rpx; margin: 0; padding: 0 26rpx; color: #fff; background: #0877f1; border-radius: 12rpx; font-size: 27rpx; line-height: 72rpx; }
.send-btn[disabled] { color: #fff; background: #a9cfff; }
.rate-card { display: block; margin: 20rpx 0 0; }
.ended { padding: 30rpx; color: #7d8590; text-align: center; background: #fff; font-size: 27rpx; }
</style>

100
pages/ai-consult/entry.vue Normal file
View File

@ -0,0 +1,100 @@
<template>
<view class="page">
<template v-if="customers.length">
<view class="title">请选择本次咨询档案</view>
<view v-for="item in customers" :key="item._id" class="customer-card" @click="open(item)">
<view class="card-header">
<view class="customer-name">{{ item.name || '未命名' }}</view>
<view v-if="item.relationship" class="relationship">{{ item.relationship }}</view>
<view class="header-spacer" />
<view v-if="enableHis" :class="['his-status', item.isConnectHis ? 'connected' : 'unconnected']">
{{ item.isConnectHis ? '已关联院内档案' : '未关联院内档案' }}
</view>
</view>
<view class="card-body">
<view class="customer-info">
<text v-if="item.sex">{{ item.sex }}</text>
<text v-if="item.sex && item.age">/</text>
<text v-if="item.age">{{ item.age }}</text>
<text v-if="item.sex || item.age"></text>
<text v-if="item.mobile">{{ item.mobile }}</text>
</view>
<view class="customer-info">证件号{{ item.idCard || '--' }}</view>
</view>
<view class="card-footer">选择此档案咨询</view>
</view>
</template>
<view v-else-if="loaded" class="empty">正在前往建档...</view>
</view>
</template>
<script setup>
import { ref } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import api from '@/utils/api';
import useAccountStore from '@/store/account';
import { storeToRefs } from 'pinia';
const { account, openid } = storeToRefs(useAccountStore());
const corpId = ref('');
const teamId = ref('');
const assistantId = ref('');
const qrid = ref('');
const customers = ref([]);
const enableHis = ref(false);
const loaded = ref(false);
function getEntryUrl() {
return `/pages/ai-consult/entry?corpId=${corpId.value}&teamId=${teamId.value}&assistantId=${assistantId.value}&qrid=${qrid.value}`;
}
function toLogin() {
uni.redirectTo({ url: `/pages/login/login?redirectUrl=${encodeURIComponent(getEntryUrl())}` });
}
function toCreateArchive() {
uni.redirectTo({ url: `/pages/archive/edit-archive?corpId=${corpId.value}&teamId=${teamId.value}&redirectUrl=${encodeURIComponent(getEntryUrl())}` });
}
async function load() {
const miniAppId = openid.value || uni.getStorageSync('openid');
const res = await api('getMiniAppCustomers', { corpId: corpId.value, miniAppId }, false);
loaded.value = true;
if (!res?.success) return uni.showToast({ title: res?.message || '查询档案失败', icon: 'none' });
customers.value = res.data || [];
enableHis.value = res.enableHis === true;
if (!customers.value.length) toCreateArchive();
}
async function open(customer) {
const res = await api('openAiConsultSession', { corpId: corpId.value, teamId: teamId.value, assistantId: assistantId.value, qrid: qrid.value, customerId: customer?._id || '', miniAppId: openid.value || uni.getStorageSync('openid') });
if (!res?.success) return uni.showToast({ title: res?.message || '创建失败', icon: 'none' });
uni.redirectTo({ url: `/pages/ai-consult/chat?corpId=${corpId.value}&sessionId=${res.data.session._id}` });
}
onLoad((opts) => {
corpId.value = opts.corpId || '';
teamId.value = opts.teamId || '';
assistantId.value = opts.assistantId || '';
qrid.value = opts.qrid || '';
if (!account.value?.mobile) return toLogin();
load();
});
</script>
<style scoped>
.page { min-height: 100vh; background: #f7f8fa; padding: 28rpx; }
.title { font-size: 34rpx; font-weight: 600; margin-bottom: 20rpx; }
.customer-card { overflow: hidden; margin-bottom: 20rpx; background: #fff; border-radius: 16rpx; box-shadow: 0 8rpx 20rpx rgba(0, 0, 0, 0.1); }
.card-header { display: flex; align-items: center; padding: 24rpx 30rpx; border-bottom: 1rpx solid #eee; }
.customer-name { flex-shrink: 0; font-size: 32rpx; font-weight: 600; color: #333; }
.relationship { flex-shrink: 0; margin-left: 10rpx; padding: 2rpx 16rpx; border: 1rpx solid #1677ff; border-radius: 4rpx; color: #1677ff; font-size: 24rpx; }
.header-spacer { flex: 1; }
.his-status { padding: 8rpx 18rpx; border-radius: 6rpx; color: #fff; font-size: 24rpx; line-height: 1.3; }
.his-status.connected { background: #22a06b; }
.his-status.unconnected { background: #ff9d00; }
.card-body { padding: 20rpx 30rpx; border-bottom: 1rpx solid #eee; }
.customer-info { min-height: 38rpx; color: #333; font-size: 28rpx; line-height: 42rpx; }
.card-footer { padding: 20rpx 30rpx; color: #1677ff; font-size: 28rpx; text-align: right; }
.empty { padding-top: 100rpx; color: #888; font-size: 25rpx; text-align: center; }
</style>

46
pages/ai-consult/list.vue Normal file
View File

@ -0,0 +1,46 @@
<template>
<full-page @reachBottom="loadMore">
<template #header>
<scroll-view scroll-x class="tabs" :show-scrollbar="false">
<view v-for="item in sources" :key="item.value" class="tab" :class="{ active: source === item.value }" @click="selectSource(item.value)">{{ item.name }}</view>
</scroll-view>
<scroll-view scroll-x class="tabs customer-tabs" :show-scrollbar="false">
<view v-for="item in customers" :key="item.value" class="tab" :class="{ active: customerId === item.value }" @click="selectCustomer(item.value)">{{ item.name }}</view>
</scroll-view>
</template>
<view class="page">
<empty-data v-if="!loading && list.length === 0" text="暂无咨询记录" />
<view v-for="item in list" :key="`${item.source}-${item.id}`" class="card" @click="openChat(item)">
<view class="title"><text>{{ item.assistantName }}</text><text class="status" :class="item.status">{{ item.statusLabel }}</text></view>
<view class="line"><text class="source" :class="item.source">{{ item.sourceLabel }}</text></view>
<view class="line">团队{{ item.teamName || item.teamId || '-' }}</view>
<view class="line">档案{{ item.customerName || '-' }}</view>
<view class="line">最近互动{{ format(item.lastActiveTime) }}</view>
<view v-if="item.sensitiveHit" class="warn">已触发敏感词</view>
</view>
</view>
</full-page>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import api from '@/utils/api';
import useAccountStore from '@/store/account';
import { storeToRefs } from 'pinia';
import FullPage from '@/components/full-page.vue';
import EmptyData from '@/components/empty-data.vue';
const { openid } = storeToRefs(useAccountStore());
const corpId = ref(''); const customerId = ref(''); const source = ref('');
const sources = [{ name: '全部咨询', value: '' }, { name: 'AI咨询', value: 'ai' }, { name: 'IM咨询', value: 'im' }];
const customers = ref([{ name: '全部档案', value: '' }]);
const list = ref([]); const page = ref(1); const pages = ref(0); const loading = ref(false);
const format = value => value ? new Date(value).toLocaleString('zh-CN', { hour12: false }) : '-';
async function loadCustomers() { const res = await api('getMiniAppCustomers', { corpId: corpId.value, miniAppId: openid.value || uni.getStorageSync('openid') }, false); if (res?.success) customers.value = [customers.value[0], ...(res.data || []).map(item => ({ name: item.name || '未命名', value: item._id }))]; }
async function load(reset = false) { if (loading.value) return; if (reset) { page.value = 1; list.value = []; } loading.value = true; const res = await api('getUnifiedConsultSessions', { corpId: corpId.value, customerId: customerId.value, miniAppId: openid.value || uni.getStorageSync('openid'), source: source.value, page: page.value, pageSize: 20 }, false); if (res?.success) { list.value = page.value === 1 ? res.list : [...list.value, ...res.list]; pages.value = res.pages || 0; } loading.value = false; }
function selectCustomer(value) { if (customerId.value === value) return; customerId.value = value; load(true); }
function selectSource(value) { if (source.value === value) return; source.value = value; load(true); }
function loadMore() { if (!loading.value && page.value < pages.value) { page.value += 1; load(); } }
function openChat(item) { const url = item.source === 'im' ? `/pages/message/index?conversationID=GROUP${item.groupId}&groupID=${item.groupId}` : `/pages/ai-consult/chat?corpId=${item.corpId}&sessionId=${item.id}`; uni.navigateTo({ url }); }
onLoad(async opts => { corpId.value = opts.corpId || ''; await loadCustomers(); await load(true); });
</script>
<style scoped>.page{min-height:100vh;background:#f7f8fa;padding:24rpx}.tabs{white-space:nowrap;background:#fff;padding:18rpx 20rpx}.customer-tabs{border-top:1rpx solid #f2f3f5;padding-top:12rpx}.tab{display:inline-block;padding:10rpx 22rpx;margin-right:16rpx;border-radius:28rpx;color:#666;background:#f5f5f5}.tab.active{color:#0877f1;background:#e8f3ff}.card{background:#fff;border-radius:16rpx;padding:24rpx;margin-bottom:20rpx}.title{display:flex;justify-content:space-between;font-size:32rpx;font-weight:600;margin-bottom:16rpx}.status{font-size:24rpx;color:#ff8a00}.status.active,.status.processing{color:#18a058}.line{font-size:26rpx;color:#777;line-height:44rpx}.source{font-size:22rpx;border-radius:6rpx;padding:2rpx 10rpx;color:#0877f1;background:#e8f3ff}.source.im{color:#7c55d1;background:#f1ebff}.warn{font-size:24rpx;color:#e34d59;margin-top:8rpx}</style>

View File

@ -61,6 +61,7 @@ const referenceCustomer = ref(null)
const healthTypes = ref([]);
const enableHis = ref(false);
const source = ref('');
const redirectUrl = ref('');
const formData = computed(() => {
return { ...customer.value, ...form.value, mobile: account.value?.mobile }
@ -173,6 +174,10 @@ async function addArchive() {
uni.navigateBack();
return;
}
if (redirectUrl.value) {
uni.redirectTo({ url: redirectUrl.value });
return;
}
// getTeam(corpId.value, teamId.value, res.data.id);
if (healthTypes.value.length) {
const nextType = healthTypes.value[0];
@ -341,6 +346,7 @@ onLoad(options => {
corpUserId.value = options.corpUserId || '';
referenceCustomerId.value = options.referenceCustomerId || '';
source.value = options.source || '';
redirectUrl.value = options.redirectUrl || '';
if (referenceCustomerId.value) {
getReferenceCustomer();
}
@ -352,4 +358,4 @@ useLoad(options => {
})
</script>
<style scoped></style>
<style scoped></style>

View File

@ -216,23 +216,26 @@ defineExpose({
}
.consult-grid {
height: 208rpx;
min-height: 208rpx;
box-sizing: border-box;
background: #fff;
border-radius: 16rpx;
padding: 24rpx 30rpx;
padding: 26rpx 22rpx;
display: grid;
grid-template-columns: repeat(4, 1fr);
align-items: center;
gap: 32rpx;
grid-auto-rows: 120rpx;
align-items: start;
column-gap: 0;
row-gap: 24rpx;
box-shadow: 0 8rpx 10rpx 0 rgba(60, 169, 145, 0.06);
}
.consult-item {
min-width: 0;
display: flex;
flex-direction: column;
align-items: center;
gap: 12rpx;
gap: 10rpx;
cursor: pointer;
}
@ -242,7 +245,7 @@ defineExpose({
.item-icon {
width: 80rpx;
height: 80;
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
@ -270,9 +273,12 @@ defineExpose({
}
.item-label {
font-size: 30rpx;
color: #666d76;
max-width: 100%;
color: #596273;
font-size: 26rpx;
line-height: 36rpx;
text-align: center;
font-weight: 400;
white-space: nowrap;
}
</style>

View File

@ -132,10 +132,11 @@ async function bindTeam() {
return toast("关联团队失败");
}
await syncExternalUserRelation();
const res1 = await api('getWxAppCustomerCount', { miniAppId: account.value.openid, corpId: team.value.corpId, teamId: team.value.teamId });
if (redirectUrl.value) {
await attempToPage(redirectUrl.value)
} else if (res1 && res1.data > 0) {
return attempToPage(redirectUrl.value);
}
const res1 = await api('getWxAppCustomerCount', { miniAppId: account.value.openid, corpId: team.value.corpId, teamId: team.value.teamId });
if (res1 && res1.data > 0) {
toHome();
} else {
attempToPage(`/pages/archive/edit-archive?corpUserId=${team.value.corpUserId || ''}&teamId=${team.value.teamId}&corpId=${team.value.corpId}`)
@ -181,7 +182,7 @@ onLoad((opts) => {
if (opts.source === "teamInvite") {
team.value = get("invite-team-info");
if (!redirectUrl.value && team.value) {
redirectUrl.value = `/pages/archive/edit-archive?corpUserId=${team.value.corpUserId || ''}&teamId=${team.value.teamId}&corpId=${team.value.corpId}`;
redirectUrl.value = team.value.redirectUrl || `/pages/archive/edit-archive?corpUserId=${team.value.corpUserId || ''}&teamId=${team.value.teamId}&corpId=${team.value.corpId}`;
}
return;
}

View File

@ -75,8 +75,13 @@ async function syncExternalUserRelation({ corpId, externalUserId, corpUserId })
}
}
async function changeTeam({ teamId, corpId, corpUserId, externalUserId, qrid, referenceCustomerId }) {
function getAiConsultEntryUrl({ teamId, corpId, qrid }) {
return `/pages/ai-consult/entry?teamId=${teamId || ''}&corpId=${corpId || ''}&qrid=${qrid || ''}`;
}
async function changeTeam({ teamId, corpId, corpUserId, externalUserId, qrid, referenceCustomerId, type }) {
const normalizedCorpId = normalizeCorpId(corpId);
const redirectUrl = type === 'aiConsult' ? getAiConsultEntryUrl({ teamId, corpId: normalizedCorpId, qrid }) : '';
loading.value = true;
const res = await api("getTeamData", { teamId, corpId: normalizedCorpId, withCorpName: true });
loading.value = false;
@ -96,13 +101,14 @@ async function changeTeam({ teamId, corpId, corpUserId, externalUserId, qrid, re
});
await login('', { forceVerify: true })
if (account.value && account.value.mobile) {
bindTeam({ corpUserId, externalUserId })
bindTeam({ corpUserId, externalUserId, redirectUrl })
} else {
set("invite-team-info", {
corpUserId,
externalUserId,
qrid,
referenceCustomerId,
redirectUrl,
corpId: team.value.corpId,
teamId: team.value.teamId,
corpName: team.value.corpName,
@ -121,7 +127,7 @@ async function changeTeam({ teamId, corpId, corpUserId, externalUserId, qrid, re
}
}
async function bindTeam({ corpUserId, externalUserId }) {
async function bindTeam({ corpUserId, externalUserId, redirectUrl = '' }) {
const res = await api('bindWxappWithTeam', { appid, corpId: team.value.corpId, teamId: team.value.teamId, openid: account.value.openid });
if (!res || !res.success) {
return toast("关联团队失败");
@ -131,6 +137,9 @@ async function bindTeam({ corpUserId, externalUserId }) {
externalUserId,
corpUserId,
});
if (redirectUrl) {
return uni.redirectTo({ url: redirectUrl });
}
const res1 = await api('getWxAppCustomerCount', { miniAppId: account.value.openid, corpId: team.value.corpId, teamId: team.value.teamId });
if (res1 && res1.data > 0) {
uni.switchTab({

View File

@ -85,7 +85,7 @@
<script setup>
import { ref, computed } from 'vue';
import { getRate, submitRate } from '@/api/corp/rate';
import api from '@/utils/api';
import { toast } from '@/utils/widget';
// Props
@ -98,6 +98,10 @@ const props = defineProps({
type: Object,
default: () => ({})
},
corpId: {
type: String,
default: ''
},
});
// Emits
@ -121,12 +125,15 @@ const ratingText = computed(() => {
const openEvaluationPopup = async () => {
evaluationRating.value = 0
evaluationComment.value = ''
const res = await getRate(props.extension.rateId);
const res = await api('getRateRecord', {
id: props.extension.rateId,
corpId: props.extension.corpId || props.corpId
});
if (res && res.success) {
record.value = res.data;
evaluationRating.value = typeof res.data.rate === 'number' ? res.data.rate : 0;
evaluationComment.value = typeof res.data.words === 'string' ? res.data.words : '';
if (record.value.status === 'init') {
record.value = res.record || {};
evaluationRating.value = typeof record.value.rate === 'number' ? record.value.rate : 0;
evaluationComment.value = typeof record.value.words === 'string' ? record.value.words : '';
if (!res.rated && res.enable) {
evaluationPopup.value.open();
emit('popupStatusChange', true); //
} else {
@ -158,8 +165,9 @@ const submitEvaluation = async () => {
}
if (loading.value) return;
loading.value = true;
const res = await submitRate({
const res = await api('submitRateRecord', {
id: props.extension.rateId,
corpId: props.extension.corpId || props.corpId,
rate: evaluationRating.value,
words: evaluationComment.value
});
@ -179,4 +187,4 @@ const submitEvaluation = async () => {
<style scoped lang="scss">
@import "../../chat.scss";
</style>
</style>

View File

@ -3,6 +3,7 @@
v-if="payload.description === 'PATIENT_RATE_MESSAGE'"
:doctorInfo="doctorInfo"
:extension="extension"
:corpId="corpId"
@popupStatusChange="handlePopupStatusChange"
/>
</template>
@ -17,6 +18,10 @@ const props = defineProps({
doctorInfo:{
type: Object,
default: () => ({})
},
corpId: {
type: String,
default: ''
}
});
@ -37,4 +42,4 @@ const extension = computed(() => {
const handlePopupStatusChange = (isOpen) => {
emit('popupStatusChange', isOpen);
}
</script>
</script>

View File

@ -8,10 +8,11 @@
<text class="loading-text">加载中...</text>
</view>
<!-- 消息列表项 -->
<view v-for="conversation in conversationList" :key="conversation.groupID || conversation.conversationID"
<view v-for="conversation in conversationList" :key="conversation.source === 'ai' ? `ai-${conversation.sessionId}` : (conversation.groupID || conversation.conversationID)"
class="message-item" @click="handleClickConversation(conversation)">
<view class="avatar-container">
<GroupAvatar :avatarList="getAvatarList(conversation.groupID)" :size="96" classType="square" />
<image v-if="conversation.source === 'ai'" class="avatar" src="/static/home/ai-consult-assistant.png" mode="aspectFill" />
<GroupAvatar v-else :avatarList="getAvatarList(conversation.groupID)" :size="96" classType="square" />
<view v-if="conversation.unreadCount > 0" class="unread-badge">
<text class="unread-text">{{
conversation.unreadCount > 99 ? "99+" : conversation.unreadCount
@ -21,13 +22,14 @@
<view class="content">
<view class="header">
<text class="name">{{ conversation.teamName }}</text>
<text class="name">{{ conversation.displayName || conversation.teamName }}</text>
<text class="time">{{
formatMessageTime(conversation.lastMessageTime)
}}</text>
</view>
<view class="patient-info">
<text v-if="conversation.source === 'ai'" class="source-tag">AI咨询</text>
咨询人 | {{ conversation.patientName }}
</view>
<view class="message-preview">
@ -72,6 +74,7 @@ import { mergeConversationWithGroupDetails } from "@/utils/conversation-merger.j
import { globalUnreadListenerManager } from "@/utils/global-unread-listener.js";
import { get } from "@/utils/cache";
import { normalizeCorpId } from "@/utils/api-base-config";
import api from "@/utils/api";
import useGroupAvatars from "./hooks/use-group-avatars.js";
import GroupAvatar from "@/components/group-avatar.vue";
import {
@ -229,9 +232,10 @@ const loadConversationList = async () => {
const result = await globalTimChatManager.getGroupList();
if (result && result.success && result.groupList) {
//
conversationList.value = await mergeConversationWithGroupDetails(
const imConversations = await mergeConversationWithGroupDetails(
result.groupList
);
conversationList.value = mergeConsultationList(imConversations);
console.log(
"群聊列表加载成功,共",
conversationList.value.length,
@ -250,6 +254,46 @@ const loadConversationList = async () => {
}
};
function mergeConsultationList(imConversations) {
const aiConversations = conversationList.value.filter((item) => item.source === "ai");
return [...imConversations, ...aiConversations].sort(
(left, right) => (right.lastMessageTime || 0) - (left.lastMessageTime || 0)
);
}
const loadAiConsultationList = async () => {
const corpId = getCurrentTeamCorpId();
const miniAppId = openid.value || account.value?.openid || "";
if (!corpId || !miniAppId) return;
try {
const result = await api("getUnifiedConsultSessions", {
corpId,
miniAppId,
source: "ai",
page: 1,
pageSize: 100,
}, false);
if (!result?.success) return;
const imConversations = conversationList.value.filter((item) => item.source !== "ai");
const aiConversations = (result.list || []).map((item) => ({
source: "ai",
sessionId: item.id,
corpId: item.corpId,
displayName: item.assistantName || "AI咨询助理",
teamName: item.teamName || "",
patientName: item.customerName || "未命名患者",
lastMessage: item.statusLabel || "AI咨询",
lastMessageTime: item.lastActiveTime || 0,
unreadCount: 0,
}));
conversationList.value = [...imConversations, ...aiConversations].sort(
(left, right) => (right.lastMessageTime || 0) - (left.lastMessageTime || 0)
);
} catch (error) {
console.log("加载AI咨询记录异常:", error.message);
}
};
//
let updateTimer = null;
@ -438,6 +482,13 @@ const formatMessageTime = (timestamp) => {
const handleClickConversation = async (conversation) => {
console.log("点击会话:", conversation);
if (conversation.source === "ai") {
uni.navigateTo({
url: `/pages/ai-consult/chat?corpId=${conversation.corpId}&sessionId=${conversation.sessionId}`,
});
return;
}
//
const conversationIndex = conversationList.value.findIndex(
(conv) => conv.conversationID === conversation.conversationID
@ -480,11 +531,11 @@ const handleLoadMore = () => {
//
const handleRefresh = async () => {
if (!hasImCorpId.value) return;
refreshing.value = true;
try {
await loadConversationList();
if (hasImCorpId.value) await loadConversationList();
await loadAiConsultationList();
} finally {
refreshing.value = false;
}
@ -499,7 +550,8 @@ async function init() {
console.log("IM初始化失败继续加载列表");
}
//
await loadConversationList();
if (imReady) await loadConversationList();
await loadAiConsultationList();
//
setupConversationListener();
} catch (error) {
@ -580,9 +632,7 @@ onHide(() => {
});
watch(hasImCorpId, (n, o) => {
if (n && !o) {
init();
}
if (n || !o) init();
}, { immediate: true })
//
onUnmounted(() => {
@ -783,6 +833,16 @@ onUnmounted(() => {
padding-bottom: 10rpx;
}
.source-tag {
display: inline-block;
margin-right: 10rpx;
padding: 1rpx 8rpx;
border-radius: 6rpx;
color: #0877f1;
background: #e8f3ff;
font-size: 22rpx;
}
.subscribe-entry {
position: fixed;
right: 32rpx;

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

@ -22,6 +22,14 @@ const urlsConfig = {
getJoinedTeams: 'getJoinedTeams',
getCorpBusinessCardStatus: 'getCorpBusinessCardStatus',
getCorpInfo: 'getCorpInfo',
getAiConsultPermission: 'getAiConsultPermission',
getAiConsultAssistants: 'getAiConsultAssistants',
getAiConsultSessions: 'getAiConsultSessions',
getUnifiedConsultSessions: 'getUnifiedConsultSessions',
getAiConsultSessionDetail: 'getAiConsultSessionDetail',
openAiConsultSession: 'openAiConsultSession',
sendAiConsultMessage: 'sendAiConsultMessage',
closeAiConsultSession: 'closeAiConsultSession',
},
knowledgeBase: {