首次提交:初始化项目代码
This commit is contained in:
@@ -0,0 +1,321 @@
|
||||
<div class="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
|
||||
<div class="border-b border-slate-200 px-6 py-4 bg-slate-50">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 class="font-semibold text-slate-800 flex items-center gap-2">
|
||||
<i class="fas fa-folder-open text-purple-500"></i> 作品集管理
|
||||
</h3>
|
||||
<p class="text-sm text-slate-500 mt-1">管理所有用户的作品集项目</p>
|
||||
</div>
|
||||
<a href="{{$.AdminPath}}/portfolio/add" class="btn btn-primary">
|
||||
<i class="fas fa-plus"></i> 新增项目
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr class="bg-slate-50 border-b border-slate-200">
|
||||
<th class="text-left px-6 py-4 text-sm font-semibold text-slate-600">项目名称</th>
|
||||
<th class="text-left px-6 py-4 text-sm font-semibold text-slate-600">用户</th>
|
||||
<th class="text-left px-6 py-4 text-sm font-semibold text-slate-600">链接</th>
|
||||
<th class="text-left px-6 py-4 text-sm font-semibold text-slate-600">隐藏状态</th>
|
||||
<th class="text-left px-6 py-4 text-sm font-semibold text-slate-600">首页展示</th>
|
||||
<th class="text-left px-6 py-4 text-sm font-semibold text-slate-600 actions-col">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ range .Items }}
|
||||
<tr class="border-b border-slate-100 hover:bg-slate-50 transition-colors">
|
||||
<td class="px-6 py-4">
|
||||
<span class="font-medium text-slate-800">{{.Name}}</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-slate-600">
|
||||
{{ if .UserID }}
|
||||
{{ $user := index $.UserMap .UserID }}
|
||||
{{ if $user }}
|
||||
{{$user.Name}}({{$user.Username}})
|
||||
{{ else }}
|
||||
<span class="text-slate-400">已失效</span>
|
||||
{{ end }}
|
||||
{{ else }}
|
||||
<span class="text-slate-400">未分配</span>
|
||||
{{ end }}
|
||||
</td>
|
||||
<td class="px-6 py-4 text-slate-600">
|
||||
{{ if .URL }}
|
||||
<a href="{{.URL}}" target="_blank" class="text-blue-600 hover:underline">点击访问</a>
|
||||
{{ else }}
|
||||
-
|
||||
{{ end }}
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
{{ if .Hidden }}
|
||||
<span class="badge badge-danger">已隐藏</span>
|
||||
{{ else }}
|
||||
<span class="badge badge-success">显示</span>
|
||||
{{ end }}
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
{{ if .ShowInResume }}
|
||||
<span class="badge badge-success">是</span>
|
||||
{{ else }}
|
||||
<span class="badge badge-slate">否</span>
|
||||
{{ end }}
|
||||
</td>
|
||||
<td class="px-6 py-4 actions-col">
|
||||
<div class="btn-group">
|
||||
<a href="{{$.AdminPath}}/portfolio/{{.ID}}/edit" class="btn btn-warning btn-xs" title="编辑">
|
||||
<i class="fas fa-edit"></i> 编辑
|
||||
</a>
|
||||
<button onclick="deletePortfolio('{{.ID}}')" class="btn btn-danger btn-xs" title="删除">
|
||||
<i class="fas fa-trash"></i> 删除
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{{ else }}
|
||||
<tr>
|
||||
<td colspan="6" class="px-6 py-12 text-center text-slate-500">
|
||||
<div class="w-16 h-16 mx-auto mb-4 rounded-full bg-slate-100 flex items-center justify-center">
|
||||
<i class="fas fa-folder-open text-slate-400 text-2xl"></i>
|
||||
</div>
|
||||
<p>暂无作品集项目</p>
|
||||
</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{ if gt .TotalPages 1 }}
|
||||
<div class="border-t border-slate-200 px-6 py-4 bg-slate-50 flex items-center justify-between">
|
||||
<div class="text-sm text-slate-500">共 {{ .Total }} 条记录,第 {{ .Page }} / {{ .TotalPages }} 页</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<a href="?page=1" class="btn btn-sm btn-slate {{ if eq .Page 1 }}opacity-50 cursor-not-allowed{{ end }}" {{ if eq .Page 1 }}onclick="return false"{{ end }}>
|
||||
<i class="fas fa-chevron-left"></i>
|
||||
<i class="fas fa-chevron-left"></i>
|
||||
</a>
|
||||
<a href="?page={{ if eq .Page 1 }}1{{ else }}{{ subtract .Page 1 }}{{ end }}" class="btn btn-sm btn-slate {{ if eq .Page 1 }}opacity-50 cursor-not-allowed{{ end }}" {{ if eq .Page 1 }}onclick="return false"{{ end }}>
|
||||
<i class="fas fa-chevron-left"></i>
|
||||
</a>
|
||||
{{ range $i := to 1 $.TotalPages }}
|
||||
{{ if or (eq $i 1) (eq $i $.TotalPages) (and (ge $i (subtract $.Page 2)) (le $i (add $.Page 2))) }}
|
||||
<a href="?page={{ $i }}" class="btn btn-sm {{ if eq $i $.Page }}btn-primary{{ else }}btn-slate{{ end }}">{{ $i }}</a>
|
||||
{{ else if and (eq $i 2) (gt (subtract $.Page 3) 1) }}
|
||||
<span class="text-slate-400 px-2">...</span>
|
||||
{{ else if and (eq $i (subtract $.TotalPages 1)) (lt (add $.Page 3) $.TotalPages) }}
|
||||
<span class="text-slate-400 px-2">...</span>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
<a href="?page={{ if eq .Page .TotalPages }}{{ .TotalPages }}{{ else }}{{ add .Page 1 }}{{ end }}" class="btn btn-sm btn-slate {{ if eq .Page .TotalPages }}opacity-50 cursor-not-allowed{{ end }}" {{ if eq .Page .TotalPages }}onclick="return false"{{ end }}>
|
||||
<i class="fas fa-chevron-right"></i>
|
||||
</a>
|
||||
<a href="?page={{ .TotalPages }}" class="btn btn-sm btn-slate {{ if eq .Page .TotalPages }}opacity-50 cursor-not-allowed{{ end }}" {{ if eq .Page .TotalPages }}onclick="return false"{{ end }}>
|
||||
<i class="fas fa-chevron-right"></i>
|
||||
<i class="fas fa-chevron-right"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
<div class="modal hidden fixed inset-0 bg-black/50 flex items-center justify-center z-50" id="confirmModal">
|
||||
<div class="modal-content bg-white rounded-xl shadow-xl w-full max-w-sm mx-4">
|
||||
<div class="flex items-center justify-between p-4 border-b border-slate-200">
|
||||
<h3 class="font-semibold text-slate-800">确认删除</h3>
|
||||
<button onclick="closeConfirmModal()" class="btn btn-secondary btn-sm">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<div class="w-10 h-10 rounded-full bg-red-100 flex items-center justify-center">
|
||||
<i class="fas fa-exclamation-triangle text-red-500"></i>
|
||||
</div>
|
||||
<p class="text-slate-700">确定要删除这个项目吗?此操作不可撤销。</p>
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<button onclick="closeConfirmModal()" class="flex-1 btn btn-secondary">取消</button>
|
||||
<button onclick="confirmDeleteAction()" class="flex-1 btn btn-danger">确认删除</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal hidden fixed inset-0 bg-black/50 flex items-center justify-center z-50" id="portfolioModal">
|
||||
<div class="modal-content bg-white rounded-xl shadow-xl w-full max-w-lg mx-4">
|
||||
<div class="flex items-center justify-between p-4 border-b border-slate-200">
|
||||
<h3 class="font-semibold text-slate-800" id="portfolioModalTitle">新增项目</h3>
|
||||
<button onclick="closePortfolioModal()" class="btn btn-secondary btn-sm">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<form id="portfolioForm" onsubmit="savePortfolio(event)">
|
||||
<input type="hidden" id="portfolioId">
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-1">选择人员</label>
|
||||
<select id="portfolioUserId" class="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500" required>
|
||||
<option value="">请选择人员</option>
|
||||
{{ range .Users }}
|
||||
<option value="{{.ID}}">{{.Name}} - {{.Email}}</option>
|
||||
{{ end }}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-1">项目名称</label>
|
||||
<input type="text" id="portfolioName" class="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500" required>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-1">项目描述</label>
|
||||
<textarea id="portfolioDescription" rows="3" class="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-1">技术栈 (每行一个)</label>
|
||||
<textarea id="portfolioTechStack" rows="3" class="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500" placeholder="Go React MySQL"></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-1">项目链接</label>
|
||||
<input type="url" id="portfolioURL" class="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500">
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-1">开始日期</label>
|
||||
<input type="date" id="portfolioStartDate" class="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-1">结束日期</label>
|
||||
<input type="date" id="portfolioEndDate" class="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500">
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<label class="flex items-center gap-2">
|
||||
<input type="checkbox" id="portfolioHidden" class="rounded border-slate-300 text-primary-600 focus:ring-primary-500">
|
||||
<span class="text-sm text-slate-700">隐藏项目</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2">
|
||||
<input type="checkbox" id="portfolioShowInResume" class="rounded border-slate-300 text-primary-600 focus:ring-primary-500">
|
||||
<span class="text-sm text-slate-700">首页展示</span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-1">访问密码 (可选)</label>
|
||||
<input type="text" id="portfolioPassword" class="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500" placeholder="留空则无需密码">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-6 flex gap-3">
|
||||
<button type="button" onclick="closePortfolioModal()" class="flex-1 btn btn-secondary">取消</button>
|
||||
<button type="submit" class="flex-1 btn btn-primary">保存</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
function openAddPortfolioModal() {
|
||||
document.getElementById('portfolioModalTitle').textContent = '新增项目';
|
||||
document.getElementById('portfolioForm').reset();
|
||||
document.getElementById('portfolioId').value = '';
|
||||
const modal = document.getElementById('portfolioModal');
|
||||
modal.classList.remove('hidden');
|
||||
modal.classList.add('active');
|
||||
}
|
||||
|
||||
function closePortfolioModal() {
|
||||
const modal = document.getElementById('portfolioModal');
|
||||
modal.classList.add('hidden');
|
||||
modal.classList.remove('active');
|
||||
}
|
||||
|
||||
function editPortfolio(id) {
|
||||
fetch('/api/portfolio/' + id)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
const item = data.data;
|
||||
document.getElementById('portfolioModalTitle').textContent = '编辑项目';
|
||||
document.getElementById('portfolioId').value = item.id;
|
||||
document.getElementById('portfolioUserId').value = item.user_id;
|
||||
document.getElementById('portfolioName').value = item.name;
|
||||
document.getElementById('portfolioDescription').value = item.description || '';
|
||||
document.getElementById('portfolioTechStack').value = (item.tech_stack || []).join('\n');
|
||||
document.getElementById('portfolioURL').value = item.url || '';
|
||||
document.getElementById('portfolioStartDate').value = item.start_date || '';
|
||||
document.getElementById('portfolioEndDate').value = item.end_date || '';
|
||||
document.getElementById('portfolioHidden').checked = item.hidden || false;
|
||||
document.getElementById('portfolioShowInResume').checked = item.show_in_resume || false;
|
||||
document.getElementById('portfolioPassword').value = item.password || '';
|
||||
const modal = document.getElementById('portfolioModal');
|
||||
modal.classList.remove('hidden');
|
||||
modal.classList.add('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function savePortfolio(event) {
|
||||
event.preventDefault();
|
||||
const id = document.getElementById('portfolioId').value;
|
||||
const url = id ? '/api/portfolio/' + id : '/api/portfolio';
|
||||
const method = id ? 'PUT' : 'POST';
|
||||
|
||||
fetch(url, {
|
||||
method: method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
user_id: document.getElementById('portfolioUserId').value,
|
||||
name: document.getElementById('portfolioName').value,
|
||||
description: document.getElementById('portfolioDescription').value,
|
||||
tech_stack: document.getElementById('portfolioTechStack').value.split('\n').filter(s => s.trim()),
|
||||
url: document.getElementById('portfolioURL').value,
|
||||
start_date: document.getElementById('portfolioStartDate').value,
|
||||
end_date: document.getElementById('portfolioEndDate').value,
|
||||
hidden: document.getElementById('portfolioHidden').checked,
|
||||
show_in_resume: document.getElementById('portfolioShowInResume').checked,
|
||||
password: document.getElementById('portfolioPassword').value,
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
closePortfolioModal();
|
||||
location.reload();
|
||||
} else {
|
||||
showToast(data.message || '保存失败', 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var deleteTargetId = null;
|
||||
|
||||
function deletePortfolio(id) {
|
||||
deleteTargetId = id;
|
||||
const modal = document.getElementById('confirmModal');
|
||||
modal.classList.remove('hidden');
|
||||
modal.classList.add('active');
|
||||
}
|
||||
|
||||
function closeConfirmModal() {
|
||||
deleteTargetId = null;
|
||||
const modal = document.getElementById('confirmModal');
|
||||
modal.classList.add('hidden');
|
||||
modal.classList.remove('active');
|
||||
}
|
||||
|
||||
function confirmDeleteAction() {
|
||||
if (!deleteTargetId) return;
|
||||
|
||||
fetch('/api/portfolio/' + deleteTargetId, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
closeConfirmModal();
|
||||
if (data.success) {
|
||||
location.reload();
|
||||
} else {
|
||||
showToast(data.message || '删除失败', 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user