解决绘制的雷达图在页面有滚动时,雷达图出现`轻微上下偏移`的问题

一、问题根源:

是微信小程序中原生 Canvas 组件与普通 DOM 元素混合使用时的经典渲染不同步问题,在长页面滚动时尤为明显。下面我将详细分析原因并提供完整的解决方案。

二、问题细分排查
  1. 渲染层差异(最主要原因)
    微信小程序的渲染架构分为逻辑层和视图层,而视图层又分为:
    WebView 渲染层:所有普通的 view、text、image 等组件都在这里渲染
    原生组件层:Canvas、map、video、camera 等组件是系统原生渲染
    这两个层是独立渲染的,当页面滚动时:
    WebView 层的标签会立即跟随滚动
    原生 Canvas 层的重绘会有几毫秒的延迟
    这种微小的时间差在人眼看来就是 "轻微上下偏移" 的抖动效果
  2. 单位混用问题
    你的代码中同时使用了px和rpx两种单位:
    Canvas 尺寸硬编码为260px(固定像素)
    卡片、边距等使用rpx(自适应像素)
    在不同分辨率的设备上,rpx会自动缩放,但px不会,导致 Canvas 和标签的相对位置在不同设备上不一致,滚动时偏移更明显。
  3. 计算时机问题
    你在onLoad生命周期中计算标签位置,但此时:
    页面 DOM 还未完全渲染完成
    Canvas 的实际尺寸可能还不是最终值
    计算出的标签位置本身就存在微小误差
三、解决方案:使用 Canvas 2D 新接口(彻底解决)

微信小程序从基础库 2.9.0 开始支持Canvas 2D 接口,它不再是原生组件,而是和普通元素一样在 WebView 层渲染,从根本上解决了不同步问题。

canvas和canvas 2d的区别

完整代码

html 复制代码
	<!-- 场景能力雷达图 -->
			<view class="card radar-card">
				<text class="card-title">场景能力雷达图</text>
				<view class="radar-wrapper">
					<view class="radar-container">
						<!-- 使用type="2d"的新Canvas -->
						<canvas type="2d" id="sceneRadarCanvas" class="radar-canvas" style="width: 260px; height: 260px;"></canvas>

						<!-- 动态渲染标签 -->
						<view class="radar-labels">
							<text class="radar-label" wx:for="{{radarLabels}}" wx:key="index"
								style="left: {{item.left}}px; top: {{item.top}}px; text-align: {{item.align}}; transform: {{item.transform}};">
								{{item.name}}
							</text>
						</view>
					</view>

					<view class="radar-legend">
						<view class="legend-item">
							<view class="legend-line gray"></view>
							<text class="legend-text">Day 1 前测</text>
						</view>
						<view class="legend-item">
							<view class="legend-line blue"></view>
							<text class="legend-text">Day 30 后测</text>
						</view>
					</view>
				</view>
			</view>
javascript 复制代码
/**
 * @page report_result
 * @description 测评结果主页面 - 高精度还原蓝狐原型设计
 * @module 康复报告模块
 * @usage 展示SADL满意度、场景适应、CHHIE-S三个维度的测评结果
 */

Page({
  data: {
    currentTab: 0,
    tabs: ['SADL满意度', '场景适应', 'CHHIE-S'],
    
    // 12角雷达图(顺时针标签)
    sceneList: [
      { name: '家庭聚餐', category: '社交与餐饮', before: 4, after: 2, change: -2 },
      { name: '嘈杂餐厅', category: '社交与餐饮', before: 5, after: 3, change: -2 },
      { name: '广场舞', category: '社交与餐饮', before: 4, after: 2, change: -2 },
      { name: '地铁公交', category: '公共交通与出行', before: 4, after: 3, change: -1 },
      { name: '高铁机场', category: '公共交通与出行', before: 5, after: 4, change: -1 },
      { name: '过马路', category: '公共交通与出行', before: 4, after: 2, change: -2 },
      { name: '办公会议', category: '职场与教育', before: 3, after: 2, change: -1 },
      { name: '讲座培训', category: '职场与教育', before: 4, after: 2, change: -2 },
      { name: '视频通话', category: '数字化与服务', before: 4, after: 2, change: -2 },
      { name: '政务柜台', category: '数字化与服务', before: 5, after: 1, change: -4 },
      { name: '家庭电视', category: '家庭与个人', before: 4, after: 2, change: -2 },
      { name: '菜市场', category: '家庭与个人', before: 4, after: 2, change: -2 }
    ],

		// 雷达图数据
		sceneRadarData: {
			before: [4, 5, 4, 4, 5, 4, 3, 4, 4, 5, 4, 4],  // 对应上面的before
			after: [2, 3, 2, 3, 4, 2, 2, 2, 2, 1, 2, 2]     // 对应上面的after
		},
				
		// 雷达图标签位置
		radarLabels: []
},

  onLoad() {

  },

  onReady() {
    if (this.data.currentTab === 1) {
      setTimeout(() => {
        this.initRadar();
      }, 100);
    }
  },

  onTabChange(e) {
    const index = parseInt(e.currentTarget.dataset.index);
    this.setData({ currentTab: index });
    
    if (index === 1) {
      setTimeout(() => {
        this.initRadar();
      }, 100);
    }
  },

	//每一个标签,支持标签上下左右微调、角度的微调
	initRadar() {
		const canvasSize = 260; // px
		const centerX = canvasSize / 2;
		const centerY = canvasSize / 2;
		
		// 与Canvas绘制逻辑完全一致的雷达半径
		const radarRadius = canvasSize * 0.75 / 2; // = 97.5px
		const sides = 12;
		const angleStep = (2 * Math.PI) / sides; // 30度 = π/6弧度
		
		const labels = [];
		
		//  格式:{ r:径向距离, x:水平偏移(+右-左), y:垂直偏移(+下-上), t:变换, a:对齐 } // 索引: 标签名(角度)
		const labelConfigs = [
			{ r: 6, x: 0, y: 0, t: 'translate(-50%, -100%)', a: 'center' },  // 0: 家庭聚餐(顶部)
			{ r: 6, x: 0, y: 0, t: 'translate(0%, -100%)', a: 'left' },     // 1: 嘈杂餐厅(右上30°)
			{ r: 6, x: 0, y: -5, t: 'translate(0%, -50%)', a: 'left' },      // 2: 广场舞(右60°)
			{ r: 6, x: -3, y: -20, t: 'translate(0%, 0%)', a: 'left' },        // 3: 地铁公交(右下90°)
			{ r: 6, x: 0, y: -22, t: 'translate(0%, 0%)', a: 'left' },        // 4: 高铁机场(最右下120°)
			{ r: 8, x: -5, y: -20, t: 'translate(0%, 0%)', a: 'right' },       // 5: 过马路(底部右150°)
			{ r: -12, x: 0, y: 0, t: 'translate(-50%, 0%)', a: 'center' },    // 6: 办公会议(底部)
			{ r: 8, x: 0, y: -20, t: 'translate(-100%, 0%)', a: 'right' },    // 7: 讲座培训(底部左210°)
			{ r: 8, x: 6, y: -2, t: 'translate(-100%, -50%)', a: 'right' },  // 8: 视频通话(左下240°)
			{ r: 6, x: 0, y: -6, t: 'translate(-100%, -50%)', a: 'right' },  // 9: 政务柜台(左270°)
			{ r: 8, x: 0, y: 6, t: 'translate(-100%, -100%)', a: 'right' }, // 10: 家庭电视(左上300°)
			{ r: 8, x: -15, y: 0, t: 'translate(0%, -100%)', a: 'center' }    // 11: 菜市场(顶部左330°)
		];
		
		for (let i = 0; i < sides; i++) {
			const angle = i * angleStep - Math.PI / 2;
			const c = labelConfigs[i];
			
			// 基础位置计算 + 独立微调
			let x = centerX + (radarRadius + c.r) * Math.cos(angle) + c.x;
			let y = centerY + (radarRadius + c.r) * Math.sin(angle) + c.y;
			
			labels.push({
				name: this.data.sceneList[i].name,
				left: x,
				top: y,
				align: c.a,
				transform: c.t
			});
		}
		
		this.setData({ radarLabels: labels }, () => {
			this.drawSceneRadar(canvasSize, canvasSize);
		});
	},

  // 绘制雷达图
  async drawSceneRadar(canvasWidth, canvasHeight) {
    // 获取Canvas实例
    const query = wx.createSelectorQuery().in(this);
    const res = await new Promise(resolve => {
      query.select('#sceneRadarCanvas')
        .fields({ node: true, size: true })
        .exec(resolve);
    });
    
    const canvas = res[0].node;
    const ctx = canvas.getContext('2d');
    
    // 设置Canvas实际尺寸(解决模糊问题)
    const dpr = wx.getSystemInfoSync().pixelRatio;
    canvas.width = canvasWidth * dpr;
    canvas.height = canvasHeight * dpr;
    ctx.scale(dpr, dpr);
    
    const centerX = canvasWidth / 2;
    const centerY = canvasHeight / 2;
    const radius = Math.min(centerX, centerY) * 0.75; // 雷达图占Canvas的75%
    const sides = 12;
    const angleStep = (2 * Math.PI) / sides;
    
    // 清空画布
    ctx.clearRect(0, 0, canvasWidth, canvasHeight);
    
    // 绘制背景网格(6层)
    ctx.strokeStyle = 'rgba(148, 163, 184, 0.25)';
    ctx.lineWidth = 1;
    
    for (let i = 1; i <= 6; i++) {
      ctx.beginPath();
      const r = (radius / 6) * i;
      for (let j = 0; j < sides; j++) {
        const angle = j * angleStep - Math.PI / 2;
        const x = centerX + r * Math.cos(angle);
        const y = centerY + r * Math.sin(angle);
        j === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
      }
      ctx.closePath();
      ctx.stroke();
    }
    
    // 绘制轴线
    ctx.beginPath();
    for (let i = 0; i < sides; i++) {
      const angle = i * angleStep - Math.PI / 2;
      const x = centerX + radius * Math.cos(angle);
      const y = centerY + radius * Math.sin(angle);
      ctx.moveTo(centerX, centerY);
      ctx.lineTo(x, y);
    }
    ctx.stroke();
    
    // 绘制前测数据
    this.drawRadarData(ctx, centerX, centerY, radius, sides, this.data.sceneRadarData.before, '#94a3b8', 'rgba(148, 163, 184, 0.15)');
    
    // 绘制后测数据
    this.drawRadarData(ctx, centerX, centerY, radius, sides, this.data.sceneRadarData.after, '#0ea5e9', 'rgba(14, 165, 233, 0.2)');
  },
  
  // 绘制雷达图数据
  drawRadarData(ctx, centerX, centerY, radius, sides, data, strokeColor, fillColor) {
    const angleStep = (2 * Math.PI) / sides;
    const maxValue = 5;  // 最大值为5
    
    // 绘制填充区域
    ctx.beginPath();
    ctx.fillStyle = fillColor;
    for (let i = 0; i < sides; i++) {
      const value = data[i] || 0;
      const r = (radius * value) / maxValue;
      const angle = i * angleStep - Math.PI / 2;
      const x = centerX + r * Math.cos(angle);
      const y = centerY + r * Math.sin(angle);
      if (i === 0) {
        ctx.moveTo(x, y);
      } else {
        ctx.lineTo(x, y);
      }
    }
    ctx.closePath();
    ctx.fill();
    
    // 绘制边框线
    ctx.beginPath();
    ctx.strokeStyle = strokeColor;
    ctx.lineWidth = 2;
    for (let i = 0; i < sides; i++) {
      const value = data[i] || 0;
      const r = (radius * value) / maxValue;
      const angle = i * angleStep - Math.PI / 2;
      const x = centerX + r * Math.cos(angle);
      const y = centerY + r * Math.sin(angle);
      if (i === 0) {
        ctx.moveTo(x, y);
      } else {
        ctx.lineTo(x, y);
      }
    }
    ctx.closePath();
    ctx.stroke();
    
    // 绘制数据点
    for (let i = 0; i < sides; i++) {
      const value = data[i] || 0;
      const r = (radius * value) / maxValue;
      const angle = i * angleStep - Math.PI / 2;
      const x = centerX + r * Math.cos(angle);
      const y = centerY + r * Math.sin(angle);
      ctx.beginPath();
      ctx.arc(x, y, 3, 0, 2 * Math.PI);
      ctx.fillStyle = strokeColor;
      ctx.fill();
    }
  },
css 复制代码
/* 基础样式 */
.report-container {
  min-height: 100vh;
  background-color: #f6f8fa;
  display: flex;
  flex-direction: column;
	/* letter-spacing: 1rpx;不能全局设置,单个容器设置,影响小容器字体换行 */
}

/* 标签栏 */
.tabs-container {
  display: flex;
  background-color: #ffffff;
  padding: 0 20rpx;
  position: relative;
  border-bottom: 1rpx solid #f0f0f0;
}

.tab-item {
  flex: 1;
  display: flex;
  justify-content: center;
  align-items: center;
  padding: 28rpx 0;
  position: relative;
}

.tab-text {
  font-size: 28rpx;
  color: #64748b;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}

.tab-item.active .tab-text {
  color: #1f2937;
  font-weight: 600;
}

.tab-indicator {
  position: absolute;
  bottom: 0;
  left: 0;
  width: 33.33%;
  height: 4rpx;
  background-color: #0ea5e9;
  transition: transform 0.3s ease;
}

/* 内容区域 */
.content-scroll {
  flex: 1;
  height: calc(100vh - 320rpx);
  padding: 20rpx 0; /* 修改:左右padding改为0,通过card的margin来控制 */
  overflow-x: hidden;
}

/* 卡片基础样式 - 精确还原原型阴影 */
.card {
  background-color: #ffffff;
  border-radius: 30rpx;
  padding: 30rpx; /* 保持内部padding为30rpx */
  margin: 0 40rpx 25rpx 40rpx; /* 修改:添加左右margin 40rpx,保持底部margin 25rpx */
  box-shadow: 0 2rpx 26rpx 0 rgba(140, 163, 197, 0.15);
}

/* 卡片基础样式 - 精确还原原型阴影 */
.card {
  background-color: #ffffff;
  border-radius: 30rpx;
  padding: 30rpx;
  margin-bottom: 50rpx;
  box-shadow: 0 2rpx 26rpx 0 rgba(140, 163, 197, 0.15);
}

.card-title {
  font-size: 32rpx;
  font-weight: 700;
  color: #1f2937;
  margin-bottom: 20rpx;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}

/* 主评分卡片 */
.main-card {
  position: relative;
}

.card-header {
  display: flex;
  align-items: flex-start;
  margin-bottom: 30rpx;
}

.header-icon-box {
  width: 72rpx;
  height: 72rpx;
  background-color: #f0f9ff;
  border-radius: 10rpx;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-right: 20rpx;
  flex-shrink: 0;
}

.icon-square {
  width: 30rpx;
  height: 30rpx;
  border: 4rpx solid #0ea5e9;
}

.header-info {
  display: flex;
  flex-direction: column;
}

.header-title {
  font-size: 32rpx;
  font-weight: 700;
  color: #1f2937;
  margin-bottom: 8rpx;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}

.header-subtitle {
  font-size: 24rpx;
  color: #64748b;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}

/* 分数对比区域 */
.score-compare {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 30rpx;
}

.score-box {
  flex: 1;
  border-radius: 30rpx;
  /* padding: 20rpx; */
  display: flex;
  flex-direction: column;
  align-items: center;
	height: 152rpx;
	justify-content: center;
}

.score-box.gray {
  background-color: #f9fafc;
}

.score-box.blue {
  background-color: #f0f9ff;
  border: 2rpx solid rgba(14, 165, 233, 0.1);
}

.score-label {
  font-size: 24rpx;
  color: #64748b;
  margin-bottom: 10rpx;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}

.score-box.blue .score-label {
  color: #0ea5e9;
}

.score-wrap {
  display: flex;
  align-items: baseline;
}

.score-num {
  font-size: 50rpx;
  font-weight: 700;
  color: #475569;
  line-height: 1;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}

.score-box.blue .score-num {
  color: #0ea5e9;
}

.score-unit {
  font-size: 24rpx;
  color: #475569;
  margin-left: 4rpx;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}

.score-box.blue .score-unit {
  color: #0ea5e9;
}

.score-arrow {
  font-size: 28rpx;
  color: #94a3b8;
  margin: 0 15rpx;
}

.score-arrow-text {
  font-size: 28rpx;
  color: #94a3b8;
  margin: 0 15rpx;
}

/* 改善状态条 */
.improvement-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  background-color: #dcfce7;
  border-radius: 20rpx;
	padding: 0rpx 30rpx;
  margin-bottom: 30rpx;
	height: 80rpx;
}

.improvement-text {
  font-size: 28rpx;
  font-weight: 700;
  color: #15803d;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}

.improvement-tag {
	width: 90rpx;
height: 32rpx;
  font-size: 22rpx;
  font-weight: 700;
  color: #ffffff;
  background-color: #15803d;
  padding: 6rpx 16rpx;
  border-radius: 22rpx;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}

/* 总结文本 */
.summary-box {
  /* 移除 flex 布局,让文本自然换行 */
}

.summary-label {
  font-size: 26rpx;
  font-weight: 700;
  color: #475569;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  display: inline;  /* 确保和内容在同一行 */
}

.summary-content {
  font-size: 26rpx;
  color: #475569;
  line-height: 1.6;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  display: inline;  /* 确保和标签在同一行 */
}

/* 三维度分析 */
.dimension-row {
  display: flex;
  justify-content: space-between;
	margin: 20rpx 0;
}

.dimension-item {
  flex: 1;
  border-radius: 20rpx;
  padding: 30rpx 0 0 20rpx;
  margin-right: 20rpx;
	height: 228rpx;
}

.dimension-item:last-child {
  margin-right: 0;
}

.dimension-item.full {
  flex: none;
  margin-right: 0;
  margin-top: 20rpx;
}

.dimension-item.green {
  background-color: #dcfce7;
}

.dimension-item.orange {
  background-color: #fef3e2;
}

.dimension-item.blue {
  background-color: #f0f9ff;
}

.dim-label {
  font-size: 22rpx;
  font-weight: 700;
  margin-bottom: 16rpx;
  display: block;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}

.dimension-item.green .dim-label {
  color: #15803d;
}

.dimension-item.orange .dim-label {
  color: #ea590a;
}

.dimension-item.blue .dim-label {
  color: #0ea5e9;
}

.dim-score-row {
  display: flex;
  align-items: center;
  margin-bottom: 10rpx;
}

.dim-score-old {
  font-size: 36rpx;
  font-weight: 600;
  color: #475569;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}

.dim-arrow {
  font-size: 28rpx;
  color: #94a3b8;
  margin: 0 16rpx;
}

.dim-arrow-text {
  font-size: 28rpx;
  color: #94a3b8;
  margin: 0 16rpx;
}

.dim-change-arrow-text {
  font-size: 24rpx;
  color: #15803d;
  margin-right: 6rpx;
  font-weight: 700;
}

.dim-change-arrow-text.orange {
  color: #ea590a;
}

.dim-change-arrow-text.blue {
  color: #0ea5e9;
}

.dim-change-arrow-text.green {
  color: #15803d;
}

.dim-score-new {
  font-size: 50rpx;
  font-weight: 700;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}

.dimension-item.green .dim-score-new {
  color: #15803d;
}

.dimension-item.orange .dim-score-new {
  color: #ea590a;
}

.dimension-item.blue .dim-score-new {
  color: #0ea5e9;
}

.dim-score-new.orange {
  color: #ea590a !important;
}

.dim-score-new.blue {
  color: #0ea5e9 !important;
}

.dim-change-row {
  display: flex;
  align-items: center;
  margin-bottom: 8rpx;
}

.dim-change-arrow {
  width: 0;
  height: 0;
  border-left: 6rpx solid transparent;
  border-right: 6rpx solid transparent;
  border-top: 8rpx solid #15803d;
  margin-right: 10rpx;
  transform: rotate(180deg);
}

.dim-change-arrow.orange {
  border-top-color: #ea590a;
}

.dim-change-arrow.blue {
  border-top-color: #0ea5e9;
}

.dim-change-arrow.green {
  border-top-color: #15803d;
}

.dim-change-text {
  font-size: 22rpx;
  font-weight: 700;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}

.dimension-item.green .dim-change-text {
  color: #15803d;
}

.dimension-item.orange .dim-change-text {
  color: #ea590a;
}

.dimension-item.blue .dim-change-text {
  color: #0ea5e9;
}

.dim-change-text.orange {
  color: #ea590a !important;
}

.dim-change-text.blue {
  color: #0ea5e9 !important;
}

.dim-change-text.green {
  color: #15803d !important;
}

.dim-fullscore {
  font-size: 22rpx;
  color: #94a3b8;
  font-weight: 700;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}

/* 进度条 */
.progress-list {
  display: flex;
  flex-direction: column;
	padding-bottom: 33rpx;
}

.progress-item {
  margin-bottom: 30rpx;
}

.progress-item:last-child {
  margin-bottom: 0;
}

.progress-label-row {
  display: flex;
  justify-content: space-between;
  margin-bottom: 10rpx;
}

.progress-name {
  font-size: 26rpx;
  color: #475569;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}

.progress-value {
  font-size: 26rpx;
  color: #0ea5e9;
  font-weight: 500;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}

.progress-track {
  height: 16rpx;
  background-color: #f1f5f9;
  border-radius: 8rpx;
  overflow: hidden;
}

.progress-fill {
  height: 100%;
  background-color: #0ea5e9;
  border-radius: 8rpx;
}

/* 时间线 */
.timeline {
  padding-left: 20rpx;
}

.timeline-item {
  display: flex;
  align-items: flex-start;
  margin-bottom: 10rpx;
  position: relative;
}

.timeline-dot {
  width: 24rpx;
  height: 24rpx;
  border-radius: 50%;
  background-color: rgba(14, 165, 233, 0.3);
  margin-right: 20rpx;
  margin-top: 6rpx;
  flex-shrink: 0;
}

.timeline-dot.active {
  background-color: rgba(14, 165, 233, 0.3);
}

.timeline-dot.current {
  background-color: #0ea5e9;
}

.timeline-line {
  position: absolute;
  left: 10rpx;
  top: 24rpx;
  width: 4rpx;
  height: calc(100% - 24rpx);
  background-color: rgba(14, 165, 233, 0.3);
}

.timeline-date {
  font-size: 24rpx;
  color: #64748b;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}

.timeline-content {
  padding-left: 44rpx;
  margin-bottom: 30rpx;
}

.timeline-content.first {
  margin-top: -10rpx;
}

.timeline-text-group {
  display: flex;
  flex-direction: column;
	margin-bottom: 20rpx;
}

.timeline-title {
  font-size: 28rpx;
  font-weight: 700;
  color: #1f2937;
  margin: 20rpx 0;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}

.timeline-title.current {
  color: #0ea5e9;
}

.timeline-detail {
  font-size: 26rpx;
  color: #475569;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}

/* 雷达图 - 最终硬编码版本 */

.radar-container {/* 新增:雷达图容器 - 关键!使用相对定位作为基准 */
  position: relative;
  width: 260px;
  height: 260px;
  margin: 0 auto; 
}

.radar-canvas {
  display: block;
}

.radar-labels {/* 标签容器 - 绝对定位覆盖在Canvas上 */
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  pointer-events: none; /* 让标签不阻挡Canvas事件 */
}

.radar-label {/* 标签样式 - 使用绝对定位 */
  position: absolute;
  font-size: 24rpx;
  color: #64748b;
  white-space: nowrap;
	/* 关键:让标签中心对准计算点 */
  /* transform: translate(-50%, -50%);  */
}

.radar-legend {
  display: flex;
  justify-content: center;
  gap: 40rpx;
  margin-top: 30rpx;
}

.legend-item {
  display: flex;
  align-items: center;
  gap: 12rpx;
}

.legend-line {
  width: 40rpx;
  height: 4rpx;
  border-radius: 2rpx;
}

.legend-line.gray {
  background-color: #94a3b8;
}

.legend-line.blue {
  background-color: #0ea5e9;
}

.legend-text {
  font-size: 24rpx;
  color: #64748b;
}


/* 场景明细 */
.scene-card {
  padding-bottom: 10rpx;
}

.scene-list {
  display: flex;
  flex-direction: column;
}

.scene-item {
  display: flex;
  align-items: center;
  padding: 24rpx 0;
  border-bottom: 1rpx solid #f1f5f9;
}

.scene-item:last-child {
  border-bottom: none;
}

.scene-icon-box {
  width: 60rpx;
  height: 60rpx;
  margin-right: 20rpx;
  flex-shrink: 0;
}

.scene-icon {
  width: 100%;
  height: 100%;
  background-color: #f1f5f9;
  border-radius: 10rpx;
}

.scene-info {
  flex: 1;
  display: flex;
  flex-direction: column;
}

.scene-name {
  font-size: 28rpx;
  color: #1f2937;
  margin-bottom: 6rpx;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}

.scene-category {
  font-size: 22rpx;
  color: #94a3b8;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}

.scene-scores {
  display: flex;
  align-items: center;
  padding-left: 20rpx;
}

.score-col {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 0 10rpx;
}

.score-dot {
  width: 16rpx;
  height: 16rpx;
  border-radius: 50%;
  margin-bottom: 6rpx;
}

.score-dot.gray {
  background-color: #94a3b8;
}

.score-dot.blue {
  background-color: #0ea5e9;
}

.score-arrow-h {
  width: 30rpx;
  height: 4rpx;
  background-color: #e2e8f0;
  margin-top: 6rpx;
}

.score-change {
  font-size: 24rpx;
  color: #0ea5e9;
  font-weight: 500;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}

/* 分级解读 */
.level-list {
  display: flex;
  flex-direction: column;
}

.level-item {
  display: flex;
  align-items: stretch;
  background-color: #ffffff;
  border-radius: 20rpx;
  border: 2rpx solid rgba(0, 0, 0, 0.06);
  margin-bottom: 20rpx;
  overflow: hidden;
  height: 122rpx;
}

.level-item.current {
  border-color: rgba(245, 158, 12, 0.15);
}

.level-badge {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 140rpx;
  height: 122rpx;
  font-size: 26rpx;
  font-weight: 700;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  flex-shrink: 0;
}

.level-badge.green {
  background-color: #dcfce7;
  color: #15803d;
  border-right: 2rpx solid #bef1d1;
}

.level-badge.yellow {
  background-color: #fffbeb;
  color: #924210;
  border-right: 2rpx solid rgba(245, 158, 12, 0.15);
}

.level-badge.red {
  background-color: #fee2e2;
  color: #d94040;
  border-right: 2rpx solid #ffd4d4;
}

.level-info {
  flex: 1;
  /* padding: 20rpx 24rpx; */
  display: flex;
  flex-direction: column;
  justify-content: center;
  min-height: 122rpx;
	margin-left: 24rpx;
	padding-right: 30rpx;
}

.level-name {
  font-size: 28rpx;
  font-weight: 700;
  color: #15803D;
  margin-bottom: 8rpx;
  line-height: 1.2;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}

.level-item.current .level-name {
  color: #924210;
}

.level-desc {
  font-size: 24rpx;
  color: #475569;
  line-height: 1.4;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}

.level-summary {
  margin-top: 30rpx;
  display: flex;
  flex-wrap: wrap;
	padding-bottom: 50rpx;
}

.summary-normal {
  font-size: 26rpx;
  color: #475569;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}

.summary-highlight {
  font-size: 26rpx;
  font-weight: 700;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}

.summary-highlight.red {
  color: #d94040;
}

.summary-highlight.yellow {
  color: #924210;
}

/* 底部间距 */
.bottom-spacer {
  height: 300rpx;
}

/* 底部操作栏 */
.bottom-bar {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  background-color: #ffffff;
  padding: 20rpx 40rpx;
  padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
  box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.08);
}

.tips-box {
  display: flex;
  align-items: flex-start;
  background-color: #f0f9ff;
  border-radius: 16rpx;
  padding: 24rpx;
  margin-bottom: 20rpx;
}

.tips-icon {
  width: 40rpx;
  height: 40rpx;
  background-color: #0ea5e9;
  border-radius: 50%;
  margin-right: 16rpx;
  flex-shrink: 0;
}

.tips-text {
  flex: 1;
  font-size: 24rpx;
  color: #475569;
  line-height: 1.5;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}

.btn-row {
  display: flex;
  gap: 20rpx;
}

.btn-share {
  flex: 1;
  height: 84rpx;
  display: flex;
  align-items: center;
  justify-content: center;
  background-color: #f1f5f9;
  border-radius: 42rpx;
  font-size: 28rpx;
  color: #475569;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}

.btn-primary {
  flex: 1;
  height: 84rpx;
  display: flex;
  align-items: center;
  justify-content: center;
  background-color: #0ea5e9;
  border-radius: 42rpx;
  font-size: 28rpx;
  color: #ffffff;
  font-weight: 500;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
相关推荐
空中海1 小时前
微信小程序 - 02 基础概念层与核心能力层
微信小程序·小程序
無名路人3 小时前
小程序点餐页吸顶滚动
前端·微信小程序·ai编程
Greg_Zhong5 小时前
微信小程序中使用canvas实现雷达图及标签对角显示(实现雷达图标签的标准做法)
微信小程序·小程序canvas实现雷达图·标签不通过canvas绘制
棋宣7 小时前
uni-app编译到微信小程序中,父传子props首次传递数据不接收的bug
微信小程序·uni-app·bug
kyh10033811209 小时前
微信小程序摇骰子功能实现|含源码
微信小程序·小程序·摇骰子小游戏·摇色子源码
Lsx_1 天前
H5 嵌入微信 / 支付宝 / 抖音小程序 WebView:调用原生能力完整方案
前端·微信小程序·webview
计算机学姐2 天前
基于微信小程序的图书馆座位预约系统【uniapp+springboot+vue】
vue.js·spring boot·微信小程序·小程序·java-ee·uni-app·intellij-idea
WKK_2 天前
uniapp 微信小程序使用TextEncoder,arrayBufferToBase64
微信小程序·小程序·uni-app
舟遥遥娓飘飘2 天前
面向零基础初学者,从环境搭建到发布上线,手把手教你开发第一个微信小程序(第3章-认识项目结构)
微信小程序·小程序·notepad++