feat: init project

This commit is contained in:
yanghao05
2025-08-05 17:26:59 +08:00
parent c5d621ad03
commit e034a2e54e
30 changed files with 5159 additions and 0 deletions

278
web/templates/list.html Normal file
View File

@@ -0,0 +1,278 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{.TableAlias}} - 数据管理系统</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<style>
.tag {
@apply inline-block px-2 py-1 text-xs font-medium rounded-full;
}
.modal {
@apply fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden;
}
.modal-content {
@apply bg-white rounded-lg max-w-4xl w-full mx-4 max-h-[80vh] overflow-y-auto;
}
.card {
@apply bg-white rounded-lg shadow-md hover:shadow-lg transition-shadow;
}
.card-body {
@apply p-6;
}
.loading {
@apply animate-pulse;
}
</style>
</head>
<body class="bg-gray-50">
<div class="min-h-screen">
<!-- Header -->
<header class="bg-white shadow-sm border-b">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between items-center py-4">
<div>
<h1 class="text-2xl font-bold text-gray-900">数据管理系统</h1>
<p class="text-sm text-gray-600">{{.TableAlias}}</p>
</div>
<div class="flex items-center space-x-4">
<select id="tableSelector" class="border border-gray-300 rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" onchange="changeTable(this.value)">
{{range .Tables}}
<option value="{{.Name}}" {{if eq .Name $.Table}}selected{{end}}>{{.Alias}}</option>
{{end}}
</select>
<div class="text-sm text-gray-600">
共 {{.Total}} 条记录
</div>
</div>
</div>
</div>
</header>
<!-- Main Content -->
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Search and Filters -->
<div class="mb-6 bg-white p-4 rounded-lg shadow">
<div class="flex flex-col sm:flex-row gap-4">
<div class="flex-1">
<input type="text" id="searchInput" placeholder="搜索..." value="{{.Search}}"
class="w-full border border-gray-300 rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div class="flex gap-2">
<button onclick="performSearch()" class="px-4 py-2 bg-blue-600 text-white rounded-md text-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500">
搜索
</button>
<button onclick="clearSearch()" class="px-4 py-2 bg-gray-300 text-gray-700 rounded-md text-sm hover:bg-gray-400 focus:outline-none focus:ring-2 focus:ring-gray-500">
清除
</button>
</div>
</div>
</div>
<!-- Data Display -->
<div class="grid gap-6">
<!-- Table View -->
<div class="bg-white shadow overflow-hidden sm:rounded-md">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
{{range $col := .Columns}}
{{if $col.ShowInList}}
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
{{$col.Alias}}
</th>
{{end}}
{{end}}
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
操作
</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
{{range $row := .Data}}
<tr class="hover:bg-gray-50">
{{range $col := $.Columns}}
{{if $col.ShowInList}}
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{{if eq $col.RenderType "text"}}
{{truncate (index $row $col.Name) 50}}
{{else}}
{{index $row $col.Name}}
{{end}}
</td>
{{end}}
{{end}}
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
<button onclick="showDetail('{{index $row "id"}}')" class="text-blue-600 hover:text-blue-800">
查看详情
</button>
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
</div>
<!-- Pagination -->
{{if gt .Pages 1}}
<div class="mt-6 bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6">
<div class="flex-1 flex justify-between sm:hidden">
{{if gt .Page 1}}
<a href="?table={{.Table}}&page={{sub .Page 1}}&per_page={{.PerPage}}&search={{.Search}}&sort={{.SortField}}&order={{.SortOrder}}"
class="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
上一页
</a>
{{end}}
{{if lt .Page .Pages}}
<a href="?table={{.Table}}&page={{add .Page 1}}&per_page={{.PerPage}}&search={{.Search}}&sort={{.SortField}}&order={{.SortOrder}}"
class="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
下一页
</a>
{{end}}
</div>
<div class="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
<div>
<p class="text-sm text-gray-700">
显示第 <span class="font-medium">{{add (mul (sub .Page 1) .PerPage) 1}}</span>
<span class="font-medium">{{if lt (mul .Page .PerPage) .Total}}{{mul .Page .PerPage}}{{else}}{{.Total}}{{end}}</span>
条,共 <span class="font-medium">{{.Total}}</span> 条记录
</p>
</div>
<div>
<nav class="relative z-0 inline-flex rounded-md shadow-sm -space-x-px">
{{if gt .Page 1}}
<a href="?table={{.Table}}&page={{sub .Page 1}}&per_page={{.PerPage}}&search={{.Search}}&sort={{.SortField}}&order={{.SortOrder}}"
class="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50">
上一页
</a>
{{end}}
{{$start := sub .Page 2}}
{{$end := add .Page 2}}
{{if lt $start 1}}
{{$start = 1}}
{{$end = min 5 .Pages}}
{{end}}
{{if gt $end .Pages}}
{{$end = .Pages}}
{{$start = max 1 (sub .Pages 4)}}
{{end}}
{{range $i := $start | $end}}
{{if eq $i .Page}}
<span class="relative inline-flex items-center px-4 py-2 border border-gray-300 bg-blue-50 text-sm font-medium text-blue-600">
{{$i}}
</span>
{{else}}
<a href="?table={{.Table}}&page={{$i}}&per_page={{.PerPage}}&search={{.Search}}&sort={{.SortField}}&order={{.SortOrder}}"
class="relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50">
{{$i}}
</a>
{{end}}
{{end}}
{{if lt .Page .Pages}}
<a href="?table={{.Table}}&page={{add .Page 1}}&per_page={{.PerPage}}&search={{.Search}}&sort={{.SortField}}&order={{.SortOrder}}"
class="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50">
下一页
</a>
{{end}}
</nav>
</div>
</div>
</div>
{{end}}
</main>
</div>
<!-- Detail Modal -->
<div id="detailModal" class="modal">
<div class="modal-content">
<div class="p-6">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-medium">详情信息</h3>
<button onclick="closeModal()" class="text-gray-400 hover:text-gray-600">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<div id="detailContent" class="space-y-4">
<!-- Content will be loaded here -->
</div>
</div>
</div>
</div>
<script>
// Utility functions
function changeTable(tableName) {
window.location.href = `/?table=${tableName}&page=1`;
}
function performSearch() {
const search = document.getElementById('searchInput').value;
const table = new URLSearchParams(window.location.search).get('table') || '{{.Table}}';
window.location.href = `/?table=${table}&page=1&search=${encodeURIComponent(search)}`;
}
function clearSearch() {
const table = new URLSearchParams(window.location.search).get('table') || '{{.Table}}';
document.getElementById('searchInput').value = '';
window.location.href = `/?table=${table}&page=1`;
}
function showDetail(id) {
const table = new URLSearchParams(window.location.search).get('table') || '{{.Table}}';
fetch(`/api/data/${table}/detail/${id}`)
.then(response => response.json())
.then(data => {
let content = '';
for (const [key, value] of Object.entries(data.data)) {
content += `
<div class="border-b pb-3">
<label class="block text-sm font-medium text-gray-700">${key}</label>
<div class="mt-1 text-sm text-gray-900">${value || ''}</div>
</div>
`;
}
document.getElementById('detailContent').innerHTML = content;
document.getElementById('detailModal').classList.remove('hidden');
})
.catch(error => {
console.error('Error loading detail:', error);
alert('加载详情失败');
});
}
function closeModal() {
document.getElementById('detailModal').classList.add('hidden');
}
// Event listeners
document.getElementById('searchInput').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
performSearch();
}
});
document.getElementById('detailModal').addEventListener('click', function(e) {
if (e.target === this) {
closeModal();
}
});
// Initialize
document.addEventListener('DOMContentLoaded', function() {
// Add enter key support for search
const searchInput = document.getElementById('searchInput');
if (searchInput) {
searchInput.focus();
}
});
</script>
</body>
</html>