@@ -0,0 +1,693 @@
< template >
< view class = "page" >
<!-- === === Mode 1 : List View === === -- >
< template v-if = "mode === 'list'" >
< view class = "hero" >
< text class = "hero-title" > 面试复盘 < / text >
< text class = "hero-sub" > 上传录音 , AI 帮你复盘面试表现 < / text >
< / view >
< view class = "stats-bar card" v-if = "listData.items.length > 0" >
< view class = "stat" >
< text class = "stat-val" > { { listData . total } } < / text >
< text class = "stat-lbl" > 复盘次数 < / text >
< / view >
< view class = "stat-sep" > < / view >
< view class = "stat" >
< text class = "stat-val" > { { avgScore } } < / text >
< text class = "stat-lbl" > 平均分 < / text >
< / view >
< view class = "stat-sep" > < / view >
< view class = "stat" >
< text class = "stat-val" > { { completedCount } } < / text >
< text class = "stat-lbl" > 已完成 < / text >
< / view >
< / view >
< view class = "list" v-if = "listData.items.length > 0" >
< view
class = "record-card card"
v-for = "item in listData.items"
:key = "item._id"
@click ="viewDetail(item._id)"
>
< view class = "record-top" >
< view class = "record-icon" >
{ { item . status === 'completed' ? ( item . analysis ? . overallScore >= 80 ? '🌟' : '📋' ) : '⏳' } }
< / view >
< view class = "record-body" >
< view class = "record-name" > { { item . position } } < / view >
< text class = "record-meta" >
{ { formatDate ( item . createdAt ) } }
< template v-if = "item.company" > · {{ item.company }} < / template >
< template v-if = "item.status === 'processing'" > · 分析中... < / template >
< template v-if = "item.status === 'failed'" > · 分析失败 < / template >
< / text >
< / view >
< view class = "record-score" v-if = "item.status === 'completed'" :class="scoreLevel(item.analysis?.overallScore)" >
{{ item.analysis ? .overallScore | | ' - - ' }}
< / view >
< view v-else class = "record-score pending" > { { item . status === 'processing' ? '...' : '!' } } < / view >
< / view >
< / view >
< view class = "load-more" v-if = "hasMore" @click="loadMore" > 加载更多 < / view >
< / view >
< view class = "empty" v-else-if = "!loading" >
< text class = "empty-icon" > 🎙 ️ < / text >
< text class = "empty-title" > 暂无复盘记录 < / text >
< text class = "empty-desc" > 面试后上传录音 , AI 帮你分析表现 < / text >
< / view >
< view class = "fixed-bottom" >
< button class = "btn-primary" @click ="mode = 'upload'" >
< text class = "btn-icon" > + < / text > 上传录音复盘
< / button >
< / view >
< / template >
<!-- === === Mode 2 : Upload View === === -- >
< template v-if = "mode === 'upload'" >
< view class = "upload-page" >
< view class = "upload-header" >
< text class = "upload-back" @click ="mode = 'list'" > ‹ 返回 < / text >
< text class = "upload-title" > 上传面试录音 < / text >
< view style = "width:60rpx" > < / view >
< / view >
< view class = "form-card card" >
< view class = "form-group" >
< text class = "form-label" > 面试岗位 < text class = "required" > * < / text > < / text >
< input class = "form-input" v-model = "form.position" placeholder="例如:前端开发工程师" / >
< / view >
< view class = "form-group" >
< text class = "form-label" > 面试公司 < / text >
< input class = "form-input" v-model = "form.company" placeholder="选填" / >
< / view >
< view class = "form-group" >
< text class = "form-label" > 上传方式 < / text >
< view class = "tab-bar" >
< view class = "tab-item" : class = "{ active: uploadMode === 'audio' }" @click ="uploadMode = 'audio'" > 录音文件 < / view >
< view class = "tab-item" : class = "{ active: uploadMode === 'text' }" @click ="uploadMode = 'text'" > 粘贴文本 < / view >
< / view >
< / view >
< ! - - Audio upload mode - - >
< view v-if = "uploadMode === 'audio'" class="audio-upload-area" >
< view class = "upload-box" @click ="chooseFile" >
< text class = "upload-icon" > 📁 < / text >
< text class = "upload-text" v-if = "!form.file" > 点击选择录音文件 < / text >
< text class = "upload-text" v-else > {{ form.file.name }} < / text >
< text class = "upload-hint" > 支持 mp3 / m4a / wav , 最大 50 MB < / text >
< / view >
< / view >
<!-- Text paste mode -- >
< view v-else class = "text-upload-area" >
< textarea
class = "text-input"
v-model = "form.text"
placeholder = "将面试录音转写为文字后粘贴在这里... 示例: 面试官:请介绍一下你自己 候选人:我毕业于..."
:maxlength = "10000"
/ >
< text class = "char-count" > { { form . text . length } } / 10000 < / text >
< / view >
< / view >
< view class = "submit-area" >
< button class = "btn-primary" @click ="submitReview" :disabled = "submitting" >
{ { submitting ? '提交中...' : '开始分析' } }
< / button >
< / view >
< / view >
< / template >
<!-- === === Mode 3 : Processing View === === -- >
< template v-if = "mode === 'processing'" >
< view class = "processing-page" >
< view class = "processing-card" >
< view class = "spinner" > < / view >
< text class = "processing-title" > AI 分析中 < / text >
< text class = "processing-desc" > 正在对面试录音进行深度分析 , 请稍候 ... < / text >
< view class = "process-steps" >
< view class = "step" : class = "{ done: processStep >= 1 }" >
< text class = "step-num" > { { processStep >= 1 ? '✓' : '1' } } < / text >
< text class = "step-label" > 文本转录 < / text >
< / view >
< view class = "step" : class = "{ done: processStep >= 2 }" >
< text class = "step-num" > { { processStep >= 2 ? '✓' : '2' } } < / text >
< text class = "step-label" > 语音分析 < / text >
< / view >
< view class = "step" : class = "{ done: processStep >= 3 }" >
< text class = "step-num" > { { processStep >= 3 ? '✓' : '3' } } < / text >
< text class = "step-label" > AI 评估 < / text >
< / view >
< / view >
< text class = "processing-hint" > 请勿离开此页面 < / text >
< / view >
< / view >
< / template >
<!-- === === Mode 4 : Report View === === -- >
< template v-if = "mode === 'report' && reportData" >
< view class = "report-page" >
< view class = "report-header" >
< text class = "report-back" @click ="mode = 'list'" > ‹ 返回列表 < / text >
< text class = "report-title" > 复盘报告 < / text >
< view style = "width:60rpx" > < / view >
< / view >
< view class = "body" >
< view class = "score-card" >
< view class = "score-circle" :class = "scoreLevel(reportData.analysis.overallScore)" >
< text class = "score-num" > { { reportData . analysis . overallScore } } < / text >
< text class = "score-label" > 总分 < / text >
< / view >
< / view >
< view class = "info-row" >
< text class = "info-label" > 面试岗位 < / text >
< text class = "info-value" > { { reportData . position } } < / text >
< / view >
< view class = "info-row" v-if = "reportData.company" >
< text class = "info-label" > 面试公司 < / text >
< text class = "info-value" > { { reportData . company } } < / text >
< / view >
< view class = "info-row" >
< text class = "info-label" > 分析时间 < / text >
< text class = "info-value" > { { formatDate ( reportData . createdAt ) } } < / text >
< / view >
< view class = "info-row" v-if = "reportData.speechAnalysis" >
< text class = "info-label" > 回答总时长 < / text >
< text class = "info-value" > { { reportData . speechAnalysis . totalDuration } } 秒 < / text >
< / view >
<!-- 四维能力评估 -- >
< view class = "section" v-if = "reportData.analysis.dimensions" >
< view class = "section-title" > 四维能力评估 < / view >
< view class = "dim-grid" >
< view class = "dim-item" v-for = "dim in dimDefs" :key="dim.key" >
< view class = "dim-header" >
< text class = "dim-name" > { { dim . label } } < / text >
< text class = "dim-score" > { { getDimValue ( dim . key ) } } 分 < / text >
< / view >
< view class = "dim-bar-bg" >
< view class = "dim-bar-fill" : style = "{ width: getDimValue(dim.key) + '%', background: dim.color }" > < / view >
< / view >
< / view >
< / view >
< / view >
<!-- 语音分析 -- >
< view class = "section" v-if = "reportData.speechAnalysis" >
< view class = "section-title" > 语音表达分析 < / view >
< view class = "speech-card" >
< view class = "speech-row" >
< text class = "speech-label" > 语气词评分 < / text >
< text class = "speech-value" :class = "scoreLevel(reportData.speechAnalysis.fillerScore)" >
{ { reportData . speechAnalysis . fillerScore } } 分
< / text >
< / view >
< view class = "speech-row" >
< text class = "speech-label" > 语气词密度 < / text >
< text class = "speech-value" > { { reportData . speechAnalysis . fillerDensity } } % < / text >
< / view >
< view class = "speech-row" >
< text class = "speech-label" > 语速 < / text >
< text class = "speech-value" > { { reportData . speechAnalysis . pace } } < / text >
< / view >
< view class = "filler-tags" v-if = "reportData.speechAnalysis.fillerWords.length > 0" >
< text class = "filler-tag" v-for = "fw in reportData.speechAnalysis.fillerWords" :key="fw.word" >
" {{ fw.word }} " × {{ fw.count }}
< / text >
< / view >
< / view >
< / view >
< ! - - 逐题分析 - - >
< view class = "section" v-if = "reportData.analysis.questionBreakdown && reportData.analysis.questionBreakdown.length > 0" >
< view class = "section-title" > 逐题分析 < / view >
< view class = "qa-list" >
< view class = "qa-item" v-for = "(qa, idx) in reportData.analysis.questionBreakdown" :key="idx" >
< view class = "qa-header" >
< text class = "qa-num" > Q { { idx + 1 } } < / text >
< text class = "qa-score" :class = "scoreLevel(qa.score)" > { { qa . score } } 分 < / text >
< / view >
< text class = "qa-question" > { { qa . question } } < / text >
< text class = "qa-answer-label" > 你的回答 : < / text >
< text class = "qa-answer" > { { qa . answer } } < / text >
< view class = "qa-comment" v-if = "qa.comment" >
< text class = "qa-comment-icon" > 💡 < / text >
< text class = "qa-comment-text" > { { qa . comment } } < / text >
< / view >
< view class = "qa-suggest" v-if = "qa.suggestedAnswer" >
< text class = "qa-suggest-icon" > 参考思路 : < / text >
< text class = "qa-suggest-text" > { { qa . suggestedAnswer } } < / text >
< / view >
< / view >
< / view >
< / view >
<!-- 改进建议 -- >
< view class = "section" v-if = "reportData.analysis.strengths.length > 0 || reportData.analysis.weaknesses.length > 0" >
< view class = "section-title" > 改进建议 < / view >
< view class = "sugg-block" v-if = "reportData.analysis.strengths.length > 0" >
< text class = "sugg-block-title sugg-good" > ✅ 表现亮点 < / text >
< text class = "sugg-item" v-for = "(s, i) in reportData.analysis.strengths" :key="'str-' + i" > {{ s }} < / text >
< / view >
< view class = "sugg-block" v-if = "reportData.analysis.weaknesses.length > 0" >
< text class = "sugg-block-title sugg-warn" > ⚠ ️ 待改进 < / text >
< text class = "sugg-item" v-for = "(w, i) in reportData.analysis.weaknesses" :key="'weak-' + i" > {{ w }} < / text >
< / view >
< view class = "sugg-block" v-if = "reportData.analysis.suggestions.length > 0" >
< text class = "sugg-block-title sugg-info" > 💡 具体建议 < / text >
< text class = "sugg-item" v-for = "(s, i) in reportData.analysis.suggestions" :key="'sug-' + i" > {{ s }} < / text >
< / view >
< / view >
< view class = "actions" >
< button class = "btn-primary" @click ="goInterview" > 再去模拟面试练练 < / button >
< button class = "btn-outline" @click ="mode = 'list'" > 返回列表 < / button >
< / view >
< / view >
< / view >
< / template >
< / view >
< / template >
< script setup >
import { ref , computed , onMounted , onUnmounted } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { api } from '../../config'
const mode = ref ( 'list' )
const loading = ref ( false )
const submitting = ref ( false )
const listData = ref ( { items : [ ] , total : 0 , page : 1 , limit : 20 } )
const processStep = ref ( 0 )
const reportData = ref ( null )
const pollTimer = ref ( null )
const reviewId = ref ( '' )
const form = ref ( { position : '' , company : '' , file : null , text : '' } )
const uploadMode = ref ( 'audio' )
const dimDefs = [
{ key : 'logic' , label : '逻辑思维' , color : '#6366F1' } ,
{ key : 'expression' , label : '表达能力' , color : '#10B981' } ,
{ key : 'professionalism' , label : '专业度' , color : '#F59E0B' } ,
{ key : 'stability' , label : '稳定性' , color : '#EF4444' } ,
]
const avgScore = computed ( ( ) => {
const scored = listData . value . items . filter ( i => i . status === 'completed' && i . analysis ? . overallScore )
if ( scored . length === 0 ) return '--'
return Math . round ( scored . reduce ( ( s , i ) => s + ( i . analysis ? . overallScore || 0 ) , 0 ) / scored . length )
} )
const completedCount = computed ( ( ) => {
return listData . value . items . filter ( i => i . status === 'completed' ) . length
} )
const hasMore = computed ( ( ) => {
return listData . value . page < Math . ceil ( listData . value . total / listData . value . limit )
} )
onMounted ( ( ) => { fetchList ( ) } )
onShow ( ( ) => {
if ( mode . value === 'list' ) fetchList ( )
} )
async function fetchList ( page = 1 ) {
const token = uni . getStorageSync ( 'token' ) || ''
if ( ! token ) return
loading . value = true
try {
const res = await uni . request ( {
url : api ( ` /interview-review/list?page= ${ page } &limit=20 ` ) ,
method : 'GET' ,
header : { 'Authorization' : ` Bearer ${ token } ` } ,
} )
if ( res . statusCode === 200 ) {
const data = res . data
if ( page === 1 ) {
listData . value = data
} else {
listData . value = {
... data ,
items : [ ... listData . value . items , ... data . items ] ,
}
}
}
} catch ( e ) { console . error ( e ) }
finally { loading . value = false }
}
function loadMore ( ) {
const next = listData . value . page + 1
fetchList ( next )
listData . value . page = next
}
function chooseFile ( ) {
uni . chooseMessageFile ? uni . chooseMessageFile ( {
count : 1 ,
type : 'file' ,
extension : [ 'mp3' , 'm4a' , 'wav' , 'aac' , 'ogg' ] ,
success : ( r ) => {
if ( r . tempFiles && r . tempFiles [ 0 ] ) {
form . value . file = r . tempFiles [ 0 ]
}
} ,
} ) : uni . showToast ( { title : '当前平台不支持文件选择' , icon : 'none' } )
}
async function submitReview ( ) {
if ( ! form . value . position . trim ( ) ) {
uni . showToast ( { title : '请填写面试岗位' , icon : 'none' } )
return
}
if ( uploadMode . value === 'audio' && ! form . value . file ) {
uni . showToast ( { title : '请选择录音文件' , icon : 'none' } )
return
}
if ( uploadMode . value === 'text' && ! form . value . text . trim ( ) ) {
uni . showToast ( { title : '请粘贴面试转录文本' , icon : 'none' } )
return
}
submitting . value = true
const token = uni . getStorageSync ( 'token' ) || ''
try {
if ( uploadMode . value === 'audio' ) {
const res = await uni . uploadFile ( {
url : api ( '/interview-review' ) ,
filePath : form . value . file . path || form . value . file . tempFilePath ,
name : 'file' ,
formData : {
position : form . value . position . trim ( ) ,
company : form . value . company . trim ( ) ,
} ,
header : { 'Authorization' : ` Bearer ${ token } ` } ,
} )
if ( res . statusCode === 201 || res . statusCode === 200 ) {
const data = typeof res . data === 'string' ? JSON . parse ( res . data ) : res . data
reviewId . value = data . id
startPolling ( data . id )
mode . value = 'processing'
animateSteps ( )
} else {
const err = typeof res . data === 'string' ? JSON . parse ( res . data ) : res . data
uni . showToast ( { title : err . message || '上传失败' , icon : 'none' } )
}
} else {
// Text submission
const res = await uni . request ( {
url : api ( '/interview-review/text' ) ,
method : 'POST' ,
data : {
position : form . value . position . trim ( ) ,
company : form . value . company . trim ( ) ,
text : form . value . text . trim ( ) ,
} ,
header : {
'Authorization' : ` Bearer ${ token } ` ,
'Content-Type' : 'application/json' ,
} ,
} )
if ( res . statusCode === 201 || res . statusCode === 200 ) {
reviewId . value = res . data . id
startPolling ( res . data . id )
mode . value = 'processing'
animateSteps ( )
} else {
uni . showToast ( { title : res . data ? . message || '提交失败' , icon : 'none' } )
}
}
} catch ( e ) {
uni . showToast ( { title : e . message || '网络错误' , icon : 'none' } )
}
finally { submitting . value = false }
}
function animateSteps ( ) {
processStep . value = 1
setTimeout ( ( ) => { processStep . value = 2 } , 3000 )
setTimeout ( ( ) => { processStep . value = 3 } , 6000 )
}
function startPolling ( id ) {
let attempts = 0
const maxAttempts = 60 // 5 minutes max
pollTimer . value = setInterval ( async ( ) => {
attempts ++
if ( attempts > maxAttempts ) {
clearInterval ( pollTimer . value )
uni . showToast ( { title : '分析超时,请稍后再查' , icon : 'none' } )
mode . value = 'list'
return
}
try {
const token = uni . getStorageSync ( 'token' ) || ''
const res = await uni . request ( {
url : api ( ` /interview-review/ ${ id } ` ) ,
method : 'GET' ,
header : { 'Authorization' : ` Bearer ${ token } ` } ,
} )
if ( res . statusCode === 200 ) {
const data = res . data
if ( data . status === 'completed' ) {
clearInterval ( pollTimer . value )
reportData . value = data
mode . value = 'report'
} else if ( data . status === 'failed' ) {
clearInterval ( pollTimer . value )
uni . showToast ( { title : '分析失败,请重新上传' , icon : 'none' } )
mode . value = 'list'
}
// status === 'processing': continue polling
}
} catch ( e ) { console . error ( e ) }
} , 5000 )
}
function viewDetail ( id ) {
const token = uni . getStorageSync ( 'token' ) || ''
uni . request ( {
url : api ( ` /interview-review/ ${ id } ` ) ,
method : 'GET' ,
header : { 'Authorization' : ` Bearer ${ token } ` } ,
success : ( res ) => {
if ( res . statusCode === 200 ) {
if ( res . data . status === 'completed' ) {
reportData . value = res . data
mode . value = 'report'
} else if ( res . data . status === 'processing' ) {
reviewId . value = id
startPolling ( id )
mode . value = 'processing'
} else {
uni . showToast ( { title : '分析失败' , icon : 'none' } )
}
}
} ,
fail : ( ) => { uni . showToast ( { title : '加载失败' , icon : 'none' } ) } ,
} )
}
function getDimValue ( key ) {
return Math . min ( 100 , Math . max ( 0 , Math . round ( reportData . value ? . analysis ? . dimensions ? . [ key ] || 0 ) ) )
}
function formatDate ( d ) {
if ( ! d ) return '--'
const date = new Date ( d )
return ` ${ String ( date . getMonth ( ) + 1 ) . padStart ( 2 , '0' ) } - ${ String ( date . getDate ( ) ) . padStart ( 2 , '0' ) } ${ String ( date . getHours ( ) ) . padStart ( 2 , '0' ) } : ${ String ( date . getMinutes ( ) ) . padStart ( 2 , '0' ) } `
}
const scoreLevel = ( s ) => {
if ( ! s && s !== 0 ) return 'pending'
if ( s >= 80 ) return 'good'
if ( s >= 60 ) return 'medium'
return 'poor'
}
function goInterview ( ) {
uni . switchTab ( { url : '/pages/index/index' } )
}
onUnmounted ( ( ) => {
if ( pollTimer . value ) clearInterval ( pollTimer . value )
} )
< / script >
< style scoped >
. page { background : # F3F4F6 ; min - height : 100 vh ; }
. hero {
background : linear - gradient ( 135 deg , # 4 F46E5 0 % , # 7 C3AED 50 % , # 6366 F1 100 % ) ;
padding : 48 rpx 32 rpx 72 rpx ; border - radius : 0 0 48 rpx 48 rpx ;
}
. hero - title { font - size : 40 rpx ; font - weight : 700 ; color : # FFF ; display : block ; }
. hero - sub { font - size : 22 rpx ; color : rgba ( 255 , 255 , 255 , 0.7 ) ; margin - top : 8 rpx ; display : block ; }
. stats - bar { display : flex ; align - items : center ; padding : 24 rpx ; margin : - 40 rpx 32 rpx 0 ; position : relative ; z - index : 1 ; border - radius : 16 rpx ; background : # FFF ; box - shadow : 0 2 rpx 12 rpx rgba ( 0 , 0 , 0 , 0.06 ) ; }
. stat { flex : 1 ; display : flex ; flex - direction : column ; align - items : center ; gap : 6 rpx ; }
. stat - val { font - size : 36 rpx ; font - weight : 700 ; color : # 4 F46E5 ; }
. stat - lbl { font - size : 20 rpx ; color : # 9 CA3AF ; }
. stat - sep { width : 1 rpx ; height : 40 rpx ; background : # E5E7EB ; }
. list { padding : 20 rpx 32 rpx 160 rpx ; }
. record - card { background : # FFF ; padding : 24 rpx 28 rpx ; margin - bottom : 16 rpx ; border - radius : 16 rpx ; box - shadow : 0 2 rpx 12 rpx rgba ( 0 , 0 , 0 , 0.04 ) ; }
. record - top { display : flex ; align - items : center ; gap : 16 rpx ; }
. record - icon { font - size : 40 rpx ; width : 64 rpx ; height : 64 rpx ; display : flex ; align - items : center ; justify - content : center ; flex - shrink : 0 ; }
. record - body { flex : 1 ; min - width : 0 ; }
. record - name { font - size : 28 rpx ; font - weight : 600 ; color : # 111827 ; }
. record - meta { font - size : 20 rpx ; color : # 9 CA3AF ; margin - top : 6 rpx ; display : block ; }
. record - score { font - size : 28 rpx ; font - weight : 700 ; width : 72 rpx ; text - align : right ; flex - shrink : 0 ; }
. record - score . good { color : # 059669 ; }
. record - score . medium { color : # D97706 ; }
. record - score . poor { color : # DC2626 ; }
. record - score . pending { color : # 9 CA3AF ; }
. empty { display : flex ; flex - direction : column ; align - items : center ; padding : 120 rpx 32 rpx 200 rpx ; }
. empty - icon { font - size : 80 rpx ; margin - bottom : 20 rpx ; }
. empty - title { font - size : 28 rpx ; font - weight : 600 ; color : # 111827 ; }
. empty - desc { font - size : 22 rpx ; color : # 9 CA3AF ; margin - top : 8 rpx ; margin - bottom : 36 rpx ; }
. fixed - bottom { position : fixed ; bottom : 0 ; left : 0 ; right : 0 ; padding : 20 rpx 32 rpx 40 rpx ; background : linear - gradient ( transparent , # F3F4F6 30 rpx ) ; }
. btn - primary { background : linear - gradient ( 135 deg , # 4 F46E5 , # 7 C3AED ) ; color : # FFF ; border - radius : 16 rpx ; height : 88 rpx ; line - height : 88 rpx ; font - size : 28 rpx ; font - weight : 600 ; display : flex ; align - items : center ; justify - content : center ; gap : 8 rpx ; }
. btn - primary : active { opacity : 0.85 ; }
. btn - icon { font - size : 36 rpx ; font - weight : 400 ; }
/* Upload Page */
. upload - page { background : # F3F4F6 ; min - height : 100 vh ; }
. upload - header { display : flex ; align - items : center ; justify - content : space - between ; padding : 24 rpx 32 rpx ; background : # FFF ; }
. upload - back { font - size : 28 rpx ; color : # 4 F46E5 ; }
. upload - title { font - size : 32 rpx ; font - weight : 600 ; color : # 111827 ; }
. form - card { background : # FFF ; border - radius : 16 rpx ; margin : 24 rpx 32 rpx ; padding : 32 rpx ; }
. form - group { margin - bottom : 28 rpx ; }
. form - label { font - size : 26 rpx ; font - weight : 600 ; color : # 374151 ; display : block ; margin - bottom : 12 rpx ; }
. required { color : # DC2626 ; }
. form - input { width : 100 % ; height : 72 rpx ; border : 2 rpx solid # E5E7EB ; border - radius : 12 rpx ; padding : 0 20 rpx ; font - size : 26 rpx ; box - sizing : border - box ; }
. tab - bar { display : flex ; gap : 0 ; background : # F3F4F6 ; border - radius : 12 rpx ; overflow : hidden ; }
. tab - item { flex : 1 ; text - align : center ; padding : 18 rpx 0 ; font - size : 24 rpx ; color : # 6 B7280 ; }
. tab - item . active { background : # 4 F46E5 ; color : # FFF ; font - weight : 600 ; }
. audio - upload - area { margin - top : 8 rpx ; }
. upload - box { border : 2 rpx dashed # D1D5DB ; border - radius : 16 rpx ; padding : 48 rpx 32 rpx ; display : flex ; flex - direction : column ; align - items : center ; gap : 12 rpx ; }
. upload - icon { font - size : 60 rpx ; }
. upload - text { font - size : 26 rpx ; color : # 374151 ; }
. upload - hint { font - size : 20 rpx ; color : # 9 CA3AF ; }
. text - upload - area { margin - top : 8 rpx ; }
. text - input { width : 100 % ; height : 360 rpx ; border : 2 rpx solid # E5E7EB ; border - radius : 12 rpx ; padding : 20 rpx ; font - size : 24 rpx ; line - height : 1.6 ; box - sizing : border - box ; }
. char - count { font - size : 20 rpx ; color : # 9 CA3AF ; text - align : right ; display : block ; margin - top : 8 rpx ; }
. submit - area { padding : 0 32 rpx 48 rpx ; }
/* Processing Page */
. processing - page { display : flex ; align - items : center ; justify - content : center ; min - height : 100 vh ; padding : 32 rpx ; }
. processing - card { background : # FFF ; border - radius : 24 rpx ; padding : 64 rpx 48 rpx ; text - align : center ; box - shadow : 0 8 rpx 40 rpx rgba ( 0 , 0 , 0 , 0.08 ) ; width : 100 % ; max - width : 500 rpx ; }
. spinner { width : 80 rpx ; height : 80 rpx ; border : 6 rpx solid # E5E7EB ; border - top - color : # 4 F46E5 ; border - radius : 50 % ; animation : spin 1 s linear infinite ; margin : 0 auto 32 rpx ; }
@ keyframes spin { to { transform : rotate ( 360 deg ) ; } }
. processing - title { font - size : 32 rpx ; font - weight : 700 ; color : # 111827 ; margin - bottom : 12 rpx ; }
. processing - desc { font - size : 24 rpx ; color : # 6 B7280 ; margin - bottom : 40 rpx ; }
. process - steps { display : flex ; gap : 16 rpx ; justify - content : center ; margin - bottom : 24 rpx ; }
. step { display : flex ; flex - direction : column ; align - items : center ; gap : 8 rpx ; }
. step - num { width : 48 rpx ; height : 48 rpx ; border - radius : 50 % ; background : # F3F4F6 ; color : # 9 CA3AF ; font - size : 22 rpx ; font - weight : 700 ; display : flex ; align - items : center ; justify - content : center ; }
. step . done . step - num { background : # 4 F46E5 ; color : # FFF ; }
. step - label { font - size : 20 rpx ; color : # 9 CA3AF ; }
. step . done . step - label { color : # 4 F46E5 ; }
. processing - hint { font - size : 20 rpx ; color : # D1D5DB ; }
/* Report Page */
. report - page { background : # F3F4F6 ; min - height : 100 vh ; }
. report - header { display : flex ; align - items : center ; justify - content : space - between ; padding : 24 rpx 32 rpx ; background : # FFF ; }
. report - back { font - size : 28 rpx ; color : # 4 F46E5 ; }
. report - title { font - size : 32 rpx ; font - weight : 600 ; color : # 111827 ; }
. body { padding : 0 32 rpx 48 rpx ; }
. score - card { display : flex ; justify - content : center ; margin : - 20 rpx 0 30 rpx ; }
. score - circle {
width : 180 rpx ; height : 180 rpx ; border - radius : 50 % ;
background : # FFF ; display : flex ; flex - direction : column ;
align - items : center ; justify - content : center ;
box - shadow : 0 8 rpx 30 rpx rgba ( 79 , 70 , 229 , 0.2 ) ;
}
. score - circle . good { box - shadow : 0 8 rpx 30 rpx rgba ( 5 , 150 , 105 , 0.2 ) ; }
. score - circle . medium { box - shadow : 0 8 rpx 30 rpx rgba ( 217 , 119 , 6 , 0.2 ) ; }
. score - circle . poor { box - shadow : 0 8 rpx 30 rpx rgba ( 220 , 38 , 38 , 0.2 ) ; }
. score - num { font - size : 56 rpx ; font - weight : 700 ; color : # 4 F46E5 ; line - height : 1.2 ; }
. good . score - num { color : # 059669 ; }
. medium . score - num { color : # D97706 ; }
. poor . score - num { color : # DC2626 ; }
. score - label { font - size : 20 rpx ; color : # 9 CA3AF ; margin - top : 4 rpx ; }
. info - row { display : flex ; justify - content : space - between ; padding : 20 rpx 0 ; border - bottom : 1 rpx solid # E5E7EB ; font - size : 24 rpx ; }
. info - label { color : # 6 B7280 ; }
. info - value { color : # 111827 ; font - weight : 500 ; }
. section { background : # FFF ; border - radius : 20 rpx ; padding : 28 rpx ; margin - top : 24 rpx ; box - shadow : 0 2 rpx 12 rpx rgba ( 0 , 0 , 0 , 0.04 ) ; }
. section - title { font - size : 28 rpx ; font - weight : 600 ; color : # 111827 ; margin - bottom : 16 rpx ; }
. dim - grid { display : flex ; flex - direction : column ; gap : 20 rpx ; }
. dim - header { display : flex ; justify - content : space - between ; margin - bottom : 8 rpx ; }
. dim - name { font - size : 24 rpx ; color : # 374151 ; }
. dim - score { font - size : 24 rpx ; color : # 4 F46E5 ; font - weight : 600 ; }
. dim - bar - bg { height : 20 rpx ; background : # F3F4F6 ; border - radius : 10 rpx ; overflow : hidden ; }
. dim - bar - fill { height : 100 % ; border - radius : 10 rpx ; }
. speech - card { display : flex ; flex - direction : column ; gap : 16 rpx ; }
. speech - row { display : flex ; justify - content : space - between ; align - items : center ; }
. speech - label { font - size : 24 rpx ; color : # 374151 ; }
. speech - value { font - size : 24 rpx ; font - weight : 600 ; color : # 111827 ; }
. speech - value . good { color : # 059669 ; }
. speech - value . medium { color : # D97706 ; }
. speech - value . poor { color : # DC2626 ; }
. filler - tags { display : flex ; flex - wrap : wrap ; gap : 8 rpx ; }
. filler - tag { background : # FEF3C7 ; color : # 92400 E ; padding : 6 rpx 16 rpx ; border - radius : 20 rpx ; font - size : 20 rpx ; }
. qa - list { display : flex ; flex - direction : column ; gap : 24 rpx ; }
. qa - item { padding : 20 rpx ; background : # F9FAFB ; border - radius : 12 rpx ; }
. qa - header { display : flex ; justify - content : space - between ; align - items : center ; margin - bottom : 12 rpx ; }
. qa - num { font - size : 22 rpx ; font - weight : 700 ; color : # 4 F46E5 ; background : # EEF2FF ; padding : 4 rpx 14 rpx ; border - radius : 8 rpx ; }
. qa - score { font - size : 24 rpx ; font - weight : 700 ; }
. qa - score . good { color : # 059669 ; }
. qa - score . medium { color : # D97706 ; }
. qa - score . poor { color : # DC2626 ; }
. qa - score . pending { color : # 9 CA3AF ; }
. qa - question { font - size : 24 rpx ; font - weight : 600 ; color : # 111827 ; margin - bottom : 8 rpx ; display : block ; }
. qa - answer - label { font - size : 20 rpx ; color : # 6 B7280 ; display : block ; margin - top : 8 rpx ; }
. qa - answer { font - size : 22 rpx ; color : # 374151 ; line - height : 1.6 ; display : block ; margin - top : 4 rpx ; white - space : pre - wrap ; }
. qa - comment { background : # EEF2FF ; padding : 12 rpx ; border - radius : 8 rpx ; margin - top : 12 rpx ; display : flex ; gap : 8 rpx ; }
. qa - comment - icon { font - size : 22 rpx ; }
. qa - comment - text { font - size : 22 rpx ; color : # 4338 CA ; line - height : 1.5 ; }
. qa - suggest { background : # FEF3C7 ; padding : 12 rpx ; border - radius : 8 rpx ; margin - top : 8 rpx ; }
. qa - suggest - icon { font - size : 22 rpx ; font - weight : 600 ; color : # 92400 E ; }
. qa - suggest - text { font - size : 22 rpx ; color : # 78350 F ; line - height : 1.5 ; }
. sugg - block { margin - bottom : 20 rpx ; }
. sugg - block - title { font - size : 24 rpx ; font - weight : 600 ; display : block ; margin - bottom : 8 rpx ; }
. sugg - good { color : # 059669 ; }
. sugg - warn { color : # D97706 ; }
. sugg - info { color : # 4 F46E5 ; }
. sugg - item { display : block ; font - size : 22 rpx ; color : # 374151 ; line - height : 1.8 ; padding - left : 20 rpx ; }
. sugg - item : : before { content : '• ' ; color : # D1D5DB ; }
. actions { display : flex ; flex - direction : column ; gap : 16 rpx ; margin - top : 32 rpx ; }
. btn - outline { background : # FFF ; color : # 4 F46E5 ; border - radius : 16 rpx ; height : 88 rpx ; line - height : 88 rpx ; font - size : 28 rpx ; border : 2 rpx solid # 4 F46E5 ; }
. btn - outline : active { background : # F5F3FF ; }
. load - more { text - align : center ; padding : 24 rpx ; color : # 4 F46E5 ; font - size : 24 rpx ; }
< / style >