首次提交:初始化项目代码
This commit is contained in:
@@ -0,0 +1,290 @@
|
||||
<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>
|
||||
<h3 class="font-semibold text-slate-800 flex items-center gap-2">
|
||||
<i class="fas fa-folder-open text-purple-500"></i> {{ if .Item.ID }}编辑项目{{ else }}新增项目{{ end }}
|
||||
</h3>
|
||||
<p class="text-sm text-slate-500 mt-1">{{ if .Item.ID }}修改作品集项目信息{{ else }}创建新的作品集项目{{ end }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<form id="portfolioForm">
|
||||
<input type="hidden" id="portfolioId" value="{{.Item.ID}}">
|
||||
<div class="space-y-6">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-1">选择人员 <span class="text-red-500">*</span></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}}" {{ if eq .ID $.Item.UserID }}selected{{ end }}>{{.Name}} - {{.Email}}</option>
|
||||
{{ end }}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-1">项目名称 <span class="text-red-500">*</span></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" value="{{.Item.Name}}" 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">{{.Item.Description}}</textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-1">技术栈</label>
|
||||
<input type="text" id="portfolioTechStack" class="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500" value="{{ join .Item.TechStack "," }}" placeholder="多个技术栈用逗号分隔">
|
||||
</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" value="{{.Item.URL}}" placeholder="https://">
|
||||
</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" value="{{.Item.StartDate}}">
|
||||
</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" value="{{.Item.EndDate}}">
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-1">访问密码</label>
|
||||
<input type="password" 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" value="{{.Item.Password}}" placeholder="留空则无需密码">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 mb-1">关联简历项目ID</label>
|
||||
<div class="space-y-2">
|
||||
<select id="portfolioProjectResumeId" class="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500">
|
||||
<option value="">请选择简历</option>
|
||||
{{ range .Resumes }}
|
||||
<option value="{{.ID}}" {{ if eq .ID $.Item.ResumeID }}selected{{ end }}>{{.BasicInfo.Name}} - {{.BasicInfo.Title}}</option>
|
||||
{{ end }}
|
||||
</select>
|
||||
<select id="portfolioProjectId" class="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500">
|
||||
<option value="">请选择项目</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" id="portfolioShowInResume" {{ if .Item.ShowInResume }}checked{{ end }} class="w-4 h-4 text-primary-600 rounded focus:ring-primary-500">
|
||||
<span class="text-sm font-medium text-slate-700">在简历中显示</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" id="portfolioHidden" {{ if .Item.Hidden }}checked{{ end }} class="w-4 h-4 text-primary-600 rounded focus:ring-2 focus:ring-primary-500">
|
||||
<span class="text-sm font-medium text-slate-700">隐藏项目</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6" style="height: calc(100vh - 26.25rem); min-height: 31.25rem;">
|
||||
<div class="flex flex-col">
|
||||
<label class="block text-sm font-medium text-slate-700 mb-1">项目文档 <span class="text-slate-400 text-xs">(Markdown格式,支持图片、表格、流程图)</span></label>
|
||||
<div class="flex-1 relative overflow-hidden rounded-lg border border-slate-300">
|
||||
<textarea id="portfolioDoc" class="w-full h-full px-4 py-2 font-mono text-sm resize-none">{{.Item.Doc}}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<label class="block text-sm font-medium text-slate-700 mb-1">预览</label>
|
||||
<div id="docPreview" class="flex-1 p-4 border border-slate-200 rounded-lg bg-slate-50 markdown-body overflow-y-auto"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="fixed bottom-0 left-0 right-0 bg-white border-t border-slate-200 px-6 py-4 shadow-lg z-50">
|
||||
<div class="flex items-center justify-center gap-3">
|
||||
<button onclick="goBack()" class="btn btn-secondary">
|
||||
<i class="fas fa-arrow-left"></i> 返回
|
||||
</button>
|
||||
<button onclick="savePortfolio()" class="btn btn-primary">
|
||||
<i class="fas fa-save"></i> 保存
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="height: 5rem;"></div>
|
||||
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.css">
|
||||
<script src="https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.5.0/github-markdown.min.css">
|
||||
<script>
|
||||
let simplemde;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
simplemde = new SimpleMDE({
|
||||
element: document.getElementById('portfolioDoc'),
|
||||
toolbar: [
|
||||
'bold', 'italic', 'strikethrough', 'heading',
|
||||
'heading-smaller', 'heading-bigger', 'heading-1', 'heading-2', 'heading-3',
|
||||
'|',
|
||||
'code', 'quote', 'unordered-list', 'ordered-list',
|
||||
'|',
|
||||
'link', 'image', 'table', 'horizontal-rule',
|
||||
'|',
|
||||
'preview', 'side-by-side', 'fullscreen',
|
||||
'|',
|
||||
'guide'
|
||||
],
|
||||
spellChecker: false,
|
||||
autoDownloadFontAwesome: true,
|
||||
previewRender: function(plainText) {
|
||||
return marked.parse(plainText) || '';
|
||||
}
|
||||
});
|
||||
|
||||
simplemde.codemirror.on('change', function() {
|
||||
const text = simplemde.value();
|
||||
const preview = document.getElementById('docPreview');
|
||||
try {
|
||||
preview.innerHTML = marked.parse(text) || '<p class="text-slate-400">暂无内容</p>';
|
||||
} catch (err) {
|
||||
preview.innerHTML = '<p class="text-red-500">预览解析错误</p>';
|
||||
}
|
||||
});
|
||||
|
||||
const initialText = simplemde.value();
|
||||
if (initialText) {
|
||||
try {
|
||||
document.getElementById('docPreview').innerHTML = marked.parse(initialText) || '<p class="text-slate-400">暂无内容</p>';
|
||||
} catch (err) {
|
||||
document.getElementById('docPreview').innerHTML = '<p class="text-red-500">预览解析错误</p>';
|
||||
}
|
||||
} else {
|
||||
document.getElementById('docPreview').innerHTML = '<p class="text-slate-400">暂无内容,开始编写项目文档...</p>';
|
||||
}
|
||||
|
||||
document.getElementById('portfolioUserId').addEventListener('change', function() {
|
||||
loadResumesByUser(this.value, '');
|
||||
});
|
||||
|
||||
document.getElementById('portfolioProjectResumeId').addEventListener('change', function() {
|
||||
loadProjectsByResume(this.value, 0);
|
||||
});
|
||||
|
||||
const userId = document.getElementById('portfolioUserId').value;
|
||||
const savedResumeId = "{{.Item.ResumeID}}";
|
||||
const savedProjectId = {{ if .Item.ProjectID }}{{.Item.ProjectID}}{{ else }}0{{ end }};
|
||||
if (userId) {
|
||||
loadResumesByUser(userId, savedResumeId, savedProjectId);
|
||||
}
|
||||
});
|
||||
|
||||
function loadResumesByUser(userId, selectedResumeId, autoLoadProjectId) {
|
||||
const resumeSelect = document.getElementById('portfolioProjectResumeId');
|
||||
resumeSelect.innerHTML = '<option value="">请选择简历</option>';
|
||||
|
||||
if (!userId) return;
|
||||
|
||||
fetch('/api/user/resumes?user_id=' + userId)
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
if (!result.success) return;
|
||||
const resumes = result.data || [];
|
||||
resumes.forEach(resume => {
|
||||
const option = document.createElement('option');
|
||||
option.value = resume.id;
|
||||
option.textContent = (resume.basic_info.name || '未命名') + ' - ' + (resume.basic_info.title || '');
|
||||
if (selectedResumeId && resume.id === selectedResumeId) {
|
||||
option.selected = true;
|
||||
}
|
||||
resumeSelect.appendChild(option);
|
||||
});
|
||||
// 恢复选中后,按需加载该简历下的项目
|
||||
if (selectedResumeId && autoLoadProjectId) {
|
||||
loadProjectsByResume(selectedResumeId, autoLoadProjectId);
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
function loadProjectsByResume(resumeId, selectedProjectId) {
|
||||
const projectSelect = document.getElementById('portfolioProjectId');
|
||||
projectSelect.innerHTML = '<option value="">请选择项目</option>';
|
||||
|
||||
if (!resumeId) return;
|
||||
|
||||
fetch('/dashboard/api/resume/' + resumeId)
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
if (!result.success) return;
|
||||
const resume = result.data || {};
|
||||
const projects = resume.projects || [];
|
||||
projects.forEach(project => {
|
||||
const option = document.createElement('option');
|
||||
option.value = project.id;
|
||||
option.textContent = project.name;
|
||||
if (selectedProjectId && project.id === selectedProjectId) {
|
||||
option.selected = true;
|
||||
}
|
||||
projectSelect.appendChild(option);
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
window.history.back();
|
||||
}
|
||||
|
||||
function savePortfolio() {
|
||||
const techStackStr = document.getElementById('portfolioTechStack').value;
|
||||
const techStack = techStackStr ? techStackStr.split(',').map(s => s.trim()).filter(s => s) : [];
|
||||
|
||||
const portfolioId = document.getElementById('portfolioId').value;
|
||||
const projectIdVal = document.getElementById('portfolioProjectId').value;
|
||||
|
||||
const data = {
|
||||
id: portfolioId ? parseInt(portfolioId) : 0,
|
||||
user_id: document.getElementById('portfolioUserId').value,
|
||||
name: document.getElementById('portfolioName').value,
|
||||
description: document.getElementById('portfolioDescription').value,
|
||||
tech_stack: techStack,
|
||||
url: document.getElementById('portfolioUrl').value,
|
||||
start_date: document.getElementById('portfolioStartDate').value,
|
||||
end_date: document.getElementById('portfolioEndDate').value,
|
||||
doc: simplemde.value(),
|
||||
show_in_resume: document.getElementById('portfolioShowInResume').checked,
|
||||
resume_id: document.getElementById('portfolioProjectResumeId').value,
|
||||
project_id: projectIdVal ? parseInt(projectIdVal) : 0,
|
||||
password: document.getElementById('portfolioPassword').value,
|
||||
hidden: document.getElementById('portfolioHidden').checked
|
||||
};
|
||||
|
||||
if (!data.user_id) {
|
||||
showCustomAlert('error', '错误', '请选择人员');
|
||||
return;
|
||||
}
|
||||
if (!data.name) {
|
||||
showCustomAlert('error', '错误', '请输入项目名称');
|
||||
return;
|
||||
}
|
||||
|
||||
const isEdit = parseInt(portfolioId) > 0;
|
||||
const url = isEdit ? '/dashboard/api/portfolio/' + portfolioId : '/dashboard/api/portfolio';
|
||||
const method = isEdit ? 'PUT' : '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', '成功', isEdit ? '项目更新成功' : '项目创建成功');
|
||||
setTimeout(() => goBack(), 1500);
|
||||
} else {
|
||||
showCustomAlert('error', '错误', '保存失败:' + (result.message || '未知错误'));
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
showCustomAlert('error', '错误', '保存失败');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user