topic.vue 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688
  1. <script setup lang="ts">
  2. // 导入必要的组件和类型
  3. import { ref, onMounted } from 'vue'
  4. import { useRoute } from 'vue-router'
  5. import { Session } from '/@/utils/storage';
  6. // 定义面试记录的接口
  7. interface InterviewRecord {
  8. question: string
  9. answer: string
  10. analysis: string
  11. score: string
  12. videoUrl?: string
  13. thumbnail?: string
  14. question_form?: number
  15. is_correct?: boolean | string
  16. question_image_url?: string
  17. parent_question_id?: number
  18. follow_up_questions?: InterviewRecord[]
  19. question_id?: number
  20. options?: {
  21. id: number
  22. option_text: string
  23. is_correct: boolean
  24. sort: number
  25. }[]
  26. answer_data?: {
  27. text_answer: string
  28. selected_options: number[]
  29. selected_option_details: {
  30. id: number
  31. is_correct: boolean
  32. option_text: string
  33. }[]
  34. }
  35. }
  36. // 定义响应式数据
  37. const route = useRoute()
  38. const interviewRecords = ref<InterviewRecord[]>([])
  39. const loading = ref(true)
  40. const error = ref('')
  41. // 添加base64编码的内联图片作为fallback
  42. const fallbackImageBase64Ref = ref('data:image/svg+xml;base64,PHN2ZyB...')
  43. // 图片错误处理函数
  44. const handleImageError = (event: Event) => {
  45. const target = event.target as HTMLImageElement
  46. if (target) {
  47. target.src = fallbackImageBase64Ref.value
  48. target.onerror = null
  49. }
  50. }
  51. // 视频相关处理函数
  52. const handleVideoLoaded = (event: Event) => {
  53. const videoElement = event.target as HTMLVideoElement
  54. if (videoElement) {
  55. if (videoElement.readyState >= 1) {
  56. captureVideoFirstFrame(videoElement)
  57. } else {
  58. videoElement.addEventListener('loadedmetadata', () => {
  59. captureVideoFirstFrame(videoElement)
  60. })
  61. }
  62. }
  63. }
  64. const handleVideoError = (event: Event) => {
  65. const videoElement = event.target as HTMLVideoElement
  66. if (videoElement) {
  67. videoElement.poster = fallbackImageBase64Ref.value
  68. console.error('视频加载错误')
  69. }
  70. }
  71. // 获取面试记录数据
  72. const fetchInterviewRecords = async () => {
  73. loading.value = true
  74. try {
  75. const id = route.query.id as string || '1'
  76. const tenant_id = route.query.tenant_id as string || '1'
  77. const application_id = route.query.applicationId as string || '1'
  78. const response = await fetch(`${import.meta.env.VITE_API_URL}/api/job/application_detail?tenant_id=${Session.get('tenant_id')}&application_id=${application_id}`)
  79. const result = await response.json()
  80. if (result.code === 2000 && result.data?.interview_progress) {
  81. // 处理面试记录
  82. const processedRecords = result.data.interview_progress
  83. /* .filter((q: any) => q.video_answer || q.answer_data) */
  84. .map((q: any) => {
  85. const record: InterviewRecord = {
  86. question: q.question_text || '未提供问题',
  87. answer: '',
  88. analysis: '',
  89. score: '',
  90. question_form: q.question_form,
  91. parent_question_id: q.parent_question_id,
  92. options: q.options,
  93. answer_data: q.answer_data,
  94. follow_up_questions: [],
  95. question_id: q.question_id
  96. }
  97. if (q.video_answer) {
  98. record.answer = q.video_answer.transcript || ''
  99. record.analysis = q.video_answer.ai_analysis?.comment || ''
  100. record.score = q.video_answer.ai_score ? `${q.video_answer.ai_score}分` : ''
  101. record.videoUrl = q.video_answer.video_url || ''
  102. }
  103. if (q.answer_data) {
  104. record.answer = q.answer_data.text_answer || ''
  105. record.is_correct = q.answer_data.selected_option_details?.some((opt: any) => opt.is_correct) || false
  106. }
  107. if (q.question_form === 3) {
  108. record.question_image_url = q.question_image_url || ''
  109. }
  110. return record
  111. })
  112. // 将记录分为主题和追问两组
  113. const mainQuestions: InterviewRecord[] = []
  114. const followUpQuestions: InterviewRecord[] = []
  115. processedRecords.forEach((record: InterviewRecord) => {
  116. if (record.question_form === 5 && record.parent_question_id) {
  117. followUpQuestions.push(record)
  118. } else {
  119. mainQuestions.push(record)
  120. }
  121. })
  122. // 对主题进行排序
  123. const sortedMainQuestions = mainQuestions.sort((a: InterviewRecord, b: InterviewRecord) => {
  124. const getPriority = (questionForm: number) => {
  125. switch (questionForm) {
  126. case 0: return 1; // 视频问答题最优先
  127. case 5: return 2; // 开放问答题次优先
  128. case 1: return 3; // 单选题
  129. case 2: return 4; // 多选题
  130. case 3: return 5; // 图片选择题
  131. case 4: return 6; // 问卷题
  132. default: return 99;
  133. }
  134. }
  135. return getPriority(a.question_form || -1) - getPriority(b.question_form || -1)
  136. })
  137. // 将追问题数组倒序
  138. const reversedFollowUps = [...followUpQuestions].reverse()
  139. // 将倒序后的追问题添加到对应的父问题中
  140. reversedFollowUps.forEach(followUp => {
  141. const parentQuestion = sortedMainQuestions.find(
  142. q => q.question_form === 0 && q.question_id === followUp.parent_question_id
  143. )
  144. if (parentQuestion) {
  145. // 如果follow_up_questions还没有初始化,先初始化为空数组
  146. if (!parentQuestion.follow_up_questions) {
  147. parentQuestion.follow_up_questions = []
  148. }
  149. // 使用unshift而不是push,这样新的追问题会被添加到数组开头
  150. parentQuestion.follow_up_questions.unshift(followUp)
  151. }
  152. })
  153. // 设置最终结果
  154. interviewRecords.value = sortedMainQuestions
  155. console.log(interviewRecords.value)
  156. } else {
  157. error.value = result.msg || '获取数据失败'
  158. }
  159. } catch (err) {
  160. console.error('获取面试记录失败:', err)
  161. error.value = '获取数据失败,请稍后重试'
  162. } finally {
  163. loading.value = false
  164. }
  165. }
  166. // 在script setup部分添加以下函数
  167. const captureVideoFirstFrame = (videoElement: HTMLVideoElement) => {
  168. // 创建一个一次性的事件监听器,在视频可以播放时捕获第一帧
  169. const captureFrame = () => {
  170. try {
  171. // 确保视频已加载足够的数据
  172. if (videoElement.readyState >= 2) {
  173. // 创建canvas元素
  174. const canvas = document.createElement('canvas')
  175. canvas.width = videoElement.videoWidth || 320
  176. canvas.height = videoElement.videoHeight || 240
  177. // 在canvas上绘制视频当前帧
  178. const ctx = canvas.getContext('2d')
  179. if (ctx) {
  180. // 确保视频当前时间设置为开始位置
  181. videoElement.currentTime = 0.1
  182. // 绘制视频帧到canvas
  183. ctx.drawImage(videoElement, 0, 0, canvas.width, canvas.height)
  184. // 将canvas转换为图片URL
  185. const thumbnailUrl = canvas.toDataURL('image/jpeg')
  186. // 设置为视频的poster属性
  187. videoElement.poster = thumbnailUrl
  188. // 清理
  189. videoElement.removeEventListener('loadeddata', captureFrame)
  190. }
  191. }
  192. } catch (error) {
  193. console.error('捕获视频第一帧失败:', error)
  194. // 设置默认缩略图
  195. videoElement.poster = fallbackImageBase64Ref.value
  196. }
  197. }
  198. // 添加事件监听器
  199. videoElement.addEventListener('loadeddata', captureFrame)
  200. // 如果视频已经加载,立即尝试捕获
  201. if (videoElement.readyState >= 2) {
  202. captureFrame()
  203. }
  204. // 添加错误处理
  205. videoElement.addEventListener('error', () => {
  206. console.error('视频加载失败')
  207. videoElement.poster = fallbackImageBase64Ref.value
  208. })
  209. // 设置超时,确保即使视频加载缓慢也能显示默认缩略图
  210. setTimeout(() => {
  211. if (!videoElement.poster || videoElement.poster === '') {
  212. videoElement.poster = fallbackImageBase64Ref.value
  213. }
  214. }, 3000)
  215. }
  216. // 在script setup中添加题型名称映射函数
  217. const getQuestionTypeName = (questionForm: number): string => {
  218. const typeMap: Record<number, string> = {
  219. 0: '视频问答题(AI面试官提问)',
  220. 1: '单选题(知识检测)',
  221. 2: '多选题(综合选择)',
  222. 3: '图片选择题(场景判断)',
  223. 4: '问卷题(个性测评)',
  224. 5: '开放问答题(追问互动)',
  225. 6: '填空题'
  226. }
  227. return typeMap[questionForm] || '未知题型'
  228. }
  229. onMounted(() => {
  230. fetchInterviewRecords()
  231. })
  232. </script>
  233. <template>
  234. <div class="page-container">
  235. <div class="content-container">
  236. <div class="max-w-4xl mx-auto p-6">
  237. <!-- 加载状态 -->
  238. <a-spin :spinning="loading" tip="加载中...">
  239. <!-- 错误提示 -->
  240. <div v-if="error" class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative" role="alert">
  241. <strong class="font-bold">错误!</strong>
  242. <span class="block sm:inline">{{ error }}</span>
  243. </div>
  244. <!-- 暂无数据提示 -->
  245. <div v-else-if="!loading && (!interviewRecords || interviewRecords.length === 0)"
  246. class="flex flex-col items-center justify-center py-12 bg-gray-50 rounded-lg">
  247. <svg class="w-16 h-16 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  248. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
  249. d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z">
  250. </path>
  251. </svg>
  252. <h3 class="mt-4 text-lg font-medium text-gray-900">暂无面试记录</h3>
  253. <p class="mt-1 text-sm text-gray-500">当前没有任何面试记录数据</p>
  254. </div>
  255. <!-- 面试记录列表 -->
  256. <div v-else-if="interviewRecords.length > 0" class="space-y-4">
  257. <div v-for="(record, index) in interviewRecords" :key="index" class="bg-gray-50 rounded-lg p-4">
  258. <!-- 题目标题部分 -->
  259. <div class="mb-4">
  260. <div class="flex items-start space-x-2">
  261. <div class="bg-blue-100 text-blue-800 px-2 py-1 rounded text-sm">第{{ index + 1 }}题</div>
  262. <div class="bg-gray-100 text-gray-800 px-2 py-1 rounded text-sm">{{ getQuestionTypeName(record.question_form || 0) }}</div>
  263. <div class="flex-1">
  264. <h3 class="font-semibold text-gray-800">{{ record.question }}</h3>
  265. </div>
  266. </div>
  267. <!-- 选择题布局 -->
  268. <div v-if="record.question_form === 1 || record.question_form === 2 || record.question_form === 4" class="mt-4">
  269. <div class="bg-white p-4 rounded-lg">
  270. <!-- 显示选项 -->
  271. <div v-if="record.options && record.options.length > 0" class="space-y-3">
  272. <div v-for="(option, optIndex) in record.options" :key="optIndex"
  273. :class="[
  274. 'p-3 rounded border',
  275. record.answer_data?.selected_option_details?.some(
  276. selected => selected.id === option.id
  277. )
  278. ? 'border-blue-300 bg-blue-50'
  279. : 'border-gray-200'
  280. ]">
  281. <div class="flex items-center justify-between">
  282. <div class="flex-1">
  283. <span class="text-gray-700">{{ option.option_text }}</span>
  284. </div>
  285. <div class="flex items-center space-x-2">
  286. <template v-if="record.question_form !== 4">
  287. <span v-if="option.is_correct" class="text-green-500">✓ 正确答案</span>
  288. <span v-if="record.answer_data?.selected_option_details?.some(
  289. selected => selected.id === option.id
  290. )"
  291. :class="option.is_correct ? 'text-green-500' : 'text-red-500'">
  292. {{ option.is_correct ? '(已选 - 正确)' : '(已选 - 错误)' }}
  293. </span>
  294. </template>
  295. <template v-else>
  296. <span v-if="record.answer_data?.selected_option_details?.some(
  297. selected => selected.id === option.id
  298. )"
  299. class="text-blue-500">
  300. (已选)
  301. </span>
  302. </template>
  303. </div>
  304. </div>
  305. </div>
  306. </div>
  307. <!-- 显示答案状态 -->
  308. <div class="mt-4 flex justify-between items-center">
  309. <div class="text-gray-600">
  310. 候选人: {{ record.answer_data?.selected_option_details?.map(
  311. opt => opt.option_text
  312. ).join(', ') || '未作答' }}
  313. </div>
  314. <div v-if="record.question_form !== 4" class="w-1/2 text-right">
  315. <span :class="[
  316. 'px-3 py-1 rounded-full text-sm',
  317. record.answer_data?.selected_option_details?.every(
  318. opt => opt.is_correct
  319. )
  320. ? 'bg-green-100 text-green-800'
  321. : 'bg-red-100 text-red-800'
  322. ]">
  323. {{ record.answer_data?.selected_option_details?.every(
  324. opt => opt.is_correct
  325. ) ? '正确' : '错误' }}
  326. </span>
  327. </div>
  328. </div>
  329. </div>
  330. </div>
  331. <!-- 视频问答题布局 -->
  332. <div v-else-if="record.question_form === 0 || record.question_form === 5" class="mt-4">
  333. <!-- 原有的视频问答题布局 -->
  334. <div class="flex space-x-4">
  335. <!-- 右侧答案和评分 -->
  336. <div class="w-2/3">
  337. <div class="bg-white p-4 rounded-lg">
  338. <div class="mb-4">
  339. <div class="flex items-start space-x-2">
  340. <div class="bg-green-100 text-green-800 px-2 py-1 rounded text-sm">候选人</div>
  341. <div class="flex-1">
  342. <p class="text-gray-700">{{ record.answer }}</p>
  343. </div>
  344. </div>
  345. </div>
  346. <div>
  347. <div class="flex items-start space-x-2">
  348. <div class="bg-yellow-100 text-yellow-800 px-2 py-1 rounded text-sm">AI评分</div>
  349. <div class="flex-1">
  350. <p class="text-gray-600 m-0">{{ record.analysis }}</p>
  351. <div class="mt-1 text-blue-500">得分:{{ record.score }}</div>
  352. </div>
  353. </div>
  354. </div>
  355. </div>
  356. </div>
  357. <!-- 左侧视频播放器 -->
  358. <div class="w-1/3">
  359. <div class="video-container">
  360. <video
  361. class="w-full h-full object-cover rounded-lg"
  362. controls
  363. :src="record.videoUrl"
  364. preload="metadata"
  365. :poster="fallbackImageBase64Ref"
  366. @loadeddata="handleVideoLoaded"
  367. @error="handleVideoError"
  368. >
  369. 您的浏览器不支持视频播放。
  370. </video>
  371. </div>
  372. </div>
  373. </div>
  374. <!-- 追问题列表 -->
  375. <div v-if="record.follow_up_questions && record.follow_up_questions.length > 0"
  376. class="mt-4 pl-8 border-l-2 border-gray-200">
  377. <div v-for="(followUp, fIndex) in record.follow_up_questions"
  378. :key="fIndex"
  379. class="mb-4">
  380. <div class="flex items-start space-x-2">
  381. <div class="bg-purple-100 text-purple-800 px-2 py-1 rounded text-sm">追问题</div>
  382. <div class="flex-1">
  383. <h4 class="font-medium text-gray-800">{{ followUp.question }}</h4>
  384. <!-- 追问题的视频和答案布局 -->
  385. <div class="mt-2">
  386. <div class="flex space-x-4">
  387. <!-- 右侧答案和评分 -->
  388. <div class="w-2/3">
  389. <div class="bg-white p-4 rounded-lg">
  390. <div class="mb-4">
  391. <div class="flex items-start space-x-2">
  392. <div class="bg-green-100 text-green-800 px-2 py-1 rounded text-sm">候选人</div>
  393. <div class="flex-1">
  394. <p class="text-gray-700">{{ followUp.answer }}</p>
  395. </div>
  396. </div>
  397. </div>
  398. <div>
  399. <div class="flex items-start space-x-2">
  400. <div class="bg-yellow-100 text-yellow-800 px-2 py-1 rounded text-sm">AI评分</div>
  401. <div class="flex-1">
  402. <p class="text-gray-600 m-0">{{ followUp.analysis }}</p>
  403. <div class="mt-1 text-blue-500">得分:{{ followUp.score }}</div>
  404. </div>
  405. </div>
  406. </div>
  407. </div>
  408. </div>
  409. <!-- 左侧视频播放器 -->
  410. <div v-if="followUp.videoUrl" class="w-1/3">
  411. <div class="video-container">
  412. <video
  413. class="w-full h-full object-cover rounded-lg"
  414. controls
  415. :src="followUp.videoUrl"
  416. preload="metadata"
  417. :poster="fallbackImageBase64Ref"
  418. @loadeddata="handleVideoLoaded"
  419. @error="handleVideoError"
  420. >
  421. 您的浏览器不支持视频播放。
  422. </video>
  423. </div>
  424. </div>
  425. </div>
  426. </div>
  427. </div>
  428. </div>
  429. </div>
  430. </div>
  431. </div>
  432. <!-- 图片题布局 -->
  433. <div v-else-if="record.question_form === 3" class="mt-4">
  434. <div class="grid grid-cols-2 gap-4">
  435. <!-- 左侧图片显示 -->
  436. <div class="bg-white p-4 rounded-lg">
  437. <div class="aspect-w-16 aspect-h-9 rounded-lg overflow-hidden bg-gray-100">
  438. <img
  439. :src="record.question_image_url"
  440. alt="题目图片"
  441. class="w-full h-full object-contain"
  442. @error="handleImageError"
  443. >
  444. </div>
  445. </div>
  446. <!-- 右侧选项和答案 -->
  447. <div class="bg-white p-4 rounded-lg">
  448. <!-- 显示选项 -->
  449. <div v-if="record.options && record.options.length > 0" class="space-y-3">
  450. <div v-for="(option, optIndex) in record.options" :key="optIndex"
  451. :class="[
  452. 'p-3 rounded border',
  453. record.answer_data?.selected_option_details?.some(
  454. selected => selected.id === option.id
  455. )
  456. ? 'border-blue-300 bg-blue-50'
  457. : 'border-gray-200'
  458. ]">
  459. <div class="flex items-center justify-between">
  460. <div class="flex-1">
  461. <span class="text-gray-700">{{ option.option_text }}</span>
  462. </div>
  463. <div class="flex items-center space-x-2">
  464. <span v-if="option.is_correct" class="text-green-500">✓ 正确答案</span>
  465. <span v-if="record.answer_data?.selected_option_details?.some(
  466. selected => selected.id === option.id
  467. )"
  468. :class="option.is_correct ? 'text-green-500' : 'text-red-500'">
  469. {{ option.is_correct ? '(已选 - 正确)' : '(已选 - 错误)' }}
  470. </span>
  471. </div>
  472. </div>
  473. </div>
  474. </div>
  475. <!-- 显示答案状态 -->
  476. <div class="mt-4 flex justify-between items-center">
  477. <div class="text-gray-600">
  478. 答案: {{ record.answer_data?.selected_option_details?.map(
  479. opt => opt.option_text
  480. ).join(', ') || '未作答' }}
  481. </div>
  482. <div>
  483. <span :class="[
  484. 'px-3 py-1 rounded-full text-sm',
  485. record.answer_data?.selected_option_details?.every(
  486. opt => opt.is_correct
  487. )
  488. ? 'bg-green-100 text-green-800'
  489. : 'bg-red-100 text-red-800'
  490. ]">
  491. {{ record.answer_data?.selected_option_details?.every(
  492. opt => opt.is_correct
  493. ) ? '正确' : '错误' }}
  494. </span>
  495. </div>
  496. </div>
  497. </div>
  498. </div>
  499. </div>
  500. <!-- 填空题 -->
  501. <div v-if="record.question_form === 6" class="mt-4">
  502. <div class="bg-white p-4 rounded-lg">
  503. <!-- 显示选项 -->
  504. <!-- <div v-if="record.options && record.options.length > 0" class="space-y-3">
  505. <div v-for="(option, optIndex) in record.options" :key="optIndex"
  506. :class="[
  507. 'p-3 rounded border',
  508. record.answer_data?.selected_option_details?.some(
  509. selected => selected.id === option.id
  510. )
  511. ? 'border-blue-300 bg-blue-50'
  512. : 'border-gray-200'
  513. ]">
  514. <div class="flex items-center justify-between">
  515. <div class="flex-1">
  516. <span class="text-gray-700">{{ option.option_text }}</span>
  517. </div>
  518. <div class="flex items-center space-x-2">
  519. <template v-if="record.question_form !== 4">
  520. <span v-if="option.is_correct" class="text-green-500">✓ 正确答案</span>
  521. <span v-if="record.answer_data?.selected_option_details?.some(
  522. selected => selected.id === option.id
  523. )"
  524. :class="option.is_correct ? 'text-green-500' : 'text-red-500'">
  525. {{ option.is_correct ? '(已选 - 正确)' : '(已选 - 错误)' }}
  526. </span>
  527. </template>
  528. <template v-else>
  529. <span v-if="record.answer_data?.selected_option_details?.some(
  530. selected => selected.id === option.id
  531. )"
  532. class="text-blue-500">
  533. (已选)
  534. </span>
  535. </template>
  536. </div>
  537. </div>
  538. </div>
  539. </div> -->
  540. <!-- 显示答案状态 -->
  541. <div class="mt-4 flex justify-between items-center">
  542. <div class="text-gray-600" >
  543. 候选人作答: {{ record.answer_data?.blank_answers?.map(
  544. opt => opt.answer_text
  545. ).join(', ') || '未作答' }}
  546. </div>
  547. <div v-if="record.question_form !== 4">
  548. <span :class="[
  549. 'px-3 py-1 rounded-full text-sm',
  550. record.answer_data?.blank_answers?.every(
  551. opt => opt.is_correct
  552. )
  553. ? 'bg-green-100 text-green-800'
  554. : 'bg-red-100 text-red-800'
  555. ]">
  556. {{ record.answer_data?.blank_answers?.every(
  557. opt => opt.is_correct
  558. ) ? '正确' : '错误' }}
  559. </span>
  560. </div>
  561. </div>
  562. </div>
  563. </div>
  564. </div>
  565. </div>
  566. </div>
  567. </a-spin>
  568. </div>
  569. </div>
  570. </div>
  571. </template>
  572. <style scoped>
  573. .video-container {
  574. position: relative;
  575. width: 100%;
  576. max-width: 240px;
  577. aspect-ratio: 3/4;
  578. margin: 0 auto;
  579. background: #000;
  580. border-radius: 12px;
  581. overflow: hidden;
  582. }
  583. /* 确保内容区域可以正常滚动 */
  584. .max-w-4xl {
  585. position: relative;
  586. height: auto;
  587. padding-bottom: 2rem;
  588. }
  589. /* 打印样式 */
  590. @media print {
  591. /* 确保内容完整显示 */
  592. .max-w-4xl {
  593. max-width: 100% !important;
  594. padding: 0 !important;
  595. margin: 0 !important;
  596. }
  597. /* 调整页面边距 */
  598. @page {
  599. margin: 1cm;
  600. }
  601. /* 确保背景色和图片打印 */
  602. * {
  603. -webkit-print-color-adjust: exact !important;
  604. color-adjust: exact !important;
  605. print-color-adjust: exact !important;
  606. }
  607. /* 避免视频容器在打印时出现问题 */
  608. .video-container {
  609. page-break-inside: avoid;
  610. break-inside: avoid;
  611. }
  612. }
  613. /* 响应式布局 */
  614. @media (max-width: 768px) {
  615. .max-w-4xl {
  616. padding: 1rem;
  617. }
  618. .grid-cols-3 {
  619. grid-template-columns: 1fr;
  620. }
  621. }
  622. /* 添加新的样式 */
  623. .page-container {
  624. height: 100vh;
  625. overflow-y: auto;
  626. background-color: white;
  627. }
  628. .content-container {
  629. max-width: 64rem; /* 4xl = 64rem */
  630. margin: 0 auto;
  631. padding: 1.5rem;
  632. }
  633. </style>