1412 lines
32 KiB
Vue
1412 lines
32 KiB
Vue
<template>
|
||
<view class="page sop-page employee-page-shell">
|
||
<view class="sop-head">
|
||
<text class="employee-kicker sop-eyebrow">SOP · 话术 · 工作流程</text>
|
||
<text class="page-title">问</text>
|
||
<text class="page-subtitle">数字师傅 · SOP问答</text>
|
||
<text class="sop-context">{{ positionLabel }} · 住宅项目</text>
|
||
<text class="toolbar-tip">先把问题问清楚,再按需生成总结卡,不伪造分类能力。</text>
|
||
</view>
|
||
|
||
<view class="panel query-panel employee-card">
|
||
<view class="query-hero">
|
||
<view class="query-copy">
|
||
<text class="query-title">问师傅</text>
|
||
<text class="query-subtitle">直接问工作中的 SOP、话术和流程</text>
|
||
</view>
|
||
<view class="query-visual" aria-hidden="true">
|
||
<view class="query-bubble primary"><text>•••</text></view>
|
||
<view class="query-bubble secondary"><text>•••</text></view>
|
||
</view>
|
||
</view>
|
||
<textarea
|
||
v-model="query"
|
||
class="textarea sop-query-textarea"
|
||
maxlength="200"
|
||
:placeholder="quickQuestionPlaceholder"
|
||
/>
|
||
<text class="query-caption">常见问题可以直接点下面快捷问法,结果会保留引用依据。</text>
|
||
<view class="chip-row">
|
||
<button v-for="item in quickQuestions" :key="item" class="chip" :disabled="loading" @click="useQuickQuestion(item)">
|
||
{{ item }}
|
||
</button>
|
||
</view>
|
||
<button class="primary-button query-submit" :disabled="loading" @click="search">
|
||
{{ loading ? '检索中' : '问老师傅' }}
|
||
</button>
|
||
<text v-if="message" class="status-message">{{ message }}</text>
|
||
</view>
|
||
|
||
<view v-if="loggedIn && !result" class="knowledge-tip-card employee-card">
|
||
<view class="knowledge-tip-icon">⌕</view>
|
||
<view class="knowledge-tip-copy">
|
||
<text class="knowledge-tip-title">知识库引用可追溯</text>
|
||
<text class="knowledge-tip-desc">答案来自标准 SOP、附件来源,查得清楚、用得放心</text>
|
||
</view>
|
||
<text class="knowledge-tip-arrow">›</text>
|
||
</view>
|
||
|
||
<view v-if="loggedIn && !result && !loading" class="answer-empty employee-card">
|
||
<text class="answer-empty-icon">•••</text>
|
||
<text class="answer-empty-title">暂无相关回答</text>
|
||
<text class="answer-empty-desc">换个问题试试吧</text>
|
||
</view>
|
||
|
||
<view v-if="!loggedIn" class="panel login-panel employee-card">
|
||
<view>
|
||
<text class="panel-title">需要登录</text>
|
||
<text class="muted block">登录后才能查询 SOP、生成总结卡并反馈答案。</text>
|
||
</view>
|
||
<button class="primary-button login-button" @click="goLogin">去登录</button>
|
||
</view>
|
||
|
||
<view v-if="result" class="panel answer-panel employee-card">
|
||
<text class="panel-title">师傅回答</text>
|
||
<view class="question-card">
|
||
<text class="question-label">我的问题</text>
|
||
<text class="question-text">{{ query }}</text>
|
||
</view>
|
||
<view class="answer-copy-card">
|
||
<text class="answer-text">{{ result.answer || '未生成直接答案,请查看下方引用。' }}</text>
|
||
</view>
|
||
|
||
<view v-if="result.keyPoints?.length" class="keypoints">
|
||
<text v-for="(point, index) in result.keyPoints" :key="point" class="keypoint">
|
||
{{ index + 1 }}. {{ cleanOrderedText(point) }}
|
||
</text>
|
||
</view>
|
||
|
||
<view v-if="result.cautions?.length" class="cautions">
|
||
<text v-for="item in result.cautions" :key="item">{{ item }}</text>
|
||
</view>
|
||
|
||
<view class="feedback-actions">
|
||
<button
|
||
class="secondary-button"
|
||
:class="{ active: feedbackVerdict === 'up' }"
|
||
:disabled="feedbackSubmitting || Boolean(feedbackVerdict) || showNegativeFeedback"
|
||
@click="feedback('up')"
|
||
>
|
||
有帮助
|
||
</button>
|
||
<button
|
||
class="secondary-button"
|
||
:class="{ active: feedbackVerdict === 'down' || showNegativeFeedback, negative: feedbackVerdict === 'down' || showNegativeFeedback }"
|
||
:disabled="feedbackSubmitting || Boolean(feedbackVerdict)"
|
||
@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>
|
||
</view>
|
||
|
||
<view v-if="result?.snippets?.length" class="panel sources-panel">
|
||
<text class="panel-title">引用依据</text>
|
||
<view v-for="(snippet, index) in result.snippets" :key="`${snippet.docTitle}-${index}`" class="snippet">
|
||
<text class="snippet-title">{{ snippet.docTitle }}</text>
|
||
<text class="snippet-content">{{ snippetExcerpt(snippet.content) }}</text>
|
||
<text v-if="snippet.fragmentId" class="snippet-id">片段 #{{ snippet.fragmentId }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="result" class="panel summary-panel employee-card">
|
||
<text class="panel-title">总结卡</text>
|
||
<text class="muted summary-copy">需要时再生成,适合把一段 SOP 整理成步骤卡、话术卡和提醒卡。</text>
|
||
<button class="ghost-button" :disabled="summaryLoading" @click="buildSummaryCard">
|
||
{{ summaryLoading ? '生成中' : '生成总结卡' }}
|
||
</button>
|
||
</view>
|
||
|
||
<view v-if="summaryCard" class="modal-mask" @click="closeSummary">
|
||
<view class="modal-card" @click.stop>
|
||
<view class="modal-header">
|
||
<text class="modal-title">{{ summaryCard.title }}</text>
|
||
<button class="modal-close" aria-label="关闭总结卡" title="关闭总结卡" @click="closeSummary">×</button>
|
||
</view>
|
||
<scroll-view scroll-y class="modal-body">
|
||
<view v-if="summaryCard.steps?.length" class="info-section">
|
||
<view class="section-title-row">
|
||
<text class="badge-num">1</text>
|
||
<text class="info-section-title">核心步骤</text>
|
||
</view>
|
||
<view class="timeline">
|
||
<view v-for="(step, index) in summaryCard.steps" :key="index" class="timeline-item">
|
||
<view class="timeline-node">{{ index + 1 }}</view>
|
||
<view class="timeline-content">
|
||
<text class="timeline-label">{{ step.label }}</text>
|
||
<text class="timeline-desc">{{ step.desc }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view v-if="summaryCard.scripts?.length" class="info-section speech-section">
|
||
<view class="section-title-row">
|
||
<text class="badge-num">2</text>
|
||
<text class="info-section-title">推荐话术</text>
|
||
</view>
|
||
<view class="speech-bubbles">
|
||
<view v-for="(script, index) in summaryCard.scripts" :key="index" class="speech-bubble">
|
||
<text class="quote-mark">“</text>
|
||
<text class="speech-text">{{ script }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view v-if="summaryCard.objections?.length" class="info-section objection-section">
|
||
<view class="section-title-row">
|
||
<text class="badge-num">3</text>
|
||
<text class="info-section-title">常见异议与回应</text>
|
||
</view>
|
||
<view class="objection-list">
|
||
<view v-for="(obj, index) in summaryCard.objections" :key="index" class="objection-card">
|
||
<view class="objection-q">
|
||
<text class="q-badge">问</text>
|
||
<text class="q-text">{{ obj.concern }}</text>
|
||
</view>
|
||
<view class="objection-a">
|
||
<text class="a-badge">答</text>
|
||
<text class="a-text">{{ obj.response }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view v-if="summaryCard.reminders?.length" class="info-section reminder-section">
|
||
<view class="section-title-row">
|
||
<text class="badge-num">4</text>
|
||
<text class="info-section-title">师傅提醒</text>
|
||
</view>
|
||
<view class="reminder-list">
|
||
<view v-for="(rem, index) in summaryCard.reminders" :key="index" class="reminder-item">
|
||
<text class="reminder-text">{{ rem }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
<view class="modal-footer">
|
||
<button class="primary-button footer-btn" @click="closeSummary">我知道了</button>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { computed, ref } from 'vue';
|
||
import { onShow } from '@dcloudio/uni-app';
|
||
import type {
|
||
AnswerFeedbackVerdict,
|
||
KnowledgeSearchResponse,
|
||
SummaryCardResponse
|
||
} from '@/types/api';
|
||
import {
|
||
generateSummaryCard,
|
||
searchKnowledge,
|
||
submitAnswerFeedback
|
||
} from '@/services/knowledge';
|
||
import { getSelectedPosition, getSelectedPositionProfile, isLoggedIn, rememberLoginRedirect } from '@/services/auth';
|
||
import { resetPageScroll } from '@/services/navigation';
|
||
import { ensureEmployeePosition } from '@/services/position';
|
||
|
||
const positionProfile = ref(getSelectedPositionProfile());
|
||
const positionLabel = ref(getSelectedPosition());
|
||
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);
|
||
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 = () => {
|
||
loggedIn.value = isLoggedIn();
|
||
positionLabel.value = getSelectedPosition();
|
||
positionProfile.value = getSelectedPositionProfile();
|
||
};
|
||
|
||
const handleRequestError = (error: unknown, fallback: string, wasLoggedIn = loggedIn.value) => {
|
||
refreshAuthState();
|
||
if (wasLoggedIn && !loggedIn.value) {
|
||
resetResultState();
|
||
message.value = '登录已过期,请重新登录';
|
||
return true;
|
||
}
|
||
message.value = error instanceof Error ? error.message : fallback;
|
||
return false;
|
||
};
|
||
|
||
const search = async () => {
|
||
if (loading.value) return;
|
||
const value = query.value.trim();
|
||
if (!value) {
|
||
message.value = '请先输入问题';
|
||
return;
|
||
}
|
||
refreshAuthState();
|
||
if (!loggedIn.value) {
|
||
uni.setStorageSync(pendingSopQueryKey, value);
|
||
goLogin();
|
||
return;
|
||
}
|
||
if (!(await ensureEmployeePosition('/pages/user/sop/index'))) {
|
||
return;
|
||
}
|
||
positionProfile.value = getSelectedPositionProfile();
|
||
positionLabel.value = getSelectedPosition();
|
||
loading.value = true;
|
||
message.value = '';
|
||
resetResultState();
|
||
try {
|
||
result.value = await searchKnowledge(value);
|
||
resetPageScroll();
|
||
if (!result.value.answer && !result.value.snippets?.length) {
|
||
message.value = '未检索到可引用内容';
|
||
}
|
||
} catch (error) {
|
||
handleRequestError(error, '检索失败', true);
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
};
|
||
|
||
const useQuickQuestion = (value: string) => {
|
||
query.value = value;
|
||
void search();
|
||
};
|
||
|
||
const snippetExcerpt = (text = '') => {
|
||
const value = text.trim();
|
||
return value.length > 260 ? `${value.slice(0, 260)}...` : value;
|
||
};
|
||
|
||
const cleanOrderedText = (text = '') =>
|
||
text
|
||
.trim()
|
||
.replace(/^(?:[一二三四五六七八九十]+[、..]|[0-9]+[.)、..])\s*/, '')
|
||
.trim();
|
||
|
||
const feedback = async (
|
||
verdict: AnswerFeedbackVerdict,
|
||
details: { reasonCodes?: string[]; comment?: string } = {}
|
||
) => {
|
||
if (!result.value || feedbackSubmitting.value || feedbackVerdict.value) return;
|
||
const fragmentIds = Array.from(
|
||
new Set(
|
||
(result.value.snippets || [])
|
||
.map((item) => item.fragmentId)
|
||
.filter((id): id is number => typeof id === 'number' && id > 0)
|
||
)
|
||
);
|
||
if (!fragmentIds.length) {
|
||
uni.showToast({ title: '暂无可反馈依据', icon: 'none' });
|
||
return;
|
||
}
|
||
|
||
feedbackSubmitting.value = true;
|
||
try {
|
||
await submitAnswerFeedback({
|
||
queryText: result.value.queryText || query.value,
|
||
fragmentIds,
|
||
verdict,
|
||
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)) {
|
||
uni.showToast({
|
||
title: message.value,
|
||
icon: 'none'
|
||
});
|
||
}
|
||
} finally {
|
||
feedbackSubmitting.value = false;
|
||
}
|
||
};
|
||
|
||
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();
|
||
if (!value) return;
|
||
summaryLoading.value = true;
|
||
message.value = '';
|
||
summaryCard.value = null;
|
||
try {
|
||
summaryCard.value = await generateSummaryCard(value);
|
||
} catch (error) {
|
||
handleRequestError(error, '总结卡生成失败', true);
|
||
} finally {
|
||
summaryLoading.value = false;
|
||
}
|
||
};
|
||
|
||
const closeSummary = () => {
|
||
summaryCard.value = null;
|
||
};
|
||
|
||
const goLogin = () => {
|
||
rememberLoginRedirect('/pages/user/sop/index');
|
||
uni.navigateTo({ url: '/pages/auth/login/index' });
|
||
};
|
||
|
||
const handleShow = async () => {
|
||
resetPageScroll();
|
||
refreshAuthState();
|
||
if (loggedIn.value && !(await ensureEmployeePosition('/pages/user/sop/index'))) {
|
||
return;
|
||
}
|
||
positionProfile.value = getSelectedPositionProfile();
|
||
if (!loggedIn.value) {
|
||
resetResultState();
|
||
}
|
||
const pending = uni.getStorageSync(pendingSopQueryKey);
|
||
if (!pending) {
|
||
if (loggedIn.value && query.value.trim() && message.value === '登录后继续查询该问题') {
|
||
void search();
|
||
}
|
||
return;
|
||
}
|
||
query.value = String(pending);
|
||
if (loggedIn.value) {
|
||
uni.removeStorageSync(pendingSopQueryKey);
|
||
void search();
|
||
} else {
|
||
message.value = '登录后继续查询该问题';
|
||
}
|
||
};
|
||
|
||
onShow(() => {
|
||
void handleShow();
|
||
});
|
||
</script>
|
||
|
||
<style scoped>
|
||
.sop-page {
|
||
padding: 0 16px 108px;
|
||
background: linear-gradient(180deg, var(--employee-bg-soft) 0%, var(--employee-bg) 100%);
|
||
}
|
||
|
||
.sop-head {
|
||
position: relative;
|
||
margin: 0 -16px;
|
||
padding: 30px 20px 24px;
|
||
border-bottom: 1px solid var(--employee-line);
|
||
background: linear-gradient(180deg, #eef4ff 0%, #f5f8fd 100%);
|
||
}
|
||
|
||
.sop-head::after {
|
||
position: absolute;
|
||
right: 20px;
|
||
bottom: 20px;
|
||
width: 42px;
|
||
height: 4px;
|
||
border-radius: 4px;
|
||
background: var(--employee-primary);
|
||
content: '';
|
||
}
|
||
|
||
.sop-eyebrow {
|
||
display: inline-flex;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.sop-head .page-title {
|
||
color: #1f2b3d;
|
||
font-size: 31px;
|
||
line-height: 1.12;
|
||
}
|
||
|
||
.sop-head .page-subtitle {
|
||
max-width: 292px;
|
||
margin-top: 10px;
|
||
color: #607086;
|
||
font-size: 14px;
|
||
line-height: 1.55;
|
||
}
|
||
|
||
.toolbar-tip {
|
||
display: block;
|
||
max-width: 286px;
|
||
margin-top: 14px;
|
||
color: #315b8f;
|
||
font-size: 12px;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.query-panel,
|
||
.answer-panel,
|
||
.summary-panel {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 14px;
|
||
}
|
||
|
||
.query-panel {
|
||
margin-top: 16px;
|
||
padding: 20px;
|
||
border-top: 0;
|
||
box-shadow: var(--employee-card-shadow);
|
||
}
|
||
|
||
.sop-query-textarea {
|
||
min-height: 112px;
|
||
border-color: #d6deea;
|
||
background: #f9fbff;
|
||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.7);
|
||
}
|
||
|
||
.query-caption {
|
||
color: #6f7b8f;
|
||
font-size: 12px;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.query-panel .textarea {
|
||
min-height: 104px;
|
||
height: 104px;
|
||
border-color: #d9dade;
|
||
border-radius: 7px;
|
||
background: #fbfbfb;
|
||
color: #25282d;
|
||
}
|
||
|
||
.query-panel .chip-row {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||
gap: 7px;
|
||
}
|
||
|
||
.query-panel .chip {
|
||
min-height: 38px;
|
||
padding: 0 10px;
|
||
border-color: #dbe5f4;
|
||
border-radius: 999px;
|
||
background: #f7faff;
|
||
color: #47607f;
|
||
font-size: 13px;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.query-submit {
|
||
width: 100%;
|
||
margin-top: 2px;
|
||
min-height: 50px;
|
||
border-radius: 18px;
|
||
background: linear-gradient(135deg, #2f6bff, #5a8cff);
|
||
box-shadow: 0 14px 28px rgba(47, 107, 255, 0.22);
|
||
}
|
||
|
||
.login-panel {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 14px;
|
||
margin-top: 22px;
|
||
border-left: 4px solid #e70014;
|
||
box-shadow: var(--employee-card-shadow);
|
||
}
|
||
|
||
.block {
|
||
display: block;
|
||
margin-top: 8px;
|
||
font-size: 13px;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.login-button {
|
||
width: 96px;
|
||
min-width: 96px;
|
||
flex: 0 0 96px;
|
||
padding: 0 8px;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.answer-text {
|
||
color: #243246;
|
||
font-size: 18px;
|
||
font-weight: 700;
|
||
line-height: 1.65;
|
||
}
|
||
|
||
.answer-panel {
|
||
border-color: var(--employee-line);
|
||
border-left: 4px solid var(--employee-primary);
|
||
box-shadow: var(--employee-card-shadow);
|
||
}
|
||
|
||
.answer-panel .panel-title {
|
||
color: var(--employee-primary);
|
||
}
|
||
|
||
.question-card,
|
||
.answer-copy-card {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
margin-top: 12px;
|
||
padding: 14px 16px;
|
||
border-radius: 14px;
|
||
}
|
||
|
||
.question-card {
|
||
background: #fff1f1;
|
||
}
|
||
|
||
.question-label {
|
||
color: #e34b4b;
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.question-text {
|
||
color: #25282d;
|
||
font-size: 16px;
|
||
line-height: 1.55;
|
||
}
|
||
|
||
.answer-copy-card {
|
||
background: #fff;
|
||
}
|
||
|
||
.keypoints {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
|
||
.keypoint {
|
||
padding: 10px 12px;
|
||
border-left: 3px solid #168243;
|
||
border-radius: 5px;
|
||
background: #f2f8f4;
|
||
color: #45484e;
|
||
font-size: 15px;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.cautions {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
padding: 10px 12px;
|
||
border-left: 3px solid #d98200;
|
||
border-radius: 5px;
|
||
background: #fff8e9;
|
||
color: #855000;
|
||
font-size: 13px;
|
||
line-height: 1.45;
|
||
}
|
||
|
||
.feedback-actions {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 10px;
|
||
}
|
||
|
||
.feedback-actions .active {
|
||
border-color: var(--employee-primary);
|
||
background: var(--employee-primary-soft);
|
||
color: var(--employee-primary);
|
||
}
|
||
|
||
.feedback-actions .negative {
|
||
border-color: var(--employee-danger);
|
||
background: #fff1f0;
|
||
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;
|
||
border-radius: 12px;
|
||
background: var(--employee-primary-soft);
|
||
color: var(--employee-primary);
|
||
font-size: 13px;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.sources-panel {
|
||
padding: 0;
|
||
border: 0;
|
||
background: transparent;
|
||
box-shadow: none;
|
||
}
|
||
|
||
.sources-panel .panel-title {
|
||
display: block;
|
||
margin-top: 26px;
|
||
color: #25282d;
|
||
font-size: 20px;
|
||
}
|
||
|
||
.snippet {
|
||
display: flex;
|
||
margin-top: 12px;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
padding: 14px;
|
||
border: 1px solid #e3e4e7;
|
||
border-color: var(--employee-line);
|
||
border-radius: 18px;
|
||
background: #ffffff;
|
||
box-shadow: 0 5px 14px rgba(37, 40, 45, 0.04);
|
||
}
|
||
|
||
.snippet-title {
|
||
color: #34373c;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.snippet-content {
|
||
color: #60636a;
|
||
font-size: 14px;
|
||
line-height: 1.55;
|
||
}
|
||
|
||
.snippet-id {
|
||
color: #96999f;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.summary-block {
|
||
display: flex;
|
||
margin-top: 14px;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
|
||
.summary-panel {
|
||
border-color: #f4e4c4;
|
||
background: #fffaf2;
|
||
box-shadow: var(--employee-card-shadow);
|
||
}
|
||
|
||
.summary-panel .muted {
|
||
font-size: 13px;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.summary-copy {
|
||
max-width: 278px;
|
||
}
|
||
|
||
.summary-panel .ghost-button {
|
||
min-height: 48px;
|
||
border: 1px solid #d7e1f5;
|
||
border-radius: 18px;
|
||
background: #f6f9ff;
|
||
color: #315b8f;
|
||
}
|
||
|
||
/* Modal mask overlay */
|
||
.modal-mask {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(15, 23, 42, 0.6);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 999;
|
||
padding: 20px;
|
||
}
|
||
|
||
.modal-card {
|
||
width: 100%;
|
||
max-width: 400px;
|
||
max-height: 80vh;
|
||
background: #ffffff;
|
||
border-radius: 16px;
|
||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.15), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
border-top: 5px solid #168243;
|
||
}
|
||
|
||
.modal-header {
|
||
padding: 16px 20px;
|
||
border-bottom: 1px solid #f1f5f9;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.modal-title {
|
||
font-size: 18px;
|
||
font-weight: 900;
|
||
color: #0f172a;
|
||
}
|
||
|
||
.modal-close {
|
||
background: transparent;
|
||
border: none;
|
||
font-size: 24px;
|
||
color: #94a3b8;
|
||
cursor: pointer;
|
||
padding: 0;
|
||
line-height: 1;
|
||
width: auto;
|
||
min-height: auto;
|
||
}
|
||
|
||
.modal-body {
|
||
flex: 1;
|
||
padding: 20px;
|
||
overflow-y: auto;
|
||
max-height: 55vh;
|
||
}
|
||
|
||
.modal-footer {
|
||
padding: 16px 20px;
|
||
border-top: 1px solid #f1f5f9;
|
||
}
|
||
|
||
.footer-btn {
|
||
width: 100%;
|
||
background: var(--employee-primary);
|
||
box-shadow: 0 8px 18px rgba(47, 107, 255, 0.18);
|
||
color: #ffffff;
|
||
}
|
||
|
||
/* Infographic Sections */
|
||
.info-section {
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.section-title-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-bottom: 14px;
|
||
}
|
||
|
||
.badge-num {
|
||
width: 20px;
|
||
height: 20px;
|
||
background: #168243;
|
||
color: #ffffff;
|
||
border-radius: 50%;
|
||
font-size: 11px;
|
||
font-weight: 800;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.info-section-title {
|
||
font-size: 15px;
|
||
font-weight: 900;
|
||
color: #1e293b;
|
||
}
|
||
|
||
/* Steps Timeline */
|
||
.timeline {
|
||
position: relative;
|
||
padding-left: 14px;
|
||
}
|
||
|
||
.timeline::before {
|
||
content: '';
|
||
position: absolute;
|
||
left: 5px;
|
||
top: 6px;
|
||
bottom: 6px;
|
||
width: 2px;
|
||
background: #e2e8f0;
|
||
}
|
||
|
||
.timeline-item {
|
||
position: relative;
|
||
padding-bottom: 16px;
|
||
display: flex;
|
||
gap: 12px;
|
||
}
|
||
|
||
.timeline-item:last-child {
|
||
padding-bottom: 0;
|
||
}
|
||
|
||
.timeline-node {
|
||
position: absolute;
|
||
left: -14px;
|
||
width: 12px;
|
||
height: 12px;
|
||
background: #168243;
|
||
color: #ffffff;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 8px;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.timeline-content {
|
||
flex: 1;
|
||
}
|
||
|
||
.timeline-label {
|
||
display: block;
|
||
font-size: 14px;
|
||
font-weight: 800;
|
||
color: #334155;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.timeline-desc {
|
||
display: block;
|
||
font-size: 12px;
|
||
color: #64748b;
|
||
line-height: 1.45;
|
||
}
|
||
|
||
/* Speech bubbles/scripts */
|
||
.speech-bubble {
|
||
position: relative;
|
||
background: #f2f8f4;
|
||
border-left: 3px solid #168243;
|
||
border-radius: 8px;
|
||
padding: 12px 14px;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.quote-mark {
|
||
position: absolute;
|
||
left: 6px;
|
||
top: -2px;
|
||
font-size: 24px;
|
||
color: rgba(22, 130, 67, 0.15);
|
||
font-family: Georgia, serif;
|
||
line-height: 1;
|
||
}
|
||
|
||
.speech-text {
|
||
font-size: 13.5px;
|
||
color: #1e293b;
|
||
line-height: 1.5;
|
||
position: relative;
|
||
z-index: 1;
|
||
}
|
||
|
||
/* Objections Q&A */
|
||
.objection-card {
|
||
background: #f8fafc;
|
||
border: 1px solid #e2e8f0;
|
||
border-radius: 8px;
|
||
padding: 12px;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.objection-q,
|
||
.objection-a {
|
||
display: flex;
|
||
gap: 8px;
|
||
}
|
||
|
||
.objection-q {
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.q-badge,
|
||
.a-badge {
|
||
flex: 0 0 18px;
|
||
height: 18px;
|
||
font-size: 10px;
|
||
font-weight: 800;
|
||
border-radius: 4px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.q-badge {
|
||
background: #fee2e2;
|
||
color: #b91c1c;
|
||
}
|
||
|
||
.a-badge {
|
||
background: #dcfce7;
|
||
color: #166534;
|
||
}
|
||
|
||
.q-text {
|
||
font-size: 13px;
|
||
font-weight: 800;
|
||
color: #334155;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.a-text {
|
||
font-size: 13px;
|
||
color: #475569;
|
||
line-height: 1.45;
|
||
}
|
||
|
||
/* Reminders alert box */
|
||
.reminder-item {
|
||
display: flex;
|
||
gap: 8px;
|
||
background: #fffbeb;
|
||
border-radius: 8px;
|
||
padding: 10px 12px;
|
||
margin-bottom: 8px;
|
||
align-items: flex-start;
|
||
border-left: 3px solid #d97706;
|
||
}
|
||
|
||
.reminder-text {
|
||
font-size: 13px;
|
||
color: #78350f;
|
||
line-height: 1.45;
|
||
}
|
||
|
||
/* 视觉稿对齐:与员工端练页统一的蓝白轻卡片体系 */
|
||
.sop-page {
|
||
padding: 0 16px 108px;
|
||
background: linear-gradient(180deg, var(--employee-bg-soft) 0%, var(--employee-bg) 100%);
|
||
}
|
||
|
||
.sop-head {
|
||
margin: 0 -16px;
|
||
padding: 30px 20px 24px;
|
||
border-bottom: 1px solid var(--employee-line);
|
||
background: linear-gradient(180deg, #eef4ff 0%, #f5f8fd 100%);
|
||
text-align: left;
|
||
}
|
||
|
||
.sop-eyebrow,
|
||
.toolbar-tip {
|
||
display: block;
|
||
}
|
||
|
||
.sop-head .page-title {
|
||
color: var(--employee-text);
|
||
font-size: 30px;
|
||
font-weight: 900;
|
||
}
|
||
|
||
.sop-head .page-subtitle {
|
||
max-width: none;
|
||
margin-top: 10px;
|
||
color: var(--employee-muted);
|
||
font-size: 14px;
|
||
}
|
||
|
||
.sop-context {
|
||
display: inline-flex;
|
||
margin-top: 14px;
|
||
padding: 7px 14px;
|
||
border: 1px solid #cfe0ff;
|
||
border-radius: 999px;
|
||
background: var(--employee-primary-soft);
|
||
color: var(--employee-primary);
|
||
font-size: 13px;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.query-panel {
|
||
margin-top: 16px;
|
||
padding: 22px 18px 18px;
|
||
border: 0;
|
||
border-radius: var(--employee-card-radius);
|
||
background: #ffffff;
|
||
box-shadow: 0 8px 24px rgba(24, 28, 36, 0.06);
|
||
}
|
||
|
||
.query-panel .textarea,
|
||
.sop-query-textarea {
|
||
min-height: 126px;
|
||
height: 126px;
|
||
border-color: #dedfe2;
|
||
border-radius: 16px;
|
||
background: #ffffff;
|
||
}
|
||
|
||
.query-panel .chip {
|
||
min-height: 40px;
|
||
border-color: #cfe0ff;
|
||
border-radius: 999px;
|
||
background: #ffffff;
|
||
color: var(--employee-primary);
|
||
}
|
||
|
||
.query-submit {
|
||
min-height: 54px;
|
||
margin-top: 8px;
|
||
border-radius: 18px;
|
||
background: linear-gradient(135deg, #2f6bff, #5a8cff);
|
||
box-shadow: 0 8px 18px rgba(47, 107, 255, 0.2);
|
||
}
|
||
|
||
.knowledge-tip-card {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
margin-top: 14px;
|
||
padding: 16px;
|
||
border: 0;
|
||
border-radius: 18px;
|
||
border: 1px solid var(--employee-line);
|
||
border-radius: var(--employee-card-radius);
|
||
background: #ffffff;
|
||
box-shadow: var(--employee-card-shadow);
|
||
}
|
||
|
||
.knowledge-tip-icon {
|
||
display: flex;
|
||
width: 46px;
|
||
height: 46px;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: 14px;
|
||
background: var(--employee-primary-soft);
|
||
color: var(--employee-primary);
|
||
font-size: 28px;
|
||
font-weight: 900;
|
||
}
|
||
|
||
.knowledge-tip-copy {
|
||
display: flex;
|
||
min-width: 0;
|
||
flex: 1;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
}
|
||
|
||
.knowledge-tip-title {
|
||
color: #25282d;
|
||
font-size: 16px;
|
||
font-weight: 900;
|
||
}
|
||
|
||
.knowledge-tip-desc {
|
||
color: #858991;
|
||
font-size: 12px;
|
||
line-height: 1.45;
|
||
}
|
||
|
||
.knowledge-tip-arrow {
|
||
color: #8c9098;
|
||
font-size: 26px;
|
||
}
|
||
|
||
.answer-empty {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-top: 14px;
|
||
padding: 28px 18px;
|
||
flex-direction: column;
|
||
border: 0;
|
||
border-radius: var(--employee-card-radius);
|
||
background: #ffffff;
|
||
box-shadow: 0 8px 24px rgba(24, 28, 36, 0.04);
|
||
}
|
||
|
||
.answer-empty-icon {
|
||
color: #f2b4b6;
|
||
font-size: 22px;
|
||
letter-spacing: 5px;
|
||
}
|
||
|
||
.answer-empty-title {
|
||
margin-top: 12px;
|
||
color: #666a72;
|
||
font-size: 18px;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.answer-empty-desc {
|
||
margin-top: 6px;
|
||
color: #9a9da3;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.answer-panel {
|
||
border-color: var(--employee-line);
|
||
border-left-color: var(--employee-primary);
|
||
}
|
||
|
||
.answer-panel .panel-title,
|
||
.result-score {
|
||
color: var(--employee-primary);
|
||
}
|
||
|
||
.keypoint,
|
||
.speech-bubble {
|
||
border-left-color: var(--employee-primary);
|
||
background: var(--employee-primary-soft);
|
||
}
|
||
|
||
.cautions,
|
||
.reminder-item {
|
||
border-left-color: var(--employee-warning);
|
||
background: #fff8ec;
|
||
color: #8b5a1d;
|
||
}
|
||
|
||
.feedback-actions .active {
|
||
border-color: var(--employee-primary);
|
||
background: var(--employee-primary-soft);
|
||
color: var(--employee-primary);
|
||
}
|
||
|
||
.feedback-actions .negative {
|
||
border-color: var(--employee-danger);
|
||
background: #fff1f0;
|
||
color: #c34d45;
|
||
}
|
||
|
||
.summary-panel {
|
||
border-color: #f4e4c4;
|
||
border-radius: var(--employee-card-radius);
|
||
background: #ffffff;
|
||
}
|
||
|
||
.summary-panel .ghost-button {
|
||
border-color: #cfe0ff;
|
||
background: var(--employee-primary-soft);
|
||
color: var(--employee-primary);
|
||
}
|
||
|
||
.modal-card {
|
||
border-top-color: var(--employee-primary);
|
||
}
|
||
|
||
.badge-num,
|
||
.timeline-node {
|
||
background: var(--employee-primary);
|
||
}
|
||
|
||
.a-badge {
|
||
background: var(--employee-primary-soft);
|
||
color: var(--employee-primary);
|
||
}
|
||
|
||
.reminder-text {
|
||
color: #8b5a1d;
|
||
}
|
||
|
||
/* 原型对齐:问师傅主卡的标题与对话插画 */
|
||
.query-hero {
|
||
position: relative;
|
||
min-height: 84px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.query-copy {
|
||
position: relative;
|
||
z-index: 1;
|
||
display: flex;
|
||
max-width: 66%;
|
||
flex-direction: column;
|
||
gap: 7px;
|
||
}
|
||
|
||
.query-title {
|
||
color: var(--employee-text);
|
||
font-size: 30px;
|
||
font-weight: 900;
|
||
line-height: 1.1;
|
||
}
|
||
|
||
.query-subtitle {
|
||
color: var(--employee-muted);
|
||
font-size: 15px;
|
||
line-height: 1.45;
|
||
}
|
||
|
||
.query-visual {
|
||
position: absolute;
|
||
top: -6px;
|
||
right: -4px;
|
||
width: 42%;
|
||
height: 96px;
|
||
border-radius: 50%;
|
||
background: var(--employee-primary-soft);
|
||
}
|
||
|
||
.query-bubble {
|
||
position: absolute;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: 50%;
|
||
letter-spacing: 3px;
|
||
}
|
||
|
||
.query-bubble.primary {
|
||
top: 7px;
|
||
right: 26px;
|
||
width: 92px;
|
||
height: 56px;
|
||
border-radius: 28px 28px 28px 10px;
|
||
background: #7ba4ff;
|
||
color: #ffffff;
|
||
font-size: 18px;
|
||
}
|
||
|
||
.query-bubble.primary::after {
|
||
position: absolute;
|
||
bottom: -8px;
|
||
left: 18px;
|
||
width: 16px;
|
||
height: 16px;
|
||
background: #7ba4ff;
|
||
content: '';
|
||
transform: rotate(28deg);
|
||
}
|
||
|
||
.query-bubble.secondary {
|
||
right: 2px;
|
||
bottom: 5px;
|
||
width: 70px;
|
||
height: 42px;
|
||
border-radius: 22px 22px 8px 22px;
|
||
background: #ffffff;
|
||
box-shadow: 0 5px 14px rgba(47, 107, 255, 0.1);
|
||
color: #9bb8ff;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.query-panel .query-caption {
|
||
display: none;
|
||
}
|
||
</style>
|