From 0ab7f1606171fbc22707be7a2ca2dfcb3c341da1 Mon Sep 17 00:00:00 2001 From: Linzp Date: Tue, 25 Aug 2026 12:05:00 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20PersonalCard=20=E7=A9=BA=E5=80=BC?= =?UTF-8?q?=E5=8D=A0=E4=BD=8D=E4=B8=8E=E7=A9=BA=E5=8C=BA=E5=9D=97=E9=9A=90?= =?UTF-8?q?=E8=97=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 头像为空时显示占位图;简介/联系方式/moreInfo/status 为空时不渲染对应区域;补充空值示例并修正横向模式分割线间距。 Co-authored-by: Cursor --- doc/example.json | 2 +- doc/personal-card.js | 107 ++++++++++++++++++++++--- package.json | 2 +- src/PersonalCard/icons.js | 6 ++ src/PersonalCard/index.js | 122 +++++++++++++++++++---------- src/PersonalCard/style.module.scss | 54 +++++++++++-- 6 files changed, 232 insertions(+), 61 deletions(-) diff --git a/doc/example.json b/doc/example.json index 830396d..b3e461e 100644 --- a/doc/example.json +++ b/doc/example.json @@ -144,7 +144,7 @@ }, { "title": "PersonalCard", - "description": "个人档案卡片组件 - 在一个示例中演示三种模式、状态切换与空值处理", + "description": "个人档案卡片:三种模式、状态切换,以及无头像占位、无简介、只有姓名等空值示例", "code": "./personal-card.js", "scope": [ { diff --git a/doc/personal-card.js b/doc/personal-card.js index 0d21f06..84d69f2 100644 --- a/doc/personal-card.js +++ b/doc/personal-card.js @@ -17,16 +17,31 @@ const statusOptions = [ { label: '忙碌', value: 'busy' } ]; +const renderCardList = (cards, mode, status) => ( + + {cards.map(item => { + const { key, label, withStatus = true, ...cardProps } = item; + return ( +
+ {label} + +
+ ); + })} +
+); + const BaseExample = () => { const [mode, setMode] = useState('large'); const [status, setStatus] = useState('online'); - const profileCards = useMemo( + const basicCards = useMemo( () => [ { key: 'full', label: '完整信息', badge: '推荐', + avatar, name: '莎拉·詹金斯', title: '高级账户战略师', description: '我的使命是确保您的团队拥有轻松扩展所需的资源和战略指导。让我们携手共创辉煌。', @@ -43,6 +58,7 @@ const BaseExample = () => { key: 'email-only', label: '仅邮箱', badge: '候选', + avatar, name: '林知夏', title: '产品经理', description: '负责产品规划与用户体验优化,擅长把复杂问题转化为清晰方案。', @@ -58,6 +74,7 @@ const BaseExample = () => { key: 'empty-contact', label: '无联系方式', badge: '归档', + avatar, name: '周予安', title: '数据分析师', description: '关注业务指标体系建设与可视化分析,强调用数据驱动决策。', @@ -72,8 +89,78 @@ const BaseExample = () => { [] ); + // 空值 / 占位:无头像、无简介、只有姓名等 + const emptyStateCards = useMemo( + () => [ + { + key: 'empty-avatar', + label: '无头像(占位)', + badge: '新入职', + name: '陈思远', + title: '前端工程师', + description: '头像为空时显示占位头像,避免破损图片。', + phone: '+86 139 1111 2222', + email: 'chensiyuan@example.com', + moreInfo: [ + { label: '性别', content: '男' }, + { label: '年龄', content: '27' }, + { label: '部门', content: '研发中心' }, + { label: '专长', content: 'React' } + ] + }, + { + key: 'empty-description', + label: '无简介', + avatar, + name: '赵清禾', + title: '设计师', + phone: '+86 137 3333 4444', + email: 'zhaoqinghe@example.com', + moreInfo: [ + { label: '性别', content: '女' }, + { label: '年龄', content: '26' }, + { label: '部门', content: '设计部' }, + { label: '专长', content: '视觉设计' } + ] + }, + { + key: 'name-only', + label: '只有姓名', + name: '顾南星' + }, + { + key: 'no-status', + label: '无 status', + withStatus: false, + avatar, + name: '沈听澜', + title: '内容运营', + description: '不传 status 时不显示头像状态点。', + phone: '+86 135 7777 8888', + email: 'shentinglan@example.com', + moreInfo: [ + { label: '性别', content: '女' }, + { label: '年龄', content: '28' }, + { label: '部门', content: '运营部' }, + { label: '专长', content: '内容策划' } + ] + }, + { + key: 'empty-more-info', + label: '无更多信息', + avatar, + name: '韩亦辰', + title: '运营专员', + description: '更多信息为空时不展示信息网格区域。', + phone: '+86 136 5555 6666', + email: 'hanyichen@example.com' + } + ], + [] + ); + return ( - +
综合示例
@@ -88,14 +175,16 @@ const BaseExample = () => {
- - {profileCards.map(item => ( -
- {item.label} - -
- ))} + {renderCardList(basicCards, mode, status)} + + +
空值与占位示例
+
+ 无头像显示占位图;无简介 / 无更多信息 / 只有姓名时,对应容器整块不渲染;无 status 时不显示状态点。 +
+ + {renderCardList(emptyStateCards, mode, status)}
); }; diff --git a/package.json b/package.json index bba46eb..67ee253 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kne/react-box", - "version": "0.1.10", + "version": "0.1.11", "description": "一个轻量级的 React 设计组件库,专注于提供精致美观的信息展示小组件", "syntax": { "esmodules": true diff --git a/src/PersonalCard/icons.js b/src/PersonalCard/icons.js index aaeade1..2f27753 100644 --- a/src/PersonalCard/icons.js +++ b/src/PersonalCard/icons.js @@ -17,3 +17,9 @@ export const QuoteIcon = ({ className = '', size = 24 }) => ( ); + +export const UserIcon = ({ className = '', size = 24 }) => ( + + + +); diff --git a/src/PersonalCard/index.js b/src/PersonalCard/index.js index eebb69a..9e3fa27 100644 --- a/src/PersonalCard/index.js +++ b/src/PersonalCard/index.js @@ -1,6 +1,6 @@ import React, { Fragment } from 'react'; import style from './style.module.scss'; -import { MailIcon, CallIcon, QuoteIcon } from './icons'; +import { MailIcon, CallIcon, QuoteIcon, UserIcon } from './icons'; /** * @param {React.ReactNode} [extra] 卡片角落扩展区(如 Checkbox) @@ -9,6 +9,10 @@ import { MailIcon, CallIcon, QuoteIcon } from './icons'; */ const PersonalCard = ({ avatar, name, title, description, phone, email, moreInfo = [], status, badge, mode = 'large', extra, footer, selected = false, className }) => { const statusClass = style[`status-${status}`] || style['status-online']; + const validMoreInfo = Array.isArray(moreInfo) ? moreInfo.filter(item => item?.content != null && item.content !== '') : []; + const hasMoreInfo = validMoreInfo.length > 0; + const hasContact = !!(phone || email); + const hasDescription = !!description; const AvatarWithStatus = ({ size = 'default' }) => { const sizeClasses = { @@ -17,10 +21,31 @@ const PersonalCard = ({ avatar, name, title, description, phone, email, moreInfo small: 'avatar-small', default: 'avatar-medium' }; + const iconSizeMap = { + large: 48, + medium: 32, + small: 24, + default: 32 + }; + const sizeKey = sizeClasses[size] ? size : 'default'; + + const renderAvatarContent = () => { + if (typeof avatar === 'function') { + return avatar({ className: style['avatar'] }); + } + if (avatar) { + return {name}; + } + return ( +
+ +
+ ); + }; return (
- {typeof avatar === 'function' ? avatar({ className: style['avatar'] }) : {name}} + {renderAvatarContent()} {status && }
); @@ -45,14 +70,13 @@ const PersonalCard = ({ avatar, name, title, description, phone, email, moreInfo const BasicInfo = ({ showDivider = true }) => { return ( - Array.isArray(moreInfo) && - moreInfo.length > 0 && ( + hasMoreInfo && (
- {moreInfo.map(({ content }, index) => { + {validMoreInfo.map(({ content }, index) => { return ( {content} - {showDivider && moreInfo.length - 1 !== index && } + {showDivider && validMoreInfo.length - 1 !== index && } ); })} @@ -62,10 +86,9 @@ const PersonalCard = ({ avatar, name, title, description, phone, email, moreInfo }; const InfoGrid = () => - Array.isArray(moreInfo) && - moreInfo.length > 0 && ( + hasMoreInfo && (
- {moreInfo.map(({ label, content }, index) => { + {validMoreInfo.map(({ label, content }, index) => { return ; })}
@@ -112,10 +135,12 @@ const PersonalCard = ({ avatar, name, title, description, phone, email, moreInfo
-
-
{description}
-
- {(phone || email) && ( + {hasDescription && ( +
+
{description}
+
+ )} + {hasContact && (
@@ -136,7 +161,7 @@ const PersonalCard = ({ avatar, name, title, description, phone, email, moreInfo {badge && {badge}}
{title &&
{title}
} - {(phone || email) && ( + {hasContact && (
@@ -144,44 +169,55 @@ const PersonalCard = ({ avatar, name, title, description, phone, email, moreInfo )}
-
- -
-
{description}
+ {(hasMoreInfo || hasDescription) && ( +
+ + {hasDescription && ( +
+
{description}
+
+ )}
-
+ )} {renderFooter()}
); - const renderHorizontal = () => ( -
- {renderExtra()} -
- -

- {name} {badge && {badge}} -

- {title &&
{title}
} - {(phone || email) && ( -
-
- - + const renderHorizontal = () => { + const showRight = hasMoreInfo || hasDescription || footer; + return ( +
+ {renderExtra()} +
+ +

+ {name} {badge && {badge}} +

+ {title &&
{title}
} + {hasContact && ( +
+
+ + +
+ )} +
+ {showRight && ( +
+ + {hasDescription && ( +
+ +
{description}
+
+ )} + {renderFooter()}
)}
-
- -
- -
{description}
-
- {renderFooter()} -
-
- ); + ); + }; const renderMode = { vertical: renderVertical, diff --git a/src/PersonalCard/style.module.scss b/src/PersonalCard/style.module.scss index 86fb297..55e4d91 100644 --- a/src/PersonalCard/style.module.scss +++ b/src/PersonalCard/style.module.scss @@ -89,6 +89,19 @@ width: 100%; height: 100%; object-fit: cover; + border-radius: 50%; + } + + .avatar-placeholder { + display: flex; + align-items: center; + justify-content: center; + background-color: #f1f5f9; + color: #cbd5e1; + } + + .avatar-placeholder-icon { + display: block; } .avatar-large { @@ -146,6 +159,19 @@ flex-direction: row; } + &.card-horizontal-single { + .card-left { + flex: 1; + border-bottom: none; + border-right: none; + + @media (min-width: 768px) { + border-bottom: none; + border-right: none; + } + } + } + .card-left { padding: 2rem; flex: 0 0 33.333333%; @@ -165,7 +191,8 @@ width: 8rem; height: 8rem; - img { + img, + .avatar-placeholder { width: 100%; height: 100%; border-radius: 50%; @@ -340,7 +367,8 @@ border-radius: 50%; box-shadow: 0 4px 12px rgba(37, 99, 235, 0.08); - img { + img, + .avatar-placeholder { width: 100%; height: 100%; border-radius: 50%; @@ -458,7 +486,8 @@ width: 4rem; height: 4rem; - img { + img, + .avatar-placeholder { width: 100%; height: 100%; border-radius: 50%; @@ -476,27 +505,36 @@ .minimal-info { display: flex; flex-direction: column; + justify-content: center; + gap: 0.375rem; + min-width: 0; .name-row { display: flex; align-items: center; + flex-wrap: wrap; gap: 0.5rem; - margin-bottom: 0.25rem; .name { font-size: 1rem; + margin: 0; + } + + .badge { + margin-left: 0; } } .title { font-size: 0.75rem; - margin-bottom: 0.375rem; + margin: 0; } .contact-row { display: flex; flex-direction: column; - gap: 0.125rem; + gap: 0.25rem; + margin-top: 0.125rem; .contact-item { display: flex; @@ -506,6 +544,7 @@ .icon { color: $text-muted; + flex-shrink: 0; } .email, @@ -513,6 +552,7 @@ font-size: 0.75rem; font-weight: 500; color: #334155; + line-height: 1.4; } .email { @@ -583,11 +623,11 @@ padding: 1rem; .minimal-header { padding-right: 1.5rem; - border-right: 1px solid $border-light; } .minimal-main { margin-top: 0; padding-left: 1.5rem; + border-left: 1px solid $border-light; } .minimal-footer { margin-top: 0;