随着年龄增长,感觉记忆力明显不如从前。每当突然想到接下来的事情时,因为不觉得紧迫,总是想着先搁置一边,等有时间了再慢慢去做。可等到真正想要动手准备开干时,却往往要费很大劲儿才能回想起原本的计划。
平时我习惯使用苹果自带的提醒事项软件来记录生活中的待办事项,网站调整类的事项也一并记录在其中。但考虑到软件中已有生活和工作两大分类了,再增加一个专门针对建站事项的记录显得不太合群。
因此,就想着干脆在网站上新增一个专门用于记录与建站相关的待办事项页面。该页面只要保持基本功能即可:登陆状态下可在前端添加、处理、编辑和删除待办事项,而非登陆状态下则只需要具备查看功能。同时,再考虑增加分页功能以便更好地管理任务就行了。

现附上实现此功能的相关代码(仅适用于该主题,CSS 样式仅供参考):
1.新建 page-todo.php 文件,添加如下代码:
<?php /* Template Name: 待办事项 */ get_header(); ?> <article class="article todo-page" itemscope itemtype="https://schema.org/Article"> <?php if (have_posts()) : while (have_posts()) : the_post(); require_once("inc/article-header.php"); endwhile; endif; ?> <div id="todo-app"></div> </article> <script> (() => { const $config = { logged: <?= json_encode(is_user_logged_in()) ?>, nonce: '<?= wp_create_nonce('wp_rest') ?>', }; $h.store.config = $config; $h.tasks.todo = () => { $h.store.todo = new Vue({ el: '#todo-app', data() { return { todos: [], newTodo: '', loading: false, filter: 'all', editing: null, editingText: '', currentPage: 1, perPage: 10, }; }, computed: { filteredTodos() { let filtered; switch(this.filter) { case 'active': filtered = this.todos.filter(t => !t.completed); break; case 'completed': filtered = this.todos.filter(t => t.completed); break; default: filtered = this.todos; } return filtered; }, paginatedTodos() { const start = (this.currentPage - 1) * this.perPage; const end = start + this.perPage; return this.filteredTodos.slice(start, end); }, totalPages() { return Math.ceil(this.filteredTodos.length / this.perPage); }, remaining() { return this.todos.filter(t => !t.completed).length; } }, watch: { filter() { this.currentPage = 1; } }, created() { this.fetchTodos(); }, methods: { async fetchTodos() { this.loading = true; try { const response = await fetch(`${$base.rest}wp/v2/todos`, { headers: { 'X-WP-Nonce': $h.store.config.nonce } }); if (response.ok) { this.todos = await response.json(); } } catch (error) { console.error('Failed to fetch todos:', error); } finally { this.loading = false; } }, async addTodo() { if (!this.newTodo.trim()) return; try { const response = await fetch(`${$base.rest}wp/v2/todos`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': $h.store.config.nonce }, body: JSON.stringify({ title: this.newTodo, completed: false }) }); if (response.ok) { const todo = await response.json(); this.todos.unshift(todo); this.newTodo = ''; this.currentPage = 1; } } catch (error) { console.error('Failed to add todo:', error); } }, startEdit(todo) { this.editing = todo.id; this.editingText = todo.title; }, async toggleTodo(todo) { try { const response = await fetch(`${$base.rest}wp/v2/todos/${todo.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': $h.store.config.nonce }, body: JSON.stringify({ completed: !todo.completed }) }); if (response.ok) { const updatedTodo = await response.json(); todo.completed = updatedTodo.completed; todo.completed_at = updatedTodo.completed_at; this.$nextTick(() => { this.updateTimes(); }); } } catch (error) { console.error('Failed to toggle todo:', error); } }, async finishEdit(todo) { const text = this.editingText.trim(); if (text) { try { const response = await fetch(`${$base.rest}wp/v2/todos/${todo.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': $h.store.config.nonce }, body: JSON.stringify({ title: text }) }); if (response.ok) { todo.title = text; } } catch (error) { console.error('Failed to update todo:', error); } } else { await this.removeTodo(todo); } this.editing = null; }, async removeTodo(todo) { try { const response = await fetch(`${$base.rest}wp/v2/todos/${todo.id}`, { method: 'DELETE', headers: { 'X-WP-Nonce': $h.store.config.nonce } }); if (response.ok) { const index = this.todos.indexOf(todo); this.todos.splice(index, 1); } } catch (error) { console.error('Failed to remove todo:', error); } }, updateTimes() { if (window.Lately) { window.Lately.init({ target: '.todo-time time' }); } }, changePage(page, event) { if (event) { event.preventDefault(); } if (page >= 1 && page <= this.totalPages && page !== this.currentPage) { this.currentPage = page; $h.scrollTo(0); } } }, mounted() { this.$nextTick(() => { this.updateTimes(); }); }, updated() { this.$nextTick(() => { this.updateTimes(); }); }, template: ` <div class="todo-container"> <div v-if="$h.store.config.logged" class="todo-header"> <div class="input-group"> <input class="form-input" type="text" id="new-todo" name="new-todo" v-model="newTodo" @keyup.enter="addTodo" placeholder=""> <button class="btn btn-primary input-group-btn" @click="addTodo"> 添加 </button> </div> </div> <div class="todo-main" v-if="todos.length"> <div class="todo-filters"> <button class="btn" :class="{'btn-primary': filter === 'all'}" @click="filter = 'all'">全部</button> <button class="btn" :class="{'btn-primary': filter === 'active'}" @click="filter = 'active'">进行中</button> <button class="btn" :class="{'btn-primary': filter === 'completed'}" @click="filter = 'completed'">已完成</button> </div> <div class="divider"></div> <ul class="todo-list"> <li v-for="todo in paginatedTodos" :key="todo.id" :class="{'completed': todo.completed}"> <div class="todo-item"> <div class="todo-content"> <label class="form-checkbox"> <input type="checkbox" :id="'todo-checkbox-' + todo.id" :name="'todo-checkbox-' + todo.id" :checked="todo.completed" @change="toggleTodo(todo)" :disabled="!$h.store.config.logged"> <i class="form-icon"></i> <div class="todo-text">{{ todo.title }}</div> </label> <div class="todo-time text-gray pt-2"> <small>创建于: <time :datetime="todo.created_at">{{ todo.created_at }}</time></small> <small v-if="todo.completed">完成于: <time :datetime="todo.completed_at">{{ todo.completed_at }}</time></small> </div> </div> <div class="todo-actions"> <button v-if="$h.store.config.logged" class="btn btn-link" @click="startEdit(todo)"> <i class="czs-write-l"></i> </button> <button v-if="$h.store.config.logged" class="btn btn-link" @click="removeTodo(todo)"> <i class="czs-trash-l"></i> </button> </div> </div> <input v-if="editing === todo.id" class="form-input" type="text" :id="'edit-todo-' + todo.id" :name="'edit-todo-' + todo.id" v-model="editingText" @blur="finishEdit(todo)" @keyup.enter="finishEdit(todo)" @keyup.esc="editing = null" v-focus> </li> </ul> <nav v-if="totalPages > 1" class="flex-center"> <ul class="pagination"> <li class="page-item" :class="{ disabled: currentPage === 1 }"> <a class="page-link" href="javascript:void(0);" @click="changePage(currentPage - 1, $event)"> <i class="czs-angle-left-l"></i> </a> </li> <li v-for="page in totalPages" :key="page" class="page-item" :class="{ active: page === currentPage }"> <a class="page-link" href="javascript:void(0);" @click="changePage(page, $event)"> {{ page }} </a> </li> <li class="page-item" :class="{ disabled: currentPage === totalPages }"> <a class="page-link" href="javascript:void(0);" @click="changePage(currentPage + 1, $event)"> <i class="czs-angle-right-l"></i> </a> </li> </ul> </nav> </div> <div v-else-if="!loading" class="empty-state"> 还没有待办事项 </div> <div v-if="loading" class="loading"></div> </div> ` }); }; })(); </script> <?php get_footer(); ?>
2.在 functions.php 文件中添加如下代码:
function create_todos_table() { global $wpdb; $table_name = $wpdb->prefix . 'todos'; $charset_collate = $wpdb->get_charset_collate(); $sql = "CREATE TABLE IF NOT EXISTS $table_name ( id bigint(20) NOT NULL AUTO_INCREMENT, user_id bigint(20) NOT NULL, title text NOT NULL, completed tinyint(1) DEFAULT 0, created_at datetime DEFAULT CURRENT_TIMESTAMP, completed_at datetime DEFAULT NULL, PRIMARY KEY (id), KEY user_id (user_id) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql); } function check_todos_table() { global $wpdb; $table_name = $wpdb->prefix . 'todos'; if($wpdb->get_var("SHOW TABLES LIKE '$table_name'") != $table_name) { create_todos_table(); } } add_action('after_switch_theme', 'create_todos_table'); add_action('init', 'check_todos_table'); function register_todo_rest_routes() { register_rest_route('wp/v2', '/todos', array( array( 'methods' => 'GET', 'callback' => 'get_todos', 'permission_callback' => '__return_true', ), array( 'methods' => 'POST', 'callback' => 'create_todo', 'permission_callback' => 'is_user_logged_in', ) )); register_rest_route('wp/v2', '/todos/(?P<id>\d+)', array( array( 'methods' => 'PUT', 'callback' => 'update_todo', 'permission_callback' => 'is_user_logged_in', ), array( 'methods' => 'DELETE', 'callback' => 'delete_todo', 'permission_callback' => 'is_user_logged_in', ) )); } add_action('rest_api_init', 'register_todo_rest_routes'); function get_todos() { global $wpdb; $todos = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}todos ORDER BY id DESC" ); return array_map(function($todo) { return array( 'id' => (int)$todo->id, 'title' => $todo->title, 'completed' => (bool)$todo->completed, 'created_at' => $todo->created_at, 'completed_at' => $todo->completed_at ); }, $todos ?: array()); } function create_todo($request) { global $wpdb; $user_id = get_current_user_id(); $title = sanitize_text_field($request['title']); $created_at = current_time('mysql'); $wpdb->insert( $wpdb->prefix . 'todos', array( 'user_id' => $user_id, 'title' => $title, 'completed' => false, 'created_at' => $created_at ), array('%d', '%s', '%d', '%s') ); return array( 'id' => $wpdb->insert_id, 'title' => $title, 'completed' => false, 'created_at' => $created_at, 'completed_at' => null ); } function update_todo($request) { global $wpdb; $user_id = get_current_user_id(); $todo_id = (int)$request['id']; $current_todo = $wpdb->get_row($wpdb->prepare( "SELECT * FROM {$wpdb->prefix}todos WHERE id = %d AND user_id = %d", $todo_id, $user_id )); if (!$current_todo) { return new WP_Error('not_found', 'Todo not found', array('status' => 404)); } $data = array(); $formats = array(); if (isset($request['title'])) { $data['title'] = sanitize_text_field($request['title']); $formats[] = '%s'; } if (isset($request['completed'])) { $data['completed'] = (bool)$request['completed']; $formats[] = '%d'; if ($data['completed']) { $data['completed_at'] = current_time('mysql'); $formats[] = '%s'; } else { $data['completed_at'] = null; $formats[] = null; } } $wpdb->update( $wpdb->prefix . 'todos', $data, array('id' => $todo_id, 'user_id' => $user_id), $formats, array('%d', '%d') ); return array( 'id' => $todo_id, 'title' => $data['title'] ?? $current_todo->title, 'completed' => $data['completed'] ?? (bool)$current_todo->completed, 'created_at' => $current_todo->created_at, 'completed_at' => $data['completed_at'] ?? $current_todo->completed_at ); } function delete_todo($request) { global $wpdb; $user_id = get_current_user_id(); $todo_id = (int)$request['id']; $wpdb->delete( $wpdb->prefix . 'todos', array('id' => $todo_id, 'user_id' => $user_id), array('%d', '%d') ); return array('deleted' => true); }
3.在 style.css 文件中添加如下样式代码:
.toggle-aside i{display:inline-block;transition:transform 0.3s ease} .toggle-aside i.is-active {transform:rotate(180deg)} .tab-item.ml-auto {margin-left:auto} .tab .tab-item:not(:last-child){margin-right:.9rem} .site-icon{width:1rem;height:1rem;vertical-align:middle;margin:0 .2rem .2rem 0;border-radius:.2rem} .todo-filters{margin:1rem 0;display:flex;align-items:center;gap:.5rem} .todo-list{list-style:none;margin:0;padding-bottom:1rem} .todo-list li{border-bottom:1px solid rgba(188,195,206,.2);padding:.5rem 0} .todo-item{display:flex;align-items:flex-start;justify-content:space-between;padding:.5rem 0} .todo-actions{display:flex;gap:.5rem} .empty-state{text-align:center;padding:2rem;color:#bcc3ce} .todo-meta{display:flex;align-items:center;gap:1rem} .todo-author{font-size:.8rem} .todo-time {display:flex;justify-content:flex-end;align-items:center;gap:1rem;margin-left:auto;margin-right:.5rem} .todo-content{flex:1;width:100%} .todo-text{border-radius:.2rem;padding:1rem;background:#f7f8fa;margin-left:.5rem}
4.最后停用主题,再重新启用主题来自动完成数据库表的创建即可。