Files

321 lines
17 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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&#10;React&#10;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>