Merge branch 'codex/brd-release-20260715' into codex/multi-tenant-knowledge-platform
# Conflicts: # AGENTS.md # README.md # backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/domain/AihrPracticeDto.java # backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrOrgSyncService.java # backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java # backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrUploadQueueService.java # backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrOrgSyncServiceTest.java # backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrPracticeSeedServiceTest.java # backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrUploadQueueServiceTest.java # docs/20260708/uni-app用户侧前端重建计划.md # docs/API_INTEGRATION.md # docs/BRD_IMPLEMENTATION_AUDIT.md # docs/BRD_PRODUCTION_MIGRATION_RUNBOOK.md # docs/DEMO_ACCEPTANCE.md # docs/DEV_SETUP.md # docs/FIGMA_IMPLEMENTATION_MATRIX.md # docs/README.md # frontend/src/views/knowledge/processing.vue # mobile-uni/src/pages/supervisor/assign/index.vue # mobile-uni/src/pages/supervisor/index/index.vue # mobile-uni/src/pages/supervisor/profile/index.vue # mobile-uni/src/pages/supervisor/review/index.vue # mobile-uni/src/pages/supervisor/reviews/index.vue # mobile-uni/src/pages/supervisor/team/index.vue # mobile-uni/src/pages/user/cases/index.vue # mobile-uni/src/pages/user/practice/index.vue # mobile-uni/src/pages/user/profile/index.vue # mobile-uni/src/pages/user/sop/index.vue # mobile-uni/src/pages/user/today/index.vue # mobile-uni/src/types/api.ts # scripts/tests/aihr-schema-migrations.test.sh
This commit is contained in:
@@ -234,3 +234,6 @@ export const consumeLoginRedirect = () => {
|
||||
};
|
||||
|
||||
export const validatePhone = (phone: string) => phonePattern.test(phone);
|
||||
|
||||
export const maskPhone = (phone: string) =>
|
||||
/^\d{8,}$/.test(phone) ? `${phone.slice(0, 3)}****${phone.slice(-4)}` : phone;
|
||||
|
||||
@@ -214,6 +214,8 @@ export const submitAnswerFeedback = (params: {
|
||||
fragmentIds: number[];
|
||||
verdict: AnswerFeedbackVerdict;
|
||||
reviewId?: number | null;
|
||||
reasonCodes?: string[];
|
||||
comment?: string;
|
||||
}) =>
|
||||
apiRequest<{ saved: number }>({
|
||||
url: '/api/knowledge/answer-feedback',
|
||||
@@ -225,7 +227,9 @@ export const submitAnswerFeedback = (params: {
|
||||
category: 'sop',
|
||||
position: getSelectedPosition(),
|
||||
source: 'mobile_uni_sop',
|
||||
reviewId: params.reviewId ?? null
|
||||
reviewId: params.reviewId ?? null,
|
||||
reasonCodes: params.reasonCodes || [],
|
||||
comment: params.comment || ''
|
||||
},
|
||||
timeout: 15000
|
||||
});
|
||||
|
||||
@@ -14,6 +14,37 @@ export const resetPageScroll = () => {
|
||||
setTimeout(scrollTop, 240);
|
||||
};
|
||||
|
||||
export const scrollPageToSelector = (selector: string, offset = 16) => {
|
||||
const scrollToTarget = () => {
|
||||
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
|
||||
const element = document.querySelector<HTMLElement>(selector);
|
||||
if (element) {
|
||||
const scrollTop = Math.max(0, window.scrollY + element.getBoundingClientRect().top - offset);
|
||||
uni.pageScrollTo({ scrollTop, duration: 0 });
|
||||
window.scrollTo({ top: scrollTop, left: 0, behavior: 'auto' });
|
||||
document.documentElement.scrollTop = scrollTop;
|
||||
document.body.scrollTop = scrollTop;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
uni.createSelectorQuery()
|
||||
.select(selector)
|
||||
.boundingClientRect((rect) => {
|
||||
const target = Array.isArray(rect) ? rect[0] : rect;
|
||||
if (target && typeof target.top === 'number') {
|
||||
uni.pageScrollTo({ scrollTop: Math.max(0, target.top - offset), duration: 0 });
|
||||
}
|
||||
})
|
||||
.exec();
|
||||
};
|
||||
|
||||
scrollToTarget();
|
||||
setTimeout(scrollToTarget, 0);
|
||||
setTimeout(scrollToTarget, 80);
|
||||
setTimeout(scrollToTarget, 240);
|
||||
};
|
||||
|
||||
const switchToToday = () => {
|
||||
uni.switchTab({ url: '/pages/user/today/index' });
|
||||
};
|
||||
|
||||
@@ -15,6 +15,12 @@ export interface SupervisorMetric {
|
||||
primary?: boolean;
|
||||
}
|
||||
|
||||
export interface TrainingTrendPoint {
|
||||
key: string;
|
||||
label: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export interface TeamProfile {
|
||||
key: string;
|
||||
extPartyId: string;
|
||||
@@ -98,8 +104,28 @@ export const manualPracticeAssignments = (assignments: PracticeAssignment[]) =>
|
||||
export const actionablePracticeAlerts = (alerts: PracticeAlert[]) =>
|
||||
alerts.filter((item) => item.type !== 'stable');
|
||||
|
||||
export const buildTrainingTrend = (records: PracticeRecord[]): TrainingTrendPoint[] =>
|
||||
records
|
||||
.filter((item) => Number.isFinite(item.score))
|
||||
.sort((a, b) => practiceTimeValue(b.time) - practiceTimeValue(a.time))
|
||||
.slice(0, 5)
|
||||
.reverse()
|
||||
.map((item, index) => ({
|
||||
key: `${item.id || item.sessionId || item.time}-${index}`,
|
||||
label: trainingTrendLabel(item.time, index),
|
||||
value: Math.max(0, Math.min(100, item.score))
|
||||
}));
|
||||
|
||||
export const isReviewEditable = (status: string | null | undefined) => status === '待复盘';
|
||||
|
||||
export type ReviewFilter = 'all' | 'low' | 'pending';
|
||||
|
||||
export const filterReviewRecords = (reviews: PracticeRecord[], filter: ReviewFilter) => {
|
||||
if (filter === 'low') return reviews.filter((item) => item.score < 80);
|
||||
if (filter === 'pending') return reviews.filter((item) => isReviewEditable(item.status));
|
||||
return reviews;
|
||||
};
|
||||
|
||||
export const isSupervisorAccessDenied = (error: unknown) => {
|
||||
const message = error instanceof Error
|
||||
? error.message
|
||||
@@ -131,14 +157,14 @@ export const buildSupervisorMetrics = (
|
||||
return [
|
||||
{
|
||||
key: 'pendingReviews',
|
||||
title: '当前待复盘',
|
||||
title: '待复盘',
|
||||
value: pendingReviews,
|
||||
desc: pendingReviews ? '优先处理低分和待处理记录' : '暂无待复盘记录',
|
||||
primary: true
|
||||
},
|
||||
{
|
||||
key: 'pendingAssignments',
|
||||
title: '当前待训练',
|
||||
title: '待训练',
|
||||
value: pendingAssignments,
|
||||
desc: pendingAssignments ? '仍有专项训练待完成' : '暂无未完成专项'
|
||||
},
|
||||
@@ -271,12 +297,42 @@ export const buildTeamProfiles = (
|
||||
|| (a.latestScore ?? 100) - (b.latestScore ?? 100));
|
||||
};
|
||||
|
||||
export const filterTeamProfiles = <T extends Pick<TeamProfile, 'displayName' | 'extPartyId'>>(
|
||||
profiles: T[],
|
||||
query: string
|
||||
) => {
|
||||
const keyword = query.trim().toLowerCase();
|
||||
if (!keyword) return profiles;
|
||||
return profiles.filter((item) =>
|
||||
item.displayName.toLowerCase().includes(keyword)
|
||||
|| item.extPartyId.toLowerCase().includes(keyword));
|
||||
};
|
||||
|
||||
export const paginateTeamProfiles = <T>(profiles: T[], requestedPage: number, pageSize: number) => {
|
||||
const safeSize = Math.max(1, Math.floor(pageSize));
|
||||
const pageCount = Math.max(1, Math.ceil(profiles.length / safeSize));
|
||||
const page = Math.min(pageCount, Math.max(1, Math.floor(requestedPage)));
|
||||
return {
|
||||
items: profiles.slice((page - 1) * safeSize, page * safeSize),
|
||||
page,
|
||||
pageCount
|
||||
};
|
||||
};
|
||||
|
||||
function identitySuffix(value: string) {
|
||||
if (/^1[3-9]\d{9}$/.test(value)) return `尾号${value.slice(-4)}`;
|
||||
if (value.length > 8) return `末尾${value.slice(-6)}`;
|
||||
return value;
|
||||
}
|
||||
|
||||
function trainingTrendLabel(value: string, index: number) {
|
||||
const parts = value.match(/\d+/g) || [];
|
||||
const [first = '', second = '', third = ''] = parts;
|
||||
if (parts.length >= 3 && first.length === 4) return `${second.padStart(2, '0')}-${third.padStart(2, '0')}`;
|
||||
if (parts.length >= 2) return `${first.padStart(2, '0')}-${second.padStart(2, '0')}`;
|
||||
return `第${index + 1}次`;
|
||||
}
|
||||
|
||||
function practiceTimeValue(value: string) {
|
||||
const parts = value.match(/\d+/g)?.map(Number) || [];
|
||||
if (parts.length < 4 || parts.some((part) => !Number.isFinite(part))) {
|
||||
@@ -284,6 +340,6 @@ function practiceTimeValue(value: string) {
|
||||
}
|
||||
const normalized = parts.length >= 5 ? parts.slice(0, 6) : [0, ...parts.slice(0, 5)];
|
||||
while (normalized.length < 6) normalized.push(0);
|
||||
const [year, month, day, hour, minute, second] = normalized;
|
||||
const [year = 0, month = 0, day = 0, hour = 0, minute = 0, second = 0] = normalized;
|
||||
return (((((year * 13 + month) * 32 + day) * 24 + hour) * 60 + minute) * 60) + second;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user