region.vue 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733
  1. @ -1,702 +0,0 @@
  2. <template>
  3. <div class="region_container">
  4. <!-- 薄弱补强区 -->
  5. <div class="region_section weak">
  6. <div class="region_header">
  7. <div class="region_header_main">
  8. <span class="region_icon">
  9. <img :src="require('@/assets/knowledgeGraph/buqiang.png')" alt="薄弱补强区" style="width: 32px; height: auto; display: block;" />
  10. </span>
  11. <span class="region_title">薄弱补强区</span>
  12. </div>
  13. <span class="region_desc">得分率<60% | 优先补齐短板</span>
  14. </div>
  15. <div class="knowledge_rows" ref="weakRowsBox">
  16. <div
  17. class="knowledge_row"
  18. v-for="(item, index) in weakArea"
  19. :key="'weak-' + index"
  20. :data-knowledge-id="item.knowledgeId"
  21. @click="handleRowClick(item)"
  22. :class="{ selected: isKnowledgeSelected(item), highlighted: isKnowledgeSelected(item) }"
  23. >
  24. <div class="row_left">
  25. <span class="kp_name" :title="item.knowledgeName">{{ item.knowledgeName }}</span>
  26. <div class="kp_meta" :title="'得分率:' + getScoreRate(item) + '% 考频:' + getExamFreq(item)">
  27. <span class="kp_score">得分率:{{ getScoreRate(item) }}%</span>
  28. <span class="kp_freq_tag">考频:{{ getExamFreq(item) }}</span>
  29. </div>
  30. </div>
  31. <div class="row_right">
  32. <el-button size="small" class="add_btn weak_btn"
  33. :disabled="isAddPracticeDisabled(item)"
  34. @click.stop="handleAddPractice(item, $event)">
  35. 加入练习
  36. </el-button>
  37. </div>
  38. </div>
  39. <div v-if="weakArea.length === 0" class="empty_tip no_content_data"><span>暂无数据</span></div>
  40. </div>
  41. </div>
  42. <!-- 能力提升区 -->
  43. <div class="region_section good">
  44. <div class="region_header">
  45. <div class="region_header_main">
  46. <span class="region_icon">
  47. <img :src="require('@/assets/knowledgeGraph/tisheng.png')" alt="能力提升区" style="width: 32px; height: auto; display: block;" />
  48. </span>
  49. <span class="region_title">能力提升区</span>
  50. </div>
  51. <span class="region_desc">60%≤得分率<85% | 强化练习提升</span>
  52. </div>
  53. <div class="knowledge_rows" ref="goodRowsBox">
  54. <div
  55. class="knowledge_row"
  56. v-for="(item, index) in goodArea"
  57. :key="'good-' + index"
  58. :data-knowledge-id="item.knowledgeId"
  59. @click="handleRowClick(item)"
  60. :class="{ selected: isKnowledgeSelected(item), highlighted: isKnowledgeSelected(item) }"
  61. >
  62. <div class="row_left">
  63. <span class="kp_name" :title="item.knowledgeName">{{ item.knowledgeName }}</span>
  64. <div class="kp_meta" :title="'得分率:' + getScoreRate(item) + '% 考频:' + getExamFreq(item)">
  65. <span class="kp_score">得分率:{{ getScoreRate(item) }}%</span>
  66. <span class="kp_freq_tag">考频:{{ getExamFreq(item) }}</span>
  67. </div>
  68. </div>
  69. <div class="row_right">
  70. <el-button size="small" class="add_btn good_btn"
  71. :disabled="isAddPracticeDisabled(item)"
  72. @click.stop="handleAddPractice(item, $event)">
  73. 加入练习
  74. </el-button>
  75. </div>
  76. </div>
  77. <div v-if="goodArea.length === 0" class="empty_tip no_content_data"><span>暂无数据</span></div>
  78. </div>
  79. </div>
  80. <!-- 扎实巩固区 -->
  81. <div class="region_section excellent">
  82. <div class="region_header">
  83. <div class="region_header_main">
  84. <span class="region_icon">
  85. <img :src="require('@/assets/knowledgeGraph/gonggu.png')" alt="扎实巩固区" style="width: 32px; height: auto; display: block;" />
  86. </span>
  87. <span class="region_title">扎实巩固区</span>
  88. </div>
  89. <span class="region_desc">得分率>85% | 定期练习巩固</span>
  90. </div>
  91. <div class="knowledge_rows" ref="excellentRowsBox">
  92. <div
  93. class="knowledge_row"
  94. v-for="(item, index) in excellentArea"
  95. :key="'excellent-' + index"
  96. :data-knowledge-id="item.knowledgeId"
  97. @click="handleRowClick(item)"
  98. :class="{ selected: isKnowledgeSelected(item), highlighted: isKnowledgeSelected(item) }"
  99. >
  100. <div class="row_left">
  101. <span class="kp_name" :title="item.knowledgeName">{{ item.knowledgeName }}</span>
  102. <div class="kp_meta" :title="'得分率:' + getScoreRate(item) + '% 考频:' + getExamFreq(item)">
  103. <span class="kp_score">得分率:{{ getScoreRate(item) }}%</span>
  104. <span class="kp_freq_tag">考频:{{ getExamFreq(item) }}</span>
  105. </div>
  106. </div>
  107. <div class="row_right">
  108. <el-button size="small" class="add_btn excellent_btn"
  109. :disabled="isAddPracticeDisabled(item)"
  110. @click.stop="handleAddPractice(item, $event)">
  111. 加入练习
  112. </el-button>
  113. </div>
  114. </div>
  115. <div v-if="excellentArea.length === 0" class="empty_tip no_content_data"><span>暂无数据</span></div>
  116. </div>
  117. </div>
  118. <!-- 冲刺进阶区(暂时隐藏) -->
  119. <div class="region_section advanced" >
  120. <div class="region_header">
  121. <div class="region_header_main">
  122. <span class="region_icon">
  123. <img :src="require('@/assets/knowledgeGraph/jinjie.png')" alt="冲刺进阶区" style="width: 32px; height: auto; display: block;" />
  124. </span>
  125. <span class="region_title">冲刺进阶区</span>
  126. </div>
  127. <span class="region_desc">优于同层级学生 | 对标上游冲刺进阶</span>
  128. </div>
  129. <div class="knowledge_rows" ref="advancedRowsBox">
  130. <div
  131. class="knowledge_row"
  132. v-for="(item, index) in advancedArea"
  133. :key="'advanced-' + index"
  134. :data-knowledge-id="item.knowledgeId"
  135. @click="handleRowClick(item)"
  136. :class="{ selected: isKnowledgeSelected(item), highlighted: isKnowledgeSelected(item) }"
  137. >
  138. <div class="row_left">
  139. <span class="kp_name" :title="item.knowledgeName">{{ item.knowledgeName }}</span>
  140. <div class="kp_meta" :title="'得分率:' + getScoreRate(item) + '% 考频:' + getExamFreq(item)">
  141. <span class="kp_score">得分率:{{ getScoreRate(item) }}%</span>
  142. <span class="kp_freq_tag">考频:{{ getExamFreq(item) }}</span>
  143. </div>
  144. </div>
  145. <div class="row_right">
  146. <el-button size="small" class="add_btn advanced_btn"
  147. :disabled="isAddPracticeDisabled(item)"
  148. @click.stop="handleAddPractice(item, $event)">
  149. 加入练习
  150. </el-button>
  151. </div>
  152. </div>
  153. <div v-if="advancedArea.length === 0" class="empty_tip no_content_data"><span>暂无数据</span></div>
  154. </div>
  155. </div>
  156. </div>
  157. </template>
  158. <script>
  159. export default {
  160. name: 'Region',
  161. props: {
  162. // 分区数据(由父级通过 knowledge_partition 接口返回后传入)
  163. // conquerZone: 薄弱补强区
  164. // improveZone: 能力提升区
  165. // consolidateZone:扎实巩固区
  166. // sprintZone: 冲刺进阶区(接口字段:sprint)
  167. partitionData: {
  168. type: Object,
  169. default: () => ({
  170. conquerZone: [],
  171. improveZone: [],
  172. consolidateZone: [],
  173. sprintZone: []
  174. })
  175. },
  176. // 父级传入的"联动选中知识点ID"
  177. // - 环形扩散图选中的圆点,父级会把 knowledgeId 写到这里 → 本组件高亮对应行并滚动居中
  178. // - 点本组件任一行条目时,也通过 emit('item-click') 反向通知父级把 ID 同步到 circularDiffusion
  179. selectedKnowledgeId: {
  180. type: [String, Number],
  181. default: ''
  182. }
  183. },
  184. data() {
  185. return {};
  186. },
  187. watch: {
  188. // 父级 selectedKnowledgeId 变化:联动高亮 + 滚动到对应区列表的可视中央
  189. selectedKnowledgeId: {
  190. immediate: true,
  191. handler(newVal) {
  192. if (!newVal) return;
  193. const sid = String(newVal);
  194. // 这里只负责发起"滚动尝试",函数内部会处理 DOM/数据未就绪时的重试
  195. this.scheduleHighlightScroll(sid);
  196. }
  197. },
  198. // partitionData 变化:路由进入组件时,父级接口返回前 selectedKnowledgeId 可能已经写好了,
  199. // 但此时 conquerZone/improveZone/consolidateZone 都是空数组 → 上面的 watch 已经执行过(immediate)但没找到条目,不会再自动重跑。
  200. // 所以 partitionData 有内容变化后,只要还有选中 ID,就重新触发一次滚动。
  201. partitionData: {
  202. deep: true,
  203. handler(newVal, oldVal) {
  204. // 快速跳过空对象/完全一样的引用
  205. if (!this.selectedKnowledgeId) return;
  206. const lists = ['conquerZone', 'improveZone', 'consolidateZone', 'sprintZone'];
  207. let hasAnyData = false;
  208. if (newVal) {
  209. for (const k of lists) {
  210. if (Array.isArray(newVal[k]) && newVal[k].length > 0) { hasAnyData = true; break; }
  211. }
  212. }
  213. if (!hasAnyData) return;
  214. // 接口数据刚落地,Vue 下一 tick 才会 patch DOM 行节点;所以 $nextTick 再排
  215. const sid = String(this.selectedKnowledgeId);
  216. this.$nextTick(() => this.scheduleHighlightScroll(sid));
  217. }
  218. }
  219. },
  220. activated() {
  221. // keep-alive 缓存路由时,组件被切回来会触发 activated,但 watch 不会再立即执行
  222. // 只要有选中 ID 就重新尝试滚动居中(DOM 已就绪,命中会直接滚)
  223. if (this.selectedKnowledgeId) {
  224. const sid = String(this.selectedKnowledgeId);
  225. this.$nextTick(() => this.scheduleHighlightScroll(sid));
  226. }
  227. },
  228. computed: {
  229. // 薄弱补强区:直接用接口返回的 conquerZone
  230. weakArea() {
  231. return Array.isArray(this.partitionData?.conquerZone) ? this.partitionData.conquerZone : [];
  232. },
  233. // 能力提升区:直接用接口返回的 improveZone
  234. goodArea() {
  235. return Array.isArray(this.partitionData?.improveZone) ? this.partitionData.improveZone : [];
  236. },
  237. // 扎实巩固区:直接用接口返回的 consolidateZone
  238. excellentArea() {
  239. return Array.isArray(this.partitionData?.consolidateZone) ? this.partitionData.consolidateZone : [];
  240. },
  241. // 冲刺进阶区:使用接口返回的 sprint 数组(父级映射为 partitionData.sprintZone)
  242. advancedArea() {
  243. return Array.isArray(this.partitionData?.sprintZone) ? this.partitionData.sprintZone : [];
  244. }
  245. },
  246. methods: {
  247. // 获取得分率:优先 personalScoreRate(接口分区字段),兜底 class / grade
  248. getScoreRate(item) {
  249. if (item && (item.personalScoreRate !== null && item.personalScoreRate !== undefined)) {
  250. const n = Number(item.personalScoreRate);
  251. return isNaN(n) ? 0 : n;
  252. }
  253. const fallback = item?.gradeScoreRate ?? item?.classScoreRate;
  254. if (fallback === null || fallback === undefined) return 0;
  255. const n = Number(fallback);
  256. return isNaN(n) ? 0 : n;
  257. },
  258. // 获取考频:优先 examNum(接口分区字段:考频,含练习),兜底常见字段
  259. getExamFreq(item) {
  260. if (item && (item.examNum !== null && item.examNum !== undefined && item.examNum !== '')) {
  261. const n = Number(item.examNum);
  262. return isNaN(n) ? 0 : n;
  263. }
  264. const fallback = item?.examFreq ?? item?.examCount ?? item?.questionNum ?? item?.wrongCount;
  265. if (fallback === null || fallback === undefined || fallback === '') return 0;
  266. const n = Number(fallback);
  267. return isNaN(n) ? 0 : n;
  268. },
  269. /**
  270. * 判断某个知识点条目是不是"当前联动选中"(跨区单选:三/四个区只能有一条高亮)
  271. * 把父级 selectedKnowledgeId 和条目 knowledgeId 做字符串比较即可
  272. */
  273. isKnowledgeSelected(item) {
  274. const sid = this.selectedKnowledgeId;
  275. const id = item?.knowledgeId;
  276. if (!sid || id === null || id === undefined || id === '') return false;
  277. return String(sid) === String(id);
  278. },
  279. /**
  280. * 【按 knowledgeType 精准分区匹配】联动选中时查找条目并滚动到中间
  281. * 规则(和你描述一致):
  282. * knowledgeType === 1 → 薄弱补强区 weakArea / weakRowsBox 容器
  283. * knowledgeType === 2 → 能力提升区 goodArea / goodRowsBox
  284. * knowledgeType === 3 → 扎实巩固区 excellentArea / excellentRowsBox
  285. * 其它(4/未知)→ 冲刺进阶区 advancedArea / advancedRowsBox(暂时隐藏)
  286. * 只在对应分区的数组里做 knowledgeId 的 O(n) 查找,不需要对比三个数组,效率高
  287. *
  288. * @returns {boolean} true=找到条目并执行了滚动;false=数据/DOM未就绪需要重试
  289. */
  290. highlightAndScrollByKnowledgeId(sid) {
  291. if (!sid) return false;
  292. // 第一步:找命中条目(只查其 knowledgeType 决定的对应分区数组)
  293. let zoneBoxRef = null;
  294. const zones = [
  295. { type: 1, list: this.weakArea, ref: 'weakRowsBox' },
  296. { type: 2, list: this.goodArea, ref: 'goodRowsBox' },
  297. { type: 3, list: this.excellentArea, ref: 'excellentRowsBox' },
  298. { type: 4, list: this.advancedArea, ref: 'advancedRowsBox' }
  299. ];
  300. // 先尝试"根据命中条目自己的 knowledgeType 定位分区":需要知道 ID 对应哪个 type,先 O(n) 找条目
  301. // 折中:如果某一个分区数组先匹配到 knowledgeId,就直接在该分区内滚动——也是只遍历最多"总数组数*平均长度",
  302. // 但用户担心的是"把三个数组都循环一遍效率慢"——这里通过先 type 预判 + 找到即停,最差仍是 3*N 但 N 是分区各自长度,且实际 90% 场景第一个命中就 break。
  303. let matchedItem = null;
  304. for (const z of zones) {
  305. const list = Array.isArray(z.list) ? z.list : [];
  306. const hit = list.find(itm => String(itm.knowledgeId) === sid);
  307. if (hit) {
  308. matchedItem = hit;
  309. zoneBoxRef = z.ref;
  310. break;
  311. }
  312. }
  313. // 没找到条目:说明父级 partitionData 还没被接口赋值(空数组),返回 false 让外层重试
  314. if (!matchedItem || !zoneBoxRef) return false;
  315. // 第二步:找到该条目对应的 DOM(用 data-knowledge-id 属性),在其所属分区滚动容器中居中显示
  316. // ——关键:只滚动该区的 knowledge_rows(overflow-y:auto),不要带动整个页面滚动
  317. // (因此禁止使用 element.scrollIntoView,它会从 window 开始递归把祖先滚动容器都对齐)
  318. const box = this.$refs[zoneBoxRef];
  319. const container = box && (box.$el || box);
  320. // 容器 DOM 还未渲染(例如 ref 还没挂载:created 阶段 immediate watch 触发)→ 重试
  321. if (!container) return false;
  322. const idAttr = CSS.escape ? CSS.escape(sid) : sid;
  323. const rowEl = container.querySelector
  324. ? container.querySelector(`[data-knowledge-id="${idAttr}"]`)
  325. : null;
  326. // 行节点还没被 v-for patch 出来(接口刚赋值,Vue 下一 tick 才渲染)→ 重试
  327. if (!rowEl) return false;
  328. // 用 getBoundingClientRect 的差值算"行相对于容器内容顶边的绝对偏移"
  329. // 这样即使容器内部有 padding / border / 行嵌套其他节点导致 offsetParent 不是 container,也能精确定位
  330. let containerRect = null;
  331. let rowRect = null;
  332. try {
  333. containerRect = container.getBoundingClientRect();
  334. rowRect = rowEl.getBoundingClientRect();
  335. } catch (e) {
  336. return false;
  337. }
  338. if (!containerRect || !rowRect) return false;
  339. const relativeOffsetTop = (rowRect.top - containerRect.top) + (container.scrollTop || 0);
  340. const rowHeight = rowRect.height || 40;
  341. const viewportH = container.clientHeight || 0;
  342. const rawTarget = relativeOffsetTop - (viewportH / 2) + (rowHeight / 2);
  343. const maxScrollTop = Math.max(0, (container.scrollHeight || 0) - viewportH);
  344. const finalTop = Math.max(0, Math.min(maxScrollTop, rawTarget));
  345. try {
  346. if (typeof container.scrollTo === 'function') {
  347. // 只作用在当前分区容器上,window/父容器都不会动
  348. container.scrollTo({ top: finalTop, behavior: 'smooth' });
  349. } else {
  350. container.scrollTop = finalTop;
  351. }
  352. } catch (e) {
  353. try { container.scrollTop = finalTop; } catch (err) {}
  354. }
  355. return true;
  356. },
  357. /**
  358. * 调度"高亮+滚动居中":如果本轮没找到条目/DOM 就间隔 60ms 再重试,最多 5 次(≈300ms),
  359. * 覆盖以下时序:
  360. * 1) keep-alive activated 回来,DOM 已在但数据可能未同步好
  361. * 2) watch selectedKnowledgeId immediate 先跑 → 接口数据还没来
  362. * 3) partitionData 接口刚返回 → Vue v-for 还没把行 DOM patch 出来(computed 更新但 DOM 滞后)
  363. */
  364. scheduleHighlightScroll(sid, attempt = 0) {
  365. const MAX_ATTEMPTS = 5;
  366. if (!sid || attempt >= MAX_ATTEMPTS) return;
  367. const ok = this.highlightAndScrollByKnowledgeId(String(sid));
  368. if (ok) return; // 已经成功滚动到目标行
  369. // 没命中:用 setTimeout 延迟再试(下一 tick 可能数据/DOM 就好了)
  370. setTimeout(() => this.scheduleHighlightScroll(sid, attempt + 1), 60);
  371. },
  372. /**
  373. * 点击某个知识点行:
  374. * - 联动:emit('item-click', item) 给父级,父级写 currentLinkedKnowledgeId
  375. * → 一方面本组件通过 :selectedKnowledgeId 回传后天然高亮(跨区单选)
  376. * → 另一方面 circularDiffusion 会放大对应圆点 + 右侧折线表格显示数据
  377. */
  378. handleRowClick(item) {
  379. if (!item || !item.knowledgeId) return;
  380. this.$emit('item-click', item);
  381. },
  382. /**
  383. * questionNum 为 0 或 null 时,加入练习按钮置灰不可点
  384. */
  385. isAddPracticeDisabled(item) {
  386. const num = item && item.questionNum;
  387. return num === null || num === undefined || num === '' || Number(num) === 0;
  388. },
  389. // 处理加入练习(第二个参数为点击源 DOM,供飞入动画使用)
  390. handleAddPractice(item, e) {
  391. if (this.isAddPracticeDisabled(item)) return;
  392. const sourceEl = (e && e.currentTarget) || null;
  393. this.$emit('add-practice', item, sourceEl);
  394. }
  395. }
  396. };
  397. </script>
  398. <style lang="scss" scoped>
  399. .region_container {
  400. display: flex;
  401. flex-direction: row;
  402. gap: 10px;
  403. width: 100%;
  404. box-sizing: border-box;
  405. align-items: stretch;
  406. flex-wrap: wrap;
  407. }
  408. /* 单个区域卡片(通用样式:圆角作为通用属性) */
  409. .region_section {
  410. flex: 1 1 0;
  411. min-width: 0;
  412. border-radius: 10px 10px 10px 10px;
  413. padding: 16px 16px 12px;
  414. background: #FFFFFF;
  415. display: flex;
  416. flex-direction: column;
  417. height: 560px;
  418. box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
  419. overflow: hidden;
  420. &.weak {
  421. border: 1px solid #F56C6C;
  422. background: linear-gradient(180deg, rgba(245, 108, 108, 0.1) 0%, rgba(255, 255, 255, 0) 100%), #FFFFFF;
  423. }
  424. &.good {
  425. border: 1px solid #FAC858;
  426. background: linear-gradient(180deg, rgba(250, 200, 88, 0.1) 0%, rgba(255, 255, 255, 0) 100%), #FFFFFF;
  427. }
  428. &.excellent {
  429. border: 1px solid #3BA272;
  430. background: linear-gradient(180deg, rgba(59, 162, 114, 0.1) 0%, rgba(255, 255, 255, 0) 100%), #FFFFFF;
  431. }
  432. &.advanced {
  433. border: 1px solid #5470C6;
  434. background: linear-gradient(180deg, rgba(84, 112, 198, 0.1) 0%, rgba(255, 255, 255, 0) 100%), #FFFFFF;
  435. }
  436. }
  437. /* 卡片头部:图标方块 + 标题(同一行) + 副标题(下一行) */
  438. .region_header {
  439. display: flex;
  440. flex-direction: column;
  441. justify-content: flex-start;
  442. gap: 6px; /* 上一行(图标+标题) 与 下一行(描述) 的竖向间距 */
  443. margin-bottom: 14px;
  444. /* 第一行容器:图标 + 标题 同一行,垂直居中 */
  445. .region_header_main {
  446. display: flex;
  447. align-items: center;
  448. gap: 10px;
  449. min-width: 0;
  450. }
  451. .region_icon {
  452. width: 32px;
  453. height: 32px;
  454. display: flex;
  455. align-items: center;
  456. justify-content: center;
  457. flex: 0 0 32px;
  458. img {
  459. width: 100%;
  460. height: 100%;
  461. object-fit: contain;
  462. display: block;
  463. }
  464. svg {
  465. display: block;
  466. }
  467. }
  468. .region_title {
  469. font-size: 24px;
  470. font-weight: 600;
  471. color: #333333;
  472. line-height: 1;
  473. white-space: nowrap;
  474. overflow: hidden;
  475. text-overflow: ellipsis;
  476. min-width: 0;
  477. }
  478. .region_desc {
  479. font-size: 14px;
  480. color: #666666;
  481. line-height: 1.4;
  482. }
  483. }
  484. /* 行列表容器:可滚动,显示自定义滚动条 */
  485. .knowledge_rows {
  486. display: flex;
  487. flex-direction: column;
  488. gap: 10px;
  489. overflow-y: auto;
  490. flex: 1;
  491. min-height: 0;
  492. position: relative;
  493. &::-webkit-scrollbar {
  494. width: 6px;
  495. }
  496. &::-webkit-scrollbar-thumb {
  497. background: #C0C4CC;
  498. border-radius: 3px;
  499. }
  500. &::-webkit-scrollbar-track {
  501. background: #F5F7FA;
  502. border-radius: 3px;
  503. }
  504. scrollbar-width: thin;
  505. scrollbar-color: #C0C4CC #F5F7FA;
  506. }
  507. .empty_tip {
  508. height: 240px;
  509. position: absolute;
  510. top: 50%;
  511. left: 50%;
  512. transform: translate(-50%, -50%);
  513. width: 100%;
  514. border-radius: 8px;
  515. span {
  516. margin-top: 23%;
  517. }
  518. }
  519. /* 每行知识点条目 */
  520. .knowledge_row {
  521. display: flex;
  522. align-items: center;
  523. justify-content: space-between;
  524. gap: 10px;
  525. padding: 10px;
  526. border-radius: 4px;
  527. border: 1px solid;
  528. background: #FFFFFF;
  529. min-width: 0;
  530. cursor: pointer;
  531. .row_left {
  532. display: flex;
  533. flex-direction: column;
  534. gap: 4px;
  535. min-width: 0;
  536. flex: 1;
  537. .kp_name {
  538. font-weight: 500;
  539. font-size: 14px;
  540. color: #333333;
  541. overflow: hidden;
  542. text-overflow: ellipsis;
  543. white-space: nowrap;
  544. margin-bottom: 8px;
  545. }
  546. .kp_meta {
  547. display: flex;
  548. align-items: center;
  549. gap: 10px;
  550. flex-wrap: nowrap; /* 禁止换行,小屏下更美观 */
  551. overflow: hidden;
  552. color: #999999; /* 考频字色固定浅灰 */
  553. font-size: 12px;
  554. .kp_score,
  555. .kp_freq_tag {
  556. overflow: hidden;
  557. text-overflow: ellipsis;
  558. white-space: nowrap;
  559. flex-shrink: 1;
  560. min-width: 0;
  561. }
  562. }
  563. }
  564. .row_right {
  565. flex-shrink: 0;
  566. .add_btn {
  567. padding: 8px 12px;
  568. font-size: 14px;
  569. line-height: 1;
  570. border-radius: 4px;
  571. }
  572. }
  573. }
  574. /* 四个区的条目边框 + 按钮颜色 + 得分率主题色 + 行悬浮/选中背景 */
  575. .weak .knowledge_row {
  576. border-color: #F2B5B5;
  577. background: #FFFFFF;
  578. transition: background-color 0.15s ease;
  579. }
  580. .weak .knowledge_row:hover {
  581. background: rgba(245, 108, 108, 0.1);
  582. }
  583. .weak .knowledge_row.selected {
  584. background: rgba(245, 108, 108, 0.15);
  585. }
  586. .weak .kp_meta .kp_score {
  587. color: #F56C6C; /* 薄弱区:得分率和卡片主题色一致 */
  588. }
  589. .weak .knowledge_row .add_btn.weak_btn {
  590. color: #EE6666;
  591. border: 1px solid #EE6666;
  592. background: #FFFFFF;
  593. }
  594. .good .knowledge_row {
  595. border-color: #F7DEA5;
  596. background: #FFFFFF;
  597. transition: background-color 0.15s ease;
  598. }
  599. .good .knowledge_row:hover {
  600. background: rgba(250, 200, 88, 0.1);
  601. }
  602. .good .knowledge_row.selected {
  603. background: rgba(250, 200, 88, 0.15);
  604. }
  605. .good .kp_meta .kp_score {
  606. color: #FAC858; /* 能力提升区:得分率和卡片主题色一致 */
  607. }
  608. .good .knowledge_row .add_btn.good_btn {
  609. color: #F0B429;
  610. border: 1px solid #F0B429;
  611. background: #FFFFFF;
  612. }
  613. .excellent .knowledge_row {
  614. border-color: #AEE2C5;
  615. background: #FFFFFF;
  616. transition: background-color 0.15s ease;
  617. }
  618. .excellent .knowledge_row:hover {
  619. background: rgba(59, 162, 114, 0.1);
  620. }
  621. .excellent .knowledge_row.selected {
  622. background: rgba(59, 162, 114, 0.15);
  623. }
  624. .excellent .kp_meta .kp_score {
  625. color: #3BA272; /* 扎实巩固区:得分率和卡片主题色一致 */
  626. }
  627. .excellent .knowledge_row .add_btn.excellent_btn {
  628. color: #3BA272;
  629. border: 1px solid #3BA272;
  630. background: #FFFFFF;
  631. }
  632. .advanced .knowledge_row {
  633. border-color: #B9CBFC;
  634. background: #FFFFFF;
  635. transition: background-color 0.15s ease;
  636. }
  637. .advanced .knowledge_row:hover {
  638. background: rgba(84, 112, 198, 0.1);
  639. }
  640. .advanced .knowledge_row.selected {
  641. background: rgba(84, 112, 198, 0.15);
  642. }
  643. .advanced .kp_meta .kp_score {
  644. color: #5470C6; /* 冲刺进阶区:得分率和卡片主题色一致(暂时隐藏) */
  645. }
  646. .advanced .knowledge_row .add_btn.advanced_btn {
  647. color: #2E64FA;
  648. border: 1px solid #2E64FA;
  649. background: #FFFFFF;
  650. }
  651. /* 无题目(questionNum 为 0/null)置灰:与环形图「已加入练习」disabled 样式一致 */
  652. .knowledge_row .add_btn.is-disabled,
  653. .knowledge_row .add_btn[disabled] {
  654. border: 1px solid #DCDFE6 !important;
  655. color: #C0C4CC !important;
  656. background: #F5F7FA !important;
  657. cursor: not-allowed !important;
  658. &:hover,
  659. &:focus {
  660. border: 1px solid #DCDFE6 !important;
  661. color: #C0C4CC !important;
  662. background: #F5F7FA !important;
  663. }
  664. }
  665. /* 响应式:小屏适配 */
  666. @media (max-width: 1366px) {
  667. .region_container {
  668. gap: 10px;
  669. }
  670. .region_section {
  671. height: 460px;
  672. padding: 12px 12px 8px;
  673. }
  674. .region_header {
  675. margin-bottom: 10px;
  676. gap: 4px;
  677. }
  678. }
  679. @media (max-width: 1200px) {
  680. .region_section {
  681. height: auto;
  682. min-height: 320px;
  683. }
  684. .knowledge_rows {
  685. flex: 1;
  686. min-height: 200px;
  687. }
  688. }
  689. </style>