微信小程序中canvas绘制面积图,解决手机和模拟器都能渲染不溢出问题

一、针对上一篇的文章:示例完整代码的补充

缘由,请访问

bash 复制代码
const ASSESSMENT_DATA_TEMPLATE = {
	// 测评分数对比
	preTestScore: 32, // 前测分数
	postTestScore: 18, // 后测分数
	preTestDate: '前测 (2026.03.20)', // 前测日期
	postTestDate: '后测 (2026.04.17)', // 后测日期
	scoreChangeText: '下降 14 分', // 分数变化文本
	improvementLevel: '明显改善', // 改善程度
	summary: '恭喜您!您的听力障碍对社交和心理的影响显著降低。目前您已能更好地适应日常交流环境,心理状态更加自信。',

	// 听力维度改善分析
	dimensions: [{
			name: '社交/环境适应',
			improvement: 45, // 改善百分比
			percent: 85 // 进度条百分比
		},
		{
			name: '情感/心理状态',
			improvement: 30,
			percent: 70
		},
		{
			name: '噪声环境下辨析',
			improvement: 20,
			percent: 55
		}
	],

	// 28 天训练数据
	trainingDays: 28, // 总打卡天数
	maxContinuousDays: 22, // 最高连续天数
	totalHours: 186, // 累计佩戴时长

	// 情绪趋势图数据
	emotionTrend: {
		weeks: ['第1周', '第2周', '第3周', '第4周'],
		values: [1, 2, 4, 4] // 情绪值 (1-5)
	},

	// 高频场景 TOP3
	topScenes: [{
			name: '家庭日常交流',
			percent: '占比 65%'
		},
		{
			name: '观看电视节目',
			percent: '占比 20%'
		},
		{
			name: '公园户外散步',
			percent: '占比 10%'
		}
	],

	// 积分与徽章
	totalPoints: 2850, // 累计积分
	unlockedBadges: ['勇敢启程', '户外探索', '通话达人'],
	badges: [{
			name: '勇敢启程',
			icon: '🚩',
			unlocked: true
		},
		{
			name: '户外探索',
			icon: '🌳',
			unlocked: true
		},
		{
			name: '通话达人',
			icon: '📞',
			unlocked: true
		},
		{
			name: '一周斗士',
			icon: '🔥',
			unlocked: false
		},
		{
			name: '两周坚持',
			icon: '📅',
			unlocked: false
		}
	],

	// 满意度评价
	rating: {
		stars: [true, true, true, true, false], // 5 颗星的状态
		value: '4.0',
		comment: '相比一个月前,戴上后听声音更自然了,也没那么吵了。'
	},

	// 后续建议
	suggestions: [{
			title: '保持佩戴时长',
			desc: '建议每日佩戴不少于 8 小时,继续巩固听力反馈机制。',
			available: true
		},
		{
			title: '挑战嘈杂环境',
			desc: '下阶段建议尝试去超市、餐厅等环境练习,逐步提升言语理解度。',
			available: true
		},
		{
			title: '专家进阶课 (敬请期待)',
			desc: '针对复杂场景的专项听力康复训练课程。',
			available: false
		}
	]
};

Page({
	/**
	 * 页面的初始数据
	 */
	data: {
		data: ASSESSMENT_DATA_TEMPLATE
	},

	/**
	 * 生命周期函数--监听页面加载
	 */
	onLoad(options) {
		// 如果有传入参数,使用参数数据,否则使用模板数据
		if (options.data) {
			try {
				const customData = JSON.parse(decodeURIComponent(options.data));
				this.setData({
					data: {
						...ASSESSMENT_DATA_TEMPLATE,
						...customData
					}
				});
			} catch (e) {
				console.log('使用默认数据');
			}
		}

		// 初始化图表
		setTimeout(() => {
			this.initEmotionChart();
		}, 500);
	},

	/**
	 * 初始化情绪趋势图(原生 Canvas 绘制)
	 */
	initEmotionChart() {
		const query = wx.createSelectorQuery().in(this);
		query.select('#emotionChart')
			.fields({
				node: true,
				size: true
			})
			.exec((res) => {
				if (!res[0]) return;

				const canvas = res[0].node;
				const ctx = canvas.getContext('2d');
				const dpr = wx.getSystemInfoSync().pixelRatio;

				// 设置画布尺寸
				const width = res[0].width;
				const height = res[0].height;
				canvas.width = width * dpr;
				canvas.height = height * dpr;
				ctx.scale(dpr, dpr);

				const data = this.data.data.emotionTrend;
				const weeks = data.weeks;
				const values = data.values;

				// 图表参数
				const padding = {
					top: 20,
					right: 20,
					bottom: 30,
					left: 50
				};
				const chartWidth = width - padding.left - padding.right;
				const chartHeight = height - padding.top - padding.bottom;
				const maxY = 5;

				// 清空画布
				ctx.fillStyle = '#f8fafc';
				ctx.fillRect(0, 0, width, height);

				// 绘制网格线
				ctx.strokeStyle = '#e2e8f0';
				ctx.setLineDash([4, 4]);
				ctx.lineWidth = 0.5;

				for (let i = 1; i <= maxY; i++) {
					const y = padding.top + (1 - i / maxY) * chartHeight;
					ctx.beginPath();
					ctx.moveTo(padding.left, y);
					ctx.lineTo(width - padding.right, y);
					ctx.stroke();

					// 绘制 Y 轴标签(emoji)- 确保在容器内
					const emojis = ['', '😟', '😐', '😊', '🤩', '🔥'];
					ctx.fillStyle = '#64748b';
					ctx.font = '16px Arial';
					ctx.textAlign = 'right';
					ctx.textBaseline = 'middle';
					ctx.fillText(emojis[i] || '', padding.left - 8, y);
				}

				// 计算点位
				const points = weeks.map((week, index) => {
					const x = padding.left + (index / (weeks.length - 1)) * chartWidth;
					const y = padding.top + (1 - values[index] / maxY) * chartHeight;
					return {
						x,
						y,
						value: values[index]
					};
				});

				// 绘制面积渐变
				const gradient = ctx.createLinearGradient(0, padding.top, 0, height - padding.bottom);
				gradient.addColorStop(0, 'rgba(14, 165, 233, 0.3)');
				gradient.addColorStop(1, 'rgba(14, 165, 233, 0)');

				ctx.fillStyle = gradient;
				ctx.beginPath();
				ctx.moveTo(points[0].x, height - padding.bottom);
				points.forEach((point, index) => {
					if (index === 0) {
						ctx.lineTo(point.x, point.y);
					} else {
						const prev = points[index - 1];
						const cp1x = prev.x + (point.x - prev.x) / 3;
						const cp2x = prev.x + (point.x - prev.x) * 2 / 3;
						ctx.bezierCurveTo(cp1x, prev.y, cp2x, point.y, point.x, point.y);
					}
				});
				ctx.lineTo(points[points.length - 1].x, height - padding.bottom);
				ctx.closePath();
				ctx.fill();

				// 绘制折线
				ctx.strokeStyle = '#0ea5e9';
				ctx.lineWidth = 3;
				ctx.setLineDash([]);
				ctx.lineCap = 'round';
				ctx.lineJoin = 'round';

				ctx.beginPath();
				points.forEach((point, index) => {
					if (index === 0) {
						ctx.moveTo(point.x, point.y);
					} else {
						const prev = points[index - 1];
						const cp1x = prev.x + (point.x - prev.x) / 3;
						const cp2x = prev.x + (point.x - prev.x) * 2 / 3;
						ctx.bezierCurveTo(cp1x, prev.y, cp2x, point.y, point.x, point.y);
					}
				});
				ctx.stroke();

				// 绘制数据点
				points.forEach((point) => {
					ctx.fillStyle = '#0ea5e9';
					ctx.beginPath();
					ctx.arc(point.x, point.y, 6, 0, Math.PI * 2);
					ctx.fill();
				});

				// 绘制 X 轴标签 - 确保在容器内
				ctx.fillStyle = '#94a3b8';
				ctx.font = '11px Arial';
				ctx.textAlign = 'center';
				ctx.textBaseline = 'top';
				weeks.forEach((week, index) => {
					const x = padding.left + (index / (weeks.length - 1)) * chartWidth;
					ctx.fillText(week, x, height - padding.bottom + 8);
				});
			});
	},

	/**
	 * 返回上一页
	 */
	goBack() {
		wx.navigateBack({
			delta: 1
		});
	},

	/**
	 * 保存图片
	 */
	saveImage() {
		wx.showToast({
			title: '保存功能开发中',
			icon: 'none'
		});

		// TODO: 实现截图保存功能
		// 可以使用 wx.canvasToTempFilePath 将页面转为图片
	},

	/**
	 * 分享成就
	 */
	shareResult() {
		wx.showShareMenu({
			withShareTicket: true,
			menus: ['shareAppMessage', 'shareTimeline']
		});
	},

	/**
	 * 页面相关事件处理
	 */
	onShareAppMessage() {
		return {
			title: '我的听力康复成果',
			path: '/pages/assessment-results/assessment-results'
		};
	},

	onShareTimeline() {
		return {
			title: '我的听力康复成果',
			query: 'data=' + encodeURIComponent(JSON.stringify(this.data.data))
		};
	}
});
bash 复制代码
<!--pages/assessment-results/assessment-results.wxml-->
<view class="page-container">
  <!-- 内容区域 -->
  <scroll-view class="content-area" scroll-y>
    <!-- 测评数据汇总 -->
    <view class="card">
      <view class="card-title">
        <text class="title-icon">📊</text>
        <text>CHHIE-S 听力社会心理量表</text>
      </view>
      
      <view class="score-compare">
        <view class="score-item">
          <text class="score-label">{{data.preTestDate}}</text>
          <text class="score-value">{{data.preTestScore}}<text class="score-unit">分</text></text>
        </view>
        <view class="score-arrow">→</view>
        <view class="score-item highlight">
          <text class="score-label highlight">{{data.postTestDate}}</text>
          <text class="score-value highlight">{{data.postTestScore}}<text class="score-unit">分</text></text>
        </view>
      </view>
      
      <view class="score-change">
        <text class="change-text">得分变化:{{data.scoreChangeText}}</text>
        <text class="change-badge">{{data.improvementLevel}}</text>
      </view>
      
      <view class="summary-text">
        <text class="summary-label">总评:</text>
        <text>{{data.summary}}</text>
      </view>
    </view>

    <!-- 听力维度改善分析 -->
    <view class="card">
      <view class="card-title-simple">听力维度改善分析</view>
      <view class="progress-list">
        <view class="progress-item" wx:for="{{data.dimensions}}" wx:key="name">
          <view class="progress-header">
            <text class="progress-name">{{item.name}}</text>
            <text class="progress-percent">{{item.improvement}}%</text>
          </view>
          <view class="progress-bg">
            <view class="progress-fill" style="width: {{item.percent}}%"></view>
          </view>
        </view>
      </view>
    </view>

    <!-- 28 天康复旅程回顾 -->
    <view class="card">
      <view class="card-title-simple">28 天康复旅程回顾</view>
      
      <view class="stats-grid">
        <view class="stat-item">
          <text class="stat-value">{{data.trainingDays}}</text>
          <text class="stat-label">总打卡天数</text>
        </view>
        <view class="stat-item divider">
          <text class="stat-value">{{data.maxContinuousDays}}</text>
          <text class="stat-label">最高连续天数</text>
        </view>
        <view class="stat-item">
          <text class="stat-value">{{data.totalHours}}<text class="stat-unit-small">h</text></text>
          <text class="stat-label">累计佩戴时长</text>
        </view>
      </view>
      
      <view class="chart-section">
        <view class="section-title">情绪趋势波动图</view>
        <view class="chart-container">
          <canvas type="2d" id="emotionChart" class="emotion-chart"></canvas>
        </view>
      </view>
      
      <view class="scene-section">
        <view class="section-title">高频听力场景 TOP 3</view>
        <view class="scene-list">
          <view class="scene-item" wx:for="{{data.topScenes}}" wx:key="name" wx:for-index="index">
            <view class="scene-rank {{index < 3 ? 'top-' + (index + 1) : ''}}">{{index + 1}}</view>
            <text class="scene-name">{{item.name}}</text>
            <text class="scene-percent">{{item.percent}}</text>
          </view>
        </view>
      </view>
    </view>

    <!-- 积分与徽章汇总 -->
    <view class="card badge-card">
      <view class="badge-header">
        <view class="badge-points">
          <text class="points-label">累计获得积分</text>
          <view class="points-value">
            <text class="shell-icon">🐚</text>
            <text class="points-number">{{data.totalPoints}}</text>
          </view>
        </view>
        <view class="badge-icon-box">
          <text class="trophy-icon">🏆</text>
        </view>
      </view>
      
      <view class="badges-section">
        <text class="badges-title">已解锁成就 ({{data.unlockedBadges.length}}个)</text>
        <scroll-view class="badges-scroll" scroll-x>
          <view class="badges-list">
            <view class="badge-item {{item.unlocked ? '' : 'locked'}}" wx:for="{{data.badges}}" wx:key="name">
              <view class="badge-icon {{item.unlocked ? 'unlocked' : 'locked'}}">
                <text>{{item.icon}}</text>
              </view>
              <text class="badge-name">{{item.name}}</text>
            </view>
          </view>
        </scroll-view>
      </view>
    </view>

    <!-- 助听器满意度 -->
    <view class="card">
      <view class="card-title-simple">助听器满意度</view>
      <view class="rating-section">
        <view class="stars">
          <text wx:for="{{data.rating.stars}}" wx:key="index" class="star {{item ? 'active' : ''}}">★</text>
        </view>
        <text class="rating-value">{{data.rating.value}} / 5.0</text>
      </view>
      <text class="rating-comment">{{data.rating.comment}}</text>
    </view>

    <!-- 后续建议与计划 -->
    <view class="card suggestion-card">
      <view class="card-title-simple">后续建议与计划</view>
      <view class="suggestion-list">
        <view class="suggestion-item" wx:for="{{data.suggestions}}" wx:key="index" wx:for-index="index">
          <view class="suggestion-num {{item.available ? '' : 'disabled'}}">{{index + 1}}</view>
          <view class="suggestion-content">
            <text class="suggestion-title {{item.available ? '' : 'disabled'}}">{{item.title}}</text>
            <text class="suggestion-desc {{item.available ? '' : 'disabled'}}">{{item.desc}}</text>
          </view>
        </view>
      </view>
    </view>

    <!-- 分享与保存 -->
    <view class="action-buttons">
      <view class="action-btn secondary" bindtap="saveImage">
        <text class="btn-icon">📥</text>
        <text>保存图片</text>
      </view>
      <view class="action-btn primary" bindtap="shareResult">
        <text class="btn-icon"></text>
        <text>分享成就</text>
      </view>
    </view>
    
    <view class="bottom-safe-area"></view>
  </scroll-view>
</view>
bash 复制代码
/* pages/assessment-results/assessment-results.wxss */

/* 基础变量 */
page {
  background-color: #f0f4f8;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
}

.page-container {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  background-color: #f0f4f8;
}

/* 内容区域 */
.content-area {
  flex: 1;
  padding: 0 16px 40px;
  box-sizing: border-box;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}

/* 卡片样式 */
.card {
	margin-top: 38rpx;
  background-color: #ffffff;
  border-radius: 24px;
  padding: 20px;
  margin-bottom: 20px;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
  position: relative;
  overflow: hidden;
}

.card::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  border-radius: 24px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
  opacity: 0.3;
  pointer-events: none;
}

.card-title {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 16px;
  font-weight: bold;
  color: #1e293b;
  margin-bottom: 16px;
}

.title-icon {
  font-size: 18px;
}

.card-title-simple {
  font-size: 16px;
  font-weight: bold;
  color: #1e293b;
  margin-bottom: 16px;
}

/* 测评分数对比 */
.score-compare {
  display: flex;
  align-items: center;
  gap: 16px;
  margin-bottom: 12px;
}

.score-item {
  flex: 1;
  background-color: #f8fafc;
  border-radius: 16px;
  padding: 12px;
  text-align: center;
}

.score-item.highlight {
  background-color: #e0f2fe;
  border: 1px solid #bae6fd;
}

.score-label {
  display: block;
  font-size: 12px;
  color: #94a3b8;
  margin-bottom: 4px;
}

.score-label.highlight {
  color: #0284c7;
  font-weight: bold;
  font-size: 11px;
}

.score-value {
  display: block;
  font-size: 24px;
  font-weight: bold;
  color: #64748b;
}

.score-value.highlight {
  color: #0284c7;
}

.score-unit {
  font-size: 14px;
  font-weight: normal;
}

.score-arrow {
  font-size: 20px;
  color: #cbd5e1;
}

/* 分数变化 */
.score-change {
  background-color: #dcfce7;
  border-radius: 12px;
  padding: 12px;
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.change-text {
  font-size: 14px;
  font-weight: bold;
  color: #166534;
}

.change-badge {
  background-color: #86efac;
  color: #166534;
  font-size: 12px;
  font-weight: bold;
  padding: 4px 8px;
  border-radius: 99px;
}

/* 总结文字 */
.summary-text {
  margin-top: 16px;
  font-size: 14px;
  line-height: 1.6;
  color: #475569;
}

.summary-label {
  font-weight: bold;
  color: #1e293b;
}

/* 进度条列表 */
.progress-list {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.progress-item {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.progress-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.progress-name {
  font-size: 13px;
  color: #475569;
}

.progress-percent {
  font-size: 13px;
  font-weight: bold;
  color: #0284c7;
}

.progress-bg {
  height: 8px;
  background-color: #e2e8f0;
  border-radius: 99px;
  overflow: hidden;
  position: relative;
}

.progress-fill {
  height: 100%;
  background: linear-gradient(90deg, #0ea5e9, #38bdf8);
  border-radius: 99px;
  transition: width 0.3s ease;
  position: relative;
  overflow: hidden;
}

.progress-fill::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: linear-gradient(90deg, rgba(255, 255, 255, 0.3), rgba(255, 255, 255, 0.1));
  border-radius: 99px;
  pointer-events: none;
}

/* 统计数据网格 */
.stats-grid {
  display: flex;
  justify-content: space-around;
  margin-bottom: 24px;
}

.stat-item {
  text-align: center;
  flex: 1;
  position: relative;
}

.stat-item.divider {
  border-left: 1px solid #f1f5f9;
  border-right: 1px solid #f1f5f9;
}

.stat-value {
  display: block;
  font-size: 24px;
  font-weight: 900;
  color: #1e293b;
  font-family: 'PingFang SC', sans-serif;
}

.stat-unit-small {
  font-size: 14px;
}

.stat-label {
  display: block;
  font-size: 10px;
  color: #94a3b8;
  margin-top: 4px;
  font-family: 'PingFang SC', sans-serif;
}

/* 图表区域 */
.chart-section {
  margin-bottom: 20px;
}

.section-title {
  font-size: 14px;
  font-weight: bold;
  color: #475569;
  margin-bottom: 12px;
}

.chart-container {
  width: 100%;
  height: 160px;
  background-color: #f8fafc;
  border-radius: 12px;
  overflow: hidden;
  position: relative;
}

.emotion-chart {
  width: 100%;
  height: 100%;
  display: block;
}

/* 场景列表 */
.scene-section {
  margin-top: 20px;
}

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

.scene-item {
  display: flex;
  align-items: center;
  gap: 12px;
  background-color: #f8fafc;
  padding: 12px;
  border-radius: 12px;
}

.scene-rank {
  width: 24px;
  height: 24px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 12px;
  font-weight: bold;
  color: #ffffff;
  background-color: #cbd5e1;
}

.scene-rank.top-1 {
  background-color: #fbbf24;
}

.scene-rank.top-2 {
  background-color: #94a3b8;
}

.scene-rank.top-3 {
  background-color: #fdba74;
}

.scene-name {
  flex: 1;
  font-size: 14px;
  color: #334155;
  font-weight: 500;
}

.scene-percent {
  font-size: 12px;
  color: #94a3b8;
}

/* 徽章卡片 */
.badge-card {
  background: linear-gradient(135deg, #0ea5e9, #2563eb);
  color: #ffffff;
}

.badge-header {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  margin-bottom: 16px;
}

.badge-points {
  flex: 1;
}

.points-label {
  font-size: 13px;
  color: #e0f2fe;
  margin-bottom: 4px;
}

.points-value {
  display: flex;
  align-items: center;
  gap: 8px;
}

.shell-icon {
  font-size: 24px;
}

.points-number {
  font-size: 32px;
  font-weight: bold;
}

.badge-icon-box {
  background-color: rgba(255, 255, 255, 0.2);
  padding: 8px;
  border-radius: 12px;
  backdrop-filter: blur(4px);
}

.trophy-icon {
  font-size: 24px;
}

/* 徽章列表 */
.badges-section {
  margin-top: 12px;
}

.badges-title {
  display: block;
  font-size: 13px;
  font-weight: bold;
  color: #e0f2fe;
  margin-bottom: 12px;
}

.badges-scroll {
  white-space: nowrap;
}

.badges-list {
  display: inline-flex;
  gap: 12px;
}

.badge-item {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  background-color: rgba(255, 255, 255, 0.3);
  padding: 10px;
  border-radius: 12px;
  border: 1px solid rgba(255, 255, 255, 0.4);
  min-width: 70px;
  position: relative;
  overflow: hidden;
}

.badge-item::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: linear-gradient(135deg, rgba(255, 255, 255, 0.4), rgba(255, 255, 255, 0.1));
  border-radius: 12px;
  pointer-events: none;
}

.badge-icon {
  width: 44px;
  height: 44px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-bottom: 4px;
  font-size: 22px;
  position: relative;
  overflow: hidden;
  z-index: 1;
}

.badge-icon.unlocked {
  background: linear-gradient(135deg, #22d3ee, #3b82f6);
  box-shadow: 0 2px 8px rgba(59, 130, 246, 0.4);
}

.badge-icon.locked {
  background-color: rgba(255, 255, 255, 0.15);
}

.badge-name {
  font-size: 11px;
  font-weight: bold;
  color: #ffffff;
  text-align: center;
  position: relative;
  z-index: 1;
}

/* 评分区域 */
.rating-section {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 8px;
}

.stars {
  display: flex;
  gap: 4px;
}

.star {
  font-size: 24px;
  color: #fbbf24;
}

.star.active {
  color: #f97316;
}

.rating-value {
  font-size: 20px;
  font-weight: bold;
  color: #475569;
}

.rating-comment {
  font-size: 14px;
  color: #64748b;
  line-height: 1.5;
}

/* 建议卡片 */
.suggestion-card {
  background-color: #f0f9ff;
  border: 1px solid #bae6fd;
}

.suggestion-list {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.suggestion-item {
  display: flex;
  gap: 12px;
}

.suggestion-num {
  width: 32px;
  height: 32px;
  border-radius: 50%;
  background-color: #7dd3fc;
  color: #0c4a6e;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 14px;
  font-weight: bold;
  flex-shrink: 0;
}

.suggestion-num.disabled {
  background-color: #e2e8f0;
  color: #94a3b8;
}

.suggestion-content {
  flex: 1;
}

.suggestion-title {
  display: block;
  font-size: 15px;
  font-weight: bold;
  color: #1e293b;
  margin-bottom: 4px;
}

.suggestion-title.disabled {
  color: #94a3b8;
}

.suggestion-desc {
  display: block;
  font-size: 13px;
  color: #475569;
  line-height: 1.5;
}

.suggestion-desc.disabled {
  color: #94a3b8;
}

/* 操作按钮 */
.action-buttons {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 16px;
  margin-top: 24px;
  padding-bottom: 24px;
}

.action-btn {
  height: 56px;
  border-radius: 16px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 16px;
  font-weight: bold;
  transition: all 0.2s;
}

.action-btn.secondary {
  background-color: #ffffff;
  border: 2px solid #e2e8f0;
  color: #334155;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}

.action-btn.primary {
  background-color: #0ea5e9;
  color: #ffffff;
  box-shadow: 0 4px 12px rgba(14, 165, 233, 0.3);
  position: relative;
}

.action-btn.primary::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  border-radius: 16px;
  box-shadow: 0 8px 24px rgba(14, 165, 233, 0.4);
  opacity: 0.5;
  pointer-events: none;
}

.btn-icon {
  margin-right: 8px;
  font-size: 18px;
}

/* 底部安全区域 */
.bottom-safe-area {
  height: 20px;
}

/* ECharts 容器 */
ec-canvas {
  width: 100%;
  height: 100%;
}
相关推荐
Greg_Zhong1 天前
微信小程序中进度条总结
微信小程序·自定义进度条·slider进度条
这是个栗子2 天前
【微信小程序问题解决】删掉 “navigationStyle“: “custom“ 后仍触发了自定义导航栏
微信小程序·小程序·navigationstyle
liangdabiao2 天前
定制的乐高马赛克像素画生成器-微信小程序版本-AI 风格优化-一键完成所有工作
人工智能·微信小程序·小程序
编程小白gogogo2 天前
苍穹外卖微信小程序导入hbuilder后点击运行选择在微信开发者工具中打开,微信开发者工具打开却没有运行微信小程序解决办法
微信小程序·小程序
天籁晴空2 天前
微信小程序 静默登录 + 授权登录 双模式配合的设计方案
前端·微信小程序·uni-app
小徐_23333 天前
uni-app 组件库 Wot UI 2.0 发布了,我们带来了这些改变!
前端·微信小程序·uni-app
Greg_Zhong3 天前
微信小程序中实现自定义颜色选择器(简陋版对比精致版)
微信小程序·自定义颜色选择器面板
杰建云1673 天前
2026年第三方平台制作微信小程序多少钱?
微信小程序·小程序·小程序制作
vipbic4 天前
独立开发复盘:我用 Uni-app + Strapi v5 肝了一个“会上瘾”的打卡小程序
前端·微信小程序