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): 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: class Meta:
model = User model = User
fields = ['id', 'username', 'email', 'phone', 'role', 'organization', 'password', 'is_active'] 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): def create(self, validated_data):
password = validated_data.pop('password') password = validated_data.pop('password')
user = User(**validated_data) user = User(**validated_data)
@ -58,6 +63,15 @@ class AdminUserSerializer(serializers.ModelSerializer):
user.save() user.save()
return user 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): class SendCodeSerializer(serializers.Serializer):
"""发送验证码 serializer""" """发送验证码 serializer"""

View File

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

View File

@ -1,9 +1,9 @@
<template> <template>
<el-container style="height:100vh;overflow:hidden;flex-direction:column;"> <div class="admin-layout">
<el-header style="background:#001529;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid #333;flex-shrink:0;"> <header class="admin-header">
<div style="color:#fff;font-weight:bold;font-size:16px;">管理后台</div> <div class="header-title">管理后台</div>
<el-dropdown> <el-dropdown>
<div style="color:#fff;cursor:pointer;display:flex;align-items:center;gap:8px;"> <div class="header-user">
<span>{{ auth.user?.email }}</span> <span>{{ auth.user?.email }}</span>
<el-icon><ArrowDown /></el-icon> <el-icon><ArrowDown /></el-icon>
</div> </div>
@ -13,10 +13,9 @@
</el-dropdown-menu> </el-dropdown-menu>
</template> </template>
</el-dropdown> </el-dropdown>
</el-header> </header>
<el-container style="flex:1;overflow:hidden;"> <div class="admin-body">
<el-aside width="200px" style="background:#001529;overflow-y:auto;height:100%;"> <aside class="admin-aside">
<div style="padding:20px;color:#fff;font-weight:bold;font-size:16px;">菜单</div>
<el-menu router :default-active="$route.path" background-color="#001529" text-color="#fff" active-text-color="#409eff"> <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/jobs">职位管理</el-menu-item>
<el-menu-item index="/admin/applications">投递管理</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/organizations">组织架构</el-menu-item>
<el-menu-item index="/admin/users">用户管理</el-menu-item> <el-menu-item index="/admin/users">用户管理</el-menu-item>
</template> </template>
<el-menu-item index="/home">返回首页</el-menu-item>
</el-menu> </el-menu>
</el-aside> </aside>
<el-main style="overflow-y:auto;height:100%;"><router-view /></el-main> <main class="admin-main">
</el-container> <router-view />
</el-container> </main>
</div>
</div>
</template> </template>
<script setup> <script setup>
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
@ -45,3 +47,54 @@ function logout() {
router.push({ name: 'Login' }) router.push({ name: 'Login' })
} }
</script> </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> <h1 class="hero-slogan">人才创造美好未来</h1>
<p class="hero-sub">与优秀的人一起做有价值的事</p> <p class="hero-sub">与优秀的人一起做有价值的事</p>
<div class="hero-btns"> <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> <span class="btn-arrow"></span>
</button> </button>
<button class="btn-secondary-lg" @click="router.push('/register')"> <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="organization_name" label="所属公司" />
<el-table-column prop="location" label="地点" /> <el-table-column prop="location" label="地点" />
<el-table-column prop="salary" 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="状态"> <el-table-column prop="status" label="状态">
<template #default="{ row }"> <template #default="{ row }">
<el-tag :type="row.status === 'published' ? 'success' : row.status === 'draft' ? 'info' : 'danger'"> <el-tag :type="row.status === 'published' ? 'success' : row.status === 'draft' ? 'info' : 'danger'">
@ -148,7 +153,9 @@ async function handleSave() {
saving.value = true saving.value = true
try { try {
const payload = { ...form } 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) if (editingJob.value) await updateJob(editingJob.value.id, payload)
else await createJob(payload) else await createJob(payload)
ElMessage.success('保存成功') ElMessage.success('保存成功')

View File

@ -89,7 +89,7 @@
v-for="child in group.children" v-for="child in group.children"
:key="child.id" :key="child.id"
class="member-card" 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-header">
<div class="member-logo"> <div class="member-logo">
@ -108,7 +108,7 @@
<span class="member-email" v-if="child.email"> <span class="member-email" v-if="child.email">
<el-icon><Message /></el-icon> {{ child.email }} <el-icon><Message /></el-icon> {{ child.email }}
</span> </span>
<el-button type="primary" link size="small">查看详情 </el-button> <el-button type="primary" link size="small">查看职位 </el-button>
</div> </div>
</div> </div>
</div> </div>

View File

@ -102,13 +102,14 @@
<script setup> <script setup>
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router' import { useRouter, useRoute } from 'vue-router'
import { useAuthStore } from '@/stores/auth' import { useAuthStore } from '@/stores/auth'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { getOrganizations } from '@/api/organizations' import { getOrganizations } from '@/api/organizations'
import { getJobs } from '@/api/jobs' import { getJobs } from '@/api/jobs'
const router = useRouter() const router = useRouter()
const route = useRoute()
const auth = useAuthStore() const auth = useAuthStore()
const orgs = ref([]) const orgs = ref([])
@ -155,7 +156,17 @@ onMounted(async () => {
try { try {
const { data } = await getOrganizations() const { data } = await getOrganizations()
orgs.value = data.results 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 } } catch { orgsError.value = true }
finally { orgsLoading.value = false } finally { orgsLoading.value = false }
}) })