feat(aihr): complete phase two pilot readiness

This commit is contained in:
2026-07-10 19:13:31 +08:00
parent 0219de6fa8
commit 13230e6b9a
48 changed files with 7040 additions and 1647 deletions
@@ -0,0 +1,333 @@
<template>
<view class="page candidate-subpage study-page">
<view class="subpage-header">
<button class="subpage-back-button" aria-label="返回候选首页" title="返回候选首页" @click="goBack">←</button>
<view class="subpage-heading">
<text class="page-title">岗前预习</text>
<text class="page-subtitle">从正式知识库生成岗位要点</text>
</view>
</view>
<view v-if="message" class="status-message">{{ message }}</view>
<view v-if="!loggedIn" class="panel login-panel">
<view>
<text class="panel-title">需要登录</text>
<text class="muted block">登录后生成生活顾问岗前预习。</text>
</view>
<button class="primary-button login-button" @click="goLogin">去登录</button>
</view>
<template v-else>
<view class="panel query-panel">
<text class="query-label">预习主题</text>
<textarea
v-model="queryText"
class="textarea query-input"
maxlength="120"
placeholder="输入要预习的岗位问题"
/>
<view class="topic-row">
<button
v-for="topic in topics"
:key="topic"
class="topic-button"
:class="{ active: queryText === topic }"
:disabled="loading"
@click="queryText = topic"
>
{{ topic }}
</button>
</view>
<button class="primary-button generate-button" :disabled="loading || !queryText.trim()" @click="loadStudy">
{{ loading ? '生成中' : '生成预习' }}
</button>
</view>
<view v-if="answer" class="panel answer-panel">
<text class="panel-title">预习要点</text>
<text class="answer-text">{{ answer }}</text>
<view v-if="keyPoints.length" class="answer-section">
<text class="section-title">关键要点</text>
<view v-for="(item, index) in keyPoints" :key="`${index}-${item}`" class="point-row">
<text class="point-index">{{ index + 1 }}</text>
<text class="point-text">{{ item }}</text>
</view>
</view>
<view v-if="cautions.length" class="answer-section caution-section">
<text class="section-title">注意事项</text>
<text v-for="item in cautions" :key="item" class="caution-text">{{ item }}</text>
</view>
</view>
<view v-if="references.length" class="panel reference-panel">
<text class="panel-title">引用依据</text>
<view v-for="item in references" :key="item" class="reference-row">
<text class="reference-dot">•</text>
<text class="reference-title">{{ item }}</text>
</view>
</view>
<view v-if="!answer && !loading" class="empty-state">
<text class="empty-title">尚未生成预习</text>
<text class="empty-desc">选择主题后调用正式 SOP 知识库,不使用前端示例答案。</text>
</view>
</template>
</view>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { onShow } from '@dcloudio/uni-app';
import { isLoggedIn, rememberLoginRedirect } from '@/services/auth';
import { candidateRoutes } from '@/services/candidate-view';
import { searchKnowledge } from '@/services/knowledge';
import { resetPageScroll } from '@/services/navigation';
const currentPath = '/pages/candidate/study/index';
const topics = [
'生活顾问接到业主投诉时,应该如何安抚、记录、跟进和反馈?',
'生活顾问接到报事报修后,应该如何登记、派单和回访?',
'生活顾问与情绪激动的业主沟通时,有哪些原则和禁忌?'
];
const queryText = ref(topics[0]);
const answer = ref('');
const keyPoints = ref<string[]>([]);
const cautions = ref<string[]>([]);
const references = ref<string[]>([]);
const loggedIn = ref(false);
const loading = ref(false);
const message = ref('');
const refreshAuthState = () => {
loggedIn.value = isLoggedIn();
};
const clearResult = () => {
answer.value = '';
keyPoints.value = [];
cautions.value = [];
references.value = [];
};
const loadStudy = async () => {
if (loading.value) return;
refreshAuthState();
if (!loggedIn.value) {
goLogin();
return;
}
const query = queryText.value.trim();
if (!query) {
message.value = '请输入预习主题';
return;
}
loading.value = true;
message.value = '';
clearResult();
try {
const result = await searchKnowledge(query, '生活顾问');
answer.value = result.answer;
keyPoints.value = result.keyPoints || [];
cautions.value = result.cautions || [];
references.value = (result.docs || []).map((item) => item.title).filter(Boolean).slice(0, 5);
if (!answer.value) message.value = '正式知识库暂未返回可用答案';
} catch (error) {
refreshAuthState();
if (!loggedIn.value) {
clearResult();
message.value = '登录已过期,请重新登录';
} else {
message.value = error instanceof Error ? error.message : '岗前预习生成失败';
}
} finally {
loading.value = false;
}
};
const goLogin = () => {
rememberLoginRedirect(currentPath);
uni.redirectTo({ url: '/pages/auth/login/index' });
};
const goBack = () => {
if (getCurrentPages().length <= 1) {
uni.redirectTo({ url: candidateRoutes.index });
return;
}
uni.navigateBack({ delta: 1, fail: () => uni.redirectTo({ url: candidateRoutes.index }) });
};
onShow(() => {
resetPageScroll();
refreshAuthState();
if (!loggedIn.value) clearResult();
});
</script>
<style scoped>
.candidate-subpage {
background: #f4f7fb;
}
.candidate-subpage .panel,
.candidate-subpage button {
border-radius: 8px;
}
.login-panel {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.block {
display: block;
margin-top: 8px;
font-size: 14px;
line-height: 1.5;
}
.login-button {
min-width: 96px;
}
.query-panel {
border-left: 4px solid #e60012;
}
.query-label {
color: #172033;
font-size: 14px;
font-weight: 800;
}
.query-input {
min-height: 88px;
margin-top: 10px;
}
.topic-row {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 12px;
}
.topic-button {
min-height: 34px;
padding: 0 10px;
border: 1px solid #cbd5e1;
background: #ffffff;
color: #475569;
font-size: 12px;
}
.topic-button.active {
border-color: #e60012;
background: #fff1f2;
color: #b91c1c;
}
.generate-button {
width: 100%;
margin-top: 16px;
}
.answer-text {
display: block;
margin-top: 12px;
color: #172033;
font-size: 15px;
line-height: 1.7;
white-space: pre-wrap;
}
.answer-section {
margin-top: 18px;
padding-top: 14px;
border-top: 1px solid #e2e8f0;
}
.section-title {
color: #172033;
font-size: 14px;
font-weight: 800;
}
.point-row {
display: flex;
gap: 10px;
margin-top: 10px;
align-items: flex-start;
}
.point-index {
display: flex;
width: 22px;
height: 22px;
flex: 0 0 22px;
align-items: center;
justify-content: center;
border-radius: 50%;
background: #f1f5f9;
color: #475569;
font-size: 11px;
font-weight: 800;
}
.point-text,
.caution-text {
color: #334155;
font-size: 14px;
line-height: 1.6;
}
.caution-text {
display: block;
margin-top: 8px;
}
.reference-row {
display: flex;
gap: 8px;
margin-top: 10px;
align-items: flex-start;
}
.reference-dot {
color: #b91c1c;
font-weight: 800;
}
.reference-title {
overflow-wrap: anywhere;
color: #475569;
font-size: 13px;
line-height: 1.5;
}
.empty-state {
padding: 56px 24px;
text-align: center;
}
.empty-title {
display: block;
color: #172033;
font-size: 17px;
font-weight: 800;
}
.empty-desc {
display: block;
margin-top: 8px;
color: #64748b;
font-size: 13px;
line-height: 1.55;
}
</style>