feat(mobile): show new ask sources and direct citation navigation

- citation chips: 正式经验 (CASE_EXP) / 同事经验·有奖征答 (COMMUNITY)
- citation navigation: CASE_EXP opens case detail via ?caseId=,
  COMMUNITY goes straight to question detail via community-question
  detailRef, falling back to the question list
- disable human-help action when the answer was already marked helpful
  (server would 409)
This commit is contained in:
2026-08-05 02:46:07 +08:00
parent 8f9c244aeb
commit 8f275b5a7a
2 changed files with 67 additions and 6 deletions
+13 -2
View File
@@ -140,7 +140,7 @@
<script setup lang="ts">
import { computed, ref } from 'vue';
import { onShow, onUnload } from '@dcloudio/uni-app';
import { onLoad, onShow, onUnload } from '@dcloudio/uni-app';
import type { CaseCurateResponse, CaseDetailResponse, CaseOrganizeResponse, CaseRecord, CaseSelectedFile, CaseUploadResponse } from '@/types/api';
import { chooseCaseAudio, curateCase, getCaseCapabilities, getCaseDetail, getCaseMediaUrl, getCaseRecords, organizeCase, uploadCaseAudio } from '@/services/case';
import { releaseAuthenticatedFileUrl } from '@/services/api';
@@ -164,6 +164,8 @@ const status = ref<'idle' | 'uploading' | 'organizing' | 'curating'>('idle');
const recordsLoading = ref(false);
const caseDetailLoading = ref(false);
const message = ref('');
/** 引用跳转带入的 caseId:加载完列表后自动打开对应详情,消费一次即清空。 */
const pendingCaseId = ref('');
const busy = computed(() => status.value !== 'idle');
const statusText = computed(() => {
if (status.value === 'uploading') return '转写中';
@@ -229,6 +231,11 @@ const loadRecords = async () => {
recordsLoading.value = true;
try {
records.value = await getCaseRecords();
if (pendingCaseId.value) {
const caseId = pendingCaseId.value;
pendingCaseId.value = '';
void openCaseDetail({ caseId });
}
} catch (error) {
handleRequestError(error, '最近案例加载失败', true);
} finally {
@@ -258,7 +265,7 @@ const refreshRecords = () => {
void loadRecords();
};
const openCaseDetail = async (record: CaseRecord) => {
const openCaseDetail = async (record: Pick<CaseRecord, 'caseId'>) => {
if (!record.caseId || caseDetailLoading.value) return;
caseDetailLoading.value = true;
caseMediaLoading.value = true;
@@ -332,6 +339,10 @@ const handleShow = async () => {
}
};
onLoad((options) => {
pendingCaseId.value = typeof options?.caseId === 'string' && options.caseId ? options.caseId : '';
});
onShow(() => {
void handleShow();
});
+54 -4
View File
@@ -263,12 +263,12 @@
<view v-if="msg.citationsOpen && msg.result.snippets?.length" class="bubble-citations">
<component
:is="snippet.detailRef && snippet.sourceType === 'DOCUMENT' ? 'button' : 'view'"
:is="citationClickable(snippet) ? 'button' : 'view'"
v-for="(snippet, index) in msg.result.snippets"
:key="`${snippet.docTitle}-${index}`"
class="citation"
:class="{ 'citation-action': Boolean(snippet.detailRef && snippet.sourceType === 'DOCUMENT') }"
@click="openCitationDetail(snippet)"
:class="{ 'citation-action': citationClickable(snippet) }"
@click="openCitation(snippet)"
>
<text class="citation-domain" :class="{ project: snippet.domain === 'PROJECT_SERVICE' }">
{{ citationDomainLabel(snippet) }}
@@ -283,6 +283,9 @@
<text class="citation-location">{{ citationLocationLabel(snippet) }}</text>
<text class="citation-open">查看出处 ›</text>
</view>
<view v-else-if="citationClickable(snippet)" class="citation-open-row">
<text class="citation-open">查看出处 ›</text>
</view>
</component>
</view>
@@ -1355,6 +1358,8 @@ const citationDomainLabel = (snippet: KnowledgeSearchResponse['snippets'][number
if (snippet.domain === 'PROJECT_SERVICE') return '项目记录';
if (snippet.domain === 'PERSONAL') return '我的资料';
if (snippet.sourceType === 'DATA_TOOL') return '数据工具';
if (snippet.sourceType === 'CASE_EXP') return '正式经验';
if (snippet.sourceType === 'COMMUNITY') return '同事经验·有奖征答';
return ({
FORMAL_POLICY: '正式制度',
OPERATING_MANUAL: '操作手册',
@@ -1395,6 +1400,47 @@ const openCitationDetail = (snippet: KnowledgeSearchResponse['snippets'][number]
uni.navigateTo({ url: `/pages/user/sop/citation-detail?ref=${encodeURIComponent(snippet.detailRef)}` });
};
/** 从引用字段解析新来源业务 ID:docId 的 case:{caseId} / community-answer:{answerId},或 detailRef 的 community-question:{questionId};解析不到即空串。 */
const citationSourceId = (
snippet: KnowledgeSearchResponse['snippets'][number],
prefix: string,
field: 'docId' | 'detailRef' = 'docId'
) => {
const value = (field === 'detailRef' ? snippet.detailRef : snippet.docId) || '';
return value.startsWith(prefix) ? value.slice(prefix.length) : '';
};
const citationClickable = (snippet: KnowledgeSearchResponse['snippets'][number]) => {
if (snippet.detailRef && snippet.sourceType === 'DOCUMENT') return true;
if (snippet.sourceType === 'CASE_EXP') return Boolean(citationSourceId(snippet, 'case:'));
if (snippet.sourceType === 'COMMUNITY') return true;
return false;
};
/** 出处跳转:DOCUMENT 走 citation-detail;CASE_EXP 去案例页按 caseId 自动打开详情;COMMUNITY 按 detailRef 直达问题详情,无则回退列表。 */
const openCitation = (snippet: KnowledgeSearchResponse['snippets'][number]) => {
if (!citationClickable(snippet)) return;
if (snippet.detailRef && snippet.sourceType === 'DOCUMENT') {
openCitationDetail(snippet);
return;
}
if (snippet.sourceType === 'CASE_EXP') {
const caseId = citationSourceId(snippet, 'case:');
if (caseId) {
uni.navigateTo({ url: `/pages/user/cases/index?caseId=${encodeURIComponent(caseId)}` });
}
return;
}
if (snippet.sourceType === 'COMMUNITY') {
const questionId = citationSourceId(snippet, 'community-question:', 'detailRef');
if (questionId) {
uni.navigateTo({ url: `/pages/user/questions/detail?id=${encodeURIComponent(questionId)}` });
return;
}
}
uni.navigateTo({ url: '/pages/user/questions/index' });
};
const noEvidenceText = (result: KnowledgeSearchResponse) => {
if (result.agentIntent === 'WEB_RESEARCH') return '公开网络中没有找到可核验来源。';
if (result.agentIntent === 'MEDIA_UNDERSTANDING') return '已分析附件,但当前资料中没有补充依据。';
@@ -1437,7 +1483,9 @@ const nextActionLabel = (action: NextAction) => {
const nextActionDisabled = (action: NextAction, msg: MasterMsg) => {
if (action.type === 'HUMAN_HELP') {
return msg.helpSubmitting || msg.feedbackSubmitting || Boolean(msg.helpQuestionId) || !msg.result.requestId;
// 已点“有帮助”再转人工必被服务端 409,直接禁用避免死路。
return msg.helpSubmitting || msg.feedbackSubmitting || Boolean(msg.helpQuestionId) || !msg.result.requestId
|| msg.feedbackVerdict === 'up';
}
if (action.type === 'DIRECT_CHANNEL') return !action.channel;
return false;
@@ -1447,6 +1495,7 @@ const nextActionButtonLabel = (action: NextAction, msg: MasterMsg) => {
if (action.type === 'HUMAN_HELP') {
if (msg.helpSubmitting || msg.feedbackSubmitting) return '提交中';
if (msg.helpQuestionId) return '已提交审核';
if (msg.feedbackVerdict === 'up') return '已反馈有帮助,如需求助请重新提问';
}
return nextActionLabel(action);
};
@@ -1516,6 +1565,7 @@ const submitAutoDownFeedback = async (msg: MasterMsg) => {
/** nextActions 的 HUMAN_HELP 入口:未提交过 down 反馈时先自动补提交,再走既有确认弹窗 + 幂等提交流程。 */
const submitHumanHelpFromNextAction = async (msg: MasterMsg) => {
if (msg.helpSubmitting || msg.helpQuestionId || !msg.result.requestId) return;
if (msg.feedbackVerdict === 'up') return;
if (!msg.feedbackVerdict && !await submitAutoDownFeedback(msg)) return;
await submitHumanHelp(msg);
};