refactor(mapper): 优化对象递归转换为JSON序列化格式
- 支持基本类型(str, int, float, bool)直接返回 - 支持SDK返回的模型对象通过__dict__递归转换 - 跳过私有属性和方法避免序列化异常 - 其他对象转换为字符串保证兼容性 feat(service): 新增候选人入库标识 - IngestionResult添加is_new字段区分新增或更新 - success_result方法新增is_new参数支持自定义设置 - duplicate_result默认is_new为False明确重复非新增 refactor(frontend): 重构侧边栏菜单布局和样式 - 简化侧边栏logo结构,调整图标大小和颜色 - 替换t-menu为div循环渲染自定义菜单项 - 菜单项支持点击事件,应用激活状态样式 - 添加蓝色指示器显示当前激活菜单项 - 优化侧边栏宽度固定,主布局采用flex布局 - 美化升级卡片视觉,调整间距和阴影统一风格
This commit is contained in:
@@ -41,6 +41,9 @@ class ResumeMapper:
|
|||||||
"""递归转换对象为可JSON序列化的格式"""
|
"""递归转换对象为可JSON序列化的格式"""
|
||||||
if obj is None:
|
if obj is None:
|
||||||
return None
|
return None
|
||||||
|
# 处理基本类型
|
||||||
|
if isinstance(obj, (str, int, float, bool)):
|
||||||
|
return obj
|
||||||
# 处理枚举类型
|
# 处理枚举类型
|
||||||
if hasattr(obj, 'value'):
|
if hasattr(obj, 'value'):
|
||||||
return obj.value
|
return obj.value
|
||||||
@@ -65,7 +68,17 @@ class ResumeMapper:
|
|||||||
value = getattr(obj, field_name)
|
value = getattr(obj, field_name)
|
||||||
result[field_name] = self._convert_to_serializable(value)
|
result[field_name] = self._convert_to_serializable(value)
|
||||||
return result
|
return result
|
||||||
return obj
|
# 处理一般对象(如SDK返回的Model对象)
|
||||||
|
if hasattr(obj, '__dict__'):
|
||||||
|
result = {}
|
||||||
|
for key, value in obj.__dict__.items():
|
||||||
|
# 跳过私有属性和方法
|
||||||
|
if key.startswith('_'):
|
||||||
|
continue
|
||||||
|
result[key] = self._convert_to_serializable(value)
|
||||||
|
return result
|
||||||
|
# 最后尝试转换为字符串
|
||||||
|
return str(obj)
|
||||||
|
|
||||||
def _entity_to_model(self, entity: Resume) -> ResumeModel:
|
def _entity_to_model(self, entity: Resume) -> ResumeModel:
|
||||||
"""将实体转换为模型"""
|
"""将实体转换为模型"""
|
||||||
|
|||||||
@@ -20,14 +20,16 @@ class IngestionResult:
|
|||||||
errors: list = None
|
errors: list = None
|
||||||
is_duplicate: bool = False
|
is_duplicate: bool = False
|
||||||
existing_candidate_id: Optional[str] = None
|
existing_candidate_id: Optional[str] = None
|
||||||
|
is_new: bool = True # 是否为新增候选人(False表示更新)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def success_result(cls, candidate_id: str, message: str = "") -> "IngestionResult":
|
def success_result(cls, candidate_id: str, message: str = "", is_new: bool = True) -> "IngestionResult":
|
||||||
"""创建成功结果"""
|
"""创建成功结果"""
|
||||||
return cls(
|
return cls(
|
||||||
success=True,
|
success=True,
|
||||||
candidate_id=candidate_id,
|
candidate_id=candidate_id,
|
||||||
message=message or "入库成功"
|
message=message or "入库成功",
|
||||||
|
is_new=is_new
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -50,7 +52,8 @@ class IngestionResult:
|
|||||||
success=True, # 重复不算失败
|
success=True, # 重复不算失败
|
||||||
is_duplicate=True,
|
is_duplicate=True,
|
||||||
existing_candidate_id=existing_id,
|
existing_candidate_id=existing_id,
|
||||||
message=message or "候选人已存在"
|
message=message or "候选人已存在",
|
||||||
|
is_new=False # 重复候选人不算新增
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,35 +3,22 @@
|
|||||||
<!-- 侧边栏 -->
|
<!-- 侧边栏 -->
|
||||||
<t-aside width="240px" class="sidebar">
|
<t-aside width="240px" class="sidebar">
|
||||||
<div class="logo">
|
<div class="logo">
|
||||||
<div class="logo-icon">
|
<t-icon name="briefcase" size="28px" color="#5B6CFF" />
|
||||||
<t-icon name="briefcase" size="24px" />
|
|
||||||
</div>
|
|
||||||
<span class="logo-text">简历智能体</span>
|
<span class="logo-text">简历智能体</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<t-menu
|
<div class="menu-container">
|
||||||
:value="$route.path"
|
<div
|
||||||
class="sidebar-menu"
|
v-for="route in menuRoutes"
|
||||||
@change="handleMenuChange"
|
:key="route.path"
|
||||||
|
class="menu-item"
|
||||||
|
:class="{ active: $route.path === route.path }"
|
||||||
|
@click="handleMenuChange(route.path)"
|
||||||
>
|
>
|
||||||
<t-menu-item v-for="route in menuRoutes" :key="route.path" :value="route.path">
|
<t-icon :name="route.meta.icon" size="22px" />
|
||||||
<template #icon>
|
|
||||||
<div class="menu-icon-wrapper" :class="{ active: $route.path === route.path }">
|
|
||||||
<t-icon :name="route.meta.icon" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<span class="menu-text">{{ route.meta.title }}</span>
|
<span class="menu-text">{{ route.meta.title }}</span>
|
||||||
</t-menu-item>
|
<div class="active-indicator"></div>
|
||||||
</t-menu>
|
|
||||||
|
|
||||||
<!-- 升级卡片 -->
|
|
||||||
<div class="upgrade-card">
|
|
||||||
<div class="upgrade-icon">
|
|
||||||
<t-icon name="secured" size="48px" />
|
|
||||||
</div>
|
</div>
|
||||||
<p class="upgrade-text">升级到 <span class="highlight">PRO</span> 版本</p>
|
|
||||||
<p class="upgrade-desc">解锁更多高级功能</p>
|
|
||||||
<t-button theme="primary" block class="upgrade-btn">立即升级</t-button>
|
|
||||||
</div>
|
</div>
|
||||||
</t-aside>
|
</t-aside>
|
||||||
|
|
||||||
@@ -122,13 +109,18 @@ onMounted(() => {
|
|||||||
.layout-container {
|
.layout-container {
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
background: var(--bg-color);
|
background: var(--bg-color);
|
||||||
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
|
width: 240px !important;
|
||||||
|
min-width: 240px !important;
|
||||||
|
max-width: 240px !important;
|
||||||
|
flex-shrink: 0 !important;
|
||||||
background: var(--sidebar-bg);
|
background: var(--sidebar-bg);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 20px 16px;
|
padding: 24px 0;
|
||||||
box-shadow: var(--shadow);
|
box-shadow: var(--shadow);
|
||||||
border-radius: 0 24px 24px 0;
|
border-radius: 0 24px 24px 0;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
@@ -138,81 +130,80 @@ onMounted(() => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding: 8px 12px;
|
padding: 0 28px;
|
||||||
margin-bottom: 24px;
|
margin-bottom: 32px;
|
||||||
}
|
white-space: nowrap;
|
||||||
|
|
||||||
.logo-icon {
|
|
||||||
width: 42px;
|
|
||||||
height: 42px;
|
|
||||||
background: var(--primary-gradient);
|
|
||||||
border-radius: 12px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
color: #fff;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo-text {
|
.logo-text {
|
||||||
font-size: 18px;
|
font-size: 20px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-menu {
|
/* 菜单容器 */
|
||||||
|
.menu-container {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
background: transparent !important;
|
padding: 0 16px;
|
||||||
border: none !important;
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-menu :deep(.t-menu__item) {
|
/* 菜单项 */
|
||||||
height: 48px;
|
.menu-item {
|
||||||
padding: 0 16px !important;
|
|
||||||
margin: 4px 0 !important;
|
|
||||||
border-radius: 12px !important;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-menu :deep(.t-menu__item:hover) {
|
|
||||||
background: #F3F4F6 !important;
|
|
||||||
color: var(--primary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-menu :deep(.t-menu__item.t-is-active) {
|
|
||||||
background: var(--primary-gradient) !important;
|
|
||||||
color: #fff !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-menu :deep(.t-menu__item.t-is-active .t-icon) {
|
|
||||||
color: #fff !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-icon-wrapper {
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
border-radius: 8px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
gap: 14px;
|
||||||
background: #F3F4F6;
|
padding: 14px 20px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--text-secondary);
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-icon-wrapper.active {
|
.menu-item:hover {
|
||||||
background: rgba(255, 255, 255, 0.2);
|
color: var(--primary-color);
|
||||||
|
background: rgba(91, 108, 255, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item.active {
|
||||||
|
color: var(--primary-color);
|
||||||
|
background: rgba(91, 108, 255, 0.1);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 右侧蓝色指示器 */
|
||||||
|
.active-indicator {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 4px;
|
||||||
|
height: 0;
|
||||||
|
background: var(--primary-color);
|
||||||
|
border-radius: 4px 0 0 4px;
|
||||||
|
transition: height 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item.active .active-indicator {
|
||||||
|
height: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-text {
|
.menu-text {
|
||||||
font-weight: 500;
|
font-size: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 升级卡片 */
|
||||||
.upgrade-card {
|
.upgrade-card {
|
||||||
background: linear-gradient(135deg, #EEF2FF 0%, #E0E7FF 100%);
|
background: linear-gradient(135deg, #EEF2FF 0%, #E0E7FF 100%);
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
padding: 24px 16px;
|
padding: 24px 16px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-top: auto;
|
margin: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.upgrade-icon {
|
.upgrade-icon {
|
||||||
@@ -242,8 +233,13 @@ onMounted(() => {
|
|||||||
border-radius: 12px !important;
|
border-radius: 12px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 主布局 */
|
||||||
.main-layout {
|
.main-layout {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
background: var(--bg-color);
|
background: var(--bg-color);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
|
|||||||
Reference in New Issue
Block a user