Commit 538d5d0eb0d76ee84b13af261d9c1cfadb367dfb

Authored by Yi Bo
1 parent 91d38a5c

feat(company): add new company creation modal and delete functionality, fix test isolation issue

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 () =&gt; {
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(() =&gt; {
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' })
... ...