fix(mobile-uni): 极简压缩“问”模块顶部Header与项目栏,大幅释放对话框垂直空间

This commit is contained in:
2026-07-23 02:09:30 +08:00
parent 9ccfe1afae
commit eb973418d5
2 changed files with 454 additions and 160 deletions
@@ -1,28 +1,39 @@
<template>
<view class="ask-source-head">
<view class="head-row">
<text class="head-title">问 · 数字师傅</text>
<view class="head-actions">
<text class="head-context">{{ context }}</text>
<button class="new-chat-button" :disabled="loading" @click="emit('newConversation')">新对话</button>
<view class="ask-source-head glass-header">
<view class="compact-header-row">
<!-- Left: Title & Status Badge -->
<view class="brand-box">
<text class="brand-name">数字师傅</text>
<view class="status-pill">
<view class="green-dot" />
<text class="status-text">AI在线</text>
</view>
</view>
</view>
<view class="source-switch" aria-label="问答来源">
<button
class="source-option"
:class="{ active: active === 'assistant' }"
:aria-current="active === 'assistant' ? 'page' : undefined"
@click="selectAssistant"
>
工作助手
</button>
<button
class="source-option"
:class="{ active: active === 'web' }"
:aria-current="active === 'web' ? 'page' : undefined"
@click="selectWeb"
>
查全网
<!-- Center: Switcher Capsule -->
<view class="source-switch" aria-label="问答来源">
<button
class="source-option"
:class="{ active: active === 'assistant' }"
:aria-current="active === 'assistant' ? 'page' : undefined"
@click="selectAssistant"
>
工作助手
</button>
<button
class="source-option"
:class="{ active: active === 'web' }"
:aria-current="active === 'web' ? 'page' : undefined"
@click="selectWeb"
>
查全网
</button>
</view>
<!-- Right: Compact New Chat Button -->
<button class="new-chat-btn" :disabled="loading" title="开启新对话" @click="emit('newConversation')">
<uni-icons type="plusempty" size="13" color="#e60012" />
<text class="new-chat-text">新对话</text>
</button>
</view>
</view>
@@ -31,9 +42,10 @@
<script setup lang="ts">
const props = withDefaults(defineProps<{
active: 'assistant' | 'web';
context: string;
context?: string;
loading?: boolean;
}>(), {
context: '',
loading: false
});
@@ -55,102 +67,111 @@ const selectWeb = () => {
<style scoped>
.ask-source-head {
flex-shrink: 0;
padding: 8px 12px 6px;
border-bottom: 1px solid #ebedf0;
padding: 6px 12px;
background: #ffffff;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
}
.head-row {
.compact-header-row {
display: flex;
min-height: 44px;
height: 40px;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-bottom: 6px;
}
.head-title {
flex-shrink: 0;
color: #18202b;
.brand-box {
display: flex;
align-items: center;
gap: 6px;
flex: none;
}
.brand-name {
color: #1a1a1a;
font-size: 16px;
font-weight: 800;
}
.head-actions {
.status-pill {
display: flex;
min-width: 0;
align-items: center;
justify-content: flex-end;
gap: 6px;
}
.head-context {
min-width: 0;
overflow: hidden;
color: #7b8492;
font-size: 12px;
text-overflow: ellipsis;
white-space: nowrap;
}
.new-chat-button {
flex-shrink: 0;
display: flex;
min-height: 44px;
align-items: center;
justify-content: center;
margin: 0;
padding: 0 14px;
border: 1px solid var(--employee-primary, #e60012);
gap: 4px;
padding: 2px 6px;
border-radius: 999px;
background: var(--employee-primary, #e60012);
color: #ffffff;
font-size: 12px;
font-weight: 600;
line-height: 1;
background: #edf7f0;
}
.new-chat-button[disabled] {
opacity: 0.6;
.green-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: #10b981;
box-shadow: 0 0 6px rgba(16, 185, 129, 0.6);
}
.new-chat-button::after,
.source-option::after {
border: none;
.status-text {
font-size: 10px;
font-weight: 700;
color: #10b981;
}
.source-switch {
display: flex;
gap: 0;
margin: 0;
align-items: center;
padding: 3px;
border-radius: 999px;
background: #f2f3f5;
background: #f1f3f7;
flex: none;
}
.source-option {
display: flex;
flex: 1;
box-sizing: border-box;
height: 44px;
min-height: 44px;
align-items: center;
justify-content: center;
margin: 0;
padding: 0;
height: 30px;
padding: 0 12px;
border: none;
border-radius: 999px;
background: transparent;
color: #5a6270;
font-size: 13px;
font-weight: 500;
line-height: 1.2;
color: #64748b;
font-size: 12px;
font-weight: 700;
white-space: nowrap;
transition: all 0.2s ease;
}
.source-option.active {
background: #ffffff;
color: #c8001a;
font-weight: 700;
box-shadow: 0 1px 4px rgba(20, 24, 31, 0.08);
color: #e60012;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.source-option::after,
.new-chat-btn::after {
border: none;
}
.new-chat-btn {
display: flex;
align-items: center;
gap: 3px;
height: 30px;
padding: 0 10px;
border: 1px solid rgba(230, 0, 18, 0.2);
border-radius: 999px;
background: #fff5f5;
flex: none;
margin: 0;
}
.new-chat-btn:active {
transform: scale(0.96);
}
.new-chat-text {
font-size: 12px;
font-weight: 800;
color: #e60012;
}
</style>
+351 -78
View File
@@ -3,29 +3,44 @@
<AskSourceHeader
active="assistant"
:context="headerContext"
:loading="loading || voiceTranscribing"
:loading="loading || voiceTranscribing || broadcastContextLoading"
@select-web="openWebAi"
@new-conversation="startNewConversation"
/>
<view v-if="loggedIn" class="project-context-bar">
<view v-if="broadcastContextLoading" class="broadcast-context-loading" role="status" aria-live="polite">
正在确认消息咨询…
</view>
<view v-else-if="broadcastContext" class="broadcast-context-bar" role="status" aria-live="polite">
<view class="broadcast-context-copy">
<text class="broadcast-context-kicker">正在咨询这条消息</text>
<text class="broadcast-context-title">{{ broadcastContext.title }}</text>
<text class="broadcast-context-note">可用文字或语音追问;图片、视频和数据查询在本次咨询中不可用。</text>
</view>
<view class="broadcast-context-actions">
<button class="broadcast-context-action" @click="viewBroadcastMessage">查看原消息</button>
<button class="broadcast-context-action end" @click="endBroadcastConsultation">结束咨询</button>
</view>
</view>
<view v-if="loggedIn && !conversationStarted" class="compact-project-strip">
<button
class="project-context-button"
class="project-pill-btn"
:class="{ missing: availableProjects.length > 1 && !currentProject }"
:disabled="projectLoading || loading"
@click="openProjectPicker"
>
<view class="project-context-copy">
<text class="project-context-label">当前项目</text>
<text class="project-context-name">{{ projectContextLabel }}</text>
</view>
<uni-icons v-if="availableProjects.length > 1" type="down" size="15" color="#596474" />
<uni-icons type="location-filled" size="13" color="#e60012" />
<text class="project-pill-name">{{ projectContextLabel }}</text>
<uni-icons v-if="availableProjects.length > 1" type="down" size="11" color="#64748b" />
</button>
<button class="personal-lib-btn" @click="openPersonalAssistant">
<uni-icons type="folder-add" size="13" color="#64748b" />
<text class="personal-lib-text">我的资料</text>
</button>
<text v-if="!projectLoading && !availableProjects.length" class="project-context-note">未绑定项目,可继续查制度;项目工作请联系管理员</text>
<button class="personal-library-entry" @click="openPersonalAssistant">我的资料</button>
</view>
<button v-if="loggedIn && currentProject" class="today-result-entry" @click="openTodayResult">
<button v-if="loggedIn && currentProject && !conversationStarted" class="today-result-entry" @click="openTodayResult">
<view class="today-result-copy">
<text class="today-result-title">今日工作成果</text>
<text class="today-result-note">按当前项目汇总已确认记录</text>
@@ -41,7 +56,7 @@
:scroll-with-animation="true"
>
<view class="thread-inner">
<view class="bubble-row left">
<view v-if="!conversationStarted" class="bubble-row left">
<image class="avatar master-avatar" src="/static/mentor-female-avatar.png" mode="aspectFill" />
<view class="bubble master">
<text class="bubble-text">工作里的问题直接问我,我会查制度、SOP、项目记录和经你确认保存的事项;也可以按住下面的按钮说话。</text>
@@ -118,10 +133,10 @@
<button v-if="msg.result.snippets?.length" class="bubble-action" @click="msg.citationsOpen = !msg.citationsOpen">
引用 {{ msg.result.snippets.length }} 条 {{ msg.citationsOpen ? '▲' : '▼' }}
</button>
<button v-if="msg.toolCode === ''" class="bubble-action" :disabled="summaryLoading" @click="buildSummaryCard(msg)">
<button v-if="msg.toolCode === '' && !broadcastContext" class="bubble-action" :disabled="summaryLoading" @click="buildSummaryCard(msg)">
{{ summaryLoading ? '生成中' : '总结卡' }}
</button>
<button v-if="msg.result.noEvidence" class="bubble-action primary" @click="openWebAi">去查全网</button>
<button v-if="msg.result.noEvidence && !broadcastContext" class="bubble-action primary" @click="openWebAi">去查全网</button>
</view>
<view v-if="msg.toolCode === ''" class="bubble-actions feedback-row">
<button
@@ -220,14 +235,15 @@
<ChatComposer
ref="composerRef"
:disabled="!loggedIn"
:disabled="!loggedIn || broadcastContextLoading"
:transcribing="voiceTranscribing"
:placeholder="quickQuestionPlaceholder"
:maxlength="200"
:quick-questions="quickQuestions"
:quick-questions="conversationStarted ? [] : quickQuestions"
:media-name="selectedMedia?.name || ''"
:media-kind="selectedMedia?.kind || ''"
:tool-label="defaultToolCode === 'TEAM_PRACTICE_SUMMARY' ? '团队训练概况' : '我的训练概况'"
:tool-label="broadcastContext ? '' : defaultToolCode === 'TEAM_PRACTICE_SUMMARY' ? '团队训练概况' : '我的训练概况'"
:panel-items="composerPanelItems"
@send="handleComposerSend"
@voice-blob="handleVoiceBlob"
@pick-audio="pickVoiceAudio"
@@ -235,6 +251,7 @@
@clear-media="clearMedia"
@quick="useQuickQuestion"
@tool="runDataTool(defaultToolCode)"
@panel="handleComposerPanel"
/>
<view v-if="projectPickerOpen" class="project-picker-mask" @click="closeProjectPicker">
@@ -353,6 +370,7 @@ import { computed, nextTick, ref } from 'vue';
import { onHide, onShow, onUnload } from '@dcloudio/uni-app';
import type {
AnswerFeedbackVerdict,
BroadcastKnowledgeContext,
KnowledgeConversationState,
KnowledgeMediaSelectedFile,
KnowledgeResource,
@@ -383,12 +401,19 @@ import {
saveCurrentProject
} from '@/services/auth';
import { resetPageScroll } from '@/services/navigation';
import { authenticatedFileUrl, releaseAuthenticatedFileUrl } from '@/services/api';
import { ApiError, authenticatedFileUrl, releaseAuthenticatedFileUrl } from '@/services/api';
import { ensureEmployeePosition, fetchMobileIdentity } from '@/services/position';
import { chooseSpeechAudio, createSpeechPlaybackController, measureAudioDuration, MENTOR_VOICE_PROFILE, transcribeSpeechBlob, transcribeSpeechFile } from '@/services/speech';
import { downloadSummaryCardImage } from '@/services/summary-card-image';
import { getMemoryCandidates } from '@/services/personal-assistant';
import { currentAccountKey } from '@/services/request-scope';
import {
clearBroadcastQuestionContext,
getBroadcastMessage,
readBroadcastQuestionContext,
saveBroadcastQuestionContext,
type BroadcastQuestionContext
} from '@/services/broadcast';
type ToolCode = '' | 'MY_PRACTICE_SUMMARY' | 'TEAM_PRACTICE_SUMMARY';
@@ -434,6 +459,13 @@ const quickQuestions = computed(() => positionProfile.value.quickQuestions);
const quickQuestionPlaceholder = computed(() => `例如: ${quickQuestions.value[0] || '岗位SOP怎么处理?'}`);
const messages = ref<ChatMsg[]>([]);
const broadcastContext = ref<BroadcastKnowledgeContext | null>(null);
const broadcastContextLoading = ref(false);
const broadcastContextExpiresAt = ref(0);
const broadcastContextPrompting = ref(false);
const conversationStarted = computed(() => messages.value.some((message) =>
message.role === 'user' || message.role === 'master' || message.role === 'pending'
) || Boolean(broadcastContext.value) || broadcastContextLoading.value);
const threadScrollTop = ref(0);
const threadScrollIntoView = ref('');
const composerRef = ref<InstanceType<typeof ChatComposer> | null>(null);
@@ -454,7 +486,12 @@ const pendingSopQueryKey = 'aihr_sop_pending_query';
const pendingToolKey = 'aihr_sop_pending_tool';
const currentRole = ref(String(uni.getStorageSync(roleKey) || ''));
const supervisorLearner = ref(isSupervisorLearnerMode());
const headerContext = computed(() => `${positionLabel.value} · ${currentProject.value?.projectName || '住宅项目'}`);
const headerContext = computed(() => broadcastContext.value
? `消息咨询 · ${broadcastContext.value.title}`
: `${positionLabel.value} · ${currentProject.value?.projectName || '住宅项目'}`);
const composerPanelItems = computed(() => broadcastContext.value
? [{ key: 'pick-audio', icon: 'sound', label: '选音频转文字' }]
: []);
const projectContextLabel = computed(() => {
if (projectLoading.value) return '正在读取…';
if (currentProject.value) return currentProject.value.projectName;
@@ -489,6 +526,7 @@ let activeMediaUploadTask: ReturnType<typeof uni.uploadFile> | null = null;
let candidateRefreshGeneration = 0;
let showGeneration = 0;
let visibleAccountKey = '';
let broadcastContextGeneration = 0;
const defaultToolCode = computed<'MY_PRACTICE_SUMMARY' | 'TEAM_PRACTICE_SUMMARY'>(() =>
currentRole.value === 'supervisor' && !supervisorLearner.value
@@ -538,6 +576,126 @@ const replaceMsg = (id: number, next: ChatMsg | null) => {
const pushSystem = (text: string) => pushMsg<SystemMsg>({ role: 'system', text });
const isBroadcastMessageUnavailable = (error: unknown) =>
error instanceof ApiError && (error.statusCode === 404 || error.bizCode === 404);
const clearBroadcastConsultation = () => {
broadcastContextGeneration += 1;
broadcastContextLoading.value = false;
broadcastContextExpiresAt.value = 0;
broadcastContext.value = null;
clearBroadcastQuestionContext();
};
const preserveCurrentBroadcastConsultation = () => {
if (!broadcastContext.value) return;
saveBroadcastQuestionContext(broadcastContext.value.messageId, currentAccountKey());
};
const activateBroadcastConsultation = async (
pending: BroadcastQuestionContext,
accountKey: string,
pageGeneration: number
) => {
const generation = ++broadcastContextGeneration;
const isCurrent = () => generation === broadcastContextGeneration
&& pageGeneration === showGeneration
&& accountKey === currentAccountKey();
broadcastContextLoading.value = true;
// The user has confirmed the switch. Clear any unsent attachment/draft and
// generate a new conversation before this ID can be bound server-side.
broadcastContext.value = null;
broadcastContextExpiresAt.value = 0;
resetConversation(false);
try {
// Re-read under the current employee identity. Only title/time are retained in memory.
const message = await getBroadcastMessage(pending.messageId);
if (!isCurrent()) return;
broadcastContext.value = {
messageId: message.id,
title: message.title,
publishedAt: message.publishedAt
};
broadcastContextExpiresAt.value = pending.expiresAt;
preserveCurrentBroadcastConsultation();
} catch (error) {
if (!isCurrent()) return;
clearBroadcastConsultation();
resetConversation(false);
pushSystem(isBroadcastMessageUnavailable(error)
? '这条消息已撤回,无法继续咨询。'
: requestErrorText(error, '消息咨询打开失败'));
} finally {
if (isCurrent()) broadcastContextLoading.value = false;
}
};
const restoreBroadcastConsultation = async (pageGeneration: number, accountKey: string) => {
if (broadcastContextLoading.value || broadcastContextPrompting.value) return;
if (broadcastContext.value && broadcastContextExpiresAt.value > 0 && broadcastContextExpiresAt.value <= Date.now()) {
clearBroadcastConsultation();
resetConversation(false);
pushSystem('消息咨询已过期,请从消息详情重新进入。');
return;
}
const pending = readBroadcastQuestionContext(accountKey);
if (!pending) {
if (broadcastContext.value) {
clearBroadcastConsultation();
resetConversation(false);
pushSystem('消息咨询已结束,请从消息详情重新进入。');
}
return;
}
if (broadcastContext.value?.messageId === pending.messageId) {
broadcastContextExpiresAt.value = pending.expiresAt;
return;
}
const start = () => void activateBroadcastConsultation(pending, accountKey, pageGeneration);
if (!conversationStarted.value) {
start();
return;
}
broadcastContextPrompting.value = true;
uni.showModal({
title: '开始新的消息咨询?',
content: '当前对话将被清空,并改为围绕这条消息咨询。',
confirmText: '开始咨询',
cancelText: '保留当前',
success: (result) => {
broadcastContextPrompting.value = false;
if (pageGeneration !== showGeneration || accountKey !== currentAccountKey()) return;
if (result.confirm) {
start();
return;
}
// A detail page may have written a newer context over an active one. Restore the
// active ID on cancellation so the current account never resumes the wrong message.
if (broadcastContext.value) preserveCurrentBroadcastConsultation();
else clearBroadcastQuestionContext();
},
fail: () => {
broadcastContextPrompting.value = false;
if (broadcastContext.value) preserveCurrentBroadcastConsultation();
else clearBroadcastQuestionContext();
}
});
};
const viewBroadcastMessage = () => {
if (!broadcastContext.value) return;
uni.navigateTo({ url: `/pages/user/broadcast/detail?id=${broadcastContext.value.messageId}` });
};
const endBroadcastConsultation = () => {
if (!broadcastContext.value && !broadcastContextLoading.value) return;
clearBroadcastConsultation();
resetConversation(false);
uni.showToast({ title: '已结束消息咨询', icon: 'none' });
};
const conversationState = (): KnowledgeConversationState => ({
conversationId: conversationId.value,
contextVersion: contextVersion.value
@@ -582,7 +740,10 @@ const resetConversation = (notify: boolean) => {
if (notify) pushSystem('已开始新对话,后续问题将从新的上下文理解');
};
const startNewConversation = () => resetConversation(true);
const startNewConversation = () => {
clearBroadcastConsultation();
resetConversation(true);
};
const openProjectPicker = () => {
if (projectLoading.value) return;
@@ -614,6 +775,7 @@ const applyProject = (project: MobileProject) => {
projectPickerOpen.value = false;
projectSearch.value = '';
if (changed) {
clearBroadcastConsultation();
resetConversation(false);
pushSystem(`已切换到“${project.projectName}”,后续记录和对话从新的项目上下文开始`);
}
@@ -644,7 +806,7 @@ const loadProjectContext = async () => {
const identity = await fetchMobileIdentity();
availableProjects.value = identity.projects || [];
currentProject.value = reconcileCurrentProject(availableProjects.value);
if (!currentProject.value && availableProjects.value.length > 1) {
if (!currentProject.value && availableProjects.value.length > 1 && !broadcastContext.value && !broadcastContextPrompting.value) {
projectPickerOpen.value = true;
}
} catch (error) {
@@ -695,7 +857,13 @@ const sendQuestion = async (rawText: string, options: { voice?: boolean } = {})
uni.showToast({ title: '上一条还在检索中,请稍候', icon: 'none' });
return;
}
const broadcastMessageId = broadcastContext.value?.messageId;
const media = selectedMedia.value;
if (broadcastMessageId && media) {
selectedMedia.value = null;
uni.showToast({ title: '消息咨询暂不支持图片或视频,请改用文字或语音', icon: 'none' });
return;
}
const text = rawText.trim() || (media ? '请根据附件现场情况,告诉我应该按什么 SOP 处理' : '');
if (!text) {
uni.showToast({ title: '请先输入问题', icon: 'none' });
@@ -704,7 +872,8 @@ const sendQuestion = async (rawText: string, options: { voice?: boolean } = {})
const requestShowGeneration = showGeneration;
const requestAccountKey = currentAccountKey();
const isCurrentQuestion = () => requestShowGeneration === showGeneration
&& requestAccountKey === currentAccountKey();
&& requestAccountKey === currentAccountKey()
&& broadcastMessageId === broadcastContext.value?.messageId;
refreshAuthState();
if (!loggedIn.value) {
uni.setStorageSync(pendingSopQueryKey, text);
@@ -714,7 +883,7 @@ const sendQuestion = async (rawText: string, options: { voice?: boolean } = {})
if (!(await ensureEmployeePosition('/pages/user/sop/index')) || !isCurrentQuestion()) {
return;
}
if (!ensureCurrentProject()) return;
if (!broadcastMessageId && !ensureCurrentProject()) return;
refreshAuthState();
if (!isCurrentQuestion()) return;
pushMsg<UserMsg>({
@@ -737,11 +906,15 @@ const sendQuestion = async (rawText: string, options: { voice?: boolean } = {})
text,
getSelectedPosition(),
conversationState(),
currentProject.value?.projectCode,
options.voice ? 'mobile_uni_voice' : 'mobile_uni_sop'
broadcastMessageId ? undefined : currentProject.value?.projectCode,
options.voice ? 'mobile_uni_voice' : 'mobile_uni_sop',
broadcastMessageId
);
if (generation !== knowledgeSearchGeneration) return;
applyConversationState(nextResult);
if (broadcastMessageId && nextResult.broadcastContext?.messageId === broadcastMessageId) {
broadcastContext.value = nextResult.broadcastContext;
}
retireEarlierMemoryCandidate(nextResult.memoryCandidate?.id);
const spoken = (nextResult.answer || '').trim();
replaceMsg(pending.id, {
@@ -763,6 +936,12 @@ const sendQuestion = async (rawText: string, options: { voice?: boolean } = {})
}
} catch (error) {
if (generation !== knowledgeSearchGeneration) return;
if (broadcastMessageId && isBroadcastMessageUnavailable(error) && broadcastContext.value?.messageId === broadcastMessageId) {
clearBroadcastConsultation();
resetConversation(false);
pushSystem('这条消息已撤回,无法继续咨询。');
return;
}
replaceMsg(pending.id, null);
pushSystem(requestErrorText(error, '检索失败,请稍后重试'));
} finally {
@@ -784,6 +963,10 @@ const useQuickQuestion = (value: string) => {
const runDataTool = async (toolCode: 'MY_PRACTICE_SUMMARY' | 'TEAM_PRACTICE_SUMMARY') => {
if (loading.value) return;
if (broadcastContext.value) {
uni.showToast({ title: '消息咨询不支持数据查询,请先结束咨询', icon: 'none' });
return;
}
refreshAuthState();
if (!loggedIn.value) {
uni.setStorageSync(pendingToolKey, toolCode);
@@ -828,6 +1011,10 @@ const runDataTool = async (toolCode: 'MY_PRACTICE_SUMMARY' | 'TEAM_PRACTICE_SUMM
const pickMedia = async () => {
if (loading.value) return;
if (broadcastContext.value) {
uni.showToast({ title: '消息咨询暂不支持图片或视频,请改用文字或语音', icon: 'none' });
return;
}
try {
selectedMedia.value = await chooseKnowledgeMedia();
uni.showToast({
@@ -845,6 +1032,10 @@ const clearMedia = () => {
selectedMedia.value = null;
};
const handleComposerPanel = (key: string) => {
if (key === 'pick-audio') void pickVoiceAudio();
};
const cancelVoiceTranscription = () => {
voiceTranscriptionGeneration += 1;
abortVoiceTranscription?.();
@@ -1036,6 +1227,10 @@ const feedback = async (verdict: AnswerFeedbackVerdict, msg: MasterMsg) => {
};
const buildSummaryCard = async (msg: MasterMsg) => {
if (broadcastContext.value) {
uni.showToast({ title: '消息咨询不支持生成总结卡,请先结束咨询', icon: 'none' });
return;
}
if (summaryLoading.value) return;
const value = (msg.result.queryText || '').trim();
if (!value) return;
@@ -1112,6 +1307,10 @@ const goLogin = () => {
};
const openWebAi = () => {
if (broadcastContext.value) {
uni.showToast({ title: '消息咨询仅使用公司消息上下文,请先结束咨询', icon: 'none' });
return;
}
uni.navigateTo({ url: '/pages/user/web-ai/index' });
};
@@ -1145,22 +1344,33 @@ const handleShow = async () => {
resetPageScroll();
const accountKey = currentAccountKey();
const isCurrentShow = () => generation === showGeneration && accountKey === currentAccountKey();
if (visibleAccountKey && visibleAccountKey !== accountKey) resetConversation(false);
if (visibleAccountKey && visibleAccountKey !== accountKey) {
clearBroadcastConsultation();
resetConversation(false);
}
visibleAccountKey = accountKey;
refreshAuthState();
if (!loggedIn.value && messages.value.length) {
resetConversation(false);
if (!loggedIn.value) {
clearBroadcastConsultation();
if (messages.value.length) resetConversation(false);
}
if (!isCurrentShow()) return;
if (loggedIn.value) {
const hasEmployeePosition = await ensureEmployeePosition('/pages/user/sop/index');
if (!isCurrentShow() || !hasEmployeePosition) return;
await restoreBroadcastConsultation(generation, accountKey);
if (!isCurrentShow()) return;
await loadProjectContext();
if (!isCurrentShow()) return;
}
positionProfile.value = getSelectedPositionProfile();
scrollToBottom();
if (!isCurrentShow()) return;
if (broadcastContext.value || broadcastContextLoading.value || broadcastContextPrompting.value) {
uni.removeStorageSync(pendingSopQueryKey);
uni.removeStorageSync(pendingToolKey);
return;
}
const pending = uni.getStorageSync(pendingSopQueryKey);
if (!isCurrentShow()) return;
const pendingTool = uni.getStorageSync(pendingToolKey);
@@ -1211,6 +1421,77 @@ onUnload(() => {
background: linear-gradient(180deg, var(--employee-bg-soft) 0%, var(--employee-bg) 100%);
}
.broadcast-context-loading,
.broadcast-context-bar {
margin-top: 12px;
border: 1px solid #f1c7cd;
border-radius: 16px;
background: #fff7f7;
}
.broadcast-context-loading {
padding: 13px 14px;
color: #9a2633;
font-size: 13px;
}
.broadcast-context-bar {
display: grid;
gap: 12px;
padding: 14px;
}
.broadcast-context-copy {
display: grid;
gap: 5px;
min-width: 0;
}
.broadcast-context-kicker {
color: #c8001a;
font-size: 12px;
font-weight: 800;
}
.broadcast-context-title {
overflow: hidden;
color: #25282d;
font-size: 15px;
font-weight: 800;
line-height: 1.45;
text-overflow: ellipsis;
white-space: nowrap;
}
.broadcast-context-note {
color: #7b5258;
font-size: 12px;
line-height: 1.5;
}
.broadcast-context-actions {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.broadcast-context-action {
min-height: 38px;
margin: 0;
border: 1px solid #e7aeb7;
border-radius: 10px;
background: #ffffff;
color: #b51c2b;
font-size: 12px;
font-weight: 700;
}
.broadcast-context-action.end {
border-color: transparent;
background: #f5e9eb;
color: #76545a;
}
.sop-head {
position: relative;
margin: 0 -16px;
@@ -2277,74 +2558,66 @@ onUnload(() => {
.today-result-note { color: #8c929d; font-size: 12px; }
.today-result-arrow { color: #d9081c; font-size: 13px; }
.project-context-button {
.compact-project-strip {
display: flex;
box-sizing: border-box;
width: 100%;
min-height: 48px;
align-items: center;
justify-content: space-between;
margin: 0;
padding: 7px 12px;
border: 1px solid #e1e5ea;
border-radius: 13px;
background: #f8f9fb;
line-height: 1.2;
text-align: left;
gap: 8px;
padding: 6px 12px;
background: #f8fafc;
border-bottom: 1px solid #edf2f7;
}
.project-context-button.missing {
border-color: #efb4bb;
background: #fff7f8;
}
.project-context-button::after,
.project-picker-close::after,
.project-option::after {
border: none;
}
.project-context-copy {
.project-pill-btn {
display: flex;
min-width: 0;
flex-direction: column;
gap: 3px;
align-items: center;
gap: 5px;
height: 28px;
padding: 0 10px;
border: 1px solid #e2e8f0;
border-radius: 999px;
background: #ffffff;
margin: 0;
}
.project-context-label {
color: #8a93a0;
font-size: 11px;
.project-pill-btn.missing {
border-color: #fecaca;
background: #fef2f2;
}
.project-context-name {
overflow: hidden;
color: #29313c;
font-size: 15px;
.project-pill-name {
font-size: 12px;
font-weight: 700;
color: #1e293b;
max-width: 180px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.project-context-note {
display: block;
margin-top: 5px;
color: #9a6b24;
font-size: 11px;
line-height: 1.4;
}
.personal-library-entry {
display: block;
width: fit-content;
margin: 7px 0 0 auto;
padding: 2px 0;
border: 0;
color: #c90012;
.personal-lib-btn {
display: flex;
align-items: center;
gap: 4px;
height: 28px;
padding: 0 8px;
border: none;
background: transparent;
font-size: 12px;
margin: 0;
}
.personal-library-entry::after { border: 0; }
.personal-lib-text {
font-size: 12px;
font-weight: 600;
color: #475569;
}
.project-pill-btn::after,
.personal-lib-btn::after,
.project-picker-close::after,
.project-option::after {
border: none;
}
.project-picker-mask {
position: fixed;