feat: init project
This commit is contained in:
278
web/templates/list.html
Normal file
278
web/templates/list.html
Normal 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>
|
||||
Reference in New Issue
Block a user