瀏覽代碼

Merge pull request #1674 from sspanel-uim/dev

Dev 20221124
M1Screw 2 年之前
父節點
當前提交
2e3817ff9f

+ 3 - 2
app/routes.php

@@ -154,9 +154,10 @@ return function (SlimApp $app): void {
         // Ticket Mange
         $this->get('/ticket', App\Controllers\Admin\TicketController::class . ':index');
         $this->post('/ticket', App\Controllers\Admin\TicketController::class . ':add');
-        $this->get('/ticket/{id}/view', App\Controllers\Admin\TicketController::class . ':show');
+        $this->get('/ticket/{id}/view', App\Controllers\Admin\TicketController::class . ':ticketView');
+        $this->put('/ticket/{id}/close', App\Controllers\Admin\TicketController::class . ':close');
         $this->put('/ticket/{id}', App\Controllers\Admin\TicketController::class . ':update');
-        $this->post('/ticket/ajax', App\Controllers\Admin\TicketController::class . ':ajax');
+        $this->delete('/ticket/{id}', App\Controllers\Admin\TicketController::class . ':delete');
 
         // Shop Mange
         $this->get('/shop', App\Controllers\Admin\ShopController::class . ':index');

+ 3 - 3
db/migrations/20000101000000_init_database.php.new

@@ -298,11 +298,11 @@ final class InitDatabase extends AbstractMigration
         $this->table('ticket', [ 'id' => false, 'primary_key' => [ 'id' ]])
             ->addColumn('id', 'biginteger', [ 'identity' => true ])
             ->addColumn('title', 'string', [])
-            ->addColumn('content', 'text', [])
-            ->addColumn('rootid', 'biginteger', [])
+            ->addColumn('content', 'json', [ 'comment' => '工单内容', 'default' => '' ])
             ->addColumn('userid', 'biginteger', [])
             ->addColumn('datetime', 'biginteger', [])
-            ->addColumn('status', 'integer', [ 'default' => 1 ])
+            ->addColumn('status', 'string', [ 'comment' => '工单状态', 'default' => '' ])
+            ->addColumn('type', 'string', [ 'comment' => '工单类型', 'default' => 'other' ])
             ->create();
 
         $this->table('unblockip', [ 'id' => false, 'primary_key' => [ 'id' ]])

+ 30 - 0
db/migrations/20221112115300_update_ticket.php

@@ -0,0 +1,30 @@
+<?php
+
+declare(strict_types=1);
+
+use Phinx\Migration\AbstractMigration;
+
+final class UpdateTicket extends AbstractMigration
+{
+    public function up(): void
+    {
+        if ($this->table('ticket')->hasColumn('rootid')) {
+            $this->table('ticket')
+                ->changeColumn('content', 'json', [ 'comment' => '工单内容', 'default' => '' ])
+                ->changeColumn('status', 'string', [ 'comment' => '工单状态', 'default' => '' ])
+                ->addColumn('type', 'string', [ 'comment' => '工单类型', 'default' => 'other' ])
+                ->removeColumn('rootid')
+                ->save();
+        }
+    }
+
+    public function down(): void
+    {
+        $this->table('ticket')
+            ->changeColumn->addColumn('content', 'text', [])
+            ->changeColumn('status', 'integer', [ 'default' => 1 ])
+            ->removeColumn('type')
+            ->addColumn('rootid', 'biginteger', [])
+            ->save();
+    }
+}

+ 1 - 0
phpinsights.php

@@ -14,6 +14,7 @@ return [
         PHP_CodeSniffer\Standards\Generic\Sniffs\Formatting\SpaceAfterNotSniff::class,
         PHP_CodeSniffer\Standards\Squiz\Sniffs\PHP\EvalSniff::class,
         PHP_CodeSniffer\Standards\Squiz\Sniffs\PHP\GlobalKeywordSniff::class,
+        PHP_CodeSniffer\Standards\Generic\Sniffs\PHP\NoSilencedErrorsSniff::class,
         PhpCsFixer\Fixer\Import\OrderedImportsFixer::class,
         SlevomatCodingStandard\Sniffs\ControlStructures\DisallowShortTernaryOperatorSniff::class,
         SlevomatCodingStandard\Sniffs\Classes\ForbiddenPublicPropertySniff::class,

+ 65 - 0
resources/views/tabler/admin/tabler_footer.tpl

@@ -1,3 +1,68 @@
+<div class="modal modal-blur fade" id="success-dialog" tabindex="-1" role="dialog" aria-hidden="true">
+    <div class="modal-dialog modal-sm modal-dialog-centered" role="document">
+        <div class="modal-content">
+            <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
+            <div class="modal-status bg-success"></div>
+            <div class="modal-body text-center py-4">
+                <i class="ti ti-circle-check icon mb-2 text-green icon-lg" style="font-size:3.5rem;"></i>
+                <p id="success-message" class="text-muted">成功</p>
+            </div>
+            <div class="modal-footer">
+                <div class="w-100">
+                    <div class="row">
+                        <div class="col">
+                            <a id="success-confirm" href="" class="btn w-100" data-bs-dismiss="modal">
+                                好
+                            </a>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>
+
+<div class="modal modal-blur fade" id="fail-dialog" tabindex="-1" role="dialog" aria-hidden="true">
+    <div class="modal-dialog modal-sm modal-dialog-centered" role="document">
+        <div class="modal-content">
+            <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
+            <div class="modal-status bg-danger"></div>
+            <div class="modal-body text-center py-4">
+                <i class="ti ti-circle-x icon mb-2 text-danger icon-lg" style="font-size:3.5rem;"></i>
+                <p id="fail-message" class="text-muted">失败</p>
+            </div>
+            <div class="modal-footer">
+                <div class="w-100">
+                    <div class="row">
+                        <div class="col">
+                            <a href="" class="btn btn-danger w-100" data-bs-dismiss="modal">
+                                确认
+                            </a>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>
+
+<div class="modal modal-blur fade" id="notice-dialog" tabindex="-1" role="dialog" aria-hidden="true">
+    <div class="modal-dialog modal-sm modal-dialog-centered" role="document">
+        <div class="modal-content">
+            <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
+            <div class="modal-status bg-yellow"></div>
+            <div class="modal-body text-center py-4">
+                <i class="ti ti-alert-circle icon mb-2 text-yellow icon-lg" style="font-size:3.5rem;"></i>
+                <p id="notice-message" class="text-muted">注意</p>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn me-auto" data-bs-dismiss="modal">取消</button>
+                <button id="notice-confirm" type="button" class="btn btn-yellow" data-bs-dismiss="modal">确认</button>
+            </div>
+        </div>
+    </div>
+</div>
+
 <footer class="footer footer-transparent d-print-none">
     <div class="container-xl">
         <div class="row text-center align-items-center flex-row-reverse">

+ 145 - 79
resources/views/tabler/admin/ticket/index.tpl

@@ -1,92 +1,158 @@
-{include file='admin/main.tpl'}
+{include file='admin/tabler_header.tpl'}
 
-<main class="content">
-    <div class="content-header ui-content-header">
-        <div class="container">
-            <h1 class="content-heading">工单</h1>
-        </div>
-    </div>
-    <div class="container">
-        <div class="col-lg-12 col-sm-12">
-            <section class="content-inner margin-top-no">
-                <div class="card">
-                    <div class="card-main">
-                        <div class="card-inner">
-                            <p>系统中的工单</p>
-                            <p>显示表项:
-                                {include file='table/checkbox.tpl'}
-                            </p>
-                        </div>
+<div class="page-wrapper">
+    <div class="container-xl">
+        <div class="page-header d-print-none text-white">
+            <div class="row align-items-center">
+                <div class="col">
+                    <h2 class="page-title" style="line-height: unset;">
+                        <span class="home-title">工单列表</span>
+                    </h2>
+                    <div class="page-pretitle">
+                        <span class="home-subtitle">查看并回复用户工单</span>
                     </div>
                 </div>
-                <div class="card">
-                    <div class="card-main">
-                        <div class="card-inner">
-                            <div class="form-group form-group-label">
-                                <label class="floating-label" for="userid"> 输入用戶 ID 快速创建新工单 </label>
-                                <input class="form-control maxwidth-edit" id="userid" type="text">
-                            </div>
-                        </div>
-                        <div class="card-inner">
-                            <div class="form-group form-group-label">
-                                <label class="floating-label" for="title"> 标题 </label>
-                                <input class="form-control maxwidth-edit" id="title" type="text">
-                            </div>
-                        </div>
-                        <div class="card-inner">
-                            <div class="form-group form-group-label">
-                                <label class="floating-label" for="content"> 内容 </label>
-                                <input class="form-control maxwidth-edit" id="content" type="text">
-                            </div>
-                        </div>
-                        <div class="card-action">
-                            <div class="card-action-btn pull-left">
-                                <a class="btn btn-flat waves-attach waves-light" id="ticket_create"><span
-                                            class="mdi mdi-check"></span>&nbsp;添加</a>
-                            </div>
+            </div>
+        </div>
+    </div>
+    <div class="page-body">
+        <div class="container-xl">
+            <div class="row row-deck row-cards">
+                <div class="col-12">
+                    <div class="card">
+                        <div class="table-responsive">
+                            <table id="data_table" class="table card-table table-vcenter text-nowrap datatable">
+                                <thead>
+                                    <tr>
+                                        <th>操作</th>
+                                        {foreach $details['field'] as $key => $value}
+                                            <th>{$value}</th>
+                                        {/foreach}
+                                    </tr>
+                                </thead>
+                                <tbody id="table_content">
+                                    {foreach $tickets as $ticket}
+                                        <tr>
+                                            <td>
+                                                <button type="button" class="btn btn-red" id="delete-ticket" 
+                                                onclick="deleteTicket({$ticket->id})">删除</button>
+                                                <button type="button" class="btn btn-orange" id="close-ticket" 
+                                                onclick="closeTicket({$ticket->id})">关闭</button>
+                                                <a class="btn btn-blue" href="/admin/ticket/{$ticket->id}/view">查看</a>
+                                            </td>
+                                            {foreach $details['field'] as $key => $value}
+                                                {if $key === 'status'}
+                                                <td>{Tools::getTicketStatus($ticket)}</td>
+                                                {elseif $key === 'type'}
+                                                <td>{Tools::getTicketType($ticket)}</td>
+                                                {elseif $key === 'datetime'}
+                                                <td>{Tools::toDateTime($ticket->$key)}</td>
+                                                {else}
+                                                <td>{$ticket->$key}</td>
+                                                {/if}
+                                            {/foreach}
+                                        </tr>
+                                    {/foreach}
+                                </tbody>
+                            </table>
                         </div>
                     </div>
                 </div>
-                <div class="table-responsive">
-                    {include file='table/table.tpl'}
-                </div>
-                {include file='dialog.tpl'}
+            </div>
         </div>
     </div>
-</main>
 
-{include file='admin/footer.tpl'}
+    <script>
+        function adjustStyle() {
+            $("td:contains('进行中')").css("color", "green");
+            $("td:contains('等待用户回复')").css("color", "blue");
+            $("td:contains('已结单')").css("color", "red");
+        }
 
-<script>
-    {include file='table/js_1.tpl'}
-    window.addEventListener('load', () => {
-        table = $('#table_tickets').DataTable({
-            ajax: 'ticket/ajax',
-            processing: true,
-            serverSide: true,
-            order: [[1, 'desc']]
-        })
-        {include file='table/js_2.tpl'}
-        function createTicket() {
-            $.ajax({
-                type: "POST",
-                url: "/admin/ticket",
-                dataType: "json",
-                data: {
-                    content: $$getValue('content'),
-                    title: $$getValue('title'),
-                    userid: $$getValue('userid')
-                },
-                success: data => {
-                    $("#result").modal();
-                    $$.getElementById('msg').innerHTML = data.msg;
+        function loadTable() {
+            $('#data_table').DataTable({
+                'iDisplayLength': 25,
+                'order': [
+                    [0, 'desc']
+                ],
+                "dom": "<'row px-3 py-3'<'col-sm-12 col-md-6'l><'col-sm-12 col-md-6'f>>" +
+                    "<'row'<'col-sm-12'tr>>" +
+                    "<'row card-footer d-flex align-items-center'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
+                language: {
+                    "sProcessing": "处理中...",
+                    "sLengthMenu": "显示 _MENU_ 条",
+                    "sZeroRecords": "没有匹配结果",
+                    "sInfo": "第 _START_ 至 _END_ 项结果,共 _TOTAL_ 项",
+                    "sInfoEmpty": "第 0 至 0 项结果,共 0 项",
+                    "sInfoFiltered": "(在 _MAX_ 项中查找)",
+                    "sInfoPostFix": "",
+                    "sSearch": "<i class=\"ti ti-search\"></i> ",
+                    "sUrl": "",
+                    "sEmptyTable": "表中数据为空",
+                    "sLoadingRecords": "载入中...",
+                    "sInfoThousands": ",",
+                    "oPaginate": {
+                        "sFirst": "首页",
+                        "sPrevious": "<i class=\"ti ti-arrow-left\"></i>",
+                        "sNext": "<i class=\"ti ti-arrow-right\"></i>",
+                        "sLast": "末页"
+                    },
+                    "oAria": {
+                        "sSortAscending": ": 以升序排列此列",
+                        "sSortDescending": ": 以降序排列此列"
+                    }
                 },
-                error: jqXHR => {
-                    $("#result").modal();
-                    $$.getElementById('msg').innerHTML = `${ldelim}jqXHR{rdelim} 发生了错误。`;
-                }
+                fnRowCallback: adjustStyle,
             });
         }
-        $$.getElementById('ticket_create').addEventListener('click', createTicket)
-    });
-</script>
+
+        function closeTicket(ticket_id) {
+            $('#notice-message').text('确定关闭此工单');
+            $('#notice-dialog').modal('show');
+            $('#notice-confirm').on('click', function () {
+                $.ajax({
+                    url: "/admin/ticket/" + ticket_id + '/close',
+                    type: 'PUT',
+                    dataType: "json",
+                    success: function(data) {
+                        if (data.ret == 1) {
+                            $('#success-message').text(data.msg);
+                            $('#success-dialog').modal('show');
+                        } else {
+                            $('#fail-message').text(data.msg);
+                            $('#fail-dialog').modal('show');
+                        }
+                    }
+                });
+            });
+        };
+
+        function deleteTicket(ticket_id) {
+            $('#notice-message').text('确定删除此工单');
+            $('#notice-dialog').modal('show');
+            $('#notice-confirm').on('click', function() {
+                $.ajax({
+                    url: "/admin/ticket/" + ticket_id,
+                    type: 'DELETE',
+                    dataType: "json",
+                    success: function(data) {
+                        if (data.ret == 1) {
+                            $('#success-message').text(data.msg);
+                            $('#success-dialog').modal('show');
+                        } else {
+                            $('#fail-message').text(data.msg);
+                            $('#fail-dialog').modal('show');
+                        }
+                    }
+                })
+            });
+        };
+
+        $("#success-confirm").click(function() {
+            location.reload();
+        });
+
+        loadTable();
+    </script>
+
+{include file='admin/tabler_footer.tpl'}

+ 135 - 175
resources/views/tabler/admin/ticket/view.tpl

@@ -1,209 +1,169 @@
-{include file='admin/main.tpl'}
-<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/css/editormd.min.css"/>
+{include file='admin/tabler_header.tpl'}
 
-<main class="content">
-    <div class="content-header ui-content-header">
-        <div class="container">
-            <h1 class="content-heading">查看工单</h1>
-        </div>
-    </div>
-    <div class="container">
-        <div class="col-lg-12 col-sm-12">
-            <section class="content-inner margin-top-no">
-
-                <div class="card">
-                    <div class="card-main">
-                        <div class="card-inner">
-                            <div class="form-group form-group-label">
-                                <label class="floating-label" for="content">内容</label>
-                                <div id="editormd">
-                                    <textarea style="display:none;" id="content"></textarea>
-                                </div>
-                            </div>
-
-                        </div>
+<div class="page-wrapper">
+    <div class="container-xl">
+        <div class="page-header d-print-none text-white">
+            <div class="row align-items-center">
+                <div class="col">
+                    <h2 class="page-title" style="line-height: unset;">
+                        <span class="home-title">工单回复</span>
+                    </h2>
+                    <div class="page-pretitle">
+                        <span class="home-subtitle">你可以在这里查看历史消息并添加回复</span>
                     </div>
                 </div>
-                <div aria-hidden="true" class="modal modal-va-middle fade" id="changetouser_modal" role="dialog"
-                     tabindex="-1">
-                    <div class="modal-dialog modal-xs">
-                        <div class="modal-content">
-                            <div class="modal-heading">
-                                <a class="modal-close" data-dismiss="modal">×</a>
-                                <h2 class="modal-title">确认要切换为该用户?</h2>
-                            </div>
-                            <div class="modal-inner">
-                                <p>切换为该用户以后,你随时可以通过菜单底部的「返回管理员身份」按钮返回本条工单。</p>
-                            </div>
-                            <div class="modal-footer">
-                                <p class="text-right">
-                                    <button class="btn btn-flat btn-brand-accent waves-attach waves-effect" data-dismiss="modal" type="button">取消</button>
-                                    <button class="btn btn-flat btn-brand-accent waves-attach" data-dismiss="modal" id="changetouser_input" type="button">确定</button>
-                                </p>
-                            </div>
+                <div class="col-auto ms-auto d-print-none">
+                    <div class="btn-list">
+                        {if $ticket->status !== 'closed'}
+                        <button href="#" class="btn btn-red d-none d-sm-inline-block" data-bs-toggle="modal"
+                            data-bs-target="#close_ticket_confirm_dialog">
+                            <i class="icon ti ti-x"></i>
+                            关闭
+                        </button>
+                        <button href="#" class="btn btn-red d-sm-none btn-icon" data-bs-toggle="modal"
+                            data-bs-target="#close_ticket_confirm_dialog">
+                            <i class="icon ti ti-x"></i>
+                        </button>
+                        {/if}
+                        <button href="#" class="btn btn-primary d-none d-sm-inline-block" data-bs-toggle="modal"
+                            data-bs-target="#add-reply">
+                            <i class="icon ti ti-plus"></i>
+                            回复
+                        </button>
+                        <button href="#" class="btn btn-primary d-sm-none btn-icon" data-bs-toggle="modal"
+                            data-bs-target="#add-reply">
+                            <i class="icon ti ti-plus"></i>
+                        </button>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+    <div class="page-body">
+        <div class="container-xl">
+            <div class="row row-cards">
+                <div class="col-12">
+                    <div class="card">
+                        <div class="card-body">
+                            <div class="h1 my-2 mb-3">#{$ticket->id} {$ticket->title}</div>
                         </div>
                     </div>
                 </div>
-                <div class="card">
-                    <div class="card-main">
-                        <div class="card-inner">
-
-                            <div class="form-group">
+            </div>
+            <div class="row justify-content-center my-3">
+                <div class="col-12">
+                    <div class="card">
+                        <div class="card-body">
+                            <div class="divide-y">
+                            {foreach $comments as $comment}
+                            <div>
                                 <div class="row">
-                                    <div class="col-md-10">
-                                        <button id="submit" type="submit" class="btn btn-brand waves-attach waves-light">添加</button>
-                                        <button id="close" type="submit" class="btn btn-brand-accent waves-attach waves-light">添加并关闭</button>
-                                        <button id="close_directly" type="submit" class="btn btn-brand-accent waves-attach waves-light">直接关闭</button>
-                                        <button  id="changetouser" class="btn btn-brand waves-attach waves-light" onClick="changetouser_modal_show()">切换为该用户</button>
+                                    <div class="col">
+                                        <div>
+                                            {nl2br($comment['comment'])}
+                                        </div>
+                                        <div class="text-muted my-1">{$comment['commenter_name']} 回复于 {Tools::toDateTime($comment['datetime'])}
+                                        </div>
+                                    </div>
+                                    <div class="col-auto">
+                                        <div>
+                                            # {$comment['comment_id'] + 1}
+                                        </div>
                                     </div>
                                 </div>
                             </div>
+                            {/foreach}
+                            </div>
                         </div>
                     </div>
                 </div>
-                {$render}
-                {foreach $ticketset as $ticket}
-                    <div class="card">
-                        <aside class="card-side pull-left" style="padding: 16px; text-align: center">
-                            <img style="border-radius: 100%; width: 100%" src="{$ticket->user()->gravatar}">
-                            <br>
-                            {$ticket->user()->user_name}
-                        </aside>
-                        <div class="card-main">
-                            <div class="card-inner">
-                                {$ticket->content}
-                            </div>
-                            <div class="card-action" style="padding: 12px"> {$ticket->datetime()}</div>
-                        </div>
+            </div>
+        </div>
+    </div>
+
+    <div class="modal modal-blur fade" id="add-reply" tabindex="-1" role="dialog" aria-hidden="true">
+        <div class="modal-dialog modal-dialog-centered modal-dialog-scrollable" role="document">
+            <div class="modal-content">
+                <div class="modal-header">
+                    <h5 class="modal-title">添加回复</h5>
+                    <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
+                </div>
+                <div class="modal-body">
+                    <div class="mb-3">
+                        <textarea id="reply-comment" class="form-control" rows="12" placeholder="请输入回复内容"></textarea>
                     </div>
-                {/foreach}
-                {$render}
-                {include file='dialog.tpl'}
+                </div>
+                <div class="modal-footer">
+                    <button type="button" class="btn me-auto" data-bs-dismiss="modal">取消</button>
+                    <button id="reply" type="button" class="btn btn-primary" data-bs-dismiss="modal">回复</button>
+                </div>
+            </div>
         </div>
     </div>
-</main>
 
-{include file='admin/footer.tpl'}
+    <div class="modal modal-blur fade" id="close_ticket_confirm_dialog" tabindex="-1" role="dialog" aria-hidden="true">
+        <div class="modal-dialog modal-dialog-centered modal-dialog-scrollable" role="document">
+            <div class="modal-content">
+                <div class="modal-header">
+                    <h5 class="modal-title">关闭工单</h5>
+                    <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
+                </div>
+                <div class="modal-body">
+                    <div class="mb-3">
+                        <p>
+                            确认关闭工单?
+                        <p>
+                    </div>
+                </div>
+                <div class="modal-footer">
+                    <button type="button" class="btn me-auto" data-bs-dismiss="modal">取消</button>
+                    <button id="confirm_close" type="button" class="btn btn-primary" data-bs-dismiss="modal">确认</button>
+                </div>
+            </div>
+        </div>
+    </div>
 
-<script src="//cdn.jsdelivr.net/npm/[email protected]/editormd.min.js"></script>
-<script>
-    function changetouser_modal_show() {
-        $("#changetouser_modal").modal();
-    }
-    window.addEventListener('load', () => {
-        function submit() {
-            $("#result").modal();
-            $$.getElementById('msg').innerHTML = `正在提交。`;
+    <script>
+        $("#reply").click(function() {
             $.ajax({
-                type: "PUT",
-                url: "/admin/ticket/{$id}",
+                url: "/admin/ticket/{$ticket->id}",
+                type: 'PUT',
                 dataType: "json",
                 data: {
-                    content: editor.getHTML(),
-                    status
+                    comment: $('#reply-comment').val()
                 },
-                success: data => {
-                    if (data.ret) {
-                        $("#result").modal();
-                        $$.getElementById('msg').innerHTML = data.msg;
-                        window.setTimeout("location.href=top.document.referrer", {$config['jump_delay']});
+                success: function(data) {
+                    if (data.ret == 1) {
+                        $('#success-message').text(data.msg);
+                        $('#success-dialog').modal('show');
                     } else {
-                        $("#result").modal();
-                        $$.getElementById('msg').innerHTML = data.msg;
+                        $('#fail-message').text(data.msg);
+                        $('#fail-dialog').modal('show');
                     }
-                },
-                error: jqXHR => {
-                    $("#result").modal();
-                    $$.getElementById('msg').innerHTML = `发生错误:${
-                            jqXHR.status
-                            }`;
                 }
-            });
-        }
-        $$.getElementById('submit').addEventListener('click', () => {
-            status = 1;
-            submit();
-        });
-        $$.getElementById('close').addEventListener('click', () => {
-            status = 0;
-            submit();
+            })
         });
-        $$.getElementById('close_directly').addEventListener('click', () => {
-            status = 0;
-            $("#result").modal();
-            $$.getElementById('msg').innerHTML = `正在提交。`;
-            $.ajax({
-                type: "PUT",
-                url: "/admin/ticket/{$id}",
-                dataType: "json",
-                data: {
-                    content: '这条工单已被关闭',
-                    status
-                },
-                success: data => {
-                    if (data.ret) {
-                        $("#result").modal();
-                        $$.getElementById('msg').innerHTML = data.msg;
-                        window.setTimeout("location.href=top.document.referrer", {$config['jump_delay']});
-                    } else {
-                        $("#result").modal();
-                        $$.getElementById('msg').innerHTML = data.msg;
-                    }
-                },
-                error: jqXHR => {
-                    $("#result").modal();
-                    $$.getElementById('msg').innerHTML = `发生错误:${
-                            jqXHR.status
-                            }`;
-                }
-            });
-        });
-        function changetouser_id() {
+
+        $("#confirm_close").click(function() {
             $.ajax({
-                type: "POST",
-                url: "/admin/user/changetouser",
+                url: "/admin/ticket/{$ticket->id}/close",
+                type: 'PUT',
                 dataType: "json",
-                data: {
-                    userid: {$ticket->user()->id},
-                    adminid: {$user->id},
-                    local: '/admin/ticket/' + {$ticket->id} +'/view'
-                },
-                success: data => {
-                    if (data.ret) {
-                        $("#result").modal();
-                        $$.getElementById('msg').innerHTML = data.msg;
-                        window.setTimeout("location.href='/user'", {$config['jump_delay']});
+                success: function(data) {
+                    if (data.ret == 1) {
+                        $('#success-message').text(data.msg);
+                        $('#success-dialog').modal('show');
                     } else {
-                        $("#result").modal();
-                        $$.getElementById('msg').innerHTML = data.msg;
+                        $('#fail-message').text(data.msg);
+                        $('#fail-dialog').modal('show');
                     }
-                },
-                error: jqXHR => {
-                    $("#result").modal();
-                    $$.getElementById('msg').innerHTML = `发生错误:${
-                            jqXHR.status
-                            }`;
                 }
-            });
-        }
-        $$.getElementById('changetouser_input').addEventListener('click', () => {
-            changetouser_id();
-        });
-    });
-    (() => {
-        editor = editormd("editormd", {
-            path: "https://cdn.jsdelivr.net/npm/[email protected]/lib/", // Autoload modules mode, codemirror, marked... dependents libs path
-            height: 450,
-            saveHTMLToTextarea: true,
-            emoji: true
+            })
         });
 
-        /*
-        // or
-        var editor = editormd({
-            id   : "editormd",
-            path : "../lib/"
+        $("#success-confirm").click(function() {
+            location.reload();
         });
-        */
-    })();
-</script>
+    </script>
+
+{include file='admin/tabler_footer.tpl'}

+ 1 - 0
resources/views/tabler/user/detect_index.tpl

@@ -1,5 +1,6 @@
 {include file='user/tabler_header.tpl'}
 
+<!-- 审计规则是用来防止DMCA和Spam,不是用来给用户建墙用的,不要以为把“违法网站”墙了,被抓了能少判哪怕一天的刑期 -->
 <div class="page-wrapper">
     <div class="container-xl">
         <div class="page-header d-print-none text-white">

+ 72 - 110
resources/views/tabler/user/ticket/index.tpl

@@ -1,10 +1,10 @@
 {include file='user/tabler_header.tpl'}
 
 <div class="page-wrapper">
-    <div class="container-xl">       
+    <div class="container-xl">
         <div class="page-header d-print-none text-white">
             <div class="row align-items-center">
-                <div class="col">                    
+                <div class="col">
                     <h2 class="page-title" style="line-height: unset;">
                         <span class="home-title">工单列表</span>
                     </h2>
@@ -16,24 +16,12 @@
                     <div class="btn-list">
                         <a href="#" class="btn btn-primary d-none d-sm-inline-block" data-bs-toggle="modal"
                             data-bs-target="#create-ticket">
-                            <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-plus" width="24"
-                                height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"
-                                stroke-linecap="round" stroke-linejoin="round">
-                                <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
-                                <line x1="12" y1="5" x2="12" y2="19"></line>
-                                <line x1="5" y1="12" x2="19" y2="12"></line>
-                            </svg>
+                            <i class="icon ti ti-plus"></i>
                             创建工单
                         </a>
                         <a href="#" class="btn btn-primary d-sm-none btn-icon" data-bs-toggle="modal"
                             data-bs-target="#create-ticket">
-                            <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-plus" width="24"
-                                height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"
-                                stroke-linecap="round" stroke-linejoin="round">
-                                <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
-                                <line x1="12" y1="5" x2="12" y2="19"></line>
-                                <line x1="5" y1="12" x2="19" y2="12"></line>
-                            </svg>
+                            <i class="icon ti ti-plus"></i>
                         </a>
                     </div>
                 </div>
@@ -44,43 +32,67 @@
         <div class="container-xl">
             <div class="row row-deck row-cards">
                 <div class="col-12">
-                    <div class="card">
-                        {if $tickets->count() != '0'}
-                            <div class="table-responsive">
-                                <table id="data_table" class="table card-table table-vcenter text-nowrap datatable">
-                                    <thead>
-                                        <tr>
-                                            <th>#</th>
-                                            <th>操作</th>
-                                            <th>标题</th>
-                                            <th>状态</th>
-                                            <th>创建时间</th>
-                                            <th>最后更新</th>
-                                        </tr>
-                                    </thead>
-                                    <tbody>
-                                        {foreach $tickets as $ticket}
-                                            <tr>
-                                                <td>{$ticket->tk_id}</td>
-                                                <td>
-                                                    <a href="/user/ticket/{$ticket->tk_id}/view">浏览</a>
-                                                </td>
-                                                <td>{$ticket->title}</td>
-                                                <td>{$ticket->closed_by}</td>
-                                                <td>{$ticket->created_at}</td>
-                                                <td>{$ticket->updated_at}</td>
-                                            </tr>
-                                        {/foreach}
-                                    </tbody>
-                                </table>
-                            </div>
-                        {else}
-                            <div class="card">
-                                <div class="card-header">
-                                    <h3 class="card-title">没有任何工单</h3>
+                    <div class="row row-cards row-deck">
+                        {if $tickets !== 0}
+                            {foreach $tickets as $ticket}
+                                <div class="col-md-4 col-sm-12">
+                                    <div class="card">
+                                        <div class="card-body">
+                                            <div class="card-stamp">
+                                                {if $ticket->status !== 'closed'}
+                                                <div class="card-stamp-icon bg-yellow">
+                                                    <i class="ti ti-clock"></i>
+                                                </div>
+                                                {else}
+                                                <div class="card-stamp-icon bg-green">
+                                                    <i class="ti ti-check"></i>
+                                                </div>
+                                                {/if}
+                                            </div>
+                                            <h3 class="card-title" style="font-size: 20px;">
+                                                #{$ticket->id}
+                                            </h3>
+                                            <p class="text-muted text-truncate" style="height: 100px;">
+                                                {$ticket->title}
+                                            </p>
+                                        </div>
+                                        <div class="card-footer">
+                                            <div class="d-flex">
+                                                <!-- 工单状态标签 -->
+                                                {if $ticket->status === 'closed'}
+                                                <span class="status status-grey">已结单</span>
+                                                {/if}
+                                                {if $ticket->status === 'open_wait_user'}
+                                                <span class="status status-orange">等待用户回复</span>
+                                                {/if}
+                                                {if $ticket->status === 'open_wait_admin'}
+                                                <span class="status status-green">处理中</span>
+                                                {/if}
+                                                <!-- 工单类型标签 -->
+                                                {if $ticket->type === 'howto'}
+                                                <span class="status status-grey">使用</span>
+                                                {/if}
+                                                {if $ticket->type === 'billing'}
+                                                <span class="status status-grey">财务</span>
+                                                {/if}
+                                                {if $ticket->type === 'account'}
+                                                <span class="status status-grey">账户</span>
+                                                {/if}
+                                                <span class="status status-grey">其他</span>
+                                                <a href="/user/ticket/{$ticket->id}/view"
+                                                    class="btn btn-primary ms-auto">查看</a>
+                                            </div>
+                                        </div>
+                                    </div>
                                 </div>
-                                <div class="card-body">如需帮助,请点击右上角按钮开启新工单</div>
+                            {/foreach}
+                        {else}
+                        <div class="card">
+                            <div class="card-header">
+                                <h3 class="card-title">没有任何工单</h3>
                             </div>
+                            <div class="card-body">如需帮助,请点击右上角按钮开启新工单</div>
+                        </div>
                         {/if}
                     </div>
                 </div>
@@ -97,40 +109,20 @@
                 </div>
                 <div class="modal-body">
                     <div class="mb-3">
-                        <select id="ticket-client" class="form-select">
-                            <option value="0">请选择有问题的设备系统类型</option>
-                            <option value="reward_or_refund">提现或退款</option>
-                            <option value="Windows">Windows</option>
-                            <option value="Macos">Macos</option>
-                            <option value="Android">Android</option>
-                            <option value="IOS">IOS</option>
-                            <option value="Route">路由器</option>
-                            <option value="Linux">Linux</option>
-                            <option value="Other">其他</option>
+                        <select id="ticket-type" class="form-select">
+                            <option value="0">请选择工单类型</option>
+                            <option value="howto">使用</option>
+                            <option value="billing">财务</option>
+                            <option value="account">账户</option>
+                            <option value="other">其他</option>
                         </select>
                     </div>
                     <div class="mb-3">
                         <input id="ticket-title" type="text" class="form-control" placeholder="请输入工单主题">
                     </div>
                     <div class="mb-3">
-                        <textarea id="ticket-content" class="form-control" rows="12" placeholder="请输入工单内容"></textarea>
-                    </div>
-                    <div class="mb-3">
-                        <input id="associated-order" type="text" class="form-control" placeholder="退款请填写订单号;提现请填写金额">
-                    </div>
-                    <div class="mb-3">
-                        <input id="receiving-method" type="text" class="form-control" placeholder="请输入接收方式,如支付宝 / 微信">
+                        <textarea id="ticket-comment" class="form-control" rows="12" placeholder="请输入工单内容"></textarea>
                     </div>
-                    <div class="mb-3">
-                        <input id="receiving-account" type="text" class="form-control"
-                            placeholder="请输入接收方式账户,如手机号 / 邮箱 / 收款码图片链接">
-                    </div>
-                    <p>* 上传图片有助于帮助解决问题,请使用图床上传。可以前往
-                        <a target="view_window" href="https://www.imgurl.org/">
-                            imgurl.org
-                        </a>
-                    </p>
-                    <p>* 工单被回复时会邮件通知您</p>
                 </div>
                 <div class="modal-footer">
                     <button type="button" class="btn me-auto" data-bs-dismiss="modal">取消</button>
@@ -149,11 +141,8 @@
                 dataType: "json",
                 data: {
                     title: $('#ticket-title').val(),
-                    content: $('#ticket-content').val(),
-                    ticket_client: $('#ticket-client').val(),
-                    receiving_method: $('#receiving-method').val(),
-                    receiving_account: $('#receiving-account').val(),
-                    associated_order: $('#associated-order').val(),
+                    comment: $('#ticket-comment').val(),
+                    type: $('#ticket-type').val(),
                 },
                 success: function(data) {
                     if (data.ret == 1) {
@@ -170,33 +159,6 @@
         $("#success-confirm").click(function() {
             location.reload();
         });
-
-        $('#ticket-client').on('change', function() {
-            var type = $('#ticket-client').val();
-            if (type == 'reward_or_refund') {
-                $("#ticket-title").val('提现或退款');
-                $("#ticket-title").attr('disabled', true);
-                $("#ticket-content").hide();
-                $("#receiving-method").show();
-                $("#receiving-account").show();
-                $("#associated-order").show();
-            } else {
-                $("#ticket-content").show();
-                $("#receiving-method").hide();
-                $("#receiving-account").hide();
-                $("#associated-order").hide();
-                $("#ticket-title").val('');
-                $("#ticket-title").attr('disabled', false);
-            }
-        });
-
-        $("#receiving-method").hide();
-        $("#receiving-account").hide();
-        $("#associated-order").hide();
-
-        $("td:contains('开启中')").css("color", "green");
-        $("td:contains('管理员')").css("color", "purple");
-        $("td:contains('您')").css("color", "orange");
     </script>
 
 {include file='user/tabler_footer.tpl'}

+ 0 - 167
resources/views/tabler/user/ticket/read.tpl

@@ -1,167 +0,0 @@
-{include file='user/tabler_header.tpl'}
-
-<div class="page-wrapper">
-    <div class="container-xl">
-        <div class="page-header d-print-none text-white">
-            <div class="row align-items-center">
-                <div class="col">
-                    <h2 class="page-title" style="line-height: unset;">
-                        <span class="home-title">工单回复</span>
-                    </h2>
-                    <div class="page-pretitle">
-                        <span class="home-subtitle">你可以在这里查看历史消息并添加回复</span>
-                    </div>
-                </div>
-                <div class="col-auto ms-auto d-print-none">
-                    <div class="btn-list">
-                        <a href="#" class="btn btn-primary d-none d-sm-inline-block" data-bs-toggle="modal"
-                            data-bs-target="#add-reply">
-                            <svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24"
-                                viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"
-                                stroke-linecap="round" stroke-linejoin="round">
-                                <path stroke="none" d="M0 0h24v24H0z" fill="none" />
-                                <line x1="12" y1="5" x2="12" y2="19" />
-                                <line x1="5" y1="12" x2="19" y2="12" />
-                            </svg>
-                            添加回复
-                        </a>
-                        <a href="#" class="btn btn-primary d-sm-none btn-icon" data-bs-toggle="modal"
-                            data-bs-target="#add-reply" aria-label="Create new report">
-                            <svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24"
-                                viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"
-                                stroke-linecap="round" stroke-linejoin="round">
-                                <path stroke="none" d="M0 0h24v24H0z" fill="none" />
-                                <line x1="12" y1="5" x2="12" y2="19" />
-                                <line x1="5" y1="12" x2="19" y2="12" />
-                            </svg>
-                        </a>
-                    </div>
-                </div>
-            </div>
-        </div>
-    </div>
-    <div class="page-body">
-        <div class="container-xl">
-            <div class="row row-cards">
-                <div class="col-12">
-                    <div class="card">
-                        <div class="card-body">
-                            <div class="h1 my-2 mb-3">#{$topic->tk_id} {$topic->title}</div>
-                        </div>
-                    </div>
-                </div>
-            </div>
-            <div class="row justify-content-center my-3">
-                <div class="col-12">
-                    <div class="card">
-                        <div class="card-body">
-                            <div class="divide-y">
-                                {$count = '0'}
-                                {$total = $discussions->count()}
-                                {foreach $discussions as $discuss}
-                                    <div>
-                                        <div class="row">
-                                            {if $discuss->user_id == $user->id}
-                                                <div class="col-auto">
-                                                    <span class="avatar">用户</span>
-                                                </div>
-                                            {else}
-                                                <div class="col-auto">
-                                                    <span class="avatar"
-                                                        style="background-image: url(/theme/tabler/static/admin.png)"></span>
-                                                </div>
-                                            {/if}
-                                            <div class="col">
-                                                <div>
-                                                    {nl2br($discuss->content)}
-                                                </div>
-                                                <div class="text-muted my-1">{$discuss->created_at}</div>
-                                            </div>
-                                            <!-- 标记最新回复 -->
-                                            {$count = $count + 1}
-                                            {if $count == $total}
-                                                <div class="col-auto align-self-center">
-                                                    <div class="badge bg-primary"></div>
-                                                </div>
-                                            {/if}
-                                        </div>
-                                    </div>
-                                    {if $count == $total && $topic->getOriginal('closed_by') != null}
-                                        <div>
-                                            <div class="row">
-                                                <div class="col-auto">
-                                                    <span class="avatar"
-                                                        style="background-image: url(/theme/tabler/static/warning.png)"></span>
-                                                </div>
-                                                <div class="col">
-                                                    <div>
-                                                        此主题帖已关闭,如有需要请创建新工单
-                                                    </div>
-                                                    <div class="text-muted my-1">
-                                                        {date('Y-m-d H:i:s', $topic->getOriginal('closed_at'))}
-                                                    </div>
-                                                </div>
-                                            </div>
-                                        </div>
-                                    {/if}
-                                {/foreach}
-                            </div>
-                        </div>
-                    </div>
-                </div>
-            </div>
-        </div>
-    </div>
-
-    <div class="modal modal-blur fade" id="add-reply" tabindex="-1" role="dialog" aria-hidden="true">
-        <div class="modal-dialog modal-dialog-centered modal-dialog-scrollable" role="document">
-            <div class="modal-content">
-                <div class="modal-header">
-                    <h5 class="modal-title">添加回复</h5>
-                    <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
-                </div>
-                <div class="modal-body">
-                    <div class="mb-3">
-                        <textarea id="reply-content" class="form-control" rows="10" placeholder="请输入回复内容"></textarea>
-                    </div>
-                    <p>* 上传图片有助于帮助解决问题,请使用图床上传。可以前往
-                        <a target="view_window" href="https://www.imgurl.org/">
-                            imgurl.org
-                        </a>
-                    </p>
-                </div>
-                <div class="modal-footer">
-                    <button type="button" class="btn me-auto" data-bs-dismiss="modal">取消</button>
-                    <button id="reply" type="button" class="btn btn-primary" data-bs-dismiss="modal">回复</button>
-                </div>
-            </div>
-        </div>
-    </div>
-
-    <script>
-        $("#reply").click(function() {
-            $.ajax({
-                url: "/user/ticket/{$topic->tk_id}",
-                type: 'PUT',
-                dataType: "json",
-                data: {
-                    content: $('#reply-content').val()
-                },
-                success: function(data) {
-                    if (data.ret == 1) {
-                        $('#success-message').text(data.msg);
-                        $('#success-dialog').modal('show');
-                    } else {
-                        $('#fail-message').text(data.msg);
-                        $('#fail-dialog').modal('show');
-                    }
-                }
-            })
-        });
-
-        $("#success-confirm").click(function() {
-            location.reload();
-        });
-    </script>
-
-{include file='user/tabler_footer.tpl'}

+ 120 - 0
resources/views/tabler/user/ticket/view.tpl

@@ -0,0 +1,120 @@
+{include file='user/tabler_header.tpl'}
+
+<div class="page-wrapper">
+    <div class="container-xl">
+        <div class="page-header d-print-none text-white">
+            <div class="row align-items-center">
+                <div class="col">
+                    <h2 class="page-title" style="line-height: unset;">
+                        <span class="home-title">工单记录</span>
+                    </h2>
+                    <div class="page-pretitle">
+                        <span class="home-subtitle">你可以在这里查看历史消息并添加回复</span>
+                    </div>
+                </div>
+                {if $ticket->status !== 'closed'}
+                <div class="col-auto ms-auto d-print-none">
+                    <div class="btn-list">
+                        <a href="#" class="btn btn-primary d-none d-sm-inline-block" data-bs-toggle="modal"
+                            data-bs-target="#add-reply">
+                            <i class="icon ti ti-plus"></i>
+                            添加回复
+                        </a>
+                        <a href="#" class="btn btn-primary d-sm-none btn-icon" data-bs-toggle="modal"
+                            data-bs-target="#add-reply" aria-label="Create new report">
+                            <i class="icon ti ti-plus"></i>
+                        </a>
+                    </div>
+                </div>
+                {/if}
+            </div>
+        </div>
+    </div>
+    <div class="page-body">
+        <div class="container-xl">
+            <div class="row row-cards">
+                <div class="col-12">
+                    <div class="card">
+                        <div class="card-body">
+                            <div class="h1 my-2 mb-3">#{$ticket->id} {$ticket->title}</div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+            <div class="row justify-content-center my-3">
+                <div class="col-12">
+                    <div class="card">
+                        <div class="card-body">
+                            <div class="divide-y">
+                                {foreach $comments as $comment}
+                                <div>
+                                    <div class="row">
+                                        <div class="col">
+                                            <div>
+                                                {nl2br($comment['comment'])}
+                                            </div>
+                                            <div class="text-muted my-1">{$comment['commenter_name']} 回复于 {Tools::toDateTime($comment['datetime'])}</div>
+                                        </div>
+                                        <div class="col-auto">
+                                            <div>
+                                                # {$comment['comment_id'] + 1}
+                                            </div>
+                                        </div>
+                                    </div>
+                                </div>
+                                {/foreach}
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+
+    <div class="modal modal-blur fade" id="add-reply" tabindex="-1" role="dialog" aria-hidden="true">
+        <div class="modal-dialog modal-dialog-centered modal-dialog-scrollable" role="document">
+            <div class="modal-content">
+                <div class="modal-header">
+                    <h5 class="modal-title">添加回复</h5>
+                    <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
+                </div>
+                <div class="modal-body">
+                    <div class="mb-3">
+                        <textarea id="reply-comment" class="form-control" rows="10" placeholder="请输入回复内容"></textarea>
+                    </div>
+                </div>
+                <div class="modal-footer">
+                    <button type="button" class="btn me-auto" data-bs-dismiss="modal">取消</button>
+                    <button id="reply" type="button" class="btn btn-primary" data-bs-dismiss="modal">回复</button>
+                </div>
+            </div>
+        </div>
+    </div>
+
+    <script>
+        $("#reply").click(function() {
+            $.ajax({
+                url: "/user/ticket/{$ticket->id}",
+                type: 'PUT',
+                dataType: "json",
+                data: {
+                    comment: $('#reply-comment').val()
+                },
+                success: function(data) {
+                    if (data.ret == 1) {
+                        $('#success-message').text(data.msg);
+                        $('#success-dialog').modal('show');
+                    } else {
+                        $('#fail-message').text(data.msg);
+                        $('#fail-dialog').modal('show');
+                    }
+                }
+            })
+        });
+
+        $("#success-confirm").click(function() {
+            location.reload();
+        });
+    </script>
+    
+{include file='user/tabler_footer.tpl'}

+ 85 - 121
src/Controllers/Admin/TicketController.php

@@ -7,7 +7,6 @@ namespace App\Controllers\Admin;
 use App\Controllers\BaseController;
 use App\Models\Ticket;
 use App\Models\User;
-use App\Utils\ResponseHelper;
 use App\Utils\Tools;
 use Slim\Http\Request;
 use Slim\Http\Response;
@@ -15,6 +14,18 @@ use voku\helper\AntiXSS;
 
 final class TicketController extends BaseController
 {
+    public static $details =
+    [
+        'field' => [
+            'id' => 'ID',
+            'title' => '主题',
+            'status' => '工单状态',
+            'type' => '工单类型',
+            'userid' => '提交用户',
+            'datetime' => '创建时间',
+        ],
+    ];
+
     /**
      * 后台工单页面
      *
@@ -22,112 +33,65 @@ final class TicketController extends BaseController
      */
     public function index(Request $request, Response $response, array $args)
     {
+        $tickets = Ticket::orderBy('id', 'desc')->get();
+
         return $response->write(
             $this->view()
-                ->assign('table_config', ResponseHelper::buildTableConfig([
-                    'op' => '操作',
-                    'id' => 'ID',
-                    'datetime' => '时间',
-                    'title' => '标题',
-                    'userid' => '用户ID',
-                    'user_name' => '用户名',
-                    'status' => '状态',
-                ], 'ticket/ajax'))
+                ->assign('tickets', $tickets)
+                ->assign('details', self::$details)
+                ->registerClass('Tools', Tools::class)
                 ->display('admin/ticket/index.tpl')
         );
     }
 
     /**
-     * 後臺創建新工單
+     * 后台 更新工单内容
      *
      * @param array     $args
      */
-    public function add(Request $request, Response $response, array $args)
+    public function update(Request $request, Response $response, array $args)
     {
-        $title = $request->getParam('title');
-        $content = $request->getParam('content');
-        $userid = $request->getParam('userid');
-        if ($title === '' || $content === '') {
+        $id = $args['id'];
+        $comment = $request->getParam('comment');
+
+        if ($comment === '') {
             return $response->withJson([
                 'ret' => 0,
                 'msg' => '非法输入',
             ]);
         }
-        if (strpos($content, 'admin') !== false || strpos($content, 'user') !== false) {
-            return $response->withJson([
-                'ret' => 0,
-                'msg' => '请求中有不当词语',
-            ]);
+
+        $ticket = Ticket::where('id', $id)->first();
+
+        if ($ticket === null) {
+            return $response->withStatus(302)->withHeader('Location', '/admin/ticket');
         }
 
-        $ticket = new Ticket();
         $antiXss = new AntiXSS();
-        $ticket->title = $antiXss->xss_clean($title);
-        $ticket->content = $antiXss->xss_clean($content);
-        $ticket->rootid = 0;
-        $ticket->userid = $userid;
-        $ticket->datetime = \time();
-        $ticket->save();
 
-        $user = User::find($userid);
-        $user->sendMail(
-            $_ENV['appName'] . '-新管理员工单被开启',
-            'news/warn.tpl',
+        $content_old = \json_decode($ticket->content, true);
+        $content_new = [
             [
-                'text' => '管理员开启了新的工单,请您及时访问用户面板处理。',
+                'comment_id' => $content_old[count($content_old) - 1]['comment_id'] + 1,
+                'commenter_name' => 'Admin',
+                'comment' => $antiXss->xss_clean($comment),
+                'datetime' => \time(),
             ],
-            []
-        );
+        ];
 
-        return $response->withJson([
-            'ret' => 1,
-            'msg' => '提交成功',
-        ]);
-    }
-
-    /**
-     * 后台 更新工单内容
-     *
-     * @param array     $args
-     */
-    public function update(Request $request, Response $response, array $args)
-    {
-        $id = $args['id'];
-        $content = $request->getParam('content');
-        $status = $request->getParam('status');
-        if ($content === '' || $status === '') {
-            return $response->withJson([
-                'ret' => 0,
-                'msg' => '请填全',
-            ]);
-        }
-        if (strpos($content, 'admin') !== false || strpos($content, 'user') !== false) {
-            return $response->withJson([
-                'ret' => 0,
-                'msg' => '请求中有不正当的词语。',
-            ]);
-        }
-        $main = Ticket::find($id);
-        $user = User::find($main->userid);
+        $user = User::find($ticket->userid);
         $user->sendMail(
             $_ENV['appName'] . '-工单被回复',
             'news/warn.tpl',
             [
-                'text' => '您好,有人回复了<a href="' . $_ENV['baseUrl'] . '/user/ticket/' . $main->id . '/view">工单</a>,请您查看。',
+                'text' => '您好,有人回复了<a href="' . $_ENV['baseUrl'] . '/user/ticket/' . $ticket->id . '/view">工单</a>,请您查看。',
             ],
             []
         );
 
-        $antiXss = new AntiXSS();
-        $ticket = new Ticket();
-        $ticket->title = $antiXss->xss_clean($main->title);
-        $ticket->content = $antiXss->xss_clean($content);
-        $ticket->rootid = $main->id;
-        $ticket->userid = $this->user->id;
-        $ticket->datetime = \time();
+        $ticket->content = \json_encode(\array_merge($content_old, $content_new));
+        $ticket->status = 'open_wait_user';
         $ticket->save();
-        $main->status = $status;
-        $main->save();
 
         return $response->withJson([
             'ret' => 1,
@@ -140,74 +104,74 @@ final class TicketController extends BaseController
      *
      * @param array     $args
      */
-    public function show(Request $request, Response $response, array $args)
+    public function ticketView(Request $request, Response $response, array $args)
     {
         $id = $args['id'];
         $ticket = Ticket::where('id', '=', $id)->first();
+        $comments = \json_decode($ticket->content, true);
+
         if ($ticket === null) {
             return $response->withStatus(302)->withHeader('Location', '/user/ticket');
         }
 
-        $pageNum = $request->getQueryParams()['page'] ?? 1;
-        $ticketset = Ticket::where('id', $id)->orWhere('rootid', '=', $id)->orderBy('datetime', 'desc')->paginate(5, ['*'], 'page', $pageNum);
-
-        $render = Tools::paginateRender($ticketset);
         return $response->write(
             $this->view()
-                ->assign('ticketset', $ticketset)
-                ->assign('id', $id)
-                ->assign('render', $render)
+                ->assign('ticket', $ticket)
+                ->assign('comments', $comments)
+                ->registerClass('Tools', Tools::class)
                 ->display('admin/ticket/view.tpl')
         );
     }
 
     /**
-     * 后台工单页面 AJAX
+     * 后台 关闭工单
      *
      * @param array     $args
      */
-    public function ajax(Request $request, Response $response, array $args)
+    public function close(Request $request, Response $response, array $args)
     {
-        $query = Ticket::getTableDataFromAdmin(
-            $request,
-            static function (&$order_field): void {
-                if (\in_array($order_field, ['op'])) {
-                    $order_field = 'id';
-                }
-                if (\in_array($order_field, ['user_name'])) {
-                    $order_field = 'userid';
-                }
-            },
-            static function ($query): void {
-                $query->where('rootid', 0);
-            }
-        );
+        $id = $args['id'];
+        $ticket = Ticket::where('id', '=', $id)->first();
 
-        $data = [];
-        foreach ($query['datas'] as $value) {
-            /** @var Ticket $value */
-
-            if ($value->user() === null) {
-                Ticket::userIsNull($value);
-                continue;
-            }
-            $tempdata = [];
-            $tempdata['op'] = '<a class="btn btn-brand" href="/admin/ticket/' . $value->id . '/view">查看</a>';
-            $tempdata['id'] = $value->id;
-            $tempdata['datetime'] = $value->datetime();
-            $tempdata['title'] = $value->title;
-            $tempdata['userid'] = $value->userid;
-            $tempdata['user_name'] = $value->userName();
-            $tempdata['status'] = $value->status();
-
-            $data[] = $tempdata;
+        if ($ticket->status === 'closed') {
+            return $response->withJson([
+                'ret' => 0,
+                'msg' => '工单已关闭',
+            ]);
         }
 
+        $user = User::find($ticket->userid);
+        $user->sendMail(
+            $_ENV['appName'] . '-工单已被关闭',
+            'news/warn.tpl',
+            [
+                'text' => '您好,您的工单 #'. $ticket->id .' 已被关闭,如果您还有问题,欢迎提交新的工单。',
+            ],
+            []
+        );
+
+        $ticket->status = 'closed';
+        $ticket->save();
+
         return $response->withJson([
-            'draw' => $request->getParam('draw'),
-            'recordsTotal' => Ticket::count(),
-            'recordsFiltered' => $query['count'],
-            'data' => $data,
+            'ret' => 1,
+            'msg' => '关闭成功',
+        ]);
+    }
+
+    /**
+     * 后台 删除工单
+     *
+     * @param array     $args
+     */
+    public function delete(Request $request, Response $response, array $args)
+    {
+        $id = $args['id'];
+        Ticket::where('id', '=', $id)->delete();
+
+        return $response->withJson([
+            'ret' => 1,
+            'msg' => '删除成功',
         ]);
     }
 }

+ 79 - 115
src/Controllers/User/TicketController.php

@@ -26,8 +26,7 @@ final class TicketController extends BaseController
         if ($_ENV['enable_ticket'] !== true) {
             return null;
         }
-        $pageNum = $request->getQueryParams()['page'] ?? 1;
-        $tickets = Ticket::where('userid', $this->user->id)->where('rootid', 0)->orderBy('datetime', 'desc')->paginate(15, ['*'], 'page', $pageNum);
+        $tickets = Ticket::where('userid', $this->user->id)->orderBy('datetime', 'desc')->get();
 
         if ($request->getParam('json') === 1) {
             return $response->withJson([
@@ -35,24 +34,11 @@ final class TicketController extends BaseController
                 'tickets' => $tickets,
             ]);
         }
-        $render = Tools::paginateRender($tickets);
 
         return $response->write(
             $this->view()
                 ->assign('tickets', $tickets)
-                ->assign('render', $render)
-                ->display('user/ticket.tpl')
-        );
-    }
-
-    /**
-     * @param array     $args
-     */
-    public function ticketCreate(Request $request, Response $response, array $args): ResponseInterface
-    {
-        return $response->write(
-            $this->view()
-                ->display('user/ticket_create.tpl')
+                ->display('user/ticket/index.tpl')
         );
     }
 
@@ -62,25 +48,34 @@ final class TicketController extends BaseController
     public function ticketAdd(Request $request, Response $response, array $args): ResponseInterface
     {
         $title = $request->getParam('title');
-        $content = $request->getParam('content');
-        $markdown = $request->getParam('markdown');
-        if ($title === '' || $content === '') {
+        $comment = $request->getParam('comment');
+        if ($title === '' || $comment === '') {
             return $response->withJson([
                 'ret' => 0,
                 'msg' => '非法输入',
             ]);
         }
 
-        $ticket = new Ticket();
         $antiXss = new AntiXSS();
+
+        $content = [
+            [
+                'comment_id' => 0,
+                'commenter_name' => $this->user->user_name,
+                'comment' => $antiXss->xss_clean($comment),
+                'datetime' => \time(),
+            ],
+        ];
+
+        $ticket = new Ticket();
         $ticket->title = $antiXss->xss_clean($title);
-        $ticket->content = $antiXss->xss_clean($content);
-        $ticket->rootid = 0;
+        $ticket->content = \json_encode($content);
         $ticket->userid = $this->user->id;
         $ticket->datetime = \time();
+        $ticket->status = 'open_wait_admin';
         $ticket->save();
 
-        if ($_ENV['mail_ticket'] === true && $markdown !== '') {
+        if ($_ENV['mail_ticket'] === true) {
             $adminUser = User::where('is_admin', 1)->get();
             foreach ($adminUser as $user) {
                 $user->sendMail(
@@ -93,11 +88,11 @@ final class TicketController extends BaseController
                 );
             }
         }
-        if ($_ENV['useScFtqq'] === true && $markdown !== '') {
+        if ($_ENV['useScFtqq'] === true) {
             $ScFtqq_SCKEY = $_ENV['ScFtqq_SCKEY'];
             $postdata = http_build_query([
                 'text' => $_ENV['appName'] . '-新工单被开启',
-                'desp' => $markdown,
+                'desp' => $title,
             ]);
             $opts = [
                 'http' => [
@@ -122,93 +117,67 @@ final class TicketController extends BaseController
     public function ticketUpdate(Request $request, Response $response, array $args): ResponseInterface
     {
         $id = $args['id'];
-        $content = $request->getParam('content');
-        $status = $request->getParam('status');
-        $markdown = $request->getParam('markdown');
-        if ($content === '' || $status === '') {
+        $comment = $request->getParam('comment');
+
+        if ($comment === '') {
             return $response->withJson([
                 'ret' => 0,
                 'msg' => '非法输入',
             ]);
         }
-        $ticket_main = Ticket::where('id', $id)->where('userid', $this->user->id)->where('rootid', 0)->first();
-        if ($ticket_main === null) {
+
+        $ticket = Ticket::where('id', $id)->where('userid', $this->user->id)->first();
+
+        if ($ticket === null) {
             return $response->withStatus(302)->withHeader('Location', '/user/ticket');
         }
-        if ($status === 1 && $ticket_main->status !== $status) {
-            if ($_ENV['mail_ticket'] === true && $markdown !== '') {
-                $adminUser = User::where('is_admin', '=', '1')->get();
-                foreach ($adminUser as $user) {
-                    $user->sendMail(
-                        $_ENV['appName'] . '-工单被重新开启',
-                        'news/warn.tpl',
-                        [
-                            'text' => '管理员,有人重新开启了<a href="' . $_ENV['baseUrl'] . '/admin/ticket/' . $ticket_main->id . '/view">工单</a>,请您及时处理。',
-                        ],
-                        []
-                    );
-                }
-            }
-            if ($_ENV['useScFtqq'] === true && $markdown !== '') {
-                $ScFtqq_SCKEY = $_ENV['ScFtqq_SCKEY'];
-                $postdata = http_build_query([
-                    'text' => $_ENV['appName'] . '-工单被重新开启',
-                    'desp' => $markdown,
-                ]);
-                $opts = [
-                    'http' => [
-                        'method' => 'POST',
-                        'header' => 'Content-type: application/x-www-form-urlencoded',
-                        'content' => $postdata,
-                    ],
-                ];
-                $context = stream_context_create($opts);
-                file_get_contents('https://sctapi.ftqq.com/' . $ScFtqq_SCKEY . '.send', false, $context);
-            }
-        } else {
-            if ($_ENV['mail_ticket'] === true && $markdown !== '') {
-                $adminUser = User::where('is_admin', 1)->get();
-                foreach ($adminUser as $user) {
-                    $user->sendMail(
-                        $_ENV['appName'] . '-工单被回复',
-                        'news/warn.tpl',
-                        [
-                            'text' => '管理员,有人回复了<a href="' . $_ENV['baseUrl'] . '/admin/ticket/' . $ticket_main->id . '/view">工单</a>,请您及时处理。',
-                        ],
-                        []
-                    );
-                }
-            }
-            if ($_ENV['useScFtqq'] === true && $markdown !== '') {
-                $ScFtqq_SCKEY = $_ENV['ScFtqq_SCKEY'];
-                $postdata = http_build_query([
-                    'text' => $_ENV['appName'] . '-工单被回复',
-                    'desp' => $markdown,
-                ]);
-                $opts = [
-                    'http' => [
-                        'method' => 'POST',
-                        'header' => 'Content-type: application/x-www-form-urlencoded',
-                        'content' => $postdata,
-                    ],
-                ];
-                $context = stream_context_create($opts);
-                file_get_contents('https://sctapi.ftqq.com/' . $ScFtqq_SCKEY . '.send', false, $context);
-            }
-        }
 
         $antiXss = new AntiXSS();
-        $ticket = new Ticket();
-        $ticket->title = $antiXss->xss_clean($ticket_main->title);
-        $ticket->content = $antiXss->xss_clean($content);
-        $ticket->rootid = $ticket_main->id;
-        $ticket->userid = $this->user->id;
-        $ticket->datetime = \time();
-        $ticket_main->status = $status;
 
-        $ticket_main->save();
+        $content_old = \json_decode($ticket->content, true);
+        $content_new = [
+            [
+                'comment_id' => $content_old[count($content_old) - 1]['comment_id'] + 1,
+                'commenter_name' => $this->user->user_name,
+                'comment' => $antiXss->xss_clean($comment),
+                'datetime' => \time(),
+            ],
+        ];
+
+        $ticket->content = \json_encode(\array_merge($content_old, $content_new));
+        $ticket->status = 'open_wait_admin';
         $ticket->save();
 
+        if ($_ENV['mail_ticket'] === true) {
+            $adminUser = User::where('is_admin', 1)->get();
+            foreach ($adminUser as $user) {
+                $user->sendMail(
+                    $_ENV['appName'] . '-工单被回复',
+                    'news/warn.tpl',
+                    [
+                        'text' => '管理员,有人回复了<a href="' . $_ENV['baseUrl'] . '/admin/ticket/' . $ticket->id . '/view">工</a>,请您及时处理。',
+                    ],
+                    []
+                );
+            }
+        }
+        if ($_ENV['useScFtqq'] === true) {
+            $ScFtqq_SCKEY = $_ENV['ScFtqq_SCKEY'];
+            $postdata = http_build_query([
+                'text' => $_ENV['appName'] . '-工单被回复',
+                'desp' => $ticket->title,
+            ]);
+            $opts = [
+                'http' => [
+                    'method' => 'POST',
+                    'header' => 'Content-type: application/x-www-form-urlencoded',
+                    'content' => $postdata,
+                ],
+            ];
+            $context = stream_context_create($opts);
+            file_get_contents('https://sctapi.ftqq.com/' . $ScFtqq_SCKEY . '.send', false, $context);
+        }
+
         return $response->withJson([
             'ret' => 1,
             'msg' => '提交成功',
@@ -221,35 +190,30 @@ final class TicketController extends BaseController
     public function ticketView(Request $request, Response $response, array $args): ResponseInterface
     {
         $id = $args['id'];
-        $ticket_main = Ticket::where('id', '=', $id)->where('userid', $this->user->id)->where('rootid', '=', 0)->first();
-        if ($ticket_main === null) {
+        $ticket = Ticket::where('id', '=', $id)->where('userid', $this->user->id)->first();
+        $comments = \json_decode($ticket->content, true);
+
+        if ($ticket === null) {
             if ($request->getParam('json') === 1) {
                 return $response->withJson([
                     'ret' => 0,
-                    'msg' => '这不是你的工单!',
+                    'msg' => '无访问权限',
                 ]);
             }
             return $response->withStatus(302)->withHeader('Location', '/user/ticket');
         }
-        $pageNum = $request->getQueryParams()['page'] ?? 1;
-        $ticketset = Ticket::where('id', $id)->orWhere('rootid', '=', $id)->orderBy('datetime', 'desc')->paginate(5, ['*'], 'page', $pageNum);
         if ($request->getParam('json') === 1) {
-            foreach ($ticketset as $set) {
-                $set->username = $set->user()->user_name;
-                $set->datetime = $set->datetime();
-            }
             return $response->withJson([
                 'ret' => 1,
-                'tickets' => $ticketset,
+                'ticket' => $ticket,
             ]);
         }
-        $render = Tools::paginateRender($ticketset);
         return $response->write(
             $this->view()
-                ->assign('ticketset', $ticketset)
-                ->assign('id', $id)
-                ->assign('render', $render)
-                ->display('user/ticket_view.tpl')
+                ->assign('ticket', $ticket)
+                ->assign('comments', $comments)
+                ->registerClass('Tools', Tools::class)
+                ->display('user/ticket/view.tpl')
         );
     }
 }

+ 1 - 29
src/Models/Ticket.php

@@ -18,9 +18,8 @@ final class Ticket extends Model
      */
     public static function userIsNull(Ticket $Ticket): void
     {
-        $tickets = Ticket::where('userid', $Ticket->userid)->where('rootid', 0)->get();
+        $tickets = Ticket::where('userid', $Ticket->userid)->get();
         foreach ($tickets as $ticket) {
-            self::where('rootid', $ticket->id)->delete();
             $ticket->delete();
         }
     }
@@ -32,31 +31,4 @@ final class Ticket extends Model
     {
         return date('Y-m-d H:i:s', $this->datetime);
     }
-
-    /**
-     * 用户
-     */
-    public function user(): ?User
-    {
-        return User::find($this->userid);
-    }
-
-    /**
-     * 用户名
-     */
-    public function userName(): string
-    {
-        if ($this->user() === null) {
-            return '用户已不存在';
-        }
-        return $this->user()->user_name;
-    }
-
-    /**
-     * 工单状态
-     */
-    public function status(): string
-    {
-        return $this->status === 1 ? '开启' : '关闭';
-    }
 }

+ 1 - 1
src/Services/Captcha.php

@@ -48,7 +48,7 @@ final class Captcha
                         'content' => $postdata,
                     ],
                     ];
-                    $json = file_get_contents('https://challenges.cloudflare.com/turnstile/v0/siteverify', false, stream_context_create($opts));
+                    $json = @file_get_contents('https://challenges.cloudflare.com/turnstile/v0/siteverify', false, stream_context_create($opts));
                     $result = \json_decode($json)->success;
                 }
                 break;

+ 34 - 0
src/Utils/Tools.php

@@ -680,4 +680,38 @@ final class Tools
         }
         return null;
     }
+
+    /**
+     * 工单状态
+     */
+    public static function getTicketStatus($ticket)
+    {
+        if ($ticket->status === 'closed') {
+            return '已结单';
+        }
+        if ($ticket->status === 'open_wait_user') {
+            return '等待用户回复';
+        }
+        if ($ticket->status === 'open_wait_admin') {
+            return '进行中';
+        }
+        return '未知';
+    }
+
+    /**
+     * 工单类型
+     */
+    public static function getTicketType($ticket)
+    {
+        if ($ticket->type === 'howto') {
+            return '使用';
+        }
+        if ($ticket->type === 'billing') {
+            return '财务';
+        }
+        if ($ticket->type === 'account') {
+            return '账户';
+        }
+        return '其他';
+    }
 }