refactor: 调整岗位、应用等模块,适配三栏设计

后端变更:
- 岗位序列化器调整,支持组织树形结构
- 应用序列化器更新
- 岗位视图逻辑兼容新的过滤需求
- 新增 JobFavorite 数据库迁移(岗位收藏功能)
- 岗位URL路由配置更新

前端变更:
- 岗位详情页面兼容新设计
- 求职者应用、简历页面样式调整
- 路由配置更新,支持三栏布局
- App.vue 组件调整
- Vite 配置微调

这些调整为首页三栏布局的完整实现提供支撑。

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
TianyangZhang 2026-03-25 13:13:09 +08:00
parent e3bdb0b496
commit 536be6c1a1
12 changed files with 554 additions and 46 deletions

View File

@ -6,6 +6,12 @@ class ApplicationCreateSerializer(serializers.ModelSerializer):
model = Application model = Application
fields = ['job'] fields = ['job']
def validate(self, data):
request = self.context['request']
if Application.objects.filter(job=data['job'], applicant=request.user).exists():
raise serializers.ValidationError({'detail': '您已投递过该职位'})
return data
def create(self, validated_data): def create(self, validated_data):
request = self.context['request'] request = self.context['request']
try: try:

View File

@ -0,0 +1,29 @@
# Generated by Django 4.2.20 on 2026-03-25 02:21
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('jobs', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='JobFavorite',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('job', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='favorited_by', to='jobs.job')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='job_favorites', to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ['-created_at'],
'unique_together': {('user', 'job')},
},
),
]

View File

@ -27,3 +27,15 @@ class Job(models.Model):
def __str__(self): def __str__(self):
return self.title return self.title
class JobFavorite(models.Model):
user = models.ForeignKey(
'accounts.User', on_delete=models.CASCADE, related_name='job_favorites'
)
job = models.ForeignKey(Job, on_delete=models.CASCADE, related_name='favorited_by')
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
unique_together = ('user', 'job')
ordering = ['-created_at']

View File

@ -1,5 +1,5 @@
from rest_framework import serializers from rest_framework import serializers
from .models import Job from .models import Job, JobFavorite
from apps.organizations.serializers import OrganizationSerializer from apps.organizations.serializers import OrganizationSerializer
from apps.organizations.models import Organization from apps.organizations.models import Organization
@ -25,3 +25,11 @@ class JobDetailSerializer(serializers.ModelSerializer):
model = Job model = Job
fields = ['id', 'title', 'category', 'location', 'salary', fields = ['id', 'title', 'category', 'location', 'salary',
'description', 'organization', 'organization_id', 'status', 'created_at'] 'description', 'organization', 'organization_id', 'status', 'created_at']
class JobFavoriteSerializer(serializers.ModelSerializer):
job = JobListSerializer(read_only=True)
class Meta:
model = JobFavorite
fields = ['id', 'job', 'created_at']

View File

@ -1,9 +1,12 @@
from django.urls import path, include from django.urls import path, include
from rest_framework.routers import DefaultRouter from rest_framework.routers import DefaultRouter
from .views import JobPublicViewSet, JobManageViewSet from .views import JobPublicViewSet, JobManageViewSet, MyFavoritesView
router = DefaultRouter() router = DefaultRouter()
router.register('public', JobPublicViewSet, basename='job-public') router.register('public', JobPublicViewSet, basename='job-public')
router.register('manage', JobManageViewSet, basename='job-manage') router.register('manage', JobManageViewSet, basename='job-manage')
urlpatterns = [path('', include(router.urls))] urlpatterns = [
path('', include(router.urls)),
path('favorites/', MyFavoritesView.as_view()),
]

View File

@ -1,10 +1,12 @@
from rest_framework import viewsets, permissions from rest_framework import viewsets, permissions, generics
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.filters import SearchFilter from rest_framework.filters import SearchFilter
from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend
from .models import Job from .models import Job, JobFavorite
from .serializers import JobListSerializer, JobDetailSerializer from .serializers import JobListSerializer, JobDetailSerializer, JobFavoriteSerializer
from .filters import JobFilter from .filters import JobFilter
from apps.accounts.permissions import IsAdminOrSuperAdmin from apps.accounts.permissions import IsAdminOrSuperAdmin, IsSeeker
class JobPublicViewSet(viewsets.ReadOnlyModelViewSet): class JobPublicViewSet(viewsets.ReadOnlyModelViewSet):
@ -20,6 +22,26 @@ class JobPublicViewSet(viewsets.ReadOnlyModelViewSet):
return JobDetailSerializer return JobDetailSerializer
return JobListSerializer return JobListSerializer
@action(detail=True, methods=['post'], permission_classes=[IsSeeker])
def favorite(self, request, pk=None):
job = self.get_object()
fav, created = JobFavorite.objects.get_or_create(user=request.user, job=job)
if not created:
fav.delete()
return Response({'collected': False})
return Response({'collected': True})
class MyFavoritesView(generics.ListAPIView):
"""求职者的收藏列表"""
serializer_class = JobFavoriteSerializer
permission_classes = [IsSeeker]
def get_queryset(self):
return JobFavorite.objects.filter(user=self.request.user).select_related(
'job', 'job__organization'
)
class JobManageViewSet(viewsets.ModelViewSet): class JobManageViewSet(viewsets.ModelViewSet):
"""管理端:公司管理员管理本公司职位""" """管理端:公司管理员管理本公司职位"""

View File

@ -1,3 +1,15 @@
<template> <template>
<router-view /> <router-view />
</template> </template>
<script setup>
import { onMounted } from 'vue'
import { useAuthStore } from '@/stores/auth'
const auth = useAuthStore()
onMounted(async () => {
if (localStorage.getItem('access_token') && !auth.user) {
try { await auth.fetchMe() } catch { /* token expired, ignore */ }
}
})
</script>

View File

@ -2,12 +2,14 @@ import { createRouter, createWebHistory } from 'vue-router'
import { useAuthStore } from '@/stores/auth' import { useAuthStore } from '@/stores/auth'
const routes = [ const routes = [
// 独立入口页
{ path: '/', name: 'Splash', component: () => import('@/views/SplashView.vue') },
// 公开门户 // 公开门户
{ {
path: '/', path: '/',
component: () => import('@/layouts/PortalLayout.vue'), component: () => import('@/layouts/PortalLayout.vue'),
children: [ children: [
{ path: '', name: 'Home', component: () => import('@/views/portal/HomeView.vue') }, { path: 'home', name: 'Home', component: () => import('@/views/portal/HomeView.vue') },
{ path: 'jobs', name: 'JobList', component: () => import('@/views/portal/JobListView.vue') }, { path: 'jobs', name: 'JobList', component: () => import('@/views/portal/JobListView.vue') },
{ path: 'jobs/:id', name: 'JobDetail', component: () => import('@/views/portal/JobDetailView.vue') }, { path: 'jobs/:id', name: 'JobDetail', component: () => import('@/views/portal/JobDetailView.vue') },
{ path: 'companies', name: 'CompanyList', component: () => import('@/views/portal/CompanyListView.vue') }, { path: 'companies', name: 'CompanyList', component: () => import('@/views/portal/CompanyListView.vue') },
@ -24,6 +26,7 @@ const routes = [
children: [ children: [
{ path: 'resume', name: 'SeekerResume', component: () => import('@/views/seeker/ResumeView.vue') }, { path: 'resume', name: 'SeekerResume', component: () => import('@/views/seeker/ResumeView.vue') },
{ path: 'applications', name: 'SeekerApplications', component: () => import('@/views/seeker/ApplicationsView.vue') }, { path: 'applications', name: 'SeekerApplications', component: () => import('@/views/seeker/ApplicationsView.vue') },
{ path: 'favorites', name: 'SeekerFavorites', component: () => import('@/views/seeker/FavoritesView.vue') },
{ path: 'profile', name: 'SeekerProfile', component: () => import('@/views/seeker/ProfileView.vue') }, { path: 'profile', name: 'SeekerProfile', component: () => import('@/views/seeker/ProfileView.vue') },
] ]
}, },

View File

@ -1,41 +1,140 @@
<template> <template>
<el-row :gutter="24" v-loading="loading" style="max-width:1000px;margin:0 auto"> <div class="job-detail-page" v-loading="loading">
<el-col :span="16"> <!-- 顶部 Banner -->
<el-card v-if="job"> <div class="job-banner" v-if="job">
<template #header> <div class="banner-inner">
<h2>{{ job.title }}</h2> <div class="banner-left">
<div class="meta"> <h1 class="job-title">{{ job.title }}</h1>
{{ job.organization?.name }} · {{ job.location }} · {{ job.salary }} <div class="job-salary">{{ job.salary }}</div>
<div class="job-meta-row">
<span class="meta-item">
<el-icon><Location /></el-icon>
{{ job.location }}
</span>
<span class="meta-divider">|</span>
<span class="meta-item">
<el-icon><OfficeBuilding /></el-icon>
{{ job.organization?.name }}
</span>
</div> </div>
</template> </div>
<div v-html="job.description" class="description"></div> <div class="banner-right">
</el-card> <div class="banner-date">发布于 {{ formatDate(job.created_at) }}</div>
</el-col> <div class="banner-actions">
<el-col :span="8"> <el-button class="btn-collect" plain @click="toggleCollect">
<el-card> <el-icon><Star /></el-icon>
<el-button type="primary" size="large" style="width:100%" @click="handleApply"> {{ collected ? '已收藏' : '收藏' }}
</el-button>
<el-button class="btn-apply" @click="handleApply" :loading="applying">
立即投递 立即投递
</el-button> </el-button>
<p class="hint" v-if="!auth.isLoggedIn">登录后才能投递</p> </div>
<p class="hint success" v-if="applied">已投递可在"我的投递"查看进度</p> <p class="apply-hint" v-if="!auth.isLoggedIn">登录后才能投递</p>
</el-card> <p class="apply-hint success" v-if="applied">已投递可在我的投递查看进度</p>
</el-col> </div>
</el-row> </div>
</div>
<!-- 主体内容 -->
<div class="job-body" v-if="job">
<div class="body-inner">
<!-- 左栏 -->
<div class="body-left">
<!-- 职位标签 -->
<div class="info-tags">
<div class="tag-item">
<span class="tag-label">职位类别</span>
<span class="tag-value">{{ job.category || '未填写' }}</span>
</div>
<div class="tag-item">
<span class="tag-label">工作地点</span>
<span class="tag-value">{{ job.location }}</span>
</div>
<div class="tag-item">
<span class="tag-label">薪资范围</span>
<span class="tag-value salary-val">{{ job.salary }}</span>
</div>
<div class="tag-item">
<span class="tag-label">发布时间</span>
<span class="tag-value">{{ formatDate(job.created_at) }}</span>
</div>
</div>
<el-divider />
<!-- 职位介绍 -->
<div class="section">
<div class="section-title">
<span class="title-bar"></span>职位介绍
</div>
<div class="category-tag" v-if="job.category">{{ job.category }}</div>
<div class="description-content" v-html="job.description"></div>
</div>
<el-divider />
<!-- 工作地点 -->
<div class="section">
<div class="section-title">
<span class="title-bar"></span>工作地点
</div>
<div class="location-text">
<el-icon><Location /></el-icon>
{{ job.location }}
</div>
</div>
</div>
<!-- 右栏 -->
<div class="body-right">
<!-- 公司信息 -->
<div class="company-card">
<div class="company-card-header">单位信息</div>
<div class="company-logo-row">
<div class="company-logo">
<img v-if="job.organization?.logo" :src="job.organization.logo" alt="logo" />
<div v-else class="logo-placeholder">{{ job.organization?.name?.charAt(0) }}</div>
</div>
<div class="company-info">
<div class="company-name">{{ job.organization?.name }}</div>
<div class="company-desc" v-if="job.organization?.description">
{{ job.organization.description.slice(0, 40) }}{{ job.organization.description.length > 40 ? '...' : '' }}
</div>
</div>
</div>
<el-divider style="margin: 12px 0" />
<div class="company-contact" v-if="job.organization?.email">
<el-icon><Message /></el-icon>
<span>{{ job.organization.email }}</span>
</div>
<el-button class="btn-apply-sm" @click="handleApply" :loading="applying" style="margin-top:12px;width:100%">
立即投递
</el-button>
<p class="apply-hint-sm" v-if="applied">已投递成功</p>
</div>
</div>
</div>
</div>
</div>
</template> </template>
<script setup> <script setup>
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { getJob } from '@/api/jobs' import { getJob, toggleFavorite } from '@/api/jobs'
import { applyJob } from '@/api/applications' import { applyJob } from '@/api/applications'
import { useAuthStore } from '@/stores/auth' import { useAuthStore } from '@/stores/auth'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { Location, OfficeBuilding, Star, Message } from '@element-plus/icons-vue'
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
const auth = useAuthStore() const auth = useAuthStore()
const job = ref(null) const job = ref(null)
const loading = ref(false) const loading = ref(false)
const applying = ref(false)
const applied = ref(false) const applied = ref(false)
const collected = ref(false)
onMounted(async () => { onMounted(async () => {
loading.value = true loading.value = true
@ -44,22 +143,316 @@ onMounted(async () => {
loading.value = false loading.value = false
}) })
function formatDate(dt) {
if (!dt) return ''
return new Date(dt).toLocaleDateString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit' })
}
async function toggleCollect() {
if (!auth.isLoggedIn) return router.push({ name: 'Login', query: { redirect: route.fullPath } })
if (!auth.isSeeker) return ElMessage.warning('管理员账号无法收藏职位')
const { data } = await toggleFavorite(job.value.id)
collected.value = data.collected
ElMessage.success(collected.value ? '已收藏,可在「关注职位」中查看' : '已取消收藏')
}
async function handleApply() { async function handleApply() {
if (!auth.isLoggedIn) return router.push({ name: 'Login', query: { redirect: route.fullPath } }) if (!auth.isLoggedIn) return router.push({ name: 'Login', query: { redirect: route.fullPath } })
if (!auth.isSeeker) return ElMessage.warning('管理员账号无法投递职位') if (!auth.isSeeker) return ElMessage.warning('管理员账号无法投递职位')
applying.value = true
try { try {
await applyJob(job.value.id) await applyJob(job.value.id)
applied.value = true applied.value = true
ElMessage.success('投递成功!') ElMessage.success('投递成功!')
} catch (e) { } catch (e) {
if (e.response?.status === 400) ElMessage.warning('您已投递过该职位') if (e.response?.status === 400) {
else ElMessage.error('投递失败,请先完善简历') ElMessage.warning(e.response.data?.detail || '您已投递过该职位')
} else {
ElMessage.error('投递失败,请先完善简历')
}
} finally {
applying.value = false
} }
} }
</script> </script>
<style scoped> <style scoped>
.meta { color: #666; margin-top: 8px; } .job-detail-page {
.description { line-height: 1.8; } background: #f5f6fa;
.hint { color: #999; font-size: 13px; margin-top: 12px; } min-height: calc(100vh - 120px);
.success { color: #67c23a; } }
/* Banner */
.job-banner {
background: linear-gradient(135deg, #e8f4fd 0%, #d0e9f8 60%, #b8d9f0 100%);
padding: 36px 0 32px;
border-bottom: 1px solid #dce8f3;
}
.banner-inner {
max-width: 1100px;
margin: 0 auto;
padding: 0 24px;
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 24px;
}
.job-title {
font-size: 26px;
font-weight: 700;
color: #1a1a2e;
margin: 0 0 10px;
letter-spacing: 0.5px;
}
.job-salary {
font-size: 22px;
font-weight: 700;
color: #e63329;
margin-bottom: 12px;
}
.job-meta-row {
display: flex;
align-items: center;
gap: 8px;
color: #555;
font-size: 14px;
}
.meta-item {
display: flex;
align-items: center;
gap: 4px;
}
.meta-divider {
color: #ccc;
}
.banner-right {
text-align: right;
flex-shrink: 0;
}
.banner-date {
font-size: 13px;
color: #888;
margin-bottom: 14px;
}
.banner-actions {
display: flex;
gap: 10px;
justify-content: flex-end;
}
.btn-collect {
border-color: #e63329 !important;
color: #e63329 !important;
background: transparent !important;
}
.btn-collect:hover {
background: #fff0f0 !important;
}
.btn-apply {
background: #e63329 !important;
border-color: #e63329 !important;
color: #fff !important;
font-weight: 600;
padding: 10px 28px;
font-size: 15px;
}
.btn-apply:hover {
background: #c42820 !important;
border-color: #c42820 !important;
}
.apply-hint {
font-size: 12px;
color: #999;
margin-top: 8px;
}
.apply-hint.success {
color: #52c41a;
}
/* Body */
.job-body {
padding: 28px 0 40px;
}
.body-inner {
max-width: 1100px;
margin: 0 auto;
padding: 0 24px;
display: flex;
gap: 20px;
align-items: flex-start;
}
.body-left {
flex: 1;
background: #fff;
border-radius: 8px;
padding: 24px 28px;
box-shadow: 0 1px 4px rgba(0,0,0,.06);
}
.body-right {
width: 280px;
flex-shrink: 0;
}
/* Info tags */
.info-tags {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px 24px;
padding-bottom: 4px;
}
.tag-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
}
.tag-label {
color: #888;
white-space: nowrap;
display: flex;
align-items: center;
gap: 4px;
}
.tag-label::before {
content: '';
display: inline-block;
width: 6px;
height: 6px;
border-radius: 50%;
background: #e63329;
flex-shrink: 0;
}
.tag-value {
color: #333;
font-weight: 500;
}
.tag-value.salary-val {
color: #e63329;
}
/* Section */
.section {
padding: 4px 0;
}
.section-title {
font-size: 16px;
font-weight: 600;
color: #1a1a2e;
margin-bottom: 16px;
display: flex;
align-items: center;
gap: 8px;
}
.title-bar {
display: inline-block;
width: 4px;
height: 16px;
background: #e63329;
border-radius: 2px;
}
.category-tag {
display: inline-block;
background: #fff0f0;
color: #e63329;
border: 1px solid #fad2d0;
border-radius: 4px;
padding: 3px 10px;
font-size: 13px;
margin-bottom: 16px;
}
.description-content {
font-size: 14px;
line-height: 1.9;
color: #444;
white-space: pre-wrap;
}
.location-text {
display: flex;
align-items: center;
gap: 6px;
font-size: 14px;
color: #555;
}
/* Company card */
.company-card {
background: #fff;
border-radius: 8px;
padding: 20px;
box-shadow: 0 1px 4px rgba(0,0,0,.06);
}
.company-card-header {
font-size: 15px;
font-weight: 600;
color: #1a1a2e;
margin-bottom: 16px;
}
.company-logo-row {
display: flex;
gap: 12px;
align-items: flex-start;
}
.company-logo {
width: 52px;
height: 52px;
border-radius: 6px;
overflow: hidden;
flex-shrink: 0;
border: 1px solid #eee;
}
.company-logo img {
width: 100%;
height: 100%;
object-fit: cover;
}
.logo-placeholder {
width: 100%;
height: 100%;
background: linear-gradient(135deg, #667eea, #764ba2);
color: #fff;
font-size: 22px;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
}
.company-info {
flex: 1;
min-width: 0;
}
.company-name {
font-size: 14px;
font-weight: 600;
color: #222;
margin-bottom: 4px;
line-height: 1.4;
}
.company-desc {
font-size: 12px;
color: #888;
line-height: 1.5;
}
.company-contact {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: #666;
word-break: break-all;
}
.btn-apply-sm {
background: #e63329 !important;
border-color: #e63329 !important;
color: #fff !important;
font-weight: 600;
}
.btn-apply-sm:hover {
background: #c42820 !important;
border-color: #c42820 !important;
}
.apply-hint-sm {
text-align: center;
font-size: 12px;
color: #52c41a;
margin-top: 8px;
}
</style> </style>

View File

@ -22,9 +22,9 @@ const applications = ref([])
const loading = ref(false) const loading = ref(false)
const STATUS_MAP = { pending:'待查看', viewed:'已查看', interviewing:'面试中', hired:'已录用', rejected:'已拒绝' } const STATUS_MAP = { pending:'待查看', viewed:'已查看', interviewing:'面试中', hired:'已录用', rejected:'已拒绝' }
const STATUS_TYPE = { pending:'info', viewed:'', interviewing:'warning', hired:'success', rejected:'danger' } const STATUS_TYPE = { pending:'info', viewed:'primary', interviewing:'warning', hired:'success', rejected:'danger' }
const statusLabel = s => STATUS_MAP[s] || s const statusLabel = s => STATUS_MAP[s] || s
const statusType = s => STATUS_TYPE[s] || '' const statusType = s => STATUS_TYPE[s] || 'info'
onMounted(async () => { onMounted(async () => {
loading.value = true loading.value = true

View File

@ -46,8 +46,12 @@
<el-card style="margin-bottom:16px"> <el-card style="margin-bottom:16px">
<template #header>简历附件</template> <template #header>简历附件</template>
<el-upload action="/api/resumes/me/" :headers="uploadHeaders" name="attachment" accept=".pdf,.doc,.docx" method="patch"> <div v-if="form.attachment" style="margin-bottom:12px">
<el-button>上传简历PDF/Word</el-button> <span style="margin-right:12px">当前附件{{ attachmentName }}</span>
<el-button type="primary" link :href="attachmentUrl" target="_blank" tag="a">查看附件</el-button>
</div>
<el-upload :http-request="uploadAttachment" name="attachment" accept=".pdf,.doc,.docx" :show-file-list="false">
<el-button>{{ form.attachment ? '重新上传' : '上传简历PDF/Word' }}</el-button>
</el-upload> </el-upload>
</el-card> </el-card>
@ -57,15 +61,12 @@
</template> </template>
<script setup> <script setup>
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted } from 'vue'
import { getMyResume, updateMyResume } from '@/api/resumes' import { getMyResume, updateMyResume, uploadResumeAttachment } from '@/api/resumes'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
const form = ref(null) const form = ref(null)
const loading = ref(false) const loading = ref(false)
const saving = ref(false) const saving = ref(false)
const uploadHeaders = computed(() => ({
Authorization: `Bearer ${localStorage.getItem('access_token')}`
}))
onMounted(async () => { onMounted(async () => {
loading.value = true loading.value = true
@ -74,13 +75,32 @@ onMounted(async () => {
loading.value = false loading.value = false
}) })
const attachmentUrl = computed(() => form.value?.attachment || '')
const attachmentName = computed(() => {
if (!form.value?.attachment) return ''
return decodeURIComponent(form.value.attachment.split('/').pop())
})
const addEducation = () => form.value.education.push({ school: '', degree: '', major: '' }) const addEducation = () => form.value.education.push({ school: '', degree: '', major: '' })
const addExperience = () => form.value.experience.push({ company: '', position: '', duration: '' }) const addExperience = () => form.value.experience.push({ company: '', position: '', duration: '' })
async function uploadAttachment({ file }) {
const fd = new FormData()
fd.append('attachment', file)
try {
const { data } = await uploadResumeAttachment(fd)
form.value.attachment = data.attachment
ElMessage.success('附件上传成功')
} catch {
ElMessage.error('附件上传失败')
}
}
async function save() { async function save() {
saving.value = true saving.value = true
try { try {
await updateMyResume(form.value) const { attachment, ...rest } = form.value
await updateMyResume(rest)
ElMessage.success('简历已保存') ElMessage.success('简历已保存')
} catch { } catch {
ElMessage.error('保存失败') ElMessage.error('保存失败')

View File

@ -16,7 +16,7 @@ export default defineConfig({
}, },
server: { server: {
proxy: { proxy: {
'/api': { target: 'http://localhost:8000', changeOrigin: true } '/api': { target: 'http://127.0.0.1:8000', changeOrigin: true }
} }
} }
}) })