feat(aihr): complete uni-app role workflows

This commit is contained in:
2026-07-09 10:42:20 +08:00
parent e4070c8b8c
commit 678543e8a5
73 changed files with 15696 additions and 134 deletions
+445
View File
@@ -0,0 +1,445 @@
<template>
<view class="page profile-page">
<text class="page-title">我</text>
<text class="page-subtitle">{{ phone || '未登录' }}</text>
<view v-if="message" class="status-message">{{ message }}</view>
<view class="panel account-card">
<view>
<text class="panel-title">账号</text>
<text class="account-phone">{{ phone || '未登录' }}</text>
</view>
<button v-if="!loggedIn" class="primary-button account-action" @click="goLogin">去登录</button>
<button v-else class="secondary-button account-action" @click="logout">退出</button>
</view>
<view v-if="profile" class="panel">
<text class="panel-title">成长摘要</text>
<view class="score-row">
<view class="score-main">
<text class="score-value">{{ profile.score }}</text>
<text class="score-label">综合能力</text>
</view>
<view class="metric-grid">
<view class="metric">
<text>{{ profile.completed }}</text>
<text>已训练</text>
</view>
<view class="metric">
<text>{{ profile.pendingReview }}</text>
<text>待复盘</text>
</view>
<view class="metric">
<text>{{ profile.trainingMinutes || 0 }}</text>
<text>分钟</text>
</view>
<view class="metric">
<text>{{ profile.aiLevel || '初级' }}</text>
<text>AI等级</text>
</view>
</view>
</view>
<view v-if="profile.dimensions?.length" class="dimension-list">
<view v-for="item in profile.dimensions" :key="item.label" class="dimension-row">
<view>
<text class="dimension-title">{{ item.label }}</text>
<text class="dimension-note">{{ item.note }}</text>
</view>
<text class="dimension-score">{{ item.value }}</text>
</view>
</view>
<view v-if="profile.growthPath?.length" class="growth-section">
<text class="section-label">成长路径</text>
<view v-for="stage in profile.growthPath" :key="stage.level" class="growth-row">
<view class="growth-copy">
<text class="growth-title">{{ stage.level }} · {{ stage.status }}</text>
<text class="growth-desc">{{ stage.evidence }}</text>
</view>
<view class="growth-progress">
<view class="growth-bar">
<view class="growth-fill" :style="{ width: `${stage.progress}%` }" />
</view>
<text>{{ stage.progress }}%</text>
</view>
</view>
</view>
</view>
<view v-if="evidence" class="panel evidence-panel">
<text class="panel-title">成长证据包</text>
<view class="evidence-grid">
<view class="evidence-metric">
<text>{{ evidence.currentLevel }}</text>
<text>当前等级</text>
</view>
<view class="evidence-metric">
<text>{{ evidence.assessmentScore }}</text>
<text>测评分</text>
</view>
<view class="evidence-metric">
<text>{{ evidence.trainingMinutes }}</text>
<text>训练分钟</text>
</view>
<view class="evidence-metric">
<text>{{ evidence.contribution }}</text>
<text>贡献度</text>
</view>
</view>
<view v-if="evidence.evidenceItems?.length" class="evidence-list">
<text v-for="item in evidence.evidenceItems" :key="item" class="evidence-item">{{ item }}</text>
</view>
</view>
<view v-if="loggedIn" class="panel">
<view class="history-head">
<text class="panel-title">最近练过</text>
<button class="ghost-button refresh-button" :disabled="loading" @click="loadData">刷新</button>
</view>
<view v-if="history.length" class="history-list">
<view v-for="item in history" :key="item.id || item.sessionId || item.time" class="history-row">
<view>
<text class="history-title">{{ item.scene }}</text>
<text class="history-desc">{{ item.time }} · {{ item.summary }}</text>
</view>
<view class="history-score">
<text>{{ item.score }}</text>
<text>{{ item.status }}</text>
</view>
</view>
</view>
<view v-else class="empty">暂无训练记录</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { onShow } from '@dcloudio/uni-app';
import type { CompetencyProfile, PracticeRecord, PromotionEvidence } from '@/types/api';
import { clearAuth, getAuth, isLoggedIn, rememberLoginRedirect } from '@/services/auth';
import { getCompetencyProfile, getPracticeHistory, getPromotionEvidence } from '@/services/practice';
const loggedIn = ref(false);
const phone = ref('');
const loading = ref(false);
const message = ref('');
const profile = ref<CompetencyProfile | null>(null);
const history = ref<PracticeRecord[]>([]);
const evidence = ref<PromotionEvidence | null>(null);
const refreshAuth = () => {
const auth = getAuth();
loggedIn.value = isLoggedIn();
phone.value = auth.phone;
};
const clearProfileData = () => {
profile.value = null;
history.value = [];
evidence.value = null;
};
const loadData = async () => {
refreshAuth();
if (!loggedIn.value) {
clearProfileData();
return;
}
loading.value = true;
message.value = '';
try {
const [nextHistory, nextProfile, nextEvidence] = await Promise.all([
getPracticeHistory(),
getCompetencyProfile(),
getPromotionEvidence()
]);
history.value = nextHistory || [];
profile.value = nextProfile || null;
evidence.value = nextEvidence || null;
} catch (error) {
refreshAuth();
if (!loggedIn.value) {
clearProfileData();
message.value = '登录已过期,请重新登录';
return;
}
message.value = error instanceof Error ? error.message : '我的数据加载失败';
} finally {
loading.value = false;
}
};
const goLogin = () => {
rememberLoginRedirect('/pages/user/profile/index');
uni.navigateTo({ url: '/pages/auth/login/index' });
};
const logout = () => {
clearAuth();
clearProfileData();
refreshAuth();
uni.showToast({ title: '已退出', icon: 'none' });
};
onShow(loadData);
</script>
<style scoped>
.profile-page {
padding-bottom: 92px;
background: #f4f7fb;
}
.account-card,
.history-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.account-phone {
display: block;
margin-top: 8px;
color: #64748b;
font-size: 14px;
}
.account-action {
min-width: 84px;
}
.score-row {
display: grid;
margin-top: 16px;
grid-template-columns: 104px 1fr;
gap: 14px;
}
.score-main {
display: flex;
min-height: 124px;
flex-direction: column;
align-items: center;
justify-content: center;
border-radius: 18px;
background: #eff6ff;
}
.score-value {
color: #1d4ed8;
font-size: 42px;
font-weight: 900;
line-height: 1;
}
.score-label {
margin-top: 8px;
color: #475569;
font-size: 13px;
}
.metric-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.metric {
display: flex;
min-height: 58px;
flex-direction: column;
justify-content: center;
padding: 8px 10px;
border-radius: 14px;
background: #f8fafc;
}
.metric text:first-child {
color: #0f172a;
font-size: 18px;
font-weight: 900;
}
.metric text:last-child {
margin-top: 4px;
color: #64748b;
font-size: 12px;
}
.evidence-panel {
background: #fff7ed;
}
.evidence-grid {
display: grid;
margin-top: 14px;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.evidence-metric {
display: flex;
min-height: 58px;
flex-direction: column;
justify-content: center;
padding: 8px 10px;
border-radius: 14px;
background: #ffffff;
}
.evidence-metric text:first-child {
color: #9a3412;
font-size: 18px;
font-weight: 900;
}
.evidence-metric text:last-child {
margin-top: 4px;
color: #64748b;
font-size: 12px;
}
.evidence-list {
margin-top: 12px;
}
.evidence-item {
display: block;
padding: 9px 0;
border-top: 1px solid #fed7aa;
color: #7c2d12;
font-size: 13px;
line-height: 1.45;
}
.dimension-list,
.history-list {
margin-top: 14px;
}
.growth-section {
margin-top: 18px;
}
.section-label {
display: block;
color: #334155;
font-size: 14px;
font-weight: 900;
}
.growth-row {
display: grid;
grid-template-columns: 1fr 96px;
gap: 12px;
padding: 12px 0;
border-top: 1px solid #e2e8f0;
}
.growth-row:first-of-type {
margin-top: 10px;
}
.growth-title,
.growth-desc {
display: block;
}
.growth-title {
color: #0f172a;
font-size: 14px;
font-weight: 900;
}
.growth-desc {
margin-top: 5px;
color: #64748b;
font-size: 12px;
line-height: 1.4;
}
.growth-progress {
display: flex;
min-width: 0;
flex-direction: column;
align-items: flex-end;
justify-content: center;
gap: 6px;
color: #1d4ed8;
font-size: 12px;
font-weight: 900;
}
.growth-bar {
width: 88px;
height: 6px;
overflow: hidden;
border-radius: 999px;
background: #dbeafe;
}
.growth-fill {
height: 100%;
border-radius: inherit;
background: #2563eb;
}
.dimension-row,
.history-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 12px 0;
border-top: 1px solid #e2e8f0;
}
.dimension-title,
.history-title {
display: block;
color: #0f172a;
font-weight: 900;
}
.dimension-note,
.history-desc {
display: block;
margin-top: 5px;
color: #64748b;
font-size: 13px;
line-height: 1.4;
}
.dimension-score {
color: #1d4ed8;
font-size: 20px;
font-weight: 900;
}
.refresh-button {
min-height: 36px;
padding: 0 12px;
font-size: 13px;
}
.history-score {
display: flex;
min-width: 52px;
flex-direction: column;
align-items: flex-end;
}
.history-score text:first-child {
color: #1d4ed8;
font-size: 20px;
font-weight: 900;
}
.history-score text:last-child {
margin-top: 4px;
color: #64748b;
font-size: 12px;
}
</style>