文章目录[隐藏]
WordPress高级教程:开发集成在线问卷调查结果实时数据看板
引言:WordPress作为企业级应用开发平台
WordPress早已超越了简单的博客系统定位,如今已成为一个功能强大的内容管理系统(CMS)和应用程序框架。全球超过43%的网站使用WordPress构建,这得益于其灵活的可扩展性和庞大的开发者社区。在本教程中,我们将探索如何通过WordPress代码二次开发,实现一个专业级的在线问卷调查结果实时数据看板,展示如何将WordPress转变为功能丰富的互联网应用平台。
传统的问卷调查工具往往独立于企业网站存在,导致数据孤岛和用户体验割裂。通过将问卷调查与数据可视化看板直接集成到WordPress中,我们可以创建无缝的用户体验,同时利用WordPress的用户管理、权限控制和内容展示能力。本教程将引导您完成从需求分析到代码实现的完整过程,适合有一定WordPress开发经验的开发者。
第一部分:项目架构设计与技术选型
1.1 需求分析与功能规划
在开始编码之前,我们需要明确项目的核心需求:
- 问卷调查功能:支持多种题型(单选、多选、矩阵、评分等)
- 实时数据收集:用户提交问卷后立即更新数据存储
- 可视化看板:多种图表展示调查结果(柱状图、饼图、趋势图等)
- 权限管理:不同用户角色查看不同级别的数据
- 响应式设计:在桌面和移动设备上都能良好显示
- 数据导出:支持将结果导出为CSV、PDF等格式
1.2 技术栈选择
基于WordPress生态,我们选择以下技术方案:
- 核心框架:WordPress 5.8+,使用自定义文章类型存储问卷和结果
- 前端框架:Vue.js 3.0 + Chart.js,实现交互式数据可视化
- 实时通信:REST API + WebSocket(可选,用于实时更新)
- 数据库优化:自定义数据库表结构,提高查询效率
- 缓存机制:Transients API + Object Cache,提升性能
1.3 系统架构设计
我们将采用分层架构设计:
表现层 (前端展示)
↓
业务逻辑层 (WordPress插件)
↓
数据访问层 (自定义数据库表+API)
↓
数据存储层 (MySQL + 缓存)
第二部分:创建WordPress问卷调查插件
2.1 插件基础结构
首先创建插件主文件 wp-survey-dashboard.php:
<?php
/**
* Plugin Name: WordPress问卷调查与数据看板
* Plugin URI: https://yourwebsite.com/
* Description: 集成在线问卷调查和实时数据看板的高级解决方案
* Version: 1.0.0
* Author: 您的名称
* License: GPL v2 or later
*/
// 防止直接访问
if (!defined('ABSPATH')) {
exit;
}
// 定义插件常量
define('WPSD_VERSION', '1.0.0');
define('WPSD_PLUGIN_DIR', plugin_dir_path(__FILE__));
define('WPSD_PLUGIN_URL', plugin_dir_url(__FILE__));
// 初始化插件
class WP_Survey_Dashboard {
private static $instance = null;
public static function get_instance() {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
private function __construct() {
$this->init_hooks();
}
private function init_hooks() {
// 激活/停用钩子
register_activation_hook(__FILE__, array($this, 'activate'));
register_deactivation_hook(__FILE__, array($this, 'deactivate'));
// 初始化
add_action('init', array($this, 'init'));
// 管理菜单
add_action('admin_menu', array($this, 'add_admin_menu'));
// 加载脚本和样式
add_action('wp_enqueue_scripts', array($this, 'enqueue_frontend_assets'));
add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_assets'));
}
public function activate() {
// 创建数据库表
$this->create_database_tables();
// 设置默认选项
update_option('wpsd_version', WPSD_VERSION);
// 刷新重写规则
flush_rewrite_rules();
}
public function deactivate() {
// 清理临时数据
// 注意:不删除调查数据
flush_rewrite_rules();
}
public function init() {
// 注册自定义文章类型
$this->register_post_types();
// 注册短代码
add_shortcode('survey_form', array($this, 'survey_form_shortcode'));
add_shortcode('survey_dashboard', array($this, 'survey_dashboard_shortcode'));
// 初始化REST API
add_action('rest_api_init', array($this, 'register_rest_routes'));
}
// 其他方法将在后续部分实现
}
// 启动插件
WP_Survey_Dashboard::get_instance();
2.2 创建数据库表结构
在 activate 方法中调用的 create_database_tables 方法:
private function create_database_tables() {
global $wpdb;
$charset_collate = $wpdb->get_charset_collate();
// 调查表
$table_surveys = $wpdb->prefix . 'wpsd_surveys';
$table_results = $wpdb->prefix . 'wpsd_results';
$table_answers = $wpdb->prefix . 'wpsd_answers';
// 创建调查表
$sql_surveys = "CREATE TABLE IF NOT EXISTS $table_surveys (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
title VARCHAR(255) NOT NULL,
description TEXT,
settings LONGTEXT,
status VARCHAR(20) DEFAULT 'draft',
created_by BIGINT(20) UNSIGNED,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY status (status),
KEY created_by (created_by)
) $charset_collate;";
// 创建结果表
$sql_results = "CREATE TABLE IF NOT EXISTS $table_results (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
survey_id BIGINT(20) UNSIGNED NOT NULL,
user_id BIGINT(20) UNSIGNED,
user_ip VARCHAR(45),
user_agent TEXT,
session_id VARCHAR(100),
completed TINYINT(1) DEFAULT 0,
started_at DATETIME DEFAULT CURRENT_TIMESTAMP,
completed_at DATETIME,
metadata LONGTEXT,
PRIMARY KEY (id),
KEY survey_id (survey_id),
KEY user_id (user_id),
KEY session_id (session_id),
KEY completed (completed)
) $charset_collate;";
// 创建答案表
$sql_answers = "CREATE TABLE IF NOT EXISTS $table_answers (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
result_id BIGINT(20) UNSIGNED NOT NULL,
question_id VARCHAR(100) NOT NULL,
question_type VARCHAR(50) NOT NULL,
question_text TEXT,
answer_value LONGTEXT,
answer_text TEXT,
answered_at DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY result_id (result_id),
KEY question_id (question_id),
KEY question_type (question_type)
) $charset_collate;";
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
dbDelta($sql_surveys);
dbDelta($sql_results);
dbDelta($sql_answers);
}
2.3 注册自定义文章类型和分类
扩展 init 方法中的 register_post_types:
private function register_post_types() {
// 注册调查自定义文章类型
register_post_type('wpsd_survey',
array(
'labels' => array(
'name' => __('问卷调查', 'wp-survey-dashboard'),
'singular_name' => __('调查', 'wp-survey-dashboard'),
'add_new' => __('添加新调查', 'wp-survey-dashboard'),
'add_new_item' => __('添加新调查', 'wp-survey-dashboard'),
'edit_item' => __('编辑调查', 'wp-survey-dashboard'),
'new_item' => __('新调查', 'wp-survey-dashboard'),
'view_item' => __('查看调查', 'wp-survey-dashboard'),
'search_items' => __('搜索调查', 'wp-survey-dashboard'),
'not_found' => __('未找到调查', 'wp-survey-dashboard'),
'not_found_in_trash' => __('回收站中无调查', 'wp-survey-dashboard'),
),
'public' => true,
'has_archive' => true,
'rewrite' => array('slug' => 'surveys'),
'supports' => array('title', 'editor', 'author', 'revisions'),
'menu_icon' => 'dashicons-chart-bar',
'show_in_rest' => true,
'rest_base' => 'wpsd_surveys',
'capability_type' => 'post',
'capabilities' => array(
'create_posts' => 'create_wpsd_surveys',
),
'map_meta_cap' => true,
)
);
// 注册调查分类
register_taxonomy('survey_category', 'wpsd_survey',
array(
'labels' => array(
'name' => __('调查分类', 'wp-survey-dashboard'),
'singular_name' => __('分类', 'wp-survey-dashboard'),
),
'hierarchical' => true,
'show_in_rest' => true,
'show_admin_column' => true,
)
);
}
第三部分:构建问卷调查前端功能
3.1 创建问卷调查表单短代码
实现 survey_form_shortcode 方法:
public function survey_form_shortcode($atts) {
// 提取短代码属性
$atts = shortcode_atts(array(
'id' => 0,
'title' => 'yes',
'description' => 'yes',
), $atts, 'survey_form');
$survey_id = intval($atts['id']);
if (!$survey_id) {
return '<div class="wpsd-error">请指定有效的调查ID</div>';
}
// 获取调查数据
global $wpdb;
$table_surveys = $wpdb->prefix . 'wpsd_surveys';
$survey = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM $table_surveys WHERE id = %d AND status = 'published'",
$survey_id
));
if (!$survey) {
return '<div class="wpsd-error">调查不存在或未发布</div>';
}
// 解析调查设置
$settings = json_decode($survey->settings, true);
$questions = isset($settings['questions']) ? $settings['questions'] : array();
// 生成唯一会话ID
$session_id = $this->generate_session_id();
// 输出调查表单
ob_start();
?>
<div class="wpsd-survey-container" data-survey-id="<?php echo esc_attr($survey_id); ?>">
<div class="wpsd-survey-header">
<?php if ($atts['title'] === 'yes') : ?>
<h2 class="wpsd-survey-title"><?php echo esc_html($survey->title); ?></h2>
<?php endif; ?>
<?php if ($atts['description'] === 'yes' && !empty($survey->description)) : ?>
<div class="wpsd-survey-description">
<?php echo wp_kses_post($survey->description); ?>
</div>
<?php endif; ?>
</div>
<form id="wpsd-survey-form-<?php echo esc_attr($survey_id); ?>" class="wpsd-survey-form" method="post">
<input type="hidden" name="survey_id" value="<?php echo esc_attr($survey_id); ?>">
<input type="hidden" name="session_id" value="<?php echo esc_attr($session_id); ?>">
<input type="hidden" name="wpsd_nonce" value="<?php echo wp_create_nonce('wpsd_submit_survey'); ?>">
<div class="wpsd-questions-container">
<?php foreach ($questions as $index => $question) : ?>
<div class="wpsd-question" data-question-id="<?php echo esc_attr($question['id']); ?>" data-question-type="<?php echo esc_attr($question['type']); ?>">
<div class="wpsd-question-header">
<h3 class="wpsd-question-title">
<span class="wpsd-question-number"><?php echo $index + 1; ?>.</span>
<?php echo esc_html($question['title']); ?>
<?php if (isset($question['required']) && $question['required']) : ?>
<span class="wpsd-required">*</span>
<?php endif; ?>
</h3>
<?php if (!empty($question['description'])) : ?>
<div class="wpsd-question-description">
<?php echo esc_html($question['description']); ?>
</div>
<?php endif; ?>
</div>
<div class="wpsd-question-body">
<?php echo $this->render_question_field($question); ?>
</div>
</div>
<?php endforeach; ?>
</div>
<div class="wpsd-form-footer">
<button type="submit" class="wpsd-submit-button">
<?php _e('提交调查', 'wp-survey-dashboard'); ?>
</button>
<div class="wpsd-form-message"></div>
</div>
</form>
</div>
<?php
return ob_get_clean();
}
private function render_question_field($question) {
$field_html = '';
$question_id = esc_attr($question['id']);
$field_name = "answers[$question_id]";
switch ($question['type']) {
case 'radio':
case 'checkbox':
$field_html .= '<div class="wpsd-options-container">';
foreach ($question['options'] as $option) {
$option_id = esc_attr($option['id']);
$input_id = "{$question_id}_{$option_id}";
if ($question['type'] === 'radio') {
$field_html .= '<div class="wpsd-option">';
$field_html .= '<input type="radio" id="' . $input_id . '" name="' . $field_name . '" value="' . esc_attr($option['value']) . '">';
$field_html .= '<label for="' . $input_id . '">' . esc_html($option['label']) . '</label>';
$field_html .= '</div>';
} else {
$field_html .= '<div class="wpsd-option">';
$field_html .= '<input type="checkbox" id="' . $input_id . '" name="' . $field_name . '[]" value="' . esc_attr($option['value']) . '">';
$field_html .= '<label for="' . $input_id . '">' . esc_html($option['label']) . '</label>';
$field_html .= '</div>';
}
}
$field_html .= '</div>';
break;
case 'select':
$field_html .= '<select name="' . $field_name . '" class="wpsd-select">';
if (isset($question['placeholder'])) {
$field_html .= '<option value="">' . esc_html($question['placeholder']) . '</option>';
}
foreach ($question['options'] as $option) {
$field_html .= '<option value="' . esc_attr($option['value']) . '">' . esc_html($option['label']) . '</option>';
}
$field_html .= '</select>';
break;
case 'rating':
$field_html .= '<div class="wpsd-rating-container" data-max-rating="' . esc_attr($question['max_rating']) . '">';
for ($i = 1; $i <= $question['max_rating']; $i++) {
$field_html .= '<div class="wpsd-rating-star" data-value="' . $i . '">';
$field_html .= '<span class="wpsd-star-icon">★</span>';
$field_html .= '</div>';
}
$field_html .= '<input type="hidden" name="' . $field_name . '" value="">';
$field_html .= '</div>';
break;
case 'text':
case 'textarea':
case 'email':
$input_type = $question['type'] === 'textarea' ? 'textarea' : 'text';
$input_type_attr = $question['type'] === 'email' ? 'email' : 'text';
if ($input_type === 'textarea') {
$field_html .= '<textarea name="' . $field_name . '" class="wpsd-textarea"';
if (isset($question['placeholder'])) {
$field_html .= ' placeholder="' . esc_attr($question['placeholder']) . '"';
}
if (isset($question['rows'])) {
$field_html .= ' rows="' . esc_attr($question['rows']) . '"';
}
$field_html .= '></textarea>';
} else {
$field_html .= '<input type="' . $input_type_attr . '" name="' . $field_name . '" class="wpsd-input"';
if (isset($question['placeholder'])) {
$field_html .= ' placeholder="' . esc_attr($question['placeholder']) . '"';
}
if (isset($question['maxlength'])) {
$field_html .= ' maxlength="' . esc_attr($question['maxlength']) . '"';
}
$field_html .= '>';
}
break;
case 'matrix':
$field_html .= '<div class="wpsd-matrix-container">';
$field_html .= '<table class="wpsd-matrix-table">';
$field_html .= '<thead><tr><th></th>';
foreach ($question['columns'] as $column) {
$field_html .= '<th>' . esc_html($column['label']) . '</th>';
}
$field_html .= '</tr></thead>';
$field_html .= '<tbody>';
foreach ($question['rows'] as $row) {
$field_html .= '<tr>';
$field_html .= '<td class="wpsd-matrix-row-label">' . esc_html($row['label']) . '</td>';
foreach ($question['columns'] as $column) {
$input_id = "{$question_id}_{$row['id']}_{$column['id']}";
$field_html .= '<td class="wpsd-matrix-cell">';
$field_html .= '<input type="radio" id="' . $input_id . '" name="answers[' . $question_id . '][' . $row['id'] . ']" value="' . esc_attr($column['value']) . '">';
$field_html .= '<label for="' . $input_id . '"></label>';
$field_html .= '</td>';
}
$field_html .= '</tr>';
}
$field_html .= '</tbody></table></div>';
break;
default:
$field_html .= '<p>不支持的问题类型: ' . esc_html($question['type']) . '</p>';
}
return $field_html;
}
private function generate_session_id() {
if (isset($_COOKIE['wpsd_session_id'])) {
return sanitize_text_field($_COOKIE['wpsd_session_id']);
}
$session_id = wp_generate_uuid4();
setcookie('wpsd_session_id', $session_id, time() + 3600 * 24 * 7, COOKIEPATH, COOKIE_DOMAIN);
return $session_id;
}
3.2 前端脚本和样式
实现前端资源加载方法:
public function enqueue_frontend_assets() {
// 只在需要时加载
global $post;
if (is_a($post, 'WP_Post') && (has_shortcode($post->post_content, 'survey_form') || has_shortcode($post->post_content, 'survey_dashboard'))) {
// 加载Vue.js
wp_enqueue_script('vue-js', 'https://cdn.jsdelivr.net/npm/vue@3.2.31/dist/vue.global.prod.js', array(), '3.2.31', true);
// 加载Chart.js
wp_enqueue_script('chart-js', 'https://cdn.jsdelivr.net/npm/chart.js@3.7.1/dist/chart.min.js', array(), '3.7.1', true);
// 加载插件主脚本
wp_enqueue_script(
'wpsd-frontend',
WPSD_PLUGIN_URL . 'assets/js/frontend.js',
array('jquery', 'vue-js', 'chart-js'),
WPSD_VERSION,
true
);
// 本地化脚本
wp_localize_script('wpsd-frontend', 'wpsd_ajax', array(
'ajax_url' => admin_url('admin-ajax.php'),
'rest_url' => rest_url('wpsd/v1/'),
'nonce' => wp_create_nonce('wpsd_ajax_nonce')
));
// 加载样式
wp_enqueue_style(
'wpsd-frontend-style',
WPSD_PLUGIN_URL . 'assets/css/frontend.css',
array(),
WPSD_VERSION
);
}
}
创建前端JavaScript文件 assets/js/frontend.js:
(function($) {
'use strict';
// 问卷调查提交处理
$(document).on('submit', '.wpsd-survey-form', function(e) {
e.preventDefault();
const $form = $(this);
const $submitBtn = $form.find('.wpsd-submit-button');
const $message = $form.find('.wpsd-form-message');
const formData = new FormData(this);
// 禁用提交按钮
$submitBtn.prop('disabled', true).text('提交中...');
$message.removeClass('wpsd-success wpsd-error').text('');
// 验证必填字段
const requiredFields = $form.find('.wpsd-required');
let isValid = true;
requiredFields.each(function() {
const $question = $(this).closest('.wpsd-question');
const questionType = $question.data('question-type');
const questionId = $question.data('question-id');
let isAnswered = false;
if (questionType === 'radio' || questionType === 'select') {
isAnswered = $form.find(`[name="answers[${questionId}]"]:checked`).length > 0 ||
$form.find(`[name="answers[${questionId}]"]`).val() !== '';
} else if (questionType === 'checkbox') {
isAnswered = $form.find(`[name="answers[${questionId}][]"]:checked`).length > 0;
} else if (questionType === 'matrix') {
const rowCount = $question.find('.wpsd-matrix-row-label').length;
const answeredRows = $question.find('input[type="radio"]:checked').length;
isAnswered = answeredRows === rowCount;
} else {
isAnswered = $form.find(`[name="answers[${questionId}]"]`).val().trim() !== '';
}
if (!isAnswered) {
$question.addClass('wpsd-error');
isValid = false;
} else {
$question.removeClass('wpsd-error');
}
});
if (!isValid) {
$message.addClass('wpsd-error').text('请填写所有必填问题');
$submitBtn.prop('disabled', false).text('提交调查');
return;
}
// 发送AJAX请求
$.ajax({
url: wpsd_ajax.ajax_url,
type: 'POST',
data: formData,
processData: false,
contentType: false,
success: function(response) {
if (response.success) {
$message.addClass('wpsd-success').text(response.data.message);
$form[0].reset();
// 更新评分控件
$form.find('.wpsd-rating-container').each(function() {
$(this).find('.wpsd-rating-star').removeClass('active');
$(this).find('input[type="hidden"]').val('');
});
// 显示感谢信息
setTimeout(function() {
$form.html(`
<div class="wpsd-thank-you">
<h3>感谢参与!</h3>
<p>您的反馈对我们非常重要。</p>
<p>您可以在调查结果页面查看实时统计数据。</p>
</div>
`);
}, 2000);
} else {
$message.addClass('wpsd-error').text(response.data.message);
$submitBtn.prop('disabled', false).text('提交调查');
}
},
error: function() {
$message.addClass('wpsd-error').text('提交失败,请稍后重试');
$submitBtn.prop('disabled', false).text('提交调查');
}
});
});
// 评分控件交互
$(document).on('mouseenter', '.wpsd-rating-star', function() {
const $container = $(this).closest('.wpsd-rating-container');
const value = $(this).data('value');
$container.find('.wpsd-rating-star').each(function() {
if ($(this).data('value') <= value) {
$(this).addClass('hover');
} else {
$(this).removeClass('hover');
}
});
});
$(document).on('mouseleave', '.wpsd-rating-container', function() {
$(this).find('.wpsd-rating-star').removeClass('hover');
});
$(document).on('click', '.wpsd-rating-star', function() {
const $container = $(this).closest('.wpsd-rating-container');
const value = $(this).data('value');
const $hiddenInput = $container.find('input[type="hidden"]');
$container.find('.wpsd-rating-star').removeClass('active hover');
$container.find('.wpsd-rating-star').each(function() {
if ($(this).data('value') <= value) {
$(this).addClass('active');
}
});
$hiddenInput.val(value);
});
// 数据看板Vue应用
if (typeof Vue !== 'undefined' && $('.wpsd-dashboard-container').length > 0) {
const { createApp } = Vue;
const DashboardApp = {
data() {
return {
surveyId: 0,
loading: true,
error: null,
surveyData: null,
chartData: {},
filters: {
dateRange: 'all',
startDate: '',
endDate: '',
questionFilter: 'all'
},
realTime: false,
refreshInterval: null
};
},
mounted() {
const container = document.querySelector('.wpsd-dashboard-container');
this.surveyId = container.dataset.surveyId;
this.realTime = container.dataset.realtime === 'true';
this.loadSurveyData();
if (this.realTime) {
this.startRealTimeUpdates();
}
},
methods: {
async loadSurveyData() {
this.loading = true;
this.error = null;
try {
const response = await fetch(
`${wpsd_ajax.rest_url}survey/${this.surveyId}/results?` +
new URLSearchParams(this.filters)
);
if (!response.ok) {
throw new Error('获取数据失败');
}
const data = await response.json();
this.surveyData = data;
this.prepareChartData();
} catch (err) {
this.error = err.message;
console.error('加载调查数据失败:', err);
} finally {
this.loading = false;
}
},
prepareChartData() {
if (!this.surveyData || !this.surveyData.questions) return;
this.chartData = {};
this.surveyData.questions.forEach(question => {
if (question.type === 'radio' || question.type === 'select' || question.type === 'checkbox') {
this.chartData[question.id] = this.createPieChartData(question);
} else if (question.type === 'rating') {
this.chartData[question.id] = this.createBarChartData(question);
} else if (question.type === 'matrix') {
this.chartData[question.id] = this.createMatrixChartData(question);
}
});
// 渲染图表
this.$nextTick(() => {
this.renderCharts();
});
},
createPieChartData(question) {
const labels = [];
const data = [];
const backgroundColors = [];
question.options.forEach((option, index) => {
labels.push(option.label);
data.push(option.count || 0);
backgroundColors.push(this.getColor(index));
});
return {
type: 'pie',
data: {
labels: labels,
datasets: [{
data: data,
backgroundColor: backgroundColors,
borderWidth: 1
}]
},
options: {
responsive: true,
plugins: {
legend: {
position: 'right'
},
title: {
display: true,
text: question.title
}
}
}
};
},
createBarChartData(question) {
const labels = [];
const data = [];
for (let i = 1; i <= question.max_rating; i++) {
labels.push(`${i}星`);
const count = question.ratings ? (question.ratings[i] || 0) : 0;
data.push(count);
}
return {
type: 'bar',
data: {
labels: labels,
datasets: [{
label: '评分分布',
data: data,
backgroundColor: 'rgba(54, 162, 235, 0.5)',
borderColor: 'rgba(54, 162, 235, 1)',
borderWidth: 1
}]
},
options: {
responsive: true,
scales: {
y: {
beginAtZero: true,
ticks: {
stepSize: 1
}
}
},
plugins: {
title: {
display: true,
text: question.title
}
}
}
};
},
createMatrixChartData(question) {
const labels = question.rows.map(row => row.label);
const datasets = [];
question.columns.forEach((column, colIndex) => {
const data = question.rows.map(row => {
const cell = question.matrix_data?.find(
d => d.row_id === row.id && d.column_id === column.id
);
return cell ? cell.count : 0;
});
datasets.push({
label: column.label,
data: data,
backgroundColor: this.getColor(colIndex, 0.5),
borderColor: this.getColor(colIndex, 1),
borderWidth: 1
});
});
return {
type: 'bar',
data: {
labels: labels,
datasets: datasets
},
options: {
responsive: true,
scales: {
x: {
stacked: true
},
y: {
stacked: true,
beginAtZero: true
}
},
plugins: {
title: {
display: true,
text: question.title
}
}
}
};
},
renderCharts() {
Object.keys(this.chartData).forEach(questionId => {
const canvas = document.getElementById(`chart-${questionId}`);
if (!canvas) return;
const ctx = canvas.getContext('2d');
const chartConfig = this.chartData[questionId];
// 销毁现有图表实例
if (canvas.chart) {
canvas.chart.destroy();
}
// 创建新图表
canvas.chart = new Chart(ctx, chartConfig);
});
},
getColor(index, alpha = 1) {
const colors = [
`rgba(255, 99, 132, ${alpha})`,
`rgba(54, 162, 235, ${alpha})`,
`rgba(255, 206, 86, ${alpha})`,
`rgba(75, 192, 192, ${alpha})`,
`rgba(153, 102, 255, ${alpha})`,
`rgba(255, 159, 64, ${alpha})`,
`rgba(199, 199, 199, ${alpha})`,
`rgba(83, 102, 255, ${alpha})`,
`rgba(40, 159, 64, ${alpha})`,
`rgba(210, 199, 199, ${alpha})`
];
return colors[index % colors.length];
},
applyFilters() {
this.loadSurveyData();
},
resetFilters() {
this.filters = {
dateRange: 'all',
startDate: '',
endDate: '',
questionFilter: 'all'
};
this.loadSurveyData();
},
startRealTimeUpdates() {
// 每30秒更新一次数据
this.refreshInterval = setInterval(() => {
this.loadSurveyData();
}, 30000);
},
stopRealTimeUpdates() {
if (this.refreshInterval) {
clearInterval(this.refreshInterval);
this.refreshInterval = null;
}
},
exportData(format) {
const url = `${wpsd_ajax.rest_url}survey/${this.surveyId}/export?format=${format}`;
window.open(url, '_blank');
}
},
beforeUnmount() {
this.stopRealTimeUpdates();
},
template: `
<div class="wpsd-dashboard">
<div v-if="loading" class="wpsd-loading">
加载中...
</div>
<div v-else-if="error" class="wpsd-error">
{{ error }}
</div>
<div v-else>
<!-- 控制面板 -->
<div class="wpsd-dashboard-controls">
<div class="wpsd-filters">
<div class="wpsd-filter-group">
<label>时间范围:</label>
<select v-model="filters.dateRange" @change="applyFilters">
<option value="all">全部时间</option>
<option value="today">今天</option>
<option value="yesterday">昨天</option>
<option value="week">本周</option>
<option value="month">本月</option>
<option value="custom">自定义</option>
</select>
</div>
<div v-if="filters.dateRange === 'custom'" class="wpsd-filter-group">
