fix(aihr): close Figma UI regression gaps

This commit is contained in:
2026-07-17 04:17:14 +08:00
parent a822b22116
commit e93421ca1e
35 changed files with 1452 additions and 107 deletions
+41 -1
View File
@@ -101,7 +101,11 @@
</view>
</view>
</view>
<view v-else class="empty">暂无入库案例</view>
<view v-else class="case-empty-state">
<view class="case-empty-icon">案</view>
<text class="case-empty-title">暂时还没有可学习案例</text>
<text class="case-empty-desc">案例经主管整理并入库后会出现在这里;提交权限由组织角色决定。</text>
</view>
</view>
<view v-if="caseDetail" class="panel case-detail-panel">
@@ -535,4 +539,40 @@ onUnload(clearCaseMediaUrl);
font-weight: 900;
white-space: nowrap;
}
.case-empty-state {
display: flex;
flex-direction: column;
align-items: center;
padding: 28px 16px 20px;
text-align: center;
}
.case-empty-icon {
display: flex;
width: 52px;
height: 52px;
align-items: center;
justify-content: center;
border-radius: 18px;
background: #fff0f1;
color: #e60012;
font-size: 20px;
font-weight: 900;
}
.case-empty-title {
margin-top: 14px;
color: #1f2937;
font-size: 15px;
font-weight: 900;
}
.case-empty-desc {
max-width: 280px;
margin-top: 7px;
color: #6b7280;
font-size: 13px;
line-height: 1.55;
}
</style>
+31 -8
View File
@@ -21,6 +21,7 @@
<template v-else-if="detail">
<view class="panel summary-panel">
<text class="section-kicker">训练摘要</text>
<view class="summary-head">
<view>
<text class="detail-title">{{ detail.scene }}</text>
@@ -33,17 +34,17 @@
<view v-if="detail.scoreItems?.length" class="panel">
<text class="panel-title">评分明细</text>
<view v-for="item in detail.scoreItems" :key="item.label" class="score-row">
<view>
<view class="score-summary-grid">
<view v-for="item in detail.scoreItems" :key="item.label" class="score-row">
<text class="score-label">{{ item.label }}</text>
<text class="score-value">{{ item.value }}</text>
<text class="score-note">{{ item.note }}</text>
</view>
<text class="score-value">{{ item.value }}</text>
</view>
</view>
<view v-if="detail.dialogue?.length" class="panel">
<text class="panel-title">对话记录</text>
<text class="panel-title">录音与逐句复盘</text>
<view v-for="(turn, index) in detail.dialogue" :key="`${turn.role}-${index}`" class="dialogue-row">
<text class="dialogue-label">{{ turn.label }}</text>
<text class="dialogue-text">{{ turn.text }}</text>
@@ -58,7 +59,7 @@
</view>
<view v-if="detail.annotations?.length || detail.mentorRewrite" class="panel">
<text class="panel-title">复盘建议</text>
<text class="panel-title">SOP 依据与改进建议</text>
<view v-for="item in detail.annotations" :key="`${item.turnIndex}-${item.issue}`" class="annotation-row">
<text class="annotation-issue">{{ item.issue }}</text>
<text class="annotation-rewrite">建议:{{ item.rewrite }}</text>
@@ -258,11 +259,24 @@ onUnload(releaseAudio);
border-top: 1px solid #ececee;
}
.score-summary-grid {
display: grid;
margin-top: 10px;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
}
.score-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
min-height: 96px;
align-items: flex-start;
justify-content: center;
flex-direction: column;
gap: 4px;
padding: 12px;
border: 0;
border-radius: 12px;
background: #f7f8fa;
}
.score-note,
@@ -342,4 +356,13 @@ onUnload(releaseAudio);
.empty-panel button {
margin-top: 14px;
}
.section-kicker {
display: block;
margin-bottom: 10px;
color: #e60012;
font-size: 12px;
font-weight: 900;
letter-spacing: 0.08em;
}
</style>
+37 -1
View File
@@ -190,7 +190,7 @@
{{ helpLoading ? '求助中' : '求助' }}
</button>
<button class="secondary-button" :disabled="busy || !canFinishPractice" @click="finish()">结束评分</button>
<button class="primary-button" :disabled="busy" @click="submit">
<button class="primary-button tabbar-safe-action" :disabled="busy" @click="submit">
{{ status === 'submitting' ? '提交中' : '提交回应' }}
</button>
</view>
@@ -209,6 +209,12 @@
<text class="result-label">本次评分</text>
<text class="result-score">{{ result.totalScore }}</text>
<text class="result-comment">{{ result.aiComment }}</text>
<view v-if="result.score?.length" class="result-dimensions">
<view v-for="item in result.score" :key="item.label" class="result-dimension">
<text>{{ item.value }}</text>
<text>{{ item.label }}</text>
</view>
</view>
<view v-if="result.mentorRewrite" class="mentor-rewrite">
<text>老师傅建议</text>
<text>{{ result.mentorRewrite }}</text>
@@ -911,6 +917,7 @@ const finish = async (force = false) => {
message.value = '';
try {
result.value = await finishPractice(sessionId.value, activeAssignmentId.value);
resetPageScroll();
status.value = 'finished';
message.value = '训练完成,主管端待复盘已更新';
activeAssignmentId.value = undefined;
@@ -1464,6 +1471,35 @@ onShow(() => {
line-height: 1.55;
}
.result-dimensions {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
}
.result-dimension {
display: flex;
min-height: 66px;
align-items: center;
justify-content: center;
flex-direction: column;
gap: 5px;
border-radius: 14px;
background: #f7f8fa;
text-align: center;
}
.result-dimension text:first-child {
color: var(--employee-primary);
font-size: 22px;
font-weight: 900;
}
.result-dimension text:last-child {
color: var(--employee-muted);
font-size: 12px;
}
.mentor-rewrite {
display: flex;
flex-direction: column;
+5 -3
View File
@@ -3,7 +3,7 @@
<view class="profile-head">
<text class="profile-eyebrow">银城物业 · 员工成长</text>
<text class="page-title">我的成长</text>
<text class="page-subtitle">{{ phone || '登录后查看训练记录和能力画像' }}</text>
<text class="page-subtitle">{{ displayPhone || '登录后查看训练记录和能力画像' }}</text>
</view>
<view v-if="message" class="status-message">{{ message }}</view>
@@ -11,7 +11,7 @@
<view class="panel account-card">
<view>
<text class="panel-title">账号</text>
<text class="account-phone">{{ phone || '未登录' }}</text>
<text class="account-phone">{{ displayPhone || '未登录' }}</text>
</view>
<button v-if="!loggedIn" class="primary-button account-action" @click="goLogin">去登录</button>
<button v-else class="secondary-button account-action" @click="logout">退出</button>
@@ -153,7 +153,7 @@
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { computed, ref } from 'vue';
import { onShow } from '@dcloudio/uni-app';
import type { CompetencyProfile, PracticeRecord, PromotionEvidence } from '@/types/api';
import {
@@ -162,6 +162,7 @@ import {
getPositionProfile,
getSelectedPositionProfile,
isLoggedIn,
maskPhone,
rememberLoginRedirect,
saveSelectedPosition,
userPositionOptions,
@@ -173,6 +174,7 @@ import { ensureEmployeePosition } from '@/services/position';
const loggedIn = ref(false);
const phone = ref('');
const displayPhone = computed(() => maskPhone(phone.value));
const loading = ref(false);
const message = ref('');
const profile = ref<CompetencyProfile | null>(null);
+130 -5
View File
@@ -84,20 +84,51 @@
<button
class="secondary-button"
:class="{ active: feedbackVerdict === 'up' }"
:disabled="feedbackSubmitting || Boolean(feedbackVerdict)"
:disabled="feedbackSubmitting || Boolean(feedbackVerdict) || showNegativeFeedback"
@click="feedback('up')"
>
有帮助
</button>
<button
class="secondary-button"
:class="{ active: feedbackVerdict === 'down', negative: feedbackVerdict === 'down' }"
:class="{ active: feedbackVerdict === 'down' || showNegativeFeedback, negative: feedbackVerdict === 'down' || showNegativeFeedback }"
:disabled="feedbackSubmitting || Boolean(feedbackVerdict)"
@click="feedback('down')"
@click="openNegativeFeedback"
>
没解决
</button>
</view>
<view v-if="showNegativeFeedback && !feedbackVerdict" class="negative-feedback-panel">
<text class="negative-feedback-title">哪里没有解决?(可多选)</text>
<view class="negative-reason-list">
<button
v-for="item in feedbackReasonOptions"
:key="item.code"
class="negative-reason"
:class="{ active: selectedFeedbackReasons.includes(item.code) }"
:disabled="feedbackSubmitting"
@click="toggleFeedbackReason(item.code)"
>
{{ item.label }}
</button>
</view>
<textarea
v-model="feedbackComment"
class="textarea negative-feedback-comment"
maxlength="500"
placeholder="补充说明(可选)"
/>
<view class="negative-feedback-actions">
<button class="secondary-button" :disabled="feedbackSubmitting" @click="closeNegativeFeedback">取消</button>
<button
class="primary-button"
:disabled="feedbackSubmitting || !selectedFeedbackReasons.length"
@click="submitNegativeFeedback"
>
{{ feedbackSubmitting ? '提交中' : '提交反馈' }}
</button>
</view>
</view>
<text v-if="feedbackVerdict" class="feedback-status">
{{ feedbackVerdict === 'up' ? '已记录为有帮助' : '已提交未解决反馈,后续会继续优化这条答案' }}
</text>
@@ -220,15 +251,28 @@ const loading = ref(false);
const summaryLoading = ref(false);
const feedbackSubmitting = ref(false);
const feedbackVerdict = ref<AnswerFeedbackVerdict | ''>('');
const showNegativeFeedback = ref(false);
const selectedFeedbackReasons = ref<string[]>([]);
const feedbackComment = ref('');
const message = ref('');
const loggedIn = ref(false);
const pendingSopQueryKey = 'aihr_sop_pending_query';
const feedbackReasonOptions = [
{ code: 'no_answer', label: '没有回答问题' },
{ code: 'not_specific', label: '步骤不够具体' },
{ code: 'wrong_reference', label: '引用依据不准确' },
{ code: 'outdated', label: '内容已经过期' },
{ code: 'not_applicable', label: '话术不适合现场' }
];
const resetResultState = () => {
result.value = null;
summaryCard.value = null;
feedbackVerdict.value = '';
feedbackSubmitting.value = false;
showNegativeFeedback.value = false;
selectedFeedbackReasons.value = [];
feedbackComment.value = '';
};
const refreshAuthState = () => {
@@ -271,6 +315,7 @@ const search = async () => {
resetResultState();
try {
result.value = await searchKnowledge(value);
resetPageScroll();
if (!result.value.answer && !result.value.snippets?.length) {
message.value = '未检索到可引用内容';
}
@@ -297,7 +342,10 @@ const cleanOrderedText = (text = '') =>
.replace(/^(?:[一二三四五六七八九十]+[、..]|[0-9]+[.)、..])\s*/, '')
.trim();
const feedback = async (verdict: AnswerFeedbackVerdict) => {
const feedback = async (
verdict: AnswerFeedbackVerdict,
details: { reasonCodes?: string[]; comment?: string } = {}
) => {
if (!result.value || feedbackSubmitting.value || feedbackVerdict.value) return;
const fragmentIds = Array.from(
new Set(
@@ -317,9 +365,12 @@ const feedback = async (verdict: AnswerFeedbackVerdict) => {
queryText: result.value.queryText || query.value,
fragmentIds,
verdict,
reviewId: result.value.reviewId
reviewId: result.value.reviewId,
reasonCodes: details.reasonCodes,
comment: details.comment
});
feedbackVerdict.value = verdict;
showNegativeFeedback.value = false;
uni.showToast({ title: verdict === 'down' ? '已进入复核' : '已记录', icon: 'none' });
} catch (error) {
if (!handleRequestError(error, '反馈失败', true)) {
@@ -333,6 +384,32 @@ const feedback = async (verdict: AnswerFeedbackVerdict) => {
}
};
const openNegativeFeedback = () => {
if (feedbackSubmitting.value || feedbackVerdict.value) return;
showNegativeFeedback.value = true;
selectedFeedbackReasons.value = [];
feedbackComment.value = '';
};
const closeNegativeFeedback = () => {
if (feedbackSubmitting.value) return;
showNegativeFeedback.value = false;
};
const toggleFeedbackReason = (code: string) => {
selectedFeedbackReasons.value = selectedFeedbackReasons.value.includes(code)
? selectedFeedbackReasons.value.filter((item) => item !== code)
: [...selectedFeedbackReasons.value, code];
};
const submitNegativeFeedback = () => {
if (!selectedFeedbackReasons.value.length) return;
void feedback('down', {
reasonCodes: selectedFeedbackReasons.value,
comment: feedbackComment.value.trim()
});
};
const buildSummaryCard = async () => {
if (summaryLoading.value) return;
const value = (result.value?.queryText || query.value).trim();
@@ -624,6 +701,54 @@ onShow(() => {
color: #c34d45;
}
.negative-feedback-panel {
display: flex;
flex-direction: column;
gap: 12px;
padding: 14px;
border-radius: 16px;
background: #fff8f8;
}
.negative-feedback-title {
color: var(--employee-text);
font-size: 15px;
font-weight: 900;
}
.negative-reason-list {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.negative-reason {
min-height: 40px;
padding: 0 10px;
border: 1px solid #f1d7d9;
background: #ffffff;
color: #5c626c;
font-size: 13px;
text-align: left;
}
.negative-reason.active {
border-color: var(--employee-primary);
background: var(--employee-primary-soft);
color: var(--employee-primary);
}
.negative-feedback-comment {
min-height: 84px;
border-color: #f1d7d9;
}
.negative-feedback-actions {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.feedback-status {
display: block;
padding: 10px 12px;
+27 -1
View File
@@ -115,7 +115,7 @@
</button>
</view>
<view class="schedule-row" v-if="home?.review">
<view class="schedule-row review-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>
@@ -917,9 +917,13 @@ onShow(() => {
}
.schedule-desc {
display: -webkit-box;
overflow: hidden;
color: #8a8d93;
font-size: 14px;
line-height: 1.4;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.schedule-action {
@@ -942,6 +946,28 @@ onShow(() => {
box-shadow: 0 8px 16px rgba(217, 130, 0, 0.16);
}
.review-schedule-row {
min-height: 62px;
padding-top: 8px;
padding-bottom: 8px;
}
.review-schedule-row .schedule-icon-wrap {
width: 42px;
height: 42px;
}
.review-schedule-row .schedule-meta,
.review-schedule-row .schedule-desc {
display: none;
}
.review-schedule-row .schedule-action {
width: 70px;
min-height: 40px;
font-size: 14px;
}
.map-grid {
display: grid;
grid-template-columns: 1fr 1fr;