S
思睿途岸教育
SOP 标准流程管理系统
用户名
密码
登 录
使用统一账号登录
S
思睿途岸
SOP 标准流程
?
新建 SOP
分类
▦
全部流程
0
📝
教学批改
0
⭐
销售转化
0
⚙️
日常运营
0
💬
学员服务
0
👥
人员管理
0
全部流程
管理团队标准操作流程,确保执行一致性
🔍
最新创建
最早创建
名称排序
步骤数量
←
编辑
删除
操作步骤
新建 SOP 流程
×
流程名称 *
分类 *
教学批改
销售转化
日常运营
学员服务
人员管理
流程说明
负责岗位
预计耗时
操作步骤
添加步骤
/* ===== AUTH ===== */ const API_BASE = '/api'; let currentUser = null; function checkLogin() { const token = localStorage.getItem('unified_token'); const userStr = localStorage.getItem('unified_user'); if (token && userStr) { try { currentUser = JSON.parse(userStr); showApp(); return; } catch(e) {} } // Try auto-verify with token if (token) { fetch('/api/me', {headers:{'Authorization':'Bearer '+token}}) .then(r=>r.json()) .then(data=>{ if (data.username) { currentUser = data; localStorage.setItem('unified_user', JSON.stringify(data)); showApp(); } else { showLogin(); } }) .catch(()=>showLogin()); return; } showLogin(); } function showLogin() { document.getElementById('loginOverlay').classList.remove('hidden'); document.querySelector('.app').style.display = 'none'; } function showApp() { document.getElementById('loginOverlay').classList.add('hidden'); document.querySelector('.app').style.display = 'flex'; // Show user in header const name = (currentUser.display_name || currentUser.username || ''); document.getElementById('headerUser').textContent = name.charAt(0).toUpperCase(); document.getElementById('headerUserName').textContent = name; loadData(); renderSOPs(); } async function doLogin() { const username = document.getElementById('loginUser').value.trim(); const password = document.getElementById('loginPass').value; const btn = document.getElementById('loginBtn'); const err = document.getElementById('loginErr'); if (!username || !password) { err.textContent = '请输入用户名和密码'; err.classList.add('show'); return; } btn.disabled = true; btn.textContent = '登录中...'; err.classList.remove('show'); try { const res = await fetch(API_BASE + '/login', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({username, password}) }); const data = await res.json(); if (data.token) { localStorage.setItem('unified_token', data.token); localStorage.setItem('unified_user', JSON.stringify(data.user)); currentUser = data.user; showApp(); } else { err.textContent = data.error || '登录失败,请检查用户名和密码'; err.classList.add('show'); } } catch(e) { err.textContent = '网络错误,请检查后重试'; err.classList.add('show'); } btn.disabled = false; btn.textContent = '登 录'; } function doLogout() { localStorage.removeItem('unified_token'); localStorage.removeItem('unified_user'); currentUser = null; showLogin(); document.getElementById('loginUser').value = ''; document.getElementById('loginPass').value = ''; } document.getElementById('loginPass').addEventListener('keydown', e=>{ if (e.key === 'Enter') doLogin(); }); /* ===== /AUTH ===== */ const STORAGE_KEY = 'sop_data_v1'; const CAT_LABELS = { teach:'教学批改', sales:'销售转化', ops:'日常运营', service:'学员服务', hr:'人员管理' }; const CAT_CLASSES = { teach:'tag-teach', sales:'tag-sales', ops:'tag-ops', service:'tag-service', hr:'tag-hr' }; let sops = []; let currentCat = 'all'; let currentDetailId = null; let editingId = null; let searchQuery = ''; let stepCount = 0; function loadData() { try { sops = JSON.parse(localStorage.getItem(STORAGE_KEY)) || []; } catch(e) { sops = []; } if (!sops.length) loadDemoData(); } function saveData() { localStorage.setItem(STORAGE_KEY, JSON.stringify(sops)); } function loadDemoData() { sops = [ { id: 1, cat: 'teach', name: '申论批改报告输出流程', owner: '批改老师', time: '30-45分钟', createdAt: Date.now() - 86400000*2, desc: '用于规范申论作文批改报告的完整输出流程,确保每份报告质量一致、反馈专业到位,提升学员批改体验。', steps: [ { title:'接收作文原稿', desc:'通过微信/学员群接收,确认学号、题目、字数,存入对应文件夹。' }, { title:'通读全文定基调', desc:'不带批语先通读一遍,判断文章整体水平,在脑中形成初步印象。' }, { title:'逐段精批注', desc:'按照「结构-论点-论据-语言」四维批注,用统一符号标注,重点问题红字标出。' }, { title:'撰写总批评语', desc:'200字以内写总批,先肯定亮点,再指出核心问题,给出具体改进方向。' }, { title:'填写评分表', desc:'按照申论评分标准打分,标注各维度得分,确保评分有依据。' }, { title:'导出PDF并发送', desc:'生成PDF批改报告,命名格式:学号_题目_日期,发送至学员并告知反馈窗口期。' } ] }, { id: 2, cat: 'sales', name: '新学员咨询转化流程', owner: '销售顾问', time: '20-30分钟', createdAt: Date.now() - 86400000*5, desc: '针对第一次咨询的潜在学员,从初次接触到完成报名的标准化转化流程,最大化报名转化率。', steps: [ { title:'主动破冰问候', desc:'30秒内回复,用名字称呼,问明来意,快速建立信任感。' }, { title:'了解基本情况', desc:'询问考试目标(省考/国考)、备考时长、申论薄弱点,建立学员画像。' }, { title:'精准痛点挖掘', desc:'通过追问找到核心痛点:是不会写?还是写了没分?还是不知道评分标准?' }, { title:'针对性产品介绍', desc:'根据痛点匹配产品(单次批改/月卡/VIP私教),重点讲差异化价值。' }, { title:'处理异议促成交', desc:'针对「贵」「再想想」「不确定有没有用」分别有应对话术,引导体验首次批改。' }, { title:'完成报名+交接', desc:'确认报名信息,拉入学员群,交接给课程老师,发送欢迎消息。' } ] }, { id: 3, cat: 'ops', name: '每日内容发布流程', owner: '新媒体运营', time: '60分钟', createdAt: Date.now() - 86400000, desc: '小红书/抖音每日内容生产和发布的标准流程,保证内容质量稳定输出,维持账号活跃度。', steps: [ { title:'选题与脚本', desc:'从选题库中选取当日话题,30分钟内完成口播脚本撰写,字数控制在300-400字。' }, { title:'录制视频', desc:'补妆整理仪表,检查背景光线,录制3-5条备选,选取最佳。' }, { title:'剪辑处理', desc:'剪掉口误/停顿,添加字幕,配背景音乐,片头片尾统一模板。' }, { title:'封面设计', desc:'制作封面图,标题字体>正文字体,关键词突出,符合平台调性。' }, { title:'发布+互动', desc:'按最佳时间段发布(18:00-21:00),发布后30分钟内回复所有评论。' } ] } ]; saveData(); } /* ===== RENDER ===== */ function renderSOPs() { const grid = document.getElementById('sopGrid'); let filtered = currentCat === 'all' ? [...sops] : sops.filter(s => s.cat === currentCat); if (searchQuery) { const q = searchQuery.toLowerCase(); filtered = filtered.filter(s => s.name.toLowerCase().includes(q) || (s.desc && s.desc.toLowerCase().includes(q)) || (s.owner && s.owner.toLowerCase().includes(q)) ); } const sort = document.getElementById('sortSelect').value; if (sort === 'newest') filtered.sort((a,b) => b.createdAt - a.createdAt); else if (sort === 'oldest') filtered.sort((a,b) => a.createdAt - b.createdAt); else if (sort === 'alpha') filtered.sort((a,b) => a.name.localeCompare(b.name, 'zh')); else if (sort === 'steps') filtered.sort((a,b) => b.steps.length - a.steps.length); grid.innerHTML = ''; filtered.forEach(sop => { const card = document.createElement('div'); card.className = 'sop-card'; card.onclick = () => openDetail(sop.id); const dots = sop.steps.slice(0,5).map((_,i) => `
` ).join(''); const extra = sop.steps.length > 5 ? `
+${sop.steps.length-5}
` : ''; card.innerHTML = `
${esc(sop.name)}
${CAT_LABELS[sop.cat]}
${esc(sop.desc)||'暂无描述'}
${sop.owner?`
👤 ${esc(sop.owner)}
`:''} ${sop.time?`
⏱ ${esc(sop.time)}
`:''}
${dots}${extra}
${sop.steps.length}步
`; grid.appendChild(card); }); // "new" card const nc = document.createElement('div'); nc.className = 'sop-card-new'; nc.onclick = openCreateModal; nc.innerHTML = `
+
新建 SOP 流程
`; grid.appendChild(nc); if (!filtered.length && searchQuery) { grid.innerHTML = `
🔍
未找到相关流程
尝试调整关键词或清空搜索
` + nc.outerHTML; } updateBadges(); } function updateBadges() { ['all','teach','sales','ops','service','hr'].forEach(c => { const el = document.getElementById('badge-'+c); if (el) el.textContent = c==='all' ? sops.length : sops.filter(s=>s.cat===c).length; }); } /* ===== FILTER ===== */ function filterCat(cat, el) { currentCat = cat; document.querySelectorAll('.sidebar-item').forEach(i=>i.classList.remove('active')); el.classList.add('active'); const names = {all:'全部流程',teach:'教学批改',sales:'销售转化',ops:'日常运营',service:'学员服务',hr:'人员管理'}; document.getElementById('currentCatTitle').textContent = names[cat]; closeDetail(); renderSOPs(); } function handleSearch() { searchQuery = document.getElementById('searchInput').value.trim(); renderSOPs(); } /* ===== DETAIL ===== */ function openDetail(id) { const sop = sops.find(s=>s.id===id); if (!sop) return; currentDetailId = id; document.getElementById('detailTitle').textContent = sop.name; const tag = document.getElementById('detailTag'); tag.textContent = CAT_LABELS[sop.cat]; tag.className = 'sop-card-tag ' + CAT_CLASSES[sop.cat]; document.getElementById('detailDesc').textContent = sop.desc || '暂无描述。'; const meta = document.getElementById('detailMeta'); meta.innerHTML = (sop.owner?`
👤 ${esc(sop.owner)}
`:'') + (sop.time?`
⏱ ${esc(sop.time)}
`:'') + `
${sop.steps.length} 个步骤
` + `
更新于 ${new Date(sop.createdAt).toLocaleDateString('zh-CN')}
`; document.getElementById('detailSteps').innerHTML = sop.steps.map((st,i)=>`
${i+1}
${esc(st.title)}
${st.desc?`
${esc(st.desc)}
`:''}
`).join(''); document.getElementById('gridView').classList.add('hidden'); document.getElementById('detailView').classList.add('active'); } function closeDetail() { currentDetailId = null; document.getElementById('detailView').classList.remove('active'); document.getElementById('gridView').classList.remove('hidden'); } function editCurrentSOP() { if(!currentDetailId) return; closeDetail(); openEditModal(currentDetailId); } function deleteCurrentSOP() { if(!currentDetailId) return; if(!confirm('确认删除此 SOP?此操作不可撤销。')) return; sops = sops.filter(s=>s.id!==currentDetailId); saveData(); closeDetail(); renderSOPs(); showToast('已删除'); } /* ===== MODAL ===== */ function openCreateModal() { editingId = null; document.getElementById('modalTitle').textContent = '新建 SOP 流程'; ['sopName','sopDesc','sopOwner','sopTime'].forEach(id=>document.getElementById(id).value=''); document.getElementById('sopCat').value='teach'; document.getElementById('stepsContainer').innerHTML=''; stepCount=0; addStep(); addStep(); addStep(); openModal(); } function openEditModal(id) { const sop = sops.find(s=>s.id===id); if(!sop) return; editingId = id; document.getElementById('modalTitle').textContent = '编辑 SOP 流程'; document.getElementById('sopName').value = sop.name; document.getElementById('sopCat').value = sop.cat; document.getElementById('sopDesc').value = sop.desc||''; document.getElementById('sopOwner').value = sop.owner||''; document.getElementById('sopTime').value = sop.time||''; document.getElementById('stepsContainer').innerHTML=''; stepCount=0; sop.steps.forEach(st=>addStep(st.title,st.desc)); openModal(); } function openModal() { document.getElementById('modalOverlay').classList.add('open'); setTimeout(()=>document.getElementById('sopName').focus(),200); } function closeModal() { document.getElementById('modalOverlay').classList.remove('open'); } function handleOverlayClick(e) { if(e.target===document.getElementById('modalOverlay')) closeModal(); } function addStep(title='', desc='') { stepCount++; const div = document.createElement('div'); div.className = 'step-item'; div.innerHTML = `
${document.getElementById('stepsContainer').children.length+1}
${esc(desc)}
×
`; document.getElementById('stepsContainer').appendChild(div); renumberSteps(); } function renumberSteps() { document.querySelectorAll('.step-item').forEach((el,i)=>{ el.querySelector('.step-num').textContent = i+1; }); } function saveSOP() { const name = document.getElementById('sopName').value.trim(); if (!name) { document.getElementById('sopName').focus(); showToast('请输入流程名称'); return; } const steps = []; document.querySelectorAll('.step-item').forEach(el=>{ const t = el.querySelector('.step-title-input').value.trim(); const d = el.querySelector('.step-desc-input').value.trim(); if (t) steps.push({title:t, desc:d}); }); if (!steps.length) { showToast('至少添加一个步骤'); return; } if (editingId) { const idx = sops.findIndex(s=>s.id===editingId); if (idx>-1) sops[idx] = {...sops[idx], name, cat:document.getElementById('sopCat').value, desc:document.getElementById('sopDesc').value.trim(), owner:document.getElementById('sopOwner').value.trim(), time:document.getElementById('sopTime').value.trim(), steps, createdAt:Date.now()}; showToast('SOP 已更新 ✓'); } else { sops.unshift({id:Date.now(), name, cat:document.getElementById('sopCat').value, desc:document.getElementById('sopDesc').value.trim(), owner:document.getElementById('sopOwner').value.trim(), time:document.getElementById('sopTime').value.trim(), steps, createdAt:Date.now()}); showToast('SOP 已保存 ✓'); } saveData(); closeModal(); renderSOPs(); } /* ===== TOAST ===== */ let toastTimer; function showToast(msg) { const t = document.getElementById('toast'); document.getElementById('toastMsg').textContent = msg; t.classList.add('show'); clearTimeout(toastTimer); toastTimer = setTimeout(()=>t.classList.remove('show'),2500); } /* ===== KEYBOARD ===== */ document.addEventListener('keydown', e=>{ if (e.key==='Escape') { if (document.getElementById('modalOverlay').classList.contains('open')) closeModal(); else if (document.getElementById('detailView').classList.contains('active')) closeDetail(); } if ((e.ctrlKey||e.metaKey)&&e.key==='Enter'&&document.getElementById('modalOverlay').classList.contains('open')) saveSOP(); }); function esc(s) { return String(s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); } /* ===== INIT ===== */ checkLogin();