Commit 538d5d0eb0d76ee84b13af261d9c1cfadb367dfb
1 parent
91d38a5c
feat(company): add new company creation modal and delete functionality, fix test isolation issue
Showing
5 changed files
with
120 additions
and
3 deletions
server/routes/company.js
| ... | ... | @@ -641,6 +641,13 @@ router.put('/:companyId', (req, res) => { |
| 641 | 641 | res.json({ success: true, company }); |
| 642 | 642 | }); |
| 643 | 643 | |
| 644 | +// DELETE /api/company/:companyId | |
| 645 | +router.delete('/:companyId', (req, res) => { | |
| 646 | + const company = companyService.delete(req.params.companyId); | |
| 647 | + if (!company) return res.status(404).json({ error: '公司不存在' }); | |
| 648 | + res.json({ success: true, company }); | |
| 649 | +}); | |
| 650 | + | |
| 644 | 651 | // POST /api/company/:companyId/shutdown |
| 645 | 652 | router.post('/:companyId/shutdown', (req, res) => { |
| 646 | 653 | const company = companyService.shutdown(req.params.companyId); | ... | ... |
server/services/company-service.js
| ... | ... | @@ -154,6 +154,23 @@ class CompanyService { |
| 154 | 154 | return company; |
| 155 | 155 | } |
| 156 | 156 | |
| 157 | + /** | |
| 158 | + * 删除公司 (仅删除 company.json 定义即可,暂时不清理目录数据) | |
| 159 | + */ | |
| 160 | + delete(companyId) { | |
| 161 | + const company = this.get(companyId); | |
| 162 | + if (!company) return null; | |
| 163 | + | |
| 164 | + const fp = path.join(DATA_DIR, companyId, 'company.json'); | |
| 165 | + if (fs.existsSync(fp)) { | |
| 166 | + // 通过重命名或者直接删除 company.json 来让 list 无法读取 | |
| 167 | + fs.renameSync(fp, path.join(DATA_DIR, companyId, 'company.deleted.json')); | |
| 168 | + } | |
| 169 | + | |
| 170 | + console.log(`[Company] 🗑️ 删除公司: ${companyId}`); | |
| 171 | + return company; | |
| 172 | + } | |
| 173 | + | |
| 157 | 174 | // ═══════════════════════════════════════ |
| 158 | 175 | // Agent 招聘系统 (Hiring Pipeline) |
| 159 | 176 | // ═══════════════════════════════════════ | ... | ... |
server/services/skill-db.js
| ... | ... | @@ -18,7 +18,7 @@ const config = require('../config'); |
| 18 | 18 | const { initSkillDir, getTargetDir, resolveSkillDir } = require('./skill-dir-manager'); |
| 19 | 19 | |
| 20 | 20 | const DATA_DIR = path.resolve(__dirname, '../../../data'); |
| 21 | -const DB_PATH = path.join(DATA_DIR, 'skills.sqlite'); | |
| 21 | +const DB_PATH = process.env.SKILLS_DB_PATH || path.join(DATA_DIR, 'skills.sqlite'); | |
| 22 | 22 | |
| 23 | 23 | let db = null; |
| 24 | 24 | ... | ... |
web/src/components/AppNav.vue
| ... | ... | @@ -41,8 +41,8 @@ |
| 41 | 41 | <div v-if="companies.length === 0" class="nav-dropdown-item text-muted">暂无公司</div> |
| 42 | 42 | </div> |
| 43 | 43 | <div class="nav-dropdown-divider"></div> |
| 44 | - <div class="nav-dropdown-item create-action" style="cursor:default; border:none; opacity: 0.6;"> | |
| 45 | - 请唤醒 MasterBot 自动创建 | |
| 44 | + <div class="nav-dropdown-item create-action clickable" @click="showNewCompanyModal = true; showDropdown = false"> | |
| 45 | + + 新增公司 | |
| 46 | 46 | </div> |
| 47 | 47 | </div> |
| 48 | 48 | </div> |
| ... | ... | @@ -79,12 +79,44 @@ |
| 79 | 79 | </button> |
| 80 | 80 | <span class="nav-version">v5.0</span> |
| 81 | 81 | </div> |
| 82 | + | |
| 83 | + <!-- New Company Modal --> | |
| 84 | + <div v-if="showNewCompanyModal" class="nav-modal-overlay" @click="showNewCompanyModal = false"> | |
| 85 | + <div class="nav-modal-content" @click.stop> | |
| 86 | + <div class="nav-modal-header"> | |
| 87 | + <h3>新增空壳公司</h3> | |
| 88 | + <button class="nav-modal-close" @click="showNewCompanyModal = false">✕</button> | |
| 89 | + </div> | |
| 90 | + <div class="nav-modal-body"> | |
| 91 | + <div class="form-group"> | |
| 92 | + <label>公司 ID (小写字母/数字)</label> | |
| 93 | + <input v-model="newCompanyForm.id" placeholder="例如: my-company" /> | |
| 94 | + </div> | |
| 95 | + <div class="form-group"> | |
| 96 | + <label>公司名称</label> | |
| 97 | + <input v-model="newCompanyForm.name" placeholder="例如: 柠檬科技有限公司" /> | |
| 98 | + </div> | |
| 99 | + <div class="form-group"> | |
| 100 | + <label>公司简介</label> | |
| 101 | + <textarea v-model="newCompanyForm.description" placeholder="简要描述公司的主要业务..." rows="3"></textarea> | |
| 102 | + </div> | |
| 103 | + <p class="nav-modal-tip">创建成功后将是空壳公司,需唤醒 MasterBot 招聘员工。</p> | |
| 104 | + </div> | |
| 105 | + <div class="nav-modal-footer"> | |
| 106 | + <button class="nav-btn-cancel" @click="showNewCompanyModal = false">取消</button> | |
| 107 | + <button class="nav-btn-submit" :disabled="isCreatingCompany" @click="submitNewCompany"> | |
| 108 | + {{ isCreatingCompany ? '⏳ 创建中...' : '🎉 确认创建' }} | |
| 109 | + </button> | |
| 110 | + </div> | |
| 111 | + </div> | |
| 112 | + </div> | |
| 82 | 113 | </nav> |
| 83 | 114 | </template> |
| 84 | 115 | |
| 85 | 116 | <script setup> |
| 86 | 117 | import { ref, computed, watch, onMounted, onUnmounted } from 'vue' |
| 87 | 118 | import { useRoute, useRouter } from 'vue-router' |
| 119 | +import axios from 'axios' | |
| 88 | 120 | import { useTheme } from '@/composables/useTheme' |
| 89 | 121 | import { useCompanies } from '@/composables/useCompanies' |
| 90 | 122 | |
| ... | ... | @@ -94,6 +126,9 @@ const { themeMode, cycleTheme } = useTheme() |
| 94 | 126 | const { companies, loadCompanies, activeCompanyId, setActiveCompany } = useCompanies() |
| 95 | 127 | |
| 96 | 128 | const showDropdown = ref(false) |
| 129 | +const showNewCompanyModal = ref(false) | |
| 130 | +const isCreatingCompany = ref(false) | |
| 131 | +const newCompanyForm = ref({ id: '', name: '', description: '' }) | |
| 97 | 132 | |
| 98 | 133 | const themeIcon = computed(() => { |
| 99 | 134 | if (themeMode.value === 'light') return '☀️' |
| ... | ... | @@ -135,6 +170,24 @@ onMounted(async () => { |
| 135 | 170 | onUnmounted(() => { |
| 136 | 171 | document.removeEventListener('click', closeDropdown) |
| 137 | 172 | }) |
| 173 | + | |
| 174 | +async function submitNewCompany() { | |
| 175 | + if (!newCompanyForm.value.id || !newCompanyForm.value.name) return alert('请填写公司ID和名称') | |
| 176 | + isCreatingCompany.value = true | |
| 177 | + try { | |
| 178 | + const res = await axios.post('/api/company/create', newCompanyForm.value) | |
| 179 | + if (res.data.success) { | |
| 180 | + alert('创建成功') | |
| 181 | + showNewCompanyModal.value = false | |
| 182 | + await loadCompanies() | |
| 183 | + router.push(`/${res.data.company.id}/overview`) | |
| 184 | + newCompanyForm.value = { id: '', name: '', description: '' } | |
| 185 | + } | |
| 186 | + } catch(e) { | |
| 187 | + alert('创建失败: ' + (e.response?.data?.error || e.message)) | |
| 188 | + } | |
| 189 | + isCreatingCompany.value = false | |
| 190 | +} | |
| 138 | 191 | </script> |
| 139 | 192 | |
| 140 | 193 | <style scoped> |
| ... | ... | @@ -198,4 +251,23 @@ onUnmounted(() => { |
| 198 | 251 | } |
| 199 | 252 | .theme-btn:hover { background: var(--bg-surface-hover); transform: scale(1.1); } |
| 200 | 253 | .nav-version { font-size: 10px; color: var(--text-faint); padding: 2px 6px; border: 1px solid var(--border-primary); border-radius: 4px; font-weight:600;} |
| 254 | + | |
| 255 | +/* Modal */ | |
| 256 | +.nav-modal-overlay { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 2000; backdrop-filter: blur(4px); } | |
| 257 | +.nav-modal-content { background: var(--bg-primary); border-radius: 12px; width: 400px; padding: 20px; box-shadow: 0 10px 40px rgba(0,0,0,0.2); border: 1px solid var(--border-primary); } | |
| 258 | +.nav-modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; } | |
| 259 | +.nav-modal-header h3 { margin: 0; font-size: 16px; color: var(--text-primary); } | |
| 260 | +.nav-modal-close { background: transparent; border: none; font-size: 16px; cursor: pointer; color: var(--text-secondary); } | |
| 261 | +.nav-modal-body .form-group { margin-bottom: 12px; } | |
| 262 | +.nav-modal-body label { display: block; font-size: 13px; font-weight: 600; color: var(--text-secondary); margin-bottom: 6px; } | |
| 263 | +.nav-modal-body input, .nav-modal-body textarea { width: 100%; padding: 8px 10px; border: 1px solid var(--border-primary); border-radius: 6px; background: var(--bg-surface); color: var(--text-primary); font-size: 13px; box-sizing: border-box; } | |
| 264 | +.nav-modal-body input:focus, .nav-modal-body textarea:focus { outline: none; border-color: var(--accent); } | |
| 265 | +.nav-modal-tip { font-size: 12px; color: var(--text-muted); margin-top: 12px; } | |
| 266 | +.nav-modal-footer { display: flex; justify-content: flex-end; gap: 12px; margin-top: 24px; } | |
| 267 | +.nav-btn-cancel { padding: 6px 16px; border-radius: 6px; border: 1px solid var(--border-primary); background: var(--bg-surface); cursor: pointer; color: var(--text-secondary); font-size: 13px; font-weight: 600; transition: all 0.2s; } | |
| 268 | +.nav-btn-cancel:hover { background: var(--bg-surface-hover); } | |
| 269 | +.nav-btn-submit { padding: 6px 16px; border-radius: 6px; border: none; background: var(--accent); cursor: pointer; color: white; font-size: 13px; font-weight: 600; transition: all 0.2s; } | |
| 270 | +.nav-btn-submit:hover { background: var(--accent-hover); } | |
| 271 | +.nav-btn-submit:disabled { opacity: 0.6; cursor: not-allowed; } | |
| 272 | +.create-action.clickable { opacity: 1; cursor: pointer; border: 1px dashed var(--border-active); } | |
| 201 | 273 | </style> | ... | ... |
web/src/views/CompanyDashboard.vue
| ... | ... | @@ -105,6 +105,11 @@ |
| 105 | 105 | {{ isStopping ? '⏳ 关闭中...' : '🛑 关闭公司' }} |
| 106 | 106 | </button> |
| 107 | 107 | <button class="btn-refresh" @click="loadCompanyStatus(selectedCompany.id)">🔄 刷新状态</button> |
| 108 | + <button | |
| 109 | + class="btn-delete" | |
| 110 | + style="background-color: var(--status-failed-bg); color: var(--status-failed); border: 1px solid var(--status-failed-bg); padding: 4px 12px; border-radius: 6px; font-size: 13px; font-weight: 600; cursor: pointer; margin-left: auto;" | |
| 111 | + @click="deleteCompany(selectedCompany.id)" | |
| 112 | + >🗑️ 删除公司</button> | |
| 108 | 113 | </div> |
| 109 | 114 | </div> |
| 110 | 115 | |
| ... | ... | @@ -1060,6 +1065,22 @@ async function stopCompany() { |
| 1060 | 1065 | isStopping.value = false |
| 1061 | 1066 | } |
| 1062 | 1067 | |
| 1068 | +async function deleteCompany(companyId) { | |
| 1069 | + if (!confirm('确认要删除该公司吗?该操作仅会从列表中移除公司声明。')) return | |
| 1070 | + try { | |
| 1071 | + await axios.delete(`/api/company/${companyId}`) | |
| 1072 | + await loadCompanies() | |
| 1073 | + if (companies.value.length > 0) { | |
| 1074 | + router.push(`/${companies.value[0].id}/overview`) | |
| 1075 | + } else { | |
| 1076 | + router.push(`/company`) | |
| 1077 | + } | |
| 1078 | + } catch (e) { | |
| 1079 | + console.error(e) | |
| 1080 | + alert('删除失败: ' + (e.response?.data?.error || e.message)) | |
| 1081 | + } | |
| 1082 | +} | |
| 1083 | + | |
| 1063 | 1084 | function formatDate(isoStr) { |
| 1064 | 1085 | if (!isoStr) return '-' |
| 1065 | 1086 | return new Date(isoStr).toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }) | ... | ... |