文章目录[隐藏]
WordPress集成教程:连接项目管理软件并展示项目状态
引言:WordPress的无限可能性
在当今数字化时代,企业网站已不再仅仅是展示公司信息的静态页面,而是逐渐演变为功能丰富的业务平台。WordPress作为全球最受欢迎的内容管理系统,其真正的力量不仅在于创建博客或简单网站,更在于通过代码二次开发实现各种复杂功能。本教程将深入探讨如何将WordPress与项目管理软件集成,实时展示项目状态,并实现一系列常用互联网小工具功能。
传统的企业网站往往与内部业务系统脱节,导致信息更新滞后、数据不一致等问题。通过将WordPress与项目管理工具(如Jira、Asana、Trello、Monday.com等)集成,我们可以创建一个动态的、实时更新的项目状态展示平台,让客户、团队成员和利益相关者随时了解项目进展。
第一部分:准备工作与环境搭建
1.1 选择合适的WordPress环境
在开始集成之前,确保你的WordPress环境满足以下要求:
- WordPress 5.0或更高版本
- PHP 7.4或更高版本(推荐PHP 8.0+)
- 支持HTTPS的域名
- 适当的服务器资源(至少1GB RAM,建议2GB以上)
1.2 安装必要插件
虽然本教程主要关注代码开发,但一些基础插件能极大提高开发效率:
- Advanced Custom Fields (ACF) - 用于创建自定义字段和管理数据
- Custom Post Type UI - 简化自定义文章类型的创建
- Query Monitor - 调试工具,监控数据库查询和性能
- WP REST API Controller - 管理REST API端点
1.3 设置子主题
为避免主题更新覆盖自定义代码,强烈建议创建子主题:
- 在
wp-content/themes/目录下创建新文件夹,如my-custom-theme -
创建
style.css文件,添加主题信息:/* Theme Name: My Custom Theme Template: parent-theme-folder-name Version: 1.0 */ - 创建
functions.php文件,用于添加自定义功能
第二部分:连接项目管理软件API
2.1 选择项目管理软件并获取API凭证
不同的项目管理软件提供不同的API接口。以Jira为例:
- 登录Jira管理员账户
- 进入"设置" > "系统" > "API令牌"
- 创建新令牌并妥善保存
- 获取你的Jira实例URL(如
https://yourcompany.atlassian.net)
2.2 创建API连接类
在WordPress中创建专门的类来处理API通信:
<?php
/**
* 项目管理软件API连接类
*/
class ProjectManagementAPI {
private $api_url;
private $api_token;
private $username;
public function __construct($url, $username, $token) {
$this->api_url = $url;
$this->username = $username;
$this->api_token = $token;
}
/**
* 发送API请求
*/
private function make_request($endpoint, $method = 'GET', $data = []) {
$url = $this->api_url . $endpoint;
$args = [
'method' => $method,
'headers' => [
'Authorization' => 'Basic ' . base64_encode($this->username . ':' . $this->api_token),
'Content-Type' => 'application/json',
'Accept' => 'application/json'
],
'timeout' => 30
];
if (!empty($data)) {
$args['body'] = json_encode($data);
}
$response = wp_remote_request($url, $args);
if (is_wp_error($response)) {
return [
'success' => false,
'error' => $response->get_error_message()
];
}
$body = wp_remote_retrieve_body($response);
$status_code = wp_remote_retrieve_response_code($response);
return [
'success' => $status_code >= 200 && $status_code < 300,
'status' => $status_code,
'data' => json_decode($body, true),
'raw_body' => $body
];
}
/**
* 获取项目列表
*/
public function get_projects() {
$endpoint = '/rest/api/3/project';
return $this->make_request($endpoint);
}
/**
* 获取特定项目的问题/任务
*/
public function get_project_issues($project_key, $max_results = 50) {
$endpoint = '/rest/api/3/search';
$jql = "project = " . $project_key;
$data = [
'jql' => $jql,
'maxResults' => $max_results,
'fields' => ['summary', 'status', 'assignee', 'created', 'updated']
];
return $this->make_request($endpoint, 'POST', $data);
}
/**
* 获取项目状态概览
*/
public function get_project_status($project_key) {
$issues_response = $this->get_project_issues($project_key, 100);
if (!$issues_response['success']) {
return $issues_response;
}
$issues = $issues_response['data']['issues'] ?? [];
$status_summary = [
'total' => 0,
'by_status' => [],
'by_assignee' => []
];
foreach ($issues as $issue) {
$status_summary['total']++;
// 按状态统计
$status_name = $issue['fields']['status']['name'] ?? '未知';
if (!isset($status_summary['by_status'][$status_name])) {
$status_summary['by_status'][$status_name] = 0;
}
$status_summary['by_status'][$status_name]++;
// 按负责人统计
$assignee_name = $issue['fields']['assignee']['displayName'] ?? '未分配';
if (!isset($status_summary['by_assignee'][$assignee_name])) {
$status_summary['by_assignee'][$assignee_name] = 0;
}
$status_summary['by_assignee'][$assignee_name]++;
}
return [
'success' => true,
'data' => $status_summary
];
}
}
?>
2.3 安全存储API凭证
永远不要在代码中硬编码API凭证。使用WordPress选项API安全存储:
<?php
/**
* 保存API设置
*/
function save_pm_api_settings() {
if (isset($_POST['pm_api_nonce']) && wp_verify_nonce($_POST['pm_api_nonce'], 'save_pm_api_settings')) {
$api_settings = [
'api_url' => sanitize_text_field($_POST['api_url']),
'username' => sanitize_text_field($_POST['username']),
'api_token' => $_POST['api_token'] // 注意:令牌需要特殊处理
];
// 加密存储令牌
if (!empty($api_settings['api_token'])) {
require_once(ABSPATH . 'wp-includes/class-phpass.php');
$hasher = new PasswordHash(8, true);
$api_settings['api_token'] = $hasher->HashPassword($api_settings['api_token']);
} else {
// 如果令牌为空,保留现有令牌
$existing_settings = get_option('pm_api_settings', []);
if (isset($existing_settings['api_token'])) {
$api_settings['api_token'] = $existing_settings['api_token'];
}
}
update_option('pm_api_settings', $api_settings);
wp_redirect(add_query_arg('settings-updated', 'true', wp_get_referer()));
exit;
}
}
add_action('admin_post_save_pm_api_settings', 'save_pm_api_settings');
/**
* 获取API设置
*/
function get_pm_api_settings() {
$settings = get_option('pm_api_settings', []);
// 解密令牌(在实际使用时)
if (isset($settings['api_token']) && !empty($settings['api_token'])) {
// 注意:实际解密逻辑需要根据加密方式实现
// 这里只是示例结构
}
return $settings;
}
?>
第三部分:在WordPress中展示项目状态
3.1 创建自定义文章类型和分类
为了更好地组织项目数据,我们创建自定义文章类型:
<?php
/**
* 注册项目自定义文章类型
*/
function register_project_post_type() {
$labels = [
'name' => '项目',
'singular_name' => '项目',
'menu_name' => '项目管理',
'add_new' => '添加新项目',
'add_new_item' => '添加新项目',
'edit_item' => '编辑项目',
'new_item' => '新项目',
'view_item' => '查看项目',
'search_items' => '搜索项目',
'not_found' => '未找到项目',
'not_found_in_trash' => '回收站中无项目'
];
$args = [
'labels' => $labels,
'public' => true,
'publicly_queryable' => true,
'show_ui' => true,
'show_in_menu' => true,
'query_var' => true,
'rewrite' => ['slug' => 'project'],
'capability_type' => 'post',
'has_archive' => true,
'hierarchical' => false,
'menu_position' => 20,
'menu_icon' => 'dashicons-portfolio',
'supports' => ['title', 'editor', 'thumbnail', 'excerpt'],
'show_in_rest' => true, // 启用Gutenberg编辑器和REST API
];
register_post_type('project', $args);
// 注册项目分类
register_taxonomy(
'project_category',
'project',
[
'label' => '项目分类',
'rewrite' => ['slug' => 'project-category'],
'hierarchical' => true,
'show_in_rest' => true
]
);
}
add_action('init', 'register_project_post_type');
?>
3.2 使用Advanced Custom Fields添加项目元数据
通过ACF添加项目与外部项目管理软件的关联字段:
<?php
/**
* 添加项目元字段
*/
function add_project_meta_fields() {
if (function_exists('acf_add_local_field_group')) {
acf_add_local_field_group([
'key' => 'group_project_meta',
'title' => '项目信息',
'fields' => [
[
'key' => 'field_project_external_id',
'label' => '外部项目ID',
'name' => 'project_external_id',
'type' => 'text',
'instructions' => '在项目管理软件中的项目标识符',
'required' => 0,
],
[
'key' => 'field_project_key',
'label' => '项目键',
'name' => 'project_key',
'type' => 'text',
'instructions' => '项目键(如Jira中的项目键)',
'required' => 0,
],
[
'key' => 'field_project_status',
'label' => '项目状态',
'name' => 'project_status',
'type' => 'select',
'choices' => [
'planning' => '规划中',
'active' => '进行中',
'on_hold' => '暂停',
'completed' => '已完成',
'cancelled' => '已取消'
],
'default_value' => 'planning',
],
[
'key' => 'field_project_start_date',
'label' => '开始日期',
'name' => 'project_start_date',
'type' => 'date_picker',
],
[
'key' => 'field_project_end_date',
'label' => '结束日期',
'name' => 'project_end_date',
'type' => 'date_picker',
],
[
'key' => 'field_project_progress',
'label' => '进度',
'name' => 'project_progress',
'type' => 'range',
'instructions' => '项目完成百分比',
'min' => 0,
'max' => 100,
'step' => 5,
'default_value' => 0,
]
],
'location' => [
[
[
'param' => 'post_type',
'operator' => '==',
'value' => 'project',
],
],
],
]);
}
}
add_action('acf/init', 'add_project_meta_fields');
?>
3.3 创建项目状态展示短代码
创建短代码以便在文章或页面中插入项目状态:
<?php
/**
* 项目状态展示短代码
*/
function project_status_shortcode($atts) {
// 解析短代码属性
$atts = shortcode_atts([
'project_id' => '', // WordPress项目ID
'project_key' => '', // 外部项目键
'show_tasks' => 'true', // 是否显示任务
'max_tasks' => 10, // 最大任务显示数量
'refresh' => 30, // 自动刷新时间(秒),0表示不自动刷新
], $atts);
// 获取项目信息
$project_data = [];
if (!empty($atts['project_id'])) {
$project_post = get_post($atts['project_id']);
if ($project_post && $project_post->post_type === 'project') {
$project_data['title'] = $project_post->post_title;
$project_data['description'] = $project_post->post_excerpt;
$project_data['progress'] = get_field('project_progress', $atts['project_id']);
$project_data['status'] = get_field('project_status', $atts['project_id']);
$project_key = get_field('project_key', $atts['project_id']);
}
}
// 如果直接提供了project_key,使用它
if (!empty($atts['project_key'])) {
$project_key = $atts['project_key'];
}
// 如果没有项目键,返回错误
if (empty($project_key)) {
return '<div class="project-status-error">未指定项目</div>';
}
// 获取API设置
$api_settings = get_pm_api_settings();
// 初始化API连接
$api = new ProjectManagementAPI(
$api_settings['api_url'] ?? '',
$api_settings['username'] ?? '',
$api_settings['api_token'] ?? ''
);
// 获取项目状态
$status_response = $api->get_project_status($project_key);
// 准备输出
ob_start();
// 添加自动刷新脚本
if ($atts['refresh'] > 0) {
?>
<script>
document.addEventListener('DOMContentLoaded', function() {
setTimeout(function() {
location.reload();
}, <?php echo $atts['refresh'] * 1000; ?>);
});
</script>
<?php
}
// 输出项目状态
?>
<div class="project-status-container" data-project-key="<?php echo esc_attr($project_key); ?>">
<div class="project-header">
<h3 class="project-title"><?php echo esc_html($project_data['title'] ?? '项目状态'); ?></h3>
<?php if (!empty($project_data['description'])): ?>
<p class="project-description"><?php echo esc_html($project_data['description']); ?></p>
<?php endif; ?>
</div>
<?php if ($status_response['success']):
$status_data = $status_response['data'];
?>
<div class="project-stats">
<div class="stat-card total-tasks">
<div class="stat-value"><?php echo $status_data['total']; ?></div>
<div class="stat-label">总任务数</div>
</div>
<?php foreach ($status_data['by_status'] as $status_name => $count): ?>
<div class="stat-card status-<?php echo sanitize_title($status_name); ?>">
<div class="stat-value"><?php echo $count; ?></div>
<div class="stat-label"><?php echo esc_html($status_name); ?></div>
</div>
<?php endforeach; ?>
</div>
<?php if ($atts['show_tasks'] === 'true'):
$tasks_response = $api->get_project_issues($project_key, $atts['max_tasks']);
if ($tasks_response['success'] && !empty($tasks_response['data']['issues'])):
?>
<div class="project-tasks">
<h4>最近任务</h4>
<table class="tasks-table">
<thead>
<tr>
<th>任务</th>
<th>状态</th>
<th>负责人</th>
<th>更新时间</th>
</tr>
</thead>
<tbody>
<?php foreach ($tasks_response['data']['issues'] as $issue):
$fields = $issue['fields'] ?? [];
?>
<tr>
<td>
<a href="#" class="task-link" data-task-key="<?php echo esc_attr($issue['key']); ?>">
<?php echo esc_html($fields['summary'] ?? '无标题'); ?>
</a>
</td>
<td>
<span class="task-status status-<?php echo sanitize_title($fields['status']['name'] ?? '未知'); ?>">
<?php echo esc_html($fields['status']['name'] ?? '未知'); ?>
</span>
</td>
displayName'] ?? '未分配'); ?></td>
<td><?php
$updated = $fields['updated'] ?? '';
if (!empty($updated)) {
echo date('Y-m-d H:i', strtotime($updated));
} else {
echo '未知';
}
?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; endif; ?>
<?php if (!empty($status_data['by_assignee'])): ?>
<div class="assignee-distribution">
<h4>任务分配情况</h4>
<div class="assignee-chart">
<?php foreach ($status_data['by_assignee'] as $assignee => $count):
$percentage = $status_data['total'] > 0 ? ($count / $status_data['total']) * 100 : 0;
?>
<div class="assignee-item">
<div class="assignee-name"><?php echo esc_html($assignee); ?></div>
<div class="assignee-bar">
<div class="assignee-bar-fill" style="width: <?php echo $percentage; ?>%"></div>
</div>
<div class="assignee-count"><?php echo $count; ?> 任务</div>
</div>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<?php else: ?>
<div class="project-status-error">
<p>无法获取项目状态数据</p>
<?php if (current_user_can('manage_options')): ?>
<p class="error-detail">错误: <?php echo esc_html($status_response['error'] ?? '未知错误'); ?></p>
<?php endif; ?>
</div>
<?php endif; ?>
<div class="project-status-footer">
<p class="update-time">最后更新: <?php echo current_time('Y-m-d H:i:s'); ?></p>
<?php if ($atts['refresh'] > 0): ?>
<p class="auto-refresh">自动刷新: 每 <?php echo $atts['refresh']; ?> 秒</p>
<?php endif; ?>
</div>
</div>
<style>
.project-status-container {
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 20px;
margin: 20px 0;
background: #fff;
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
}
.project-header {
margin-bottom: 25px;
padding-bottom: 15px;
border-bottom: 2px solid #f0f0f0;
}
.project-title {
margin: 0 0 10px 0;
color: #333;
}
.project-description {
margin: 0;
color: #666;
font-size: 14px;
}
.project-stats {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 15px;
margin-bottom: 25px;
}
.stat-card {
background: #f8f9fa;
border-radius: 6px;
padding: 15px;
text-align: center;
border-left: 4px solid #0073aa;
}
.stat-card.total-tasks {
border-left-color: #0073aa;
}
.stat-card.status-已完成 {
border-left-color: #46b450;
}
.stat-card.status-进行中 {
border-left-color: #00a0d2;
}
.stat-card.status-待处理 {
border-left-color: #ffb900;
}
.stat-value {
font-size: 24px;
font-weight: bold;
color: #333;
margin-bottom: 5px;
}
.stat-label {
font-size: 12px;
color: #666;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.project-tasks {
margin-bottom: 25px;
}
.project-tasks h4 {
margin-bottom: 15px;
color: #444;
}
.tasks-table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
}
.tasks-table th {
background: #f8f9fa;
padding: 12px 15px;
text-align: left;
font-weight: 600;
color: #555;
border-bottom: 2px solid #e0e0e0;
}
.tasks-table td {
padding: 12px 15px;
border-bottom: 1px solid #eee;
}
.tasks-table tr:hover {
background: #f9f9f9;
}
.task-link {
color: #0073aa;
text-decoration: none;
}
.task-link:hover {
text-decoration: underline;
}
.task-status {
display: inline-block;
padding: 3px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
}
.status-已完成 {
background: #d1f0d9;
color: #1e7c1e;
}
.status-进行中 {
background: #d1e8f0;
color: #0a6a8c;
}
.status-待处理 {
background: #fff0d1;
color: #b36b00;
}
.assignee-distribution {
margin-bottom: 20px;
}
.assignee-distribution h4 {
margin-bottom: 15px;
color: #444;
}
.assignee-item {
display: flex;
align-items: center;
margin-bottom: 10px;
}
.assignee-name {
width: 150px;
font-size: 14px;
color: #555;
}
.assignee-bar {
flex-grow: 1;
height: 20px;
background: #f0f0f0;
border-radius: 10px;
overflow: hidden;
margin: 0 15px;
}
.assignee-bar-fill {
height: 100%;
background: linear-gradient(90deg, #0073aa, #00a0d2);
border-radius: 10px;
transition: width 0.5s ease;
}
.assignee-count {
width: 80px;
text-align: right;
font-size: 14px;
color: #666;
}
.project-status-footer {
margin-top: 20px;
padding-top: 15px;
border-top: 1px solid #eee;
font-size: 12px;
color: #888;
display: flex;
justify-content: space-between;
}
.project-status-error {
padding: 20px;
background: #f8d7da;
border: 1px solid #f5c6cb;
border-radius: 4px;
color: #721c24;
}
.error-detail {
font-size: 12px;
margin-top: 10px;
color: #856404;
}
@media (max-width: 768px) {
.project-stats {
grid-template-columns: repeat(2, 1fr);
}
.assignee-item {
flex-direction: column;
align-items: flex-start;
}
.assignee-name {
width: 100%;
margin-bottom: 5px;
}
.assignee-bar {
width: 100%;
margin: 5px 0;
}
.assignee-count {
width: 100%;
text-align: left;
}
.tasks-table {
display: block;
overflow-x: auto;
}
}
</style>
<?php
return ob_get_clean();
}
add_shortcode('project_status', 'project_status_shortcode');
?>
## 第四部分:实现常用互联网小工具功能
### 4.1 实时数据仪表板小工具
创建一个WordPress小工具,显示多个项目的实时状态:
<?php
/**
- 项目状态仪表板小工具
*/
class ProjectDashboardWidget extends WP_Widget {
public function __construct() {
parent::__construct(
'project_dashboard_widget',
'项目状态仪表板',
['description' => '显示多个项目的实时状态']
);
}
public function widget($args, $instance) {
echo $args['before_widget'];
$title = apply_filters('widget_title', $instance['title']);
if (!empty($title)) {
echo $args['before_title'] . $title . $args['after_title'];
}
// 获取配置的项目键
$project_keys = !empty($instance['project_keys']) ?
explode(',', $instance['project_keys']) : [];
if (empty($project_keys)) {
echo '<p>请配置要显示的项目</p>';
echo $args['after_widget'];
return;
}
// 获取API设置
$api_settings = get_pm_api_settings();
$api = new ProjectManagementAPI(
$api_settings['api_url'] ?? '',
$api_settings['username'] ?? '',
$api_settings['api_token'] ?? ''
);
echo '<div class="project-dashboard-widget">';
foreach ($project_keys as $project_key) {
$project_key = trim($project_key);
if (empty($project_key)) continue;
$status_response = $api->get_project_status($project_key);
echo '<div class="dashboard-project-item">';
echo '<h4>' . esc_html($project_key) . '</h4>';
if ($status_response['success']) {
$status_data = $status_response['data'];
// 计算完成率
$completed = $status_data['by_status']['已完成'] ?? 0;
$completion_rate = $status_data['total'] > 0 ?
round(($completed / $status_data['total']) * 100) : 0;
echo '<div class="completion-bar">';
echo '<div class="completion-fill" style="width: ' . $completion_rate . '%"></div>';
echo '</div>';
echo '<div class="project-metrics">';
echo '<span class="metric">总任务: ' . $status_data['total'] . '</span>';
echo '<span class="metric">完成率: ' . $completion_rate . '%</span>';
echo '</div>';
} else {
echo '<p class="error">无法获取数据</p>';
}
echo '</div>';
}
echo '</div>';
// 添加样式
?>
<style>
.project-dashboard-widget {
font-size: 14px;
}
.dashboard-project-item {
margin-bottom: 15px;
padding: 10px;
background: #f8f9fa;
border-radius: 4px;
border-left: 3px solid #0073aa;
}
.dashboard-project-item h4 {
margin: 0 0 8px 0;
font-size: 14px;
color: #333;
}
.completion-bar {
height: 6px;
background: #e0e0e0;
border-radius: 3px;
overflow: hidden;
margin-bottom: 8px;
}
.completion-fill {
height: 100%;
background: linear-gradient(90deg, #0073aa, #00a0d2);
border-radius: 3px;
transition: width 0.5s ease;
}
.project-metrics {
display: flex;
justify-content: space-between;
font-size: 12px;
color: #666;
}
.metric {
display: inline-block;
}
.error {
color: #dc3232;
font-size: 12px;
margin: 0;
}
</style>
<?php
echo $args['after_widget'];
}
public function form($instance) {
$title = $instance['title'] ?? '项目状态';
$project_keys = $instance['project_keys'] ?? '';
?>
<p>
<label for="<?php echo $this->get_field_id('title'); ?>">标题:</label>
<input class="widefat" id="<?php echo $this->get_field_id('title'); ?>"
name="<?php echo $this->get_field_name('title'); ?>"
type="text" value="<?php echo esc_attr($title); ?>">
</p>
<p>
<label for="<?php echo $this->get_field_id('project_keys'); ?>">项目键(用逗号分隔):</label>
<input class="widefat" id="<?php echo $this->get_field_id('project_keys'); ?>"
name="<?php echo $this->get_field_name('project_keys'); ?>"
type="text" value="<?php echo esc_attr($project_keys); ?>">
<small>例如: PROJ1, PROJ2, PROJ3</small>
</p>
<?php
}
public function update($new_instance, $old_instance) {
$instance = [];
$instance['title'] = !empty($new_instance['title']) ?
sanitize_text_field($new_instance['title']) : '';
$instance['project_keys'] = !empty($new_instance['project_keys']) ?
sanitize_text_field($new_instance['project_keys']) : '';
return $instance;
}
}
// 注册小工具
function register_project_dashboard_widget() {
register_widget('ProjectDashboardWidget');
}
add_action('widgets_init', 'register_project_dashboard_widget');
?>
### 4.2 项目时间线小工具
创建一个可视化项目时间线的小工具:
<?php
/**
- 项目时间线小工具
*/
function project_timeline_shortcode($atts) {
$atts = shortcode_atts([
'project_id' => '',
'height' => '400px',
'show_milestones' => 'true',
], $atts);
if (empty($atts['project_id'])) {
return '<p>请指定项目ID</p>';
}
// 获取项目信息
$project_post = get_post($atts['project_id']);
if (!$project_post || $project_post->post_type !== 'project') {
return '<p>项目不存在</p>';
}
// 获取项目时间线数据
$timeline_data = get_project_timeline_data($atts['project_id']);
ob_start();
?>
<div class="project-timeline-container" style="height: <?php echo esc_attr($atts['height']); ?>">
<div class="timeline-header">
<h3><?php echo esc_html($project_post->post_title); ?> 时间线</h3>
</div>
<div class="timeline-wrapper">
<div class="timeline">
<?php foreach ($timeline_data as $item): ?>
<div class="timeline-item <?php echo esc_attr($item['type']); ?>"
data-date="<?php echo esc_attr($item['date']); ?>">
<div class="timeline-marker"></div>
<div class="timeline-content">
<div class="timeline-date"><?php echo esc_html($item['display_date']); ?></div>
<h4 class="timeline-title"><?php echo esc_html($item['title']); ?></h4>
<?php if (!empty($item['description'])): ?>
<p class="timeline-description"><?php echo esc_html($item['description']); ?></p>
<?php endif; ?>
<?php if (!empty($item['status'])): ?>
<span class="timeline-status status-<?php echo esc_attr($item['status']); ?>">
<?php echo esc_html($item['status']); ?>
</span>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
<style>
.project-timeline-container {
border: 1px solid #e0e0e0;
border-radius: 8px;
overflow: hidden;
background: #fff;
}
.timeline-header {
padding: 15px 20px;
background: #f8f9fa;
border-bottom: 1px solid #e0e0e0;
}
.timeline-header h3 {
margin: 0;
font-size: 18px;
color: #333;
}
.timeline-wrapper {
padding: 20px;
height: calc(100% - 60px);
overflow-y: auto;
}
.timeline {
position: relative;
padding-left: 30px;
}
.timeline::before {
content: '';
position: absolute;
left: 10px;
top: 0;
bottom: 0;
width: 2px;
background: #0073aa;
}
.timeline-item {
position: relative;
margin-bottom: 25px;
}
.timeline-marker {
position: absolute;
left: -25px;
top: 5px;
width: 12px;
height: 12px;
border-radius: 50%;
background: #fff;
border: 3px solid #0073aa;
z-index: 1;
}
.timeline-item.milestone .timeline-marker {
border-color:
