phpMyAdmin页面虽然好用但是关闭开启很麻烦,如果一直挂在公网很不安全,所以我就做了个网站页面管理服务器里的数据库,需要的可以拿去用。
代码;
<?php
declare(strict_types=1);
session_start();
// ========== 请修改访问密钥(浏览器首次访问需带 ?key= 或表单填写) ==========
const ACCESS_KEY = '7758';
// 可选:仅允许这些 IP 访问(留空 [] 表示不限制)
const ALLOW_IPS = [];
const SQL_RESULT_MAX_ROWS = 500;
const SQL_BATCH_MAX_STATEMENTS = 50;
const BROWSE_PAGE_SIZE = 50;
function clientIp(): string
{
foreach (['HTTP_CF_CONNECTING_IP', 'HTTP_X_FORWARDED_FOR', 'REMOTE_ADDR'] as $k) {
if (empty($_SERVER[$k])) {
continue;
}
$ip = trim(explode(',', (string) $_SERVER[$k])[0]);
if (filter_var($ip, FILTER_VALIDATE_IP)) {
return $ip;
}
}
return '0.0.0.0';
}
function ipAllowed(): bool
{
if (ALLOW_IPS === []) {
return true;
}
return in_array(clientIp(), ALLOW_IPS, true);
}
if (!ipAllowed()) {
http_response_code(403);
exit('Forbidden (IP).');
}
function ensureCsrfToken(): string
{
if (empty($_SESSION['csrf_token']) || !is_string($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf_token'];
}
function csrfValid(): bool
{
$posted = $_POST['csrf_token'] ?? '';
return is_string($posted)
&& isset($_SESSION['csrf_token'])
&& is_string($_SESSION['csrf_token'])
&& hash_equals($_SESSION['csrf_token'], $posted);
}
function csrfField(): void
{
echo '<input type="hidden" name="csrf_token" value="' . h(ensureCsrfToken()) . '">';
}
function gateLoginSuccess(): void
{
$_SESSION['gate_ok'] = true;
session_regenerate_id(true);
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
$gateError = null;
if ($_SERVER['REQUEST_METHOD'] === 'POST' && empty($_SESSION['gate_ok']) && ($_POST['gate_action'] ?? '') === 'login') {
$key = (string) ($_POST['key'] ?? '');
if ($key !== '' && hash_equals(ACCESS_KEY, $key)) {
gateLoginSuccess();
header('Location: index.php');
exit;
}
$gateError = '密钥错误';
}
if (empty($_SESSION['gate_ok'])) {
$key = isset($_GET['key']) ? (string) $_GET['key'] : '';
if ($key !== '' && hash_equals(ACCESS_KEY, $key)) {
gateLoginSuccess();
}
}
if (!empty($_SESSION['gate_ok'])) {
ensureCsrfToken();
}
if (!empty($_GET['logout'])) {
$_SESSION = [];
session_destroy();
header('Location: ' . strtok($_SERVER['REQUEST_URI'] ?? 'index.php', '?'));
exit;
}
// ---------- PDO ----------
function pdoConnect(string $host, string $port, string $user, string $pass, string $dbname): PDO
{
$dsn = sprintf(
'mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4',
$host,
$port !== '' ? $port : '3306',
$dbname
);
return new PDO($dsn, $user, $pass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]);
}
function sessionDb(): ?array
{
return isset($_SESSION['db_config']) && is_array($_SESSION['db_config'])
? $_SESSION['db_config']
: null;
}
function getPdo(): ?PDO
{
$cfg = sessionDb();
if ($cfg === null) {
return null;
}
try {
return pdoConnect(
$cfg['host'],
$cfg['port'],
$cfg['user'],
$cfg['pass'],
$cfg['dbname']
);
} catch (Throwable $e) {
unset($_SESSION['db_config']);
return null;
}
}
function isReadOnlySql(string $sql): bool
{
$s = ltrim($sql);
if ($s === '') {
return true;
}
$head = strtolower(substr($s, 0, 20));
foreach (['select', 'show', 'describe', 'desc', 'explain', 'with'] as $prefix) {
if (strpos($head, $prefix) === 0) {
return true;
}
}
return false;
}
function sqlBlocked(string $sql): ?string
{
$bad = [
'into outfile', 'into dumpfile', 'load_file', 'load data',
'grant ', 'revoke ', 'create user', 'drop user',
];
$lower = strtolower($sql);
foreach ($bad as $needle) {
if (strpos($lower, $needle) !== false) {
return '已拦截高风险语句: ' . $needle;
}
}
return null;
}
function h(?string $s): string
{
return htmlspecialchars((string) $s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
function tableFieldNames(?array $tableMeta): array
{
if ($tableMeta === null) {
return [];
}
$names = [];
foreach ($tableMeta as $col) {
$f = (string) ($col['Field'] ?? '');
if ($f !== '' && preg_match('/^[a-zA-Z0-9_]+$/', $f)) {
$names[] = $f;
}
}
return $names;
}
/** @return array{0: string, 1: array<int, string>} */
function buildBrowseSearchWhere(array $tableMeta, string $keyword, string $field): array
{
$keyword = trim($keyword);
if ($keyword === '') {
return ['', []];
}
$like = '%' . str_replace(['\\', '%', '_'], ['\\\\', '\\%', '\\_'], $keyword) . '%';
$fields = tableFieldNames($tableMeta);
if ($field !== '' && in_array($field, $fields, true)) {
$quoted = '`' . str_replace('`', '``', $field) . '`';
return [' WHERE ' . $quoted . ' LIKE ?', [$like]];
}
$parts = [];
$params = [];
foreach ($fields as $f) {
$quoted = '`' . str_replace('`', '``', $f) . '`';
$parts[] = 'CAST(' . $quoted . ' AS CHAR) LIKE ?';
$params[] = $like;
}
if ($parts === []) {
return ['', []];
}
return [' WHERE (' . implode(' OR ', $parts) . ')', $params];
}
/** @return list<string> */
function splitSqlStatements(string $sql): array
{
$statements = [];
foreach (explode(';', $sql) as $chunk) {
$s = trim($chunk);
if ($s === '') {
continue;
}
$statements[] = $s;
}
return $statements;
}
/**
* @param list<string> $statements
* @return list<array{index:int,total:int,sql:string,ok:bool,error:?string,rows:?array,info:?string}>
*/
function executeSqlBatch(PDO $pdo, array $statements): array
{
$results = [];
$total = count($statements);
foreach ($statements as $i => $stmtSql) {
$index = $i + 1;
$block = sqlBlocked($stmtSql);
if ($block !== null) {
$results[] = [
'index' => $index,
'total' => $total,
'sql' => $stmtSql,
'ok' => false,
'error' => $block,
'rows' => null,
'info' => null,
];
break;
}
try {
if (isReadOnlySql($stmtSql)) {
$stmt = $pdo->query($stmtSql);
$rows = $stmt ? $stmt->fetchAll() : [];
$n = count($rows);
$info = "共 {$n} 行";
if ($n > SQL_RESULT_MAX_ROWS) {
$rows = array_slice($rows, 0, SQL_RESULT_MAX_ROWS);
$info = "共 {$n} 行,仅显示前 " . SQL_RESULT_MAX_ROWS . ' 行';
}
$results[] = [
'index' => $index,
'total' => $total,
'sql' => $stmtSql,
'ok' => true,
'error' => null,
'rows' => $rows,
'info' => $info,
];
} else {
$aff = $pdo->exec($stmtSql);
$results[] = [
'index' => $index,
'total' => $total,
'sql' => $stmtSql,
'ok' => true,
'error' => null,
'rows' => null,
'info' => '执行成功,影响行数: ' . ($aff === false ? '未知' : (string) $aff),
];
}
} catch (Throwable $e) {
$results[] = [
'index' => $index,
'total' => $total,
'sql' => $stmtSql,
'ok' => false,
'error' => $e->getMessage(),
'rows' => null,
'info' => null,
];
break;
}
}
return $results;
}
// ---------- 处理 POST ----------
$flash = $_SESSION['flash'] ?? null;
unset($_SESSION['flash']);
$error = null;
$resultRows = null;
$resultInfo = null;
$sqlBatchResults = [];
$sqlDefault = '';
$browsePage = max(1, (int) ($_GET['page'] ?? 1));
$browseTotal = 0;
$browseTotalPages = 1;
$browseSearchQ = trim((string) ($_GET['q'] ?? ''));
$browseSearchCol = (string) ($_GET['col'] ?? '');
if ($browseSearchCol !== '' && !preg_match('/^[a-zA-Z0-9_]+$/', $browseSearchCol)) {
$browseSearchCol = '';
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_SESSION['gate_ok'])) {
$action = $_POST['action'] ?? '';
if (!csrfValid()) {
$error = '安全校验失败,请刷新页面后重试';
$action = '';
}
if ($action === 'connect') {
$host = trim((string) ($_POST['host'] ?? '127.0.0.1'));
$port = trim((string) ($_POST['port'] ?? '3306'));
$user = (string) ($_POST['user'] ?? '');
$pass = (string) ($_POST['pass'] ?? '');
$dbname = trim((string) ($_POST['dbname'] ?? ''));
try {
pdoConnect($host, $port, $user, $pass, $dbname);
$_SESSION['db_config'] = compact('host', 'port', 'user', 'pass', 'dbname');
$_SESSION['flash'] = '已连接数据库: ' . $dbname;
header('Location: index.php');
exit;
} catch (Throwable $e) {
$error = '连接失败: ' . $e->getMessage();
}
}
if ($action === 'disconnect') {
unset($_SESSION['db_config']);
$_SESSION['flash'] = '已断开数据库连接';
header('Location: index.php');
exit;
}
if ($action === 'run_sql') {
$sql = trim((string) ($_POST['sql'] ?? ''));
$sqlDefault = $sql;
if (!empty($_POST['ctx_table']) && preg_match('/^[a-zA-Z0-9_]+$/', (string) $_POST['ctx_table'])) {
$_SESSION['ctx_table'] = (string) $_POST['ctx_table'];
} else {
unset($_SESSION['ctx_table']);
}
if (!empty($_POST['ctx_tab']) && $_POST['ctx_tab'] === 'sql') {
$_SESSION['ctx_main_tab'] = 'sql';
} else {
unset($_SESSION['ctx_main_tab']);
}
$pdo = getPdo();
if ($pdo === null) {
$error = '请先连接数据库';
} elseif ($sql === '') {
$error = 'SQL 不能为空';
} else {
$statements = splitSqlStatements($sql);
if ($statements === []) {
$error = 'SQL 不能为空';
} elseif (count($statements) > SQL_BATCH_MAX_STATEMENTS) {
$error = '单次最多执行 ' . SQL_BATCH_MAX_STATEMENTS . ' 条语句(用分号分隔)';
} else {
$sqlBatchResults = executeSqlBatch($pdo, $statements);
$failed = null;
foreach ($sqlBatchResults as $item) {
if (!$item['ok']) {
$failed = $item;
break;
}
}
if ($failed !== null) {
$error = '第 ' . $failed['index'] . ' 条失败:' . $failed['error'];
} elseif (count($sqlBatchResults) === 1 && $sqlBatchResults[0]['ok']) {
$resultRows = $sqlBatchResults[0]['rows'];
$resultInfo = $sqlBatchResults[0]['info'];
}
}
}
}
}
$pdo = !empty($_SESSION['gate_ok']) ? getPdo() : null;
$tables = [];
$tableMeta = null;
$createSql = null;
$tableStatus = null;
$columnComments = [];
$activeTable = isset($_GET['table']) ? (string) $_GET['table'] : '';
$mainTab = isset($_GET['tab']) ? (string) $_GET['tab'] : '';
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'run_sql') {
if (!empty($_SESSION['ctx_table'])) {
$activeTable = (string) $_SESSION['ctx_table'];
}
if (!empty($_SESSION['ctx_main_tab'])) {
$mainTab = 'sql';
}
}
unset($_SESSION['ctx_table'], $_SESSION['ctx_main_tab']);
$rawView = $_GET['view'] ?? 'browse';
if ($rawView === 'data') {
$rawView = 'browse';
}
$tableView = in_array($rawView, ['browse', 'structure', 'sql'], true) ? $rawView : 'browse';
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'run_sql') {
$tableView = 'sql';
$mainTab = '';
}
if ($pdo !== null && $activeTable !== '') {
if (!preg_match('/^[a-zA-Z0-9_]+$/', $activeTable)) {
$error = '无效表名';
$activeTable = '';
} else {
try {
$q = '`' . str_replace('`', '``', $activeTable) . '`';
$st = $pdo->query('SHOW TABLE STATUS LIKE ' . $pdo->quote($activeTable))->fetch();
$tableStatus = $st ?: null;
if (in_array($tableView, ['structure', 'browse', 'sql'], true)) {
$tableMeta = $pdo->query('SHOW FULL COLUMNS FROM ' . $q)->fetchAll();
foreach ($tableMeta as $col) {
$field = (string) ($col['Field'] ?? '');
$comment = trim((string) ($col['Comment'] ?? ''));
if ($field !== '' && $comment !== '') {
$columnComments[$field] = $comment;
}
}
}
if ($tableView === 'structure') {
$cr = $pdo->query('SHOW CREATE TABLE ' . $q)->fetch();
$createSql = $cr['Create Table'] ?? ($cr[1] ?? null);
}
$loadBrowse = $tableView === 'browse'
&& $_SERVER['REQUEST_METHOD'] !== 'POST'
&& $resultRows === null;
if ($loadBrowse) {
[$whereSql, $whereParams] = buildBrowseSearchWhere($tableMeta ?? [], $browseSearchQ, $browseSearchCol);
$countStmt = $pdo->prepare('SELECT COUNT(*) FROM ' . $q . $whereSql);
$countStmt->execute($whereParams);
$browseTotal = (int) $countStmt->fetchColumn();
$browseTotalPages = max(1, (int) ceil($browseTotal / BROWSE_PAGE_SIZE));
if ($browsePage > $browseTotalPages) {
$browsePage = $browseTotalPages;
}
$offset = ($browsePage - 1) * BROWSE_PAGE_SIZE;
$dataStmt = $pdo->prepare(
'SELECT * FROM ' . $q . $whereSql . ' LIMIT ' . (int) $offset . ', ' . BROWSE_PAGE_SIZE
);
$dataStmt->execute($whereParams);
$resultRows = $dataStmt->fetchAll();
$shown = count($resultRows);
$from = $browseTotal === 0 ? 0 : $offset + 1;
$to = $offset + $shown;
$resultInfo = "第 {$browsePage} / {$browseTotalPages} 页,本页 {$shown} 行(第 {$from}–{$to} 条,共 {$browseTotal} 行)";
if ($browseSearchQ !== '') {
$scope = $browseSearchCol !== '' ? $browseSearchCol : '全部字段';
$resultInfo .= ";搜索「" . $browseSearchQ . "」(" . $scope . ')';
}
}
if ($tableView === 'sql' && $sqlDefault === '' && $_SERVER['REQUEST_METHOD'] !== 'POST') {
$sqlDefault = 'SELECT * FROM `' . $activeTable . '` LIMIT 100;';
}
} catch (Throwable $e) {
$error = $e->getMessage();
}
}
}
if ($mainTab === 'sql' && $sqlDefault === '' && $_SERVER['REQUEST_METHOD'] !== 'POST') {
$sqlDefault = '';
}
if ($pdo !== null) {
try {
foreach ($pdo->query('SHOW TABLE STATUS')->fetchAll() as $row) {
$tables[] = $row;
}
} catch (Throwable $e) {
$error = $e->getMessage();
}
}
$cfg = sessionDb();
function renderBrowsePager(string $table, int $page, int $totalPages, string $searchQ = '', string $searchCol = ''): void
{
if ($totalPages <= 1) {
return;
}
$extra = '';
if ($searchQ !== '') {
$extra .= '&q=' . rawurlencode($searchQ);
}
if ($searchCol !== '') {
$extra .= '&col=' . rawurlencode($searchCol);
}
$base = '?table=' . rawurlencode($table) . '&view=browse' . $extra . '&page=';
echo '<div class="pager">';
echo '<p style="text-align:left;margin:0;">';
if ($page > 1) {
echo '<a href="' . h($base . '1') . '">首页</a> | ';
echo '<a href="' . h($base . ($page - 1)) . '">上一页</a> | ';
}
echo '<strong>第 ' . (int) $page . ' / ' . (int) $totalPages . ' 页</strong>';
if ($page < $totalPages) {
echo ' | <a href="' . h($base . ($page + 1)) . '">下一页</a>';
echo ' | <a href="' . h($base . $totalPages) . '">末页</a>';
}
echo '</p></div>';
}
function renderResultTable(?array $rows, ?string $info, bool $wideGrid = true, array $columnComments = []): void
{
if ($info !== null && $info !== '') {
echo '<p class="msg-ok" style="margin:0 0 10px;">' . h($info) . '</p>';
}
if ($rows === null) {
return;
}
if ($rows === []) {
echo '<p class="hint" style="text-align:left;margin:0;">查询成功,结果为空。</p>';
return;
}
$tblClass = 'data' . ($wideGrid ? ' data-grid' : '');
$wrapClass = 'data-scroll' . ($wideGrid ? ' data-scroll-wide' : '');
echo '<div class="' . h($wrapClass) . '"><table class="' . h($tblClass) . '"><tr>';
foreach (array_keys($rows[0]) as $col) {
$col = (string) $col;
echo '<th>';
echo '<span class="col-name">' . h($col) . '</span>';
if (!empty($columnComments[$col])) {
echo '<span class="col-comment">' . h($columnComments[$col]) . '</span>';
}
echo '</th>';
}
echo '</tr>';
foreach ($rows as $row) {
echo '<tr>';
foreach ($row as $v) {
echo '<td>' . h($v === null ? 'NULL' : (string) $v) . '</td>';
}
echo '</tr>';
}
echo '</table></div>';
}
/** @param list<array{index:int,total:int,sql:string,ok:bool,error:?string,rows:?array,info:?string}> $batch */
function renderSqlBatchResults(array $batch, array $columnComments = []): void
{
foreach ($batch as $item) {
echo '<div class="sql-batch-item">';
echo '<p class="sql-batch-title">语句 ' . (int) $item['index'] . ' / ' . (int) $item['total'] . '</p>';
echo '<pre class="sql-batch-sql">' . h($item['sql']) . '</pre>';
if (!$item['ok']) {
echo '<p class="msg-err" style="margin:0;">' . h((string) $item['error']) . '</p>';
} elseif ($item['rows'] !== null) {
renderResultTable($item['rows'], $item['info'], true, $columnComments);
} elseif ($item['info'] !== null) {
echo '<p class="msg-ok" style="margin:0;">' . h($item['info']) . '</p>';
}
echo '</div>';
}
}
function formatBytes($n): string
{
$n = (int) $n;
if ($n < 1024) {
return $n . ' B';
}
if ($n < 1024 * 1024) {
return round($n / 1024, 1) . ' KB';
}
return round($n / 1024 / 1024, 2) . ' MB';
}
function quoteSqlTable(string $table): string
{
return '`' . str_replace('`', '``', $table) . '`';
}
/** @return array<string, list<array{label: string, sql: string}>> */
function getSqlSnippets(string $table): array
{
$q = quoteSqlTable($table);
return [
'表维护' => [
['label' => '优化表 OPTIMIZE', 'sql' => "OPTIMIZE TABLE {$q};"],
['label' => '检查表 CHECK', 'sql' => "CHECK TABLE {$q};"],
['label' => '分析表 ANALYZE', 'sql' => "ANALYZE TABLE {$q};"],
['label' => '修复表 REPAIR', 'sql' => "REPAIR TABLE {$q};"],
],
'查看表' => [
['label' => '字段结构', 'sql' => "SHOW FULL COLUMNS FROM {$q};"],
['label' => '所有索引', 'sql' => "SHOW INDEX FROM {$q};"],
['label' => '建表语句', 'sql' => "SHOW CREATE TABLE {$q};"],
['label' => '行数统计', 'sql' => "SELECT COUNT(*) AS total FROM {$q};"],
['label' => '预览 20 行', 'sql' => "SELECT * FROM {$q} LIMIT 20;"],
['label' => '表状态', 'sql' => "SHOW TABLE STATUS LIKE '" . addslashes($table) . "';"],
],
'全文索引' => [
['label' => '本表全文索引', 'sql' => "SHOW INDEX FROM {$q} WHERE Index_type = 'FULLTEXT';"],
[
'label' => '全库全文索引',
'sql' => "SELECT TABLE_NAME, INDEX_NAME, GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX) AS cols\n"
. "FROM information_schema.STATISTICS\n"
. "WHERE TABLE_SCHEMA = DATABASE() AND INDEX_TYPE = 'FULLTEXT'\n"
. "GROUP BY TABLE_NAME, INDEX_NAME;",
],
],
'整库' => [
['label' => '当前库名', 'sql' => 'SELECT DATABASE() AS db;'],
['label' => '所有表', 'sql' => 'SHOW TABLES;'],
['label' => '所有表状态', 'sql' => 'SHOW TABLE STATUS;'],
],
'MySQL 变量' => [
[
'label' => '全文相关变量',
'sql' => "SHOW VARIABLES LIKE 'ft_min_word_len';\n"
. "SHOW VARIABLES LIKE 'innodb_ft_min_token_size';\n"
. "SHOW VARIABLES LIKE 'ngram_token_size';",
],
['label' => '连接数', 'sql' => "SHOW STATUS LIKE 'Threads_connected';"],
],
];
}
function renderSqlSnippets(string $table): void
{
echo '<div class="sql-snippets">';
echo '<p class="sql-snippets-hint">常用命令 · 点击填入上方输入框(模板表:<code>' . h($table) . '</code>'
. ',在左侧选中表时会自动换成本表)</p>';
foreach (getSqlSnippets($table) as $group => $items) {
echo '<div class="sql-snippet-group">';
echo '<span class="sql-snippet-group-title">' . h($group) . '</span>';
echo '<div class="sql-snippet-btns">';
foreach ($items as $item) {
echo '<button type="button" class="sql-snippet-btn" data-sql="' . h($item['sql']) . '">'
. h($item['label']) . '</button>';
}
echo '</div></div>';
}
echo '</div>';
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>数据库管理</title>
<style>
:root {
--bg: #e2e5eb;
--panel: #f4f5f7;
--border: #c8cdd6;
--text: #333a42;
--muted: #6a7380;
--accent: #5a8ec2;
--ok: #3a8a62;
--err: #c25252;
}
* { box-sizing: border-box; }
body { margin:0; font:14px/1.5 system-ui, "Segoe UI", sans-serif; background:var(--bg); color:var(--text); }
a { color:#4a78a8; text-decoration:none; }
a:hover { text-decoration:underline; }
header {
padding:12px 20px; border-bottom:1px solid var(--border);
display:flex; flex-wrap:wrap; gap:12px; align-items:center; justify-content:space-between;
background:#ebecef;
}
header h1 { margin:0; font-size:16px; font-weight:600; color:var(--text); }
.page-center {
display:flex; justify-content:center; align-items:flex-start;
min-height:calc(100vh - 49px); padding:40px 16px;
}
.card-narrow { width:100%; max-width:400px; }
.card-narrow h2 { text-align:center; }
.card-narrow form { text-align:left; }
.card-narrow .form-actions { text-align:center; margin-top:16px; }
.wrap { display:flex; min-height:calc(100vh - 49px); }
aside {
width:260px; flex-shrink:0; border-right:1px solid var(--border);
background:var(--panel); overflow:auto; max-height:calc(100vh - 49px);
}
main { flex:1; padding:16px 20px; overflow:auto; background:var(--bg); }
.tbl-list { list-style:none; margin:0; padding:8px 0; }
.tbl-list li a { display:block; padding:6px 16px; color:var(--text); }
.tbl-list li a:hover, .tbl-list li a.active { background:#dde2e9; text-decoration:none; }
.tbl-list .meta { font-size:11px; color:var(--muted); padding:0 16px 4px; }
.card {
background:var(--panel); border:1px solid var(--border); border-radius:10px;
padding:20px 22px; margin-bottom:16px; box-shadow:0 1px 2px rgba(0,0,0,.04);
}
label { display:block; margin:10px 0 4px; color:var(--muted); font-size:12px; }
input[type=text], input[type=password], textarea {
width:100%; padding:9px 11px; border:1px solid var(--border); border-radius:6px;
background:#fafbfc; color:var(--text);
}
input:focus, textarea:focus { outline:none; border-color:var(--accent); background:#fff; }
textarea { min-height:140px; font-family:ui-monospace, monospace; font-size:13px; }
button, .btn {
padding:9px 18px; border-radius:6px; border:0; cursor:pointer; font-size:13px;
background:var(--accent); color:#fff;
}
button:hover { filter:brightness(.96); }
button.secondary { background:#8b96a3; }
button.danger { background:#b05050; }
.msg-ok { color:var(--ok); margin-bottom:12px; }
.msg-err { color:var(--err); margin-bottom:12px; }
table.data { width:100%; border-collapse:collapse; font-size:12px; background:#fafbfc; }
table.data th, table.data td {
border:1px solid var(--border); padding:6px 10px; text-align:left; vertical-align:top;
}
table.data th { background:#e4e8ee; position:sticky; top:0; font-weight:600; z-index:1; }
table.data-grid { width:max-content; min-width:100%; table-layout:auto; }
table.data-grid th, table.data-grid td {
white-space:nowrap; word-break:normal; max-width:280px; overflow:hidden;
text-overflow:ellipsis; vertical-align:middle;
}
table.data-grid th { position:sticky; top:0; white-space:normal; vertical-align:bottom; }
table.data-grid th .col-name { display:block; font-weight:600; white-space:nowrap; }
table.data-grid th .col-comment {
display:block; font-weight:normal; font-size:11px; color:#5c6b7a;
margin-top:4px; white-space:normal; line-height:1.35; max-width:260px;
}
.data-scroll-wide { overflow-x:auto; overflow-y:auto; -webkit-overflow-scrolling:touch; }
.pager { margin-top:12px; font-size:13px; }
.pager a { margin:0 2px; }
.search-bar {
display:flex; flex-wrap:wrap; gap:10px; align-items:center; margin-bottom:14px;
padding-bottom:14px; border-bottom:1px solid var(--border);
}
.search-bar select, .search-bar input[type=text] { width:auto; min-width:140px; max-width:100%; flex:1 1 160px; }
.search-bar .search-q { flex:2 1 220px; min-width:180px; }
.search-bar button.secondary { background:#9aa5b1; }
.btn-link { font-size:13px; color:#4a78a8; align-self:center; }
.sr-only { position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; clip:rect(0,0,0,0); border:0; }
.sql-batch-item { margin-top:16px; padding-top:12px; border-top:1px dashed var(--border); }
.sql-batch-item:first-of-type { margin-top:12px; border-top:0; padding-top:0; }
.sql-batch-title { margin:0 0 6px; font-size:13px; font-weight:600; color:var(--muted); }
.sql-batch-sql {
margin:0 0 10px; font-size:12px; background:#eceff3; padding:8px 10px;
border-radius:6px; border:1px solid var(--border); white-space:pre-wrap; word-break:break-all;
}
.sql-snippets {
margin-top:16px; padding:12px; background:#eef1f5; border:1px solid var(--border); border-radius:8px;
}
.sql-snippets-hint { margin:0 0 10px; font-size:12px; color:var(--muted); text-align:left; }
.sql-snippet-group { margin-bottom:10px; }
.sql-snippet-group:last-child { margin-bottom:0; }
.sql-snippet-group-title {
display:inline-block; min-width:4.5em; font-size:12px; font-weight:600; color:#4a5a6a;
vertical-align:top; margin-top:6px;
}
.sql-snippet-btns { display:inline-flex; flex-wrap:wrap; gap:6px; max-width:calc(100% - 5em); }
.sql-snippet-btn {
padding:5px 10px; font-size:12px; border-radius:5px; border:1px solid #b8c4d0;
background:#fff; color:#333a42; cursor:pointer;
}
.sql-snippet-btn:hover { background:#e8f0f8; border-color:var(--accent); }
pre.create {
white-space:pre-wrap; word-break:break-all; font-size:12px;
background:#eceff3; padding:12px; border-radius:6px; overflow:auto; border:1px solid var(--border);
}
.breadcrumb { font-size:13px; color:var(--muted); margin-bottom:10px; }
.breadcrumb strong { color:var(--text); }
.tabs { display:flex; flex-wrap:wrap; gap:0; margin-bottom:14px; border-bottom:1px solid var(--border); }
.tabs a {
padding:8px 16px; margin-bottom:-1px; border:1px solid transparent;
color:var(--muted); text-decoration:none; font-size:13px;
}
.tabs a:hover { color:var(--text); background:#e8ebf0; text-decoration:none; }
.tabs a.active {
color:var(--text); background:var(--panel); border-color:var(--border);
border-bottom-color:var(--panel); font-weight:600;
}
.data-scroll { overflow:auto; max-height:min(70vh, 640px); border:1px solid var(--border); border-radius:6px; }
.data-scroll-wide { max-height:min(75vh, 720px); }
.aside-head { padding:12px 16px 8px; font-size:13px; font-weight:600; border-bottom:1px solid var(--border); }
.aside-head a { font-weight:normal; font-size:12px; margin-left:8px; }
.hint { font-size:12px; color:var(--muted); margin-top:16px; line-height:1.55; text-align:center; }
form.inline { display:inline; }
h3.sec-title { margin:0 0 12px; font-size:14px; font-weight:600; }
</style>
</head>
<body>
<header>
<h1>数据库管理</h1>
<div>
<?php if (!empty($_SESSION['gate_ok'])): ?>
<?php if ($cfg): ?>
<span style="color:var(--muted);font-size:13px;">
<?= h($cfg['user'] . '@' . $cfg['host'] . '/' . $cfg['dbname']) ?>
</span>
<form class="inline" method="post" style="margin-left:8px;">
<?php csrfField(); ?>
<input type="hidden" name="action" value="disconnect">
<button type="submit" class="secondary">断开</button>
</form>
<?php endif; ?>
<a href="?logout=1" style="margin-left:8px;">退出访问</a>
<?php endif; ?>
</div>
</header>
<?php if (empty($_SESSION['gate_ok'])): ?>
<div class="page-center">
<div class="card card-narrow">
<h2 style="margin-top:0;font-size:15px;">访问验证</h2>
<p class="hint" style="text-align:left;margin-top:0;">请输入访问密钥。</p>
<?php if ($gateError): ?><p class="msg-err"><?= h($gateError) ?></p><?php endif; ?>
<form method="post">
<input type="hidden" name="gate_action" value="login">
<label>访问密钥</label>
<input type="password" name="key" autocomplete="off" required>
<div class="form-actions"><button type="submit">进入</button></div>
</form>
</div>
</div>
<?php elseif ($pdo === null): ?>
<div class="page-center">
<div class="card card-narrow">
<h2 style="margin-top:0;font-size:15px;">连接 MySQL</h2>
<?php if ($flash): ?><p class="msg-ok"><?= h($flash) ?></p><?php endif; ?>
<?php if ($error): ?><p class="msg-err"><?= h($error) ?></p><?php endif; ?>
<form method="post">
<?php csrfField(); ?>
<input type="hidden" name="action" value="connect">
<label>主机</label>
<input type="text" name="host" value="<?= h($_POST['host'] ?? '127.0.0.1') ?>">
<label>端口</label>
<input type="text" name="port" value="<?= h($_POST['port'] ?? '3306') ?>">
<label>用户名</label>
<input type="text" name="user" id="connect-user" value="<?= h($_POST['user'] ?? '') ?>" required autocomplete="off">
<label>密码</label>
<input type="password" name="pass" autocomplete="off">
<label>数据库名</label>
<input type="text" name="dbname" id="connect-dbname" value="<?= h($_POST['dbname'] ?? '') ?>" required autocomplete="off">
<div class="form-actions"><button type="submit">连接</button></div>
</form>
<p class="hint">凭证仅存于当前 PHP 会话,关闭浏览器或点「断开」后清除;不落盘配置文件。</p>
</div>
</div>
<?php else:
$dbName = $cfg['dbname'] ?? '';
$showSqlPanel = ($activeTable !== '' && $tableView === 'sql') || ($activeTable === '' && $mainTab === 'sql');
?>
<div class="wrap">
<aside>
<div class="aside-head">
<?= h($dbName) ?>
<a href="index.php">结构</a>
</div>
<p class="meta" style="padding:8px 16px 4px;">表(<?= count($tables) ?>)</p>
<ul class="tbl-list">
<?php foreach ($tables as $t):
$name = $t['Name'] ?? '';
$active = $name === $activeTable;
?>
<li>
<a class="<?= $active ? 'active' : '' ?>" href="?table=<?= urlencode($name) ?>&view=browse">
<?= h($name) ?>
</a>
</li>
<?php endforeach; ?>
</ul>
</aside>
<main>
<?php if ($flash): ?><p class="msg-ok"><?= h($flash) ?></p><?php endif; ?>
<?php if ($error): ?><p class="msg-err"><?= h($error) ?></p><?php endif; ?>
<?php if ($activeTable === ''): ?>
<p class="breadcrumb">服务器:<?= h($cfg['host'] ?? '') ?> / 数据库:<strong><?= h($dbName) ?></strong></p>
<nav class="tabs">
<a class="active" href="index.php">结构</a>
<a class="<?= $mainTab === 'sql' ? 'active' : '' ?>" href="?tab=sql">SQL</a>
</nav>
<?php if ($mainTab !== 'sql'): ?>
<div class="card" style="padding:16px;">
<h3 class="sec-title">数据表一览</h3>
<div class="data-scroll" style="max-height:none;">
<table class="data">
<tr>
<th>表名</th>
<th>操作</th>
<th>行数</th>
<th>引擎</th>
<th>数据大小</th>
<th>备注</th>
</tr>
<?php foreach ($tables as $t):
$name = $t['Name'] ?? '';
?>
<tr>
<td><a href="?table=<?= urlencode($name) ?>&view=browse"><?= h($name) ?></a></td>
<td>
<a href="?table=<?= urlencode($name) ?>&view=browse">浏览</a>
|
<a href="?table=<?= urlencode($name) ?>&view=structure">结构</a>
</td>
<td><?= h((string) ($t['Rows'] ?? '')) ?></td>
<td><?= h((string) ($t['Engine'] ?? '')) ?></td>
<td><?= h(formatBytes($t['Data_length'] ?? 0)) ?></td>
<td><?= h((string) ($t['Comment'] ?? '')) ?></td>
</tr>
<?php endforeach; ?>
</table>
</div>
</div>
<?php endif; ?>
<?php else: ?>
<p class="breadcrumb">
<a href="index.php"><?= h($dbName) ?></a>
/
<strong><?= h($activeTable) ?></strong>
<?php if ($tableStatus): ?>
<span style="margin-left:8px;">(约 <?= h((string) ($tableStatus['Rows'] ?? '?')) ?> 行)</span>
<?php endif; ?>
</p>
<nav class="tabs">
<a class="<?= $tableView === 'browse' ? 'active' : '' ?>" href="?table=<?= urlencode($activeTable) ?>&view=browse">浏览</a>
<a class="<?= $tableView === 'structure' ? 'active' : '' ?>" href="?table=<?= urlencode($activeTable) ?>&view=structure">结构</a>
<a class="<?= $tableView === 'sql' ? 'active' : '' ?>" href="?table=<?= urlencode($activeTable) ?>&view=sql">SQL</a>
</nav>
<?php if ($tableView === 'browse'): ?>
<div class="card">
<h3 class="sec-title">浏览:<?= h($activeTable) ?></h3>
<form method="get" class="search-bar">
<input type="hidden" name="table" value="<?= h($activeTable) ?>">
<input type="hidden" name="view" value="browse">
<label class="sr-only" for="search-col">字段</label>
<select name="col" id="search-col" title="搜索字段">
<option value="">全部字段</option>
<?php foreach (tableFieldNames($tableMeta ?? []) as $fn): ?>
<option value="<?= h($fn) ?>"<?= $browseSearchCol === $fn ? ' selected' : '' ?>>
<?= h($fn) ?><?= !empty($columnComments[$fn]) ? ' — ' . h($columnComments[$fn]) : '' ?>
</option>
<?php endforeach; ?>
</select>
<input type="text" class="search-q" name="q" value="<?= h($browseSearchQ) ?>" placeholder="输入关键词搜索(模糊匹配)" autocomplete="off">
<button type="submit">搜索</button>
<?php if ($browseSearchQ !== ''): ?>
<a class="btn-link" href="?table=<?= urlencode($activeTable) ?>&view=browse">清除搜索</a>
<?php endif; ?>
</form>
<?php renderResultTable($resultRows, $resultInfo, true, $columnComments); ?>
<?php renderBrowsePager($activeTable, $browsePage, $browseTotalPages, $browseSearchQ, $browseSearchCol); ?>
</div>
<?php elseif ($tableView === 'structure'): ?>
<?php if ($tableMeta): ?>
<div class="card">
<h3 class="sec-title">表结构</h3>
<div class="data-scroll" style="max-height:none;">
<table class="data">
<tr>
<th>字段</th><th>类型</th><th>排序规则</th><th>允许空</th>
<th>键</th><th>默认值</th><th>额外</th><th>备注</th>
</tr>
<?php foreach ($tableMeta as $col): ?>
<tr>
<td><?= h((string) ($col['Field'] ?? '')) ?></td>
<td><?= h((string) ($col['Type'] ?? '')) ?></td>
<td><?= h((string) ($col['Collation'] ?? '')) ?></td>
<td><?= h((string) ($col['Null'] ?? '')) ?></td>
<td><?= h((string) ($col['Key'] ?? '')) ?></td>
<td><?= h(isset($col['Default']) ? (string) $col['Default'] : '') ?></td>
<td><?= h((string) ($col['Extra'] ?? '')) ?></td>
<td><?= h((string) ($col['Comment'] ?? '')) ?></td>
</tr>
<?php endforeach; ?>
</table>
</div>
</div>
<?php endif; ?>
<?php if ($createSql): ?>
<div class="card">
<h3 class="sec-title">建表语句</h3>
<pre class="create"><?= h($createSql) ?></pre>
</div>
<?php endif; ?>
<?php endif; ?>
<?php endif; ?>
<?php if ($showSqlPanel):
$sqlSnippetTable = ($activeTable !== '' && preg_match('/^[a-zA-Z0-9_]+$/', $activeTable))
? $activeTable
: 'mac_vod';
?>
<div class="card">
<h3 class="sec-title">执行 SQL</h3>
<form method="post" id="sql-exec-form">
<?php csrfField(); ?>
<input type="hidden" name="action" value="run_sql">
<?php if ($activeTable !== ''): ?>
<input type="hidden" name="ctx_table" value="<?= h($activeTable) ?>">
<?php else: ?>
<input type="hidden" name="ctx_tab" value="sql">
<?php endif; ?>
<textarea id="sql-textarea" name="sql" placeholder="支持多条语句,用英文分号 ; 分隔。也可点下方常用命令一键填入。"><?= h($sqlDefault) ?></textarea>
<p style="margin-top:10px;">
<button type="submit">执行</button>
</p>
</form>
<?php renderSqlSnippets($sqlSnippetTable); ?>
<?php if ($tableView === 'sql' || $mainTab === 'sql'): ?>
<?php if ($sqlBatchResults !== []): ?>
<?php renderSqlBatchResults($sqlBatchResults, $columnComments); ?>
<?php else: ?>
<?php renderResultTable($resultRows, $resultInfo, true, $columnComments); ?>
<?php endif; ?>
<?php endif; ?>
</div>
<?php endif; ?>
<p class="hint" style="text-align:left;">不用时请在宝塔关闭本站。</p>
</main>
</div>
<?php endif; ?>
<script>
(function () {
var userEl = document.getElementById('connect-user');
var dbEl = document.getElementById('connect-dbname');
if (!userEl || !dbEl) return;
var dbManual = dbEl.value.trim() !== '' && dbEl.value !== userEl.value;
userEl.addEventListener('input', function () {
if (!dbManual) dbEl.value = userEl.value;
});
dbEl.addEventListener('input', function () {
dbManual = dbEl.value !== userEl.value;
});
if (!dbManual && userEl.value.trim() !== '' && dbEl.value.trim() === '') {
dbEl.value = userEl.value;
}
})();
(function () {
var ta = document.getElementById('sql-textarea');
if (!ta) return;
document.querySelectorAll('.sql-snippet-btn').forEach(function (btn) {
btn.addEventListener('click', function () {
var sql = btn.getAttribute('data-sql');
if (sql != null) ta.value = sql;
ta.focus();
});
});
})();
</script>
</body>
</html>
使用流程;
新建立个网站新建个index.php把代码放进去即可,打开网站看见输入密钥页面后输入代码里“const ACCESS_KEY”的值即可。下面的流程自行登陆就行。用完了可以直接关闭网站,不让他在公网一直暴露,注意使用ssl