Move "代客提交工单" back into a native <dialog> on the ticket list while the ticket workspace stays a full page. Closing asks before discarding input, ignores backdrop clicks, resets the form and returns focus; an uncertain create result keeps the form locked behind "重新加载". Remove the ?view=new create page route and its styles. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
150 lines
8.9 KiB
PHP
150 lines
8.9 KiB
PHP
<?php
|
|
/** Runs the real native renderers and query builder without a WordPress installation. */
|
|
namespace {
|
|
define('ABSPATH', __DIR__);
|
|
define('ARRAY_A', 'ARRAY_A');
|
|
function __($text, $domain = '') { return $text; }
|
|
function esc_html($text) { return htmlspecialchars((string) $text, ENT_QUOTES, 'UTF-8'); }
|
|
function esc_attr($text) { return esc_html($text); }
|
|
function esc_html__($text, $domain = '') { return esc_html($text); }
|
|
function esc_attr__($text, $domain = '') { return esc_attr($text); }
|
|
function esc_url($url) { return preg_match('~^(https?://|#)~', (string) $url) ? esc_attr($url) : ''; }
|
|
function wpautop($text) { return '<p>' . str_replace("\n\n", '</p><p>', $text) . '</p>'; }
|
|
function sanitize_key($text) { return preg_replace('/[^a-z0-9_\-]/', '', strtolower((string) $text)); }
|
|
function sanitize_text_field($text) { return trim(strip_tags((string) $text)); }
|
|
function wp_unslash($text) { return stripslashes((string) $text); }
|
|
function absint($value) { return abs((int) $value); }
|
|
function selected($a, $b, $echo = true) { return (string) $a === (string) $b ? ' selected="selected"' : ''; }
|
|
function number_format_i18n($number) { return number_format($number); }
|
|
function admin_url($path) { return 'https://example.test/wp-admin/' . $path; }
|
|
function rest_url($path) { return 'https://example.test/wp-json/' . $path; }
|
|
function get_current_user_id() { return 7; }
|
|
function wp_get_current_user() { return (object) ['ID' => 7, 'display_name' => 'Operator']; }
|
|
function get_users($args) { return [(object) ['ID' => 7, 'display_name' => 'Operator'], (object) ['ID' => 8, 'display_name' => 'Second']]; }
|
|
function get_edit_user_link($id) { return admin_url('user-edit.php?user_id=' . $id); }
|
|
function aksrt_get_avatar_url($id, $size) { return 'https://example.test/avatar/' . $id; }
|
|
function add_query_arg($args, $url, $base = null) {
|
|
if (!is_array($args)) { $args = [$args => $url]; $url = $base; }
|
|
$parts = parse_url($url);
|
|
parse_str($parts['query'] ?? '', $query);
|
|
return $parts['scheme'] . '://' . $parts['host'] . ($parts['path'] ?? '') . '?' . http_build_query(array_merge($query, $args));
|
|
}
|
|
}
|
|
namespace AKSRT\Ticket {
|
|
class TicketSettings {
|
|
public static bool $orders = true;
|
|
public static function title_max() { return 100; }
|
|
public static function content_max() { return 1000; }
|
|
public static function reply_max() { return 500; }
|
|
public static function order_link_enabled() { return self::$orders; }
|
|
}
|
|
class TicketTypeService {
|
|
public static bool $empty = false;
|
|
public static function all() { return self::$empty ? [] : [['id' => 1, 'name' => 'Support <script>']]; }
|
|
}
|
|
class TicketStatusService {
|
|
public const KEY_PENDING = 'pending';
|
|
public const KEY_USER_REPLIED = 'user_replied';
|
|
public static function all() {
|
|
return [
|
|
['status_key' => 'pending', 'name' => '待接单', 'is_closed' => 0, 'color' => '#f0a020'],
|
|
['status_key' => 'handling', 'name' => '处理中', 'is_closed' => 0, 'color' => '#18a058'],
|
|
['status_key' => 'closed', 'name' => '已关闭', 'is_closed' => 1, 'color' => '#999999'],
|
|
['status_key' => 'archived', 'name' => '已归档', 'is_closed' => 1, 'color' => '#999999'],
|
|
];
|
|
}
|
|
public static function map() { return array_column(self::all(), null, 'status_key'); }
|
|
public static function can_transition($from, $to) { return $from !== 'closed' && !($from === 'pending' && $to === 'closed'); }
|
|
}
|
|
class TicketPrereplyService {
|
|
public static function all() { return [['content' => 'Preset <script>alert(1)</script>']]; }
|
|
}
|
|
class TicketNoteService {
|
|
public static function for_ticket($id) { return [['admin_name' => 'Admin <img>', 'created_at' => '2026-10-31', 'content' => 'Secret <script>alert(1)</script>']]; }
|
|
}
|
|
class TicketRepository {
|
|
public static array $queries = [];
|
|
public static array $counts = [];
|
|
public static int $reads = 0;
|
|
public static int $replies_read = 0;
|
|
public static function get_instance() { static $repo; return $repo ??= new self(); }
|
|
public function sample($id = 21) {
|
|
return [
|
|
'id' => $id, 'ticket_num' => 'TK-000' . $id, 'title' => 'Question <svg onload=alert(1)>',
|
|
'user_id' => 12, 'user_name' => 'User <b>x</b>', 'admin_id' => 7, 'admin_name' => 'Operator',
|
|
'ticket_type_id' => 1, 'ticket_type_name' => 'Support <script>',
|
|
'status' => $id === 99 ? 'closed' : 'handling', 'status_label' => $id === 99 ? '已关闭' : '处理中', 'status_color' => '#18a058', 'is_closed' => $id === 99,
|
|
'content' => 'Description <script>alert(1)</script>', 'attachment' => ['https://example.test/image.jpg'],
|
|
'post_time' => '2026-10-30 12:00:00', 'last_reply_time' => '', 'post_admin_id' => 7,
|
|
'order_number' => 'ORDER-1', 'urge_count' => 1, 'last_urge_time' => '2026-10-31 11:00:00',
|
|
];
|
|
}
|
|
public function find($id) { self::$reads++; return $id === 404 ? null : $this->sample($id); }
|
|
public function paginate($args, $admin) {
|
|
self::$queries[] = $args;
|
|
return ['items' => $args['offset'] > 20 ? [] : [$this->sample()], 'total' => 21];
|
|
}
|
|
public function statistic($args, $admin) { self::$counts[] = $args; return ['pending' => 2, 'handling' => 4, 'closed' => 6, 'archived' => 3]; }
|
|
public function replies($id) {
|
|
self::$replies_read++;
|
|
return [
|
|
['author_type' => 'system', 'content' => '催单', 'created_at' => '2026-10-31'],
|
|
['author_type' => 'user', 'author_id' => 12, 'author_name' => 'User', 'content' => 'Reply <script>', 'created_at' => '2026-10-31', 'attachment' => [], 'quote' => ['is_deleted' => true, 'content' => 'Deleted']],
|
|
];
|
|
}
|
|
}
|
|
}
|
|
namespace AKSRT\Core {
|
|
class DBMigrator { public static function table($name) { return 'wp_aksrt_' . $name; } }
|
|
}
|
|
namespace {
|
|
$root = dirname(__DIR__, 2);
|
|
foreach (['NativeUi', 'AdminListPagination', 'UsersMenusTicketsList', 'UsersMenusTicketsCreate', 'UsersMenusTicketsDetail', 'UsersMenusTicketsCenter'] as $file) {
|
|
require $root . '/src/Admin/Native/' . $file . '.php';
|
|
}
|
|
require $root . '/src/Ticket/TicketQuery.php';
|
|
class TicketPage {
|
|
use \AKSRT\Admin\Native\UsersMenusTicketsCenter;
|
|
public const CAPABILITY = 'manage_options';
|
|
private function ensure_cap() {}
|
|
}
|
|
class QueryProbe {
|
|
use \AKSRT\Ticket\TicketQuery;
|
|
public function where($args) { return $this->ticket_where($args, true); }
|
|
public function counts($args) { return $this->ticket_statistic($args, true); }
|
|
}
|
|
class FakeDb {
|
|
public array $params = [];
|
|
public string $sql = '';
|
|
public function esc_like($text) { return addcslashes($text, '_%\\'); }
|
|
public function prepare($sql, $params) { $this->sql = $sql; $this->params = $params; return $sql; }
|
|
public function get_results($sql, $format) { return [['status' => 'closed', 'total' => 2]]; }
|
|
}
|
|
function render_page(TicketPage $page, array $get): string {
|
|
$_GET = $get;
|
|
ob_start();
|
|
$page->render_tickets_page();
|
|
return (string) ob_get_clean();
|
|
}
|
|
$repo = \AKSRT\Ticket\TicketRepository::class;
|
|
$page = new TicketPage();
|
|
$list = render_page($page, ['status' => 'handling', 'ticket_type_id' => 1, 's' => 'needle', 'paged' => 99]);
|
|
$initial = ['queries' => $repo::$queries, 'counts' => $repo::$counts, 'detail_reads' => $repo::$reads, 'reply_reads' => $repo::$replies_read];
|
|
$list_queries = count($repo::$queries);
|
|
$open = render_page($page, ['id' => 21, 'status' => 'pending', 's' => 'not in list', 'paged' => 3, 'aksrt_ticket_msg' => 'reply']);
|
|
$closed = render_page($page, ['id' => 99]);
|
|
$missing = render_page($page, ['id' => 404]);
|
|
$detail_list_queries = count($repo::$queries) - $list_queries;
|
|
$bogus_notice = render_page($page, ['id' => 21, 'aksrt_ticket_msg' => '<script>']);
|
|
\AKSRT\Ticket\TicketTypeService::$empty = true;
|
|
$no_types = render_page($page, []);
|
|
\AKSRT\Ticket\TicketTypeService::$empty = false;
|
|
\AKSRT\Ticket\TicketSettings::$orders = false;
|
|
$no_orders = render_page($page, []);
|
|
$GLOBALS['wpdb'] = new FakeDb();
|
|
$query = new QueryProbe();
|
|
$where = $query->where([]);
|
|
$statistic = $query->counts(['status' => ['pending', 'handling', 'closed', 'archived'], 'ticket_type_id' => 1, 'keywords' => 'needle']);
|
|
echo json_encode(compact('list', 'initial', 'open', 'closed', 'missing', 'detail_list_queries', 'bogus_notice', 'no_types', 'no_orders', 'where', 'statistic') + ['count_sql' => $GLOBALS['wpdb']->sql, 'count_params' => $GLOBALS['wpdb']->params], JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
}
|