fix(mobile): require employee position before user flow
This commit is contained in:
+3
-2
@@ -374,14 +374,15 @@ public class AihrOrgSyncService {
|
|||||||
if (!keywordValue.isBlank()) {
|
if (!keywordValue.isBlank()) {
|
||||||
String like = "%" + keywordValue + "%";
|
String like = "%" + keywordValue + "%";
|
||||||
where.append("""
|
where.append("""
|
||||||
and (person_name like ? or ext_party_id like ? or position_name like ?
|
and (person_name like ? or ext_party_id like ? or person_phone like ?
|
||||||
or dept_name like ? or project_name like ?)
|
or position_name like ? or dept_name like ? or project_name like ?)
|
||||||
""");
|
""");
|
||||||
args.add(like);
|
args.add(like);
|
||||||
args.add(like);
|
args.add(like);
|
||||||
args.add(like);
|
args.add(like);
|
||||||
args.add(like);
|
args.add(like);
|
||||||
args.add(like);
|
args.add(like);
|
||||||
|
args.add(like);
|
||||||
}
|
}
|
||||||
String project = clean(projectCode);
|
String project = clean(projectCode);
|
||||||
if (!project.isBlank()) {
|
if (!project.isBlank()) {
|
||||||
|
|||||||
@@ -37,6 +37,12 @@
|
|||||||
"navigationBarTitleText": "登录"
|
"navigationBarTitleText": "登录"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/auth/position/index",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "确认岗位"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/common/building/index",
|
"path": "pages/common/building/index",
|
||||||
"style": {
|
"style": {
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import type { LoginResult } from '@/types/api';
|
|||||||
import { apiRequest } from '@/services/api';
|
import { apiRequest } from '@/services/api';
|
||||||
import { consumeLoginRedirect, getAuth, isLoggedIn, phoneKey, saveAuth, validatePhone } from '@/services/auth';
|
import { consumeLoginRedirect, getAuth, isLoggedIn, phoneKey, saveAuth, validatePhone } from '@/services/auth';
|
||||||
import { resetPageScroll } from '@/services/navigation';
|
import { resetPageScroll } from '@/services/navigation';
|
||||||
|
import { goToMobileTarget, nextUserRouteAfterLogin } from '@/services/position';
|
||||||
|
|
||||||
const stored = getAuth();
|
const stored = getAuth();
|
||||||
const phone = ref(stored.phone || uni.getStorageSync(phoneKey) || '');
|
const phone = ref(stored.phone || uni.getStorageSync(phoneKey) || '');
|
||||||
@@ -48,20 +49,9 @@ const smsCode = ref('');
|
|||||||
const sending = ref(false);
|
const sending = ref(false);
|
||||||
const loggingIn = ref(false);
|
const loggingIn = ref(false);
|
||||||
const message = ref('');
|
const message = ref('');
|
||||||
const tabPages = new Set([
|
const goAfterLogin = async () => {
|
||||||
'/pages/user/today/index',
|
|
||||||
'/pages/user/practice/index',
|
|
||||||
'/pages/user/sop/index',
|
|
||||||
'/pages/user/profile/index'
|
|
||||||
]);
|
|
||||||
|
|
||||||
const goAfterLogin = () => {
|
|
||||||
const redirect = consumeLoginRedirect() || '/pages/user/today/index';
|
const redirect = consumeLoginRedirect() || '/pages/user/today/index';
|
||||||
if (tabPages.has(redirect)) {
|
goToMobileTarget(await nextUserRouteAfterLogin(redirect));
|
||||||
uni.switchTab({ url: redirect });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
uni.redirectTo({ url: redirect });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const sendCode = async () => {
|
const sendCode = async () => {
|
||||||
@@ -115,7 +105,7 @@ const login = async () => {
|
|||||||
throw new Error('登录响应缺少认证信息');
|
throw new Error('登录响应缺少认证信息');
|
||||||
}
|
}
|
||||||
saveAuth(result, value);
|
saveAuth(result, value);
|
||||||
goAfterLogin();
|
await goAfterLogin();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.value = error instanceof Error ? error.message : '登录失败';
|
message.value = error instanceof Error ? error.message : '登录失败';
|
||||||
} finally {
|
} finally {
|
||||||
@@ -126,7 +116,7 @@ const login = async () => {
|
|||||||
onShow(() => {
|
onShow(() => {
|
||||||
resetPageScroll();
|
resetPageScroll();
|
||||||
if (isLoggedIn()) {
|
if (isLoggedIn()) {
|
||||||
goAfterLogin();
|
void goAfterLogin();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -0,0 +1,157 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page position-page">
|
||||||
|
<view class="position-head">
|
||||||
|
<text class="page-title">确认岗位</text>
|
||||||
|
<text class="page-subtitle">{{ phone }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="message" class="status-message">{{ message }}</view>
|
||||||
|
|
||||||
|
<view class="panel">
|
||||||
|
<text class="panel-title">选择当前岗位</text>
|
||||||
|
<view class="position-list">
|
||||||
|
<button
|
||||||
|
v-for="item in positionCards"
|
||||||
|
:key="item.value"
|
||||||
|
class="position-card"
|
||||||
|
:class="{ active: item.value === selectedPosition }"
|
||||||
|
:disabled="saving"
|
||||||
|
@click="selectPosition(item.value)"
|
||||||
|
>
|
||||||
|
<view class="position-copy">
|
||||||
|
<text class="position-name">{{ item.value }}</text>
|
||||||
|
<text class="position-title">{{ item.heroTitle }}</text>
|
||||||
|
</view>
|
||||||
|
<text class="position-mark">{{ item.value === selectedPosition ? '已选' : '选择' }}</text>
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { onLoad, onShow } from '@dcloudio/uni-app';
|
||||||
|
import {
|
||||||
|
getAuth,
|
||||||
|
getPositionProfile,
|
||||||
|
isLoggedIn,
|
||||||
|
rememberLoginRedirect,
|
||||||
|
saveSelectedPosition,
|
||||||
|
userPositionOptions,
|
||||||
|
type UserPosition
|
||||||
|
} from '@/services/auth';
|
||||||
|
import { goToMobileTarget, hydratePositionFromOrg } from '@/services/position';
|
||||||
|
import { resetPageScroll } from '@/services/navigation';
|
||||||
|
|
||||||
|
const redirect = ref('/pages/user/today/index');
|
||||||
|
const selectedPosition = ref<UserPosition | ''>('');
|
||||||
|
const saving = ref(false);
|
||||||
|
const message = ref('');
|
||||||
|
const phone = computed(() => getAuth().phone || '未登录');
|
||||||
|
const positionCards = userPositionOptions.map((item) => getPositionProfile(item));
|
||||||
|
|
||||||
|
const finish = () => {
|
||||||
|
goToMobileTarget(redirect.value || '/pages/user/today/index');
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectPosition = (position: UserPosition) => {
|
||||||
|
if (saving.value) return;
|
||||||
|
saving.value = true;
|
||||||
|
const saved = saveSelectedPosition(position);
|
||||||
|
selectedPosition.value = saved;
|
||||||
|
finish();
|
||||||
|
};
|
||||||
|
|
||||||
|
const autoFillFromOrg = async () => {
|
||||||
|
if (!isLoggedIn()) {
|
||||||
|
rememberLoginRedirect(redirect.value);
|
||||||
|
uni.redirectTo({ url: '/pages/auth/login/index' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const officialPosition = await hydratePositionFromOrg();
|
||||||
|
if (officialPosition) {
|
||||||
|
selectedPosition.value = officialPosition;
|
||||||
|
finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
message.value = '未从组织数据匹配到岗位,请确认当前岗位';
|
||||||
|
};
|
||||||
|
|
||||||
|
onLoad((query) => {
|
||||||
|
if (typeof query?.redirect === 'string') {
|
||||||
|
const target = decodeURIComponent(query.redirect);
|
||||||
|
if (target.startsWith('/pages/')) {
|
||||||
|
redirect.value = target;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onShow(() => {
|
||||||
|
resetPageScroll();
|
||||||
|
void autoFillFromOrg();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.position-page {
|
||||||
|
display: flex;
|
||||||
|
min-height: 100vh;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.position-head {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.position-list {
|
||||||
|
display: flex;
|
||||||
|
margin-top: 16px;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.position-card {
|
||||||
|
display: flex;
|
||||||
|
min-height: 72px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 16px;
|
||||||
|
background: #f8fafc;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.position-card.active {
|
||||||
|
border-color: #2563eb;
|
||||||
|
background: #eff6ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.position-copy {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.position-name {
|
||||||
|
color: #0f172a;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.position-title {
|
||||||
|
margin-top: 6px;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.position-mark {
|
||||||
|
color: #2563eb;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 900;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -177,6 +177,7 @@ import { answerDailyDrill, finishPractice, getPracticeAssignments, getPracticePr
|
|||||||
import { isLoggedIn, rememberLoginRedirect } from '@/services/auth';
|
import { isLoggedIn, rememberLoginRedirect } from '@/services/auth';
|
||||||
import { searchKnowledge } from '@/services/knowledge';
|
import { searchKnowledge } from '@/services/knowledge';
|
||||||
import { resetPageScroll } from '@/services/navigation';
|
import { resetPageScroll } from '@/services/navigation';
|
||||||
|
import { ensureEmployeePosition } from '@/services/position';
|
||||||
import { chooseSpeechAudio, transcribeSpeechBlob, transcribeSpeechFile } from '@/services/speech';
|
import { chooseSpeechAudio, transcribeSpeechBlob, transcribeSpeechFile } from '@/services/speech';
|
||||||
|
|
||||||
type PracticeStatus = 'idle' | 'starting' | 'active' | 'submitting' | 'finished';
|
type PracticeStatus = 'idle' | 'starting' | 'active' | 'submitting' | 'finished';
|
||||||
@@ -675,11 +676,18 @@ const finish = async (force = false) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onShow(() => {
|
const handleShow = async () => {
|
||||||
resetPageScroll();
|
resetPageScroll();
|
||||||
refreshAuthState();
|
refreshAuthState();
|
||||||
|
if (loggedIn.value && !(await ensureEmployeePosition('/pages/user/practice/index'))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
void loadAssignments();
|
void loadAssignments();
|
||||||
void loadPrepCard();
|
void loadPrepCard();
|
||||||
|
};
|
||||||
|
|
||||||
|
onShow(() => {
|
||||||
|
void handleShow();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,26 @@
|
|||||||
<button v-else class="secondary-button account-action" @click="logout">退出</button>
|
<button v-else class="secondary-button account-action" @click="logout">退出</button>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<view v-if="loggedIn" class="panel position-panel">
|
||||||
|
<view class="position-current">
|
||||||
|
<view>
|
||||||
|
<text class="panel-title">岗位身份</text>
|
||||||
|
<text class="position-title">{{ positionProfile.value }} · {{ positionProfile.heroTitle }}</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">
|
<view v-if="profile" class="panel">
|
||||||
<text class="panel-title">成长摘要</text>
|
<text class="panel-title">成长摘要</text>
|
||||||
<view class="score-row">
|
<view class="score-row">
|
||||||
@@ -117,9 +137,20 @@
|
|||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { onShow } from '@dcloudio/uni-app';
|
import { onShow } from '@dcloudio/uni-app';
|
||||||
import type { CompetencyProfile, PracticeRecord, PromotionEvidence } from '@/types/api';
|
import type { CompetencyProfile, PracticeRecord, PromotionEvidence } from '@/types/api';
|
||||||
import { clearAuth, getAuth, isLoggedIn, rememberLoginRedirect } from '@/services/auth';
|
import {
|
||||||
|
clearAuth,
|
||||||
|
getAuth,
|
||||||
|
getPositionProfile,
|
||||||
|
getSelectedPositionProfile,
|
||||||
|
isLoggedIn,
|
||||||
|
rememberLoginRedirect,
|
||||||
|
saveSelectedPosition,
|
||||||
|
userPositionOptions,
|
||||||
|
type UserPosition
|
||||||
|
} from '@/services/auth';
|
||||||
import { getCompetencyProfile, getPracticeHistory, getPromotionEvidence } from '@/services/practice';
|
import { getCompetencyProfile, getPracticeHistory, getPromotionEvidence } from '@/services/practice';
|
||||||
import { resetPageScroll } from '@/services/navigation';
|
import { resetPageScroll } from '@/services/navigation';
|
||||||
|
import { ensureEmployeePosition } from '@/services/position';
|
||||||
|
|
||||||
const loggedIn = ref(false);
|
const loggedIn = ref(false);
|
||||||
const phone = ref('');
|
const phone = ref('');
|
||||||
@@ -128,11 +159,14 @@ const message = ref('');
|
|||||||
const profile = ref<CompetencyProfile | null>(null);
|
const profile = ref<CompetencyProfile | null>(null);
|
||||||
const history = ref<PracticeRecord[]>([]);
|
const history = ref<PracticeRecord[]>([]);
|
||||||
const evidence = ref<PromotionEvidence | null>(null);
|
const evidence = ref<PromotionEvidence | null>(null);
|
||||||
|
const positionProfile = ref(getSelectedPositionProfile());
|
||||||
|
const positionCards = userPositionOptions.map((item) => getPositionProfile(item));
|
||||||
|
|
||||||
const refreshAuth = () => {
|
const refreshAuth = () => {
|
||||||
const auth = getAuth();
|
const auth = getAuth();
|
||||||
loggedIn.value = isLoggedIn();
|
loggedIn.value = isLoggedIn();
|
||||||
phone.value = auth.phone;
|
phone.value = auth.phone;
|
||||||
|
positionProfile.value = getSelectedPositionProfile();
|
||||||
};
|
};
|
||||||
|
|
||||||
const clearProfileData = () => {
|
const clearProfileData = () => {
|
||||||
@@ -147,6 +181,10 @@ const loadData = async () => {
|
|||||||
clearProfileData();
|
clearProfileData();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!(await ensureEmployeePosition('/pages/user/profile/index'))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
refreshAuth();
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
message.value = '';
|
message.value = '';
|
||||||
try {
|
try {
|
||||||
@@ -183,6 +221,13 @@ const logout = () => {
|
|||||||
uni.showToast({ title: '已退出', icon: 'none' });
|
uni.showToast({ title: '已退出', icon: 'none' });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const changePosition = (position: UserPosition) => {
|
||||||
|
const saved = saveSelectedPosition(position);
|
||||||
|
if (!saved) return;
|
||||||
|
positionProfile.value = getSelectedPositionProfile();
|
||||||
|
uni.showToast({ title: '岗位已更新', icon: 'none' });
|
||||||
|
};
|
||||||
|
|
||||||
onShow(() => {
|
onShow(() => {
|
||||||
resetPageScroll();
|
resetPageScroll();
|
||||||
void loadData();
|
void loadData();
|
||||||
@@ -214,6 +259,48 @@ onShow(() => {
|
|||||||
min-width: 84px;
|
min-width: 84px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.position-current {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.position-title {
|
||||||
|
display: block;
|
||||||
|
margin-top: 8px;
|
||||||
|
color: #2563eb;
|
||||||
|
font-size: 16px;
|
||||||
|
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 #e2e8f0;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #f8fafc;
|
||||||
|
color: #475569;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 900;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.position-chip.active {
|
||||||
|
border-color: #2563eb;
|
||||||
|
background: #eff6ff;
|
||||||
|
color: #1d4ed8;
|
||||||
|
}
|
||||||
|
|
||||||
.score-row {
|
.score-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
v-model="query"
|
v-model="query"
|
||||||
class="textarea"
|
class="textarea"
|
||||||
maxlength="200"
|
maxlength="200"
|
||||||
placeholder="例如: 业主投诉噪音,生活顾问应该怎么处理?"
|
:placeholder="quickQuestionPlaceholder"
|
||||||
/>
|
/>
|
||||||
<view class="chip-row">
|
<view class="chip-row">
|
||||||
<button v-for="item in quickQuestions" :key="item" class="chip" :disabled="loading" @click="useQuickQuestion(item)">
|
<button v-for="item in quickQuestions" :key="item" class="chip" :disabled="loading" @click="useQuickQuestion(item)">
|
||||||
@@ -107,7 +107,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { onShow } from '@dcloudio/uni-app';
|
import { onShow } from '@dcloudio/uni-app';
|
||||||
import type {
|
import type {
|
||||||
AnswerFeedbackVerdict,
|
AnswerFeedbackVerdict,
|
||||||
@@ -119,10 +119,13 @@ import {
|
|||||||
searchKnowledge,
|
searchKnowledge,
|
||||||
submitAnswerFeedback
|
submitAnswerFeedback
|
||||||
} from '@/services/knowledge';
|
} from '@/services/knowledge';
|
||||||
import { isLoggedIn, rememberLoginRedirect } from '@/services/auth';
|
import { getSelectedPositionProfile, isLoggedIn, rememberLoginRedirect } from '@/services/auth';
|
||||||
import { resetPageScroll } from '@/services/navigation';
|
import { resetPageScroll } from '@/services/navigation';
|
||||||
|
import { ensureEmployeePosition } from '@/services/position';
|
||||||
|
|
||||||
const quickQuestions = ['业主投诉噪音怎么安抚', '停车费催缴怎么说', '生活顾问的一天做什么'];
|
const positionProfile = ref(getSelectedPositionProfile());
|
||||||
|
const quickQuestions = computed(() => positionProfile.value.quickQuestions);
|
||||||
|
const quickQuestionPlaceholder = computed(() => `例如: ${quickQuestions.value[0] || '岗位SOP怎么处理?'}`);
|
||||||
const query = ref('');
|
const query = ref('');
|
||||||
const result = ref<KnowledgeSearchResponse | null>(null);
|
const result = ref<KnowledgeSearchResponse | null>(null);
|
||||||
const summaryCard = ref<SummaryCardResponse | null>(null);
|
const summaryCard = ref<SummaryCardResponse | null>(null);
|
||||||
@@ -143,6 +146,7 @@ const resetResultState = () => {
|
|||||||
|
|
||||||
const refreshAuthState = () => {
|
const refreshAuthState = () => {
|
||||||
loggedIn.value = isLoggedIn();
|
loggedIn.value = isLoggedIn();
|
||||||
|
positionProfile.value = getSelectedPositionProfile();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRequestError = (error: unknown, fallback: string, wasLoggedIn = loggedIn.value) => {
|
const handleRequestError = (error: unknown, fallback: string, wasLoggedIn = loggedIn.value) => {
|
||||||
@@ -169,6 +173,10 @@ const search = async () => {
|
|||||||
goLogin();
|
goLogin();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!(await ensureEmployeePosition('/pages/user/sop/index'))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
positionProfile.value = getSelectedPositionProfile();
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
message.value = '';
|
message.value = '';
|
||||||
resetResultState();
|
resetResultState();
|
||||||
@@ -256,9 +264,13 @@ const goLogin = () => {
|
|||||||
uni.navigateTo({ url: '/pages/auth/login/index' });
|
uni.navigateTo({ url: '/pages/auth/login/index' });
|
||||||
};
|
};
|
||||||
|
|
||||||
onShow(() => {
|
const handleShow = async () => {
|
||||||
resetPageScroll();
|
resetPageScroll();
|
||||||
refreshAuthState();
|
refreshAuthState();
|
||||||
|
if (loggedIn.value && !(await ensureEmployeePosition('/pages/user/sop/index'))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
positionProfile.value = getSelectedPositionProfile();
|
||||||
if (!loggedIn.value) {
|
if (!loggedIn.value) {
|
||||||
resetResultState();
|
resetResultState();
|
||||||
}
|
}
|
||||||
@@ -276,6 +288,10 @@ onShow(() => {
|
|||||||
} else {
|
} else {
|
||||||
message.value = '登录后继续查询该问题';
|
message.value = '登录后继续查询该问题';
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onShow(() => {
|
||||||
|
void handleShow();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
|
|
||||||
<view class="mentor-hero">
|
<view class="mentor-hero">
|
||||||
<view class="hero-copy">
|
<view class="hero-copy">
|
||||||
<text class="hero-role">生活顾问</text>
|
<text class="hero-role">{{ positionProfile.value }}</text>
|
||||||
<text class="hero-title">客服管家</text>
|
<text class="hero-title">{{ positionProfile.heroTitle }}</text>
|
||||||
<text class="hero-sub">有问题,随时问老师傅</text>
|
<text class="hero-sub">有问题,随时问老师傅</text>
|
||||||
</view>
|
</view>
|
||||||
<image class="mentor-image" src="/static/mentor.png" mode="aspectFit" />
|
<image class="mentor-image" src="/static/mentor.png" mode="aspectFit" />
|
||||||
@@ -95,9 +95,9 @@
|
|||||||
<view v-else class="empty">暂无工具入口</view>
|
<view v-else class="empty">暂无工具入口</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<button class="tip-card" @click="ask('催费话术')">
|
<button class="tip-card" @click="ask(primaryTipQuestion)">
|
||||||
<text class="tip-mark">贴</text>
|
<text class="tip-mark">贴</text>
|
||||||
<text>今日小贴士:催费沟通先共情,再说明,再给方案。</text>
|
<text>{{ positionProfile.tip }}</text>
|
||||||
<text class="tip-arrow">›</text>
|
<text class="tip-arrow">›</text>
|
||||||
</button>
|
</button>
|
||||||
</view>
|
</view>
|
||||||
@@ -109,14 +109,17 @@ import { onShow } from '@dcloudio/uni-app';
|
|||||||
import type { PracticeAssignment, RoleHome } from '@/types/api';
|
import type { PracticeAssignment, RoleHome } from '@/types/api';
|
||||||
import { getMobileHome } from '@/services/home';
|
import { getMobileHome } from '@/services/home';
|
||||||
import { getPracticeAssignments } from '@/services/practice';
|
import { getPracticeAssignments } from '@/services/practice';
|
||||||
import { getAuth, isLoggedIn, rememberLoginRedirect } from '@/services/auth';
|
import { getAuth, getSelectedPositionProfile, isLoggedIn, rememberLoginRedirect } from '@/services/auth';
|
||||||
import { resetPageScroll } from '@/services/navigation';
|
import { resetPageScroll } from '@/services/navigation';
|
||||||
|
import { ensureEmployeePosition } from '@/services/position';
|
||||||
|
|
||||||
const home = ref<RoleHome | null>(null);
|
const home = ref<RoleHome | null>(null);
|
||||||
const assignments = ref<PracticeAssignment[]>([]);
|
const assignments = ref<PracticeAssignment[]>([]);
|
||||||
const message = ref('');
|
const message = ref('');
|
||||||
const loggedIn = ref(false);
|
const loggedIn = ref(false);
|
||||||
const quickQuestions = ['催费话术', '投诉应对', '收费标准'];
|
const positionProfile = ref(getSelectedPositionProfile());
|
||||||
|
const quickQuestions = computed(() => positionProfile.value.quickQuestions);
|
||||||
|
const primaryTipQuestion = computed(() => quickQuestions.value[0] || '岗位SOP');
|
||||||
const pendingSopQueryKey = 'aihr_sop_pending_query';
|
const pendingSopQueryKey = 'aihr_sop_pending_query';
|
||||||
const pendingAssignmentKey = 'aihr_pending_assignment_id';
|
const pendingAssignmentKey = 'aihr_pending_assignment_id';
|
||||||
|
|
||||||
@@ -176,12 +179,17 @@ const mentorBubbleText = computed(() =>
|
|||||||
|
|
||||||
const loadHome = async () => {
|
const loadHome = async () => {
|
||||||
loggedIn.value = isLoggedIn();
|
loggedIn.value = isLoggedIn();
|
||||||
|
positionProfile.value = getSelectedPositionProfile();
|
||||||
message.value = '';
|
message.value = '';
|
||||||
assignments.value = [];
|
assignments.value = [];
|
||||||
if (!loggedIn.value) {
|
if (!loggedIn.value) {
|
||||||
home.value = null;
|
home.value = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!(await ensureEmployeePosition('/pages/user/today/index'))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
positionProfile.value = getSelectedPositionProfile();
|
||||||
const [homeResult, assignmentResult] = await Promise.allSettled([
|
const [homeResult, assignmentResult] = await Promise.allSettled([
|
||||||
getMobileHome('user'),
|
getMobileHome('user'),
|
||||||
getPracticeAssignments(true)
|
getPracticeAssignments(true)
|
||||||
|
|||||||
@@ -4,18 +4,97 @@ export const tokenKey = 'aihr_mobile_access_token';
|
|||||||
export const clientIdKey = 'aihr_mobile_client_id';
|
export const clientIdKey = 'aihr_mobile_client_id';
|
||||||
export const phoneKey = 'aihr_mobile_phone';
|
export const phoneKey = 'aihr_mobile_phone';
|
||||||
export const loginRedirectKey = 'aihr_mobile_login_redirect';
|
export const loginRedirectKey = 'aihr_mobile_login_redirect';
|
||||||
|
export const positionKey = 'aihr_mobile_position';
|
||||||
export const phonePattern = /^1[3-9]\d{9}$/;
|
export const phonePattern = /^1[3-9]\d{9}$/;
|
||||||
|
|
||||||
|
export const userPositionOptions = ['生活顾问', '保安', '保洁', '保修', '客服'] as const;
|
||||||
|
|
||||||
|
export type UserPosition = typeof userPositionOptions[number];
|
||||||
|
|
||||||
|
export interface UserPositionProfile {
|
||||||
|
value: UserPosition;
|
||||||
|
heroTitle: string;
|
||||||
|
quickQuestions: string[];
|
||||||
|
tip: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AuthState {
|
export interface AuthState {
|
||||||
token: string;
|
token: string;
|
||||||
clientId: string;
|
clientId: string;
|
||||||
phone: string;
|
phone: string;
|
||||||
|
position: UserPosition | '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const positionProfiles: Record<UserPosition, UserPositionProfile> = {
|
||||||
|
生活顾问: {
|
||||||
|
value: '生活顾问',
|
||||||
|
heroTitle: '客服管家',
|
||||||
|
quickQuestions: ['催费话术', '投诉应对', '收费标准'],
|
||||||
|
tip: '今日小贴士:催费沟通先共情,再说明,再给方案。'
|
||||||
|
},
|
||||||
|
保安: {
|
||||||
|
value: '保安',
|
||||||
|
heroTitle: '秩序岗',
|
||||||
|
quickQuestions: ['门岗核验', '巡逻异常', '车辆指引'],
|
||||||
|
tip: '今日小贴士:异常先确认人车物,再登记,再同步主管。'
|
||||||
|
},
|
||||||
|
保洁: {
|
||||||
|
value: '保洁',
|
||||||
|
heroTitle: '环境岗',
|
||||||
|
quickQuestions: ['公区清洁', '垃圾分类', '消杀标准'],
|
||||||
|
tip: '今日小贴士:高频区域先看地面、扶手、电梯按钮。'
|
||||||
|
},
|
||||||
|
保修: {
|
||||||
|
value: '保修',
|
||||||
|
heroTitle: '工程维修',
|
||||||
|
quickQuestions: ['报修接单', '上门话术', '安全检查'],
|
||||||
|
tip: '今日小贴士:上门前确认故障、工具、预约时间。'
|
||||||
|
},
|
||||||
|
客服: {
|
||||||
|
value: '客服',
|
||||||
|
heroTitle: '服务前台',
|
||||||
|
quickQuestions: ['来访接待', '投诉登记', '工单跟进'],
|
||||||
|
tip: '今日小贴士:复杂诉求先复述确认,再记录责任人与时限。'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const cleanText = (value: unknown) => String(value || '').trim();
|
||||||
|
|
||||||
|
export const normalizePosition = (value: unknown): UserPosition | '' => {
|
||||||
|
const text = cleanText(value);
|
||||||
|
if (!text) return '';
|
||||||
|
if (text.includes('生活顾问') || text.includes('客服管家') || text.includes('物业管家') || text.includes('管家')) {
|
||||||
|
return '生活顾问';
|
||||||
|
}
|
||||||
|
if (text.includes('保安') || text.includes('秩序') || text.includes('安保')) return '保安';
|
||||||
|
if (text.includes('保洁') || text.includes('环境')) return '保洁';
|
||||||
|
if (text.includes('保修') || text.includes('维修') || text.includes('工程')) return '保修';
|
||||||
|
if (text.includes('客服') || text.includes('前台')) return '客服';
|
||||||
|
return (userPositionOptions as readonly string[]).includes(text) ? (text as UserPosition) : '';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const hasSelectedPosition = () => Boolean(normalizePosition(uni.getStorageSync(positionKey)));
|
||||||
|
|
||||||
|
export const getSelectedPosition = (): UserPosition =>
|
||||||
|
normalizePosition(uni.getStorageSync(positionKey)) || '生活顾问';
|
||||||
|
|
||||||
|
export const getPositionProfile = (position: unknown) =>
|
||||||
|
positionProfiles[normalizePosition(position) || '生活顾问'];
|
||||||
|
|
||||||
|
export const getSelectedPositionProfile = () => getPositionProfile(getSelectedPosition());
|
||||||
|
|
||||||
|
export const saveSelectedPosition = (position: unknown) => {
|
||||||
|
const normalized = normalizePosition(position);
|
||||||
|
if (!normalized) return '';
|
||||||
|
uni.setStorageSync(positionKey, normalized);
|
||||||
|
return normalized;
|
||||||
|
};
|
||||||
|
|
||||||
export const getAuth = (): AuthState => ({
|
export const getAuth = (): AuthState => ({
|
||||||
token: uni.getStorageSync(tokenKey) || '',
|
token: uni.getStorageSync(tokenKey) || '',
|
||||||
clientId: uni.getStorageSync(clientIdKey) || '',
|
clientId: uni.getStorageSync(clientIdKey) || '',
|
||||||
phone: uni.getStorageSync(phoneKey) || ''
|
phone: uni.getStorageSync(phoneKey) || '',
|
||||||
|
position: normalizePosition(uni.getStorageSync(positionKey))
|
||||||
});
|
});
|
||||||
|
|
||||||
export const isLoggedIn = () => {
|
export const isLoggedIn = () => {
|
||||||
@@ -24,6 +103,10 @@ export const isLoggedIn = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const saveAuth = (result: LoginResult, phone: string) => {
|
export const saveAuth = (result: LoginResult, phone: string) => {
|
||||||
|
const previousPhone = uni.getStorageSync(phoneKey) || '';
|
||||||
|
if (previousPhone && previousPhone !== phone) {
|
||||||
|
uni.removeStorageSync(positionKey);
|
||||||
|
}
|
||||||
uni.setStorageSync(tokenKey, result.access_token);
|
uni.setStorageSync(tokenKey, result.access_token);
|
||||||
uni.setStorageSync(clientIdKey, result.client_id);
|
uni.setStorageSync(clientIdKey, result.client_id);
|
||||||
uni.setStorageSync(phoneKey, phone);
|
uni.setStorageSync(phoneKey, phone);
|
||||||
@@ -33,6 +116,7 @@ export const clearAuth = () => {
|
|||||||
uni.removeStorageSync(tokenKey);
|
uni.removeStorageSync(tokenKey);
|
||||||
uni.removeStorageSync(clientIdKey);
|
uni.removeStorageSync(clientIdKey);
|
||||||
uni.removeStorageSync(phoneKey);
|
uni.removeStorageSync(phoneKey);
|
||||||
|
uni.removeStorageSync(positionKey);
|
||||||
uni.removeStorageSync(loginRedirectKey);
|
uni.removeStorageSync(loginRedirectKey);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type {
|
|||||||
SummaryCardResponse
|
SummaryCardResponse
|
||||||
} from '@/types/api';
|
} from '@/types/api';
|
||||||
import { apiRequest } from './api';
|
import { apiRequest } from './api';
|
||||||
|
import { getSelectedPosition } from './auth';
|
||||||
|
|
||||||
type RawKnowledgeSnippet = KnowledgeSnippet & {
|
type RawKnowledgeSnippet = KnowledgeSnippet & {
|
||||||
title?: string;
|
title?: string;
|
||||||
@@ -24,6 +25,7 @@ export const searchKnowledge = async (queryText: string) => {
|
|||||||
data: {
|
data: {
|
||||||
queryText,
|
queryText,
|
||||||
category: 'sop',
|
category: 'sop',
|
||||||
|
position: getSelectedPosition(),
|
||||||
limit: 5
|
limit: 5
|
||||||
},
|
},
|
||||||
timeout: 30000
|
timeout: 30000
|
||||||
@@ -47,7 +49,7 @@ export const submitAnswerFeedback = (params: {
|
|||||||
fragmentIds: params.fragmentIds,
|
fragmentIds: params.fragmentIds,
|
||||||
verdict: params.verdict,
|
verdict: params.verdict,
|
||||||
category: 'sop',
|
category: 'sop',
|
||||||
position: '生活顾问',
|
position: getSelectedPosition(),
|
||||||
source: 'mobile_uni_sop'
|
source: 'mobile_uni_sop'
|
||||||
},
|
},
|
||||||
timeout: 15000
|
timeout: 15000
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import type { OrgSnapshotResponse } from '@/types/api';
|
||||||
|
import { apiRequest } from './api';
|
||||||
|
import {
|
||||||
|
getAuth,
|
||||||
|
hasSelectedPosition,
|
||||||
|
normalizePosition,
|
||||||
|
saveSelectedPosition,
|
||||||
|
type UserPosition
|
||||||
|
} from './auth';
|
||||||
|
|
||||||
|
export const userTabPages = new Set([
|
||||||
|
'/pages/user/today/index',
|
||||||
|
'/pages/user/practice/index',
|
||||||
|
'/pages/user/sop/index',
|
||||||
|
'/pages/user/profile/index'
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const isUserRoute = (url: string) => url.startsWith('/pages/user/');
|
||||||
|
|
||||||
|
export const positionSetupUrl = (redirect: string) =>
|
||||||
|
`/pages/auth/position/index?redirect=${encodeURIComponent(redirect || '/pages/user/today/index')}`;
|
||||||
|
|
||||||
|
export const goToMobileTarget = (url: string) => {
|
||||||
|
if (userTabPages.has(url)) {
|
||||||
|
uni.switchTab({ url });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
uni.redirectTo({ url });
|
||||||
|
};
|
||||||
|
|
||||||
|
export const hydratePositionFromOrg = async (phone = getAuth().phone): Promise<UserPosition | ''> => {
|
||||||
|
const keyword = phone.trim();
|
||||||
|
if (!keyword) return '';
|
||||||
|
try {
|
||||||
|
const snapshot = await apiRequest<OrgSnapshotResponse>({
|
||||||
|
url: `/api/aihr/org/snapshot?keyword=${encodeURIComponent(keyword)}&status=active&pageSize=1`,
|
||||||
|
method: 'GET',
|
||||||
|
timeout: 15000
|
||||||
|
});
|
||||||
|
const officialPosition = snapshot.rows?.[0]?.positionName || '';
|
||||||
|
return saveSelectedPosition(officialPosition);
|
||||||
|
} catch {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const nextUserRouteAfterLogin = async (redirect: string) => {
|
||||||
|
if (!isUserRoute(redirect) || hasSelectedPosition()) return redirect;
|
||||||
|
const officialPosition = await hydratePositionFromOrg();
|
||||||
|
return normalizePosition(officialPosition) ? redirect : positionSetupUrl(redirect);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ensureEmployeePosition = async (redirect: string) => {
|
||||||
|
const nextRoute = await nextUserRouteAfterLogin(redirect);
|
||||||
|
if (nextRoute === redirect) return true;
|
||||||
|
uni.navigateTo({ url: nextRoute });
|
||||||
|
return false;
|
||||||
|
};
|
||||||
@@ -14,7 +14,7 @@ import type {
|
|||||||
PromotionEvidence
|
PromotionEvidence
|
||||||
} from '@/types/api';
|
} from '@/types/api';
|
||||||
import { apiRequest } from './api';
|
import { apiRequest } from './api';
|
||||||
import { getAuth } from './auth';
|
import { getAuth, getSelectedPosition } from './auth';
|
||||||
|
|
||||||
const defaultScenarioId = 'complaint-water';
|
const defaultScenarioId = 'complaint-water';
|
||||||
|
|
||||||
@@ -40,6 +40,7 @@ export const startPractice = (scenarioId = defaultScenarioId, assignmentId?: num
|
|||||||
data: {
|
data: {
|
||||||
extPartyId: currentExtPartyId(),
|
extPartyId: currentExtPartyId(),
|
||||||
scenarioId,
|
scenarioId,
|
||||||
|
position: getSelectedPosition(),
|
||||||
mode: 'mobile',
|
mode: 'mobile',
|
||||||
assignmentId
|
assignmentId
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -48,6 +48,28 @@ export interface RoleHome {
|
|||||||
todos?: TodoItem[];
|
todos?: TodoItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface OrgPersonRow {
|
||||||
|
id: number;
|
||||||
|
projectCode: string;
|
||||||
|
projectName: string;
|
||||||
|
deptName: string;
|
||||||
|
extPartyId: string;
|
||||||
|
personName: string;
|
||||||
|
positionName: string;
|
||||||
|
positionLevel: string;
|
||||||
|
employmentStatus: string;
|
||||||
|
snapshotDate: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OrgSnapshotResponse {
|
||||||
|
total: number;
|
||||||
|
active: number;
|
||||||
|
departed: number;
|
||||||
|
pageNum: number;
|
||||||
|
pageSize: number;
|
||||||
|
rows: OrgPersonRow[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface KnowledgeSnippet {
|
export interface KnowledgeSnippet {
|
||||||
docTitle: string;
|
docTitle: string;
|
||||||
content: string;
|
content: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user