fix: 修复用户编辑和职位发布报错,优化首页跳转逻辑

- 修复超管编辑用户时password必填导致报错,改为更新时可选
- 修复单位管理员发布职位时organization_id必填校验失败
- 首页"一键进入"按钮跳转到公司列表页
- 成员单位卡片点击跳转到首页对应公司的职位列表
- 管理后台侧边栏新增"返回首页"入口

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
TianyangZhang 2026-03-27 13:36:39 +08:00
parent bb896f8922
commit 0fc9ad7971
7 changed files with 107 additions and 21 deletions

View File

@ -45,12 +45,17 @@ class UserSerializer(serializers.ModelSerializer):
class AdminUserSerializer(serializers.ModelSerializer):
"""超管用于创建/管理公司管理员账号"""
password = serializers.CharField(write_only=True, min_length=6)
password = serializers.CharField(write_only=True, min_length=6, required=False)
class Meta:
model = User
fields = ['id', 'username', 'email', 'phone', 'role', 'organization', 'password', 'is_active']
def validate(self, attrs):
if not self.instance and not attrs.get('password'):
raise serializers.ValidationError({'password': '创建用户时密码为必填项'})
return attrs
def create(self, validated_data):
password = validated_data.pop('password')
user = User(**validated_data)
@ -58,6 +63,15 @@ class AdminUserSerializer(serializers.ModelSerializer):
user.save()
return user
def update(self, instance, validated_data):
password = validated_data.pop('password', None)
for attr, value in validated_data.items():
setattr(instance, attr, value)
if password:
instance.set_password(password)
instance.save()
return instance
class SendCodeSerializer(serializers.Serializer):
"""发送验证码 serializer"""

View File

@ -18,7 +18,8 @@ class JobDetailSerializer(serializers.ModelSerializer):
organization_id = serializers.PrimaryKeyRelatedField(
source='organization',
queryset=Organization.objects.all(),
write_only=True
write_only=True,
required=False
)
class Meta:

View File

@ -1,9 +1,9 @@
<template>
<el-container style="height:100vh;overflow:hidden;flex-direction:column;">
<el-header style="background:#001529;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid #333;flex-shrink:0;">
<div style="color:#fff;font-weight:bold;font-size:16px;">管理后台</div>
<div class="admin-layout">
<header class="admin-header">
<div class="header-title">管理后台</div>
<el-dropdown>
<div style="color:#fff;cursor:pointer;display:flex;align-items:center;gap:8px;">
<div class="header-user">
<span>{{ auth.user?.email }}</span>
<el-icon><ArrowDown /></el-icon>
</div>
@ -13,10 +13,9 @@
</el-dropdown-menu>
</template>
</el-dropdown>
</el-header>
<el-container style="flex:1;overflow:hidden;">
<el-aside width="200px" style="background:#001529;overflow-y:auto;height:100%;">
<div style="padding:20px;color:#fff;font-weight:bold;font-size:16px;">菜单</div>
</header>
<div class="admin-body">
<aside class="admin-aside">
<el-menu router :default-active="$route.path" background-color="#001529" text-color="#fff" active-text-color="#409eff">
<el-menu-item index="/admin/jobs">职位管理</el-menu-item>
<el-menu-item index="/admin/applications">投递管理</el-menu-item>
@ -24,11 +23,14 @@
<el-menu-item index="/admin/organizations">组织架构</el-menu-item>
<el-menu-item index="/admin/users">用户管理</el-menu-item>
</template>
<el-menu-item index="/home">返回首页</el-menu-item>
</el-menu>
</el-aside>
<el-main style="overflow-y:auto;height:100%;"><router-view /></el-main>
</el-container>
</el-container>
</aside>
<main class="admin-main">
<router-view />
</main>
</div>
</div>
</template>
<script setup>
import { useRouter } from 'vue-router'
@ -45,3 +47,54 @@ function logout() {
router.push({ name: 'Login' })
}
</script>
<style scoped>
.admin-layout {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
.admin-header {
height: 56px;
flex-shrink: 0;
background: #001529;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
border-bottom: 1px solid #333;
}
.header-title {
color: #fff;
font-weight: bold;
font-size: 16px;
}
.header-user {
color: #fff;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
}
.admin-body {
flex: 1;
display: flex;
overflow: hidden;
}
.admin-aside {
width: 200px;
flex-shrink: 0;
background: #001529;
overflow-y: auto;
}
.admin-main {
flex: 1;
overflow-y: auto;
padding: 20px;
background: #f5f6fa;
}
</style>

View File

@ -45,8 +45,8 @@
<h1 class="hero-slogan">人才创造美好未来</h1>
<p class="hero-sub">与优秀的人一起做有价值的事</p>
<div class="hero-btns">
<button class="btn-primary-lg" @click="router.push('/jobs')">
浏览职位
<button class="btn-primary-lg" @click="router.push('/companies')">
一键进入
<span class="btn-arrow"></span>
</button>
<button class="btn-secondary-lg" @click="router.push('/register')">

View File

@ -15,6 +15,11 @@
<el-table-column prop="organization_name" label="所属公司" />
<el-table-column prop="location" label="地点" />
<el-table-column prop="salary" label="薪资" />
<el-table-column label="发布时间" width="160">
<template #default="{ row }">
{{ row.created_at ? new Date(row.created_at).toLocaleDateString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit' }) : '-' }}
</template>
</el-table-column>
<el-table-column prop="status" label="状态">
<template #default="{ row }">
<el-tag :type="row.status === 'published' ? 'success' : row.status === 'draft' ? 'info' : 'danger'">
@ -148,7 +153,9 @@ async function handleSave() {
saving.value = true
try {
const payload = { ...form }
if (!auth.isSuperAdmin) delete payload.organization_id
if (!auth.isSuperAdmin) {
payload.organization_id = auth.user?.organization ?? undefined
}
if (editingJob.value) await updateJob(editingJob.value.id, payload)
else await createJob(payload)
ElMessage.success('保存成功')

View File

@ -89,7 +89,7 @@
v-for="child in group.children"
:key="child.id"
class="member-card"
@click="$router.push(`/companies/${child.id}`)"
@click="$router.push({ path: '/home', query: { org: child.id } })"
>
<div class="member-header">
<div class="member-logo">
@ -108,7 +108,7 @@
<span class="member-email" v-if="child.email">
<el-icon><Message /></el-icon> {{ child.email }}
</span>
<el-button type="primary" link size="small">查看详情 </el-button>
<el-button type="primary" link size="small">查看职位 </el-button>
</div>
</div>
</div>

View File

@ -102,13 +102,14 @@
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useRouter, useRoute } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import { ElMessage } from 'element-plus'
import { getOrganizations } from '@/api/organizations'
import { getJobs } from '@/api/jobs'
const router = useRouter()
const route = useRoute()
const auth = useAuthStore()
const orgs = ref([])
@ -155,7 +156,17 @@ onMounted(async () => {
try {
const { data } = await getOrganizations()
orgs.value = data.results
if (orgs.value.length > 0) selectOrg(orgs.value[0])
const targetOrgId = route.query.org ? Number(route.query.org) : null
let targetOrg = null
if (targetOrgId) {
for (const org of orgs.value) {
if (org.id === targetOrgId) { targetOrg = org; break }
const child = org.children?.find(c => c.id === targetOrgId)
if (child) { targetOrg = child; break }
}
}
if (targetOrg) selectOrg(targetOrg)
else if (orgs.value.length > 0) selectOrg(orgs.value[0])
} catch { orgsError.value = true }
finally { orgsLoading.value = false }
})