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()) {
|
||||
String like = "%" + keywordValue + "%";
|
||||
where.append("""
|
||||
and (person_name like ? or ext_party_id like ? or position_name like ?
|
||||
or dept_name like ? or project_name like ?)
|
||||
and (person_name like ? or ext_party_id like ? or person_phone 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);
|
||||
}
|
||||
String project = clean(projectCode);
|
||||
if (!project.isBlank()) {
|
||||
|
||||
@@ -37,6 +37,12 @@
|
||||
"navigationBarTitleText": "登录"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/auth/position/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "确认岗位"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/common/building/index",
|
||||
"style": {
|
||||
|
||||
@@ -41,6 +41,7 @@ import type { LoginResult } from '@/types/api';
|
||||
import { apiRequest } from '@/services/api';
|
||||
import { consumeLoginRedirect, getAuth, isLoggedIn, phoneKey, saveAuth, validatePhone } from '@/services/auth';
|
||||
import { resetPageScroll } from '@/services/navigation';
|
||||
import { goToMobileTarget, nextUserRouteAfterLogin } from '@/services/position';
|
||||
|
||||
const stored = getAuth();
|
||||
const phone = ref(stored.phone || uni.getStorageSync(phoneKey) || '');
|
||||
@@ -48,20 +49,9 @@ const smsCode = ref('');
|
||||
const sending = ref(false);
|
||||
const loggingIn = ref(false);
|
||||
const message = ref('');
|
||||
const tabPages = new Set([
|
||||
'/pages/user/today/index',
|
||||
'/pages/user/practice/index',
|
||||
'/pages/user/sop/index',
|
||||
'/pages/user/profile/index'
|
||||
]);
|
||||
|
||||
const goAfterLogin = () => {
|
||||
const goAfterLogin = async () => {
|
||||
const redirect = consumeLoginRedirect() || '/pages/user/today/index';
|
||||
if (tabPages.has(redirect)) {
|
||||
uni.switchTab({ url: redirect });
|
||||
return;
|
||||
}
|
||||
uni.redirectTo({ url: redirect });
|
||||
goToMobileTarget(await nextUserRouteAfterLogin(redirect));
|
||||
};
|
||||
|
||||
const sendCode = async () => {
|
||||
@@ -115,7 +105,7 @@ const login = async () => {
|
||||
throw new Error('登录响应缺少认证信息');
|
||||
}
|
||||
saveAuth(result, value);
|
||||
goAfterLogin();
|
||||
await goAfterLogin();
|
||||
} catch (error) {
|
||||
message.value = error instanceof Error ? error.message : '登录失败';
|
||||
} finally {
|
||||
@@ -126,7 +116,7 @@ const login = async () => {
|
||||
onShow(() => {
|
||||
resetPageScroll();
|
||||
if (isLoggedIn()) {
|
||||
goAfterLogin();
|
||||
void goAfterLogin();
|
||||
}
|
||||
});
|
||||
</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 { searchKnowledge } from '@/services/knowledge';
|
||||
import { resetPageScroll } from '@/services/navigation';
|
||||
import { ensureEmployeePosition } from '@/services/position';
|
||||
import { chooseSpeechAudio, transcribeSpeechBlob, transcribeSpeechFile } from '@/services/speech';
|
||||
|
||||
type PracticeStatus = 'idle' | 'starting' | 'active' | 'submitting' | 'finished';
|
||||
@@ -675,11 +676,18 @@ const finish = async (force = false) => {
|
||||
}
|
||||
};
|
||||
|
||||
onShow(() => {
|
||||
const handleShow = async () => {
|
||||
resetPageScroll();
|
||||
refreshAuthState();
|
||||
if (loggedIn.value && !(await ensureEmployeePosition('/pages/user/practice/index'))) {
|
||||
return;
|
||||
}
|
||||
void loadAssignments();
|
||||
void loadPrepCard();
|
||||
};
|
||||
|
||||
onShow(() => {
|
||||
void handleShow();
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -14,6 +14,26 @@
|
||||
<button v-else class="secondary-button account-action" @click="logout">退出</button>
|
||||
</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">
|
||||
<text class="panel-title">成长摘要</text>
|
||||
<view class="score-row">
|
||||
@@ -117,9 +137,20 @@
|
||||
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 {
|
||||
clearAuth,
|
||||
getAuth,
|
||||
getPositionProfile,
|
||||
getSelectedPositionProfile,
|
||||
isLoggedIn,
|
||||
rememberLoginRedirect,
|
||||
saveSelectedPosition,
|
||||
userPositionOptions,
|
||||
type UserPosition
|
||||
} from '@/services/auth';
|
||||
import { getCompetencyProfile, getPracticeHistory, getPromotionEvidence } from '@/services/practice';
|
||||
import { resetPageScroll } from '@/services/navigation';
|
||||
import { ensureEmployeePosition } from '@/services/position';
|
||||
|
||||
const loggedIn = ref(false);
|
||||
const phone = ref('');
|
||||
@@ -128,11 +159,14 @@ const message = ref('');
|
||||
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 refreshAuth = () => {
|
||||
const auth = getAuth();
|
||||
loggedIn.value = isLoggedIn();
|
||||
phone.value = auth.phone;
|
||||
positionProfile.value = getSelectedPositionProfile();
|
||||
};
|
||||
|
||||
const clearProfileData = () => {
|
||||
@@ -147,6 +181,10 @@ const loadData = async () => {
|
||||
clearProfileData();
|
||||
return;
|
||||
}
|
||||
if (!(await ensureEmployeePosition('/pages/user/profile/index'))) {
|
||||
return;
|
||||
}
|
||||
refreshAuth();
|
||||
loading.value = true;
|
||||
message.value = '';
|
||||
try {
|
||||
@@ -183,6 +221,13 @@ const logout = () => {
|
||||
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(() => {
|
||||
resetPageScroll();
|
||||
void loadData();
|
||||
@@ -214,6 +259,48 @@ onShow(() => {
|
||||
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 {
|
||||
display: grid;
|
||||
margin-top: 16px;
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
v-model="query"
|
||||
class="textarea"
|
||||
maxlength="200"
|
||||
placeholder="例如: 业主投诉噪音,生活顾问应该怎么处理?"
|
||||
:placeholder="quickQuestionPlaceholder"
|
||||
/>
|
||||
<view class="chip-row">
|
||||
<button v-for="item in quickQuestions" :key="item" class="chip" :disabled="loading" @click="useQuickQuestion(item)">
|
||||
@@ -107,7 +107,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { computed, ref } from 'vue';
|
||||
import { onShow } from '@dcloudio/uni-app';
|
||||
import type {
|
||||
AnswerFeedbackVerdict,
|
||||
@@ -119,10 +119,13 @@ import {
|
||||
searchKnowledge,
|
||||
submitAnswerFeedback
|
||||
} from '@/services/knowledge';
|
||||
import { isLoggedIn, rememberLoginRedirect } from '@/services/auth';
|
||||
import { getSelectedPositionProfile, isLoggedIn, rememberLoginRedirect } from '@/services/auth';
|
||||
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 result = ref<KnowledgeSearchResponse | null>(null);
|
||||
const summaryCard = ref<SummaryCardResponse | null>(null);
|
||||
@@ -143,6 +146,7 @@ const resetResultState = () => {
|
||||
|
||||
const refreshAuthState = () => {
|
||||
loggedIn.value = isLoggedIn();
|
||||
positionProfile.value = getSelectedPositionProfile();
|
||||
};
|
||||
|
||||
const handleRequestError = (error: unknown, fallback: string, wasLoggedIn = loggedIn.value) => {
|
||||
@@ -169,6 +173,10 @@ const search = async () => {
|
||||
goLogin();
|
||||
return;
|
||||
}
|
||||
if (!(await ensureEmployeePosition('/pages/user/sop/index'))) {
|
||||
return;
|
||||
}
|
||||
positionProfile.value = getSelectedPositionProfile();
|
||||
loading.value = true;
|
||||
message.value = '';
|
||||
resetResultState();
|
||||
@@ -256,9 +264,13 @@ const goLogin = () => {
|
||||
uni.navigateTo({ url: '/pages/auth/login/index' });
|
||||
};
|
||||
|
||||
onShow(() => {
|
||||
const handleShow = async () => {
|
||||
resetPageScroll();
|
||||
refreshAuthState();
|
||||
if (loggedIn.value && !(await ensureEmployeePosition('/pages/user/sop/index'))) {
|
||||
return;
|
||||
}
|
||||
positionProfile.value = getSelectedPositionProfile();
|
||||
if (!loggedIn.value) {
|
||||
resetResultState();
|
||||
}
|
||||
@@ -276,6 +288,10 @@ onShow(() => {
|
||||
} else {
|
||||
message.value = '登录后继续查询该问题';
|
||||
}
|
||||
};
|
||||
|
||||
onShow(() => {
|
||||
void handleShow();
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
|
||||
<view class="mentor-hero">
|
||||
<view class="hero-copy">
|
||||
<text class="hero-role">生活顾问</text>
|
||||
<text class="hero-title">客服管家</text>
|
||||
<text class="hero-role">{{ positionProfile.value }}</text>
|
||||
<text class="hero-title">{{ positionProfile.heroTitle }}</text>
|
||||
<text class="hero-sub">有问题,随时问老师傅</text>
|
||||
</view>
|
||||
<image class="mentor-image" src="/static/mentor.png" mode="aspectFit" />
|
||||
@@ -95,9 +95,9 @@
|
||||
<view v-else class="empty">暂无工具入口</view>
|
||||
</view>
|
||||
|
||||
<button class="tip-card" @click="ask('催费话术')">
|
||||
<button class="tip-card" @click="ask(primaryTipQuestion)">
|
||||
<text class="tip-mark">贴</text>
|
||||
<text>今日小贴士:催费沟通先共情,再说明,再给方案。</text>
|
||||
<text>{{ positionProfile.tip }}</text>
|
||||
<text class="tip-arrow">›</text>
|
||||
</button>
|
||||
</view>
|
||||
@@ -109,14 +109,17 @@ import { onShow } from '@dcloudio/uni-app';
|
||||
import type { PracticeAssignment, RoleHome } from '@/types/api';
|
||||
import { getMobileHome } from '@/services/home';
|
||||
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 { ensureEmployeePosition } from '@/services/position';
|
||||
|
||||
const home = ref<RoleHome | null>(null);
|
||||
const assignments = ref<PracticeAssignment[]>([]);
|
||||
const message = ref('');
|
||||
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 pendingAssignmentKey = 'aihr_pending_assignment_id';
|
||||
|
||||
@@ -176,12 +179,17 @@ const mentorBubbleText = computed(() =>
|
||||
|
||||
const loadHome = async () => {
|
||||
loggedIn.value = isLoggedIn();
|
||||
positionProfile.value = getSelectedPositionProfile();
|
||||
message.value = '';
|
||||
assignments.value = [];
|
||||
if (!loggedIn.value) {
|
||||
home.value = null;
|
||||
return;
|
||||
}
|
||||
if (!(await ensureEmployeePosition('/pages/user/today/index'))) {
|
||||
return;
|
||||
}
|
||||
positionProfile.value = getSelectedPositionProfile();
|
||||
const [homeResult, assignmentResult] = await Promise.allSettled([
|
||||
getMobileHome('user'),
|
||||
getPracticeAssignments(true)
|
||||
|
||||
@@ -4,18 +4,97 @@ export const tokenKey = 'aihr_mobile_access_token';
|
||||
export const clientIdKey = 'aihr_mobile_client_id';
|
||||
export const phoneKey = 'aihr_mobile_phone';
|
||||
export const loginRedirectKey = 'aihr_mobile_login_redirect';
|
||||
export const positionKey = 'aihr_mobile_position';
|
||||
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 {
|
||||
token: string;
|
||||
clientId: 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 => ({
|
||||
token: uni.getStorageSync(tokenKey) || '',
|
||||
clientId: uni.getStorageSync(clientIdKey) || '',
|
||||
phone: uni.getStorageSync(phoneKey) || ''
|
||||
phone: uni.getStorageSync(phoneKey) || '',
|
||||
position: normalizePosition(uni.getStorageSync(positionKey))
|
||||
});
|
||||
|
||||
export const isLoggedIn = () => {
|
||||
@@ -24,6 +103,10 @@ export const isLoggedIn = () => {
|
||||
};
|
||||
|
||||
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(clientIdKey, result.client_id);
|
||||
uni.setStorageSync(phoneKey, phone);
|
||||
@@ -33,6 +116,7 @@ export const clearAuth = () => {
|
||||
uni.removeStorageSync(tokenKey);
|
||||
uni.removeStorageSync(clientIdKey);
|
||||
uni.removeStorageSync(phoneKey);
|
||||
uni.removeStorageSync(positionKey);
|
||||
uni.removeStorageSync(loginRedirectKey);
|
||||
};
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import type {
|
||||
SummaryCardResponse
|
||||
} from '@/types/api';
|
||||
import { apiRequest } from './api';
|
||||
import { getSelectedPosition } from './auth';
|
||||
|
||||
type RawKnowledgeSnippet = KnowledgeSnippet & {
|
||||
title?: string;
|
||||
@@ -24,6 +25,7 @@ export const searchKnowledge = async (queryText: string) => {
|
||||
data: {
|
||||
queryText,
|
||||
category: 'sop',
|
||||
position: getSelectedPosition(),
|
||||
limit: 5
|
||||
},
|
||||
timeout: 30000
|
||||
@@ -47,7 +49,7 @@ export const submitAnswerFeedback = (params: {
|
||||
fragmentIds: params.fragmentIds,
|
||||
verdict: params.verdict,
|
||||
category: 'sop',
|
||||
position: '生活顾问',
|
||||
position: getSelectedPosition(),
|
||||
source: 'mobile_uni_sop'
|
||||
},
|
||||
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
|
||||
} from '@/types/api';
|
||||
import { apiRequest } from './api';
|
||||
import { getAuth } from './auth';
|
||||
import { getAuth, getSelectedPosition } from './auth';
|
||||
|
||||
const defaultScenarioId = 'complaint-water';
|
||||
|
||||
@@ -40,6 +40,7 @@ export const startPractice = (scenarioId = defaultScenarioId, assignmentId?: num
|
||||
data: {
|
||||
extPartyId: currentExtPartyId(),
|
||||
scenarioId,
|
||||
position: getSelectedPosition(),
|
||||
mode: 'mobile',
|
||||
assignmentId
|
||||
},
|
||||
|
||||
@@ -48,6 +48,28 @@ export interface RoleHome {
|
||||
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 {
|
||||
docTitle: string;
|
||||
content: string;
|
||||
|
||||
Reference in New Issue
Block a user