feat: add positions management admin tab + career advisor homepage entry
- admin.vue: new '岗位' tab with CRUD list/modal/api functions/styles - index.vue: add AI择业顾问 entry card linking to career page - Backend CRUD endpoints already exist, no backend changes needed
This commit is contained in:
@@ -21,6 +21,7 @@
|
||||
<text class="tab" :class="{ active: tab === 'orders' }" @click="switchTab('orders')">订单</text>
|
||||
<text class="tab" :class="{ active: tab === 'pricing' }" @click="switchTab('pricing')">定价</text>
|
||||
<text class="tab" :class="{ active: tab === 'share' }" @click="switchTab('share')">分享</text>
|
||||
<text class="tab" :class="{ active: tab === 'positions' }" @click="switchTab('positions')">岗位</text>
|
||||
<text class="tab" :class="{ active: tab === 'admins' }" @click="switchTab('admins')">管理</text>
|
||||
</view>
|
||||
|
||||
@@ -272,6 +273,31 @@
|
||||
<text class="empty-text" v-if="!shareLoading && shareVisitors.length === 0">暂无访问记录</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 岗位管理 -->
|
||||
<view v-if="tab === 'positions'" class="section">
|
||||
<view class="search-bar">
|
||||
<text class="section-label" style="flex:1;margin:0">岗位列表({{ positions.length }})</text>
|
||||
<button class="search-btn" @click="openPositionModal(null)">新增岗位</button>
|
||||
</view>
|
||||
<view class="position-mgr-list" v-if="!posLoading">
|
||||
<view class="pos-mgr-row" v-for="p in positions" :key="p._id">
|
||||
<view class="pos-mgr-main">
|
||||
<text class="pos-mgr-cat" :class="p.category === 'ai' ? 'cat-ai' : 'cat-tr'">{{ p.category === 'ai' ? 'AI' : '传统' }}</text>
|
||||
<view class="pos-mgr-body">
|
||||
<text class="pos-mgr-name">{{ p.name }}</text>
|
||||
<text class="pos-mgr-meta">{{ p.company || '-' }} · {{ p.salary || '-' }} · sort:{{ p.sort }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="pos-mgr-actions">
|
||||
<text class="pos-mgr-btn edit" @click="openPositionModal(p)">编辑</text>
|
||||
<text class="pos-mgr-btn del" @click="deletePosition(p._id, p.name)">删除</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="empty-text" v-if="positions.length === 0 && !posLoading">暂无岗位</text>
|
||||
</view>
|
||||
<text class="loading-text" v-if="posLoading">加载中...</text>
|
||||
</view>
|
||||
|
||||
<!-- 额度调整弹窗 -->
|
||||
<view class="modal-mask" v-if="creditModal.show" @click="closeCreditModal">
|
||||
<view class="modal-content" @click.stop>
|
||||
@@ -286,6 +312,32 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 岗位编辑弹窗 -->
|
||||
<view class="modal-mask" v-if="posModal.show" @click="closePositionModal">
|
||||
<view class="modal-content" @click.stop>
|
||||
<text class="modal-title">{{ posModal.isNew ? '新增岗位' : '编辑岗位' }}</text>
|
||||
<view class="cfg-row"><text>岗位名称</text><input class="cfg-input" v-model="posForm.name" placeholder="必填" /></view>
|
||||
<view class="cfg-row"><text>薪资</text><input class="cfg-input" v-model="posForm.salary" placeholder="如 15-25K" /></view>
|
||||
<view class="cfg-row"><text>公司</text><input class="cfg-input" v-model="posForm.company" placeholder="如 字节跳动" /></view>
|
||||
<view class="cfg-row"><text>排序</text><input class="cfg-input" type="digit" v-model.number="posForm.sort" /></view>
|
||||
<view class="cfg-row"><text>分类</text>
|
||||
<picker :range="['AI岗位','传统岗位']" @change="e => posForm.category = e.detail.value === 0 ? 'ai' : 'traditional'">
|
||||
<text class="cfg-val">{{ posForm.category === 'ai' ? 'AI岗位' : '传统岗位' }}</text>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="cfg-row"><text>启用</text>
|
||||
<picker :range="['启用','停用']" @change="e => posForm.active = e.detail.value === 0" :value="posForm.active ? 0 : 1">
|
||||
<text class="cfg-val">{{ posForm.active ? '启用' : '停用' }}</text>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="modal-actions">
|
||||
<button class="modal-btn cancel" @click="closePositionModal">取消</button>
|
||||
<button class="modal-btn confirm" @click="savePosition">保存</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 管理员 -->
|
||||
<view v-if="tab === 'admins'" class="section">
|
||||
<view class="search-bar">
|
||||
@@ -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; }
|
||||
</style>
|
||||
|
||||
@@ -43,6 +43,15 @@
|
||||
</view>
|
||||
<text class="fp-action">开始</text>
|
||||
</view>
|
||||
<view class="feature-secondary" style="margin-bottom: 16rpx;">
|
||||
<view class="fs-card card" @click="goCareer">
|
||||
<view class="fs-top">
|
||||
<view class="fs-icon fs-progress"><text class="fs-emoji">🎯</text></view>
|
||||
<text class="fs-name">AI 择业顾问</text>
|
||||
</view>
|
||||
<text class="fs-brief">专业分析 · 岗位匹配 · 智能推荐</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="feature-tertiary">
|
||||
<view class="fs-card card" @click="goResume">
|
||||
<view class="fs-top">
|
||||
@@ -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)}` })
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user