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序列化的格式"""
|
||||
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:
|
||||
"""将实体转换为模型"""
|
||||
|
||||
@@ -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 # 重复候选人不算新增
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user