feat: restore learning feedback and knowledge capabilities

This commit is contained in:
2026-07-18 00:50:11 +08:00
parent 876a23a123
commit 6bdc86e3fc
152 changed files with 30337 additions and 953 deletions
+170 -56
View File
@@ -22,22 +22,42 @@
<view>
<text class="panel-title">岗位身份</text>
<text class="position-title">{{ positionProfile.value }} · {{ positionProfile.heroTitle }}</text>
<text class="position-source">岗位来自人力系统组织同步,如需调整请联系 HR。</text>
</view>
</view>
<view class="position-actions">
<button
v-for="item in positionCards"
:key="item.value"
class="position-chip"
:class="{ active: item.value === positionProfile.value }"
@click="changePosition(item.value)"
>
{{ item.value }}
</button>
</view>
</view>
<view v-if="profile" class="panel profile-summary">
<view v-if="supervisorLearning" class="panel supervisor-mode-panel">
<view>
<text class="panel-title">当前为学员模式</text>
<text class="position-source">主管组织身份未改变,仅切换到个人学习与训练界面。</text>
</view>
<button class="secondary-button account-action" @click="returnToSupervisor">返回主管工作台</button>
</view>
<view class="panel display-panel">
<view class="display-heading">
<view>
<text class="panel-title">显示与辅助</text>
<text class="display-current">字体大小 · {{ currentFontSizeLabel }}</text>
</view>
</view>
<view class="font-size-options" role="group" aria-label="字体大小">
<button
v-for="option in appFontSizeOptions"
:key="option.value"
class="font-size-option"
:class="{ active: fontSize === option.value }"
:aria-pressed="fontSize === option.value"
@click="changeFontSize(option.value)"
>
{{ option.label }}
</button>
</view>
<text class="display-note">选择后会同步到今日、练、问、我等全部页面,并保留你的设置。</text>
</view>
<view v-if="loggedIn && profile" class="panel profile-summary">
<text class="panel-title">成长摘要</text>
<text class="ai-score-disclaimer">AI训练综合分仅作训练参考,不等于BRD五维绩效总分,人可复核/否决</text>
<view class="score-row">
@@ -92,7 +112,15 @@
</view>
</view>
<view v-if="evidence" class="panel evidence-panel">
<button v-if="loggedIn" class="panel points-entry" @click="openPoints">
<view>
<text class="panel-title">我的积分</text>
<text class="points-entry-desc">查看问题榜奖励积分与学习学分明细</text>
</view>
<text class="points-entry-arrow">›</text>
</button>
<view v-if="loggedIn && evidence" class="panel evidence-panel">
<text class="panel-title">成长证据包(待人工复核)</text>
<view class="evidence-grid">
<view class="evidence-metric">
@@ -153,20 +181,24 @@
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { computed, ref } from 'vue';
import { onShow } from '@dcloudio/uni-app';
import type { CompetencyProfile, PracticeRecord, PromotionEvidence } from '@/types/api';
import {
clearAuth,
getAuth,
getPositionProfile,
getSelectedPositionProfile,
isLoggedIn,
rememberLoginRedirect,
saveSelectedPosition,
userPositionOptions,
type UserPosition
isSupervisorLearnerMode,
leaveSupervisorLearnerMode,
rememberLoginRedirect
} from '@/services/auth';
import {
appFontSizeOptions,
getAppFontSize,
setAppFontSize,
type AppFontSize
} from '@/services/accessibility';
import { getCompetencyProfile, getPracticeHistory, getPromotionEvidence } from '@/services/practice';
import { resetPageScroll } from '@/services/navigation';
import { ensureEmployeePosition } from '@/services/position';
@@ -179,31 +211,62 @@ const profile = ref<CompetencyProfile | null>(null);
const history = ref<PracticeRecord[]>([]);
const evidence = ref<PromotionEvidence | null>(null);
const positionProfile = ref(getSelectedPositionProfile());
const positionCards = userPositionOptions.map((item) => getPositionProfile(item));
const supervisorLearning = ref(false);
const fontSize = ref<AppFontSize>(getAppFontSize());
const currentFontSizeLabel = computed(() =>
appFontSizeOptions.find((item) => item.value === fontSize.value)?.label || '标准'
);
let profileLoadGeneration = 0;
let profileAccountPhone = '';
const refreshAuth = () => {
const auth = getAuth();
loggedIn.value = isLoggedIn();
phone.value = auth.phone;
positionProfile.value = getSelectedPositionProfile();
supervisorLearning.value = isSupervisorLearnerMode();
fontSize.value = getAppFontSize();
};
const changeFontSize = (value: AppFontSize) => {
fontSize.value = setAppFontSize(value);
uni.showToast({ title: `已切换为${currentFontSizeLabel.value}`, icon: 'none' });
};
const returnToSupervisor = () => {
leaveSupervisorLearnerMode();
supervisorLearning.value = false;
uni.redirectTo({ url: '/pages/supervisor/index/index' });
};
const openPoints = () => {
uni.navigateTo({ url: '/pages/user/points/index' });
};
const clearProfileData = () => {
profile.value = null;
history.value = [];
evidence.value = null;
profileAccountPhone = '';
};
const loadData = async () => {
const generation = ++profileLoadGeneration;
refreshAuth();
if (!loggedIn.value) {
clearProfileData();
return;
}
const requestedPhone = getAuth().phone;
if (profileAccountPhone && profileAccountPhone !== requestedPhone) {
clearProfileData();
}
if (!(await ensureEmployeePosition('/pages/user/profile/index'))) {
return;
}
refreshAuth();
if (generation !== profileLoadGeneration || !loggedIn.value) return;
const accountPhone = getAuth().phone;
loading.value = true;
message.value = '';
try {
@@ -212,19 +275,27 @@ const loadData = async () => {
getCompetencyProfile(),
getPromotionEvidence()
]);
refreshAuth();
if (generation !== profileLoadGeneration) return;
if (!loggedIn.value || getAuth().phone !== accountPhone) {
clearProfileData();
return;
}
history.value = nextHistory || [];
profile.value = nextProfile || null;
evidence.value = nextEvidence || null;
profileAccountPhone = accountPhone;
} catch (error) {
refreshAuth();
if (!loggedIn.value) {
if (generation !== profileLoadGeneration) return;
if (!loggedIn.value || getAuth().phone !== accountPhone) {
clearProfileData();
message.value = '登录已过期,请重新登录';
message.value = loggedIn.value ? '账号已切换,请刷新' : '登录已过期,请重新登录';
return;
}
message.value = error instanceof Error ? error.message : '我的数据加载失败';
} finally {
loading.value = false;
if (generation === profileLoadGeneration) loading.value = false;
}
};
@@ -234,19 +305,14 @@ const goLogin = () => {
};
const logout = () => {
profileLoadGeneration += 1;
loading.value = false;
clearAuth();
clearProfileData();
refreshAuth();
uni.showToast({ title: '已退出', icon: 'none' });
};
const changePosition = (position: UserPosition) => {
const saved = saveSelectedPosition(position);
if (!saved) return;
positionProfile.value = getSelectedPositionProfile();
uni.showToast({ title: '岗位已更新', icon: 'none' });
};
const openHistory = (id?: number) => {
if (!id) {
message.value = '这条训练记录暂不支持查看详情';
@@ -313,13 +379,58 @@ onShow(() => {
}
.account-card,
.history-head {
.history-head,
.supervisor-mode-panel,
.display-heading {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.display-panel {
display: flex;
flex-direction: column;
gap: 14px;
}
.display-current,
.display-note {
display: block;
margin-top: 7px;
color: var(--employee-muted);
font-size: 12px;
line-height: 1.5;
}
.font-size-options {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 8px;
padding: 4px;
border-radius: 14px;
background: #f0f3f8;
}
.font-size-option {
min-width: 0;
min-height: 42px;
padding: 0 2px;
border: 1px solid transparent;
border-radius: 11px;
background: transparent;
color: var(--employee-text);
font-size: 13px;
white-space: nowrap;
}
.font-size-option.active {
border-color: #f0c8cc;
background: #ffffff;
color: var(--employee-primary);
box-shadow: 0 4px 12px rgba(24, 34, 48, 0.08);
}
.account-card {
margin-top: 16px;
border-left: 4px solid var(--employee-primary);
@@ -352,31 +463,12 @@ onShow(() => {
font-weight: 900;
}
.position-actions {
display: grid;
margin-top: 14px;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 8px;
}
.position-chip {
display: flex;
min-height: 38px;
align-items: center;
justify-content: center;
border: 1px solid #dce5f2;
border-radius: var(--employee-chip-radius);
background: #f8fbff;
color: #526176;
font-size: 13px;
font-weight: 900;
white-space: nowrap;
}
.position-chip.active {
border-color: var(--employee-primary);
background: var(--employee-primary-soft);
color: var(--employee-primary);
.position-source {
display: block;
margin-top: 7px;
color: var(--employee-muted);
font-size: 12px;
line-height: 1.5;
}
.profile-summary {
@@ -572,6 +664,28 @@ onShow(() => {
background: var(--employee-primary);
}
.points-entry {
display: flex;
width: calc(100% - 0px);
align-items: center;
justify-content: space-between;
color: var(--ink);
text-align: left;
}
.points-entry-desc {
display: block;
margin-top: 5px;
color: var(--muted);
font-size: 13px;
line-height: 1.5;
}
.points-entry-arrow {
color: var(--muted);
font-size: 28px;
}
.dimension-row,
.history-row {
display: flex;