mirror of
https://github.com/RRQM/TouchSocket.git
synced 2025-12-18 09:26:42 +08:00
新增(RepoBadge): 增加跨平台 PR/Issue 徽章组件
新增 RepoBadge 组件及样式,支持 GitHub/Gitee PR/Issue 徽章展示 新增 repoDefaults.js 管理仓库基础链接和路由 doc.code-snippets 增加相关代码片段,便于插入组件 index.mdx 历史更新区集成 RepoBadge,提升文档交互性
This commit is contained in:
14
handbook/.vscode/doc.code-snippets
vendored
14
handbook/.vscode/doc.code-snippets
vendored
@@ -157,6 +157,13 @@
|
||||
],
|
||||
"description": "importCardLink"
|
||||
},
|
||||
"importRepoBadge": {
|
||||
"prefix": "importRepoBadge",
|
||||
"body": [
|
||||
"import RepoBadge from \"@site/src/components/RepoBadge.js\";"
|
||||
],
|
||||
"description": "importRepoBadge"
|
||||
},
|
||||
"CardLink": {
|
||||
"prefix": "CardLink",
|
||||
"body": [
|
||||
@@ -171,6 +178,13 @@
|
||||
],
|
||||
"description": "CustomCodeBlock"
|
||||
},
|
||||
"RepoBadge": {
|
||||
"prefix": "RepoBadge",
|
||||
"body": [
|
||||
"<RepoBadge id=\"$0\" type=\"issue\" platform=\"gitee\" />"
|
||||
],
|
||||
"description": "repoBadge"
|
||||
},
|
||||
"BilibiliCard": {
|
||||
"prefix": "BilibiliCard",
|
||||
"body": [
|
||||
|
||||
242
handbook/src/components/RepoBadge.js
Normal file
242
handbook/src/components/RepoBadge.js
Normal file
@@ -0,0 +1,242 @@
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import classes from "./RepoBadge.module.css";
|
||||
import repoDefaults from "@site/src/config/repoDefaults";
|
||||
import GitHubIcon from "@site/src/components/icons/GitHubIcon";
|
||||
import GiteeIcon from "@site/src/components/icons/GiteeIcon";
|
||||
|
||||
const PrIcon = ({ className }) => (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" className={className}>
|
||||
<path d="M7.177 3.073L9.573.677A.25.25 0 0110 .854v4.792a.25.25 0 01-.427.177L7.177 3.427a.25.25 0 010-.354zM3.75 2.5a.75.75 0 100 1.5.75.75 0 000-1.5zm-2.25.75a2.25 2.25 0 113 2.122v5.256a2.251 2.251 0 11-1.5 0V5.372A2.25 2.25 0 011.5 3.25zM11 2.5h-1V4h1a1 1 0 011 1v5.628a2.251 2.251 0 101.5 0V5A2.5 2.5 0 0011 2.5zm1 10.25a.75.75 0 111.5 0 .75.75 0 01-1.5 0zM3.75 12a.75.75 0 100 1.5.75.75 0 000-1.5z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
const IssueIcon = ({ className }) => (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" className={className}>
|
||||
<path d="M8 9.5a1.5 1.5 0 100-3 1.5 1.5 0 000 3z" />
|
||||
<path fillRule="evenodd" d="M8 0a8 8 0 100 16A8 8 0 008 0zM1.5 8a6.5 6.5 0 1113 0 6.5 6.5 0 01-13 0z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
const TYPE_METADATA = {
|
||||
pr: {
|
||||
label: "PR",
|
||||
className: classes.typePr,
|
||||
icon: PrIcon,
|
||||
},
|
||||
issue: {
|
||||
label: "Issue",
|
||||
className: classes.typeIssue,
|
||||
icon: IssueIcon,
|
||||
},
|
||||
};
|
||||
|
||||
const PLATFORM_LABELS = {
|
||||
github: "GitHub",
|
||||
gitee: "Gitee",
|
||||
};
|
||||
|
||||
const PLATFORM_ICONS = {
|
||||
github: GitHubIcon,
|
||||
gitee: GiteeIcon,
|
||||
};
|
||||
|
||||
const PLATFORM_AVATAR_BUILDERS = {
|
||||
github: (username) => `https://github.com/${username}.png`,
|
||||
gitee: (username) => `https://gitee.com/${username}.png`,
|
||||
};
|
||||
|
||||
const PLATFORM_API_BUILDERS = {
|
||||
github: ({ owner, repo }, type, id) => {
|
||||
const safeOwner = encodeURIComponent(owner);
|
||||
const safeRepo = encodeURIComponent(repo);
|
||||
const safeId = encodeURIComponent(String(id));
|
||||
const resource = type === "pr" ? "pulls" : "issues";
|
||||
return `https://api.github.com/repos/${safeOwner}/${safeRepo}/${resource}/${safeId}`;
|
||||
},
|
||||
gitee: ({ owner, repo }, type, id) => {
|
||||
if (type !== "issue") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const safeOwner = encodeURIComponent(owner);
|
||||
const safeRepo = encodeURIComponent(repo);
|
||||
const safeId = encodeURIComponent(String(id));
|
||||
|
||||
return `https://gitee.com/api/v5/repos/${safeOwner}/${safeRepo}/issues/${safeId}`;
|
||||
},
|
||||
};
|
||||
|
||||
const trimTrailingSlash = (value) => value?.replace(/\/$/, "") ?? "";
|
||||
const resolveRepoCoordinates = (urlString) => {
|
||||
if (!urlString) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
const url = new URL(urlString);
|
||||
const segments = url.pathname.split("/").filter(Boolean);
|
||||
if (segments.length >= 2) {
|
||||
return {
|
||||
owner: segments[0],
|
||||
repo: segments[1],
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
// ignore parsing issues
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
const buildPlatformAvatarUrl = (platform, username) => {
|
||||
if (typeof username !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const normalized = username.trim();
|
||||
if (!normalized) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const builder = PLATFORM_AVATAR_BUILDERS[platform];
|
||||
return builder ? builder(encodeURIComponent(normalized)) : undefined;
|
||||
};
|
||||
|
||||
export default function RepoBadge(props) {
|
||||
const {
|
||||
id,
|
||||
type = "pr",
|
||||
platform = "github",
|
||||
author,
|
||||
authorHandle,
|
||||
authorAvatar,
|
||||
} = props;
|
||||
|
||||
const normalizedType = typeof type === "string" ? type.toLowerCase() : "pr";
|
||||
const normalizedPlatform = typeof platform === "string" ? platform.toLowerCase() : "github";
|
||||
|
||||
const typeMeta = TYPE_METADATA[normalizedType] ?? TYPE_METADATA.pr;
|
||||
const platformDefaults = repoDefaults[normalizedPlatform] ?? repoDefaults.github;
|
||||
|
||||
const baseUrl = trimTrailingSlash(platformDefaults.baseUrl);
|
||||
const routeSegment = platformDefaults.routes?.[normalizedType];
|
||||
const repoCoordinates = useMemo(() => resolveRepoCoordinates(baseUrl), [baseUrl]);
|
||||
|
||||
const computedHref = id && baseUrl && routeSegment ? `${baseUrl}/${routeSegment}/${id}` : undefined;
|
||||
|
||||
const manualAuthorName = author ?? authorHandle;
|
||||
const manualAvatar = authorAvatar ?? buildPlatformAvatarUrl(normalizedPlatform, authorHandle ?? author);
|
||||
const [remoteMeta, setRemoteMeta] = useState();
|
||||
|
||||
const shouldFetchMeta = Boolean(
|
||||
(!manualAuthorName || !manualAvatar) &&
|
||||
repoCoordinates &&
|
||||
id &&
|
||||
PLATFORM_API_BUILDERS[normalizedPlatform]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!shouldFetchMeta) {
|
||||
setRemoteMeta(undefined);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const builder = PLATFORM_API_BUILDERS[normalizedPlatform];
|
||||
if (!builder) {
|
||||
setRemoteMeta(undefined);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const apiUrl = builder(repoCoordinates, normalizedType, id);
|
||||
if (!apiUrl) {
|
||||
setRemoteMeta(undefined);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
let isMounted = true;
|
||||
setRemoteMeta(undefined);
|
||||
|
||||
fetch(apiUrl, { signal: controller.signal })
|
||||
.then((response) => (response.ok ? response.json() : null))
|
||||
.then((data) => {
|
||||
if (!isMounted || !data) {
|
||||
return;
|
||||
}
|
||||
|
||||
const user =
|
||||
data.user ??
|
||||
data.author ??
|
||||
data.creator ??
|
||||
data.sender ??
|
||||
data.owner ??
|
||||
data.assignee;
|
||||
|
||||
if (!user) {
|
||||
return;
|
||||
}
|
||||
|
||||
setRemoteMeta({
|
||||
name: user.name ?? user.login ?? user.username ?? user.nick_name,
|
||||
login: user.login ?? user.username ?? user.name ?? user.nick_name,
|
||||
avatar:
|
||||
user.avatar_url ??
|
||||
user.avatar ??
|
||||
user.portrait ??
|
||||
user.avatarUrl ??
|
||||
user.avatar_url_template,
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
controller.abort();
|
||||
};
|
||||
}, [shouldFetchMeta, normalizedPlatform, normalizedType, repoCoordinates, id]);
|
||||
|
||||
const remoteAuthorName = remoteMeta?.name ?? remoteMeta?.login;
|
||||
const remoteAvatar = remoteMeta?.avatar;
|
||||
|
||||
const resolvedAuthorName = manualAuthorName ?? remoteAuthorName;
|
||||
const avatarSource = manualAvatar ?? remoteAvatar;
|
||||
|
||||
const badgeTitle = `${typeMeta.label} #${id ?? "?"} · ${PLATFORM_LABELS[normalizedPlatform] ?? normalizedPlatform}`;
|
||||
const TypeIcon = typeMeta.icon;
|
||||
|
||||
const platformLabel = PLATFORM_LABELS[normalizedPlatform] ?? normalizedPlatform;
|
||||
const PlatformIcon = PLATFORM_ICONS[normalizedPlatform];
|
||||
|
||||
const authorChip = avatarSource || resolvedAuthorName ? (
|
||||
<span className={classes.author}>
|
||||
{avatarSource ? (
|
||||
<img
|
||||
className={classes.avatar}
|
||||
src={avatarSource}
|
||||
alt={resolvedAuthorName ? `${resolvedAuthorName} avatar` : "Contributor avatar"}
|
||||
loading="lazy"
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
) : null}
|
||||
{resolvedAuthorName ? <span className={classes.authorName}>{resolvedAuthorName}</span> : null}
|
||||
</span>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<a
|
||||
className={`${classes.badge} ${typeMeta.className}`}
|
||||
href={computedHref}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
title={badgeTitle}
|
||||
>
|
||||
<span className={classes.content}>
|
||||
{TypeIcon ? <TypeIcon className={classes.typeIcon} /> : <span>{typeMeta.label}</span>}
|
||||
</span>
|
||||
{authorChip}
|
||||
<span className={classes.platform}>
|
||||
{PlatformIcon ? <PlatformIcon width={14} height={14} className={classes.platformIcon} /> : platformLabel}
|
||||
</span>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
81
handbook/src/components/RepoBadge.module.css
Normal file
81
handbook/src/components/RepoBadge.module.css
Normal file
@@ -0,0 +1,81 @@
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 2px 12px;
|
||||
border-radius: 999px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
text-decoration: none;
|
||||
color: #fff;
|
||||
transition: transform 0.15s ease, box-shadow 0.2s ease, opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.badge:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 8px 20px rgba(15, 21, 36, 0.25);
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
.badge:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 4px 14px rgba(15, 21, 36, 0.25);
|
||||
}
|
||||
|
||||
.typePr {
|
||||
background: linear-gradient(135deg, #1f883d, #22863a);
|
||||
}
|
||||
|
||||
.typeIssue {
|
||||
background: linear-gradient(135deg, #bf8700, #bc4c00);
|
||||
}
|
||||
|
||||
.content {
|
||||
display: inline-flex;
|
||||
align-items: baseline;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.id {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.platform {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.platformIcon {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.typeIcon {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.author {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 0 6px;
|
||||
border-radius: 999px;
|
||||
background-color: rgba(255, 255, 255, 0.16);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid rgba(15, 21, 36, 0.15);
|
||||
object-fit: cover;
|
||||
background-color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.authorName {
|
||||
white-space: nowrap;
|
||||
}
|
||||
17
handbook/src/config/repoDefaults.js
Normal file
17
handbook/src/config/repoDefaults.js
Normal file
@@ -0,0 +1,17 @@
|
||||
const DEFAULT_REPOSITORIES = {
|
||||
github: {
|
||||
baseUrl: "https://github.com/RRQM/TouchSocket",
|
||||
routes: {
|
||||
pr: "pull",
|
||||
issue: "issues",
|
||||
},
|
||||
},
|
||||
gitee: {
|
||||
baseUrl: "https://gitee.com/RRQM_Home/TouchSocket",
|
||||
routes: {
|
||||
issue: "issues",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default DEFAULT_REPOSITORIES;
|
||||
@@ -7,6 +7,7 @@ title: 历史更新
|
||||
import useBaseUrl from "@docusaurus/useBaseUrl";
|
||||
import Tag from "@site/src/components/Tag.js";
|
||||
import Highlight from '@site/src/components/Highlight.js';
|
||||
import RepoBadge from "@site/src/components/RepoBadge.js";
|
||||
|
||||
|
||||
## v4.0.2
|
||||
@@ -34,8 +35,8 @@ import Highlight from '@site/src/components/Highlight.js';
|
||||
|
||||
#### TouchSocket
|
||||
|
||||
- <Tag>修复</Tag> `TcpClientBase` SSL连接时 `TargetHost` 可能为空的问题。
|
||||
- <Tag>修复</Tag> `ReconnectionOption` 重连逻辑在客户端已释放时继续执行的问题。[PR](https://github.com/RRQM/TouchSocket/pull/97)
|
||||
- <Tag>修复</Tag> `TcpClientBase` SSL连接时 `TargetHost` 可能为空的问题。
|
||||
- <Tag>修复</Tag> `ReconnectionOption` 重连逻辑在客户端已释放时继续执行的问题。 <RepoBadge id="97" type="pr" platform="github" />
|
||||
- <Tag>优化</Tag> `TcpServiceBase` 优化 `Backlog` 配置处理逻辑。
|
||||
- <Tag>调整</Tag> `TouchSocketConfigExtension` 将 `BacklogProperty` 默认值从 `null` 改为 `100`。
|
||||
- <Tag>调整</Tag> `TcpListenOption` 将所有属性改为 `init` 访问器,增强不可变性。
|
||||
|
||||
Reference in New Issue
Block a user