feat(mobile-uni): 彻底完成登录页、首页、学习中心与问答页UI/UX极简重构及单条消息追问功能

This commit is contained in:
2026-07-23 02:16:57 +08:00
parent 63dea94fc1
commit b4ae0e6da0
12 changed files with 620 additions and 9 deletions
+122
View File
@@ -0,0 +1,122 @@
import { apiRequest } from './api';
export const broadcastQuestionContextStorageKey = 'aihr_broadcast_question_context';
export const broadcastQuestionContextTtlMs = 15 * 60 * 1000;
export interface BroadcastQuestionContext {
messageId: number;
accountKey: string;
expiresAt: number;
}
export interface BroadcastMessageSummary {
id: number;
title: string;
read: boolean;
publishedAt: string;
}
export interface BroadcastMessage extends BroadcastMessageSummary {
content: string;
}
export interface BroadcastMessagePage {
total: number;
pageNum: number;
pageSize: number;
rows: BroadcastMessageSummary[];
}
export const getBroadcastUnreadCount = async () => {
const result = await apiRequest<{ unreadCount: number }>({
url: '/api/aihr/broadcast/unread-count',
method: 'GET',
timeout: 15000
});
return Math.max(0, Number(result?.unreadCount) || 0);
};
export const listBroadcastMessages = (pageNum = 1, pageSize = 20) =>
apiRequest<BroadcastMessagePage>({
url: `/api/aihr/broadcast/messages?pageNum=${pageNum}&pageSize=${pageSize}`,
method: 'GET',
timeout: 15000
});
export const getBroadcastMessage = (id: number) =>
apiRequest<BroadcastMessage>({
url: `/api/aihr/broadcast/messages/${id}`,
method: 'GET',
timeout: 15000
});
export const markBroadcastMessageRead = (id: number) =>
apiRequest<void>({
url: `/api/aihr/broadcast/messages/${id}/read`,
method: 'POST',
timeout: 15000
});
const isSafeMessageId = (value: unknown): value is number =>
typeof value === 'number' && Number.isSafeInteger(value) && value > 0;
const parseQuestionContext = (value: unknown): BroadcastQuestionContext | null => {
if (!value || typeof value !== 'object') return null;
const candidate = value as Partial<BroadcastQuestionContext>;
return isSafeMessageId(candidate.messageId)
&& typeof candidate.accountKey === 'string'
&& candidate.accountKey.length > 0
&& typeof candidate.expiresAt === 'number'
&& Number.isSafeInteger(candidate.expiresAt)
&& candidate.expiresAt > 0
? {
messageId: candidate.messageId,
accountKey: candidate.accountKey,
expiresAt: candidate.expiresAt
}
: null;
};
export const clearBroadcastQuestionContext = () => {
try {
uni.removeStorageSync(broadcastQuestionContextStorageKey);
} catch {
// Storage is only a navigation handoff. A failed cleanup must not expose content.
}
};
/**
* Stores only a short-lived, account-bound message identifier. The title, body and
* tenant are deliberately never written to local storage or navigation URLs.
*/
export const saveBroadcastQuestionContext = (
messageId: number,
accountKey: string,
ttlMs = broadcastQuestionContextTtlMs
) => {
if (!isSafeMessageId(messageId) || !accountKey || !Number.isSafeInteger(ttlMs) || ttlMs <= 0) return false;
try {
uni.setStorageSync(broadcastQuestionContextStorageKey, {
messageId,
accountKey,
expiresAt: Date.now() + ttlMs
} satisfies BroadcastQuestionContext);
return true;
} catch {
return false;
}
};
/** Returns a valid account-bound context and clears stale, malformed or foreign data. */
export const readBroadcastQuestionContext = (accountKey: string, now = Date.now()): BroadcastQuestionContext | null => {
try {
const context = parseQuestionContext(uni.getStorageSync(broadcastQuestionContextStorageKey));
if (!context || context.accountKey !== accountKey || context.expiresAt <= now) {
clearBroadcastQuestionContext();
return null;
}
return context;
} catch {
return null;
}
};
+8 -3
View File
@@ -83,7 +83,8 @@ export const searchKnowledge = async (
position = getSelectedPosition(),
conversation?: KnowledgeConversationState,
projectCode?: string,
source: 'mobile_uni_sop' | 'mobile_uni_voice' = 'mobile_uni_sop'
source: 'mobile_uni_sop' | 'mobile_uni_voice' = 'mobile_uni_sop',
broadcastMessageId?: number
) => {
const response = await apiRequest<UnifiedKnowledgeResponse>({
url: '/api/knowledge/query',
@@ -96,7 +97,10 @@ export const searchKnowledge = async (
limit: 5,
conversationId: conversation?.conversationId,
contextVersion: conversation?.contextVersion,
projectCode
projectCode,
...(typeof broadcastMessageId === 'number' && Number.isSafeInteger(broadcastMessageId) && broadcastMessageId > 0
? { broadcastMessageId }
: {})
},
timeout: 30000
});
@@ -198,7 +202,8 @@ const normalizeUnifiedResponse = (response: UnifiedKnowledgeResponse): Knowledge
intent: response.intent,
rewrittenQuery: response.rewrittenQuery,
resources: response.resources || [],
memoryCandidate: response.memoryCandidate || legacy.memoryCandidate
memoryCandidate: response.memoryCandidate || legacy.memoryCandidate,
broadcastContext: response.broadcastContext || legacy.broadcastContext || null
};
};
+5 -3
View File
@@ -1,9 +1,11 @@
import type { ProjectWorkResult, WorkResult } from '@/types/api';
import { apiRequest } from './api';
const params = (projectCode: string, workDate?: string) => {
const params = (projectCode: string, workDate?: string, pageNum?: number, pageSize?: number) => {
const query = new URLSearchParams({ projectCode });
if (workDate) query.set('workDate', workDate);
if (pageNum) query.set('pageNum', String(pageNum));
if (pageSize) query.set('pageSize', String(pageSize));
return query.toString();
};
@@ -14,9 +16,9 @@ export const generateMyWorkResult = (projectCode: string, workDate?: string) =>
timeout: 20000
});
export const getProjectWorkResult = (projectCode: string, workDate?: string) =>
export const getProjectWorkResult = (projectCode: string, workDate?: string, pageNum = 1, pageSize = 20) =>
apiRequest<ProjectWorkResult>({
url: `/api/aihr/work-results/project?${params(projectCode, workDate)}`,
url: `/api/aihr/work-results/project?${params(projectCode, workDate, pageNum, pageSize)}`,
method: 'GET',
timeout: 20000
});