2.1 版本文件

This commit is contained in:
2026-08-14 10:55:06 +08:00
commit a3434ec620
67 changed files with 12559 additions and 0 deletions
+185
View File
@@ -0,0 +1,185 @@
<?php
// 前台:我的工单(列表 + 详情,支持多轮回复)。
require __DIR__ . '/includes/init.php';
requireLogin('mytickets.php');
$user = currentUser();
$pageTitle = __('mytickets.title');
$viewId = isset($_GET['view']) ? (int) $_GET['view'] : 0;
// 用户重新开启已关闭工单
if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST['reopen']) && $viewId > 0) {
verifyCsrf();
$stmt = db()->prepare('SELECT id, user_id, status FROM ' . tn('tickets') . ' WHERE id = ?');
$stmt->execute([$viewId]);
$t = $stmt->fetch();
if (!$t || (int) $t['user_id'] !== (int) $user['id']) {
$err = __('mytickets.err_no_perm');
} elseif ($t['status'] !== 'closed') {
$err = __('mytickets.err_not_closed');
} else {
db()->prepare('UPDATE ' . tn('tickets') . ' SET status = ?, updated_at = NOW(), closed_at = NULL WHERE id = ?')
->execute(['open', $viewId]);
redirect('mytickets.php?view=' . $viewId . '&ok=1');
}
}
// 用户追加回复
$err = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST['user_reply']) && $viewId > 0) {
verifyCsrf();
$reply = trim($_POST['reply_message'] ?? '');
$stmt = db()->prepare('SELECT id, user_id, status FROM ' . tn('tickets') . ' WHERE id = ?');
$stmt->execute([$viewId]);
$t = $stmt->fetch();
if (!$t || (int) $t['user_id'] !== (int) $user['id']) {
$err = __('mytickets.err_no_perm');
} elseif ($t['status'] === 'closed') {
$err = __('mytickets.err_closed_reply');
} elseif ($reply === '') {
$err = __('mytickets.err_reply_empty');
} elseif (strlen($reply) > 5000) {
$err = __('mytickets.err_reply_long');
} else {
db()->prepare('INSERT INTO ' . tn('ticket_replies') . ' (ticket_id, user_id, is_admin, message, created_at) VALUES (?,?,?,?,NOW())')
->execute([$viewId, $user['id'], 0, $reply]);
db()->prepare('UPDATE ' . tn('tickets') . ' SET status = ?, updated_at = NOW() WHERE id = ?')
->execute(['open', $viewId]);
redirect('mytickets.php?view=' . $viewId . '&ok=1');
}
}
$ticket = null;
$replies = [];
if ($viewId > 0) {
$stmt = db()->prepare(
'SELECT t.*, d.name AS dept_name FROM ' . tn('tickets') . ' t
LEFT JOIN ' . tn('ticket_departments') . ' d ON d.id = t.dept_id
WHERE t.id = ? AND t.user_id = ?'
);
$stmt->execute([$viewId, $user['id']]);
$ticket = $stmt->fetch();
if ($ticket) {
$rstmt = db()->prepare(
'SELECT r.*, u.username, u.nickname FROM ' . tn('ticket_replies') . ' r
LEFT JOIN ' . tn('users') . ' u ON u.id = r.user_id
WHERE r.ticket_id = ? ORDER BY r.created_at ASC'
);
$rstmt->execute([$viewId]);
$replies = $rstmt->fetchAll();
}
}
$list = db()->prepare(
'SELECT t.*, d.name AS dept_name FROM ' . tn('tickets') . ' t
LEFT JOIN ' . tn('ticket_departments') . ' d ON d.id = t.dept_id
WHERE t.user_id = ? ORDER BY t.updated_at DESC'
);
$list->execute([$user['id']]);
$list = $list->fetchAll();
require 'includes/header.php';
?>
<?php if (isset($_GET['ok'])): ?>
<p class="banner-ok" style="max-width:880px;margin:18px auto 0;"><i class="fas fa-check-circle"></i> <?= __('mytickets.ok_msg') ?></p>
<?php endif; ?>
<?php if ($err): ?>
<p class="form-err" style="max-width:880px;margin:18px auto 0;"><?= h($err) ?></p>
<?php endif; ?>
<?php if ($ticket): ?>
<section class="section">
<h2 class="sec-title"><i class="fas fa-ticket-alt"></i> <?= str_replace('{id}', (int)$ticket['id'], __('mytickets.ticket_detail')) ?></h2>
<div class="panel">
<div class="ticket-meta">
<span class="badge-status status-<?= h($ticket['status']) ?>"><?= ticketStatusLabel($ticket['status']) ?></span>
<span class="muted"><?= __('mytickets.meta_type') ?><?= ticketTypeLabel($ticket['type']) ?></span>
<span class="muted"><?= __('mytickets.meta_dept') ?><?= h($ticket['dept_name'] ?? __('mytickets.meta_unspecified')) ?></span>
<span class="muted"><?= __('mytickets.meta_priority') ?><?= ticketPriorityLabel($ticket['priority']) ?></span>
<span class="muted"><?= __('mytickets.meta_time') ?><?= date('Y-m-d H:i', strtotime($ticket['created_at'])) ?></span>
</div>
<?php if ($ticket['type'] === 'renewal' && $ticket['order_id']): ?>
<p class="ticket-order"><?= str_replace('{link}', '<a href="order_detail.php?id=' . (int)$ticket['order_id'] . '">#' . (int)$ticket['order_id'] . '</a>', __('mytickets.renewal_note')) ?></p>
<?php endif; ?>
<div class="ticket-timeline">
<div class="ticket-bubble user-bubble">
<div class="bubble-head"><i class="fas fa-user"></i> <?= __('mytickets.bubble_me') ?><?= date('Y-m-d H:i', strtotime($ticket['created_at'])) ?></div>
<h3 style="margin:8px 0 6px;font-size:1.05rem;"><?= h($ticket['subject']) ?></h3>
<div class="ticket-msg"><?= nl2br(h($ticket['message'])) ?></div>
</div>
<?php foreach ($replies as $r): ?>
<?php if ((int)$r['is_admin'] === 1): ?>
<div class="ticket-bubble admin-bubble">
<div class="bubble-head"><i class="fas fa-user-shield"></i> <?= __('mytickets.bubble_staff') ?><?= date('Y-m-d H:i', strtotime($r['created_at'])) ?></div>
<div class="ticket-msg"><?= nl2br(h($r['message'])) ?></div>
</div>
<?php else: ?>
<div class="ticket-bubble user-bubble">
<div class="bubble-head"><i class="fas fa-user"></i> <?= __('mytickets.bubble_me') ?><?= date('Y-m-d H:i', strtotime($r['created_at'])) ?></div>
<div class="ticket-msg"><?= nl2br(h($r['message'])) ?></div>
</div>
<?php endif; ?>
<?php endforeach; ?>
</div>
<?php if ($ticket['status'] !== 'closed'): ?>
<form method="post" action="" class="grid-form" style="margin-top:18px;">
<?= csrfField() ?>
<input type="hidden" name="user_reply" value="1">
<label class="span2"><?= __('mytickets.reply_label') ?>
<textarea name="reply_message" rows="4" autocomplete="off" placeholder="<?= __('mytickets.reply_placeholder') ?>" required></textarea>
</label>
<div class="form-actions span2">
<button type="submit" class="btn btn-primary"><i class="fas fa-paper-plane"></i> <?= __('mytickets.send_reply') ?></button>
<a href="mytickets.php" class="btn btn-ghost"><?= __('mytickets.back_list') ?></a>
</div>
</form>
<?php else: ?>
<p class="muted" style="margin-top:14px;"><i class="fas fa-lock"></i> <?= __('mytickets.closed_lock') ?></p>
<form method="post" action="" style="margin-top:10px;display:inline-block;">
<?= csrfField() ?>
<input type="hidden" name="reopen" value="1">
<button type="submit" class="btn btn-primary"><i class="fas fa-undo"></i> <?= __('mytickets.reopen_btn') ?></button>
<a href="mytickets.php" class="btn btn-ghost"><i class="fas fa-arrow-left"></i> <?= __('mytickets.back_list') ?></a>
</form>
<?php endif; ?>
</div>
</section>
<?php else: ?>
<section class="section">
<h2 class="sec-title"><i class="fas fa-ticket-alt"></i> <?= __('mytickets.list_title') ?></h2>
<div style="margin-bottom:14px;">
<a href="ticket.php" class="btn btn-primary"><i class="fas fa-plus"></i> <?= __('mytickets.new_ticket') ?></a>
</div>
<?php if (empty($list)): ?>
<p class="empty"><?= __('mytickets.no_tickets') ?></p>
<?php else: ?>
<div class="panel">
<table class="data-table">
<thead><tr><th><?= __('mytickets.col_id') ?></th><th><?= __('mytickets.col_subject') ?></th><th><?= __('mytickets.col_type') ?></th><th><?= __('mytickets.col_dept') ?></th><th><?= __('mytickets.col_priority') ?></th><th><?= __('mytickets.col_status') ?></th><th><?= __('mytickets.col_updated') ?></th><th></th></tr></thead>
<tbody>
<?php foreach ($list as $t): ?>
<tr>
<td>#<?= (int) $t['id'] ?></td>
<td><?= h($t['subject']) ?></td>
<td><?= ticketTypeLabel($t['type']) ?></td>
<td><?= h($t['dept_name'] ?? __('mytickets.meta_unspecified')) ?></td>
<td><?= ticketPriorityLabel($t['priority']) ?></td>
<td><span class="badge-status status-<?= h($t['status']) ?>"><?= ticketStatusLabel($t['status']) ?></span></td>
<td><?= date('m-d H:i', strtotime($t['updated_at'])) ?></td>
<td><a href="mytickets.php?view=<?= (int) $t['id'] ?>" class="mini-btn"><?= __('mytickets.view_btn') ?></a></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</section>
<?php endif; ?>
<?php require 'includes/footer.php'; ?>