首次提交:初始化项目代码

This commit is contained in:
sunct
2026-07-31 14:44:48 +08:00
commit a4fe393571
119 changed files with 29105 additions and 0 deletions
+421
View File
@@ -0,0 +1,421 @@
<div class="bg-white rounded-2xl shadow-lg border border-slate-100 p-6">
<div class="flex items-center justify-between mb-6">
<div>
<h3 class="font-bold text-slate-800 flex items-center gap-3">
<i class="fas fa-file-alt text-blue-500"></i> {{ if .User }}{{.User.Name}}{{ else }}未知用户{{ end }} 的简历管理
</h3>
<p class="text-sm text-slate-500 mt-1">管理用户的简历信息和内容</p>
</div>
<div class="flex items-center gap-3">
<a href="{{.AdminPath}}/user/{{.User.ID}}/resume/new/edit" class="btn btn-primary">
<i class="fas fa-plus"></i> 创建简历
</a>
<a href="{{.AdminPath}}/users" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> 返回列表
</a>
</div>
</div>
<div class="overflow-x-auto">
<table class="w-full">
<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">创建时间</th>
<th class="text-left px-6 py-4 text-sm font-semibold text-slate-600">操作</th>
</tr>
</thead>
<tbody>
{{ range .Resumes }}
<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">{{.BasicInfo.Name}}</span>
</td>
<td class="px-6 py-4 text-slate-600">{{.BasicInfo.Title}}</td>
<td class="px-6 py-4 text-slate-600">
<a href="/{{.Route}}" target="_blank" class="text-blue-500 hover:text-blue-700 hover:underline">
/{{.Route}}
</a>
</td>
<td class="px-6 py-4">
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium {{ if .ShowInHome }}bg-green-100 text-green-700{{ else }}bg-slate-100 text-slate-500{{ end }}">
{{ if .ShowInHome }}<i class="fas fa-check mr-1"></i>展示{{ else }}隐藏{{ end }}
</span>
</td>
<td class="px-6 py-4">
{{ if .Password }}
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-amber-100 text-amber-700">
<i class="fas fa-lock mr-1"></i>已设置
</span>
{{ else }}
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-slate-100 text-slate-500">
<i class="fas fa-unlock mr-1"></i>无密码
</span>
{{ end }}
</td>
<td class="px-6 py-4 text-slate-500 text-sm">{{.CreatedAt}}</td>
<td class="px-6 py-4">
<div class="btn-group">
<a href="/{{.Route}}" target="_blank" class="btn btn-primary btn-xs">
<i class="fas fa-eye"></i> 预览
</a>
<a href="{{$.AdminPath}}/user/{{$.User.ID}}/resume/{{.ID}}/edit" class="btn btn-warning btn-xs">
<i class="fas fa-edit"></i> 编辑
</a>
<button onclick="toggleShowInHome('{{.ID}}', {{.ShowInHome}})" class="btn btn-success btn-xs">
<i class="fas {{ if .ShowInHome }}fa-eye-slash{{ else }}fa-eye{{ end }}"></i> {{ if .ShowInHome }}隐藏{{ else }}展示{{ end }}
</button>
<button onclick="setPassword('{{.ID}}')" class="btn btn-info btn-xs">
<i class="fas fa-key"></i> 密码
</button>
<button onclick="deleteResume('{{.ID}}')" class="btn btn-danger btn-xs">
<i class="fas fa-trash"></i> 删除
</button>
</div>
</td>
</tr>
{{ else }}
<tr>
<td colspan="8" 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-file-alt text-slate-400 text-2xl"></i>
</div>
<p>暂无简历数据</p>
</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
</div>
<div class="modal hidden fixed inset-0 bg-black/50 flex items-center justify-center z-50" id="resumeModal">
<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-5 border-b border-slate-200">
<h3 class="font-semibold text-slate-800 flex items-center gap-2" id="resumeModalTitle">
<i class="fas fa-file-alt text-blue-500"></i> 新建简历
</h3>
<button onclick="closeResumeModal()" class="text-slate-400 hover:text-slate-600 p-1 hover:bg-slate-100 rounded-lg transition-all">
<i class="fas fa-times"></i>
</button>
</div>
<div class="p-5">
<form id="resumeForm" onsubmit="saveResume(event)">
<input type="hidden" id="resumeId">
<input type="hidden" id="resumeUserId">
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-slate-700 mb-1.5">访问路由</label>
<div class="relative">
<div class="absolute left-3 top-1/2 transform -translate-y-1/2 text-slate-400">
<i class="fas fa-link"></i>
</div>
<input type="text" id="resumeRoute" class="w-full pl-10 pr-4 py-2.5 border border-slate-200 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all" required>
</div>
<p class="text-xs text-slate-500 mt-1">用于访问简历的路径,如 zhangsan,访问地址为 /zhangsan</p>
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1.5">姓名</label>
<div class="relative">
<div class="absolute left-3 top-1/2 transform -translate-y-1/2 text-slate-400">
<i class="fas fa-user"></i>
</div>
<input type="text" id="resumeName" class="w-full pl-10 pr-4 py-2.5 border border-slate-200 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all" required>
</div>
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1.5">职位</label>
<div class="relative">
<div class="absolute left-3 top-1/2 transform -translate-y-1/2 text-slate-400">
<i class="fas fa-briefcase"></i>
</div>
<input type="text" id="resumeTitle" class="w-full pl-10 pr-4 py-2.5 border border-slate-200 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all">
</div>
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1.5">邮箱</label>
<div class="relative">
<div class="absolute left-3 top-1/2 transform -translate-y-1/2 text-slate-400">
<i class="fas fa-envelope"></i>
</div>
<input type="email" id="resumeEmail" class="w-full pl-10 pr-4 py-2.5 border border-slate-200 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all">
</div>
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1.5">手机号</label>
<div class="relative">
<div class="absolute left-3 top-1/2 transform -translate-y-1/2 text-slate-400">
<i class="fas fa-phone"></i>
</div>
<input type="tel" id="resumePhone" class="w-full pl-10 pr-4 py-2.5 border border-slate-200 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all">
</div>
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1.5">所在地</label>
<div class="relative">
<div class="absolute left-3 top-1/2 transform -translate-y-1/2 text-slate-400">
<i class="fas fa-map-marker-alt"></i>
</div>
<input type="text" id="resumeLocation" class="w-full pl-10 pr-4 py-2.5 border border-slate-200 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all">
</div>
</div>
<div>
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" id="resumeShowInHome" class="w-4 h-4 text-blue-600 rounded focus:ring-blue-500">
<span class="text-sm font-medium text-slate-700">在首页展示</span>
</label>
</div>
</div>
<div class="flex items-center justify-center gap-3 mt-6">
<button type="button" onclick="closeResumeModal()" class="btn btn-secondary">
<i class="fas fa-times"></i> 取消
</button>
<button type="submit" class="btn btn-primary">
<i class="fas fa-save"></i> 保存
</button>
</div>
</form>
</div>
</div>
</div>
<div class="modal hidden fixed inset-0 bg-black/50 flex items-center justify-center z-50" id="passwordModal">
<div class="modal-content bg-white rounded-xl shadow-xl w-full max-w-md mx-4">
<div class="flex items-center justify-between p-5 border-b border-slate-200">
<h3 class="font-semibold text-slate-800 flex items-center gap-2">
<i class="fas fa-key text-purple-500"></i> 设置访问密码
</h3>
<button onclick="closePasswordModal()" class="text-slate-400 hover:text-slate-600 p-1 hover:bg-slate-100 rounded-lg transition-all">
<i class="fas fa-times"></i>
</button>
</div>
<div class="p-5">
<form id="passwordForm" onsubmit="savePassword(event)">
<input type="hidden" id="passwordResumeId">
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-slate-700 mb-1.5">访问密码</label>
<div class="relative">
<div class="absolute left-3 top-1/2 transform -translate-y-1/2 text-slate-400">
<i class="fas fa-lock"></i>
</div>
<input type="password" id="resumePassword" class="w-full pl-10 pr-4 py-2.5 border border-slate-200 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all" placeholder="输入密码,留空则取消密码保护">
</div>
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1.5">确认密码</label>
<div class="relative">
<div class="absolute left-3 top-1/2 transform -translate-y-1/2 text-slate-400">
<i class="fas fa-lock"></i>
</div>
<input type="password" id="resumePasswordConfirm" class="w-full pl-10 pr-4 py-2.5 border border-slate-200 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all" placeholder="再次输入密码">
</div>
</div>
</div>
<div class="flex items-center justify-center gap-3 mt-6">
<button type="button" onclick="closePasswordModal()" class="btn btn-secondary">
<i class="fas fa-times"></i> 取消
</button>
<button type="submit" class="btn btn-primary">
<i class="fas fa-save"></i> 保存
</button>
</div>
</form>
</div>
</div>
</div>
<script>
function getUserId() {
const pathParts = window.location.pathname.split('/');
return pathParts[pathParts.indexOf('user') + 1];
}
function openAddResumeModal() {
const userId = getUserId();
if (!userId) {
showCustomAlert('error', '错误', '用户ID无效');
return;
}
fetch('/dashboard/api/user/' + userId)
.then(response => response.json())
.then(user => {
document.getElementById('resumeModalTitle').innerHTML = '<i class="fas fa-file-alt text-blue-500"></i> 新建简历';
document.getElementById('resumeId').value = '';
document.getElementById('resumeUserId').value = userId;
document.getElementById('resumeRoute').value = user.username || 'resume-' + userId.substring(0, 8);
document.getElementById('resumeName').value = user.name || '';
document.getElementById('resumeTitle').value = '';
document.getElementById('resumeEmail').value = user.email || '';
document.getElementById('resumePhone').value = user.phone || '';
document.getElementById('resumeLocation').value = '';
document.getElementById('resumeShowInHome').checked = true;
document.getElementById('resumeModal').classList.remove('hidden');
})
.catch(error => {
showCustomAlert('error', '错误', '获取用户信息失败');
});
}
function closeResumeModal() {
document.getElementById('resumeModal').classList.add('hidden');
}
function editResume(id) {
fetch('/dashboard/api/resume/' + id)
.then(response => response.json())
.then(resume => {
document.getElementById('resumeModalTitle').innerHTML = '<i class="fas fa-file-edit text-yellow-500"></i> 编辑简历';
document.getElementById('resumeId').value = resume.id || resume.ID || '';
document.getElementById('resumeUserId').value = resume.user_id || resume.UserID || '';
document.getElementById('resumeRoute').value = resume.route || resume.Route || '';
document.getElementById('resumeName').value = resume.basic_info?.name || resume.BasicInfo?.Name || '';
document.getElementById('resumeTitle').value = resume.basic_info?.title || resume.BasicInfo?.Title || '';
document.getElementById('resumeEmail').value = resume.basic_info?.email || resume.BasicInfo?.Email || '';
document.getElementById('resumePhone').value = resume.basic_info?.phone || resume.BasicInfo?.Phone || '';
document.getElementById('resumeLocation').value = resume.basic_info?.location || resume.BasicInfo?.Location || '';
document.getElementById('resumeShowInHome').checked = resume.show_in_home || resume.ShowInHome || false;
document.getElementById('resumeModal').classList.remove('hidden');
})
.catch(error => {
console.error('Error:', error);
showCustomAlert('error', '错误', '获取简历信息失败');
});
}
function saveResume(event) {
event.preventDefault();
const id = document.getElementById('resumeId').value;
const userId = document.getElementById('resumeUserId').value;
const data = {
user_id: userId,
route: document.getElementById('resumeRoute').value,
show_in_home: document.getElementById('resumeShowInHome').checked,
basic_info: {
name: document.getElementById('resumeName').value,
title: document.getElementById('resumeTitle').value,
email: document.getElementById('resumeEmail').value,
phone: document.getElementById('resumePhone').value,
location: document.getElementById('resumeLocation').value
}
};
let url, method;
if (id) {
url = '/dashboard/api/resume/' + id;
method = 'PUT';
} else {
url = '/dashboard/api/resume/create';
method = 'POST';
}
fetch(url, {
method: method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
})
.then(response => response.json())
.then(result => {
if (result.success) {
showCustomAlert('success', '成功', '保存成功');
closeResumeModal();
setTimeout(() => location.reload(), 1500);
} else {
showCustomAlert('error', '错误', '保存失败:' + (result.message || '未知错误'));
}
})
.catch(error => {
console.error('Error:', error);
showCustomAlert('error', '错误', '保存失败');
});
}
function toggleShowInHome(id, currentValue) {
fetch('/dashboard/api/resume/' + id + '/show_in_home', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ show_in_home: !currentValue })
})
.then(response => response.json())
.then(result => {
if (result.success) {
location.reload();
} else {
showCustomAlert('error', '错误', '操作失败:' + (result.message || '未知错误'));
}
})
.catch(error => {
console.error('Error:', error);
showCustomAlert('error', '错误', '操作失败');
});
}
function setPassword(id) {
document.getElementById('passwordResumeId').value = id;
document.getElementById('resumePassword').value = '';
document.getElementById('resumePasswordConfirm').value = '';
document.getElementById('passwordModal').classList.remove('hidden');
}
function closePasswordModal() {
document.getElementById('passwordModal').classList.add('hidden');
}
function savePassword(event) {
event.preventDefault();
const id = document.getElementById('passwordResumeId').value;
const password = document.getElementById('resumePassword').value;
const confirmPassword = document.getElementById('resumePasswordConfirm').value;
if (password !== confirmPassword) {
showCustomAlert('error', '错误', '两次输入的密码不一致');
return;
}
fetch('/dashboard/api/resume/' + id + '/password', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password: password })
})
.then(response => response.json())
.then(result => {
if (result.success) {
showCustomAlert('success', '成功', '密码设置成功');
closePasswordModal();
setTimeout(() => location.reload(), 1500);
} else {
showCustomAlert('error', '错误', '设置失败:' + (result.message || '未知错误'));
}
})
.catch(error => {
console.error('Error:', error);
showCustomAlert('error', '错误', '设置失败');
});
}
function deleteResume(id) {
showCustomConfirm('确认删除', '确定要删除该简历吗?此操作不可恢复。', function(confirmed) {
if (!confirmed) return;
fetch('/dashboard/api/resume/' + id, { method: 'DELETE' })
.then(response => response.json())
.then(result => {
if (result.success) {
location.reload();
} else {
showCustomAlert('error', '错误', '删除失败:' + (result.message || '未知错误'));
}
})
.catch(error => {
console.error('Error:', error);
showCustomAlert('error', '错误', '删除失败');
});
});
}
</script>