diff --git a/assets/admin/src/api/modules/index.js b/assets/admin/src/api/modules/index.js
index 9e9d622f..750bdc61 100644
--- a/assets/admin/src/api/modules/index.js
+++ b/assets/admin/src/api/modules/index.js
@@ -6,6 +6,7 @@ import { extensionsApiMethods } from './extensions.js'
import { toolsApiMethods } from './tools.js'
import { schemaApiMethods } from './schema.js'
import { iconsApiMethods } from './icons.js'
+import { ticketsApiMethods } from './tickets.js'
export const apiMethods = {
...globalApiMethods,
@@ -16,4 +17,5 @@ export const apiMethods = {
...toolsApiMethods,
...schemaApiMethods,
...iconsApiMethods,
+ ...ticketsApiMethods,
}
diff --git a/assets/admin/src/api/modules/tickets.js b/assets/admin/src/api/modules/tickets.js
new file mode 100644
index 00000000..f6ad3479
--- /dev/null
+++ b/assets/admin/src/api/modules/tickets.js
@@ -0,0 +1,55 @@
+/**
+ * 工单管理端 REST(命名空间 aksrt/v1/admin)。
+ *
+ * TicketRestAdmin 把三块字典 CRUD(工单状态 / 部门 / 预设回复)挂在
+ * aksrt/v1/admin 下,全部要求 manage_options。api 客户端的 baseUrl 只到
+ * aksrt/v1,因此这里必须用 /aksrt/ 全路径形式(joinRestUrl 的
+ * startsWith('/aksrt/') 分支会原样拼接,不再追加 base 前缀)。
+ */
+
+const ADMIN = '/aksrt/v1/admin/tickets'
+
+export const ticketsApiMethods = {
+ /* ---------------- 工单状态 ---------------- */
+
+ listTicketStatuses() { return this.request(`${ADMIN}/statuses`) },
+ createTicketStatus(data) {
+ return this.request(`${ADMIN}/statuses`, { method: 'POST', body: JSON.stringify(data) })
+ },
+ updateTicketStatus(id, data) {
+ return this.request(`${ADMIN}/statuses/${id}`, { method: 'POST', body: JSON.stringify(data) })
+ },
+ deleteTicketStatus(id) {
+ return this.request(`${ADMIN}/statuses/${id}/delete`, { method: 'POST', body: '{}' })
+ },
+
+ /* ---------------- 工单部门 ---------------- */
+
+ listTicketTypes() { return this.request(`${ADMIN}/types`) },
+ createTicketType(data) {
+ return this.request(`${ADMIN}/types`, { method: 'POST', body: JSON.stringify(data) })
+ },
+ updateTicketType(id, data) {
+ return this.request(`${ADMIN}/types/${id}`, { method: 'POST', body: JSON.stringify(data) })
+ },
+ deleteTicketType(id) {
+ return this.request(`${ADMIN}/types/${id}/delete`, { method: 'POST', body: '{}' })
+ },
+
+ /* ---------------- 预设回复 ---------------- */
+
+ listTicketPrereplies() { return this.request(`${ADMIN}/prereplies`) },
+ createTicketPrereply(data) {
+ return this.request(`${ADMIN}/prereplies`, { method: 'POST', body: JSON.stringify(data) })
+ },
+ updateTicketPrereply(id, data) {
+ return this.request(`${ADMIN}/prereplies/${id}`, { method: 'POST', body: JSON.stringify(data) })
+ },
+ deleteTicketPrereply(id) {
+ return this.request(`${ADMIN}/prereplies/${id}/delete`, { method: 'POST', body: '{}' })
+ },
+ /** 排序入参必须是全量 ID,数量不符后端直接拒绝。 */
+ reorderTicketPrereplies(ids) {
+ return this.request(`${ADMIN}/prereplies/order`, { method: 'POST', body: JSON.stringify({ ids }) })
+ },
+}
diff --git a/assets/admin/src/constants/sections/users.js b/assets/admin/src/constants/sections/users.js
index 1c74de48..db8941f9 100644
--- a/assets/admin/src/constants/sections/users.js
+++ b/assets/admin/src/constants/sections/users.js
@@ -130,13 +130,14 @@ export const usersSection = {
canonicalDomain: 'users',
section: 'report',
}),
- schemaPage({
+ customPage({
key: 'users-ticket',
name: 'UsersTicket',
label: '工单中心',
route: '/users/ticket',
title: '工单中心',
- desc: '工单功能开关、工单号前缀、提交公告与提交表单(图片上传、关联订单、催单冷却)配置;部门、状态与预设回复在 WordPress 用户菜单的「工单管理」中维护。',
+ desc: '工单功能开关、工单号前缀、提交公告与提交表单(图片上传、关联订单、催单冷却)配置,以及部门、状态、预设回复维护;工单处理在 WordPress 用户菜单的「工单管理」中进行。',
+ component: 'UsersTicket',
canonicalDomain: 'users',
section: 'ticket',
}),
diff --git a/assets/admin/src/pages/users/UsersTicket.vue b/assets/admin/src/pages/users/UsersTicket.vue
new file mode 100644
index 00000000..76df3d9e
--- /dev/null
+++ b/assets/admin/src/pages/users/UsersTicket.vue
@@ -0,0 +1,69 @@
+
+
+
+
+
+ 工单处理
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/assets/admin/src/pages/users/partials/ticket/TicketPrereplyPanel.vue b/assets/admin/src/pages/users/partials/ticket/TicketPrereplyPanel.vue
new file mode 100644
index 00000000..05a1cb38
--- /dev/null
+++ b/assets/admin/src/pages/users/partials/ticket/TicketPrereplyPanel.vue
@@ -0,0 +1,225 @@
+
+
+
+
+
+ 预设回复是管理员回复框的一键插入话术,按下列顺序展示;拖动行调整顺序后需点「保存排序」。
+
+
+
+
+
+
+
+ ⋮⋮
+ {{ index + 1 }}
+ {{ element.content }}
+
+ 编辑
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 取消
+ 保存
+
+
+
+
+
+
+
+
+
diff --git a/assets/admin/src/pages/users/partials/ticket/TicketStatusPanel.vue b/assets/admin/src/pages/users/partials/ticket/TicketStatusPanel.vue
new file mode 100644
index 00000000..a1c54ebc
--- /dev/null
+++ b/assets/admin/src/pages/users/partials/ticket/TicketStatusPanel.vue
@@ -0,0 +1,261 @@
+
+
+
+
+
+ 默认状态不可修改或删除;删除非默认状态前需先清空该状态下的工单。标记为「完结」的状态不可再流转。
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ form.name || '预览' }}
+
+
+
+
+
+ 开启后该状态视为终态,工单不可再流转
+
+
+
+
+ 取消
+ 保存
+
+
+
+
+
+
+
+
+
diff --git a/assets/admin/src/pages/users/partials/ticket/TicketTypePanel.vue b/assets/admin/src/pages/users/partials/ticket/TicketTypePanel.vue
new file mode 100644
index 00000000..29f396f6
--- /dev/null
+++ b/assets/admin/src/pages/users/partials/ticket/TicketTypePanel.vue
@@ -0,0 +1,252 @@
+
+
+
+
+
+ 部门决定用户提交工单时的分类与默认受理人;删除前需先清空该部门下的工单。
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 取消
+ 保存
+
+
+
+
+
+
+
diff --git a/assets/admin/src/router/component-loaders/users.js b/assets/admin/src/router/component-loaders/users.js
index a171cab8..cbe34425 100644
--- a/assets/admin/src/router/component-loaders/users.js
+++ b/assets/admin/src/router/component-loaders/users.js
@@ -3,4 +3,5 @@ export const usersPageLoaders = {
UsersLevel: () => import('@/pages/users/UsersLevel.vue'),
UsersBadge: () => import('@/pages/users/UsersBadge.vue'),
UsersWechatTemplate: () => import('@/pages/users/UsersWechatTemplate.vue'),
+ UsersTicket: () => import('@/pages/users/UsersTicket.vue'),
}
diff --git a/assets/admin/src/schema/sections/users/ticket.js b/assets/admin/src/schema/sections/users/ticket.js
index 31ca3ed4..5fb18fa7 100644
--- a/assets/admin/src/schema/sections/users/ticket.js
+++ b/assets/admin/src/schema/sections/users/ticket.js
@@ -8,7 +8,7 @@ export const ticketSchema = {
key: 'users.ticket',
title: '工单中心',
icon: 'fa fas fa-headset',
- description: '工单功能开关、工单号前缀、提交公告与提交表单(图片上传、关联订单、催单冷却)配置。部门、状态与预设回复在「用户 → 工单管理」中维护。',
+ description: '工单功能开关、工单号前缀、提交公告与提交表单(图片上传、关联订单、催单冷却)配置;部门、状态与预设回复在本页其余分页中维护。',
defaults: {
ticket_s: true,
ticket_num_prefix: 'T',
diff --git a/src/Admin/Native/UsersMenusTicketsCenter.php b/src/Admin/Native/UsersMenusTicketsCenter.php
index 41ee0fe2..e94470f5 100644
--- a/src/Admin/Native/UsersMenusTicketsCenter.php
+++ b/src/Admin/Native/UsersMenusTicketsCenter.php
@@ -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 '
';
echo '
' . esc_html__('工单管理', 'aksrt') . '
';
+ printf(
+ '
%s
',
+ esc_url(admin_url('admin.php?page=aksrt-settings#/users/ticket')),
+ esc_html__('工单配置(部门 / 状态 / 预设回复)', 'aksrt')
+ );
echo ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
$this->render_ticket_notices();
- echo '
';
- foreach (['tickets' => __('工单列表', 'aksrt'), 'config' => __('配置', 'aksrt')] as $key => $label) {
- printf(
- '%s',
- esc_url(admin_url('users.php?page=' . self::TICKET_PAGE_SLUG . '&tab=' . $key)),
- $tab === $key ? 'nav-tab-active' : '',
- esc_html($label)
- );
- }
- echo '
';
-
- if ($tab === 'config') {
- $this->render_ticket_config_tab();
- } else {
- $this->render_ticket_list_tab();
- }
+ $this->render_ticket_list_tab();
echo '
';
}
@@ -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(
- '',
- 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);'
diff --git a/src/Admin/Native/UsersMenusTicketsConfig.php b/src/Admin/Native/UsersMenusTicketsConfig.php
deleted file mode 100644
index 90ea7c13..00000000
--- a/src/Admin/Native/UsersMenusTicketsConfig.php
+++ /dev/null
@@ -1,165 +0,0 @@
-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 '' . esc_html__('工单部门', 'aksrt') . '
';
- echo '';
- echo '| ID | ' . esc_html__('名称', 'aksrt') . ' | '
- . '' . esc_html__('说明', 'aksrt') . ' | '
- . '' . esc_html__('受理管理员', 'aksrt') . ' | |
';
- foreach ($types as $row) {
- $edit_url = add_query_arg(['type_id' => (int) $row['id']], $this->ticket_page_url(['tab' => 'config']));
- $cells = sprintf(
- '| %d | %s | %s | %s | ',
- (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 .= '' . esc_html__('编辑', 'aksrt') . ' ';
- $cells .= $this->ticket_delete_button('type_delete', 'type_id', (int) $row['id'], __('删除', 'aksrt'));
- $cells .= ' |
';
- echo $cells; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
- }
- echo '
';
-
- echo '' . esc_html($editing === null ? __('新增部门', 'aksrt') : __('编辑部门', 'aksrt')) . '
';
- echo '';
- }
-
- /** 自定义状态:默认状态拒绝改删。 */
- 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 '' . esc_html__('工单状态', 'aksrt') . '
';
- echo '' . esc_html__('默认状态不可修改或删除;删除非默认状态前需先清空该状态下的工单。', 'aksrt') . '
';
- echo '';
- echo '| ID | ' . esc_html__('名称', 'aksrt') . ' | '
- . '' . esc_html__('颜色', 'aksrt') . ' | '
- . '' . esc_html__('完结', 'aksrt') . ' | '
- . '' . esc_html__('默认', 'aksrt') . ' | |
';
- foreach ($statuses as $row) {
- $status_cells = sprintf(
- '| %d | %s | '
- . '%s | '
- . '%s | %s | ',
- (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 .= '' . esc_html__('编辑', 'aksrt') . ' ';
- $status_cells .= $this->ticket_delete_button('status_delete', 'status_id', (int) $row['id'], __('删除', 'aksrt'));
- } else {
- $status_cells .= '—';
- }
- $status_cells .= ' |
';
- echo $status_cells; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
- }
- echo '
';
-
- echo '' . esc_html($editing === null ? __('新增状态', 'aksrt') : __('编辑状态', 'aksrt')) . '
';
- echo '';
- }
-
- /** 预设回复:表格 + 行内编辑 + 新建表单。 */
- 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 '' . esc_html__('预设回复', 'aksrt') . '
';
- echo '';
- echo '| ID | ' . esc_html__('内容', 'aksrt') . ' | |
';
- if ($rows === []) {
- echo '| ' . esc_html__('暂无预设回复', 'aksrt') . ' |
';
- }
- foreach ($rows as $row) {
- $reply_cells = sprintf(
- '| %d | %s | ',
- (int) $row['id'],
- esc_html((string) $row['content'])
- );
- $reply_cells .= '' . esc_html__('编辑', 'aksrt') . ' ';
- $reply_cells .= $this->ticket_delete_button('prereply_delete', 'prereply_id', (int) $row['id'], __('删除', 'aksrt'));
- $reply_cells .= ' |
';
- echo $reply_cells; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
- }
- echo '
';
-
- echo '' . esc_html($editing === null ? __('新增预设回复', 'aksrt') : __('编辑预设回复', 'aksrt')) . '
';
- echo '';
- }
-}
diff --git a/src/Admin/Native/UsersMenusTicketsList.php b/src/Admin/Native/UsersMenusTicketsList.php
index b2a7cc78..d0b6bee8 100644
--- a/src/Admin/Native/UsersMenusTicketsList.php
+++ b/src/Admin/Native/UsersMenusTicketsList.php
@@ -205,7 +205,6 @@ trait UsersMenusTicketsList {
$html .= '';
$html .= '';
$html .= '';
- $html .= '';
$html .= wp_nonce_field('aksrt_ticket_reply_' . $id, '_wpnonce', true, false);
$html .= '';
$html .= '';
@@ -217,7 +216,6 @@ trait UsersMenusTicketsList {
$html .= '';
$html .= '';
$html .= '';
- $html .= '';
$html .= wp_nonce_field('aksrt_ticket_status_' . $id, '_wpnonce', true, false);
$html .= '