Browse Source

new ticket system

iamsaltedfish 3 năm trước cách đây
mục cha
commit
dae75b14a3

+ 5 - 4
app/routes.php

@@ -140,10 +140,11 @@ return function (SlimApp $app) {
 
         // 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->put('/ticket/{id}',              App\Controllers\Admin\TicketController::class . ':update');
-        $this->post('/ticket/ajax',             App\Controllers\Admin\TicketController::class . ':ajax');
+        $this->get('/ticket/{id}/view',         App\Controllers\Admin\TicketController::class . ':read');
+        $this->put('/ticket/{id}',              App\Controllers\Admin\TicketController::class . ':addReply');
+        $this->put('/ticket/{id}/close',        App\Controllers\Admin\TicketController::class . ':closeTk');
+        $this->post('/ticket/ajax',             App\Controllers\Admin\TicketController::class . ':ajaxQuery');
+        $this->delete('/ticket/{id}',           App\Controllers\Admin\TicketController::class . ':delete');
 
         // Shop Mange
         $this->get('/shop',                     App\Controllers\Admin\ShopController::class . ':index');

+ 1 - 1
config/.config.example.php

@@ -82,6 +82,7 @@ $_ENV['enable_expired_checkin'] = true; // 是否允许过期用户签到
 $_ENV['checkinMin'] = 100; // 签到可获得的最低流量(MB)
 $_ENV['checkinMax'] = 300; // 签到可获得的最多流量(MB)
 $_ENV['enable_ticket'] = true; // 是否开启工单系统
+$_ENV['mail_ticket'] = true; // 是否开启工单邮件提醒
 $_ENV['enable_docs'] = true; // 是否开启文档系统
 
 /*
@@ -92,7 +93,6 @@ $_ENV['sendPageLimit'] = 50; // 发信分页数
 $_ENV['email_queue'] = true; // 邮件队列开关
 $_ENV['mail_filter'] = 0; // 0关闭; 1白名单模式; 2黑名单模式
 $_ENV['mail_filter_list'] = ['qq.com', 'vip.qq.com', 'foxmail.com'];
-$_ENV['mail_ticket'] = true; // 是否开启工单邮件提醒
 $_ENV['notify_limit_mode'] = true; // false为关闭,per为按照百分比提醒,mb为按照固定剩余流量提醒
 $_ENV['notify_limit_value'] = 20; // 当上一项为per时,此处填写百分比;当上一项为mb时,此处填写流量
 

+ 33 - 0
databases/migrations/20220416033504_work_order_table.php

@@ -0,0 +1,33 @@
+<?php
+declare(strict_types=1);
+
+use Phinx\Migration\AbstractMigration;
+
+final class WorkOrderTable extends AbstractMigration
+{
+    /**
+     * Change Method.
+     *
+     * Write your reversible migrations using this method.
+     *
+     * More information on writing migrations is available here:
+     * https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
+     *
+     * Remember to call "create()" or "update()" and NOT "save()" when working
+     * with the Table class.
+     */
+    public function change(): void
+    {
+        $table = $this->table('work_order');
+        $table->addColumn('tk_id', 'integer', array('comment' => '围绕主题'))
+            ->addColumn('is_topic', 'integer', array('comment' => '是否是主题帖'))
+            ->addColumn('title', 'text', array('comment' => '主题帖标题', 'default' => null, 'null' => true))
+            ->addColumn('content', 'text', array('comment' => '围绕主题帖的回复内容'))
+            ->addColumn('user_id', 'integer', array('comment' => '提交用户'))
+            ->addColumn('created_at', 'integer', array('comment' => '创建时间'))
+            ->addColumn('updated_at', 'integer', array('comment' => '更新时间'))
+            ->addColumn('closed_at', 'integer', array('comment' => '关闭时间', 'default' => null, 'null' => true))
+            ->addColumn('closed_by', 'text', array('comment' => '关闭人', 'default' => null, 'null' => true))
+            ->create();
+    }
+}

BIN
public/theme/tabler/static/admin.png


BIN
public/theme/tabler/static/icons8-admin-64.png


BIN
public/theme/tabler/static/icons8-male-user-96.png


BIN
public/theme/tabler/static/technical-support.png


BIN
public/theme/tabler/static/user.png


+ 1 - 1
resources/email/news/warn.tpl

@@ -133,7 +133,7 @@
                                             <tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
                                                 <td style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 5px 0;"
                                                     valign="top">Hi, {$user->user_name}<br
-                                                            style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"/>邮箱: {$user->email}
+                                                            style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"/>
                                                 </td>
                                             </tr>
                                             <tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">

+ 322 - 68
resources/views/material/admin/ticket/index.tpl

@@ -1,92 +1,346 @@
-{include file='admin/main.tpl'}
-
-<main class="content">
-    <div class="content-header ui-content-header">
-        <div class="container">
-            <h1 class="content-heading">工单</h1>
+{include file='admin/tabler_admin_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">
+                        <span class="home-title">{$details['title']['title']}</span>
+                    </h2>
+                    <div class="page-pretitle">
+                        <span class="home-subtitle">
+                            {$details['title']['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="#search-dialog">
+                            <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-search"
+                                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>
+                                <circle cx="10" cy="10" r="7"></circle>
+                                <line x1="21" y1="21" x2="15" y2="15"></line>
+                            </svg>
+                            搜索
+                        </a>
+                        <a href="#" class="btn btn-primary d-sm-none btn-icon" data-bs-toggle="modal"
+                            data-bs-target="#search-dialog">
+                            <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-search"
+                                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>
+                                <circle cx="10" cy="10" r="7"></circle>
+                                <line x1="21" y1="21" x2="15" y2="15"></line>
+                            </svg>
+                        </a>
+                    </div>
+                </div>
+            </div>
         </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 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 $logs as $log}
+                                        <tr>
+                                            <td>
+                                                <a class="text-red" href="#" onclick="deleteItem('{$log->tk_id}')">删除</a>
+                                                <a class="text-orange" href="#" onclick="closeItem('{$log->tk_id}')">关闭</a>
+                                                <a class="text-primray" href="/admin/ticket/{$log->tk_id}/view">回复</a>
+                                            </td>
+                                            {foreach $details['field'] as $key => $value}
+                                                <td>{$log->$key}</td>
+                                            {/foreach}
+                                        </tr>
+                                    {/foreach}
+                                </tbody>
+                            </table>
                         </div>
                     </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>
+
+    <div class="modal modal-blur fade" id="search-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">
+                    {foreach $details['search_dialog'] as $from}
+                        {if $from['type'] == 'input'}
+                            <div class="form-group mb-3 row">
+                                <label class="form-label col-3 col-form-label">{$from['info']}</label>
+                                <div class="col">
+                                    <input id="search-{$from['id']}" type="text" class="form-control"
+                                        placeholder="{$from['placeholder']}">
+                                </div>
                             </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">
+                        {/if}
+                        {if $from['type'] == 'textarea'}
+                            <div class="form-group mb-3 row">
+                                <label class="form-label col-3 col-form-label">{$from['info']}</label>
+                                <textarea id="search-{$from['id']}" class="col form-control" rows="{$from['rows']}"
+                                    placeholder="{$from['placeholder']}"></textarea>
                             </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">
+                        {/if}
+                        {if $from['type'] == 'select'}
+                            <div class="form-group mb-3 row">
+                                <label class="form-label col-3 col-form-label">{$from['info']}</label>
+                                <select id="search-{$from['id']}" class="col form-select">
+                                    {foreach $from['select'] as $key => $value}
+                                        <option value="{$key}">{$value}</option>
+                                    {/foreach}
+                                </select>
+                            </div>
+                        {/if}
+                    {/foreach}
+                </div>
+                <div class="modal-footer">
+                    <button type="button" class="btn me-auto" data-bs-dismiss="modal">取消</button>
+                    <button id="submit-query" type="button" class="btn btn-primary" data-bs-dismiss="modal">搜索</button>
+                </div>
+            </div>
+        </div>
+    </div>
+
+    <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">
+                    <svg xmlns="http://www.w3.org/2000/svg" class="icon mb-2 text-green icon-lg" 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" />
+                        <circle cx="12" cy="12" r="9" />
+                        <path d="M9 12l2 2l4 -4" />
+                    </svg>
+                    <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 btn-success w-100" data-bs-dismiss="modal">
+                                    确认
+                                </a>
                             </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="icon">check</span>&nbsp;添加</a>
+                    </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">
+                    <svg xmlns="http://www.w3.org/2000/svg" class="icon mb-2 text-danger icon-lg" 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 d="M12 9v2m0 4v.01" />
+                        <path
+                            d="M5 19h14a2 2 0 0 0 1.84 -2.75l-7.1 -12.25a2 2 0 0 0 -3.5 0l-7.1 12.25a2 2 0 0 0 1.75 2.75" />
+                    </svg>
+                    <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 class="table-responsive">
-                    {include file='table/table.tpl'}
+            </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">
+                    <svg xmlns="http://www.w3.org/2000/svg" class="icon mb-2 text-yellow icon-lg" 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>
+                        <circle cx="12" cy="12" r="9"></circle>
+                        <line x1="12" y1="17" x2="12" y2="17.01"></line>
+                        <path d="M12 13.5a1.5 1.5 0 0 1 1 -1.5a2.6 2.6 0 1 0 -3 -4"></path>
+                    </svg>
+                    <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>
-                {include file='dialog.tpl'}
+            </div>
         </div>
     </div>
-</main>
 
-{include file='admin/footer.tpl'}
+    <script>
+        function adjustStyle() {
+            $("td:contains('开启中')").css("color", "green");
+            $("td:contains('null')").css("font-style", "italic");
+        }
+
+        function loadTable() {
+            $('#data_table').DataTable({
+                'iDisplayLength': 25,
+                'scrollX': true,
+                '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": ": 以降序排列此列"
+                    }
+                }
+            });
+        }
+
+        function deleteItem(id) {
+            item_id = id;
+            action = 'delete';
 
-<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() {
+            $('#fail-message').text('确定要删除此项么');
+            $('#fail-dialog').modal('show');
+        }
+
+        function closeItem(id) {
+            item_id = id;
+            action = 'close';
+
+            $('#notice-message').text('确定要关闭此工单么');
+            $('#notice-dialog').modal('show');
+        }
+
+        $("#submit-query").click(function() {
             $.ajax({
                 type: "POST",
-                url: "/admin/ticket",
+                url: "/admin/{$details['route']}/ajax",
                 dataType: "json",
                 data: {
-                    content: $$getValue('content'),
-                    title: $$getValue('title'),
-                    userid: $$getValue('userid')
-                },
-                success: data => {
-                    $("#result").modal();
-                    $$.getElementById('msg').innerHTML = data.msg;
+                    {foreach $details['search_dialog'] as $from}
+                        {$from['id']}: $('#search-{$from['id']}').val(),
+                    {/foreach}
                 },
-                error: jqXHR => {
-                    $("#result").modal();
-                    $$.getElementById('msg').innerHTML = `${ldelim}jqXHR{rdelim} 发生了错误。`;
+                success: function(data) {
+                    if (data.ret == 1) {
+                        var str = '';
+                        for (var i = 0; i < data.result.length; i++) {
+                            str += "<tr><td>" +
+                                '<a class=\"text-red\" href="#" onclick="deleteItem(' + data
+                                .result[i].id + ')">删除</a>' +
+                                "</td><td>" + data.result[i].id +
+                                {foreach $details['field'] as $key => $value}
+                                    {if $key != 'id'}
+                                        "</td><td>" + data.result[i].{$key} +
+                                    {/if}
+                                {/foreach} "</td></tr>";
+                        }
+                        $('#data_table').DataTable().destroy();
+                        $("#table_content").html(str);
+                        loadTable();
+                        adjustStyle();
+                    }
                 }
-            });
-        }
-        $$.getElementById('ticket_create').addEventListener('click', createTicket)
-    });
-</script>
+            })
+        });
+
+        $("#notice-confirm").click(function() {
+            if (action == 'delete') {
+                $.ajax({
+                    url: "/admin/{$details['route']}/" + item_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');
+                        }
+                    }
+                })
+            }
+            if (action == 'close') {
+                $.ajax({
+                    url: "/admin/{$details['route']}/" + item_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');
+                        }
+                    }
+                })
+            }
+        });
+
+        $("#success-confirm").click(function() {
+            location.reload();
+        });
+
+        loadTable();
+        adjustStyle();
+    </script>
+
+{include file='admin/tabler_admin_footer.tpl'}

+ 205 - 0
resources/views/material/admin/ticket/read.tpl

@@ -0,0 +1,205 @@
+{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">
+                        <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">
+                        <button 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>
+                            添加回复
+                        </button>
+                        <button href="#" class="btn btn-primary d-sm-none btn-icon" 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>
+                        </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">#{$topic->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 != '0'}
+                                                <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">{$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>
+                                {/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="12" 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>
+
+    <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">
+                    <svg xmlns="http://www.w3.org/2000/svg" class="icon mb-2 text-green icon-lg" 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" />
+                        <circle cx="12" cy="12" r="9" />
+                        <path d="M9 12l2 2l4 -4" />
+                    </svg>
+                    <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 btn-success 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">
+                    <svg xmlns="http://www.w3.org/2000/svg" class="icon mb-2 text-danger icon-lg" 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 d="M12 9v2m0 4v.01" />
+                        <path
+                            d="M5 19h14a2 2 0 0 0 1.84 -2.75l-7.1 -12.25a2 2 0 0 0 -3.5 0l-7.1 12.25a2 2 0 0 0 1.75 2.75" />
+                    </svg>
+                    <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>
+
+    <script>
+        $("#reply").click(function() {
+            $.ajax({
+                url: "/admin/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'}

+ 0 - 209
resources/views/material/admin/ticket/view.tpl

@@ -1,209 +0,0 @@
-{include file='admin/main.tpl'}
-<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/css/editormd.min.css"/>
-
-<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>
-                </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>
-                    </div>
-                </div>
-                <div class="card">
-                    <div class="card-main">
-                        <div class="card-inner">
-
-                            <div class="form-group">
-                                <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>
-                                </div>
-                            </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>
-                {/foreach}
-                {$render}
-                {include file='dialog.tpl'}
-        </div>
-    </div>
-</main>
-
-{include file='admin/footer.tpl'}
-
-<script src="https://cdn.staticfile.org/editor-md/1.5.0/editormd.min.js"></script>
-<script>
-    function changetouser_modal_show() {
-        $("#changetouser_modal").modal();
-    }
-    window.addEventListener('load', () => {
-        function submit() {
-            $("#result").modal();
-            $$.getElementById('msg').innerHTML = `正在提交。`;
-            $.ajax({
-                type: "PUT",
-                url: "/admin/ticket/{$id}",
-                dataType: "json",
-                data: {
-                    content: editor.getHTML(),
-                    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
-                            }`;
-                }
-            });
-        }
-        $$.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() {
-            $.ajax({
-                type: "POST",
-                url: "/admin/user/changetouser",
-                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']});
-                    } else {
-                        $("#result").modal();
-                        $$.getElementById('msg').innerHTML = data.msg;
-                    }
-                },
-                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/"
-        });
-        */
-    })();
-</script>

+ 0 - 111
resources/views/material/user/ticket/create.tpl

@@ -1,111 +0,0 @@
-{include file='user/main.tpl'}
-<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/css/editormd.min.css"/>
-
-<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="title">标题</label>
-                                <input class="form-control maxwidth-edit" id="title" type="text">
-                            </div>
-                        </div>
-                    </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="content">内容</label>
-                                <div id="editormd">
-                                    <textarea style="display:none;" id="content"></textarea>
-                                </div>
-                            </div>
-                        </div>
-                    </div>
-                </div>
-                <div class="card">
-                    <div class="card-main">
-                        <div class="card-inner">
-
-                            <div class="form-group">
-                                <div class="row">
-                                    <div class="col-md-10 col-md-push-1">
-                                        <button id="submit" type="submit" class="btn btn-block btn-brand">添加</button>
-                                    </div>
-                                </div>
-                            </div>
-                        </div>
-                    </div>
-                </div>
-                {include file='dialog.tpl'}
-            </section>
-        </div>
-    </div>
-</main>
-
-{include file='user/footer.tpl'}
-
-<script src="https://cdn.staticfile.org/editor-md/1.5.0/editormd.min.js"></script>
-<script>
-    $(document).ready(function () {
-        function submit() {
-            $("#result").modal();
-            $$.getElementById('msg').innerHTML = '正在提交...'
-            $.ajax({
-                type: "POST",
-                url: "/user/ticket",
-                dataType: "json",
-                data: {
-                    content: editor.getHTML(),
-                    markdown: $('.editormd-markdown-textarea').val(),
-                    title: $$getValue('title')
-                },
-                success: (data) => {
-                    if (data.ret) {
-                        $("#result").modal();
-                        $$.getElementById('msg').innerHTML = data.msg;
-                        window.setTimeout("location.href='/user/ticket'", {$config['jump_delay']});
-                    } else {
-                        $("#result").modal();
-                        $$.getElementById('msg').innerHTML = data.msg;
-                    }
-                },
-                error: (jqXHR) => {
-                    $("#msg-error").hide(10);
-                    $("#msg-error").show(100);
-                    $$.getElementById('msg-error-p').innerHTML = `发生错误:${
-                            jqXHR.status
-                            }`;
-                }
-            });
-        }
-        $("#submit").click(function () {
-            submit();
-        });
-    });
-    $(function () {
-        editor = editormd("editormd", {
-            path: "https://cdn.jsdelivr.net/npm/[email protected]/lib/", // Autoload modules mode, codemirror, marked... dependents libs path
-            height: 720,
-            saveHTMLToTextarea: true,
-            emoji: true
-        });
-        /*
-        // or
-        var editor = editormd({
-            id   : "editormd",
-            path : "../lib/"
-        });
-        */
-    });
-</script>

+ 207 - 72
resources/views/material/user/ticket/index.tpl

@@ -1,72 +1,207 @@
-{include file='user/main.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>
-                        </div>
-                    </div>
-                </div>
-                <div class="card">
-                    <div class="card-main">
-                        <div class="card-inner">
-                            <div class="card-table">
-                                <div class="table-responsive table-user">
-                                    {$render}
-                                    <table class="table">
-                                        <tr>
-                                            <th>发起日期</th>
-                                            <th>工单标题</th>
-                                            <th>工单状态</th>
-                                            <th>操作</th>
-                                        </tr>
-                                        {foreach $tickets as $ticket}
-                                            <tr>
-                                                <td>{$ticket->datetime()}</td>
-                                                <td>{$ticket->title}</td>
-                                                {if $ticket->status==1}
-                                                    <td>工单服务中</td>
-                                                {else}
-                                                    <td>工单已结束</td>
-                                                {/if}
-                                                <td>
-                                                    <a class="btn btn-brand" href="/user/ticket/{$ticket->id}/view">查看</a>
-                                                </td>
-                                            </tr>
-                                        {/foreach}
-                                    </table>
-                                    {$render}
-                                </div>
-                            </div>
-                        </div>
-                    </div>
-                </div>
-                <div class="card">
-                    <div class="card-main">
-                        <div class="card-inner">
-                            <div class="form-group">
-                                <div class="row">
-                                    <div class="col-md-10 col-md-push-1">
-                                        <a class="btn btn-block btn-brand waves-attach waves-light"
-                                            href="/user/ticket/create">创建新工单</a>
-                                    </div>
-                                </div>
-                            </div>
-                        </div>
-                    </div>
-                </div>
-            </section>
-        </div>
-    </div>
-</main>
-
-{include file='user/footer.tpl'}
+{include file='user/tabler_header.tpl'}
+<div class="page-wrapper">
+    <div class="container-xl">
+        <!-- Page title -->
+        <div class="page-header d-print-none text-white">
+            <div class="row align-items-center">
+                <div class="col">
+                    <!-- Page pre-title -->
+                    <h2 class="page-title">
+                        <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="#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>
+                            创建工单
+                        </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>
+                        </a>
+                    </div>
+                </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">
+                        {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>
+                                                    <a href="/user/ticket/{$ticket->tk_id}/view">浏览</a>
+                                                </td>
+                                                <td>{$ticket->tk_id}</td>
+                                                <td>{$ticket->title}</td>
+                                                <td>{$ticket->created_at}</td>
+                                                <td>{$ticket->updated_at}</td>
+                                                <td>{$ticket->closed_by}</td>
+                                            </tr>
+                                        {/foreach}
+                                    </tbody>
+                                </table>
+                            </div>
+                        {else}
+                            <div class="card">
+                                <div class="card-header">
+                                    <h3 class="card-title">没有任何工单</h3>
+                                </div>
+                                <div class="card-body">如需帮助,请点击右上角按钮开启新工单</div>
+                            </div>
+                        {/if}
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+
+    <div class="modal modal-blur fade" id="create-ticket" 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">
+                        <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>
+                    <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="create-ticket-button" type="button" class="btn btn-primary"
+                        data-bs-dismiss="modal">创建</button>
+                </div>
+            </div>
+        </div>
+    </div>
+
+    <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">
+                    <svg xmlns="http://www.w3.org/2000/svg" class="icon mb-2 text-green icon-lg" 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" />
+                        <circle cx="12" cy="12" r="9" />
+                        <path d="M9 12l2 2l4 -4" />
+                    </svg>
+                    <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">
+                    <svg xmlns="http://www.w3.org/2000/svg" class="icon mb-2 text-danger icon-lg" 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 d="M12 9v2m0 4v.01" />
+                        <path
+                            d="M5 19h14a2 2 0 0 0 1.84 -2.75l-7.1 -12.25a2 2 0 0 0 -3.5 0l-7.1 12.25a2 2 0 0 0 1.75 2.75" />
+                    </svg>
+                    <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>
+
+    <script>
+        $("#create-ticket-button").click(function() {
+            $.ajax({
+                type: "POST",
+                url: "/user/ticket",
+                dataType: "json",
+                data: {
+                    title: $('#ticket-title').val(),
+                    content: $('#ticket-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();
+        });
+
+        $("td:contains('开启中')").css("color", "green");
+    </script>
+
+{include file='user/tabler_footer.tpl'}

+ 205 - 154
resources/views/material/user/ticket/read.tpl

@@ -1,154 +1,205 @@
-{include file='user/main.tpl'}
-
-<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/css/editormd.min.css"/>
-<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>
-                </div>
-                <div class="card">
-                    <div class="card-main">
-                        <div class="card-inner">
-                            <div class="form-group">
-                                <div class="row">
-                                    <div class="col-md-10">
-                                        <button id="submit" type="submit" class="btn btn-brand">添加</button>
-                                        <button id="close" type="submit" class="btn btn-brand-accent">添加并关闭</button>
-                                        <button id="close_directly" type="submit" class="btn btn-brand-accent waves-attach waves-light">直接关闭</button>
-                                    </div>
-                                </div>
-                            </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>
-                {/foreach}
-                {$render}
-                {include file='dialog.tpl'}
-            </section>
-        </div>
-    </div>
-</main>
-
-{include file='user/footer.tpl'}
-
-<script src="https://cdn.staticfile.org/editor-md/1.5.0/editormd.min.js"></script>
-<script>
-    $(document).ready(function () {
-        function submit() {
-            $("#result").modal();
-            $$.getElementById('msg').innerHTML = '正在提交';
-            $.ajax({
-                type: "PUT",
-                url: "/user/ticket/{$id}",
-                dataType: "json",
-                data: {
-                    content: editor.getHTML(),
-                    markdown: editor.getMarkdown(),
-                    status
-                },
-                success: (data) => {
-                    if (data.ret) {
-                        $("#result").modal();
-                        $$.getElementById('msg').innerHTML = data.msg;
-                        window.setTimeout("location.href='/user/ticket'", {$config['jump_delay']});
-                    } else {
-                        $("#result").modal();
-                        $$.getElementById('msg').innerHTML = data.msg;
-                    }
-                },
-                error: (jqXHR) => {
-                    $("#msg-error").hide(10);
-                    $("#msg-error").show(100);
-                    $$.getElementById('msg-error-p').innerHTML = `发生错误:${
-                            jqXHR.status
-                            }`;
-                }
-            });
-        }
-        $("#submit").click(function () {
-            status = 1;
-            submit();
-        });
-        $("#close").click(function () {
-            status = 0;
-            submit();
-        });
-        $("#close_directly").click(function () {
-            status = 0;
-            $("#result").modal();
-            $$.getElementById('msg').innerHTML = '正在提交';
-            $.ajax({
-                type: "PUT",
-                url: "/user/ticket/{$id}",
-                dataType: "json",
-                data: {
-                    content: '这条工单已被关闭',
-                    status
-                },
-                success: (data) => {
-                    if (data.ret) {
-                        $("#result").modal();
-                        $$.getElementById('msg').innerHTML = data.msg;
-                        window.setTimeout("location.href='/user/ticket'", {$config['jump_delay']});
-                    } else {
-                        $("#result").modal();
-                        $$.getElementById('msg').innerHTML = data.msg;
-                    }
-                },
-                error: (jqXHR) => {
-                    $("#msg-error").hide(10);
-                    $("#msg-error").show(100);
-                    $$.getElementById('msg-error-p').innerHTML = `发生错误:${
-                        jqXHR.status
-                    }`;
-                }
-            });
-        });
-    });
-    $(function () {
-        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/"
-        });
-        */
-    });
-</script>
+{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">
+                        <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->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">{$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>
+                                {/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="8" 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>
+
+    <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">
+                    <svg xmlns="http://www.w3.org/2000/svg" class="icon mb-2 text-green icon-lg" 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" />
+                        <circle cx="12" cy="12" r="9" />
+                        <path d="M9 12l2 2l4 -4" />
+                    </svg>
+                    <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 btn-success 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">
+                    <svg xmlns="http://www.w3.org/2000/svg" class="icon mb-2 text-danger icon-lg" 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 d="M12 9v2m0 4v.01" />
+                        <path
+                            d="M5 19h14a2 2 0 0 0 1.84 -2.75l-7.1 -12.25a2 2 0 0 0 -3.5 0l-7.1 12.25a2 2 0 0 0 1.75 2.75" />
+                    </svg>
+                    <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>
+
+    <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'}

+ 189 - 182
src/Controllers/Admin/TicketController.php

@@ -1,232 +1,239 @@
 <?php
-
 namespace App\Controllers\Admin;
 
-use App\Controllers\AdminController;
-use App\Models\{
-    User,
-    Ticket
-};
+use App\Services\Auth;
+use Slim\Http\Request;
+use Slim\Http\Response;
 use App\Utils\Tools;
+use App\Services\Mail;
+use App\Models\User;
+use App\Models\Setting;
+use App\Models\WorkOrder;
 use voku\helper\AntiXSS;
-use Slim\Http\{
-    Request,
-    Response
-};
+use App\Controllers\AdminController;
 
 class TicketController extends AdminController
 {
-    /**
-     * 后台工单页面
-     *
-     * @param Request   $request
-     * @param Response  $response
-     * @param array     $args
-     */
+    public static function page(){
+        $details = [
+            'route' => 'ticket',
+            'title' => [
+                'title' => '工单列表',
+                'subtitle' => '所有用户提交的工单',
+            ],
+            'field' => [
+                'tk_id' => '#',
+                'title' => '主题',
+                'user_id' => '提交用户',
+                'created_at' => '创建时间',
+                'updated_at' => '更新时间',
+                'closed_at' => '关闭时间',
+                'closed_by' => '状态'
+            ],
+            'search_dialog' => [
+                [
+                    'id' => 'user_id',
+                    'info' => '提交用户',
+                    'type' => 'input',
+                    'placeholder' => '请输入',
+                    'exact' => true, // 精确匹配; false 时模糊匹配
+                ],
+                [
+                    'id' => 'title',
+                    'info' => '工单主题',
+                    'type' => 'input',
+                    'placeholder' => '请输入',
+                    'exact' => false,
+                ],
+                [
+                    'id' => 'content',
+                    'info' => '工单回复内容',
+                    'type' => 'input',
+                    'placeholder' => '请输入',
+                    'exact' => false,
+                ],
+                [
+                    'id' => 'closed_by',
+                    'info' => '工单状态',
+                    'type' => 'select',
+                    'select' => [
+                        'all' => '所有状态',
+                        'admin' => '被管理员关闭',
+                        'system' => '被系统关闭',
+                    ],
+                    'exact' => true,
+                ],
+            ],
+        ];
+
+        return $details;
+    }
+
     public function index($request, $response, $args)
     {
-        $table_config['total_column'] = array(
-            'op'        => '操作',
-            'id'        => 'ID',
-            'datetime'  => '时间',
-            'title'     => '标题',
-            'userid'    => '用户ID',
-            'user_name' => '用户名',
-            'status'    => '状态'
-        );
-        $table_config['default_show_column'] = array(
-            'op', 'id',
-            'datetime', 'title', 'userid', 'user_name', 'status'
-        );
-        $table_config['ajax_url'] = 'ticket/ajax';
+        $logs = WorkOrder::where('is_topic', 1)
+        ->orderBy('id', 'desc')
+        ->get();
+
         return $response->write(
             $this->view()
-                ->assign('table_config', $table_config)
+                ->assign('logs', $logs)
+                ->assign('details', self::page())
                 ->display('admin/ticket/index.tpl')
         );
     }
 
-    /**
-     * 後臺創建新工單
-     *
-     * @param Request   $request
-     * @param Response  $response
-     * @param array     $args
-     */
-    public function add($request, $response, $args)
+    public function read($request, $response, $args)
     {
-        $title    = $request->getParam('title');
-        $content  = $request->getParam('content');
-        $markdown = $request->getParam('markdown');
-        $userid   = $request->getParam('userid');
-        if ($title == '' || $content == '') {
-            return $response->withJson([
-                'ret' => 0,
-                'msg' => '非法输入'
-            ]);
-        }
-        if (strpos($content, 'admin') !== false || strpos($content, 'user') !== false) {
-            return $response->withJson([
-                'ret' => 0,
-                'msg' => '请求中有不当词语'
-            ]);
+        $tk_id = $args['id'];
+        $topic = WorkOrder::where('tk_id', $tk_id)
+        ->where('is_topic', '1')
+        ->first();
+
+        if ($topic == null) {
+            return null;
         }
 
-        $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();
+        $discussions = WorkOrder::where('tk_id', $tk_id)->get();
 
-        $user = User::find($userid);
-        $user->sendMail(
-            $_ENV['appName'] . '-新管理员工单被开启',
-            'news/warn.tpl',
-            [
-                'text' => '管理员开启了新的工单,请您及时访问用户面板处理。'
-            ],
-            []
+        return $response->write(
+            $this->view()
+                ->assign('topic', $topic)
+                ->assign('discussions', $discussions)
+                ->display('admin/ticket/read.tpl')
         );
-
-        return $response->withJson([
-            'ret' => 1,
-            'msg' => '提交成功'
-        ]);
     }
 
-    /**
-     * 后台 更新工单内容
-     *
-     * @param Request   $request
-     * @param Response  $response
-     * @param array     $args
-     */
-    public function update($request, $response, $args)
+    public function addReply($request, $response, $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) {
+        try {
+            $tk_id = $args['id'];
+            $ticket = WorkOrder::where('tk_id', $tk_id)->first();
+            if ($ticket == null) {
+                throw new \Exception('回复的主题帖不存在');
+            }
+            $topic = WorkOrder::where('tk_id', $tk_id)
+            ->where('is_topic', '1')
+            ->first();
+            if ($topic->closed_by == '已关闭') {
+                throw new \Exception('此主题帖已关闭');
+            }
+            $content = $request->getParam('content');
+            if ($content == '') {
+                throw new \Exception('请添加回复内容');
+            }
+
+            $anti_xss = new AntiXSS();
+            $ticket = new WorkOrder;
+            $ticket->tk_id = $tk_id;
+            $ticket->is_topic = 0;
+            $ticket->title = null;
+            $ticket->content = $anti_xss->xss_clean($content);
+            $ticket->user_id = 0; // 管理员的回复设为0
+            $ticket->created_at = time();
+            $ticket->updated_at = time();
+            $ticket->closed_at = null;
+            $ticket->closed_by = null;
+            $ticket->save();
+
+            $topic->updated_at = time();
+            $topic->save();
+        } catch (\Exception $e) {
             return $response->withJson([
                 'ret' => 0,
-                'msg' => '请求中有不正当的词语。'
+                'msg' => $e->getMessage()
             ]);
         }
-        $main = Ticket::find($id);
-        $user = User::find($main->userid);
-        $user->sendMail(
-            $_ENV['appName'] . '-工单被回复',
-            'news/warn.tpl',
-            [
-                'text' => '您好,有人回复了<a href="' . $_ENV['baseUrl'] . '/user/ticket/' . $main->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->save();
-        $main->status           = $status;
-        $main->save();
+        if ($_ENV['mail_ticket']) {
+            $anti_xss = new AntiXSS();
+            $user = User::find($topic->user_id);
+            $user->sendMail($_ENV['appName'] . ' - 工单被回复', 'news/warn.tpl',
+                [
+                    'text' => '工单主题:' . $anti_xss->xss_clean($topic->title) .
+                    '<br/>' . '新添回复:' . $anti_xss->xss_clean($content)
+                ], []
+            );
+        }
 
         return $response->withJson([
             'ret' => 1,
-            'msg' => '提交成功'
+            'msg' => '回复成功'
         ]);
     }
 
-    /**
-     * 后台 查看指定工单
-     *
-     * @param Request   $request
-     * @param Response  $response
-     * @param array     $args
-     */
-    public function show($request, $response, $args)
+    public function ajaxQuery($request, $response, $args)
     {
-        $id            = $args['id'];
-        $ticket = Ticket::where('id','=', $id)->first();
-        if($ticket == null) {
-            return $response->withStatus(302)->withHeader('Location', '/user/ticket');
+        $condition = [];
+        $details = self::page();
+        foreach ($details['search_dialog'] as $from)
+        {
+            $field = $from['id'];
+            $keyword = $request->getParam($field);
+            if ($from['type'] == 'input') {
+                if ($from['exact']) {
+                    ($keyword != '') && array_push($condition, [$field, '=', $keyword]);
+                } else {
+                    ($keyword != '') && array_push($condition, [$field, 'like', '%'.$keyword.'%']);
+                }
+            }
+            if ($from['type'] == 'select') {
+                ($keyword != 'all') && array_push($condition, [$field, '=', $keyword]);
+            }
         }
 
-        $pageNum       = $request->getQueryParams()['page'] ?? 1;
-        $ticketset     = Ticket::where('id', $id)->orWhere('rootid', '=', $id)->orderBy('datetime', 'desc')->paginate(5, ['*'], 'page', $pageNum);
-        $ticketset->setPath('/admin/ticket/' . $id . '/view');
+        $results = WorkOrder::orderBy('id', 'desc')
+        ->where($condition)
+        ->limit(500)
+        ->get();
 
-        $render = Tools::paginate_render($ticketset);
-        return $response->write(
-            $this->view()
-                ->assign('ticketset', $ticketset)
-                ->assign('id', $id)
-                ->assign('render', $render)
-                ->display('admin/ticket/view.tpl')
-        );
+        return $response->withJson([
+            'ret' => 1,
+            'result' => $results
+        ]);
     }
 
-    /**
-     * 后台工单页面 AJAX
-     *
-     * @param Request   $request
-     * @param Response  $response
-     * @param array     $args
-     */
-    public function ajax($request, $response, $args)
+    public function closeTk($request, $response, $args)
     {
-        $query = Ticket::getTableDataFromAdmin(
-            $request,
-            static function (&$order_field) {
-                if (in_array($order_field, ['op'])) {
-                    $order_field = 'id';
-                }
-                if (in_array($order_field, ['user_name'])) {
-                    $order_field = 'userid';
-                }
-            },
-            static function ($query) {
-                $query->where('rootid', 0);
-            }
-        );
+        $item_id = $args['id'];
+        $ticket = WorkOrder::where('is_topic', '1')
+        ->where('tk_id', $item_id)
+        ->first();
+        if ($ticket->closed_by == '已关闭') {
+            return $response->withJson([
+                'ret' => 0,
+                'msg' => '工单已是关闭状态'
+            ]);
+        }
 
-        $data  = [];
-        foreach ($query['datas'] as $value) {
-            /** @var Ticket $value */
+        $ticket->closed_at = time();
+        $ticket->closed_by = 'admin';
+        $ticket->save();
 
-            if ($value->user() == null) {
-                Ticket::user_is_null($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->user_name();
-            $tempdata['status']     = $value->status();
-
-            $data[] = $tempdata;
+        if ($_ENV['mail_ticket']) {
+            $anti_xss = new AntiXSS();
+            $user = User::find($ticket->user_id);
+            $user->sendMail($_ENV['appName'] . ' - 工单被关闭', 'news/warn.tpl',
+                [
+                    'text' => '工单主题:' . $anti_xss->xss_clean($ticket->title)
+                ], []
+            );
         }
 
         return $response->withJson([
-            'draw'            => $request->getParam('draw'),
-            'recordsTotal'    => Ticket::count(),
-            'recordsFiltered' => $query['count'],
-            'data'            => $data,
+            'ret' => 1,
+            'msg' => '已关闭此工单'
+        ]);
+    }
+
+    public function delete($request, $response, $args)
+    {
+        $item_id = $args['id'];
+        WorkOrder::where('tk_id', $item_id)->delete();
+
+        return $response->withJson([
+            'ret' => 1,
+            'msg' => '删除成功'
         ]);
     }
 }

+ 116 - 167
src/Controllers/User/TicketController.php

@@ -1,61 +1,37 @@
 <?php
-
 namespace App\Controllers\User;
 
-use App\Controllers\UserController;
-use App\Models\{
-    User,
-    Ticket
-};
+use App\Models\WorkOrder;
+use App\Models\User;
 use App\Utils\Tools;
+use Slim\Http\Request;
+use Slim\Http\Response;
 use voku\helper\AntiXSS;
-use Slim\Http\{
-    Request,
-    Response
-};
+use App\Controllers\UserController;
 use Psr\Http\Message\ResponseInterface;
 
-/**
- *  TicketController
- */
 class TicketController extends UserController
 {
-    /**
-     * @param Request   $request
-     * @param Response  $response
-     * @param array     $args
-     */
     public function ticket($request, $response, $args): ?ResponseInterface
     {
-        if ($_ENV['enable_ticket'] != true) {
+        if ($_ENV['enable_ticket'] = false) {
             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->setPath('/user/ticket');
 
-        if ($request->getParam('json') == 1) {
-            return $response->withJson([
-                'ret'     => 1,
-                'tickets' => $tickets
-            ]);
-        }
-        $render = Tools::paginate_render($tickets);
+        $tickets = WorkOrder::where('user_id', $this->user->id)
+        ->where('is_topic', 1)
+        ->orderBy('id', 'desc')
+        ->limit(20)
+        ->get();
 
         return $response->write(
             $this->view()
                 ->assign('tickets', $tickets)
-                ->assign('render', $render)
                 ->display('user/ticket/index.tpl')
         );
     }
 
-    /**
-     * @param Request   $request
-     * @param Response  $response
-     * @param array     $args
-     */
-    public function ticket_create($request, $response, $args): ResponseInterface
+    public function ticket_create($request, $response, $args)
     {
         return $response->write(
             $this->view()
@@ -63,170 +39,143 @@ class TicketController extends UserController
         );
     }
 
-    /**
-     * @param Request   $request
-     * @param Response  $response
-     * @param array     $args
-     */
-    public function ticket_add($request, $response, $args): ResponseInterface
+    public function ticket_add($request, $response, $args)
     {
-        $title    = $request->getParam('title');
-        $content  = $request->getParam('content');
-        $markdown = $request->getParam('markdown');
-        if ($title == '' || $content == '') {
-            return $response->withJson([
-                'ret' => 0,
-                'msg' => '非法输入'
-            ]);
-        }
-        if (strpos($content, 'admin') !== false || strpos($content, 'user') !== false) {
+        $title = $request->getParam('title');
+        $content = $request->getParam('content');
+
+        try {
+            if ($title == '') {
+                throw new \Exception('请填写工单标题');
+            }
+            if ($content == '') {
+                throw new \Exception('请填写工单内容');
+            }
+            if (strpos($content, 'admin') !== false || strpos($content, 'user') !== false) {
+                throw new \Exception('工单内容不能包含关键词 admin 和 user');
+            }
+
+            $last_tk_id = WorkOrder::where('is_topic', 1)->orderBy('id', 'desc')->first();
+
+            $anti_xss = new AntiXSS();
+            $ticket = new WorkOrder;
+            $ticket->tk_id = (empty($last_tk_id)) ? 1 : $last_tk_id->tk_id + 1;
+            $ticket->is_topic = 1;
+            $ticket->title = $anti_xss->xss_clean($title);
+            $ticket->content = $anti_xss->xss_clean($content);
+            $ticket->user_id = $this->user->id;
+            $ticket->created_at = time();
+            $ticket->updated_at = time();
+            $ticket->closed_at = null;
+            $ticket->closed_by = null;
+            $ticket->save();
+        } catch (\Exception $e) {
             return $response->withJson([
                 'ret' => 0,
-                'msg' => '请求中有不当词语'
+                'msg' => $e->getMessage()
             ]);
         }
 
-        $ticket           = new Ticket();
-        $antiXss          = new AntiXSS();
-        $ticket->title    = $antiXss->xss_clean($title);
-        $ticket->content  = $antiXss->xss_clean($content);
-        $ticket->rootid   = 0;
-        $ticket->userid   = $this->user->id;
-        $ticket->datetime = time();
-        $ticket->save();
-
-        if ($_ENV['mail_ticket'] == true && $markdown != '') {
-            $adminUser = User::where('is_admin', 1)->get();
-            foreach ($adminUser as $user) {
-                $user->sendMail(
-                    $_ENV['appName'] . '-新工单被开启',
-                    'news/warn.tpl',
+        if ($_ENV['mail_ticket']) {
+            $admins = User::where('is_admin', 1)->get();
+            foreach ($admins as $admin) {
+                $admin->sendMail($_ENV['appName'] . ' - 新的工单', 'news/warn.tpl',
                     [
-                        'text' => '管理员,有人开启了新的工单,请您及时处理。'
-                    ],
-                    []
+                        'text' => '新工单开启:' . $anti_xss->xss_clean($title)
+                    ], []
                 );
             }
         }
 
         return $response->withJson([
             'ret' => 1,
-            'msg' => '提交成功'
+            'msg' => '新工单已创建'
         ]);
     }
 
-    /**
-     * @param Request   $request
-     * @param Response  $response
-     * @param array     $args
-     */
-    public function ticket_update($request, $response, $args): ResponseInterface
+    public function ticket_update($request, $response, $args)
     {
-        $id       = $args['id'];
-        $content  = $request->getParam('content');
-        $status   = $request->getParam('status');
-        $markdown = $request->getParam('markdown');
-        if ($content == '' || $status == '') {
-            return $response->withJson([
-                'ret' => 0,
-                'msg' => '非法输入'
-            ]);
-        }
-        if (strpos($content, 'admin') !== false || strpos($content, 'user') !== false) {
+        try {
+            $tk_id = $args['id'];
+            $ticket = WorkOrder::where('tk_id', $tk_id)->first();
+            if ($ticket == null) {
+                throw new \Exception('回复的主题帖不存在');
+            }
+            $topic = WorkOrder::where('tk_id', $tk_id)
+            ->where('is_topic', '1')
+            ->first();
+            if ($topic->user_id != $this->user->id) {
+                throw new \Exception('此主题帖不属于你');
+            }
+            if ($topic->closed_by == '已关闭') {
+                throw new \Exception('此主题帖已关闭,如有需要请创建新工单');
+            }
+            $content = $request->getParam('content');
+            if ($content == '') {
+                throw new \Exception('请添加回复内容');
+            }
+            if (strpos($content, 'admin') !== false || strpos($content, 'user') !== false) {
+                throw new \Exception('回复内容不能包含关键词 admin 和 user');
+            }
+
+            $anti_xss = new AntiXSS();
+            $ticket = new WorkOrder;
+            $ticket->tk_id = $tk_id;
+            $ticket->is_topic = 0;
+            $ticket->title = null;
+            $ticket->content = $anti_xss->xss_clean($content);
+            $ticket->user_id = $this->user->id;
+            $ticket->created_at = time();
+            $ticket->updated_at = time();
+            $ticket->closed_at = null;
+            $ticket->closed_by = null;
+            $ticket->save();
+
+            $topic->updated_at = time();
+            $topic->save();
+        } catch (\Exception $e) {
             return $response->withJson([
                 'ret' => 0,
-                'msg' => '请求中有不当词语'
+                'msg' => $e->getMessage()
             ]);
         }
-        $ticket_main = Ticket::where('id', $id)->where('userid', $this->user->id)->where('rootid', 0)->first();
-        if ($ticket_main == 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>,请您及时处理。'
-                        ],
-                        []
-                    );
-                }
-            }
-        } 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['mail_ticket']) {
+            $admins = User::where('is_admin', 1)->get();
+            foreach ($admins as $admin) {
+                $admin->sendMail($_ENV['appName'] . ' - 用户工单回复', 'news/warn.tpl',
+                    [
+                        'text' => '工单主题:' . $anti_xss->xss_clean($topic->title) .
+                        '<br/>' . '新添回复:' . $anti_xss->xss_clean($content)
+                    ], []
+                );
             }
         }
 
-        $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();
-        $ticket->save();
-
         return $response->withJson([
             'ret' => 1,
-            'msg' => '提交成功'
+            'msg' => '回复成功'
         ]);
     }
 
-    /**
-     * @param Request   $request
-     * @param Response  $response
-     * @param array     $args
-     */
-    public function ticket_view($request, $response, $args): ResponseInterface
+    public function ticket_view($request, $response, $args)
     {
-        $id           = $args['id'];
-        $ticket_main  = Ticket::where('id', '=', $id)->where('userid', $this->user->id)->where('rootid', '=', 0)->first();
-        if ($ticket_main == null) {
-            if ($request->getParam('json') == 1) {
-                return $response->withJson([
-                    'ret' => 0,
-                    '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);
-        $ticketset->setPath('/user/ticket/' . $id . '/view');
-        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
-            ]);
+        $tk_id = $args['id'];
+        $topic = WorkOrder::where('tk_id', $tk_id)
+        ->where('is_topic', '1')
+        ->first();
+
+        if ($topic == null || $topic->user_id != $this->user->id) {
+            // 避免平级越权
+            return null;
         }
-        $render = Tools::paginate_render($ticketset);
+
+        $discussions = WorkOrder::where('tk_id', $tk_id)->get();
+
         return $response->write(
             $this->view()
-                ->assign('ticketset', $ticketset)
-                ->assign('id', $id)
-                ->assign('render', $render)
+                ->assign('topic', $topic)
+                ->assign('discussions', $discussions)
                 ->display('user/ticket/read.tpl')
         );
     }

+ 28 - 0
src/Models/WorkOrder.php

@@ -0,0 +1,28 @@
+<?php
+namespace App\Models;
+
+class WorkOrder extends Model
+{
+    protected $connection = 'default';
+    protected $table = 'work_order';
+
+    public function getCreatedAtAttribute($value)
+    {
+        return date('Y-m-d H:i:s', $value);
+    }
+
+    public function getUpdatedAtAttribute($value)
+    {
+        return date('Y-m-d H:i:s', $value);
+    }
+
+    public function getClosedAtAttribute($value)
+    {
+        return ($value == null) ? 'null' : date('Y-m-d H:i:s', $value);
+    }
+
+    public function getClosedByAttribute($value)
+    {
+        return ($value == null) ? '开启中' : '已关闭';
+    }
+}