feat(aihr): finalize practice growth and access boundaries
This commit is contained in:
@@ -42,6 +42,18 @@
|
||||
<text class="head-title">{{ displayName }} · {{ detail.scene }}</text>
|
||||
</view>
|
||||
<text class="head-meta">{{ detail.time }} · {{ detail.status }}</text>
|
||||
<view class="curriculum-summary">
|
||||
<text class="curriculum-summary-label">训练归属</text>
|
||||
<view class="curriculum-summary-tags">
|
||||
<text class="curriculum-summary-tag">{{ detail.projectType || '住宅' }}</text>
|
||||
<text class="curriculum-summary-tag">{{ detail.position || '岗位待补录' }}</text>
|
||||
<text class="curriculum-summary-tag">{{ detail.growthLevel || '层级待补录' }}</text>
|
||||
<text class="curriculum-summary-tag">{{ detail.competencyCode || '能力项待归类' }}</text>
|
||||
</view>
|
||||
<text v-if="detail.collaborationPositions" class="curriculum-summary-note">协同岗位:{{ detail.collaborationPositions }}</text>
|
||||
<text v-if="detail.curriculumVersion && detail.curriculumVersion !== '历史未记录'" class="curriculum-summary-note">目录版本:{{ detail.curriculumVersion }}(本次训练快照)</text>
|
||||
<text v-else class="curriculum-summary-note">目录版本未记录;该训练早于课程快照机制,不能按当前场景倒推。</text>
|
||||
</view>
|
||||
<view class="ai-summary">
|
||||
<view class="ai-summary-label">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3l1.9 5.1L19 10l-5.1 1.9L12 17l-1.9-5.1L5 10l5.1-1.9z"/><path d="M19 15l.9 2.1L22 18l-2.1.9L19 21l-.9-2.1L16 18l2.1-.9z"/></svg>
|
||||
@@ -184,7 +196,7 @@
|
||||
:class="{ active: nextScenarioId === item.id }"
|
||||
@click="selectNextScenario(item.id)"
|
||||
>
|
||||
{{ item.name }}
|
||||
{{ item.name }}<text v-if="item.growthLevel || item.competencyCode"> · {{ item.growthLevel || '基础' }}{{ item.competencyCode ? ` · ${item.competencyCode}` : '' }}</text>
|
||||
</button>
|
||||
</view>
|
||||
<textarea
|
||||
@@ -196,6 +208,23 @@
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view v-if="nextConfirmableGrowthLevel" class="next-training-section">
|
||||
<text class="field-label">训练层级确认(可选)</text>
|
||||
<view class="scenario-row">
|
||||
<button class="scenario-chip" :class="{ active: !confirmedGrowthLevel }" @click="confirmedGrowthLevel = ''">
|
||||
暂不确认
|
||||
</button>
|
||||
<button
|
||||
class="scenario-chip"
|
||||
:class="{ active: confirmedGrowthLevel === nextConfirmableGrowthLevel }"
|
||||
@click="confirmedGrowthLevel = nextConfirmableGrowthLevel"
|
||||
>
|
||||
确认可进入{{ nextConfirmableGrowthLevel }}
|
||||
</button>
|
||||
</view>
|
||||
<text class="growth-confirmation-note">仅记录训练层级证据,不调整职级、薪酬或上岗资格。</text>
|
||||
</view>
|
||||
|
||||
<button class="primary-button submit-button tabbar-safe-action" :disabled="marking" @click="markReviewed">
|
||||
{{ marking ? '提交中…' : '标记已复盘' }}
|
||||
</button>
|
||||
@@ -207,6 +236,9 @@
|
||||
<text v-if="detail.incentivePoint != null" class="complete-point">
|
||||
人工贡献分 {{ detail.incentivePoint }}
|
||||
</text>
|
||||
<text v-if="detail.confirmedGrowthLevel" class="complete-point">
|
||||
已确认可进入{{ detail.confirmedGrowthLevel }}{{ detail.growthConfirmedTime ? ` · ${detail.growthConfirmedTime}` : '' }}
|
||||
</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -226,7 +258,7 @@ import { computed, onBeforeUnmount, ref } from 'vue';
|
||||
import { onLoad, onShow } from '@dcloudio/uni-app';
|
||||
import type { ReviewDetail, ReviewDialogue } from '@/types/api';
|
||||
import { authenticatedFileUrl, releaseAuthenticatedFileUrl } from '@/services/api';
|
||||
import { clearAuth, isLoggedIn, rememberLoginRedirect } from '@/services/auth';
|
||||
import { clearAuth, isLoggedIn, rememberLoginRedirect, type UserPosition } from '@/services/auth';
|
||||
import { resetPageScroll } from '@/services/navigation';
|
||||
import { getPracticeReviewDetail, getPracticeScenarios, getPracticeTeamSnapshot, markPracticeReviewed } from '@/services/practice';
|
||||
import SupervisorTabBar from '@/components/SupervisorTabBar.vue';
|
||||
@@ -235,7 +267,6 @@ import {
|
||||
createAsyncGenerationGuard,
|
||||
isReviewEditable,
|
||||
isSupervisorAccessDenied,
|
||||
supervisorScenarioOptions,
|
||||
type SupervisorScenarioOption,
|
||||
type SupervisorScenarioId
|
||||
} from '@/services/supervisor-view';
|
||||
@@ -246,8 +277,9 @@ const detail = ref<ReviewDetail | null>(null);
|
||||
const advice = ref('');
|
||||
const incentivePoint = ref('');
|
||||
const nextScenarioId = ref<SupervisorScenarioId | ''>('');
|
||||
const scenarioOptions = ref<SupervisorScenarioOption[]>([...supervisorScenarioOptions]);
|
||||
const scenarioOptions = ref<SupervisorScenarioOption[]>([]);
|
||||
const nextReason = ref('');
|
||||
const confirmedGrowthLevel = ref('');
|
||||
const loggedIn = ref(false);
|
||||
const accessDenied = ref(false);
|
||||
const loading = ref(false);
|
||||
@@ -258,7 +290,16 @@ const audioLoading = ref<Record<string, boolean>>({});
|
||||
const audioErrors = ref<Record<string, string>>({});
|
||||
const audioRequests = createAsyncGenerationGuard();
|
||||
const memberNames = ref<Record<string, string>>({});
|
||||
const scenarioPosition = (value?: string): UserPosition | null => {
|
||||
const known: UserPosition[] = ['生活顾问', '保安', '保洁', '保修', '客服'];
|
||||
return known.includes(value as UserPosition) ? value as UserPosition : null;
|
||||
};
|
||||
const editable = computed(() => isReviewEditable(detail.value?.status));
|
||||
const nextConfirmableGrowthLevel = computed(() => {
|
||||
if (detail.value?.growthLevel === '初级') return '中级';
|
||||
if (detail.value?.growthLevel === '中级') return '高级';
|
||||
return '';
|
||||
});
|
||||
const loadFailed = computed(() => !loading.value && !!message.value && !detail.value);
|
||||
const firstLoading = computed(() => loading.value && !detail.value && !message.value);
|
||||
|
||||
@@ -315,6 +356,7 @@ const clearDetail = () => {
|
||||
incentivePoint.value = '';
|
||||
nextScenarioId.value = '';
|
||||
nextReason.value = '';
|
||||
confirmedGrowthLevel.value = '';
|
||||
};
|
||||
|
||||
const refreshAuthState = () => {
|
||||
@@ -410,6 +452,8 @@ const loadDetail = async () => {
|
||||
incentivePoint.value = detail.value.incentivePoint == null ? '' : String(detail.value.incentivePoint);
|
||||
nextScenarioId.value = '';
|
||||
nextReason.value = '';
|
||||
confirmedGrowthLevel.value = '';
|
||||
void loadScenarioOptions();
|
||||
} catch (error) {
|
||||
clearDetail();
|
||||
handleRequestError(error, '复盘详情加载失败');
|
||||
@@ -419,18 +463,30 @@ const loadDetail = async () => {
|
||||
};
|
||||
|
||||
const loadScenarioOptions = async () => {
|
||||
const position = scenarioPosition(detail.value?.position);
|
||||
if (!position) {
|
||||
scenarioOptions.value = [];
|
||||
nextScenarioId.value = '';
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const rows = await getPracticeScenarios('生活顾问');
|
||||
const rows = await getPracticeScenarios(position);
|
||||
const nextOptions = rows
|
||||
.filter((item) => item.enabled !== false && item.id && item.name)
|
||||
.map((item) => ({ id: item.id, name: item.name }));
|
||||
.filter((item) => item.enabled !== false && (!item.reviewStatus || item.reviewStatus === '已发布') && item.id && item.name)
|
||||
.map((item) => ({
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
growthLevel: item.growthLevel,
|
||||
competencyCode: item.competencyCode
|
||||
}));
|
||||
if (nextOptions.length) {
|
||||
scenarioOptions.value = nextOptions;
|
||||
} else {
|
||||
scenarioOptions.value = [];
|
||||
}
|
||||
} catch {
|
||||
scenarioOptions.value = [...supervisorScenarioOptions];
|
||||
scenarioOptions.value = [];
|
||||
nextScenarioId.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
@@ -469,7 +525,8 @@ const markReviewed = async () => {
|
||||
advice: advice.value.trim(),
|
||||
incentivePoint: point,
|
||||
nextScenarioId: nextScenarioId.value || undefined,
|
||||
nextReason: nextScenarioId.value ? nextReason.value.trim() : undefined
|
||||
nextReason: nextScenarioId.value ? nextReason.value.trim() : undefined,
|
||||
confirmedGrowthLevel: confirmedGrowthLevel.value || undefined
|
||||
});
|
||||
const params = new URLSearchParams({ id: String(detail.value.id) });
|
||||
if (assignedScenario) params.set('assigned', assignedScenario);
|
||||
@@ -691,6 +748,51 @@ onBeforeUnmount(() => {
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.curriculum-summary {
|
||||
margin-top: 11px;
|
||||
padding: 10px 11px;
|
||||
border-radius: 12px;
|
||||
background: #f7faff;
|
||||
}
|
||||
|
||||
.curriculum-summary-label,
|
||||
.curriculum-summary-note {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.curriculum-summary-label {
|
||||
color: #315b8f;
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.curriculum-summary-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-top: 7px;
|
||||
}
|
||||
|
||||
.curriculum-summary-tag {
|
||||
max-width: 100%;
|
||||
padding: 3px 7px;
|
||||
overflow: hidden;
|
||||
border-radius: 999px;
|
||||
background: #eaf2ff;
|
||||
color: #4b6381;
|
||||
font-size: 10px;
|
||||
line-height: 1.35;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.curriculum-summary-note {
|
||||
margin-top: 5px;
|
||||
color: #778395;
|
||||
font-size: 10px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.head-score {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
@@ -1108,6 +1210,14 @@ onBeforeUnmount(() => {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.growth-confirmation-note {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
color: #7b8798;
|
||||
font-size: 12px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.submit-button {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
|
||||
Reference in New Issue
Block a user