Files
prop-ai-hr/mobile-uni/src/pages/user/today/index.vue
T

1048 lines
22 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="page today-page employee-page-shell">
<view class="brand-bar">
<view class="brand-left">
<image class="brand-logo" src="/static/brand-logo.png" mode="aspectFit" />
<text class="brand-name">银城员工端</text>
</view>
<view class="brand-bell" @click="openBuilding('消息通知')">
<uni-icons class="bell-icon" type="notification" size="28" color="#16191f" />
<view class="bell-dot" />
</view>
</view>
<view v-if="message" class="status-message">{{ message }}</view>
<view class="mentor-hero">
<view class="hero-copy">
<text class="hero-role">{{ positionProfile.value }}</text>
<text class="hero-title">{{ positionProfile.heroTitle }}</text>
<text class="hero-sub">有问题,随时问老师傅</text>
</view>
<view class="mentor-stage">
<view class="mentor-bubble">
<text>{{ mentorBubbleText }}</text>
</view>
<image class="mentor-image" src="/static/mentor-hifi.png" mode="aspectFit" />
</view>
</view>
<view class="ask-card employee-card">
<view class="voice-panel">
<button class="voice-primary" @click="goSop">
<view class="voice-emblem">
<text class="voice-mark">问</text>
</view>
<view class="voice-copy">
<text class="voice-title">问老师傅</text>
<text class="voice-desc">语音提问,快速解答</text>
</view>
</button>
<view class="voice-divider" />
<button class="voice-secondary" @click="goSop">
<view class="keyboard-mark">
<uni-icons class="keyboard-icon" type="chatboxes" size="24" color="#5d6571" />
</view>
<view class="voice-copy secondary-copy">
<text class="voice-title secondary-title">打字问</text>
<text class="voice-desc">输入问题获得解答</text>
</view>
</button>
</view>
<view class="mentor-chips">
<view class="chip-row">
<button
v-for="(item, index) in quickQuestions"
:key="item"
class="chip"
:class="`chip-${index}`"
@click="ask(item)"
>
<uni-icons
class="chip-icon"
:type="chipIcon(index)"
size="22"
color="#e70014"
/>
<text class="chip-text">{{ item }}</text>
<text class="chip-arrow">›</text>
</button>
</view>
</view>
</view>
<view class="section-card employee-card login-section" v-if="!loggedIn">
<view class="login-copy">
<text class="login-title">登录后查看今日安排</text>
<text class="login-desc">训练任务、复习提醒和成长数据会按你的手机号展示。</text>
</view>
<button class="primary-button login-button" @click="goLogin">去登录</button>
</view>
<view class="section-card employee-card schedule-card" v-else-if="hasSchedule">
<view class="section-head">
<view class="section-title">
<view class="section-bar" />
<text>今日安排</text>
</view>
<button class="section-link" @click="openTool('刻意练习')">
<text>{{ scheduleCountLabel }}</text>
<text class="section-link-arrow">›</text>
</button>
</view>
<view class="schedule-list">
<view class="schedule-row" v-if="primaryScheduleTitle">
<view class="schedule-icon-wrap">
<uni-icons class="schedule-icon" type="compose" size="26" color="#e70014" />
</view>
<view class="schedule-copy">
<text class="schedule-title">{{ primaryScheduleTitle }}</text>
<view class="schedule-meta">
<text class="meta-item"><uni-icons type="calendar" size="15" color="#8a8d93" /> {{ primaryScheduleTime }}</text>
<text class="meta-dot">·</text>
<text class="meta-item"><uni-icons type="location" size="15" color="#8a8d93" /> {{ primarySchedulePlace }}</text>
</view>
<text class="schedule-desc">{{ primaryScheduleDesc }}</text>
</view>
<button class="schedule-action" @click="openPrimarySchedule">开始</button>
</view>
<view class="schedule-row" v-if="home?.review">
<view class="schedule-icon-wrap review-icon">
<uni-icons class="schedule-icon" type="calendar-filled" size="26" color="#d98200" />
</view>
<view class="schedule-copy">
<text class="schedule-title">{{ home?.review?.title }}</text>
<view class="schedule-meta">
<text class="meta-item"><uni-icons type="calendar" size="15" color="#8a8d93" /> 今日</text>
<text class="meta-dot">·</text>
<text class="meta-item"><uni-icons type="location" size="15" color="#8a8d93" /> 线上复习</text>
</view>
<text class="schedule-desc">{{ home?.review?.desc }}</text>
</view>
<button class="schedule-action review-action" @click="ask(home?.review?.title || '岗前SOP复习')">去复习</button>
</view>
</view>
</view>
<view class="section-card employee-card map-card">
<view class="section-head">
<view class="section-title">
<view class="section-bar" />
<text>我的地图</text>
</view>
</view>
<view class="map-grid">
<button
v-for="tile in mapTiles"
:key="tile.title"
class="map-tile"
:class="tile.tone"
@click="openTool(tile.title)"
>
<view class="map-symbol" :class="tile.tone">
<text>{{ tile.shortTitle }}</text>
</view>
<view class="map-copy">
<text class="map-title">{{ tile.title }}</text>
<text class="map-desc">{{ tile.desc }}</text>
</view>
<text class="map-arrow">›</text>
</button>
</view>
<button class="tip-card" @click="ask(primaryTipQuestion)">
<uni-icons class="tip-mark" type="help" size="22" color="#d98200" />
<text class="tip-label">小贴士</text>
<text class="tip-divider">|</text>
<text class="tip-text">{{ tipBody }}</text>
<text class="tip-arrow">›</text>
</button>
</view>
</view>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import { onShow } from '@dcloudio/uni-app';
import type { PracticeAssignment, RoleHome } from '@/types/api';
import { getMobileHome } from '@/services/home';
import { getPracticeAssignments } from '@/services/practice';
import { 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 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';
const chipIcons = ['chatbubble', 'help', 'wallet'];
const chipIcon = (index: number) => chipIcons[index] || 'chatbubble';
const mapTiles = computed(() => [
{
title: '问老师傅',
shortTitle: '问',
desc: '随时提问\n快速解决问题',
tone: 'ask'
},
{
title: '刻意练习',
shortTitle: '练',
desc: '情景演练\n提升服务能力',
tone: 'practice'
},
{
title: '学习中心',
shortTitle: '学',
desc: '课程学习\n掌握专业知识',
tone: 'learn'
},
{
title: '工作上报',
shortTitle: '报',
desc: '日报/任务/问题\n及时上报反馈',
tone: 'report'
}
]);
const primaryAssignment = computed(() =>
assignments.value.find((item) => item.status === '待训练' && item.source !== 'daily')
|| assignments.value.find((item) => item.status === '待训练')
|| null
);
const hasSchedule = computed(() => Boolean(primaryAssignment.value || home.value?.primary || home.value?.review));
const scheduleCount = computed(() => {
let count = 0;
if (primaryAssignment.value || home.value?.primary) count += 1;
if (home.value?.review) count += 1;
return count;
});
const scheduleCountLabel = computed(() => `${scheduleCount.value || 1} 项任务`);
const primaryScheduleTitle = computed(() => {
const item = primaryAssignment.value;
if (item) {
return item.source === 'daily'
? (item.question || item.scenarioName || '每日三题')
: (item.scenarioName || item.question || '专项训练');
}
if (home.value?.primary?.title) {
return home.value.primary.title.replace('三角色对练', '').replace('重点训练', '专项练习').trim() || '场景练习';
}
return '';
});
const primaryScheduleDesc = computed(() => {
const item = primaryAssignment.value;
if (item) {
if (item.source === 'daily') return '提交后显示反馈与参考答案';
return item.reason || '学习投诉处理全流程与沟通话术';
}
return home.value?.primary?.desc || '进入练习页查看今日任务';
});
const primaryScheduleTime = computed(() => {
const item = primaryAssignment.value;
if (item?.source === 'daily') return '今日';
return '09:30-11:30';
});
const primarySchedulePlace = computed(() => {
const item = primaryAssignment.value;
if (item?.source === 'daily') return '线上答题';
return '线上学习';
});
const tipBody = computed(() => {
const tip = positionProfile.value.tip || '';
return tip.replace(/^今日小贴士[::]\s*/, '').replace(/^小贴士[::]\s*/, '') || tip;
});
const mentorBubbleText = computed(() => {
if (!loggedIn.value) return '登录后看今天任务';
if (primaryScheduleTitle.value) {
const topic = primaryScheduleTitle.value.replace(/专项|训练|练习/g, '').trim();
if (topic) return `今天先把${topic}练熟`;
}
return '今天先把投诉处理练熟';
});
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)
]);
if (homeResult.status === 'fulfilled') {
home.value = homeResult.value;
} else {
home.value = null;
message.value = homeResult.reason instanceof Error ? homeResult.reason.message : '首页数据加载失败';
}
if (assignmentResult.status === 'fulfilled') {
assignments.value = assignmentResult.value;
} else {
loggedIn.value = isLoggedIn();
if (!loggedIn.value) {
home.value = null;
message.value = '登录已过期,请重新登录';
return;
}
message.value = assignmentResult.reason instanceof Error ? assignmentResult.reason.message : '今日任务加载失败';
}
};
const goSop = () => {
uni.switchTab({ url: '/pages/user/sop/index' });
};
const ask = (query: string) => {
uni.setStorageSync(pendingSopQueryKey, query);
uni.switchTab({ url: '/pages/user/sop/index' });
};
const goLogin = () => {
rememberLoginRedirect('/pages/user/today/index');
uni.navigateTo({ url: '/pages/auth/login/index' });
};
const openPrimarySchedule = () => {
if (primaryAssignment.value) {
uni.setStorageSync(pendingAssignmentKey, String(primaryAssignment.value.id));
}
uni.switchTab({ url: '/pages/user/practice/index' });
};
const openBuilding = (title: string) => {
uni.navigateTo({ url: `/pages/common/building/index?title=${encodeURIComponent(title)}` });
};
const openTool = (title: string) => {
if (title.includes('问') || title.includes('SOP') || title.includes('sop')) {
goSop();
return;
}
if (title.includes('练') || title.includes('训练') || title.includes('开始')) {
uni.switchTab({ url: '/pages/user/practice/index' });
return;
}
if (title.includes('学') || title.includes('案例学习')) {
uni.navigateTo({ url: '/pages/user/learning/index' });
return;
}
if (title.includes('案例') || title.includes('素材') || title.includes('提交')) {
uni.navigateTo({ url: '/pages/user/cases/index' });
return;
}
openBuilding(title);
};
onShow(() => {
resetPageScroll();
void loadHome();
});
</script>
<style scoped>
.today-page {
min-height: 100vh;
padding: 0 24px 112px;
background: linear-gradient(180deg, #ffffff 0%, #fbfbfb 62%, #f6f6f6 100%);
}
.brand-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: calc(env(safe-area-inset-top) + 20px) 2px 10px;
}
.brand-left {
display: flex;
align-items: center;
gap: 10px;
}
.brand-logo {
width: 34px;
height: 34px;
}
.brand-name {
color: #1a1a1a;
font-size: 20px;
font-weight: 800;
letter-spacing: 0.02em;
}
.brand-bell {
position: relative;
display: flex;
width: 42px;
height: 42px;
align-items: center;
justify-content: center;
}
.bell-icon {
line-height: 1;
}
.bell-dot {
position: absolute;
top: 6px;
right: 7px;
width: 8px;
height: 8px;
border: 1.5px solid #f5f5f6;
border-radius: 50%;
background: #e70014;
}
.status-message {
margin: 4px 0 8px;
padding: 10px 12px;
border-radius: 12px;
background: #fff4f4;
color: #c01222;
font-size: 13px;
}
.mentor-hero {
position: relative;
display: flex;
min-height: 232px;
align-items: flex-start;
justify-content: space-between;
margin-top: 2px;
padding: 6px 0 0;
overflow: hidden;
}
.hero-copy {
position: relative;
z-index: 2;
display: flex;
max-width: 45%;
flex-direction: column;
padding-top: 11px;
}
.hero-role,
.hero-title {
color: #111111;
font-size: 42px;
font-weight: 900;
line-height: 1.12;
letter-spacing: 0;
}
.hero-sub {
margin-top: 18px;
color: #8a8d93;
font-size: 17px;
white-space: nowrap;
line-height: 1.4;
}
.mentor-stage {
position: relative;
z-index: 1;
width: 240px;
height: 227px;
flex: none;
}
.mentor-image {
position: absolute;
right: -20px;
bottom: 0;
width: 240px;
height: 240px;
transform: translateY(50px);
filter: drop-shadow(0 10px 16px rgba(20, 24, 32, 0.12));
}
.mentor-bubble {
position: absolute;
top: 38px;
right: 130px;
z-index: 3;
width: 112px;
padding: 10px 12px;
border-radius: 18px;
background: #ffffff;
box-shadow: 0 8px 20px rgba(20, 24, 32, 0.1);
}
.mentor-bubble::after {
position: absolute;
right: -6px;
bottom: 18px;
width: 12px;
height: 12px;
border-radius: 2px;
background: #ffffff;
content: '';
transform: rotate(45deg);
}
.mentor-bubble text {
color: #33363c;
font-size: 15px;
font-weight: 600;
line-height: 1.45;
}
.ask-card {
position: relative;
z-index: 4;
margin-top: 0;
padding: 16px 14px 25px;
border: 0;
border-radius: 20px;
background: #ffffff;
box-shadow: 0 10px 28px rgba(24, 28, 36, 0.06);
}
.voice-panel {
display: flex;
align-items: center;
min-height: 82px;
gap: 2px;
transform: translateY(7px);
}
.voice-primary,
.voice-secondary {
display: flex;
min-width: 0;
align-items: center;
margin: 0;
border: 0;
border-radius: 0;
background: transparent;
box-shadow: none;
text-align: left;
}
.voice-primary::after,
.voice-secondary::after {
border: 0;
}
.voice-primary {
flex: 1.7;
gap: 20px;
padding: 0 8px 0 14px;
}
.voice-secondary {
flex: 1;
gap: 10px;
padding: 0 2px 0 16px;
}
.voice-divider {
width: 1px;
height: 42px;
flex: none;
background: #ececee;
}
.voice-emblem {
display: flex;
width: 80px;
height: 80px;
flex: none;
align-items: center;
justify-content: center;
border-radius: 50%;
background: #e70014;
box-shadow: 0 0 0 8px #fff1f2, 0 8px 16px rgba(231, 0, 20, 0.22);
}
.voice-mark {
color: #ffffff;
font-size: 24px;
font-weight: 900;
line-height: 1;
}
.voice-copy {
display: flex;
min-width: 0;
flex-direction: column;
gap: 4px;
}
.voice-title {
color: #1a1a1a;
font-size: 18px;
font-weight: 800;
line-height: 1.2;
white-space: nowrap;
}
.secondary-title {
font-size: 18px;
}
.voice-desc {
color: #9a9da3;
font-size: 12px;
font-weight: 500;
line-height: 1.3;
white-space: nowrap;
}
.secondary-copy {
max-width: 108px;
}
.secondary-copy .voice-desc {
width: 96px;
white-space: normal;
line-height: 1.45;
}
.keyboard-mark {
display: flex;
width: 40px;
height: 40px;
flex: none;
align-items: center;
justify-content: center;
border-radius: 50%;
background: #f3f4f6;
}
.keyboard-icon {
color: #6b7280;
line-height: 1;
}
.mentor-chips {
margin-top: 14px;
padding-top: 12px;
border-top: 1px solid #f0f1f3;
}
.chip-row {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 8px;
padding: 0 10px;
}
.chip {
display: flex;
width: 100%;
min-height: 40px;
align-items: center;
justify-content: center;
gap: 4px;
padding: 0 6px;
border: 1px solid #ececee;
border-radius: 12px;
background: #ffffff;
color: #e70014;
font-size: 13px;
font-weight: 700;
}
.chip-icon {
font-size: 13px;
line-height: 1;
}
.chip-text {
max-width: 4.5em;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.chip-arrow {
color: #f0717c;
font-size: 14px;
font-weight: 500;
}
.section-card {
margin-top: 12px;
padding: 22px 18px;
border: 0;
border-radius: 22px;
background: #ffffff;
box-shadow: 0 8px 24px rgba(24, 28, 36, 0.05);
}
.login-section {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.login-copy {
min-width: 0;
flex: 1;
}
.login-title,
.login-desc {
display: block;
}
.login-title {
color: #1a1a1a;
font-size: 16px;
font-weight: 800;
}
.login-desc {
margin-top: 6px;
color: #8a8d93;
font-size: 13px;
line-height: 1.45;
}
.login-button {
width: 88px;
min-width: 88px;
min-height: 42px;
flex: 0 0 88px;
border-radius: 12px;
background: #e70014;
color: #ffffff;
font-size: 15px;
font-weight: 800;
box-shadow: 0 8px 16px rgba(231, 0, 20, 0.18);
}
.section-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.section-title {
display: flex;
align-items: center;
gap: 8px;
color: #1a1a1a;
font-size: 22px;
font-weight: 800;
}
.section-bar {
width: 4px;
height: 22px;
border-radius: 2px;
background: #e70014;
}
.section-link {
display: flex;
align-items: center;
gap: 2px;
color: #9a9da3;
font-size: 13px;
font-weight: 600;
background: transparent;
}
.section-link-arrow {
font-size: 15px;
color: #b0b3b8;
}
.schedule-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.schedule-row {
display: flex;
align-items: center;
gap: 16px;
padding: 4px 0;
}
.schedule-icon-wrap {
display: flex;
width: 60px;
height: 60px;
flex: none;
align-items: center;
justify-content: center;
border-radius: 50%;
background: #fff1f1;
}
.review-icon {
background: #fff7eb;
}
.schedule-icon {
line-height: 1;
}
.schedule-copy {
display: flex;
min-width: 0;
flex: 1;
flex-direction: column;
gap: 4px;
}
.schedule-title {
color: #1a1a1a;
font-size: 19px;
font-weight: 800;
line-height: 1.25;
}
.schedule-meta {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 4px;
color: #9a9da3;
font-size: 14px;
}
.meta-dot {
color: #c5c7cb;
}
.schedule-desc {
color: #8a8d93;
font-size: 14px;
line-height: 1.4;
}
.schedule-action {
display: flex;
width: 86px;
min-height: 52px;
flex: none;
align-items: center;
justify-content: center;
border-radius: 12px;
background: #e70014;
box-shadow: 0 8px 16px rgba(231, 0, 20, 0.18);
color: #ffffff;
font-size: 18px;
font-weight: 800;
}
.review-action {
background: #d98200;
box-shadow: 0 8px 16px rgba(217, 130, 0, 0.16);
}
.map-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.map-tile {
display: flex;
min-height: 108px;
align-items: center;
gap: 10px;
padding: 16px 14px;
border: 1px solid #f0f1f3;
border-radius: 16px;
background: #ffffff;
text-align: left;
overflow: hidden;
}
.map-symbol {
display: flex;
width: 56px;
height: 56px;
flex: none;
align-items: center;
justify-content: center;
border-radius: 50%;
color: #ffffff;
font-size: 24px;
font-weight: 900;
}
.map-symbol.ask {
background: #e70014;
box-shadow: 0 6px 12px rgba(231, 0, 20, 0.2);
}
.map-symbol.practice {
background: #1f9a4a;
box-shadow: 0 6px 12px rgba(31, 154, 74, 0.2);
}
.map-symbol.learn {
background: #e8900c;
box-shadow: 0 6px 12px rgba(232, 144, 12, 0.2);
}
.map-symbol.report {
background: #4b5563;
box-shadow: 0 6px 12px rgba(75, 85, 99, 0.18);
}
.map-copy {
display: flex;
min-width: 0;
flex: 1;
flex-direction: column;
gap: 2px;
}
.map-title {
color: #1a1a1a;
font-size: 18px;
font-weight: 800;
line-height: 1.2;
}
.map-desc {
color: #9a9da3;
font-size: 13px;
line-height: 1.35;
white-space: pre-line;
}
.map-arrow {
color: #c5c7cb;
font-size: 18px;
flex: none;
}
.tip-card {
display: flex;
margin-top: 18px;
min-height: 58px;
align-items: center;
gap: 6px;
padding: 14px 16px;
border: 0;
border-radius: 14px;
background: #fff6e8;
box-shadow: none;
text-align: left;
}
.tip-mark {
flex: none;
line-height: 1;
}
.tip-label {
flex: none;
color: #b07000;
font-size: 15px;
font-weight: 800;
}
.tip-divider {
flex: none;
margin: 0 2px;
color: #e0c08a;
font-size: 14px;
}
.tip-text {
min-width: 0;
flex: 1;
overflow: hidden;
color: #8a5a12;
font-size: 12px;
font-weight: 600;
line-height: 1.4;
text-overflow: ellipsis;
white-space: nowrap;
}
.tip-arrow {
flex: none;
color: #c9a46a;
font-size: 16px;
}
@media (max-width: 360px) {
.hero-role,
.hero-title {
font-size: 28px;
}
.mentor-stage {
width: 140px;
height: 150px;
}
.mentor-image {
width: 128px;
height: 150px;
}
.mentor-bubble {
right: 92px;
width: 96px;
padding: 8px 10px;
}
.voice-title {
font-size: 16px;
}
.secondary-title {
font-size: 14px;
}
.voice-desc {
font-size: 11px;
}
.chip-text {
max-width: 3.8em;
font-size: 12px;
}
}
@media (max-width: 400px) {
.chip-row {
padding-right: 0;
padding-left: 0;
}
}
</style>