| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673 |
- <template>
- <div class="circularDiffusion">
- <!-- 教师端样式 -->
- <div class="knowledge_class">
- <!-- 图形查看容器 -->
- <div class="graph_content">
- <!-- 左侧图表 -->
- <div class="chart_container">
- <!-- 顶部容器:图例和视图切换按钮 -->
- <div class="top_container">
- <!-- 自定义图例 -->
- <div class="legend">
- <div class="legend_item" :class="{ 'selected': selectedLegend.weak }" @click="toggleLegend('weak')">
- <span class="legend_dot weak"></span>
- <span class="legend_text">{{ mode === 'student' && activeComparison ? '薄弱(得分率差值<-10%)'
- : '薄弱(0%≤得分率<60%)' }}</span>
- </div>
- <div class="legend_item" :class="{ 'selected': selectedLegend.good }" @click="toggleLegend('good')">
- <span class="legend_dot good"></span>
- <span class="legend_text">
- {{ mode === 'student' && activeComparison ? '持平(-10%≤得分率差值≤10%)' : '良好(60%≤得分率< 85 %) ' }}</span>
- </div>
- <div class="legend_item" :class="{ ' selected': selectedLegend.excellent }"
- @click="toggleLegend('excellent')">
- <span class="legend_dot excellent"></span>
- <span class="legend_text">{{ mode === 'student' && activeComparison ? '优势(得分率差值>10%)' :
- '优秀(85%≤得分率)'
- }}</span>
- </div>
- </div>
- </div>
- <div ref="chart" class="chart" style="width: 100%;"></div>
- </div>
- <!-- 右侧容器 -->
- <div class="knowledge_right_container">
- <!-- 折线图 -->
- <div class="line_chart_container">
- <!-- 标题栏(放到折线图上方):左右布局,左侧上下结构 -->
- <div class="chart_title" v-if="selectedKnowledgeName">
- <div class="chart_title_left">
- <span class="title_text" :title="selectedKnowledgeName">{{ selectedKnowledgeName }}</span>
- <span class="exam_freq">考频:{{ filteredTableData.length }}</span>
- </div>
- <el-button
- class="add_practice_btn"
- :class="addPracticeBtnThemeClass"
- :disabled="selectedIsJoin"
- @click="handleAddPractice">
- {{ selectedIsJoin ? '已加入练习' : '加入练习' }}
- </el-button>
- </div>
- <div ref="lineChart" class="line_chart"></div>
- </div>
- <!-- Tab切换和表格 -->
- <div class="table_container">
- <el-radio-group v-model="activeTableTab" class="table_tabs_radio">
- <el-radio-button label="all">全部</el-radio-button>
- <el-radio-button label="exam">考试</el-radio-button>
- <el-radio-button label="practice">练习</el-radio-button>
- </el-radio-group>
- <el-table :data="filteredTableData" border stripe class="data_table">
- <el-table-column prop="index" label="序号" width="60" align="center">
- <template slot-scope="scope">
- {{ scope.$index + 1 }}
- </template>
- </el-table-column>
- <el-table-column prop="examName" label="考试名称" show-overflow-tooltip align="center"></el-table-column>
- <el-table-column prop="examType" label="类型" width="100" align="center"></el-table-column>
- <el-table-column prop="scoreRate" label="得分率" width="100" align="center">
- <template slot-scope="scope">
- <span :style="{ color: getCurrentColor() }">{{ scope.row.scoreRate }}%</span>
- </template>
- </el-table-column>
- </el-table>
- </div>
- </div>
- </div>
- </div>
- </div>
- </template>
- <script>
- // 引入echarts
- import * as echarts from 'echarts';
- export default {
- name: 'CircularDiffusion',
- props: {
- studentName: {
- type: String,
- default: ''
- },
- mode: {
- type: String,
- default: 'class',
- validator: (value) => {
- return ['class', 'student'].includes(value);
- }
- },
- subjectName: {
- type: String,
- default: '知识点'
- },
- subjectScoreRate: {
- type: Number,
- default: 0
- },
- tableData: {
- type: Array,
- default: () => []
- },
- fatalVulnerability: {
- type: Array,
- default: () => []
- },
- highVulnerability: {
- type: Array,
- default: () => []
- },
- allKnowledgeList: {
- type: Array,
- default: () => []
- },
- isRecordKnowledge: {
- type: Boolean,
- default: false
- },
- studentKnowledgeLayering: {
- type: String,
- default: ''
- },
- classLevel: {
- type: [String, Number],
- default: ''
- },
- classGroupName: {
- type: String,
- default: ''
- },
- historicalChangeData: {
- type: Object,
- default: () => ({
- examNames: [],
- scores: [],
- trendData: [],
- personalScoreRate: []
- })
- },
- historicalChangeLoading: {
- type: Boolean,
- default: false
- },
- currentKnowledgeId: {
- type: [String, Number],
- default: ''
- },
- scoreRateTypes: {
- type: Array,
- default: () => []
- },
- comparisonOptions: {
- type: Array,
- default: null
- },
- isPush: {
- type: Boolean,
- default: false
- },
- // 父级已成功"加入练习"的知识点ID映射:{ [knowledgeId]: true }
- addedKnowledgeIds: {
- type: Object,
- default: () => ({})
- },
- },
- computed: {
- // 从扁平 allKnowledgeList(或tableData递归兜底)获取当前选中节点的完整对象
- currentKnowledgeItem() {
- if (!this.selectedKnowledgeId) return null;
- const sid = String(this.selectedKnowledgeId);
- if (Array.isArray(this.allKnowledgeList) && this.allKnowledgeList.length) {
- const hit = this.allKnowledgeList.find(item => String(item.knowledgeId) === sid);
- if (hit) return hit;
- }
- // 再从树形 tableData 递归找一次兜底(扁平列表可能为空但树形有数据)
- const findInTree = (arr) => {
- for (const item of arr) {
- if (String(item.knowledgeId) === sid) return item;
- if (Array.isArray(item.children) && item.children.length) {
- const found = findInTree(item.children);
- if (found) return found;
- }
- }
- return null;
- };
- return findInTree(this.tableData || []);
- },
- // 选中的知识点名称
- selectedKnowledgeName() {
- const item = this.currentKnowledgeItem;
- return item ? (item.knowledgeName ?? '') : '';
- },
- // 当前选中知识点是否"已加入练习"
- // 优先级:1) 父级 addedKnowledgeIds(当前会话刚点按钮成功,立即生效)→ 2) 节点自身 isJoin===1(接口返回的静态字段)
- selectedIsJoin() {
- if (this.selectedKnowledgeId
- && this.addedKnowledgeIds
- && Object.prototype.hasOwnProperty.call(this.addedKnowledgeIds, String(this.selectedKnowledgeId))) {
- return true;
- }
- const item = this.currentKnowledgeItem;
- return item ? (Number(item.isJoin) === 1) : false;
- },
- // 加入练习按钮主题 class(按当前选中圆点的类型匹配颜色)
- // 与 getCurrentColor() / getNodeColor() 严格对齐:
- // knowledgeType=3 优秀/扎实巩固 → #3BA272
- // knowledgeType=2 一般/能力提升 → #FAC858
- // knowledgeType=1 薄弱 → #EE6666
- addPracticeBtnThemeClass() {
- const t = Number(this.selectedKnowledgeType);
- if (t === 3) return 'btn_theme_excellent';
- if (t === 2) return 'btn_theme_good';
- return 'btn_theme_weak';
- },
- // 选中知识点的考频
- selectedExamFreq() {
- const item = this.currentKnowledgeItem;
- if (item) {
- // 兼容常见考频字段名
- return item.examFrequency ?? item.examFreq ?? item.freq ?? item.frequency ?? item.count ?? '--';
- }
- return '--';
- },
- // 当前选中节点的考试/练习明细数组(来自后端 examList 字段)
- // 每个元素结构:{ dataName, dataType, dataRate }
- selectedExamList() {
- const item = this.currentKnowledgeItem;
- if (item && Array.isArray(item.examList)) {
- return item.examList.filter(e => e); // 过滤空项
- }
- return [];
- },
- // 筛选表格数据:全部 / 考试(dataType=1) / 练习(dataType=2)
- // 并将 examList 字段映射为表格列需要的字段:
- // dataName -> examName, dataType -> examType/type, dataRate -> scoreRate
- filteredTableData() {
- const list = (this.selectedExamList || []).map(item => {
- const dataTypeNumber = Number(item.dataType);
- const examTypeLabel = dataTypeNumber === 1 ? '考试' : (dataTypeNumber === 2 ? '练习' : '');
- const typeKey = dataTypeNumber === 1 ? 'exam' : 'practice';
- return {
- examName: item.dataName ?? '',
- examType: item.examType ?? examTypeLabel,
- scoreRate: item.dataRate ?? 0,
- type: typeKey,
- dataType: dataTypeNumber,
- dataRate: item.dataRate
- };
- });
- if (this.activeTableTab === 'all') {
- return list;
- }
- if (this.activeTableTab === 'exam') {
- return list.filter(row => row.dataType === 1 || row.type === 'exam');
- }
- return list.filter(row => row.dataType === 2 || row.type === 'practice');
- }
- },
- data() {
- return {
- chart: null,
- lineChart: null,
- _isMounted: false,
- _isFirstRender: true,
- activeComparison: null,
- selectedLegend: {
- weak: true,
- good: true,
- excellent: true
- },
- legendNumbers: [1, 2, 3],
- selectedIndex: 0,
- selectedKnowledgeId: '',
- selectedKnowledgeType: 3,
- chartRefreshKey: 0,
- showKnowledgePrompt: this.isRecordKnowledge,
- activeTableTab: 'all'
- };
- },
- mounted() {
- this._isMounted = true;
- this.$nextTick(() => {
- this.initChart();
- this.initLineChart();
- });
- },
- beforeDestroy() {
- this._isMounted = false;
- if (this.chart) {
- this.chart.dispose();
- }
- if (this.lineChart) {
- this.lineChart.dispose();
- }
- },
- watch: {
- // 当知识点列表数据变化时,更新选中项的类型与折线图/表格
- allKnowledgeList: {
- handler(newVal) {
- if (newVal && newVal.length > 0 && this.selectedKnowledgeId) {
- const item = newVal.find(item => String(item.knowledgeId) === String(this.selectedKnowledgeId));
- if (item) {
- this.selectedKnowledgeType = item.knowledgeType || 3;
- }
- if (this.lineChart) this.updateLineChart();
- }
- },
- deep: true
- },
- tableData: {
- deep: true,
- handler(newVal) {
- if (this.chart) {
- this.$nextTick(() => {
- this.updateChart();
- // 数据刷新后,如果父组件没传 currentKnowledgeId,则重新走一次默认选中
- if (!this.currentKnowledgeId) {
- this.setDefaultSelection();
- }
- if (this.lineChart) this.updateLineChart();
- });
- }
- }
- },
- currentKnowledgeId: {
- handler(newVal) {
- if (newVal === undefined || newVal === null) {
- this.selectedKnowledgeId = '';
- return;
- }
- this.selectedKnowledgeId = newVal;
- // 更新知识点类型:先在 allKnowledgeList 扁平表中找
- let item = (this.allKnowledgeList || []).find(i => String(i.knowledgeId) === String(newVal));
- // 兜底:在 tableData 整棵树里按 knowledgeId 查找(保证 region 点任何行都能拿到 knowledgeType)
- if (!item && Array.isArray(this.tableData)) {
- const stack = [...this.tableData];
- while (stack.length && !item) {
- const n = stack.pop();
- if (n && String(n.knowledgeId) === String(newVal)) {
- item = n;
- break;
- }
- if (n && Array.isArray(n.children) && n.children.length) {
- for (let i = 0; i < n.children.length; i++) stack.push(n.children[i]);
- }
- }
- }
- if (item) {
- this.selectedKnowledgeType = item.knowledgeType || 3;
- }
- if (this.chart) this.updateChart();
- // 更新折线图和表格(依赖 examList 驱动,computed 会同步)
- if (this.lineChart) this.updateLineChart();
- },
- immediate: true
- },
- // 考试/练习 tab 切换时重新计算表格数据(computed.filteredTableData 会自动响应)
- activeTableTab() {
- // 预留:切换 tab 后若需做高亮/滚动控制可补
- },
- isRecordKnowledge: {
- handler(newVal) {
- this.showKnowledgePrompt = newVal;
- }
- },
- scoreRateTypes: {
- deep: true,
- handler(newVal) {
- const val = Array.isArray(newVal) ? newVal : [];
- this.selectedLegend.weak = val.length === 0 || val.includes(1);
- this.selectedLegend.good = val.length === 0 || val.includes(2);
- this.selectedLegend.excellent = val.length === 0 || val.includes(3);
- this.legendNumbers = val.length === 0 ? [1, 2, 3] : [...val].sort((a, b) => a - b);
- if (this.chart) this.updateChart();
- }
- },
- },
- methods: {
- // 获取当前颜色(根据选中知识点类型)
- getCurrentColor() {
- if (this.selectedKnowledgeType === 3) {
- return '#3BA272';
- } else if (this.selectedKnowledgeType === 2) {
- return '#FAC858';
- } else {
- return '#EE6666';
- }
- },
- // 初始化折线图
- initLineChart() {
- if (!this.$refs.lineChart) return;
- if (this.lineChart) {
- this.lineChart.dispose();
- }
- this.lineChart = echarts.init(this.$refs.lineChart);
- this.updateLineChart();
- },
- // 更新折线图:数据源为当前选中知识点节点上的 examList 数组
- // xAxis = examList[i].dataName
- // yAxis series.data = examList[i].dataRate (0~100百分比)
- updateLineChart() {
- const LOG_TAG = '%c[Diffusion.updateLineChart]';
- const LOG_STYLE = 'background:#3BA272;color:#fff;padding:2px 8px;border-radius:4px;font-weight:500;';
- const callerStack = (new Error()).stack
- ?.split('\n')
- ?.slice(1, 5)
- ?.map(s => s.trim())
- ?.join(' | ') || '';
- const snapshotBefore = {
- hasLineChartInstance: !!this.lineChart,
- hasLineChartRef: !!this.$refs.lineChart,
- selectedKnowledgeId: this.selectedKnowledgeId ?? '(empty)',
- selectedKnowledgeType: this.selectedKnowledgeType,
- selectedExamListLength: (this.selectedExamList || []).length,
- filteredTableDataLength: (this.filteredTableData || []).length,
- activeTableTab: this.activeTableTab,
- caller: callerStack
- };
- if (!this.lineChart) {
- console.warn(LOG_TAG + ' 提前跳过:lineChart 实例不存在(initLineChart 还未执行 / 已被 dispose)', LOG_STYLE, snapshotBefore);
- return;
- }
- const color = this.getCurrentColor();
- const examList = this.selectedExamList || [];
- const xAxisData = examList.map(item => item.dataName ?? '');
- const seriesData = examList.map(item => (item.dataRate === null || item.dataRate === undefined ? null : Number(item.dataRate)));
- const option = {
- tooltip: {
- trigger: 'axis',
- formatter: (params) => {
- const p = Array.isArray(params) ? params[0] : params;
- if (!p || p.value === null || p.value === undefined) return `${p.name ?? ''}`;
- return `${p.name}: ${p.value}%`;
- }
- },
- grid: {
- left: '5%',
- right: '5%',
- top: '10%',
- bottom: '10%'
- },
- xAxis: {
- show: false,
- type: 'category',
- data: xAxisData,
- axisLabel: {
- show: false,
- fontSize: 10,
- color: '#999',
- rotate: 30
- },
- axisLine: {
- show: false,
- lineStyle: { color: '#ECEEF3' }
- },
- axisTick: {
- show: false
- }
- },
- yAxis: {
- show: false,
- type: 'value',
- min: 0,
- max: 100,
- axisLabel: {
- show: false,
- fontSize: 10,
- color: '#999',
- formatter: '{value}%'
- },
- splitLine: {
- show: false,
- lineStyle: {
- color: '#f5f5f5',
- type: 'dashed'
- }
- }
- },
- series: [{
- data: seriesData,
- type: 'line',
- smooth: true,
- symbol: 'circle',
- symbolSize: 8,
- connectNulls: false,
- itemStyle: {
- color: '#fff',
- borderColor: color,
- borderWidth: 2
- },
- lineStyle: {
- color: color,
- width: 2
- },
- areaStyle: {
- color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
- { offset: 0, color: color + '40' },
- { offset: 1, color: color + '05' }
- ])
- },
- // 确保圆点显示在折线上方,遮挡线条
- z: 10
- }]
- };
- const t0 = performance.now();
- this.lineChart.setOption(option, true); // notMerge=true,防止上一个节点残留x轴/点
- this.lineChart.resize();
- const t1 = performance.now();
- },
- // 处理加入练习:把当前选中节点的完整字段 emit 给父组件,父组件调加入练习接口
- handleAddPractice() {
- const item = this.currentKnowledgeItem;
- const knowledgeId = this.selectedKnowledgeId ?? item?.knowledgeId ?? null;
- const knowledgeName = this.selectedKnowledgeName ?? item?.knowledgeName ?? '';
- if (!knowledgeId) {
- this.$message && this.$message.warning && this.$message.warning('请先选择一个知识点');
- return;
- }
- // 得分率:personalScoreRate → gradeScoreRate → classScoreRate
- let personalScoreRate = item?.personalScoreRate;
- if (personalScoreRate === null || personalScoreRate === undefined || personalScoreRate === '') {
- personalScoreRate = item?.gradeScoreRate ?? item?.classScoreRate;
- }
- if (personalScoreRate === null || personalScoreRate === undefined || personalScoreRate === '') {
- personalScoreRate = 0;
- } else {
- personalScoreRate = Number(personalScoreRate);
- if (isNaN(personalScoreRate)) personalScoreRate = 0;
- }
- // 知识点类型 1薄弱 2一般 3优秀
- let knowledgeType = Number(item?.knowledgeType ?? item?.type ?? 0);
- if (isNaN(knowledgeType)) knowledgeType = 0;
- this.$emit('add-practice', {
- knowledgeId,
- knowledgeName,
- personalScoreRate,
- knowledgeType,
- ...(item ? { ...item } : {})
- });
- },
- setDefaultSelection() {
- // 如果父组件已经传入 currentKnowledgeId,则使用父级指定的选中项,不再重选
- // ——但如果父级指定的节点被"图例过滤规则"判定为不可见/降级(hasData=false),则需要重选
- if (this.currentKnowledgeId) {
- this.selectedKnowledgeId = this.currentKnowledgeId;
- // 先按当前图例规则重算一遍过滤树,判断父级指定的节点是否"最终可见且有数据"
- const filteredTree = this.calcFilteredTree(this.tableData);
- let matchNode = null;
- const findStack = [...filteredTree];
- while (findStack.length && !matchNode) {
- const n = findStack.pop();
- if (n && String(n.knowledgeId) === String(this.currentKnowledgeId)) {
- matchNode = n;
- break;
- }
- if (n && Array.isArray(n.children) && n.children.length > 0) {
- for (let i = 0; i < n.children.length; i++) findStack.push(n.children[i]);
- }
- }
- const isFinalValid = matchNode && matchNode.__finalVisible && matchNode.__finalHasData;
- if (isFinalValid) {
- // 父级指定的节点可见且有效:直接沿用
- this.selectedKnowledgeType = matchNode.knowledgeType || 3;
- if (this.lineChart) this.updateLineChart();
- // 通知父级联动 state(保证首次进来默认选中时 region 卡片也高亮对应条目 + 居中滚动)
- this.$emit('knowledge-item-click', {
- knowledgeId: matchNode.knowledgeId,
- knowledgeName: matchNode.knowledgeName,
- personalScoreRate: matchNode.personalScoreRate != null ? matchNode.personalScoreRate : matchNode.scoreRate,
- knowledgeType: matchNode.knowledgeType,
- examList: Array.isArray(matchNode.examList) ? matchNode.examList : []
- });
- return;
- }
- // 否则:父级指定被图例隐藏或降级 → 走重选逻辑(下面的 legacyLists/树形)
- }
- // 1. 优先从扁平化 allKnowledgeList / highVulnerability / fatalVulnerability 取第一个
- // ——仍然要过一遍"图例过滤",只有最终可见+hasData 的才能选中
- let targetNode = null;
- const legacyLists = [this.allKnowledgeList, this.highVulnerability, this.fatalVulnerability];
- for (const lst of legacyLists) {
- if (!Array.isArray(lst) || lst.length === 0) continue;
- // 从 lst 中找第一个:经过 calcFilteredTree 判定后 __finalVisible=true && __finalHasData=true
- const quickTree = this.calcFilteredTree(lst.map(it => ({ ...it }))); // 扁平化结构当 root 层处理即可
- for (let i = 0; i < quickTree.length; i++) {
- const n = quickTree[i];
- if (n && n.__finalVisible && n.__finalHasData) {
- targetNode = n;
- break;
- }
- }
- if (targetNode) break;
- }
- // 2. 兜底:从 tableData(树形)取"第一棵节点树的最后一个可见且有效叶子"
- if (!targetNode) {
- targetNode = this.findFirstVisibleLeafNode(this.calcFilteredTree(this.tableData));
- }
- if (!targetNode) {
- // 没找到任何可见有效节点(例如全部图例都关闭)→ 清空选中
- this.selectedKnowledgeId = '';
- this.selectedKnowledgeType = 3;
- if (this.lineChart) this.updateLineChart();
- return;
- }
- this.selectedIndex = 0;
- this.selectedKnowledgeId = targetNode.knowledgeId;
- this.selectedKnowledgeType = targetNode.knowledgeType || 3;
- // 初始化折线图(确保颜色正确)
- if (this.lineChart) {
- this.updateLineChart();
- }
- // 统一:应用选中大小(放大/还原)
- this.applySelectedSizeToChart(this.selectedKnowledgeId);
- // 通知父级联动 state(保证首次进来默认选中 / 图例过滤重选后 region 卡片也高亮对应条目 + 居中滚动)
- this.$emit('knowledge-item-click', {
- knowledgeId: targetNode.knowledgeId,
- knowledgeName: targetNode.knowledgeName,
- personalScoreRate: targetNode.personalScoreRate != null ? targetNode.personalScoreRate : targetNode.scoreRate,
- knowledgeType: targetNode.knowledgeType,
- examList: Array.isArray(targetNode.examList) ? targetNode.examList : []
- });
- },
- switchView(view) {
- this.$emit('view-change', view);
- },
- classIsGroup() {
- if (this.classLevel == 1) {
- return true;
- } else
- return false;
- },
- getNodeColor(knowledgeType) {
- const type = knowledgeType;
- if (type == 3) {
- return '#3BA272';
- } else if (type == 2) {
- return '#FAC858';
- } else {
- return '#EE6666';
- }
- },
- // 工具:knowledgeType -> legend 级别
- getNodeLevel(knowledgeType) {
- if (knowledgeType == 3) return 'excellent';
- if (knowledgeType == 2) return 'good';
- return 'weak';
- },
- /**
- * 基于 selectedLegend 计算"过滤后"的树形展示状态(后序遍历:先处理 children 再决定当前节点)
- * 规则(与产品说明严格对齐):
- * 1) 叶子节点:
- * - 自己的类型被图例隐藏(selectedLegend[type]=false)→ 整节点隐藏(opacity=0, invisible=true)
- * - 自己的类型显示 → 保持 hasData/颜色 原样,visible
- * 2) 非叶子节点(有 children):
- * a. 如果存在「可见且有数据」的子(不管子是哪一层,只要子树里存在)
- * → 父节点必须可见(否则子画不出来):
- * - 若自己的类型被图例隐藏 → 降级为「灰色节点、hasData=false」(名称保留,不显示真实得分率/颜色,不参与点击)
- * - 若自己的类型显示 → 保持原 hasData/颜色 原样
- * b. 如果不存在「可见且有数据」的子(全子树都被隐藏或都无数据)
- * → 父节点按自身规则走:
- * - 自己类型被隐藏 → 隐藏
- * - 自己类型显示 → 保持原 hasData/颜色 原样
- * 3) 返回的每一项新增字段(不修改原 props.tableData,做浅克隆):
- * __finalVisible : boolean 该节点最终是否可见
- * __finalHasData : boolean 是否显示为"有数据节点"(false=灰色圆点,无得分率)
- * __finalColor : string 最终绘制颜色
- * __scoreRate : number|null 得分率(若 __finalHasData=false 则不再用)
- * __rawHasData : boolean 原始数据是否有 scoreRate(用于 applySelectedSizeToChart 重算)
- */
- calcFilteredTree(data) {
- const vm = this;
- if (!Array.isArray(data)) return [];
- return data.map(item => {
- const node = { ...(item || {}) };
- const scoreRate = vm.classLevel == 2 || vm.mode == 'student' ? node.classScoreRate : node.gradeScoreRate;
- const rawHasData = scoreRate !== null && scoreRate !== undefined;
- node.__scoreRate = rawHasData ? Number(scoreRate) : null;
- node.__rawHasData = rawHasData;
- const selfLevel = vm.getNodeLevel(node.knowledgeType);
- const selfTypeHidden = vm.selectedLegend[selfLevel] === false;
- const hasChildren = Array.isArray(node.children) && node.children.length > 0;
- if (hasChildren) {
- // 先递归处理 children
- const subChildren = vm.calcFilteredTree(node.children);
- node.children = subChildren;
- // 统计:子树中存在多少"最终可见且最终 hasData=true"的节点(任意深度)
- let subHasVisibleHasData = false;
- const stack = [...subChildren];
- while (stack.length && !subHasVisibleHasData) {
- const n = stack.pop();
- if (n && n.__finalVisible && n.__finalHasData) {
- subHasVisibleHasData = true;
- break;
- }
- if (n && Array.isArray(n.children) && n.children.length > 0) {
- for (let i = 0; i < n.children.length; i++) stack.push(n.children[i]);
- }
- }
- if (subHasVisibleHasData) {
- // 父必须可见
- node.__finalVisible = true;
- if (selfTypeHidden) {
- // 降级为灰色节点(hasData=false)
- node.__finalHasData = false;
- node.__finalColor = '#BFC1C7';
- } else {
- node.__finalHasData = rawHasData;
- node.__finalColor = rawHasData ? vm.getNodeColor(node.knowledgeType) : '#BFC1C7';
- }
- } else {
- // 子树没有任何有效可见节点 → 父按自身规则
- if (selfTypeHidden) {
- node.__finalVisible = false;
- node.__finalHasData = rawHasData;
- node.__finalColor = '#BFC1C7';
- } else {
- node.__finalVisible = true;
- node.__finalHasData = rawHasData;
- node.__finalColor = rawHasData ? vm.getNodeColor(node.knowledgeType) : '#BFC1C7';
- }
- }
- } else {
- // 叶子节点
- if (selfTypeHidden) {
- node.__finalVisible = false;
- node.__finalHasData = rawHasData;
- node.__finalColor = '#BFC1C7';
- } else {
- node.__finalVisible = true;
- node.__finalHasData = rawHasData;
- node.__finalColor = rawHasData ? vm.getNodeColor(node.knowledgeType) : '#BFC1C7';
- }
- }
- return node;
- });
- },
- /**
- * 从过滤后的树形数据中,找到"最终可见且有数据"的一个叶子节点(用于默认选中)
- * - 优先在"第一棵子树的最后一个可见叶子"里找(和原 setDefaultSelection 风格一致)
- * - 如果整棵树都没可见有效点,返回 null
- * - 如果入参 excludeKnowledgeId 提供,则跳过该 knowledgeId(用于重选时跳过被隐藏的旧选中)
- */
- findFirstVisibleLeafNode(list, excludeKnowledgeId = null) {
- if (!Array.isArray(list) || list.length === 0) return null;
- // 遍历所有节点(深度优先),收集可见+hasData=true 的叶子
- const stack = [];
- // 从前往后 push,让最后一棵树最后处理;每次 pop 最后一个 children,以匹配"第一棵树最后一个叶子"
- for (let i = list.length - 1; i >= 0; i--) stack.push(list[i]);
- let best = null;
- while (stack.length) {
- const n = stack.pop();
- if (!n) continue;
- const isLeaf = !Array.isArray(n.children) || n.children.length === 0;
- const isValid = n.__finalVisible && n.__finalHasData &&
- (excludeKnowledgeId === null || String(n.knowledgeId) !== String(excludeKnowledgeId));
- if (isValid && isLeaf) {
- // 每找到一个就覆盖(因为从后入栈+pop,最后保留下的就是第一棵树最后一个叶子)
- best = n;
- }
- if (Array.isArray(n.children) && n.children.length > 0) {
- for (let i = n.children.length - 1; i >= 0; i--) stack.push(n.children[i]);
- }
- }
- return best;
- },
- /**
- * 统一生成 ECharts tree 结构 children
- * ——基于 calcFilteredTree 输出的节点(已经带 __final* 字段)
- * @param {Array} data calcFilteredTree 处理过的 tree(或子集 children)
- * @param {number} level 当前层级
- * @param {string|null} overrideKnowledgeId
- */
- generateTreeData(data, level = 0, overrideKnowledgeId = null) {
- const vm = this;
- const currentKnowledgeId = overrideKnowledgeId !== null ? overrideKnowledgeId : vm.selectedKnowledgeId;
- return (data || []).map(item => {
- const finalHasData = !!item.__finalHasData;
- const finalVisible = !!item.__finalVisible;
- const finalColor = item.__finalColor || '#BFC1C7';
- const finalScoreRate = item.__scoreRate;
- // 注意:节点被过滤(__finalVisible=false)时,即使 isMatched 也不显示放大——因为看不见
- const isMatched = finalHasData && finalVisible && (String(item.knowledgeId) === String(currentKnowledgeId));
- let symbolSize = 5;
- if (isMatched) {
- symbolSize = 25; // 选中:放大
- } else if (finalHasData) {
- symbolSize = 10; // 有效但未选中:默认
- }
- // 选中节点的绘制权重更高,防止被非选中圆点遮挡
- const drawZ = isMatched ? 99 : 10;
- const drawZLevel = isMatched ? 2 : 0;
- const node = {
- name: item.knowledgeName,
- symbolSize: symbolSize,
- value: finalScoreRate,
- itemStyle: {
- color: finalColor,
- opacity: finalVisible ? 1 : 0
- },
- lineStyle: {
- opacity: finalVisible ? 1 : 0
- },
- hasData: finalHasData, // 用于 tooltip 显示/点击忽略(灰色不能点)
- knowledgeId: item.knowledgeId,
- knowledgeType: item.knowledgeType,
- z: drawZ,
- zlevel: drawZLevel
- };
- if (item.children && item.children.length > 0) {
- node.children = vm.generateTreeData(item.children, level + 1, overrideKnowledgeId);
- }
- return node;
- });
- },
- /**
- * 统一入口:将当前选中的 knowledgeId 对应的 symbolSize 应用到 ECharts 图表上
- * ——基于过滤后的 filteredTree(保证图例过滤后重新算节点大小和选中状态)
- */
- applySelectedSizeToChart(overrideKnowledgeId) {
- if (!this.chart) return;
- const selectedId = overrideKnowledgeId !== undefined ? overrideKnowledgeId : this.selectedKnowledgeId;
- const filteredTree = this.calcFilteredTree(this.tableData);
- this.chart.setOption({
- series: [{
- data: [
- {
- name: this.subjectName,
- symbolSize: 5,
- value: this.subjectScoreRate,
- itemStyle: {
- color: '#BFC1C7',
- opacity: 1
- },
- hasData: false,
- children: this.generateTreeData(filteredTree, 0, selectedId)
- }
- ]
- }]
- }, {
- animation: true,
- animationDuration: 300
- });
- },
- /**
- * 从树形 list 中取"第一棵节点树的最后一个叶子节点"
- * 规则:取 list[0],沿 children[last] 一路下钻,直到没有 children
- */
- findFirstTreeLastLeafNode(list) {
- if (!Array.isArray(list) || list.length === 0) return null;
- let node = list[0];
- while (node && Array.isArray(node.children) && node.children.length > 0) {
- node = node.children[node.children.length - 1];
- }
- return node || null;
- },
- initChart() {
- try {
- if (!this.$refs.chart) {
- console.error('图表容器不存在');
- return;
- }
- this.createChart();
- }
- catch (error) {
- console.error('图表初始化失败:', error);
- }
- },
- createChart() {
- try {
- if (!this.$refs.chart) {
- console.error('图表容器不存在');
- return;
- }
- if (this.chart) {
- this.chart.dispose();
- }
- this.chart = echarts.init(this.$refs.chart);
- const vm = this;
- const countOutermostNodes = (data) => {
- let count = 0;
- const traverse = (nodes) => {
- nodes.forEach(node => {
- if (node.children && node.children.length > 0) {
- traverse(node.children);
- } else {
- count++;
- }
- });
- };
- traverse(data);
- return count;
- };
- const outermostCount = countOutermostNodes(this.tableData);
- const getOptimalZoom = () => {
- let zoom = 1.05;
- return zoom;
- };
- const getOptimalRadius = () => {
- if (outermostCount <= 5) {
- return ['20%', '65%'];
- }
- return ['12%', '75%'];
- };
- const option = {
- tooltip: {
- formatter: (params) => {
- if (params.treePathInfo && params.treePathInfo.length === 1) {
- return params.name;
- }
- const hasData = params.data && params.data.hasData;
- if (hasData) {
- return `${params.name}<br/>得分率: ${params.value !== null && params.value !== undefined ? params.value : '0'}%`;
- } else {
- return params.name;
- }
- }
- },
- animationDurationUpdate: 1500,
- animationEasingUpdate: 'quinticInOut',
- series: [
- {
- type: 'tree',
- layout: 'radial',
- symbol: 'circle',
- initialTreeDepth: 999,
- expandAndCollapse: false,
- orient: 'radial',
- roam: false,
- left: '8%',
- right: '8%',
- top: '8%',
- bottom: '8%',
- radius: getOptimalRadius(),
- nodeScaleRatio: 1,
- layerPadding: outermostCount <= 5 ? [20, 10] : [10, 5],
- data: [
- {
- name: this.subjectName,
- symbolSize: 5,
- value: this.subjectScoreRate,
- itemStyle: {
- color: '#BFC1C7'
- },
- hasData: false,
- children: this.generateTreeData(this.calcFilteredTree(this.tableData))
- }
- ],
- label: {
- show: false
- },
- lineStyle: {
- color: '#ECEEF3',
- width: 1,
- type: 'solid'
- },
- emphasis: {
- focus: 'adjacency',
- lineStyle: {
- width: 1
- }
- }
- }
- ]
- };
- this.chart.setOption(option);
- this.chart.resize();
- // 设置默认选中项
- this.$nextTick(() => {
- this.setDefaultSelection();
- });
- this.chart.on('click', (params) => {
- const dataItem = params && params.data;
- // 1. 灰色圆点(无数据 hasData=false)或学科中心节点:点击忽略,不做任何处理
- const isSubjectCenter = params.treePathInfo && params.treePathInfo.length === 1;
- if (isSubjectCenter || !dataItem || !dataItem.hasData) {
- return;
- }
- // 2. 从点击节点本身的 knowledgeId 去查(点击 params.data.knowledgeId 与 原始tableData 匹配)
- const clickedKnowledgeId = dataItem.knowledgeId;
- const allLists = [this.allKnowledgeList, this.highVulnerability, this.fatalVulnerability];
- let knowledgeItem = null;
- let targetIndex = -1;
- // 先从扁平化列表里找(用于回传 index 给父级)
- if (clickedKnowledgeId !== undefined && clickedKnowledgeId !== null && clickedKnowledgeId !== '') {
- for (const list of allLists) {
- if (!Array.isArray(list)) continue;
- targetIndex = list.findIndex(item => String(item.knowledgeId) === String(clickedKnowledgeId));
- if (targetIndex !== -1) {
- knowledgeItem = list[targetIndex];
- break;
- }
- }
- }
- // 扁平化找不到,就从 tableData 递归查找,按 name 兜底
- if (!knowledgeItem && params.name) {
- const findByNodeName = (arr) => {
- for (const item of arr) {
- if (item.knowledgeName === params.name) return item;
- if (Array.isArray(item.children) && item.children.length) {
- const f = findByNodeName(item.children);
- if (f) return f;
- }
- }
- return null;
- };
- knowledgeItem = findByNodeName(this.tableData);
- // 再回填一次 index
- if (knowledgeItem && knowledgeItem.knowledgeId !== undefined) {
- for (const list of allLists) {
- if (!Array.isArray(list)) continue;
- targetIndex = list.findIndex(item =>
- String(item.knowledgeId) === String(knowledgeItem.knowledgeId)
- );
- if (targetIndex !== -1) break;
- }
- }
- }
- // 3. 如果还是找不到知识点项,按点击的 data 做最小赋值(至少保证选中ID、类型对得上,有放大效果)
- if (!knowledgeItem) {
- knowledgeItem = {
- knowledgeId: clickedKnowledgeId ?? params.name,
- knowledgeName: params.name,
- knowledgeType: dataItem.knowledgeType || 3
- };
- }
- // 4. 回传事件:保持向后兼容 {item, index},同时把核心字段打平在 payload 顶层
- // ——父级联动(handleKnowledgeItemClick)会按顶层 knowledgeId 写入 currentLinkedKnowledgeId
- // ——外层其他监听者也可继续用 item / index
- this.$emit('knowledge-item-click', {
- item: knowledgeItem,
- index: targetIndex,
- knowledgeId: knowledgeItem.knowledgeId,
- knowledgeName: knowledgeItem.knowledgeName,
- personalScoreRate: knowledgeItem.personalScoreRate != null
- ? knowledgeItem.personalScoreRate
- : knowledgeItem.scoreRate,
- knowledgeType: knowledgeItem.knowledgeType,
- examList: Array.isArray(knowledgeItem.examList) ? knowledgeItem.examList : []
- });
- // 5. 更新内部状态
- if (targetIndex !== -1) this.selectedIndex = targetIndex;
- this.selectedKnowledgeId = knowledgeItem.knowledgeId;
- this.selectedKnowledgeType = knowledgeItem.knowledgeType || 3;
- // 6. 折线图颜色同步
- if (this.lineChart) {
- this.updateLineChart();
- }
- // 7. 统一更新圆点大小:当前点放大(25),上一个选中点自动恢复为10
- this.applySelectedSizeToChart(this.selectedKnowledgeId);
- });
- window.addEventListener('resize', () => {
- if (this.chart) {
- this.chart.resize();
- }
- });
- }
- catch (error) {
- console.error('图表创建失败:', error);
- }
- },
- updateChart() {
- try {
- const LOG_TAG = '%c[Diffusion.updateChart]';
- const LOG_STYLE = 'background:#2E64FA;color:#fff;padding:2px 8px;border-radius:4px;font-weight:500;';
- const callerStack = (new Error()).stack
- ?.split('\n')
- ?.slice(1, 5)
- ?.map(s => s.trim())
- ?.join(' | ') || '';
- if (!this.$refs.chart) {
- console.warn(LOG_TAG + ' 提前跳过:$refs.chart 不存在(模板 ref 未挂载 / 容器尚未渲染)', LOG_STYLE);
- return;
- }
- const t0 = performance.now();
- const oldChartExist = !!this.chart;
- this.createChart();
- const t1 = performance.now();
- }
- catch (error) {
- console.error('[Diffusion.updateChart] 图表更新失败:', error);
- }
- },
- reset() {
- this.selectedIndex = 0;
- this.updateChart();
- },
- toggleLegend(type) {
- const prevSelected = this.selectedLegend[type];
- this.selectedLegend[type] = !prevSelected;
- const typeMap = {
- weak: 1,
- good: 2,
- excellent: 3
- };
- const number = typeMap[type];
- if (!this.selectedLegend[type]) {
- const index = this.legendNumbers.indexOf(number);
- if (index > -1) {
- this.legendNumbers.splice(index, 1);
- }
- } else {
- if (!this.legendNumbers.includes(number)) {
- this.legendNumbers.push(number);
- }
- this.legendNumbers.sort((a, b) => a - b);
- }
- const hasAllNumbers = this.legendNumbers.includes(1) && this.legendNumbers.includes(2) && this.legendNumbers.includes(3);
- const dataToSend = hasAllNumbers ? null : this.legendNumbers;
- this.$emit('legend-numbers-change', dataToSend);
- // ---- 图例变化后:重算过滤 → 重绘扩散图 → 若选中被隐藏则重选可见节点 ----
- this.$nextTick(() => {
- // 1) 全量重绘(内部 createChart 已经是 calcFilteredTree(tableData) 再生成 children)
- if (this.chart) {
- this.updateChart();
- }
- // 2) 判断当前选中的节点在新的图例规则下是否"最终可见且有数据"
- const filteredTree = this.calcFilteredTree(this.tableData);
- let matchNode = null;
- const findStack = [...filteredTree];
- while (findStack.length && !matchNode) {
- const n = findStack.pop();
- if (n && this.selectedKnowledgeId &&
- String(n.knowledgeId) === String(this.selectedKnowledgeId)) {
- matchNode = n;
- break;
- }
- if (n && Array.isArray(n.children) && n.children.length > 0) {
- for (let i = 0; i < n.children.length; i++) findStack.push(n.children[i]);
- }
- }
- const stillValid = matchNode && matchNode.__finalVisible && matchNode.__finalHasData;
- if (!stillValid) {
- // 当前选中的点被隐藏/降级 → 重选一个可见有效叶子(排除当前,避免又选回来)
- const replacement = this.findFirstVisibleLeafNode(filteredTree, this.selectedKnowledgeId);
- if (replacement) {
- this.selectedKnowledgeId = replacement.knowledgeId;
- this.selectedKnowledgeType = replacement.knowledgeType || 3;
- // 重新应用圆点放大
- if (this.chart) this.applySelectedSizeToChart(replacement.knowledgeId);
- // 同步折线图/表格(computed 依赖 selectedKnowledgeId → selectedExamList 会变,折线这里手动触发)
- if (this.lineChart) this.updateLineChart();
- this.$emit('knowledge-item-click', {
- knowledgeId: replacement.knowledgeId,
- knowledgeName: replacement.knowledgeName
- });
- } else {
- // 全部关闭了 → 清空
- this.selectedKnowledgeId = '';
- this.selectedKnowledgeType = 3;
- if (this.chart) this.applySelectedSizeToChart('');
- if (this.lineChart) this.updateLineChart();
- }
- } else {
- // 当前选中仍然有效 → 仅重新"应用选中大小"(因为 createChart 后放大状态没保留)
- if (this.chart) this.applySelectedSizeToChart(this.selectedKnowledgeId);
- }
- });
- },
- }
- };
- </script>
- <style lang="scss" scoped>
- .circularDiffusion {
- position: relative;
- }
- .circularDiffusion,
- .knowledge_graph_student {
- .knowledge_class {
- :deep(.vxe-body--row.selected-row),
- :deep(.selected-row) {
- background-color: rgba(46, 100, 250, 0.1) !important;
- color: #2E64FA !important;
- * {
- color: #2E64FA !important;
- }
- .rate_dot {
- border-color: rgba(46, 100, 250, 0.1) !important;
- }
- }
- .graph_header {
- margin-bottom: 20px;
- position: relative;
- display: flex;
- justify-content: space-between;
- align-items: center;
- .legend {
- display: flex;
- gap: 20px;
- .legend_item {
- display: flex;
- align-items: center;
- gap: 5px;
- cursor: pointer;
- font-weight: 500;
- &.selected {
- opacity: 1;
- }
- &:hover {
- opacity: 1;
- }
- .legend_dot {
- width: 20px;
- height: 10px;
- border-radius: 2px;
- transition: all 0.3s ease;
- &.weak {
- background: #ccc;
- }
- &.good {
- background: #cccccc;
- }
- &.excellent {
- background: #ccc;
- }
- }
- .legend_text {
- font-size: 12px;
- color: #999999;
- transition: all 0.3s ease;
- }
- &.selected {
- .legend_dot.weak {
- background: #EE6666;
- }
- .legend_dot.good {
- background: #FAC858;
- }
- .legend_dot.excellent {
- background: #3BA272;
- }
- .legend_text {
- color: #333;
- }
- }
- }
- }
- }
- .graph_content {
- display: flex;
- gap: 20px;
- position: relative;
- .knowledge_right_container {
- width: 500px;
- display: flex;
- flex-direction: column;
- gap: 10px;
- .line_chart_container {
- border: 1px solid #DCDFE6;
- border-radius: 6px;
- position: relative;
- display: flex;
- flex-direction: column;
- /* 标题栏:左右分布,放在折线图上方 */
- .chart_title {
- display: flex;
- justify-content: space-between;
- align-items: center;
- gap: 12px;
- padding: 10px 16px;
- border-bottom: 1px solid #DCDFE6;
- /* 左侧:上下结构(标题 + 考频) */
- .chart_title_left {
- flex: 1;
- min-width: 0;
- display: flex;
- flex-direction: column;
- justify-content: center;
- gap: 4px;
- }
- .title_text {
- font-weight: 500;
- font-size: 14px;
- color: #333333;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- line-height: 1.2;
- }
- .exam_freq {
- font-weight: 400;
- font-size: 12px;
- color: #999999;
- line-height: 1.2;
- }
- /* 加入练习按钮:使用 Element UI el-button,描边风格跟随当前选中圆点的 knowledgeType 颜色 */
- .add_practice_btn {
- border-radius: 4px;
- font-weight: 500;
- font-size: 14px;
- background: #fff !important;
- flex-shrink: 0;
- padding: 0;
- width: 100px;
- height: 36px;
- line-height: 36px;
- text-align: center;
- /* 薄弱:knowledgeType=1 → 与选中圆点的 #EE6666 颜色一致 */
- &.btn_theme_weak {
- border: 1px solid #EE6666 !important;
- color: #EE6666 !important;
- &:hover, &:focus {
- border: 1px solid #EE6666 !important;
- color: #EE6666 !important;
- background: #fff !important;
- }
- }
- /* 一般/能力提升:knowledgeType=2 → #FAC858 */
- &.btn_theme_good {
- border: 1px solid #FAC858 !important;
- color: #FAC858 !important;
- &:hover, &:focus {
- border: 1px solid #FAC858 !important;
- color: #FAC858 !important;
- background: #fff !important;
- }
- }
- /* 优秀/扎实巩固:knowledgeType=3 → #3BA272 */
- &.btn_theme_excellent {
- border: 1px solid #3BA272 !important;
- color: #3BA272 !important;
- &:hover, &:focus {
- border: 1px solid #3BA272 !important;
- color: #3BA272 !important;
- background: #fff !important;
- }
- }
- /* isJoin=1 的置灰状态:disabled 时边框和文字变浅灰,背景淡灰(优先级最高,覆盖主题色) */
- &.is-disabled,
- &[disabled] {
- border: 1px solid #DCDFE6 !important;
- color: #C0C4CC !important;
- background: #F5F7FA !important;
- cursor: not-allowed !important;
- &:hover,
- &:focus {
- border: 1px solid #DCDFE6 !important;
- color: #C0C4CC !important;
- background: #F5F7FA !important;
- }
- }
- }
- }
- .line_chart {
- width: calc(100% - 30px);
- height: 180px;
- padding: 15px;
- }
- }
- .table_container {
- /* 按钮组 tabs:按设计图样式 */
- .table_tabs_radio {
- margin-bottom: 10px;
- display: inline-flex;
- align-items: center;
- line-height: 0;
- border-collapse: collapse;
- :deep(.el-radio-button) {
- /* 让选中的按钮在视觉上压过相邻按钮的边框 */
- &:first-child .el-radio-button__inner {
- border-radius: 4px 0 0 4px;
- }
- &:last-child .el-radio-button__inner {
- border-radius: 0 4px 4px 0;
- }
- }
- :deep(.el-radio-button__inner) {
- height: 36px;
- line-height: 36px;
- padding: 0 20px;
- font-size: 13px;
- color: #909399; /* 未选中:灰字 */
- background: #FFFFFF;
- border: 1px solid #DCDFE6; /* 未选中:灰边框 */
- box-sizing: border-box;
- margin: 0;
- box-shadow: none !important;
- transition: none;
- position: relative;
- border-radius: 0;
- }
- /* 相邻按钮的重叠(Element 默认 margin-left:-1px,这里保持默认通过 border-color 变化即可,显式指定分隔线颜色) */
- :deep(.el-radio-button + .el-radio-button) {
- margin-left: -1px;
- }
- /* 选中态:蓝边框 + 蓝字 + 白底(完全匹配图片) */
- :deep(.el-radio-button__orig-radio:checked + .el-radio-button__inner) {
- color: #2E64FA;
- background-color: #FFFFFF;
- border-color: #2E64FA;
- box-shadow: none !important;
- z-index: 2; /* 把选中按钮抬高,其蓝色边框盖住相邻按钮的灰色边框 */
- }
- /* 未选中 hover:保持灰字灰边框,不变成蓝色 */
- :deep(.el-radio-button__orig-radio:not(:checked) + .el-radio-button__inner:hover) {
- color: #909399;
- border-color: #DCDFE6;
- background-color: #FFFFFF;
- }
- :deep(.el-radio-button__inner:hover) {
- color: #909399;
- }
- }
- .data_table {
- height: 330px;
- border-radius: 6px;
- /* 整表统一 cell 高度为 40px(Element UI 是通过 .cell 控制) */
- :deep(.el-table__header-wrapper th .cell),
- :deep(.el-table__body-wrapper td .cell) {
- height: 40px;
- line-height: 40px;
- padding-top: 0;
- padding-bottom: 0;
- box-sizing: border-box;
-
- }
- /* 表头样式 */
- :deep(.el-table__header th) {
- background: #F5F7FA;
- color: #333333;
- font-weight: 500;
- font-size: 14px;
- padding:0px !important;
- }
- /* 斑马纹:开启 stripe 后,Element UI 会给偶数行加 el-table__row--striped */
- :deep(.el-table__body tr.el-table__row--striped td) {
- background: #FAFBFC;
- }
- :deep(.el-table__body tr.el-table__row--striped td.el-table__cell) {
- background: #FAFBFC;
- }
- /* 表体样式 */
- :deep(.el-table__body td) {
- font-weight: 400;
- font-size: 14px;
- color: #666666;
- padding:0px !important;
- }
- /* hover 行时不改变斑马纹底色(仅轻微提亮边框感知) */
- :deep(.el-table__body tr:hover > td) {
- background-color: #EFF6FF;
- }
- }
- }
- }
- .item_chart_container {
- border: 1px solid #E9EBEF;
- border-radius: 10px;
- margin-bottom: 10px;
- }
- .chart_container {
- flex: 1;
- min-width: 0;
- border-radius: 10px;
- border: 1px solid #E9EBEF;
- position: relative;
- padding: 20px;
- overflow: hidden;
- .top_container {
- display: flex;
- justify-content: space-between;
- align-items: flex-start;
- flex-wrap: wrap;
- gap: 10px;
- margin-bottom: 10px;
- }
- .legend {
- display: flex;
- gap: 20px;
- flex-wrap: wrap;
- flex: 1;
- min-width: 0;
- .legend_item {
- display: flex;
- align-items: center;
- gap: 5px;
- cursor: pointer;
- font-weight: 500;
- &.selected {
- opacity: 1;
- }
- &:hover {
- opacity: 1;
- }
- .legend_dot {
- width: 20px;
- height: 10px;
- border-radius: 2px;
- transition: all 0.3s ease;
- &.weak {
- background: #ccc;
- }
- &.good {
- background: #ccc;
- }
- &.excellent {
- background: #ccc;
- }
- }
- .legend_text {
- font-size: 12px;
- color: #999999;
- transition: all 0.3s ease;
- }
- &.selected {
- .legend_dot.weak {
- background: #EE6666;
- }
- .legend_dot.good {
- background: #FAC858;
- }
- .legend_dot.excellent {
- background: #3BA272;
- }
- .legend_text {
- color: #333;
- }
- }
- }
- }
- .chart {
- display: flex;
- justify-content: center;
- align-items: center;
- min-height: 590px;
- }
- }
- .knowledge_list {
- width: 100%;
- height: 410px;
- overflow-y: auto;
- padding-right: 10px;
- position: relative;
- &::-webkit-scrollbar {
- width: 6px;
- }
- &::-webkit-scrollbar-track {
- background: #f1f1f1;
- border-radius: 3px;
- }
- &::-webkit-scrollbar-thumb {
- background: #c1c1c1;
- border-radius: 3px;
- }
- &::-webkit-scrollbar-thumb:hover {
- background: #a8a8a8;
- }
- }
- }
- }
- }
- </style>
|