feat(aihr): switch interview and case flows to real-model-first pipeline
AI 面试 /start 动态出题、/finish 按真实回答结构化评分(未配模型用本地 Rubric 估分);案例沉淀 /upload 改 multipart 真实语音+ASR 转写, /organize 用真实 transcript 调 chat 模型整理;seed service 重构为 AihrInterviewService/AihrCaseService;前端面试/案例/对练页与移动端、 demo-check、验收文档同步真实链路口径。
This commit is contained in:
+721
-9
@@ -159,7 +159,7 @@
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<Card title="今日任务" action="查看全部" @action="tap('查看全部')">
|
||||
<Card title="今日任务" action="查看全部" @action="openUserAction('tasks')">
|
||||
<div class="task-card">
|
||||
<div class="task-copy">
|
||||
<strong>{{ workerRole.primary.badge }}</strong>
|
||||
@@ -174,7 +174,7 @@
|
||||
<button type="button" @click="handlePrimaryAction">{{ workerRole.primary.cta }}</button>
|
||||
</div>
|
||||
|
||||
<button class="review-card" type="button" @click="tap(workerRole.review.title)">
|
||||
<button class="review-card" type="button" @click="openUserAction('sop')">
|
||||
<IconBubble icon="document" tone="amber" />
|
||||
<span>
|
||||
<b>{{ workerRole.review.title }}</b>
|
||||
@@ -223,10 +223,201 @@
|
||||
</Card>
|
||||
|
||||
<Card :title="workerRole.toolTitle">
|
||||
<ToolGrid :items="workerRole.tools" />
|
||||
<ToolGrid :items="workerRole.tools" :handler="openUserTool" />
|
||||
</Card>
|
||||
|
||||
<Card title="我的进度" :action="workerRole.recommend" compact-action @action="tap(workerRole.recommend)">
|
||||
<Card v-if="roleKey === 'user' && activeUserPanel" :title="userPanelTitle">
|
||||
<div class="user-panel">
|
||||
<template v-if="activeUserPanel === 'tasks'">
|
||||
<button class="panel-row" type="button" @click="startMobilePractice">
|
||||
<IconBubble icon="headset" tone="teal" />
|
||||
<span>
|
||||
<b>{{ workerRole.primary.title }}</b>
|
||||
<small>{{ workerRole.primary.desc }}</small>
|
||||
</span>
|
||||
<Icon name="chevron-right" size="24" />
|
||||
</button>
|
||||
<button class="panel-row" type="button" @click="openUserAction('sop')">
|
||||
<IconBubble icon="document" tone="amber" />
|
||||
<span>
|
||||
<b>{{ workerRole.review.title }}</b>
|
||||
<small>{{ workerRole.review.desc }}</small>
|
||||
</span>
|
||||
<Icon name="chevron-right" size="24" />
|
||||
</button>
|
||||
<button class="panel-row" type="button" @click="openUserAction('cases')">
|
||||
<IconBubble icon="folder" tone="orange" />
|
||||
<span>
|
||||
<b>案例学习</b>
|
||||
<small>查看优秀服务案例和可复用话术</small>
|
||||
</span>
|
||||
<Icon name="chevron-right" size="24" />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<template v-else-if="activeUserPanel === 'sop' || activeUserPanel === 'cases'">
|
||||
<form class="knowledge-search" @submit.prevent="searchKnowledge()">
|
||||
<input v-model.trim="knowledgeQuery" :placeholder="activeUserPanel === 'cases' ? '搜索案例关键词' : '搜索SOP关键词'" />
|
||||
<button type="submit" :disabled="knowledgeLoading">{{ knowledgeLoading ? '查询中' : '查询' }}</button>
|
||||
</form>
|
||||
<em v-if="knowledgeMessage" class="panel-message">{{ knowledgeMessage }}</em>
|
||||
|
||||
<div v-if="knowledgeResult" class="knowledge-result">
|
||||
<strong>{{ knowledgeResult.answer }}</strong>
|
||||
<p v-if="knowledgeResult.reference">{{ knowledgeResult.reference }}</p>
|
||||
|
||||
<div v-if="knowledgeResult.training?.length" class="panel-tags">
|
||||
<span v-for="item in knowledgeResult.training" :key="item">{{ item }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="knowledgeResult.docs?.length" class="result-list">
|
||||
<h3>命中文档</h3>
|
||||
<p v-for="doc in knowledgeResult.docs" :key="`${doc.title}-${doc.hit}`">
|
||||
<b>{{ doc.title }}</b>
|
||||
<small>{{ doc.desc }} · {{ doc.hit }}</small>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-if="knowledgeResult.snippets?.length" class="snippet-list">
|
||||
<h3>引用片段</h3>
|
||||
<blockquote v-for="item in knowledgeResult.snippets" :key="`${item.title}-${item.text}`">
|
||||
<b>{{ item.title }}</b>
|
||||
<span>{{ item.text }}</span>
|
||||
</blockquote>
|
||||
</div>
|
||||
|
||||
<div v-if="knowledgeResult.records?.length" class="result-list">
|
||||
<h3>{{ activeUserPanel === 'cases' ? '可学习案例' : '知识记录' }}</h3>
|
||||
<p v-for="record in knowledgeResult.records" :key="`${record.title}-${record.version}`">
|
||||
<b>{{ record.title }}</b>
|
||||
<small>{{ record.summary }} · {{ record.status }}</small>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="activeUserPanel === 'submit'">
|
||||
<label class="file-picker">
|
||||
<span>{{ materialFile?.name || '选择语音素材' }}</span>
|
||||
<small>支持 mp3 / wav / m4a / webm / ogg,上传后自动转写并整理案例</small>
|
||||
<input type="file" accept="audio/*,.mp3,.wav,.m4a,.webm,.ogg" @change="handleMaterialFile" />
|
||||
</label>
|
||||
<button class="panel-primary" type="button" :disabled="materialUploading" @click="submitMaterial">
|
||||
{{ materialUploading ? '提交中' : '上传并整理' }}
|
||||
</button>
|
||||
<em v-if="materialMessage" class="panel-message">{{ materialMessage }}</em>
|
||||
|
||||
<div v-if="materialResult" class="case-result">
|
||||
<div v-if="materialResult.upload">
|
||||
<h3>ASR转写</h3>
|
||||
<p>{{ materialResult.upload.transcript }}</p>
|
||||
<div class="panel-tags">
|
||||
<span v-for="tag in materialResult.upload.tags" :key="tag.text">{{ tag.text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="materialResult.organize">
|
||||
<h3>案例整理</h3>
|
||||
<p v-if="materialResult.organize.aiSummary">{{ materialResult.organize.aiSummary }}</p>
|
||||
<dl>
|
||||
<template v-for="item in materialResult.organize.summary" :key="item.label">
|
||||
<dt>{{ item.label }}</dt>
|
||||
<dd>{{ item.text }}</dd>
|
||||
</template>
|
||||
</dl>
|
||||
</div>
|
||||
<div v-if="materialResult.curate?.records?.length">
|
||||
<h3>入库记录</h3>
|
||||
<p v-for="record in materialResult.curate.records" :key="record.title">
|
||||
<b>{{ record.title }}</b>
|
||||
<small>{{ record.summary }}</small>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="activeUserPanel === 'profile'">
|
||||
<div v-if="competencyProfile" class="profile-card panel-profile">
|
||||
<span>
|
||||
<small>综合能力</small>
|
||||
<b>{{ competencyProfile.score }}</b>
|
||||
</span>
|
||||
<span>
|
||||
<small>训练历史</small>
|
||||
<b>{{ competencyProfile.completed }}</b>
|
||||
</span>
|
||||
<span>
|
||||
<small>待复盘</small>
|
||||
<b>{{ competencyProfile.pendingReview }}</b>
|
||||
</span>
|
||||
<div class="profile-dims">
|
||||
<p v-for="item in competencyProfile.dimensions" :key="item.label">
|
||||
<span>{{ item.label }}</span>
|
||||
<b>{{ item.value }}</b>
|
||||
<small>{{ item.note }}</small>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="practiceHistory.length" class="record-list">
|
||||
<h3>训练历史</h3>
|
||||
<button v-for="item in practiceHistory" :key="`${item.time}-${item.scene}`" type="button" @click="openUserHistory(item)">
|
||||
<span>
|
||||
<b>{{ item.scene }} · {{ item.score }}分</b>
|
||||
<small>{{ item.time }} · {{ item.summary }}</small>
|
||||
</span>
|
||||
<em>{{ item.status }}</em>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="activeUserPanel === 'history'">
|
||||
<div v-if="practiceHistory.length && !userReviewDetail" class="record-list">
|
||||
<h3>训练历史</h3>
|
||||
<button v-for="item in practiceHistory" :key="`${item.time}-${item.scene}`" type="button" @click="openUserHistory(item)">
|
||||
<span>
|
||||
<b>{{ item.scene }} · {{ item.score }}分</b>
|
||||
<small>{{ item.time }} · {{ item.summary }}</small>
|
||||
</span>
|
||||
<em>{{ item.status }}</em>
|
||||
</button>
|
||||
</div>
|
||||
<em v-if="userReviewMessage" class="panel-message">{{ userReviewMessage }}</em>
|
||||
<div v-if="userReviewDetail" class="review-detail">
|
||||
<div class="review-detail-head">
|
||||
<span>
|
||||
<b>{{ userReviewDetail.scene }} · {{ userReviewDetail.score }}分</b>
|
||||
<small>{{ userReviewDetail.time }} · {{ userReviewDetail.status }}</small>
|
||||
</span>
|
||||
<em>{{ userReviewDetail.status }}</em>
|
||||
</div>
|
||||
<div class="review-score-grid">
|
||||
<p v-for="item in userReviewDetail.scoreItems" :key="item.label">
|
||||
<span>{{ item.label }}</span>
|
||||
<b>{{ item.value }}</b>
|
||||
<small>{{ item.note }}</small>
|
||||
</p>
|
||||
</div>
|
||||
<div class="practice-dialogue review-dialogue">
|
||||
<p v-for="(turn, index) in userReviewDetail.dialogue" :key="`${turn.role}-${index}`" :class="turn.role">
|
||||
<span>{{ turn.label }}</span>
|
||||
{{ turn.text }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="review-summary">
|
||||
<b>导师改写</b>
|
||||
<p>{{ userReviewDetail.mentorRewrite }}</p>
|
||||
<b>AI点评</b>
|
||||
<p>{{ userReviewDetail.aiComment }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="panel-actions">
|
||||
<button type="button" @click="activeUserPanel = ''">收起</button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card title="我的进度" :action="workerRole.recommend" compact-action @action="openUserAction('recommend')">
|
||||
<div class="progress-grid">
|
||||
<ProgressTile v-for="item in workerRole.metrics" :key="item.title" :item="item" />
|
||||
</div>
|
||||
@@ -255,7 +446,7 @@
|
||||
|
||||
<div v-if="practiceHistory.length" class="record-list">
|
||||
<h3>训练历史</h3>
|
||||
<button v-for="item in practiceHistory" :key="`${item.time}-${item.scene}`" type="button" @click="tap(item.scene)">
|
||||
<button v-for="item in practiceHistory" :key="`${item.time}-${item.scene}`" type="button" @click="openUserHistory(item)">
|
||||
<span>
|
||||
<b>{{ item.scene }} · {{ item.score }}分</b>
|
||||
<small>{{ item.time }} · {{ item.summary }}</small>
|
||||
@@ -267,7 +458,7 @@
|
||||
</template>
|
||||
|
||||
<nav class="tabbar" aria-label="底部导航">
|
||||
<button v-for="tab in role.tabs" :key="tab.label" type="button" :class="{ active: tab.active }" @click="tap(tab.label)">
|
||||
<button v-for="tab in role.tabs" :key="tab.label" type="button" :class="{ active: tab.active }" @click="openTab(tab.label)">
|
||||
<Icon :name="tab.icon" size="28" />
|
||||
<span>{{ tab.label }}</span>
|
||||
</button>
|
||||
@@ -282,6 +473,7 @@ import { computed, defineComponent, h, ref, watch } from 'vue';
|
||||
|
||||
type RoleKey = 'user' | 'candidate' | 'supervisor';
|
||||
type Tone = 'teal' | 'blue' | 'orange' | 'purple' | 'amber';
|
||||
type UserPanelKey = '' | 'tasks' | 'sop' | 'cases' | 'submit' | 'profile' | 'history' | 'recommend';
|
||||
type IconName =
|
||||
| 'bars'
|
||||
| 'book'
|
||||
@@ -453,6 +645,85 @@ interface CompetencyProfile {
|
||||
dimensions: CompetencyDimension[];
|
||||
}
|
||||
|
||||
interface KnowledgeDoc {
|
||||
title: string;
|
||||
desc: string;
|
||||
hit: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
interface KnowledgeSnippet {
|
||||
title: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
interface KnowledgeRecord {
|
||||
title: string;
|
||||
category: string;
|
||||
version: string;
|
||||
status: string;
|
||||
owner: string;
|
||||
summary: string;
|
||||
}
|
||||
|
||||
interface KnowledgeSearchResponse {
|
||||
queryText: string;
|
||||
category: string;
|
||||
answer: string;
|
||||
reference: string;
|
||||
docs: KnowledgeDoc[];
|
||||
snippets: KnowledgeSnippet[];
|
||||
training: string[];
|
||||
records: KnowledgeRecord[];
|
||||
}
|
||||
|
||||
interface CaseTag {
|
||||
text: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
interface CaseSummaryItem {
|
||||
label: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
interface CaseUploadResponse {
|
||||
caseId: string;
|
||||
fileName: string;
|
||||
transcript: string;
|
||||
tags: CaseTag[];
|
||||
source: string;
|
||||
}
|
||||
|
||||
interface CaseOrganizeResponse {
|
||||
caseId: string;
|
||||
summary: CaseSummaryItem[];
|
||||
tags: CaseTag[];
|
||||
aiSummary: string;
|
||||
source: string;
|
||||
}
|
||||
|
||||
interface CaseRecord {
|
||||
title: string;
|
||||
type: string;
|
||||
status: string;
|
||||
time: string;
|
||||
owner: string;
|
||||
summary: string;
|
||||
}
|
||||
|
||||
interface CaseCurateResponse {
|
||||
selectedCaseIds: string[];
|
||||
records: CaseRecord[];
|
||||
sampleHint: string;
|
||||
}
|
||||
|
||||
interface MaterialResult {
|
||||
upload: CaseUploadResponse;
|
||||
organize: CaseOrganizeResponse;
|
||||
curate: CaseCurateResponse;
|
||||
}
|
||||
|
||||
const aliases: Record<string, RoleKey> = {
|
||||
h5: 'user',
|
||||
user: 'user',
|
||||
@@ -499,9 +770,20 @@ const practiceMessage = ref('');
|
||||
const practiceHistory = ref<PracticeRecord[]>([]);
|
||||
const practiceReviews = ref<PracticeRecord[]>([]);
|
||||
const selectedReview = ref<PracticeReviewDetail | null>(null);
|
||||
const userReviewDetail = ref<PracticeReviewDetail | null>(null);
|
||||
const userReviewMessage = ref('');
|
||||
const reviewMessage = ref('');
|
||||
const markingReviewed = ref(false);
|
||||
const competencyProfile = ref<CompetencyProfile | null>(null);
|
||||
const activeUserPanel = ref<UserPanelKey>('');
|
||||
const knowledgeQuery = ref('住宅投诉处理流程');
|
||||
const knowledgeResult = ref<KnowledgeSearchResponse | null>(null);
|
||||
const knowledgeLoading = ref(false);
|
||||
const knowledgeMessage = ref('');
|
||||
const materialFile = ref<File | null>(null);
|
||||
const materialUploading = ref(false);
|
||||
const materialMessage = ref('');
|
||||
const materialResult = ref<MaterialResult | null>(null);
|
||||
const voiceEnabled = ref(true);
|
||||
const recording = ref(false);
|
||||
const asrBusy = ref(false);
|
||||
@@ -519,6 +801,19 @@ const cycleRole = () => {
|
||||
syncPath();
|
||||
};
|
||||
|
||||
const userPanelTitle = computed(() => {
|
||||
const titles: Record<Exclude<UserPanelKey, ''>, string> = {
|
||||
tasks: '今日任务',
|
||||
sop: 'SOP查询',
|
||||
cases: '案例学习',
|
||||
submit: '提交素材',
|
||||
profile: '我的能力',
|
||||
history: '训练复盘',
|
||||
recommend: '专项推荐'
|
||||
};
|
||||
return activeUserPanel.value ? titles[activeUserPanel.value] : '员工工具';
|
||||
});
|
||||
|
||||
interface ApiPayload<T> {
|
||||
code?: number;
|
||||
msg?: string;
|
||||
@@ -562,7 +857,7 @@ const playCustomerVoice = async (text: string) => {
|
||||
);
|
||||
customerAudio?.pause();
|
||||
customerAudio = new Audio(data.audioUrl);
|
||||
void customerAudio.play();
|
||||
void customerAudio.play().catch(() => undefined);
|
||||
} catch {
|
||||
// TTS 未配置或失败时静默降级为纯文本
|
||||
}
|
||||
@@ -622,6 +917,186 @@ const handlePrimaryAction = () => {
|
||||
void startMobilePractice();
|
||||
};
|
||||
|
||||
const searchKnowledge = async (query = knowledgeQuery.value, category = activeUserPanel.value === 'cases' ? 'case' : 'sop') => {
|
||||
const cleaned = query.trim();
|
||||
if (!cleaned) {
|
||||
knowledgeMessage.value = '请输入关键词';
|
||||
return;
|
||||
}
|
||||
knowledgeQuery.value = cleaned;
|
||||
knowledgeLoading.value = true;
|
||||
knowledgeMessage.value = '';
|
||||
try {
|
||||
knowledgeResult.value = await readApi<KnowledgeSearchResponse>(
|
||||
await fetch('/dev-api/api/knowledge/search', {
|
||||
method: 'POST',
|
||||
headers: apiHeaders(),
|
||||
body: JSON.stringify({ queryText: cleaned, category, limit: 4 })
|
||||
})
|
||||
);
|
||||
knowledgeMessage.value = '已从知识库检索';
|
||||
} catch (error) {
|
||||
knowledgeResult.value = null;
|
||||
knowledgeMessage.value = error instanceof Error ? error.message : '知识库查询失败';
|
||||
} finally {
|
||||
knowledgeLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const openUserAction = (panel: UserPanelKey) => {
|
||||
if (roleKey.value !== 'user') {
|
||||
tap(panel || roleKey.value);
|
||||
return;
|
||||
}
|
||||
|
||||
activeUserPanel.value = panel;
|
||||
userReviewDetail.value = null;
|
||||
userReviewMessage.value = '';
|
||||
|
||||
if (panel === 'sop') {
|
||||
knowledgeQuery.value = '住宅投诉处理流程';
|
||||
void searchKnowledge(knowledgeQuery.value, 'sop');
|
||||
return;
|
||||
}
|
||||
|
||||
if (panel === 'cases') {
|
||||
knowledgeQuery.value = '投诉接待优秀案例';
|
||||
void searchKnowledge(knowledgeQuery.value, 'case');
|
||||
return;
|
||||
}
|
||||
|
||||
if (panel === 'recommend') {
|
||||
activeUserPanel.value = 'sop';
|
||||
knowledgeQuery.value = '催费沟通专项';
|
||||
void searchKnowledge(knowledgeQuery.value, 'sop');
|
||||
return;
|
||||
}
|
||||
|
||||
if (panel === 'profile' || panel === 'history') {
|
||||
void fetchPracticeData();
|
||||
}
|
||||
};
|
||||
|
||||
const openUserTool = (item: ToolItem) => {
|
||||
if (roleKey.value !== 'user') {
|
||||
tap(item.title);
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.title === '每日一练') {
|
||||
void startMobilePractice();
|
||||
return;
|
||||
}
|
||||
if (item.title === '查SOP') {
|
||||
openUserAction('sop');
|
||||
return;
|
||||
}
|
||||
if (item.title === '案例学习') {
|
||||
openUserAction('cases');
|
||||
return;
|
||||
}
|
||||
if (item.title === '提交素材') {
|
||||
openUserAction('submit');
|
||||
return;
|
||||
}
|
||||
tap(item.title);
|
||||
};
|
||||
|
||||
const openTab = (label: string) => {
|
||||
if (roleKey.value !== 'user') {
|
||||
tap(label);
|
||||
return;
|
||||
}
|
||||
|
||||
if (label === '首页') {
|
||||
activeUserPanel.value = '';
|
||||
return;
|
||||
}
|
||||
if (label === '训练') {
|
||||
void startMobilePractice();
|
||||
return;
|
||||
}
|
||||
if (label === '知识') {
|
||||
openUserAction('sop');
|
||||
return;
|
||||
}
|
||||
if (label === '我的') {
|
||||
openUserAction('profile');
|
||||
return;
|
||||
}
|
||||
tap(label);
|
||||
};
|
||||
|
||||
const openUserHistory = async (item: PracticeRecord) => {
|
||||
if (!item.id) {
|
||||
activeUserPanel.value = 'history';
|
||||
userReviewMessage.value = '这条训练记录暂无复盘详情';
|
||||
userReviewDetail.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
activeUserPanel.value = 'history';
|
||||
userReviewDetail.value = null;
|
||||
userReviewMessage.value = '';
|
||||
try {
|
||||
userReviewDetail.value = await readApi<PracticeReviewDetail>(
|
||||
await fetch(`/dev-api/api/aihr/mobile/practice/reviews/${item.id}`, { headers: apiHeaders() })
|
||||
);
|
||||
} catch (error) {
|
||||
userReviewMessage.value = error instanceof Error ? error.message : '训练复盘加载失败';
|
||||
}
|
||||
};
|
||||
|
||||
const handleMaterialFile = (event: Event) => {
|
||||
const input = event.target as HTMLInputElement;
|
||||
materialFile.value = input.files?.[0] || null;
|
||||
materialResult.value = null;
|
||||
materialMessage.value = materialFile.value ? '已选择语音素材' : '';
|
||||
};
|
||||
|
||||
const submitMaterial = async () => {
|
||||
if (!materialFile.value) {
|
||||
materialMessage.value = '请先选择语音素材';
|
||||
return;
|
||||
}
|
||||
|
||||
materialUploading.value = true;
|
||||
materialMessage.value = '';
|
||||
materialResult.value = null;
|
||||
try {
|
||||
const form = new FormData();
|
||||
form.append('file', materialFile.value);
|
||||
form.append('projectExtOrgId', phone.value || 'mobile-user');
|
||||
const upload = await readApi<CaseUploadResponse>(
|
||||
await fetch('/dev-api/api/knowledge/case/upload', {
|
||||
method: 'POST',
|
||||
headers: authOnlyHeaders(),
|
||||
body: form
|
||||
})
|
||||
);
|
||||
const organize = await readApi<CaseOrganizeResponse>(
|
||||
await fetch('/dev-api/api/knowledge/case/organize', {
|
||||
method: 'POST',
|
||||
headers: apiHeaders(),
|
||||
body: JSON.stringify({ caseId: upload.caseId })
|
||||
})
|
||||
);
|
||||
const curate = await readApi<CaseCurateResponse>(
|
||||
await fetch('/dev-api/api/knowledge/case/curate', {
|
||||
method: 'POST',
|
||||
headers: apiHeaders(),
|
||||
body: JSON.stringify({ caseId: upload.caseId, criteria: '员工端提交素材', limit: 1 })
|
||||
})
|
||||
);
|
||||
materialResult.value = { upload, organize, curate };
|
||||
materialMessage.value = '素材已完成转写、整理和入库';
|
||||
} catch (error) {
|
||||
materialMessage.value = error instanceof Error ? error.message : '素材提交失败';
|
||||
} finally {
|
||||
materialUploading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const appendPracticeTurn = (role: PracticeRole, text: string) => {
|
||||
const labels: Record<PracticeRole, string> = {
|
||||
customer: 'AI业主',
|
||||
@@ -979,6 +1454,7 @@ const fetchPracticeData = async () => {
|
||||
practiceHistory.value = [];
|
||||
practiceReviews.value = [];
|
||||
selectedReview.value = null;
|
||||
userReviewDetail.value = null;
|
||||
competencyProfile.value = null;
|
||||
return;
|
||||
}
|
||||
@@ -1005,6 +1481,7 @@ const fetchPracticeData = async () => {
|
||||
practiceHistory.value = [];
|
||||
practiceReviews.value = [];
|
||||
selectedReview.value = null;
|
||||
userReviewDetail.value = null;
|
||||
competencyProfile.value = null;
|
||||
}
|
||||
};
|
||||
@@ -1014,6 +1491,11 @@ watch(
|
||||
() => {
|
||||
apiHome.value = null;
|
||||
reviewMessage.value = '';
|
||||
userReviewMessage.value = '';
|
||||
if (roleKey.value !== 'user') {
|
||||
activeUserPanel.value = '';
|
||||
userReviewDetail.value = null;
|
||||
}
|
||||
void fetchMobileHome();
|
||||
void fetchPracticeData();
|
||||
},
|
||||
@@ -1211,7 +1693,8 @@ const IconBubble = defineComponent({
|
||||
|
||||
const ToolGrid = defineComponent({
|
||||
props: {
|
||||
items: { type: Array as PropType<ToolItem[]>, required: true }
|
||||
items: { type: Array as PropType<ToolItem[]>, required: true },
|
||||
handler: { type: Function as PropType<(item: ToolItem) => void>, default: undefined }
|
||||
},
|
||||
setup(props) {
|
||||
return () =>
|
||||
@@ -1219,7 +1702,7 @@ const ToolGrid = defineComponent({
|
||||
'div',
|
||||
{ class: 'tool-grid' },
|
||||
props.items.map((item) =>
|
||||
h('button', { type: 'button', class: `tone-${item.tone}`, onClick: () => tap(item.title) }, [
|
||||
h('button', { type: 'button', class: `tone-${item.tone}`, onClick: () => (props.handler ? props.handler(item) : tap(item.title)) }, [
|
||||
h(IconBubble, { icon: item.icon, tone: item.tone }),
|
||||
h('span', [h('b', item.title), h('small', item.desc)]),
|
||||
h(Icon, { name: 'chevron-right', size: 24 })
|
||||
@@ -1852,6 +2335,235 @@ p {
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.user-panel {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.panel-row {
|
||||
width: 100%;
|
||||
min-height: 66px;
|
||||
padding: 10px 11px;
|
||||
border: 1px solid rgba(213, 225, 233, 0.9);
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.panel-row b,
|
||||
.knowledge-result strong,
|
||||
.case-result h3,
|
||||
.result-list h3,
|
||||
.snippet-list h3 {
|
||||
color: #101826;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.panel-row b {
|
||||
display: block;
|
||||
font-size: 15px;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.panel-row small {
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
color: #607086;
|
||||
font-size: 12px;
|
||||
line-height: 1.35;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.knowledge-search {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 78px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.knowledge-search input {
|
||||
min-width: 0;
|
||||
height: 42px;
|
||||
border: 1px solid rgba(20, 177, 169, 0.24);
|
||||
border-radius: 10px;
|
||||
padding: 0 11px;
|
||||
color: #111827;
|
||||
background: #fff;
|
||||
font: inherit;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.knowledge-search button,
|
||||
.panel-primary,
|
||||
.panel-actions button {
|
||||
min-height: 42px;
|
||||
border-radius: 10px;
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg, #1cbdb4, #07948e);
|
||||
box-shadow: 0 7px 15px rgba(9, 151, 144, 0.18);
|
||||
font-size: 14px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.knowledge-search button:disabled,
|
||||
.panel-primary:disabled {
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.panel-message {
|
||||
color: #0a9a94;
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.knowledge-result,
|
||||
.case-result {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.knowledge-result strong {
|
||||
font-size: 16px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.knowledge-result > p,
|
||||
.case-result p,
|
||||
.case-result dd {
|
||||
margin: 0;
|
||||
color: #506078;
|
||||
font-size: 13px;
|
||||
line-height: 1.45;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.panel-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.panel-tags span {
|
||||
max-width: 100%;
|
||||
padding: 5px 8px;
|
||||
border-radius: 999px;
|
||||
color: #079a95;
|
||||
background: rgba(15, 174, 166, 0.1);
|
||||
font-size: 12px;
|
||||
line-height: 1.2;
|
||||
font-weight: 850;
|
||||
}
|
||||
|
||||
.result-list,
|
||||
.snippet-list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.result-list h3,
|
||||
.snippet-list h3,
|
||||
.case-result h3 {
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.result-list p,
|
||||
.snippet-list blockquote,
|
||||
.case-result > div {
|
||||
margin: 0;
|
||||
padding: 10px;
|
||||
border: 1px solid rgba(213, 225, 233, 0.9);
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.result-list b,
|
||||
.snippet-list b,
|
||||
.case-result b {
|
||||
display: block;
|
||||
color: #101826;
|
||||
font-size: 14px;
|
||||
line-height: 1.25;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.result-list small,
|
||||
.snippet-list span {
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
color: #607086;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.file-picker {
|
||||
min-height: 86px;
|
||||
padding: 13px;
|
||||
border: 1px dashed rgba(20, 177, 169, 0.38);
|
||||
border-radius: 12px;
|
||||
background: #f8ffff;
|
||||
display: grid;
|
||||
align-content: center;
|
||||
gap: 6px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.file-picker span {
|
||||
color: #101826;
|
||||
font-size: 15px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.file-picker small {
|
||||
color: #607086;
|
||||
font-size: 12px;
|
||||
line-height: 1.35;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.file-picker input {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.case-result dl {
|
||||
margin: 0;
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.case-result dt {
|
||||
color: #0a9a94;
|
||||
font-size: 13px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.panel-profile {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.panel-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.panel-actions button {
|
||||
min-width: 76px;
|
||||
color: #079a95;
|
||||
background: rgba(15, 174, 166, 0.1);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.tool-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
|
||||
Reference in New Issue
Block a user