@windcoder/astro-theme-orchid 0.4.4 → 0.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/components/PostResources.astro +75 -0
- package/src/i18n/en.ts +11 -0
- package/src/i18n/ja.ts +11 -0
- package/src/i18n/ko.ts +11 -0
- package/src/i18n/ru.ts +11 -0
- package/src/i18n/zh-CN.ts +11 -0
- package/src/i18n/zh-TW.ts +11 -0
- package/src/routes/friends.astro +399 -19
- package/src/routes/posts/[...slug].astro +3 -0
- package/src/schema.ts +88 -2
- package/src/scripts/share-poster.ts +2 -0
- package/src/styles/global.css +408 -0
package/package.json
CHANGED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* 文章资源区:frontmatter resources(name/url/password)在正文后渲染资源卡。
|
|
4
|
+
* 按 url 域名识别品牌图标(gitee/github),其余地址用通用外链图标;
|
|
5
|
+
* password 有值时展示提取码。新地址类型无需改组件,直接复用 name/url 展示。
|
|
6
|
+
*/
|
|
7
|
+
import { t } from "../i18n/index.js";
|
|
8
|
+
|
|
9
|
+
interface ResourceItem {
|
|
10
|
+
name: string;
|
|
11
|
+
url: string;
|
|
12
|
+
password?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface Props {
|
|
16
|
+
resources?: ResourceItem[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const { resources } = Astro.props;
|
|
20
|
+
const items = (resources ?? []).filter((r) => r.url && r.url.trim() !== "");
|
|
21
|
+
|
|
22
|
+
type IconKind = "gitee" | "github" | "link";
|
|
23
|
+
function iconFor(url: string): IconKind {
|
|
24
|
+
let host = "";
|
|
25
|
+
try {
|
|
26
|
+
host = new URL(url).hostname.replace(/^www\./, "");
|
|
27
|
+
} catch {
|
|
28
|
+
return "link";
|
|
29
|
+
}
|
|
30
|
+
if (host === "gitee.com") return "gitee";
|
|
31
|
+
if (host === "github.com") return "github";
|
|
32
|
+
return "link";
|
|
33
|
+
}
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
{
|
|
37
|
+
items.length > 0 && (
|
|
38
|
+
<section class="post-resources card reveal" aria-label={t("postResources")}>
|
|
39
|
+
<h2 class="post-resources-title">
|
|
40
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z"/><path d="m3.3 7 8.7 5 8.7-5"/><path d="M12 22V12"/></svg>
|
|
41
|
+
{t("postResources")}
|
|
42
|
+
</h2>
|
|
43
|
+
<ul class="post-resources-list">
|
|
44
|
+
{items.map((r) => {
|
|
45
|
+
const kind = iconFor(r.url);
|
|
46
|
+
return (
|
|
47
|
+
<li class="post-resources-item">
|
|
48
|
+
<a href={r.url} target="_blank" rel="noopener">
|
|
49
|
+
<span class="post-resources-icon" aria-hidden="true">
|
|
50
|
+
{kind === "gitee" && (
|
|
51
|
+
<svg viewBox="0 0 1024 1024" fill="currentColor"><path d="M512 1024C229.222 1024 0 794.778 0 512S229.222 0 512 0s512 229.222 512 512-229.222 512-512 512zm259.149-568.883h-290.74a25.293 25.293 0 0 0-25.292 25.293l-0.081 63.206c0 13.952 11.315 25.293 25.267 25.293h177.024c13.978 0 25.293 11.315 25.293 25.267v12.646a75.853 75.853 0 0 1-75.853 75.853h-240.23a25.293 25.293 0 0 1-25.267-25.293V417.203a75.853 75.853 0 0 1 75.827-75.853h353.946a25.293 25.293 0 0 0 25.267-25.292l0.077-63.207a25.293 25.293 0 0 0-25.268-25.293H417.152a189.62 189.62 0 0 0-189.62 189.645V771.15c0 13.977 11.316 25.293 25.294 25.293h372.94a170.65 170.65 0 0 0 170.65-170.65V480.384a25.293 25.293 0 0 0-25.267-25.267z"/></svg>
|
|
52
|
+
)}
|
|
53
|
+
{kind === "github" && (
|
|
54
|
+
<svg viewBox="0 0 16 16" fill="currentColor"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8z"/></svg>
|
|
55
|
+
)}
|
|
56
|
+
{kind === "link" && (
|
|
57
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>
|
|
58
|
+
)}
|
|
59
|
+
</span>
|
|
60
|
+
<span class="post-resources-name">{r.name}</span>
|
|
61
|
+
{r.password && r.password.trim() !== "" && (
|
|
62
|
+
<span class="post-resources-password">
|
|
63
|
+
{t("resourcePassword")}
|
|
64
|
+
<code>{r.password}</code>
|
|
65
|
+
</span>
|
|
66
|
+
)}
|
|
67
|
+
<svg class="post-resources-ext" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 3h6v6"/><path d="M10 14 21 3"/><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/></svg>
|
|
68
|
+
</a>
|
|
69
|
+
</li>
|
|
70
|
+
);
|
|
71
|
+
})}
|
|
72
|
+
</ul>
|
|
73
|
+
</section>
|
|
74
|
+
)
|
|
75
|
+
}
|
package/src/i18n/en.ts
CHANGED
|
@@ -78,6 +78,14 @@ const en: Record<UiKey, string> = {
|
|
|
78
78
|
avatarAlt: "Avatar of {name}",
|
|
79
79
|
noFriends: "No friend links yet",
|
|
80
80
|
applyFriend: "Apply for a link",
|
|
81
|
+
friendSearch: "Search links…",
|
|
82
|
+
friendAll: "All",
|
|
83
|
+
noFriendMatch: "No matching links",
|
|
84
|
+
copy: "Copy",
|
|
85
|
+
friendSiteName: "Site name",
|
|
86
|
+
friendSiteDesc: "Site description",
|
|
87
|
+
friendSiteUrl: "Site URL",
|
|
88
|
+
friendAvatarUrl: "Avatar URL",
|
|
81
89
|
/* Bangumi watching page (§4.93) */
|
|
82
90
|
bangumiEmpty: "Nothing being watched yet",
|
|
83
91
|
bangumiFail: "Failed to fetch Bangumi data. Rebuild the site to retry.",
|
|
@@ -146,6 +154,9 @@ const en: Record<UiKey, string> = {
|
|
|
146
154
|
"No series yet. Add a series field to a post's frontmatter to create one.",
|
|
147
155
|
seriesProgress: "{cur} / {total}",
|
|
148
156
|
seriesCatalog: "Series catalog",
|
|
157
|
+
/* Post resources: resource card rendered from frontmatter resources */
|
|
158
|
+
postResources: "Related resources",
|
|
159
|
+
resourcePassword: "Code",
|
|
149
160
|
/* Music player (§4.36) */
|
|
150
161
|
musicOpen: "Music player",
|
|
151
162
|
musicClose: "Collapse player",
|
package/src/i18n/ja.ts
CHANGED
|
@@ -78,6 +78,14 @@ const ja: Record<UiKey, string> = {
|
|
|
78
78
|
avatarAlt: "{name} のアバター",
|
|
79
79
|
noFriends: "リンクはまだありません",
|
|
80
80
|
applyFriend: "リンク申請",
|
|
81
|
+
friendSearch: "リンクを検索…",
|
|
82
|
+
friendAll: "すべて",
|
|
83
|
+
noFriendMatch: "該当するリンクがありません",
|
|
84
|
+
copy: "コピー",
|
|
85
|
+
friendSiteName: "サイト名",
|
|
86
|
+
friendSiteDesc: "サイト説明",
|
|
87
|
+
friendSiteUrl: "サイトURL",
|
|
88
|
+
friendAvatarUrl: "アバターURL",
|
|
81
89
|
/* 番組視聴ページ */
|
|
82
90
|
bangumiEmpty: "視聴中の番組はまだありません",
|
|
83
91
|
bangumiFail:
|
|
@@ -150,6 +158,9 @@ const ja: Record<UiKey, string> = {
|
|
|
150
158
|
"シリーズがまだありません。記事の frontmatter に series を記入すると作成できます。",
|
|
151
159
|
seriesProgress: "{cur} / {total}",
|
|
152
160
|
seriesCatalog: "シリーズ目次",
|
|
161
|
+
/* 記事リソース:frontmatter resources から描画されるリソースカード */
|
|
162
|
+
postResources: "関連リソース",
|
|
163
|
+
resourcePassword: "パスワード",
|
|
153
164
|
/* 音楽プレイヤー */
|
|
154
165
|
musicOpen: "音楽プレイヤー",
|
|
155
166
|
musicClose: "プレイヤーを閉じる",
|
package/src/i18n/ko.ts
CHANGED
|
@@ -77,6 +77,14 @@ const ko: Record<UiKey, string> = {
|
|
|
77
77
|
avatarAlt: "{name}의 아바타",
|
|
78
78
|
noFriends: "아직 링크가 없습니다",
|
|
79
79
|
applyFriend: "링크 신청",
|
|
80
|
+
friendSearch: "링크 검색…",
|
|
81
|
+
friendAll: "전체",
|
|
82
|
+
noFriendMatch: "일치하는 링크가 없습니다",
|
|
83
|
+
copy: "복사",
|
|
84
|
+
friendSiteName: "사이트 이름",
|
|
85
|
+
friendSiteDesc: "사이트 설명",
|
|
86
|
+
friendSiteUrl: "사이트 URL",
|
|
87
|
+
friendAvatarUrl: "아바타 URL",
|
|
80
88
|
/* 애니 시청 페이지 */
|
|
81
89
|
bangumiEmpty: "시청 중인 애니메이션이 없습니다",
|
|
82
90
|
bangumiFail:
|
|
@@ -149,6 +157,9 @@ const ko: Record<UiKey, string> = {
|
|
|
149
157
|
"아직 시리즈가 없습니다. 글 frontmatter에 series를 입력하면 생성됩니다.",
|
|
150
158
|
seriesProgress: "{cur} / {total}",
|
|
151
159
|
seriesCatalog: "시리즈 목차",
|
|
160
|
+
/* 게시글 리소스: frontmatter resources 로 렌더링되는 리소스 카드 */
|
|
161
|
+
postResources: "관련 자료",
|
|
162
|
+
resourcePassword: "추출 코드",
|
|
152
163
|
/* 음악 플레이어 */
|
|
153
164
|
musicOpen: "음악 플레이어",
|
|
154
165
|
musicClose: "플레이어 접기",
|
package/src/i18n/ru.ts
CHANGED
|
@@ -77,6 +77,14 @@ const ru: Record<UiKey, string> = {
|
|
|
77
77
|
avatarAlt: "Аватар {name}",
|
|
78
78
|
noFriends: "Ссылок пока нет",
|
|
79
79
|
applyFriend: "Заявка на обмен ссылками",
|
|
80
|
+
friendSearch: "Поиск ссылок…",
|
|
81
|
+
friendAll: "Все",
|
|
82
|
+
noFriendMatch: "Ничего не найдено",
|
|
83
|
+
copy: "Копировать",
|
|
84
|
+
friendSiteName: "Название сайта",
|
|
85
|
+
friendSiteDesc: "Описание сайта",
|
|
86
|
+
friendSiteUrl: "Ссылка на сайт",
|
|
87
|
+
friendAvatarUrl: "Ссылка на аватар",
|
|
80
88
|
/* Страница отслеживания аниме */
|
|
81
89
|
bangumiEmpty: "Пока ничего не смотрю",
|
|
82
90
|
bangumiFail:
|
|
@@ -150,6 +158,9 @@ const ru: Record<UiKey, string> = {
|
|
|
150
158
|
"Серий пока нет. Заполните поле series в frontmatter записи, чтобы создать.",
|
|
151
159
|
seriesProgress: "{cur} / {total}",
|
|
152
160
|
seriesCatalog: "Содержание серии",
|
|
161
|
+
/* Ресурсы статьи: карточка из frontmatter resources */
|
|
162
|
+
postResources: "Материалы",
|
|
163
|
+
resourcePassword: "Код доступа",
|
|
153
164
|
/* Музыкальный плеер */
|
|
154
165
|
musicOpen: "Музыкальный плеер",
|
|
155
166
|
musicClose: "Свернуть плеер",
|
package/src/i18n/zh-CN.ts
CHANGED
|
@@ -91,6 +91,14 @@ const zhCN = {
|
|
|
91
91
|
avatarAlt: "{name} 的头像",
|
|
92
92
|
noFriends: "还没有友链",
|
|
93
93
|
applyFriend: "申请友链",
|
|
94
|
+
friendSearch: "搜索友链…",
|
|
95
|
+
friendAll: "全部",
|
|
96
|
+
noFriendMatch: "没有匹配的友链",
|
|
97
|
+
copy: "复制",
|
|
98
|
+
friendSiteName: "站点名称",
|
|
99
|
+
friendSiteDesc: "站点描述",
|
|
100
|
+
friendSiteUrl: "站点链接",
|
|
101
|
+
friendAvatarUrl: "头像链接",
|
|
94
102
|
/* 番组追番页(§4.93) */
|
|
95
103
|
bangumiEmpty: "还没有在看中的番剧",
|
|
96
104
|
bangumiFail: "番组数据拉取失败,请重新构建站点后重试",
|
|
@@ -166,6 +174,9 @@ const zhCN = {
|
|
|
166
174
|
seriesEmpty: "还没有专题,在文章 frontmatter 中填写 series 字段即可创建。",
|
|
167
175
|
seriesProgress: "{cur} / {total}",
|
|
168
176
|
seriesCatalog: "专题目录",
|
|
177
|
+
/* 文章资源区:frontmatter resources 渲染的资源卡 */
|
|
178
|
+
postResources: "相关资源",
|
|
179
|
+
resourcePassword: "提取码",
|
|
169
180
|
/* 音乐播放器(§4.36):data-labels 经组件 data-labels 属性带入客户端 */
|
|
170
181
|
musicOpen: "音乐播放器",
|
|
171
182
|
musicClose: "收起播放器",
|
package/src/i18n/zh-TW.ts
CHANGED
|
@@ -77,6 +77,14 @@ const zhTW: Record<UiKey, string> = {
|
|
|
77
77
|
avatarAlt: "{name} 的頭像",
|
|
78
78
|
noFriends: "還沒有友鏈",
|
|
79
79
|
applyFriend: "申請友鏈",
|
|
80
|
+
friendSearch: "搜尋友鏈…",
|
|
81
|
+
friendAll: "全部",
|
|
82
|
+
noFriendMatch: "沒有符合的友鏈",
|
|
83
|
+
copy: "複製",
|
|
84
|
+
friendSiteName: "站點名稱",
|
|
85
|
+
friendSiteDesc: "站點描述",
|
|
86
|
+
friendSiteUrl: "站點連結",
|
|
87
|
+
friendAvatarUrl: "頭像連結",
|
|
80
88
|
/* 番組追番頁 */
|
|
81
89
|
bangumiEmpty: "還沒有在看中的番劇",
|
|
82
90
|
bangumiFail: "番組資料拉取失敗,請重新建置網站後重試",
|
|
@@ -145,6 +153,9 @@ const zhTW: Record<UiKey, string> = {
|
|
|
145
153
|
seriesEmpty: "還沒有專題,在文章 frontmatter 中填寫 series 欄位即可建立。",
|
|
146
154
|
seriesProgress: "{cur} / {total}",
|
|
147
155
|
seriesCatalog: "專題目錄",
|
|
156
|
+
/* 文章資源區:frontmatter resources 渲染的資源卡 */
|
|
157
|
+
postResources: "相關資源",
|
|
158
|
+
resourcePassword: "提取碼",
|
|
148
159
|
/* 音樂播放器 */
|
|
149
160
|
musicOpen: "音樂播放器",
|
|
150
161
|
musicClose: "收起播放器",
|
package/src/routes/friends.astro
CHANGED
|
@@ -22,19 +22,47 @@ const friendImgs = await Promise.all(
|
|
|
22
22
|
),
|
|
23
23
|
);
|
|
24
24
|
|
|
25
|
-
// 可选 spec 正文(src/content/spec/friends.md
|
|
25
|
+
// 可选 spec 正文(src/content/spec/friends.md),渲染在区块卡上方
|
|
26
26
|
const entry = await getEntry("spec", "friends").catch(() => null);
|
|
27
27
|
const { Content } = entry ? await render(entry) : { Content: null };
|
|
28
28
|
|
|
29
|
-
const
|
|
30
|
-
|
|
29
|
+
const friendsCfg = config.friends;
|
|
30
|
+
const apply = friendsCfg.apply;
|
|
31
|
+
const siteInfo = friendsCfg.siteInfo;
|
|
32
|
+
const notice = friendsCfg.notice;
|
|
33
|
+
|
|
34
|
+
// 分组 pills:由 list tags 聚合去重(保持首次出现顺序),无 tags 自动隐藏
|
|
35
|
+
const groupTags = [...new Set(friends.flatMap((f) => f.tags))].filter(Boolean);
|
|
36
|
+
const showGroups = friendsCfg.filter.groups && groupTags.length > 0;
|
|
37
|
+
const showSearch = friendsCfg.filter.search;
|
|
38
|
+
|
|
39
|
+
// 本站信息卡:name/desc/site/avatar 任一非空才渲染
|
|
40
|
+
const siteInfoRows = (
|
|
41
|
+
[
|
|
42
|
+
[t("friendSiteName"), siteInfo.name],
|
|
43
|
+
[t("friendSiteDesc"), siteInfo.desc],
|
|
44
|
+
[t("friendSiteUrl"), siteInfo.site],
|
|
45
|
+
[t("friendAvatarUrl"), siteInfo.avatar],
|
|
46
|
+
] as [string, string][]
|
|
47
|
+
).filter(([, v]) => v);
|
|
48
|
+
const hasSiteInfo = siteInfoRows.length > 0;
|
|
49
|
+
// 本站头像三格式解析(§4.97),空串跳过用字头像兜底
|
|
50
|
+
const siteAvatar = siteInfo.avatar
|
|
51
|
+
? await resolveSiteImage(siteInfo.avatar)
|
|
52
|
+
: "";
|
|
53
|
+
|
|
54
|
+
// 申请模板整块复制文本:label: value 每行一条(模板块右上角一键复制)
|
|
55
|
+
const applyTemplateText = apply.template
|
|
56
|
+
.map((r) => `${r.label}: ${r.value}`)
|
|
57
|
+
.join("\n");
|
|
31
58
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
59
|
+
// 申请卡:有步骤或模板时渲染富卡(与站点信息并排);否则回退旧的横向申请条
|
|
60
|
+
const hasApplyRich = Boolean(
|
|
61
|
+
apply.title && (apply.steps.length > 0 || apply.template.length > 0),
|
|
62
|
+
);
|
|
63
|
+
---
|
|
37
64
|
|
|
65
|
+
<BaseLayout title={friendsCfg.title} page="friends">
|
|
38
66
|
{
|
|
39
67
|
Content && (
|
|
40
68
|
<article class="card page-card prose reveal" data-lightbox-gallery>
|
|
@@ -43,14 +71,87 @@ const apply = config.friends.apply;
|
|
|
43
71
|
)
|
|
44
72
|
}
|
|
45
73
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
<div class="friend-
|
|
49
|
-
|
|
50
|
-
|
|
74
|
+
<section class="friend-zone card reveal" data-friend-zone>
|
|
75
|
+
<div class="friend-toolbar">
|
|
76
|
+
<div class="friend-zone-title">
|
|
77
|
+
<svg
|
|
78
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
79
|
+
viewBox="0 0 24 24"
|
|
80
|
+
fill="none"
|
|
81
|
+
stroke="currentColor"
|
|
82
|
+
stroke-width="2"
|
|
83
|
+
stroke-linecap="round"
|
|
84
|
+
stroke-linejoin="round"
|
|
85
|
+
aria-hidden="true"
|
|
86
|
+
>
|
|
87
|
+
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
|
|
88
|
+
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
|
|
89
|
+
</svg>
|
|
90
|
+
<span>{friendsCfg.title}</span>
|
|
91
|
+
</div>
|
|
92
|
+
{
|
|
93
|
+
showSearch && (
|
|
94
|
+
<div class="friend-search">
|
|
95
|
+
<svg
|
|
96
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
97
|
+
viewBox="0 0 24 24"
|
|
98
|
+
fill="none"
|
|
99
|
+
stroke="currentColor"
|
|
100
|
+
stroke-width="2"
|
|
101
|
+
stroke-linecap="round"
|
|
102
|
+
stroke-linejoin="round"
|
|
103
|
+
aria-hidden="true"
|
|
104
|
+
>
|
|
105
|
+
<circle cx="11" cy="11" r="8" />
|
|
106
|
+
<path d="m21 21-4.3-4.3" />
|
|
107
|
+
</svg>
|
|
108
|
+
<input
|
|
109
|
+
type="search"
|
|
110
|
+
placeholder={t("friendSearch")}
|
|
111
|
+
aria-label={t("friendSearch")}
|
|
112
|
+
data-friend-search
|
|
113
|
+
/>
|
|
114
|
+
</div>
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
</div>
|
|
118
|
+
{friendsCfg.description && <p class="friend-zone-desc">{friendsCfg.description}</p>}
|
|
119
|
+
{
|
|
120
|
+
showGroups && (
|
|
121
|
+
<div class="friend-pills" role="group" aria-label={t("friendAll")}>
|
|
122
|
+
<button type="button" class="chip is-on" data-friend-pill="">
|
|
123
|
+
{t("friendAll")}
|
|
124
|
+
</button>
|
|
125
|
+
{groupTags.map((gt) => (
|
|
126
|
+
<button type="button" class="chip" data-friend-pill={gt}>
|
|
127
|
+
{gt}
|
|
128
|
+
</button>
|
|
129
|
+
))}
|
|
130
|
+
</div>
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
<div class="friend-grid">
|
|
134
|
+
{
|
|
135
|
+
friends.map((f, i) => (
|
|
136
|
+
<a
|
|
137
|
+
class="friend-card card"
|
|
138
|
+
href={f.site}
|
|
139
|
+
target="_blank"
|
|
140
|
+
rel="noopener"
|
|
141
|
+
data-friend-card
|
|
142
|
+
data-search={[f.title, f.desc, ...f.tags]
|
|
143
|
+
.filter(Boolean)
|
|
144
|
+
.join(" ")
|
|
145
|
+
.toLowerCase()}
|
|
146
|
+
data-tags={JSON.stringify(f.tags)}
|
|
147
|
+
>
|
|
51
148
|
<div class="friend-avatar">
|
|
52
149
|
{f.image ? (
|
|
53
|
-
<img
|
|
150
|
+
<img
|
|
151
|
+
src={friendImgs[i]}
|
|
152
|
+
alt={t("avatarAlt", { name: f.title })}
|
|
153
|
+
loading="lazy"
|
|
154
|
+
/>
|
|
54
155
|
) : (
|
|
55
156
|
<span class="avatar-fallback">{f.title.slice(0, 1)}</span>
|
|
56
157
|
)}
|
|
@@ -67,15 +168,191 @@ const apply = config.friends.apply;
|
|
|
67
168
|
)}
|
|
68
169
|
</div>
|
|
69
170
|
</a>
|
|
70
|
-
))
|
|
171
|
+
))
|
|
172
|
+
}
|
|
173
|
+
</div>
|
|
174
|
+
<p class="tag-empty" data-friend-empty hidden>
|
|
175
|
+
{t("noFriendMatch")}
|
|
176
|
+
</p>
|
|
177
|
+
</section>
|
|
178
|
+
|
|
179
|
+
{
|
|
180
|
+
(hasSiteInfo || hasApplyRich) && (
|
|
181
|
+
<div class="friend-duo">
|
|
182
|
+
{hasSiteInfo && (
|
|
183
|
+
<section class="friend-siteinfo card reveal">
|
|
184
|
+
<div class="friend-card-title">
|
|
185
|
+
<svg
|
|
186
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
187
|
+
viewBox="0 0 24 24"
|
|
188
|
+
fill="none"
|
|
189
|
+
stroke="currentColor"
|
|
190
|
+
stroke-width="2"
|
|
191
|
+
stroke-linecap="round"
|
|
192
|
+
stroke-linejoin="round"
|
|
193
|
+
aria-hidden="true"
|
|
194
|
+
>
|
|
195
|
+
<path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
|
|
196
|
+
<polyline points="9 22 9 12 15 12 15 22" />
|
|
197
|
+
</svg>
|
|
198
|
+
<span>{siteInfo.title}</span>
|
|
199
|
+
</div>
|
|
200
|
+
<div class="siteinfo-head">
|
|
201
|
+
<div
|
|
202
|
+
class="siteinfo-avatar"
|
|
203
|
+
data-siteinfo-avatar
|
|
204
|
+
data-avatar-name={siteInfo.name}
|
|
205
|
+
>
|
|
206
|
+
{siteAvatar ? (
|
|
207
|
+
<img src={siteAvatar} alt={siteInfo.name} loading="lazy" />
|
|
208
|
+
) : (
|
|
209
|
+
<span class="avatar-fallback">
|
|
210
|
+
{(siteInfo.name || "?").slice(0, 1)}
|
|
211
|
+
</span>
|
|
212
|
+
)}
|
|
213
|
+
</div>
|
|
214
|
+
<div class="siteinfo-meta">
|
|
215
|
+
<div class="siteinfo-name">{siteInfo.name}</div>
|
|
216
|
+
{siteInfo.desc && <p class="siteinfo-desc">{siteInfo.desc}</p>}
|
|
217
|
+
</div>
|
|
218
|
+
</div>
|
|
219
|
+
{siteInfoRows.map(([label, value]) => (
|
|
220
|
+
<div class="kv-row">
|
|
221
|
+
<div class="kv-main">
|
|
222
|
+
<span class="kv-label">{label}</span>
|
|
223
|
+
<span class="kv-value">{value}</span>
|
|
224
|
+
</div>
|
|
225
|
+
<button
|
|
226
|
+
type="button"
|
|
227
|
+
class="copy-btn"
|
|
228
|
+
data-copy={value}
|
|
229
|
+
aria-label={t("copy")}
|
|
230
|
+
title={t("copy")}
|
|
231
|
+
>
|
|
232
|
+
<svg
|
|
233
|
+
class="icon-copy"
|
|
234
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
235
|
+
viewBox="0 0 24 24"
|
|
236
|
+
fill="none"
|
|
237
|
+
stroke="currentColor"
|
|
238
|
+
stroke-width="2"
|
|
239
|
+
stroke-linecap="round"
|
|
240
|
+
stroke-linejoin="round"
|
|
241
|
+
aria-hidden="true"
|
|
242
|
+
>
|
|
243
|
+
<rect width="14" height="14" x="8" y="8" rx="2" />
|
|
244
|
+
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" />
|
|
245
|
+
</svg>
|
|
246
|
+
<svg
|
|
247
|
+
class="icon-check"
|
|
248
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
249
|
+
viewBox="0 0 24 24"
|
|
250
|
+
fill="none"
|
|
251
|
+
stroke="currentColor"
|
|
252
|
+
stroke-width="2"
|
|
253
|
+
stroke-linecap="round"
|
|
254
|
+
stroke-linejoin="round"
|
|
255
|
+
aria-hidden="true"
|
|
256
|
+
>
|
|
257
|
+
<path d="M20 6 9 17l-5-5" />
|
|
258
|
+
</svg>
|
|
259
|
+
</button>
|
|
260
|
+
</div>
|
|
261
|
+
))}
|
|
262
|
+
</section>
|
|
263
|
+
)}
|
|
264
|
+
{hasApplyRich && (
|
|
265
|
+
<section class="friend-apply-card card reveal">
|
|
266
|
+
<div class="friend-card-title">
|
|
267
|
+
<svg
|
|
268
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
269
|
+
viewBox="0 0 24 24"
|
|
270
|
+
fill="none"
|
|
271
|
+
stroke="currentColor"
|
|
272
|
+
stroke-width="2"
|
|
273
|
+
stroke-linecap="round"
|
|
274
|
+
stroke-linejoin="round"
|
|
275
|
+
aria-hidden="true"
|
|
276
|
+
>
|
|
277
|
+
<rect width="20" height="16" x="2" y="4" rx="2" />
|
|
278
|
+
<path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" />
|
|
279
|
+
</svg>
|
|
280
|
+
<span>{apply.title}</span>
|
|
281
|
+
</div>
|
|
282
|
+
{apply.description && <p class="fa-desc">{apply.description}</p>}
|
|
283
|
+
{apply.steps.length > 0 && (
|
|
284
|
+
<ol class="apply-steps">
|
|
285
|
+
{apply.steps.map((s, i) => (
|
|
286
|
+
<li class="apply-step">
|
|
287
|
+
<span class="apply-step-num">{i + 1}</span>
|
|
288
|
+
<div>
|
|
289
|
+
<div class="apply-step-title">{s.title}</div>
|
|
290
|
+
{s.description && <p class="apply-step-desc">{s.description}</p>}
|
|
291
|
+
</div>
|
|
292
|
+
</li>
|
|
293
|
+
))}
|
|
294
|
+
</ol>
|
|
295
|
+
)}
|
|
296
|
+
{apply.template.length > 0 && (
|
|
297
|
+
<div class="apply-template">
|
|
298
|
+
<button
|
|
299
|
+
type="button"
|
|
300
|
+
class="copy-btn"
|
|
301
|
+
data-copy={applyTemplateText}
|
|
302
|
+
aria-label={t("copy")}
|
|
303
|
+
title={t("copy")}
|
|
304
|
+
>
|
|
305
|
+
<svg
|
|
306
|
+
class="icon-copy"
|
|
307
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
308
|
+
viewBox="0 0 24 24"
|
|
309
|
+
fill="none"
|
|
310
|
+
stroke="currentColor"
|
|
311
|
+
stroke-width="2"
|
|
312
|
+
stroke-linecap="round"
|
|
313
|
+
stroke-linejoin="round"
|
|
314
|
+
aria-hidden="true"
|
|
315
|
+
>
|
|
316
|
+
<rect width="14" height="14" x="8" y="8" rx="2" />
|
|
317
|
+
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" />
|
|
318
|
+
</svg>
|
|
319
|
+
<svg
|
|
320
|
+
class="icon-check"
|
|
321
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
322
|
+
viewBox="0 0 24 24"
|
|
323
|
+
fill="none"
|
|
324
|
+
stroke="currentColor"
|
|
325
|
+
stroke-width="2"
|
|
326
|
+
stroke-linecap="round"
|
|
327
|
+
stroke-linejoin="round"
|
|
328
|
+
aria-hidden="true"
|
|
329
|
+
>
|
|
330
|
+
<path d="M20 6 9 17l-5-5" />
|
|
331
|
+
</svg>
|
|
332
|
+
</button>
|
|
333
|
+
<div class="apply-template-text">
|
|
334
|
+
{apply.template.map((row) => (
|
|
335
|
+
<div class="apply-template-line">
|
|
336
|
+
{row.label}: {row.value}
|
|
337
|
+
</div>
|
|
338
|
+
))}
|
|
339
|
+
</div>
|
|
340
|
+
</div>
|
|
341
|
+
)}
|
|
342
|
+
{apply.link && (
|
|
343
|
+
<a class="btn" href={apply.link}>
|
|
344
|
+
{apply.label || t("applyFriend")}
|
|
345
|
+
</a>
|
|
346
|
+
)}
|
|
347
|
+
</section>
|
|
348
|
+
)}
|
|
71
349
|
</div>
|
|
72
|
-
) : (
|
|
73
|
-
<p class="tag-empty reveal">{t('noFriends')}</p>
|
|
74
350
|
)
|
|
75
351
|
}
|
|
76
352
|
|
|
77
353
|
{
|
|
78
|
-
|
|
354
|
+
// 兼容旧配置:无步骤无模板时保留原横向申请条
|
|
355
|
+
apply.title && !hasApplyRich && (
|
|
79
356
|
<section class="friend-apply card reveal">
|
|
80
357
|
<div>
|
|
81
358
|
<div class="fa-title">{apply.title}</div>
|
|
@@ -83,13 +360,116 @@ const apply = config.friends.apply;
|
|
|
83
360
|
</div>
|
|
84
361
|
{apply.link && (
|
|
85
362
|
<a class="btn" href={apply.link}>
|
|
86
|
-
{apply.label || t(
|
|
363
|
+
{apply.label || t("applyFriend")}
|
|
87
364
|
</a>
|
|
88
365
|
)}
|
|
89
366
|
</section>
|
|
90
367
|
)
|
|
91
368
|
}
|
|
92
369
|
|
|
370
|
+
{
|
|
371
|
+
notice.items.length > 0 && (
|
|
372
|
+
<section class="friend-notice card reveal">
|
|
373
|
+
<div class="friend-card-title">
|
|
374
|
+
<svg
|
|
375
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
376
|
+
viewBox="0 0 24 24"
|
|
377
|
+
fill="none"
|
|
378
|
+
stroke="currentColor"
|
|
379
|
+
stroke-width="2"
|
|
380
|
+
stroke-linecap="round"
|
|
381
|
+
stroke-linejoin="round"
|
|
382
|
+
aria-hidden="true"
|
|
383
|
+
>
|
|
384
|
+
<path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9" />
|
|
385
|
+
<path d="M10.3 21a1.94 1.94 0 0 0 3.4 0" />
|
|
386
|
+
</svg>
|
|
387
|
+
<span>{notice.title}</span>
|
|
388
|
+
</div>
|
|
389
|
+
<dl class="notice-list">
|
|
390
|
+
{notice.items.map((item) => (
|
|
391
|
+
<div class="notice-item">
|
|
392
|
+
<dt>{item.term}</dt>
|
|
393
|
+
{item.description && <dd>{item.description}</dd>}
|
|
394
|
+
</div>
|
|
395
|
+
))}
|
|
396
|
+
</dl>
|
|
397
|
+
</section>
|
|
398
|
+
)
|
|
399
|
+
}
|
|
400
|
+
|
|
93
401
|
<!-- §4.113:友链页评论区,支持直接留言申请友链(provider 未启用时自动 no-op) -->
|
|
94
402
|
<Comment path="/friends" />
|
|
95
403
|
</BaseLayout>
|
|
404
|
+
|
|
405
|
+
<script>
|
|
406
|
+
// 友链区块卡:搜索 + 分组 pills 叠加过滤
|
|
407
|
+
const zone = document.querySelector<HTMLElement>("[data-friend-zone]");
|
|
408
|
+
if (zone) {
|
|
409
|
+
const search = zone.querySelector<HTMLInputElement>("[data-friend-search]");
|
|
410
|
+
const pills = Array.from(
|
|
411
|
+
zone.querySelectorAll<HTMLButtonElement>("[data-friend-pill]"),
|
|
412
|
+
);
|
|
413
|
+
const cards = Array.from(
|
|
414
|
+
zone.querySelectorAll<HTMLElement>("[data-friend-card]"),
|
|
415
|
+
);
|
|
416
|
+
const empty = zone.querySelector<HTMLElement>("[data-friend-empty]");
|
|
417
|
+
let keyword = "";
|
|
418
|
+
let group = "";
|
|
419
|
+
const applyFilter = () => {
|
|
420
|
+
let visible = 0;
|
|
421
|
+
for (const card of cards) {
|
|
422
|
+
const okKeyword =
|
|
423
|
+
!keyword || (card.dataset.search || "").includes(keyword);
|
|
424
|
+
let tags: string[] = [];
|
|
425
|
+
try {
|
|
426
|
+
tags = JSON.parse(card.dataset.tags || "[]") as string[];
|
|
427
|
+
} catch {
|
|
428
|
+
/* data-tags 解析失败时按无分组处理 */
|
|
429
|
+
}
|
|
430
|
+
const okGroup = !group || tags.includes(group);
|
|
431
|
+
const show = okKeyword && okGroup;
|
|
432
|
+
card.hidden = !show;
|
|
433
|
+
if (show) visible++;
|
|
434
|
+
}
|
|
435
|
+
if (empty) empty.hidden = visible > 0;
|
|
436
|
+
};
|
|
437
|
+
search?.addEventListener("input", () => {
|
|
438
|
+
keyword = (search.value || "").trim().toLowerCase();
|
|
439
|
+
applyFilter();
|
|
440
|
+
});
|
|
441
|
+
for (const pill of pills) {
|
|
442
|
+
pill.addEventListener("click", () => {
|
|
443
|
+
group = pill.dataset.friendPill || "";
|
|
444
|
+
for (const p of pills) p.classList.toggle("is-on", p === pill);
|
|
445
|
+
applyFilter();
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// 复制按钮:点击复制 data-copy 内容,短暂切换成功态
|
|
451
|
+
for (const btn of document.querySelectorAll<HTMLButtonElement>("[data-copy]")) {
|
|
452
|
+
btn.addEventListener("click", async () => {
|
|
453
|
+
const value = btn.dataset.copy || "";
|
|
454
|
+
try {
|
|
455
|
+
await navigator.clipboard.writeText(value);
|
|
456
|
+
} catch {
|
|
457
|
+
/* 剪贴板不可用时静默降级(按钮仍给出成功反馈) */
|
|
458
|
+
}
|
|
459
|
+
btn.classList.add("is-copied");
|
|
460
|
+
setTimeout(() => btn.classList.remove("is-copied"), 1500);
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// 本站头像加载失败时回退字头像(外链头像 404 / 防盗链兜底)
|
|
465
|
+
const avatarBox = document.querySelector<HTMLElement>("[data-siteinfo-avatar]");
|
|
466
|
+
const avatarImg = avatarBox?.querySelector("img");
|
|
467
|
+
if (avatarBox && avatarImg) {
|
|
468
|
+
avatarImg.addEventListener("error", () => {
|
|
469
|
+
const fb = document.createElement("span");
|
|
470
|
+
fb.className = "avatar-fallback";
|
|
471
|
+
fb.textContent = (avatarBox.dataset.avatarName || "?").slice(0, 1);
|
|
472
|
+
avatarImg.replaceWith(fb);
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
</script>
|
|
@@ -10,6 +10,7 @@ import EncryptedContent from "virtual:theme/components/EncryptedContent.astro";
|
|
|
10
10
|
import License from "virtual:theme/components/License.astro";
|
|
11
11
|
import OutdatedCard from "virtual:theme/components/OutdatedCard.astro";
|
|
12
12
|
import PostNav from "virtual:theme/components/PostNav.astro";
|
|
13
|
+
import PostResources from "virtual:theme/components/PostResources.astro";
|
|
13
14
|
import RecommendedPost from "virtual:theme/components/RecommendedPost.astro";
|
|
14
15
|
import Sidebar from "virtual:theme/components/Sidebar.astro";
|
|
15
16
|
import Toc from "virtual:theme/components/Toc.astro";
|
|
@@ -303,6 +304,8 @@ const jsonLd = {
|
|
|
303
304
|
{/* 过时提醒(§4.11):许可卡之前展示;加密文章不渲染 */}
|
|
304
305
|
{!d.password && <OutdatedCard updated={d.updated ?? d.published} published={d.published} />}
|
|
305
306
|
<License licenseName={d.licenseName} licenseUrl={d.licenseUrl} enabled={d.license} />
|
|
307
|
+
{/* 文章资源区:frontmatter resources 非空时渲染;加密文章随正文语义一并隐藏 */}
|
|
308
|
+
{!d.password && <PostResources resources={d.resources} />}
|
|
306
309
|
<PostNav prev={prev ?? undefined} next={next ?? undefined} />
|
|
307
310
|
{/* 支持卡(P2):打赏入口 + 分享海报(share-poster.ts 仍按 #qy-share-btn 与 data-* 绑定) */}
|
|
308
311
|
{
|
package/src/schema.ts
CHANGED
|
@@ -706,6 +706,29 @@ export const themeConfigSchema = z.object({
|
|
|
706
706
|
description: z.string().default(""),
|
|
707
707
|
/** 页面级排序:weight 权重降序 / random 随机(构建期) */
|
|
708
708
|
sort: z.enum(["weight", "random"]).default("weight"),
|
|
709
|
+
/** 友链区块卡工具条:搜索框与分组 pills 开关(分组由 list tags 聚合,无 tags 自动隐藏) */
|
|
710
|
+
filter: z
|
|
711
|
+
.object({
|
|
712
|
+
search: z.boolean().default(true),
|
|
713
|
+
groups: z.boolean().default(true),
|
|
714
|
+
})
|
|
715
|
+
.default({ search: true, groups: true }),
|
|
716
|
+
/** 本站信息卡(name/desc/site/avatar 任一非空才渲染整卡) */
|
|
717
|
+
siteInfo: z
|
|
718
|
+
.object({
|
|
719
|
+
title: z.string().default("本站信息"),
|
|
720
|
+
name: z.string().default(""),
|
|
721
|
+
desc: z.string().default(""),
|
|
722
|
+
site: z.string().default(""),
|
|
723
|
+
avatar: z.string().default(""),
|
|
724
|
+
})
|
|
725
|
+
.default({
|
|
726
|
+
title: "本站信息",
|
|
727
|
+
name: "",
|
|
728
|
+
desc: "",
|
|
729
|
+
site: "",
|
|
730
|
+
avatar: "",
|
|
731
|
+
}),
|
|
709
732
|
/** 申请友链卡片(title 留空则整卡不渲染) */
|
|
710
733
|
apply: z
|
|
711
734
|
.object({
|
|
@@ -714,8 +737,47 @@ export const themeConfigSchema = z.object({
|
|
|
714
737
|
/** 申请入口链接(mailto 或表单页) */
|
|
715
738
|
link: z.string().default(""),
|
|
716
739
|
label: z.string().default("申请友链"),
|
|
740
|
+
/** 申请步骤(空数组不渲染) */
|
|
741
|
+
steps: z
|
|
742
|
+
.array(
|
|
743
|
+
z.object({
|
|
744
|
+
title: z.string(),
|
|
745
|
+
description: z.string().default(""),
|
|
746
|
+
}),
|
|
747
|
+
)
|
|
748
|
+
.default([]),
|
|
749
|
+
/** 可复制申请资料模板行(空数组不渲染) */
|
|
750
|
+
template: z
|
|
751
|
+
.array(
|
|
752
|
+
z.object({
|
|
753
|
+
label: z.string(),
|
|
754
|
+
value: z.string(),
|
|
755
|
+
}),
|
|
756
|
+
)
|
|
757
|
+
.default([]),
|
|
758
|
+
})
|
|
759
|
+
.default({
|
|
760
|
+
title: "",
|
|
761
|
+
description: "",
|
|
762
|
+
link: "",
|
|
763
|
+
label: "申请友链",
|
|
764
|
+
steps: [],
|
|
765
|
+
template: [],
|
|
766
|
+
}),
|
|
767
|
+
/** 注意事项卡(items 为空不渲染) */
|
|
768
|
+
notice: z
|
|
769
|
+
.object({
|
|
770
|
+
title: z.string().default("注意事项"),
|
|
771
|
+
items: z
|
|
772
|
+
.array(
|
|
773
|
+
z.object({
|
|
774
|
+
term: z.string(),
|
|
775
|
+
description: z.string().default(""),
|
|
776
|
+
}),
|
|
777
|
+
)
|
|
778
|
+
.default([]),
|
|
717
779
|
})
|
|
718
|
-
.default({ title: "",
|
|
780
|
+
.default({ title: "注意事项", items: [] }),
|
|
719
781
|
list: z
|
|
720
782
|
.array(
|
|
721
783
|
z.object({
|
|
@@ -738,7 +800,17 @@ export const themeConfigSchema = z.object({
|
|
|
738
800
|
title: "友链",
|
|
739
801
|
description: "",
|
|
740
802
|
sort: "weight",
|
|
741
|
-
|
|
803
|
+
filter: { search: true, groups: true },
|
|
804
|
+
siteInfo: { title: "本站信息", name: "", desc: "", site: "", avatar: "" },
|
|
805
|
+
apply: {
|
|
806
|
+
title: "",
|
|
807
|
+
description: "",
|
|
808
|
+
link: "",
|
|
809
|
+
label: "申请友链",
|
|
810
|
+
steps: [],
|
|
811
|
+
template: [],
|
|
812
|
+
},
|
|
813
|
+
notice: { title: "注意事项", items: [] },
|
|
742
814
|
list: [],
|
|
743
815
|
}),
|
|
744
816
|
/**
|
|
@@ -1639,6 +1711,20 @@ export function definePostSchema(extensions: z.ZodRawShape = {}) {
|
|
|
1639
1711
|
* 组名直接来自本字段文本,无 i18n 参与。
|
|
1640
1712
|
*/
|
|
1641
1713
|
seriesGroup: z.string().optional(),
|
|
1714
|
+
/**
|
|
1715
|
+
* 文章相关资源:正文后渲染资源卡区。name 展示名、url 跳转地址、
|
|
1716
|
+
* password 提取码(可空)。按 url 域名识别品牌图标(gitee/github),
|
|
1717
|
+
* 其余地址类型直接复用通用外链图标展示,无需再改 schema。
|
|
1718
|
+
*/
|
|
1719
|
+
resources: z
|
|
1720
|
+
.array(
|
|
1721
|
+
z.object({
|
|
1722
|
+
name: z.string(),
|
|
1723
|
+
url: z.string(),
|
|
1724
|
+
password: z.string().optional(),
|
|
1725
|
+
}),
|
|
1726
|
+
)
|
|
1727
|
+
.optional(),
|
|
1642
1728
|
})
|
|
1643
1729
|
.extend(extensions);
|
|
1644
1730
|
}
|
|
@@ -451,6 +451,8 @@ export async function openSharePoster(btn: HTMLElement) {
|
|
|
451
451
|
img.src = poster;
|
|
452
452
|
img.hidden = false;
|
|
453
453
|
}
|
|
454
|
+
/* 生成完成:移除 loading(否则转圈常驻在海报上方) */
|
|
455
|
+
modal.querySelector<HTMLElement>(".poster-loading")?.remove();
|
|
454
456
|
if (saveBtn) {
|
|
455
457
|
saveBtn.disabled = false;
|
|
456
458
|
saveBtn.addEventListener("click", () => {
|
package/src/styles/global.css
CHANGED
|
@@ -3339,6 +3339,96 @@ html.dark .outdated-warning {
|
|
|
3339
3339
|
border-radius: var(--radius-sm);
|
|
3340
3340
|
background: var(--bg-soft);
|
|
3341
3341
|
}
|
|
3342
|
+
/* 文章资源区:frontmatter resources 渲染的资源卡(License 之后、上下篇之前) */
|
|
3343
|
+
.post-resources {
|
|
3344
|
+
margin-top: 28px;
|
|
3345
|
+
padding: 16px 20px;
|
|
3346
|
+
}
|
|
3347
|
+
.post-resources-title {
|
|
3348
|
+
display: flex;
|
|
3349
|
+
align-items: center;
|
|
3350
|
+
gap: 8px;
|
|
3351
|
+
margin: 0 0 10px;
|
|
3352
|
+
font-size: 15.5px;
|
|
3353
|
+
font-weight: 700;
|
|
3354
|
+
color: var(--ink);
|
|
3355
|
+
}
|
|
3356
|
+
.post-resources-title svg {
|
|
3357
|
+
width: 16px;
|
|
3358
|
+
height: 16px;
|
|
3359
|
+
color: var(--accent);
|
|
3360
|
+
}
|
|
3361
|
+
.post-resources-list {
|
|
3362
|
+
list-style: none;
|
|
3363
|
+
margin: 0;
|
|
3364
|
+
padding: 0;
|
|
3365
|
+
display: grid;
|
|
3366
|
+
gap: 4px;
|
|
3367
|
+
}
|
|
3368
|
+
.post-resources-item a {
|
|
3369
|
+
display: flex;
|
|
3370
|
+
align-items: center;
|
|
3371
|
+
gap: 10px;
|
|
3372
|
+
padding: 7px 8px;
|
|
3373
|
+
border-radius: var(--radius-sm);
|
|
3374
|
+
font-size: 14px;
|
|
3375
|
+
color: var(--ink-2);
|
|
3376
|
+
transition:
|
|
3377
|
+
background 0.15s ease,
|
|
3378
|
+
color 0.15s ease;
|
|
3379
|
+
}
|
|
3380
|
+
.post-resources-item a:hover {
|
|
3381
|
+
background: var(--bg-soft);
|
|
3382
|
+
color: var(--accent-deep);
|
|
3383
|
+
}
|
|
3384
|
+
.post-resources-icon {
|
|
3385
|
+
flex: none;
|
|
3386
|
+
display: inline-flex;
|
|
3387
|
+
width: 18px;
|
|
3388
|
+
height: 18px;
|
|
3389
|
+
color: var(--muted);
|
|
3390
|
+
}
|
|
3391
|
+
.post-resources-item a:hover .post-resources-icon {
|
|
3392
|
+
color: var(--accent);
|
|
3393
|
+
}
|
|
3394
|
+
.post-resources-icon svg {
|
|
3395
|
+
width: 100%;
|
|
3396
|
+
height: 100%;
|
|
3397
|
+
}
|
|
3398
|
+
.post-resources-name {
|
|
3399
|
+
min-width: 0;
|
|
3400
|
+
overflow: hidden;
|
|
3401
|
+
text-overflow: ellipsis;
|
|
3402
|
+
white-space: nowrap;
|
|
3403
|
+
}
|
|
3404
|
+
.post-resources-password {
|
|
3405
|
+
flex: none;
|
|
3406
|
+
display: inline-flex;
|
|
3407
|
+
align-items: center;
|
|
3408
|
+
gap: 4px;
|
|
3409
|
+
margin-left: auto;
|
|
3410
|
+
font-size: 12.5px;
|
|
3411
|
+
color: var(--muted);
|
|
3412
|
+
}
|
|
3413
|
+
.post-resources-password code {
|
|
3414
|
+
padding: 1px 6px;
|
|
3415
|
+
border-radius: 4px;
|
|
3416
|
+
border: 1px solid var(--border);
|
|
3417
|
+
background: var(--bg-soft);
|
|
3418
|
+
font-size: 12px;
|
|
3419
|
+
color: var(--ink-2);
|
|
3420
|
+
}
|
|
3421
|
+
.post-resources-ext {
|
|
3422
|
+
flex: none;
|
|
3423
|
+
width: 14px;
|
|
3424
|
+
height: 14px;
|
|
3425
|
+
color: var(--muted);
|
|
3426
|
+
opacity: 0.6;
|
|
3427
|
+
}
|
|
3428
|
+
.post-resources-item a:hover .post-resources-ext {
|
|
3429
|
+
color: var(--accent);
|
|
3430
|
+
opacity: 1;
|
|
3431
|
+
}
|
|
3342
3432
|
.post-nav {
|
|
3343
3433
|
margin-top: 24px;
|
|
3344
3434
|
display: grid;
|
|
@@ -4526,6 +4616,320 @@ html.dark .friend-card:hover .friend-name {
|
|
|
4526
4616
|
padding: 1px 9px;
|
|
4527
4617
|
}
|
|
4528
4618
|
|
|
4619
|
+
/* ---------- 10.3 友链区块卡(标题 + 搜索 + 分组 + 网格) ---------- */
|
|
4620
|
+
.friend-zone {
|
|
4621
|
+
margin-top: 26px;
|
|
4622
|
+
padding: 22px 24px;
|
|
4623
|
+
}
|
|
4624
|
+
.friend-toolbar {
|
|
4625
|
+
display: flex;
|
|
4626
|
+
align-items: center;
|
|
4627
|
+
gap: 12px;
|
|
4628
|
+
flex-wrap: wrap;
|
|
4629
|
+
}
|
|
4630
|
+
.friend-zone-title {
|
|
4631
|
+
display: flex;
|
|
4632
|
+
align-items: center;
|
|
4633
|
+
gap: 10px;
|
|
4634
|
+
font-family: var(--font-display);
|
|
4635
|
+
font-weight: 700;
|
|
4636
|
+
font-size: 17px;
|
|
4637
|
+
}
|
|
4638
|
+
.friend-zone-title svg {
|
|
4639
|
+
flex: none;
|
|
4640
|
+
width: 20px;
|
|
4641
|
+
height: 20px;
|
|
4642
|
+
color: var(--accent);
|
|
4643
|
+
}
|
|
4644
|
+
.friend-zone-desc {
|
|
4645
|
+
margin-top: 6px;
|
|
4646
|
+
font-size: 14px;
|
|
4647
|
+
color: var(--muted);
|
|
4648
|
+
line-height: 1.7;
|
|
4649
|
+
}
|
|
4650
|
+
.friend-search {
|
|
4651
|
+
margin-left: auto;
|
|
4652
|
+
display: flex;
|
|
4653
|
+
align-items: center;
|
|
4654
|
+
gap: 8px;
|
|
4655
|
+
width: min(230px, 100%);
|
|
4656
|
+
padding: 7px 12px;
|
|
4657
|
+
border: 1px solid var(--border);
|
|
4658
|
+
border-radius: 999px;
|
|
4659
|
+
background: var(--bg-soft);
|
|
4660
|
+
transition: border-color 0.15s;
|
|
4661
|
+
}
|
|
4662
|
+
.friend-search:focus-within {
|
|
4663
|
+
border-color: var(--accent);
|
|
4664
|
+
}
|
|
4665
|
+
.friend-search svg {
|
|
4666
|
+
flex: none;
|
|
4667
|
+
width: 15px;
|
|
4668
|
+
height: 15px;
|
|
4669
|
+
color: var(--muted);
|
|
4670
|
+
}
|
|
4671
|
+
.friend-search input {
|
|
4672
|
+
flex: 1;
|
|
4673
|
+
min-width: 0;
|
|
4674
|
+
border: none;
|
|
4675
|
+
background: transparent;
|
|
4676
|
+
outline: none;
|
|
4677
|
+
font-size: 13.5px;
|
|
4678
|
+
color: var(--ink);
|
|
4679
|
+
}
|
|
4680
|
+
.friend-search input::placeholder {
|
|
4681
|
+
color: var(--muted);
|
|
4682
|
+
}
|
|
4683
|
+
.friend-pills {
|
|
4684
|
+
margin-top: 14px;
|
|
4685
|
+
display: flex;
|
|
4686
|
+
flex-wrap: wrap;
|
|
4687
|
+
gap: 8px;
|
|
4688
|
+
}
|
|
4689
|
+
.friend-pills .chip {
|
|
4690
|
+
cursor: pointer;
|
|
4691
|
+
font-family: inherit;
|
|
4692
|
+
}
|
|
4693
|
+
.friend-zone .friend-grid {
|
|
4694
|
+
margin-top: 18px;
|
|
4695
|
+
}
|
|
4696
|
+
/* .friend-card 的 display:flex 会覆盖 UA 的 [hidden],需显式收回 */
|
|
4697
|
+
.friend-card[hidden],
|
|
4698
|
+
.tag-empty[hidden] {
|
|
4699
|
+
display: none;
|
|
4700
|
+
}
|
|
4701
|
+
.friend-zone .tag-empty {
|
|
4702
|
+
margin-top: 18px;
|
|
4703
|
+
}
|
|
4704
|
+
|
|
4705
|
+
/* ---------- 10.4 本站信息 / 申请友链 双卡与注意事项 ---------- */
|
|
4706
|
+
.friend-duo {
|
|
4707
|
+
margin-top: 20px;
|
|
4708
|
+
display: grid;
|
|
4709
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
4710
|
+
gap: 20px;
|
|
4711
|
+
}
|
|
4712
|
+
.friend-duo > .card:only-child {
|
|
4713
|
+
grid-column: 1 / -1;
|
|
4714
|
+
}
|
|
4715
|
+
@media (max-width: 767px) {
|
|
4716
|
+
.friend-duo {
|
|
4717
|
+
grid-template-columns: 1fr;
|
|
4718
|
+
}
|
|
4719
|
+
}
|
|
4720
|
+
.friend-siteinfo,
|
|
4721
|
+
.friend-apply-card {
|
|
4722
|
+
display: flex;
|
|
4723
|
+
flex-direction: column;
|
|
4724
|
+
}
|
|
4725
|
+
.friend-siteinfo,
|
|
4726
|
+
.friend-apply-card,
|
|
4727
|
+
.friend-notice {
|
|
4728
|
+
padding: 22px 24px;
|
|
4729
|
+
}
|
|
4730
|
+
.friend-card-title {
|
|
4731
|
+
display: flex;
|
|
4732
|
+
align-items: center;
|
|
4733
|
+
gap: 10px;
|
|
4734
|
+
font-family: var(--font-display);
|
|
4735
|
+
font-weight: 700;
|
|
4736
|
+
font-size: 17px;
|
|
4737
|
+
}
|
|
4738
|
+
.friend-card-title svg {
|
|
4739
|
+
flex: none;
|
|
4740
|
+
width: 18px;
|
|
4741
|
+
height: 18px;
|
|
4742
|
+
color: var(--accent);
|
|
4743
|
+
}
|
|
4744
|
+
/* 本站信息卡头:头像 + 名称 + 描述(对齐参考布局) */
|
|
4745
|
+
.siteinfo-head {
|
|
4746
|
+
display: flex;
|
|
4747
|
+
align-items: center;
|
|
4748
|
+
gap: 12px;
|
|
4749
|
+
margin-top: 16px;
|
|
4750
|
+
}
|
|
4751
|
+
.siteinfo-avatar {
|
|
4752
|
+
flex: none;
|
|
4753
|
+
display: flex;
|
|
4754
|
+
align-items: center;
|
|
4755
|
+
justify-content: center;
|
|
4756
|
+
width: 48px;
|
|
4757
|
+
height: 48px;
|
|
4758
|
+
border-radius: 50%;
|
|
4759
|
+
overflow: hidden;
|
|
4760
|
+
border: 1px solid var(--border);
|
|
4761
|
+
background: var(--bg-soft);
|
|
4762
|
+
}
|
|
4763
|
+
.siteinfo-avatar img {
|
|
4764
|
+
width: 100%;
|
|
4765
|
+
height: 100%;
|
|
4766
|
+
object-fit: cover;
|
|
4767
|
+
}
|
|
4768
|
+
.siteinfo-meta {
|
|
4769
|
+
min-width: 0;
|
|
4770
|
+
}
|
|
4771
|
+
.siteinfo-name {
|
|
4772
|
+
font-family: var(--font-display);
|
|
4773
|
+
font-weight: 700;
|
|
4774
|
+
font-size: 15px;
|
|
4775
|
+
}
|
|
4776
|
+
.siteinfo-desc {
|
|
4777
|
+
margin-top: 2px;
|
|
4778
|
+
font-size: 13px;
|
|
4779
|
+
color: var(--muted);
|
|
4780
|
+
line-height: 1.5;
|
|
4781
|
+
}
|
|
4782
|
+
.kv-row {
|
|
4783
|
+
display: flex;
|
|
4784
|
+
align-items: center;
|
|
4785
|
+
gap: 10px;
|
|
4786
|
+
margin-top: 10px;
|
|
4787
|
+
padding: 10px 12px;
|
|
4788
|
+
background: var(--bg-soft);
|
|
4789
|
+
border: 1px solid var(--border);
|
|
4790
|
+
border-radius: 8px;
|
|
4791
|
+
}
|
|
4792
|
+
.kv-main {
|
|
4793
|
+
flex: 1;
|
|
4794
|
+
min-width: 0;
|
|
4795
|
+
display: flex;
|
|
4796
|
+
flex-direction: column;
|
|
4797
|
+
gap: 2px;
|
|
4798
|
+
}
|
|
4799
|
+
.kv-label {
|
|
4800
|
+
font-size: 12px;
|
|
4801
|
+
color: var(--muted);
|
|
4802
|
+
}
|
|
4803
|
+
.kv-value {
|
|
4804
|
+
font-size: 13.5px;
|
|
4805
|
+
color: var(--ink);
|
|
4806
|
+
line-height: 1.5;
|
|
4807
|
+
word-break: break-all;
|
|
4808
|
+
}
|
|
4809
|
+
.copy-btn {
|
|
4810
|
+
flex: none;
|
|
4811
|
+
display: inline-flex;
|
|
4812
|
+
align-items: center;
|
|
4813
|
+
justify-content: center;
|
|
4814
|
+
width: 26px;
|
|
4815
|
+
height: 26px;
|
|
4816
|
+
border: 1px solid var(--border);
|
|
4817
|
+
border-radius: 6px;
|
|
4818
|
+
background: transparent;
|
|
4819
|
+
color: var(--muted);
|
|
4820
|
+
cursor: pointer;
|
|
4821
|
+
transition:
|
|
4822
|
+
color 0.15s,
|
|
4823
|
+
border-color 0.15s;
|
|
4824
|
+
}
|
|
4825
|
+
.copy-btn:hover {
|
|
4826
|
+
color: var(--accent-ink);
|
|
4827
|
+
border-color: var(--accent);
|
|
4828
|
+
}
|
|
4829
|
+
.copy-btn svg {
|
|
4830
|
+
width: 14px;
|
|
4831
|
+
height: 14px;
|
|
4832
|
+
}
|
|
4833
|
+
.copy-btn .icon-check {
|
|
4834
|
+
display: none;
|
|
4835
|
+
}
|
|
4836
|
+
.copy-btn.is-copied {
|
|
4837
|
+
color: var(--accent-ink);
|
|
4838
|
+
border-color: var(--accent);
|
|
4839
|
+
}
|
|
4840
|
+
.copy-btn.is-copied .icon-copy {
|
|
4841
|
+
display: none;
|
|
4842
|
+
}
|
|
4843
|
+
.copy-btn.is-copied .icon-check {
|
|
4844
|
+
display: block;
|
|
4845
|
+
}
|
|
4846
|
+
.friend-apply-card .fa-desc {
|
|
4847
|
+
margin-top: 6px;
|
|
4848
|
+
font-size: 14px;
|
|
4849
|
+
color: var(--muted);
|
|
4850
|
+
line-height: 1.7;
|
|
4851
|
+
}
|
|
4852
|
+
.apply-steps {
|
|
4853
|
+
list-style: none;
|
|
4854
|
+
margin: 16px 0 0;
|
|
4855
|
+
padding: 0;
|
|
4856
|
+
display: grid;
|
|
4857
|
+
gap: 12px;
|
|
4858
|
+
}
|
|
4859
|
+
.apply-step {
|
|
4860
|
+
display: flex;
|
|
4861
|
+
align-items: flex-start;
|
|
4862
|
+
gap: 12px;
|
|
4863
|
+
}
|
|
4864
|
+
.apply-step-num {
|
|
4865
|
+
flex: none;
|
|
4866
|
+
display: inline-flex;
|
|
4867
|
+
align-items: center;
|
|
4868
|
+
justify-content: center;
|
|
4869
|
+
width: 22px;
|
|
4870
|
+
height: 22px;
|
|
4871
|
+
border-radius: 50%;
|
|
4872
|
+
background: var(--accent-soft);
|
|
4873
|
+
color: var(--accent-ink);
|
|
4874
|
+
font-size: 12px;
|
|
4875
|
+
font-weight: 700;
|
|
4876
|
+
margin-top: 1px;
|
|
4877
|
+
}
|
|
4878
|
+
.apply-step-title {
|
|
4879
|
+
font-weight: 600;
|
|
4880
|
+
font-size: 14.5px;
|
|
4881
|
+
}
|
|
4882
|
+
.apply-step-desc {
|
|
4883
|
+
margin-top: 2px;
|
|
4884
|
+
font-size: 13px;
|
|
4885
|
+
color: var(--muted);
|
|
4886
|
+
line-height: 1.6;
|
|
4887
|
+
}
|
|
4888
|
+
/* 申请资料模板:整块展示 + 右上角一键复制(对齐参考布局) */
|
|
4889
|
+
.apply-template {
|
|
4890
|
+
position: relative;
|
|
4891
|
+
margin-top: 14px;
|
|
4892
|
+
padding: 12px 46px 12px 16px;
|
|
4893
|
+
background: var(--bg-soft);
|
|
4894
|
+
border: 1px solid var(--border);
|
|
4895
|
+
border-radius: 8px;
|
|
4896
|
+
}
|
|
4897
|
+
.apply-template .copy-btn {
|
|
4898
|
+
position: absolute;
|
|
4899
|
+
top: 8px;
|
|
4900
|
+
right: 8px;
|
|
4901
|
+
}
|
|
4902
|
+
.apply-template-line {
|
|
4903
|
+
font-size: 13px;
|
|
4904
|
+
color: var(--ink);
|
|
4905
|
+
line-height: 1.9;
|
|
4906
|
+
word-break: break-all;
|
|
4907
|
+
}
|
|
4908
|
+
.friend-apply-card .btn {
|
|
4909
|
+
margin-top: 16px;
|
|
4910
|
+
width: 100%;
|
|
4911
|
+
justify-content: center;
|
|
4912
|
+
}
|
|
4913
|
+
.friend-notice {
|
|
4914
|
+
margin-top: 20px;
|
|
4915
|
+
}
|
|
4916
|
+
.notice-list {
|
|
4917
|
+
margin: 14px 0 0;
|
|
4918
|
+
padding: 0;
|
|
4919
|
+
display: grid;
|
|
4920
|
+
gap: 12px;
|
|
4921
|
+
}
|
|
4922
|
+
.notice-item dt {
|
|
4923
|
+
font-weight: 700;
|
|
4924
|
+
font-size: 14px;
|
|
4925
|
+
}
|
|
4926
|
+
.notice-item dd {
|
|
4927
|
+
margin: 2px 0 0;
|
|
4928
|
+
font-size: 13.5px;
|
|
4929
|
+
color: var(--muted);
|
|
4930
|
+
line-height: 1.7;
|
|
4931
|
+
}
|
|
4932
|
+
|
|
4529
4933
|
/* ---------- 10.1 字头像兜底(友链/打赏名单无图时) ---------- */
|
|
4530
4934
|
.avatar-fallback {
|
|
4531
4935
|
display: inline-flex;
|
|
@@ -5592,6 +5996,10 @@ a[data-encoded-email] {
|
|
|
5592
5996
|
height: auto;
|
|
5593
5997
|
border-radius: var(--radius-sm);
|
|
5594
5998
|
}
|
|
5999
|
+
/* display:block 会覆盖 [hidden] 的 UA 样式,显式声明防生成前空框露出 */
|
|
6000
|
+
.poster-img[hidden] {
|
|
6001
|
+
display: none;
|
|
6002
|
+
}
|
|
5595
6003
|
.poster-loading {
|
|
5596
6004
|
display: flex;
|
|
5597
6005
|
align-items: center;
|