refactor(ticket): move the config tab into the SPA ticket center

users.php → 工单管理 的「配置」Tab 只管理部门 / 状态 / 预设回复三块
DB 字典,与 SPA「工单中心」已有的 8 个标量配置分裂成两处;且同一套
CRUD 在 admin_post 与 TicketConfigRest 里各写了一遍,REST 侧此前
没有任何调用方。

- 删除原生 config tab:UsersMenusTicketsConfig.php 整份移除,
  UsersMenusTicketsCenter 由 512 行降到 251 行(六个写处理方法、
  分发分支、十条提示码与 ticket_delete_button 一并清理)
- SPA /users/ticket 改为 customPage:内嵌 SchemaPage 承载原有 8 字段,
  三块字典改为 n-data-table + n-modal + n-dialog,取色器/管理员多选/
  拖拽排序替代原生手填表单
- TicketConfigRest 成为三块字典的唯一实现,新增 api/modules/tickets.js
  封装 aksrt/v1/admin/tickets*
- 双向深链:原生页跳 SPA 配置页,SPA 页跳回原生工单列表

契约测试同步更新,并新增 ticket-spa-config-contract 守护接线。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
kate522andClaude Fable 5 committed 2026-10-26 19:48:58 +08:00
1 parent d32bdc0581
commit e7ca0ee5df
19 files changed
+1074 -509

No files matched your search

+17 -278
View File
@@ -2,12 +2,11 @@
/**
* 工单管理后台页(users.php → aksrt_tickets)。
*
* 两个 Tab:
* 1. tickets — 工单列表:筛选 + 排序表头 + 行内展开详情(沟通记录 / 回复 / 备注 / 状态迁移)
* 2. config — 配置:部门 / 状态 / 预设回复
* 单页承载工单列表:筛选 + 排序表头 + 行内展开详情(沟通记录 / 回复 / 备注 / 状态迁移)。
* 部门、状态与预设回复的配置已迁至主题后台 SPA「工单中心」(/users/ticket),
* 由 TicketConfigRest 提供 REST 实现。
*
* 写操作走 admin_post(非 REST),与本站其它原生后台页一致;前端 JS 见
* assets/js/admin-ticket-center.js。
* 写操作走 admin_post(非 REST),与本站其它原生后台页一致。
*
* @package AKSRT
*/
@@ -19,7 +18,6 @@ defined('ABSPATH') || exit;
trait UsersMenusTicketsCenter {
use UsersMenusTicketsList;
use UsersMenusTicketsConfig;
private const TICKET_PAGE_SLUG = 'aksrt_tickets';
private const TICKET_PER_PAGE = 20;
@@ -38,30 +36,18 @@ trait UsersMenusTicketsCenter {
if (!current_user_can(self::CAPABILITY)) {
wp_die(esc_html__('权限不足', 'aksrt'));
}
$tab = isset($_GET['tab']) ? sanitize_key((string) wp_unslash($_GET['tab'])) : 'tickets';
$tab = $tab === 'config' ? 'config' : 'tickets';
echo '<div class="wrap">';
echo '<h1>' . esc_html__('工单管理', 'aksrt') . '</h1>';
printf(
'<p><a class="button" href="%s">%s</a></p>',
esc_url(admin_url('admin.php?page=aksrt-settings#/users/ticket')),
esc_html__('工单配置(部门 / 状态 / 预设回复)', 'aksrt')
);
echo '<style>' . $this->ticket_page_css() . '</style>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
$this->render_ticket_notices();
echo '<h2 class="nav-tab-wrapper">';
foreach (['tickets' => __('工单列表', 'aksrt'), 'config' => __('配置', 'aksrt')] as $key => $label) {
printf(
'<a href="%s" class="nav-tab %s">%s</a>',
esc_url(admin_url('users.php?page=' . self::TICKET_PAGE_SLUG . '&tab=' . $key)),
$tab === $key ? 'nav-tab-active' : '',
esc_html($label)
);
}
echo '</h2>';
if ($tab === 'config') {
$this->render_ticket_config_tab();
} else {
$this->render_ticket_list_tab();
}
$this->render_ticket_list_tab();
echo '</div>';
}
@@ -76,16 +62,6 @@ trait UsersMenusTicketsCenter {
'failed' => __('工单状态更新失败:当前状态不允许该操作。', 'aksrt'),
'replied' => __('回复已提交。', 'aksrt'),
'reply_failed' => __('回复提交失败。', 'aksrt'),
'type_saved' => __('部门已保存。', 'aksrt'),
'type_deleted' => __('部门已删除。', 'aksrt'),
'type_in_use' => __('该部门下还有工单,无法删除。', 'aksrt'),
'status_saved' => __('状态已保存。', 'aksrt'),
'status_deleted' => __('状态已删除。', 'aksrt'),
'status_default' => __('默认状态不可修改或删除。', 'aksrt'),
'status_in_use' => __('该状态下还有工单,无法删除。', 'aksrt'),
'prereply_saved' => __('预设回复已保存。', 'aksrt'),
'prereply_deleted' => __('预设回复已删除。', 'aksrt'),
'prereply_order_failed' => __('预设回复排序失败:需要提交全部条目。', 'aksrt'),
'note_saved' => __('备注已保存。', 'aksrt'),
'note_deleted' => __('备注已删除。', 'aksrt'),
'created' => __('工单已创建。', 'aksrt'),
@@ -100,6 +76,8 @@ trait UsersMenusTicketsCenter {
/**
* 写操作:统一入口,按 $action 分发。nonce 以 action + id 为单位,
* 与 PayOrders 的行内处理表单同模式。
*
* 部门 / 状态 / 预设回复的写操作已迁至 SPA「工单中心」(TicketConfigRest)。
*/
public function handle_ticket_update(): void {
if (!current_user_can(self::CAPABILITY)) {
@@ -117,24 +95,6 @@ trait UsersMenusTicketsCenter {
case 'reply':
$code = $this->ticket_action_reply($id);
break;
case 'type':
$code = $this->ticket_action_type();
break;
case 'status_def':
$code = $this->ticket_action_status_def();
break;
case 'prereply':
$code = $this->ticket_action_prereply();
break;
case 'prereply_delete':
$code = $this->ticket_action_prereply_delete();
break;
case 'type_delete':
$code = $this->ticket_action_type_delete();
break;
case 'status_delete':
$code = $this->ticket_action_status_delete();
break;
case 'note':
$code = $this->ticket_action_note($id);
break;
@@ -143,11 +103,10 @@ trait UsersMenusTicketsCenter {
break;
}
$args = ['page' => self::TICKET_PAGE_SLUG, 'aksrt_ticket_msg' => $code];
if (isset($_POST['ticket_tab'])) {
$args['tab'] = sanitize_key((string) wp_unslash($_POST['ticket_tab']));
}
wp_safe_redirect(add_query_arg($args, admin_url('users.php')));
wp_safe_redirect(add_query_arg(
['page' => self::TICKET_PAGE_SLUG, 'aksrt_ticket_msg' => $code],
admin_url('users.php')
));
exit;
}
@@ -209,204 +168,6 @@ trait UsersMenusTicketsCenter {
return 'replied';
}
/** @return string 提示码 */
private function ticket_action_type(): string {
if (!class_exists('\AKSRT\Ticket\TicketTypeService')) {
return 'failed';
}
$id = isset($_POST['type_id']) ? absint($_POST['type_id']) : 0;
$name = isset($_POST['name']) ? sanitize_text_field((string) wp_unslash($_POST['name'])) : '';
$name = trim($name);
$description = isset($_POST['description']) ? sanitize_text_field((string) wp_unslash($_POST['description'])) : '';
$admin_ids = isset($_POST['admin_ids']) ? array_map('absint', (array) $_POST['admin_ids']) : [];
if ($name === '' || \AKSRT\Ticket\TicketTypeService::name_taken($name, $id)) {
return 'failed';
}
$payload = [
'name' => $name,
'description' => $description,
'admin_ids' => \AKSRT\Ticket\TicketTypeService::sanitize_admin_ids($admin_ids),
];
if ($id > 0) {
if (!\AKSRT\Ticket\TicketTypeService::exists($id)) {
return 'failed';
}
global $wpdb;
$table = \AKSRT\Core\DBMigrator::table('ticket_type');
$saved = $wpdb->update($table, [
'name' => $payload['name'],
'description' => $payload['description'],
'admin_ids' => wp_json_encode($payload['admin_ids']),
'updated_at' => current_time('mysql'),
], ['id' => $id], ['%s', '%s', '%s', '%s'], ['%d']);
if ($saved === false) {
return 'failed';
}
\AKSRT\Ticket\TicketTypeService::flush();
return 'type_saved';
}
global $wpdb;
$table = \AKSRT\Core\DBMigrator::table('ticket_type');
if ($table === '') {
return 'failed';
}
$max = (int) $wpdb->get_var("SELECT MAX(sort_order) FROM {$table}"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
$inserted = $wpdb->insert($table, [
'name' => $payload['name'],
'description' => $payload['description'],
'admin_ids' => wp_json_encode($payload['admin_ids']),
'sort_order' => $max + 1,
'created_at' => current_time('mysql'),
], ['%s', '%s', '%s', '%d', '%s']);
if ($inserted !== 1) {
return 'failed';
}
\AKSRT\Ticket\TicketTypeService::flush();
return 'type_saved';
}
/** @return string 提示码 */
private function ticket_action_status_def(): string {
if (!class_exists('\AKSRT\Ticket\TicketStatusService')) {
return 'failed';
}
global $wpdb;
$table = \AKSRT\Core\DBMigrator::table('ticket_status');
if ($table === '') {
return 'failed';
}
$id = isset($_POST['status_id']) ? absint($_POST['status_id']) : 0;
$name = isset($_POST['name']) ? sanitize_text_field((string) wp_unslash($_POST['name'])) : '';
$name = trim($name);
$color = \AKSRT\Ticket\TicketStatusService::sanitize_color(
(string) wp_unslash($_POST['color'] ?? '')
);
$is_closed = isset($_POST['is_closed']) ? 1 : 0;
if ($name === '') {
return 'failed';
}
if ($id > 0) {
$row = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$table} WHERE id = %d", // phpcs:ignore WordPress.DB.PreparedSQL
$id
), ARRAY_A);
if (!is_array($row)) {
return 'failed';
}
if (!empty($row['is_default'])) {
return 'status_default';
}
$saved = $wpdb->update($table, [
'name' => $name,
'color' => $color,
'is_closed' => $is_closed,
'updated_at' => current_time('mysql'),
], ['id' => $id], ['%s', '%s', '%d', '%s'], ['%d']);
if ($saved === false) {
return 'failed';
}
\AKSRT\Ticket\TicketStatusService::flush();
return 'status_saved';
}
$max = (int) $wpdb->get_var("SELECT MAX(sort_order) FROM {$table}"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
$inserted = $wpdb->insert($table, [
'status_key' => \AKSRT\Ticket\TicketStatusService::make_key($name),
'name' => $name,
'color' => $color,
'is_closed' => $is_closed,
'sort_order' => $max + 1,
'created_at' => current_time('mysql'),
], ['%s', '%s', '%s', '%d', '%d', '%s']);
if ($inserted !== 1) {
return 'failed';
}
\AKSRT\Ticket\TicketStatusService::flush();
return 'status_saved';
}
/** @return string 提示码 */
private function ticket_action_prereply(): string {
if (!class_exists('\AKSRT\Ticket\TicketPrereplyService')) {
return 'failed';
}
$id = isset($_POST['prereply_id']) ? absint($_POST['prereply_id']) : 0;
$content = isset($_POST['content']) ? sanitize_textarea_field((string) wp_unslash($_POST['content'])) : '';
$content = trim($content);
if ($content === '') {
return 'failed';
}
if ($id > 0) {
if (\AKSRT\Ticket\TicketPrereplyService::find($id) === null) {
return 'failed';
}
return \AKSRT\Ticket\TicketPrereplyService::update($id, $content) ? 'prereply_saved' : 'failed';
}
return \AKSRT\Ticket\TicketPrereplyService::create($content) > 0 ? 'prereply_saved' : 'failed';
}
/** @return string 提示码 */
private function ticket_action_prereply_delete(): string {
$id = isset($_POST['prereply_id']) ? absint($_POST['prereply_id']) : 0;
if ($id <= 0 || !class_exists('\AKSRT\Ticket\TicketPrereplyService')) {
return 'failed';
}
return \AKSRT\Ticket\TicketPrereplyService::delete($id) ? 'prereply_deleted' : 'failed';
}
/** @return string 提示码 */
private function ticket_action_type_delete(): string {
$id = isset($_POST['type_id']) ? absint($_POST['type_id']) : 0;
if ($id <= 0 || !class_exists('\AKSRT\Ticket\TicketTypeService')) {
return 'failed';
}
if (\AKSRT\Ticket\TicketRepository::get_instance()->count_by_type($id) > 0) {
return 'type_in_use';
}
global $wpdb;
$table = \AKSRT\Core\DBMigrator::table('ticket_type');
if ($table === '') {
return 'failed';
}
$wpdb->delete($table, ['id' => $id], ['%d']);
\AKSRT\Ticket\TicketTypeService::flush();
return 'type_deleted';
}
/** @return string 提示码 */
private function ticket_action_status_delete(): string {
$id = isset($_POST['status_id']) ? absint($_POST['status_id']) : 0;
if ($id <= 0 || !class_exists('\AKSRT\Ticket\TicketStatusService')) {
return 'failed';
}
global $wpdb;
$table = \AKSRT\Core\DBMigrator::table('ticket_status');
if ($table === '') {
return 'failed';
}
$row = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$table} WHERE id = %d", // phpcs:ignore WordPress.DB.PreparedSQL
$id
), ARRAY_A);
if (!is_array($row)) {
return 'failed';
}
if (!empty($row['is_default'])) {
return 'status_default';
}
$key = sanitize_key((string) $row['status_key']);
if (\AKSRT\Ticket\TicketRepository::get_instance()->count_by_status($key) > 0) {
return 'status_in_use';
}
$wpdb->delete($table, ['id' => $id], ['%d']);
\AKSRT\Ticket\TicketStatusService::flush();
return 'status_deleted';
}
/** @return string 提示码 */
private function ticket_action_note(int $id): string { if ($id <= 0 || !class_exists('\AKSRT\Ticket\TicketNoteService')) {
return 'failed';
@@ -471,29 +232,7 @@ trait UsersMenusTicketsCenter {
);
}
/**
* 行内删除按钮:独立小表单 + 二次确认,nonce 以 action + id 为单位。
* 删除类操作不占用列表主表单,避免误删。
*/
private function ticket_delete_button(string $action, string $field, int $id, string $label): string {
return sprintf(
'<form method="post" action="%s" style="display:inline;" onsubmit="return confirm(\'%s\');">'
. '<input type="hidden" name="action" value="aksrt_ticket_update" />'
. '<input type="hidden" name="ticket_action" value="%s" />'
. '<input type="hidden" name="ticket_tab" value="config" />'
. '<input type="hidden" name="%s" value="%d" />%s'
. '<button type="submit" class="button-link-delete">%s</button></form>',
esc_url(admin_url('admin-post.php')),
esc_js(__('确认删除?', 'aksrt')),
esc_attr($action),
esc_attr($field),
$id,
wp_nonce_field('aksrt_ticket_' . $action . '_' . $id, '_wpnonce', true, false),
esc_html($label)
);
}
/** 工单页共享样式(徽章取状态颜色,两个 Tab 都用)。 */
/** 工单页共享样式(徽章取状态颜色)。 */
private function ticket_page_css(): string {
return '.aksrt-ticket-badge{display:inline-block;padding:2px 10px;border-radius:999px;'
. 'background:color-mix(in srgb,var(--aksrt-ticket-color,#6b7280) 14%,transparent);'
@@ -1,165 +0,0 @@
<?php
/**
* 工单管理后台页 — 配置 Tab 渲染(部门 / 状态 / 预设回复)。
*
* @package AKSRT
*/
namespace AKSRT\Admin\Native;
defined('ABSPATH') || exit;
trait UsersMenusTicketsConfig {
private function render_ticket_config_tab(): void {
$this->render_ticket_types_panel();
$this->render_ticket_statuses_panel();
$this->render_ticket_prereplies_panel();
}
/** 部门:表格 + 行内编辑 + 新建表单。 */
private function render_ticket_types_panel(): void {
$types = \AKSRT\Ticket\TicketTypeService::all();
$edit_id = isset($_GET['type_id']) ? absint($_GET['type_id']) : 0;
$editing = null;
foreach ($types as $row) {
if ((int) $row['id'] === $edit_id) {
$editing = $row;
break;
}
}
echo '<h3>' . esc_html__('工单部门', 'aksrt') . '</h3>';
echo '<table class="widefat fixed striped posts" style="max-width:900px;">';
echo '<thead><tr><th width="8%">ID</th><th width="24%">' . esc_html__('名称', 'aksrt') . '</th>'
. '<th width="38%">' . esc_html__('说明', 'aksrt') . '</th>'
. '<th width="20%">' . esc_html__('受理管理员', 'aksrt') . '</th><th width="10%"></th></tr></thead><tbody>';
foreach ($types as $row) {
$edit_url = add_query_arg(['type_id' => (int) $row['id']], $this->ticket_page_url(['tab' => 'config']));
$cells = sprintf(
'<tr><td>%d</td><td>%s</td><td>%s</td><td>%s</td><td>',
(int) $row['id'],
esc_html((string) $row['name']),
esc_html((string) $row['description'] !== '' ? (string) $row['description'] : '—'),
esc_html($row['admin_ids'] === [] ? '—' : implode(', ', $row['admin_ids']))
);
$cells .= '<a href="' . esc_url($edit_url) . '">' . esc_html__('编辑', 'aksrt') . '</a> ';
$cells .= $this->ticket_delete_button('type_delete', 'type_id', (int) $row['id'], __('删除', 'aksrt'));
$cells .= '</td></tr>';
echo $cells; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
echo '</tbody></table>';
echo '<h4>' . esc_html($editing === null ? __('新增部门', 'aksrt') : __('编辑部门', 'aksrt')) . '</h4>';
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '">';
echo '<input type="hidden" name="action" value="aksrt_ticket_update" />';
echo '<input type="hidden" name="ticket_action" value="type" />';
echo '<input type="hidden" name="ticket_tab" value="config" />';
echo '<input type="hidden" name="type_id" value="' . esc_attr((string) ($editing['id'] ?? 0)) . '" />';
echo wp_nonce_field('aksrt_ticket_type_' . (int) ($editing['id'] ?? 0), '_wpnonce', true, false);
echo '<p><input type="text" name="name" value="' . esc_attr((string) ($editing['name'] ?? '')) . '" placeholder="' . esc_attr__('部门名称', 'aksrt') . '" /></p>';
echo '<p><input type="text" name="description" class="large-text" value="' . esc_attr((string) ($editing['description'] ?? '')) . '" placeholder="' . esc_attr__('部门说明(可选)', 'aksrt') . '" /></p>';
echo '<p><input type="text" name="admin_ids" class="large-text" value="' . esc_attr(implode(',', (array) ($editing['admin_ids'] ?? []))) . '" placeholder="' . esc_attr__('受理管理员用户 ID,逗号分隔(可选)', 'aksrt') . '" /></p>';
submit_button(__('保存部门', 'aksrt'), 'primary', '', false);
echo '</form>';
}
/** 自定义状态:默认状态拒绝改删。 */
private function render_ticket_statuses_panel(): void {
$statuses = \AKSRT\Ticket\TicketStatusService::all();
$edit_id = isset($_GET['status_id']) ? absint($_GET['status_id']) : 0;
$editing = null;
foreach ($statuses as $row) {
if ((int) $row['id'] === $edit_id) {
$editing = $row;
break;
}
}
echo '<h3>' . esc_html__('工单状态', 'aksrt') . '</h3>';
echo '<p class="description">' . esc_html__('默认状态不可修改或删除;删除非默认状态前需先清空该状态下的工单。', 'aksrt') . '</p>';
echo '<table class="widefat fixed striped posts" style="max-width:760px;">';
echo '<thead><tr><th width="8%">ID</th><th width="22%">' . esc_html__('名称', 'aksrt') . '</th>'
. '<th width="18%">' . esc_html__('颜色', 'aksrt') . '</th>'
. '<th width="16%">' . esc_html__('完结', 'aksrt') . '</th>'
. '<th width="16%">' . esc_html__('默认', 'aksrt') . '</th><th width="20%"></th></tr></thead><tbody>';
foreach ($statuses as $row) {
$status_cells = sprintf(
'<tr><td>%d</td><td>%s</td>'
. '<td><span class="aksrt-ticket-badge" style="--aksrt-ticket-color:%s">%s</span></td>'
. '<td>%s</td><td>%s</td><td>',
(int) $row['id'],
esc_html((string) $row['name']),
esc_attr((string) $row['color']),
esc_html((string) $row['color']),
esc_html(!empty($row['is_closed']) ? __('是', 'aksrt') : __('否', 'aksrt')),
esc_html(!empty($row['is_default']) ? __('是', 'aksrt') : __('否', 'aksrt'))
);
if (empty($row['is_default'])) {
$status_cells .= '<a href="' . esc_url(add_query_arg(['status_id' => (int) $row['id']], $this->ticket_page_url(['tab' => 'config']))) . '">' . esc_html__('编辑', 'aksrt') . '</a> ';
$status_cells .= $this->ticket_delete_button('status_delete', 'status_id', (int) $row['id'], __('删除', 'aksrt'));
} else {
$status_cells .= '—';
}
$status_cells .= '</td></tr>';
echo $status_cells; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
echo '</tbody></table>';
echo '<h4>' . esc_html($editing === null ? __('新增状态', 'aksrt') : __('编辑状态', 'aksrt')) . '</h4>';
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '">';
echo '<input type="hidden" name="action" value="aksrt_ticket_update" />';
echo '<input type="hidden" name="ticket_action" value="status_def" />';
echo '<input type="hidden" name="ticket_tab" value="config" />';
echo '<input type="hidden" name="status_id" value="' . esc_attr((string) ($editing['id'] ?? 0)) . '" />';
echo wp_nonce_field('aksrt_ticket_status_def_' . (int) ($editing['id'] ?? 0), '_wpnonce', true, false);
echo '<p><input type="text" name="name" value="' . esc_attr((string) ($editing['name'] ?? '')) . '" placeholder="' . esc_attr__('状态名称', 'aksrt') . '" /> ';
echo '<input type="text" name="color" value="' . esc_attr((string) ($editing['color'] ?? '#6b7280')) . '" placeholder="#6b7280" style="width:120px;" /> ';
echo '<label><input type="checkbox" name="is_closed" value="1"' . checked(!empty($editing['is_closed']), true, false) . ' /> ' . esc_html__('完结状态', 'aksrt') . '</label> ';
submit_button(__('保存状态', 'aksrt'), 'primary', '', false);
echo '</p></form>';
}
/** 预设回复:表格 + 行内编辑 + 新建表单。 */
private function render_ticket_prereplies_panel(): void {
$rows = \AKSRT\Ticket\TicketPrereplyService::all();
$edit_id = isset($_GET['prereply_id']) ? absint($_GET['prereply_id']) : 0;
$editing = null;
foreach ($rows as $row) {
if ((int) $row['id'] === $edit_id) {
$editing = $row;
break;
}
}
echo '<h3>' . esc_html__('预设回复', 'aksrt') . '</h3>';
echo '<table class="widefat fixed striped posts" style="max-width:900px;">';
echo '<thead><tr><th width="8%">ID</th><th width="72%">' . esc_html__('内容', 'aksrt') . '</th><th width="20%"></th></tr></thead><tbody>';
if ($rows === []) {
echo '<tr><td colspan="3">' . esc_html__('暂无预设回复', 'aksrt') . '</td></tr>';
}
foreach ($rows as $row) {
$reply_cells = sprintf(
'<tr><td>%d</td><td>%s</td><td>',
(int) $row['id'],
esc_html((string) $row['content'])
);
$reply_cells .= '<a href="' . esc_url(add_query_arg(['prereply_id' => (int) $row['id']], $this->ticket_page_url(['tab' => 'config']))) . '">' . esc_html__('编辑', 'aksrt') . '</a> ';
$reply_cells .= $this->ticket_delete_button('prereply_delete', 'prereply_id', (int) $row['id'], __('删除', 'aksrt'));
$reply_cells .= '</td></tr>';
echo $reply_cells; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
echo '</tbody></table>';
echo '<h4>' . esc_html($editing === null ? __('新增预设回复', 'aksrt') : __('编辑预设回复', 'aksrt')) . '</h4>';
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '">';
echo '<input type="hidden" name="action" value="aksrt_ticket_update" />';
echo '<input type="hidden" name="ticket_action" value="prereply" />';
echo '<input type="hidden" name="ticket_tab" value="config" />';
echo '<input type="hidden" name="prereply_id" value="' . esc_attr((string) ($editing['id'] ?? 0)) . '" />';
echo wp_nonce_field('aksrt_ticket_prereply_' . (int) ($editing['id'] ?? 0), '_wpnonce', true, false);
echo '<p><textarea name="content" rows="3" class="large-text" placeholder="' . esc_attr__('预设回复内容', 'aksrt') . '">' . esc_textarea((string) ($editing['content'] ?? '')) . '</textarea></p>';
submit_button(__('保存预设回复', 'aksrt'), 'primary', '', false);
echo '</form>';
}
}
@@ -205,7 +205,6 @@ trait UsersMenusTicketsList {
$html .= '<input type="hidden" name="action" value="aksrt_ticket_update" />';
$html .= '<input type="hidden" name="ticket_action" value="reply" />';
$html .= '<input type="hidden" name="ticket_id" value="' . esc_attr((string) $id) . '" />';
$html .= '<input type="hidden" name="ticket_tab" value="tickets" />';
$html .= wp_nonce_field('aksrt_ticket_reply_' . $id, '_wpnonce', true, false);
$html .= '<p><textarea name="content" rows="3" class="large-text" placeholder="' . esc_attr__('填写回复内容', 'aksrt') . '"></textarea></p>';
$html .= '<p><button type="submit" class="button button-primary">' . esc_html__('提交回复', 'aksrt') . '</button></p>';
@@ -217,7 +216,6 @@ trait UsersMenusTicketsList {
$html .= '<input type="hidden" name="action" value="aksrt_ticket_update" />';
$html .= '<input type="hidden" name="ticket_action" value="status" />';
$html .= '<input type="hidden" name="ticket_id" value="' . esc_attr((string) $id) . '" />';
$html .= '<input type="hidden" name="ticket_tab" value="tickets" />';
$html .= wp_nonce_field('aksrt_ticket_status_' . $id, '_wpnonce', true, false);
$html .= '<p><label>' . esc_html__('状态', 'aksrt') . ' <select name="status">';
foreach (\AKSRT\Ticket\TicketStatusService::all() as $row) {
@@ -259,7 +257,6 @@ trait UsersMenusTicketsList {
$html .= '<input type="hidden" name="action" value="aksrt_ticket_update" />';
$html .= '<input type="hidden" name="ticket_action" value="note" />';
$html .= '<input type="hidden" name="ticket_id" value="' . esc_attr((string) $id) . '" />';
$html .= '<input type="hidden" name="ticket_tab" value="tickets" />';
$html .= wp_nonce_field('aksrt_ticket_note_' . $id, '_wpnonce', true, false);
$html .= '<p><input type="text" name="content" class="large-text" placeholder="' . esc_attr__('添加内部备注(用户不可见)', 'aksrt') . '" /> '
. '<button type="submit" class="button">' . esc_html__('添加备注', 'aksrt') . '</button></p>';