Compare commits
8 Commits
94a515924b
...
1ae7b23641
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ae7b23641 | ||
|
|
fdf9428f36 | ||
|
|
3770881c92 | ||
|
|
bbf9e81f1f | ||
|
|
68c6db7422 | ||
|
|
dace2bb2da | ||
|
|
1654b4267e | ||
|
|
ea9535be7b |
27
pages.json
27
pages.json
@ -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
158
pages/ai-consult/chat.vue
Normal 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
100
pages/ai-consult/entry.vue
Normal 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
46
pages/ai-consult/list.vue
Normal 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>
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
|
||||
BIN
static/home/ai-consult-assistant.png
Normal file
BIN
static/home/ai-consult-assistant.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.1 KiB |
@ -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: {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user