diff --git a/zhiyin-app/src/pages/admin/admin.vue b/zhiyin-app/src/pages/admin/admin.vue index 3a0b128..ba1dc8d 100644 --- a/zhiyin-app/src/pages/admin/admin.vue +++ b/zhiyin-app/src/pages/admin/admin.vue @@ -21,6 +21,7 @@ 订单 定价 分享 + 岗位 管理 @@ -272,6 +273,31 @@ 暂无访问记录 + + + + + + + + + + {{ p.category === 'ai' ? 'AI' : '传统' }} + + {{ p.name }} + {{ p.company || '-' }} · {{ p.salary || '-' }} · sort:{{ p.sort }} + + + + 编辑 + 删除 + + + 暂无岗位 + + 加载中... + + @@ -286,6 +312,32 @@ + + + + + {{ posModal.isNew ? '新增岗位' : '编辑岗位' }} + 岗位名称 + 薪资 + 公司 + 排序 + 分类 + + {{ posForm.category === 'ai' ? 'AI岗位' : '传统岗位' }} + + + 启用 + + {{ posForm.active ? '启用' : '停用' }} + + + + + + + + + @@ -356,6 +408,20 @@ const sprintPriceTemp = ref(49.9) const growthFeaturesText = ref('') const sprintFeaturesText = ref('') +// Position management +const positions = ref([]) +const posLoading = ref(false) +const posModal = ref({ show: false, isNew: false }) +const posForm = reactive({ + name: '', + salary: '', + company: '', + icon: '', + sort: 0, + active: true, + category: 'ai', +}) + const growthPriceDisplay = computed(() => growthPriceTemp.value.toFixed(1)) const sprintPriceDisplay = computed(() => sprintPriceTemp.value.toFixed(1)) @@ -421,6 +487,7 @@ const switchTab = (t) => { tab.value = t if (t === 'users' && users.value.length === 0) loadUsers() if (t === 'interviews' && interviews.value.length === 0) loadInterviews() + if (t === 'positions') loadPositions() if (t === 'resumes' && resumes.value.length === 0) loadResumes() if (t === 'admins' && adminList.value.length === 0) loadAdmins() if (t === 'pricing') loadPricing() @@ -540,6 +607,84 @@ const loadAdmins = async () => { } catch(e) { console.error(e) } } +// ─── 岗位管理 ────────────────────── +const apiPositions = (path, opts = {}) => { + return uni.request({ + url: api('/positions' + path), + method: opts.method || 'POST', + header: { 'Authorization': `Bearer ${token()}`, 'Content-Type': 'application/json', ...opts.headers }, + data: opts.body || opts.data, + }) +} + +const loadPositions = async () => { + posLoading.value = true + try { + const res = await apiPositions('/admin/list') + if (res.statusCode === 200) positions.value = res.data || [] + } catch (e) { console.error(e) } + finally { posLoading.value = false } +} + +const openPositionModal = (position) => { + if (position) { + posForm.name = position.name || '' + posForm.salary = position.salary || '' + posForm.company = position.company || '' + posForm.icon = position.icon || '' + posForm.sort = position.sort ?? 0 + posForm.active = position.active ?? true + posForm.category = position.category || 'traditional' + posModal.value = { show: true, isNew: false } + } else { + posForm.name = '' + posForm.salary = '' + posForm.company = '' + posForm.icon = '' + posForm.sort = positions.value.length + 1 + posForm.active = true + posForm.category = 'ai' + posModal.value = { show: true, isNew: true } + } +} + +const closePositionModal = () => { + posModal.value = { show: false, isNew: false } +} + +const savePosition = async () => { + if (!posForm.name.trim()) { + uni.showToast({ title: '岗位名称不能为空', icon: 'none' }) + return + } + try { + const res = await apiPositions('/admin/save', { + method: 'POST', body: { ...posForm }, + }) + if (res.statusCode === 200) { + uni.showToast({ title: '保存成功', icon: 'success' }) + closePositionModal() + loadPositions() + } else throw new Error() + } catch { uni.showToast({ title: '保存失败', icon: 'none' }) } +} + +const deletePosition = (id, name) => { + uni.showModal({ + title: '删除岗位', content: `确定删除"${name}"?`, + success: async (r) => { + if (!r.confirm) return + try { + const res = await apiPositions('/admin/' + id, { method: 'DELETE' }) + if (res.statusCode === 200) { + uni.showToast({ title: '已删除', icon: 'success' }) + loadPositions() + } else throw new Error() + } catch { uni.showToast({ title: '删除失败', icon: 'none' }) } + }, + }) +} + const searchAdmin = async () => { if (!adminKeyword.value.trim()) return try { @@ -740,4 +885,18 @@ const doAdjustCredits = async () => { .modal-btn { flex: 1; height: 72rpx; border-radius: var(--radius-sm); font-size: 26rpx; border: none; } .modal-btn.cancel { background: #F3F4F6; color: var(--color-text-secondary); } .modal-btn.confirm { background: var(--color-primary); color: #FFF; } +/* ─── 岗位管理 ───── */ +.position-mgr-list { display: flex; flex-direction: column; gap: 8rpx; } +.pos-mgr-row { background: #FFF; padding: 16rpx; border-radius: var(--radius-sm); display: flex; align-items: center; gap: 12rpx; } +.pos-mgr-main { flex: 1; display: flex; align-items: center; gap: 12rpx; } +.pos-mgr-cat { font-size: 18rpx; padding: 2rpx 12rpx; border-radius: var(--radius-round); font-weight: 600; } +.pos-mgr-cat.cat-ai { background: #EEF2FF; color: var(--color-primary); } +.pos-mgr-cat.cat-tr { background: #F3F4F6; color: var(--color-text-tertiary); } +.pos-mgr-body { display: flex; flex-direction: column; } +.pos-mgr-name { font-size: 24rpx; font-weight: 600; color: var(--color-text); } +.pos-mgr-meta { font-size: 20rpx; color: var(--color-text-tertiary); margin-top: 2rpx; } +.pos-mgr-actions { display: flex; gap: 8rpx; } +.pos-mgr-btn { font-size: 20rpx; padding: 4rpx 16rpx; border-radius: var(--radius-round); } +.pos-mgr-btn.edit { color: var(--color-primary); border: 2rpx solid var(--color-primary); } +.pos-mgr-btn.del { color: #EF4444; border: 2rpx solid #EF4444; } diff --git a/zhiyin-app/src/pages/index/index.vue b/zhiyin-app/src/pages/index/index.vue index ea688d5..8238c27 100644 --- a/zhiyin-app/src/pages/index/index.vue +++ b/zhiyin-app/src/pages/index/index.vue @@ -43,6 +43,15 @@ 开始 + + + + 🎯 + AI 择业顾问 + + 专业分析 · 岗位匹配 · 智能推荐 + + @@ -246,6 +255,7 @@ const goProgress = () => uni.navigateTo({ url: '/pages/progress/progress' }) const goContribute = () => uni.navigateTo({ url: '/pages/contribute/contribute' }) const goBank = () => uni.navigateTo({ url: '/pages/company-bank/bank' }) const goInternship = () => uni.navigateTo({ url: '/pages/internship/internship' }) +const goCareer = () => uni.navigateTo({ url: '/pages/career/career' }) const startInterview = (pos) => uni.navigateTo({ url: `/pages/interview/interview?position=${encodeURIComponent(pos.name)}` })