refactor: 使用动态页面方式重构每次都需要重载的列表页面

This commit is contained in:
Emil Zhai 2022-12-04 18:07:16 +00:00
parent 7fbbbbd7f4
commit 0a2c0ee97e
No known key found for this signature in database
GPG Key ID: 780B385DB72F1EBD
11 changed files with 652 additions and 79 deletions

194
Access_Logs.php Normal file
View File

@ -0,0 +1,194 @@
<?php
if (!defined('__ACCESS_PLUGIN_ROOT__')) {
throw new Exception('Bootstrap file not found');
}
class Access_Logs {
/**
* 允许访问的业务函数名及其对应的允许访问方法
* @var string[]
*/
private static $rpcTypes = [
'get' => 'GET',
'delete' => 'POST',
];
private $config;
private $db;
private $request;
public function __construct($request) {
$this->db = Typecho_Db::get();
$this->request = $request;
$this->config = Typecho_Widget::widget('Widget_Options')->plugin('Access');
}
/**
* 创建过滤器对应的数据库查询语句
*
* @access private
* @return void
*/
private function filterQueryBuilder($query, $filters, $fuzzy)
{
$ids = array_key_exists('ids', $filters) ? $filters['ids'] : '';
$ip = array_key_exists('ip', $filters) ? $filters['ip'] : '';
$ua = array_key_exists('ua', $filters) ? $filters['ua'] : '';
$cid = array_key_exists('cid', $filters) ? $filters['cid'] : '';
$path = array_key_exists('path', $filters) ? $filters['path'] : '';
$robot = array_key_exists('robot', $filters) ? $filters['robot'] : '';
$compare = $fuzzy === '1' ? ' LIKE ?' : ' = ?';
$empty = $fuzzy ? '%' : '';
if (!empty($ids) && count($ids) > 0) {
$query->where(join(' OR ', array_fill(0, count($ids), 'id = ?')), ...$ids);
}
if ($ip !== $empty) {
$query->where('ip' . $compare, $ip);
}
if ($ua !== $empty) {
$query->where('ua' . $compare, $ua);
}
if ($cid !== $empty) {
$query->where('content_id' . $compare, $cid);
}
if ($path !== $empty) {
$query->where('path' . $compare, $path);
}
if ($robot !== $empty) {
$query->where('robot = ?', $robot);
}
}
/**
* 根据过滤器,获取详细访问日志数据
*
* @access private
* @return ?array
* @throws Exception
*/
public function get(): ?array
{
$resp = [];
$filters = array(
'ip' => $this->request->get('ip', ''),
'ua' => $this->request->get('ua', ''),
'cid' => $this->request->get('cid', ''),
'path' => $this->request->get('path', ''),
'robot' => $this->request->get('robot', ''),
);
$fuzzy = $this->request->get('fuzzy', '');
$pageSize = intval($this->config->pageSize);
$pageNum = intval($this->request->get('page', 1));
$counterQuery = $this->db->select('count(1) AS count')->from('table.access_logs');
$dataQuery = $this->db->select()->from('table.access_logs')
->order('time', Typecho_Db::SORT_DESC)
->offset((max(intval($pageNum), 1) - 1) * $pageSize)
->limit($pageSize);
$this->filterQueryBuilder($dataQuery, $filters, $fuzzy);
$this->filterQueryBuilder($counterQuery, $filters, $fuzzy);
$resp['count'] = $this->db->fetchAll($counterQuery)[0]['count'];
$resp['pagination'] = [
'size' => $pageSize,
'current' => $pageNum,
'total' => floor($resp['count'] / $pageSize),
];
$resp['logs'] = $this->db->fetchAll($dataQuery);
foreach ($resp['logs'] as &$row) {
$ua = new Access_UA($row['ua']);
if ($ua->isRobot()) {
$name = $ua->getRobotID();
$version = $ua->getRobotVersion();
} else {
$name = $ua->getBrowserName();
$version = $ua->getBrowserVersion();
}
if ($name == '') {
$row['display_name'] = _t('未知');
} elseif ($version == '') {
$row['display_name'] = $name;
} else {
$row['display_name'] = $name . ' / ' . $version;
}
if($row['ip_country'] == '中国') {
$row['ip_loc'] = "{$row['ip_province']} {$row['ip_city']}";
} else {
$row['ip_loc'] = $row['ip_country'];
}
}
return $resp;
}
/**
* 根据过滤器,删除详细访问日志数据
*
* @access private
* @return ?array
* @throws Exception
*/
public function delete(): ?array
{
$resp = [];
$counterQuery = $this->db->select('count(1) AS count')->from('table.access_logs');
$operatorQuery = $this->db->delete('table.access_logs');
$ids = $this->request->get('ids', '');
$ip = $this->request->get('ip', '');
$ua = $this->request->get('ua', '');
$cid = $this->request->get('cid', '');
$path = $this->request->get('path', '');
$robot = $this->request->get('robot', '');
if ($ids) {
$ids = Json::decode($ids, true);
if (!is_array($ids)) {
throw new Exception('Bad Request', 400);
}
$this->filterQueryBuilder($counterQuery, ['ids' => $ids], false);
$this->filterQueryBuilder($operatorQuery, ['ids' => $ids], false);
} else if ($ip || $ua || $cid || $path || $robot) {
$filters = array(
'ip' => $ip,
'ua' => $ua,
'cid' => $cid,
'path' => $path,
'robot' => $robot,
);
$fuzzy = $this->request->get('fuzzy', '');
$this->filterQueryBuilder($counterQuery, $filters, $fuzzy);
$this->filterQueryBuilder($operatorQuery, $filters, $fuzzy);
} else {
throw new Exception('Bad Request', 400);
}
$resp['count'] = $this->db->fetchAll($counterQuery)[0]['count'];
$this->db->query($operatorQuery);
return $resp;
}
/**
* 业务调度入口
*
* @access public
* @param string rpcType 调用过程类型
* @return ?array
* @throws Exception
*/
public function invoke(string $rpcType): ?array {
if(!method_exists($this, $rpcType) || !array_key_exists($rpcType, Access_Logs::$rpcTypes))
throw new Exception('Bad Request', 400);
$method = Access_Logs::$rpcTypes[$rpcType];
if (
($method === 'GET' && !$this->request->isGet())
|| ($method === 'POST' && !$this->request->isPost())
|| ($method === 'PUT' && !$this->request->isPut())
) {
throw new Exception('Method Not Allowed', 405);
}
return $this->$rpcType();
}
}

View File

@ -62,6 +62,27 @@ class Access_Action extends Typecho_Widget implements Widget_Interface_Do
}
}
public function logs() {
try {
$this->checkAuth(); # 鉴权
$rpcType = $this->request->get('rpc'); # 业务类型
$logs = new Access_Logs($this->request);
$data = $logs->invoke($rpcType); # 进行业务分发并调取数据
$errCode = 0;
$errMsg = 'ok';
} catch (Exception $e) {
$data = null;
$errCode = $e->getCode();
$errMsg = $e->getMessage();
}
$this->response->throwJson([
'code' => $errCode,
'message' => $errMsg,
'data' => $data
]);
}
public function statistic() {
try {
$this->checkAuth(); # 鉴权

View File

@ -25,6 +25,7 @@ class Access_Plugin implements Typecho_Plugin_Interface
Helper::addPanel(1, self::$panel, _t('Access控制台'), _t('Access插件控制台'), 'subscriber');
Helper::addRoute("access_track_gif", "/access/log/track.gif", "Access_Action", 'writeLogs');
Helper::addRoute("access_delete_logs", "/access/log/delete", "Access_Action", 'deleteLogs');
Helper::addRoute("access_logs", "/access/logs", "Access_Action", 'logs');
Helper::addRoute('access_statistic_view', '/access/statistic/view', 'Access_Action', 'statistic');
Typecho_Plugin::factory('Widget_Archive')->beforeRender = array('Access_Plugin', 'backend');
Typecho_Plugin::factory('Widget_Archive')->footer = array('Access_Plugin', 'frontend');

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,33 @@
/**
* Object.assign() - Polyfill
*
* @ref https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
*/
"use strict";
(function () {
if (typeof Object.assign != "function") {
(function () {
Object.assign = function (target) {
"use strict";
if (target === undefined || target === null) {
throw new TypeError("Cannot convert undefined or null to object");
}
var output = Object(target);
for (var index = 1; index < arguments.length; index++) {
var source = arguments[index];
if (source !== undefined && source !== null) {
for (var nextKey in source) {
if (source.hasOwnProperty(nextKey)) {
output[nextKey] = source[nextKey];
}
}
}
}
return output;
};
})();
}
})();

View File

@ -0,0 +1,79 @@
a[data-action="search-anchor"] {
cursor: pointer;
}
.typecho-access-logs-search {
position: relative;
}
.typecho-access-logs-filter {
position: absolute;
top: 100%;
right: 0;
background: #f6f6f3;
padding: 10px 20px;
margin-top: 2px;
border: 1px solid #e9e9e6;
width: 400px;
pointer-events: none;
opacity: 0;
transition: ease-in-out opacity 0.3s;
}
.typecho-access-logs-filter--visible {
pointer-events: unset;
opacity: 1;
}
.typecho-access-logs-filter-item {
margin: 5px 10px;
display: flex;
}
.typecho-access-logs-filter-item__label {
display: block;
flex: 0 0 auto;
width: 100px;
}
.typecho-access-logs-filter-item__content {
flex: 1 1 auto;
}
.typecho-access-logs-filter-apply {
padding: 15px 10px 10px;
display: flex;
justify-content: center;
}
.typecho-access-logs-filter-apply > *:not(:last-child) {
margin-right: 20px;
}
.typecho-access-logs-filter-apply__btn {
flex: 0 0 auto;
}
.typecho-access-logs-pagination-jump {
float: right;
display: flex;
align-items: center;
margin-left: 10px;
}
.typecho-access-logs-pagination-jump__number {
width: 50px;
text-align: center;
}
.typecho-access-logs-pagination-jump__text {
padding-left: 5px;
}
.typecho-access-logs-pagination-jump__total {
padding-left: 5px;
}
.typecho-access-logs-pagination-item {
cursor: pointer;
}

View File

@ -1,4 +1,240 @@
$(document).ready(function () {
var pageNum = 1;
function getPageNum() {
return pageNum;
}
function setPageNum(n) {
pageNum = Number.parseInt(n, 10) || 1;
}
function getFilters() {
return {
fuzzy: $('[name="filter-fuzzy"]').val(),
ua: $('[name="filter-ua"]').val(),
ip: $('[name="filter-ip"]').val(),
cid: $('[name="filter-cid"]').val(),
path: $('[name="filter-path"]').val(),
robot: $('[name="filter-robot"]').val(),
};
}
function setFilters(filters) {
$('[name="filter-fuzzy"]').val('fuzzy' in filters ? filters.fuzzy : '');
$('[name="filter-ua"]').val('ua' in filters ? filters.ua : '');
$('[name="filter-ip"]').val('ip' in filters ? filters.ip : '');
$('[name="filter-cid"]').val('cid' in filters ? filters.cid : '');
$('[name="filter-path"]').val('path' in filters ? filters.path : '');
$('[name="filter-robot"]').val('robot' in filters ? filters.robot : '');
}
function fetchLogs() {
var startTime = new Date().valueOf();
$('.typecho-list')
.loadingModal({ text: '正在获取数据...', backgroundColor: '#292d33' })
.loadingModal(
'animation',
[
'doubleBounce',
'rotatingPlane',
// 'wave',
// 'wanderingCubes',
'foldingCube',
][Math.floor(Math.random() * 3)]
)
.loadingModal('show');
$.ajax({
url: "/access/logs",
method: "get",
dataType: "json",
data: Object.assign(
{ },
getFilters(),
{ rpc: 'get', page: getPageNum() }
),
success: function (res) {
// make sure loading animation visible for better experience
var minDuring = 300;
var during = new Date().valueOf() - startTime;
if (during < minDuring) {
setTimeout(function() { $('.typecho-list').loadingModal('hide'); }, minDuring - during);
} else {
$('.typecho-list').loadingModal('hide');
}
if (res.code === 0) {
// logs list
var $tbody, $tr, $td;
$tbody = $('.typecho-list-table tbody');
$tbody.html('');
$.each(res.data.logs, function(index, item) {
$tr = $('<tr />', { id: item.id, 'data-id': item.id });
// id
$td = $('<td />');
$td.append($('<input />', {
type: 'checkbox',
value: item.id,
name: 'id[]',
'data-id': item.id,
}));
$tr.append($td);
// url
$td = $('<td />');
$td.append($('<a />', {
'data-action': 'search-anchor',
'data-filter': JSON.stringify({ path: item.path }),
}).text(item.url.replace(/%23/u, '#')));
$tr.append($td);
// ua
$td = $('<td />');
$td.append($('<a />', {
title: item.ua,
'data-action': 'search-anchor',
'data-filter': JSON.stringify({ ua: item.ua }),
}).text(item.display_name));
$tr.append($td);
// ip
$td = $('<td />');
$td.append($('<a />', {
'data-action': 'search-anchor',
'data-filter': JSON.stringify({ ip: item.ip }),
}).text(item.ip));
$tr.append($td);
// ip_loc
$td = $('<td />');
$td.append($('<span />').text(item.ip_loc));
$tr.append($td);
// referer
$td = $('<td />');
$td.append($('<a />', {
'data-action': 'search-anchor',
'data-filter': JSON.stringify({ referer: item.referer }),
}).text(item.referer));
$tr.append($td);
// time
$td = $('<td />');
$td.append($('<span />').text(dayjs(item.time * 1000).format('YYYY-MM-DD hh:mm:ss')));
$tr.append($td);
// append row to table body
$tbody.append($tr);
});
// logs pagination
$('a[data-action="search-anchor"]').click(onSearchAnchorClick);
var $pagination;
$pagination = $('.typecho-pager');
$pagination.html('');
var startPage, stopPage;
if (res.data.pagination.total <= 10 || res.data.pagination.current <= 5) {
startPage = 1;
stopPage = Math.min(res.data.pagination.total, res.data.pagination.current + 5);
} else if (res.data.pagination.total - res.data.pagination.current <= 5) {
startPage = res.data.pagination.total - 10;
stopPage = res.data.pagination.total;
} else {
startPage = res.data.pagination.current - 5;
stopPage = res.data.pagination.current + 5;
}
if (startPage > 1) {
$pagination.append(
$('<li />')
.append($('<a />', { class: 'typecho-access-logs-pagination-item', 'data-action': 'prev-page' })
.text('«')
.click(onPrevPage)
)
);
}
for (let index = startPage; index <= stopPage; index++) {
$pagination.append(
$('<li />', { class: index === res.data.pagination.current ? 'current' : '' })
.append(
$('<a />', {
class: 'typecho-access-logs-pagination-item',
'data-action': 'goto-page',
'data-page': index,
})
.text(index)
.click(onGotoPage)
)
);
}
if (stopPage < res.data.pagination.total) {
$pagination.append(
$('<li />')
.append($('<a />', { class: 'typecho-access-logs-pagination-item', 'data-action': 'next-page' })
.text('»')
.click(onNextPage)
)
);
}
$('input[name="page-jump"]').val(res.data.pagination.current);
$('.typecho-access-logs-pagination-jump__total').text(res.data.pagination.total);
} else {
swal({
icon: "error",
title: "错误",
text: "查询出错啦",
});
}
},
error: function (xhr, status, error) {
$('body').loadingModal('hide');
swal({
icon: "error",
title: "错误",
text: "请求错误 code: " + xhr.status,
});
},
});
}
function onSearchAnchorClick(e) {
setPageNum(1);
setFilters(JSON.parse(e.target.getAttribute('data-filter')));
$('button[data-action="apply"]').first().click();
}
function onPrevPage() {
setPageNum(getPageNum() - 1);
fetchLogs();
}
function onGotoPage(e) {
setPageNum(e.target.getAttribute('data-page'));
fetchLogs();
}
function onNextPage() {
setPageNum(getPageNum() + 1);
fetchLogs();
}
$('button[data-action="apply"]').click(function() {
fetchLogs();
$('.typecho-access-logs-filter').removeClass('typecho-access-logs-filter--visible');
});
$('button[data-action="reset"]').click(function() {
setPageNum(1);
setFilters({ robot: '0' });
fetchLogs();
$('.typecho-access-logs-filter').removeClass('typecho-access-logs-filter--visible');
});
$('button[data-action="switch-filter"]').click(function() {
$('.typecho-access-logs-filter').toggleClass('typecho-access-logs-filter--visible');
});
$('input[name="page-jump"]').on('keypress', function(e) {
if (e.which == 13) {
setPageNum(e.target.value);
fetchLogs();
}
});
$('a[data-action="ua"]').click(function () {
swal({
icon: "info",
@ -29,17 +265,19 @@ $(document).ready(function () {
});
if (ids.length != 0) {
$.ajax({
url: "/access/log/delete",
url: "/access/logs",
method: "post",
dataType: "json",
contentType: "application/json",
data: JSON.stringify(ids),
success: function (data) {
if (data.code == 0) {
data: {
rpc: 'delete',
ids: JSON.stringify(ids),
},
success: function (res) {
if (res.code == 0) {
swal({
icon: "success",
title: "删除成功",
text: "所选记录已删除",
text: "成功删除" + res.data.count + "条记录",
});
$.each(ids, function (index, elem) {
$('.typecho-list-table tbody tr[data-id="' + elem + '"]')
@ -116,4 +354,6 @@ $(document).ready(function () {
$form.find('button[type="button"]').on("click", function () {
$form.submit();
});
fetchLogs();
});

View File

@ -1,6 +1,5 @@
<div class="col-mb-12 typecho-list">
<div class="typecho-list-operate clearfix">
<div class="operate">
<label><i class="sr-only"><?php _e('全选'); ?></i><input type="checkbox" class="typecho-table-select-all" /></label>
<div class="btn-group btn-drop">
@ -11,44 +10,51 @@
</div>
</div>
<form method="get" class="search-form">
<div class="search" role="search">
<?php if ($request->get('filter', 'all') != 'all'): ?>
<a href="<?php $options->adminUrl('extending.php?panel=' . Access_Plugin::$panel . '&action=logs'); ?>"><?php _e('&laquo; 取消筛选'); ?></a>
<?php endif; ?>
<input type="hidden" value="<?= $request->get('panel'); ?>" name="panel" />
<?php if(isset($request->page)): ?>
<input type="hidden" value="<?= $request->get('page'); ?>" name="page" />
<?php endif; ?>
<select name="filter">
<option <?php if($request->filter == 'all'): ?> selected="true"<?php endif; ?>value="all"><?php _e('所有'); ?></option>
<option <?php if($request->filter == 'ip'): ?> selected="true"<?php endif; ?>value="ip"><?php _e('按IP'); ?></option>
<option <?php if($request->filter == 'post'): ?> selected="true"<?php endif; ?>value="post"><?php _e('按文章'); ?></option>
<option <?php if($request->filter == 'path'): ?> selected="true"<?php endif; ?>value="path"><?php _e('按路由'); ?></option>
</select>
<select style="<?php if(!in_array($request->get('filter', 'all'), ['ip', 'path'])): ?>display: none<?php endif; ?>" name="fuzzy">
<option <?php if($request->fuzzy != '1'): ?> selected="true"<?php endif; ?>value=""><?php _e('精确匹配'); ?></option>
<option <?php if($request->fuzzy == '1'): ?> selected="true"<?php endif; ?>value="1"><?php _e('模糊匹配'); ?></option>
</select>
<input style="<?php if($request->get('filter', 'all') != 'ip'): ?>display: none<?php endif; ?>" type="text" class="text-s" placeholder="" value="<?= htmlspecialchars($request->ip ?: ''); ?>" name="ip" />
<select style="<?php if($request->get('filter', 'all') != 'post'): ?>display: none<?php endif; ?>" name="cid">
<?php foreach ($access->logs['cidList'] as $content):?>
<option <?php if($request->cid == $content['cid']): ?> selected="true"<?php endif; ?>value="<?= $content['cid'];?>"><?= $content['title'];?> (<?= $content['count'];?>)</option>
<?php endforeach;?>
</select>
<input style="<?php if($request->get('filter', 'all') != 'path'): ?>display: none<?php endif; ?>" type="text" class="text-s" placeholder="" value="<?= htmlspecialchars($request->path ?: ''); ?>" name="path" />
<select name="type">
<option <?php if($request->type == 1): ?> selected="true"<?php endif; ?>value="1"><?php _e('默认(仅人类)'); ?></option>
<option <?php if($request->type == 2): ?> selected="true"<?php endif; ?>value="2"><?php _e('仅爬虫'); ?></option>
<option <?php if($request->type == 3): ?> selected="true"<?php endif; ?>value="3"><?php _e('所有'); ?></option>
</select>
<input type="hidden" name="page" value="1">
<button type="button" class="btn btn-s"><?php _e('筛选'); ?></button>
<div class="search typecho-access-logs-search" role="search">
<button data-action="apply" type="button" class="btn btn-s"><?php _e('刷新'); ?></button>
<button data-action="switch-filter" type="button" class="btn btn-s"><?php _e('筛选'); ?></button>
<div class="typecho-access-logs-filter">
<div class="typecho-access-logs-filter-item">
<label class="typecho-access-logs-filter-item__label">匹配方式</label>
<select class="typecho-access-logs-filter-item__content" name="filter-fuzzy">
<option value=""><?php _e('精确匹配'); ?></option>
<option value="1"><?php _e('模糊匹配'); ?></option>
</select>
</div>
<div class="typecho-access-logs-filter-item">
<label class="typecho-access-logs-filter-item__label">UA</label>
<input class="typecho-access-logs-filter-item__content" type="text" class="text-s" name="filter-ua" autocomplete="off" />
</div>
<div class="typecho-access-logs-filter-item">
<label class="typecho-access-logs-filter-item__label">IP</label>
<input class="typecho-access-logs-filter-item__content" type="text" class="text-s" name="filter-ip" autocomplete="off" />
</div>
<div class="typecho-access-logs-filter-item">
<label class="typecho-access-logs-filter-item__label">文章ID</label>
<select class="typecho-access-logs-filter-item__content" name="filter-cid">
<option value=""><?php _e('不限'); ?></option>
</select>
</div>
<div class="typecho-access-logs-filter-item">
<label class="typecho-access-logs-filter-item__label">受访地址</label>
<input class="typecho-access-logs-filter-item__content" type="text" class="text-s" name="filter-path" autocomplete="off" />
</div>
<div class="typecho-access-logs-filter-item">
<label class="typecho-access-logs-filter-item__label">访客类型</label>
<select class="typecho-access-logs-filter-item__content" name="filter-robot">
<option value="0"><?php _e('默认(仅人类)'); ?></option>
<option value="1"><?php _e('仅爬虫'); ?></option>
<option value=""><?php _e('所有'); ?></option>
</select>
</div>
<div class="typecho-access-logs-filter-apply">
<button class="btn btn-m typecho-access-logs-filter-apply__btn" data-action="reset" type="button"><?php _e('重 置'); ?></button>
<button class="btn btn-m typecho-access-logs-filter-apply__btn" data-action="apply" type="button"><?php _e('应 用'); ?></button>
</div>
</div>
</form>
</div>
</div>
<form method="post" class="operate-form">
<div class="typecho-table-wrap">
<table class="typecho-list-table">
<colgroup>
@ -56,9 +62,9 @@
<col width="28%"/>
<col width="25%"/>
<col width="18%"/>
<col width="16%"/>
<col width="14%"/>
<col width="20%"/>
<col width="20%"/>
<col width="18%"/>
</colgroup>
<thead>
<tr>
@ -72,50 +78,46 @@
</tr>
</thead>
<tbody>
<?php if(!empty($access->logs['list'])): ?>
<?php foreach ($access->logs['list'] as $log): ?>
<tr id="<?= $log['id']; ?>" data-id="<?= $log['id']; ?>">
<td><input type="checkbox" data-id="<?= $log['id']; ?>" value="<?= $log['id']; ?>" name="id[]"/></td>
<td><a target="_self" href="<?php $options->adminUrl('extending.php?panel=' . Access_Plugin::$panel . '&filter=path&path=' . $log['path'] . '&type='. $request->type); ?>"><?= urldecode(str_replace("%23", "#", $log['url'])); ?></a></td>
<td><a data-action="ua" href="#" title="<?= $log['ua'];?>"><?= $log['display_name']; ?></a></td>
<td><a data-action="ip" data-ip="<?= $log['ip'] ?>" href="<?php $options->adminUrl('extending.php?panel=' . Access_Plugin::$panel . '&filter=ip&ip=' . $log['ip'] . '&type='. $request->type); ?>"><?= $log['ip']; ?></td>
<td><?= $log['ip_loc'] ?></td>
<td><a target="_blank" data-action="referer" href="<?= $log['referer']; ?>"><?= $log['referer']; ?></a></td>
<td><?= date('Y-m-d H:i:s', $log['time']); ?></td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr>
<td colspan="6"><h6 class="typecho-list-table-title"><?php _e('当前无日志'); ?></h6></td>
<td colspan="7"><h6 class="typecho-list-table-title">loading</h6></td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
</form>
<div class="typecho-list-operate clearfix">
<form method="get">
<div class="operate">
<label><i class="sr-only"><?php _e('全选'); ?></i><input type="checkbox" class="typecho-table-select-all" /></label>
<div class="btn-group btn-drop">
<button class="btn dropdown-toggle btn-s" type="button"><i class="sr-only"><?php _e('操作'); ?></i><?php _e('选中项'); ?> <i class="i-caret-down"></i></button>
<ul class="dropdown-menu">
<li><a data-action="delete" href="javascript:;"><?php _e('删除'); ?></a></li>
</ul>
</div>
<div class="operate">
<label>
<i class="sr-only"><?php _e('全选'); ?></i>
<input type="checkbox" class="typecho-table-select-all" />
</label>
<div class="btn-group btn-drop">
<button class="btn dropdown-toggle btn-s" type="button">
<i class="sr-only"><?php _e('操作'); ?></i>
<span><?php _e('选中项'); ?></span>
<i class="i-caret-down"></i>
</button>
<ul class="dropdown-menu">
<li>
<a data-action="delete" href="javascript:;"><?php _e('删除'); ?></a>
</li>
</ul>
</div>
</div>
<?php if($access->logs['rows'] > 1): ?>
<ul class="typecho-pager">
<?= $access->logs['page']; ?>
</ul>
<?php endif; ?>
</form>
<div class="typecho-access-logs-pagination-jump">
<input class="text-s typecho-access-logs-pagination-jump__number" type="text" name="page-jump" autocomplete="off" />
<span class="typecho-access-logs-pagination-jump__text">/</span>
<span class="typecho-access-logs-pagination-jump__total">loading</span>
</div>
<ul class="typecho-pager"></ul>
</div>
</div>
<script src="<?php $options->pluginUrl('Access/page/sweetalert.min.js')?>"></script>
<script type="text/javascript" defer src="<?php $options->pluginUrl('Access/page/routes/logs/index.js')?>"></script>
<script src="<?php $options->pluginUrl('Access/page/components/object.assign/index.js')?>"></script>
<script src="<?php $options->pluginUrl('Access/page/components/sweetalert/index.js')?>"></script>
<script src="<?php $options->pluginUrl('Access/page/components/dayjs/index.js')?>"></script>
<link rel="stylesheet" href="<?php $options->pluginUrl('Access/page/components/loadingmodal/index.css')?>">
<script defer src="<?php $options->pluginUrl('Access/page/components/loadingmodal/index.js')?>"></script>
<link rel="stylesheet" href="<?php $options->pluginUrl('Access/page/routes/logs/index.css')?>">
<script defer src="<?php $options->pluginUrl('Access/page/routes/logs/index.js')?>"></script>