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:
2026-03-25 10:48:38 +08:00
parent eedaac69b0
commit 6f3487a09a
3 changed files with 91 additions and 79 deletions

View File

@@ -41,6 +41,9 @@ class ResumeMapper:
"""递归转换对象为可JSON序列化的格式"""
if obj is None:
return None
# 处理基本类型
if isinstance(obj, (str, int, float, bool)):
return obj
# 处理枚举类型
if hasattr(obj, 'value'):
return obj.value
@@ -65,7 +68,17 @@ class ResumeMapper:
value = getattr(obj, field_name)
result[field_name] = self._convert_to_serializable(value)
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:
"""将实体转换为模型"""

View File

@@ -20,14 +20,16 @@ class IngestionResult:
errors: list = None
is_duplicate: bool = False
existing_candidate_id: Optional[str] = None
is_new: bool = True # 是否为新增候选人False表示更新
@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(
success=True,
candidate_id=candidate_id,
message=message or "入库成功"
message=message or "入库成功",
is_new=is_new
)
@classmethod
@@ -50,7 +52,8 @@ class IngestionResult:
success=True, # 重复不算失败
is_duplicate=True,
existing_candidate_id=existing_id,
message=message or "候选人已存在"
message=message or "候选人已存在",
is_new=False # 重复候选人不算新增
)

View File

@@ -3,35 +3,22 @@
<!-- 侧边栏 -->
<t-aside width="240px" class="sidebar">
<div class="logo">
<div class="logo-icon">
<t-icon name="briefcase" size="24px" />
</div>
<t-icon name="briefcase" size="28px" color="#5B6CFF" />
<span class="logo-text">简历智能体</span>
</div>
<t-menu
:value="$route.path"
class="sidebar-menu"
@change="handleMenuChange"
>
<t-menu-item v-for="route in menuRoutes" :key="route.path" :value="route.path">
<template #icon>
<div class="menu-icon-wrapper" :class="{ active: $route.path === route.path }">
<t-icon :name="route.meta.icon" />
</div>
</template>
<div class="menu-container">
<div
v-for="route in menuRoutes"
:key="route.path"
class="menu-item"
:class="{ active: $route.path === route.path }"
@click="handleMenuChange(route.path)"
>
<t-icon :name="route.meta.icon" size="22px" />
<span class="menu-text">{{ route.meta.title }}</span>
</t-menu-item>
</t-menu>
<!-- 升级卡片 -->
<div class="upgrade-card">
<div class="upgrade-icon">
<t-icon name="secured" size="48px" />
<div class="active-indicator"></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>
</t-aside>
@@ -122,13 +109,18 @@ onMounted(() => {
.layout-container {
height: 100vh;
background: var(--bg-color);
display: flex;
}
.sidebar {
width: 240px !important;
min-width: 240px !important;
max-width: 240px !important;
flex-shrink: 0 !important;
background: var(--sidebar-bg);
display: flex;
flex-direction: column;
padding: 20px 16px;
padding: 24px 0;
box-shadow: var(--shadow);
border-radius: 0 24px 24px 0;
z-index: 10;
@@ -138,81 +130,80 @@ onMounted(() => {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 12px;
margin-bottom: 24px;
}
.logo-icon {
width: 42px;
height: 42px;
background: var(--primary-gradient);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
padding: 0 28px;
margin-bottom: 32px;
white-space: nowrap;
}
.logo-text {
font-size: 18px;
font-size: 20px;
font-weight: 700;
color: var(--text-primary);
}
.sidebar-menu {
/* 菜单容器 */
.menu-container {
flex: 1;
background: transparent !important;
border: none !important;
padding: 0 16px;
overflow-y: auto;
overflow-x: hidden;
}
.sidebar-menu :deep(.t-menu__item) {
height: 48px;
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;
/* 菜单项 */
.menu-item {
display: flex;
align-items: center;
justify-content: center;
background: #F3F4F6;
gap: 14px;
padding: 14px 20px;
margin-bottom: 8px;
border-radius: 12px;
cursor: pointer;
color: var(--text-secondary);
transition: all 0.3s ease;
position: relative;
overflow: hidden;
white-space: nowrap;
}
.menu-icon-wrapper.active {
background: rgba(255, 255, 255, 0.2);
.menu-item:hover {
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 {
font-weight: 500;
font-size: 15px;
}
/* 升级卡片 */
.upgrade-card {
background: linear-gradient(135deg, #EEF2FF 0%, #E0E7FF 100%);
border-radius: 20px;
padding: 24px 16px;
text-align: center;
margin-top: auto;
margin: 16px;
}
.upgrade-icon {
@@ -242,8 +233,13 @@ onMounted(() => {
border-radius: 12px !important;
}
/* 主布局 */
.main-layout {
flex: 1;
min-width: 0;
background: var(--bg-color);
display: flex;
flex-direction: column;
}
.header {