@bbki.ng/site 5.4.21 → 5.4.24

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.
Files changed (103) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/index.html +12 -19
  3. package/package.json +5 -8
  4. package/src/blog/app.tsx +32 -44
  5. package/src/blog/components/article/index.tsx +8 -17
  6. package/src/blog/components/index.tsx +0 -12
  7. package/src/blog/components/share/share-btn.tsx +5 -8
  8. package/src/blog/constants/index.ts +1 -16
  9. package/src/blog/constants/routes.ts +12 -22
  10. package/src/blog/context/bbcontext.tsx +7 -7
  11. package/src/blog/hooks/index.ts +3 -18
  12. package/src/blog/hooks/use_dynamic_logo.tsx +6 -11
  13. package/src/blog/hooks/use_posts.ts +19 -10
  14. package/src/blog/hooks/use_role.ts +9 -14
  15. package/src/blog/hooks/use_streaming.ts +16 -16
  16. package/src/blog/index.tsx +5 -11
  17. package/src/blog/pages/cover/index.tsx +0 -1
  18. package/src/blog/pages/extensions/txt/article.tsx +7 -21
  19. package/src/blog/pages/extensions/txt/index.tsx +3 -20
  20. package/src/blog/pages/login/index.tsx +18 -42
  21. package/src/blog/swr.tsx +4 -7
  22. package/src/blog/utils/index.ts +9 -172
  23. package/vite.config.js +2 -1
  24. package/src/blog/articles/anti-logic.mdx +0 -61
  25. package/src/blog/articles/bbking-manual.mdx +0 -7
  26. package/src/blog/articles/black-river.mdx +0 -8
  27. package/src/blog/articles/celebration.mdx +0 -21
  28. package/src/blog/articles/cloth.mdx +0 -11
  29. package/src/blog/articles/cooking.mdx +0 -7
  30. package/src/blog/articles/cooldown.mdx +0 -12
  31. package/src/blog/articles/cousin.mdx +0 -15
  32. package/src/blog/articles/fall.mdx +0 -8
  33. package/src/blog/articles/img.mdx +0 -104
  34. package/src/blog/articles/leaves.mdx +0 -7
  35. package/src/blog/articles/liqiu.mdx +0 -7
  36. package/src/blog/articles/loading.mdx +0 -144
  37. package/src/blog/articles/love.mdx +0 -19
  38. package/src/blog/articles/major-cold.mdx +0 -14
  39. package/src/blog/articles/marshroom.mdx +0 -17
  40. package/src/blog/articles/men-without-women.mdx +0 -19
  41. package/src/blog/articles/moment.mdx +0 -9
  42. package/src/blog/articles/movie-day.mdx +0 -15
  43. package/src/blog/articles/photos.mdx +0 -13
  44. package/src/blog/articles/projects.mdx +0 -8
  45. package/src/blog/articles/pseudo-spring.mdx +0 -7
  46. package/src/blog/articles/quote.mdx +0 -26
  47. package/src/blog/articles/red-gun.mdx +0 -19
  48. package/src/blog/articles/rice-noodle.mdx +0 -21
  49. package/src/blog/articles/spring-cooldown.mdx +0 -8
  50. package/src/blog/articles/spring-rain.mdx +0 -10
  51. package/src/blog/articles/travel.mdx +0 -22
  52. package/src/blog/articles/warming-up.mdx +0 -10
  53. package/src/blog/articles/web-burnning.mdx +0 -10
  54. package/src/blog/articles/woke-up.mdx +0 -7
  55. package/src/blog/articles/xwy-and-snowing.mdx +0 -13
  56. package/src/blog/articles/xwy.mdx +0 -9
  57. package/src/blog/components/ImageUploader.tsx +0 -55
  58. package/src/blog/components/Img_ctx_menu/index.tsx +0 -67
  59. package/src/blog/components/Logger.tsx +0 -9
  60. package/src/blog/components/Pochacco/Pochacco.tsx +0 -29
  61. package/src/blog/components/Pochacco/idle.tsx +0 -31
  62. package/src/blog/components/Pochacco/watch.tsx +0 -28
  63. package/src/blog/components/Version.tsx +0 -14
  64. package/src/blog/components/app_ctx_menu/LoginMenuItem.tsx +0 -72
  65. package/src/blog/components/app_ctx_menu/PostMenuItem.tsx +0 -22
  66. package/src/blog/components/app_ctx_menu/VersionMenuItem.tsx +0 -13
  67. package/src/blog/components/app_ctx_menu/ViewSourceMenuItem.tsx +0 -34
  68. package/src/blog/components/app_ctx_menu/index.tsx +0 -35
  69. package/src/blog/components/article_ctx_menu/index.tsx +0 -58
  70. package/src/blog/components/aspect_ratio_box/index.tsx +0 -29
  71. package/src/blog/components/blur_cover/index.tsx +0 -28
  72. package/src/blog/components/hotkey_nav/index.tsx +0 -51
  73. package/src/blog/components/progress_bar/index.tsx +0 -31
  74. package/src/blog/components/reaction/emojis.tsx +0 -143
  75. package/src/blog/components/reaction/oh_reaction.tsx +0 -105
  76. package/src/blog/components/reload_prompt/index.tsx +0 -51
  77. package/src/blog/components/tags/index.tsx +0 -52
  78. package/src/blog/components/video_player/index.tsx +0 -82
  79. package/src/blog/global/mdx.d.ts +0 -9
  80. package/src/blog/hooks/useLoadingIndicator.ts +0 -12
  81. package/src/blog/hooks/useScrollToTop.ts +0 -24
  82. package/src/blog/hooks/useTransitionCls.ts +0 -36
  83. package/src/blog/hooks/use_authed.ts +0 -7
  84. package/src/blog/hooks/use_authed_fetcher.ts +0 -8
  85. package/src/blog/hooks/use_authed_string_post.ts +0 -42
  86. package/src/blog/hooks/use_clipboard_content.ts +0 -21
  87. package/src/blog/hooks/use_clipboard_to_post.ts +0 -48
  88. package/src/blog/hooks/use_del_img.ts +0 -22
  89. package/src/blog/hooks/use_delete_post.ts +0 -22
  90. package/src/blog/hooks/use_file_to_post.ts +0 -38
  91. package/src/blog/hooks/use_img_loading.ts +0 -16
  92. package/src/blog/hooks/use_post.ts +0 -26
  93. package/src/blog/hooks/use_projects.ts +0 -67
  94. package/src/blog/hooks/use_route_name.ts +0 -7
  95. package/src/blog/hooks/use_shared_string_to_post.ts +0 -23
  96. package/src/blog/hooks/use_supa_session.ts +0 -31
  97. package/src/blog/hooks/use_text_plain_file.ts +0 -36
  98. package/src/blog/hooks/use_uploader.ts +0 -34
  99. package/src/blog/hooks/use_video_controls.ts +0 -71
  100. package/src/blog/pages/upload/index.tsx +0 -39
  101. package/src/blog/types/supabase.ts +0 -12
  102. package/src/blog/types/upload.ts +0 -16
  103. package/src/blog/utils/tags.ts +0 -21
@@ -1,23 +1,13 @@
1
- import React, { ReactElement, useContext, useEffect } from 'react';
2
- import { withArticleWrapper } from '@/components';
3
- import { MdxArticle } from '@/types/articles';
4
- import { NotFound, DropZone } from '@bbki.ng/components';
5
- import { useLocation, useParams } from 'react-router-dom';
1
+ import React from 'react';
2
+ import { NotFound } from '@bbki.ng/components';
3
+ import { useParams } from 'react-router-dom';
6
4
  import { usePosts } from '@/hooks/use_posts';
7
5
  import { ArticlePage } from '@/components/article';
8
- import { GlobalLoadingContext } from '@/context/global_loading_state_provider';
9
- import { useFile2Post } from '@/hooks/use_file_to_post';
10
- import { useAuthed } from '@/hooks/use_authed';
11
- import { ArticleCtxMenu } from '@/components/article_ctx_menu';
12
6
  import { useBlogScrollReset } from '@/hooks/use_blog_scroll_pos_restoration';
13
7
 
14
8
  export default () => {
15
9
  const { title } = useParams();
16
10
  const { posts, isError, isLoading } = usePosts(title);
17
- const { setIsLoading } = useContext(GlobalLoadingContext);
18
-
19
- const reader = useFile2Post();
20
- const isKing = useAuthed();
21
11
 
22
12
  useBlogScrollReset();
23
13
 
@@ -33,15 +23,11 @@ export default () => {
33
23
  return null;
34
24
  }
35
25
 
36
- const date = posts.created_at ? posts.created_at.split('T')[0] : '';
26
+ const date = posts.createdAt ? posts.createdAt.split('T')[0] : '';
37
27
 
38
28
  return (
39
- <DropZone onDrop={reader} disabled={!isKing}>
40
- <ArticlePage title={title} date={date}>
41
- <ArticleCtxMenu>
42
- <div dangerouslySetInnerHTML={{ __html: posts.content }} />
43
- </ArticleCtxMenu>
44
- </ArticlePage>
45
- </DropZone>
29
+ <ArticlePage title={title} date={date}>
30
+ <div dangerouslySetInnerHTML={{ __html: posts.content }} />
31
+ </ArticlePage>
46
32
  );
47
33
  };
@@ -1,11 +1,7 @@
1
1
  import React from 'react';
2
- import { LinkProps, DropZone, Button } from '@bbki.ng/components';
2
+ import { LinkProps, Button } from '@bbki.ng/components';
3
3
  import { usePosts } from '@/hooks/use_posts';
4
4
  import { CenterLinkList } from '@/components';
5
- import { useAuthed } from '@/hooks/use_authed';
6
- import { useFile2Post } from '@/hooks/use_file_to_post';
7
- import { useClipboardToPost } from '@/hooks/use_clipboard_to_post';
8
- import { useLocation } from 'react-router-dom';
9
5
  import { useBlogScroll, useBlogScrollRestoration } from '@/hooks/use_blog_scroll_pos_restoration';
10
6
 
11
7
  type TxtProps = {
@@ -13,7 +9,7 @@ type TxtProps = {
13
9
  articleList?: LinkProps[];
14
10
  };
15
11
 
16
- const Posts = (props: TxtProps) => {
12
+ export default (props: TxtProps) => {
17
13
  const { titleList, isLoading, isError } = usePosts();
18
14
 
19
15
  useBlogScrollRestoration();
@@ -28,11 +24,9 @@ const Posts = (props: TxtProps) => {
28
24
  return <CenterLinkList links={props.articleList} />;
29
25
  }
30
26
 
31
- const links = [...titleList];
32
-
33
27
  return (
34
28
  <CenterLinkList
35
- links={props.articleList || links}
29
+ links={props.articleList || titleList}
36
30
  loading={isLoading}
37
31
  footer={
38
32
  <Button onClick={gotoTop} className="mt-128">
@@ -55,14 +49,3 @@ const Posts = (props: TxtProps) => {
55
49
  />
56
50
  );
57
51
  };
58
-
59
- export default (props: TxtProps) => {
60
- const reader = useFile2Post();
61
- const isKing = useAuthed();
62
-
63
- return (
64
- <DropZone onDrop={reader} disabled={!isKing}>
65
- <Posts {...props} />
66
- </DropZone>
67
- );
68
- };
@@ -1,48 +1,24 @@
1
- import React, { useContext, useState } from "react";
2
- import { Button, ButtonType } from "@bbki.ng/components";
3
- import { OauthProvider } from "@/types/supabase";
4
- import { supabase } from "@/constants";
5
- import { ArticlePage } from "@/components/article";
6
- import { useSupabaseSession } from "@/hooks/use_supa_session";
7
- import { Navigate } from "react-router-dom";
8
- import { GlobalLoadingContext } from "@/context/global_loading_state_provider";
1
+ import React from 'react';
2
+ import { ArticlePage } from '@/components/article';
9
3
 
4
+ /**
5
+ * Login page
6
+ * Note: OAuth authentication has been removed. Authentication is now handled
7
+ * via API keys in the CLI tool. Frontend authentication may be re-implemented
8
+ * in the future.
9
+ */
10
10
  export const Login = () => {
11
- const { isLoading, setIsLoading } = useContext(GlobalLoadingContext);
12
-
13
- const session = useSupabaseSession();
14
- if (session) {
15
- return <Navigate to="/" />;
16
- }
17
-
18
11
  return (
19
- <ArticlePage title="第三方账号登录">
20
- <>
21
- <Button
22
- type={isLoading ? ButtonType.DISABLED : ButtonType.PRIMARY}
23
- className="ml-8"
24
- onClick={async () => {
25
- setIsLoading(true);
26
- return supabase.auth.signIn({
27
- provider: OauthProvider.GITHUB,
28
- });
29
- }}
30
- >
31
- GitHub
32
- </Button>
33
- <Button
34
- type={isLoading ? ButtonType.DISABLED : ButtonType.PRIMARY}
35
- className="ml-8"
36
- onClick={async () => {
37
- setIsLoading(true);
38
- return supabase.auth.signIn({
39
- provider: OauthProvider.Spotify,
40
- });
41
- }}
42
- >
43
- Spotify
44
- </Button>
45
- </>
12
+ <ArticlePage title="登录">
13
+ <div className="prose dark:prose-invert">
14
+ <p className="text-gray-600 dark:text-gray-400">网页登录功能已暂时禁用。</p>
15
+ <p className="text-gray-600 dark:text-gray-400">
16
+ 如需管理内容,请使用 CLI 工具并通过 API Key 进行认证:
17
+ </p>
18
+ <pre className="bg-gray-100 dark:bg-gray-800 p-4 rounded">
19
+ <code>bbking login</code>
20
+ </pre>
21
+ </div>
46
22
  </ArticlePage>
47
23
  );
48
24
  };
package/src/blog/swr.tsx CHANGED
@@ -1,15 +1,12 @@
1
- import React from "react";
2
- import { SWRConfig } from "swr";
3
- import { apiFetcher, withToken } from "@/utils";
4
- import { useSupabaseSession } from "@/hooks/use_supa_session";
1
+ import React from 'react';
2
+ import { SWRConfig } from 'swr';
3
+ import { cfApiFetcher } from '@/utils';
5
4
 
6
5
  export const SWR = (props: { children: any }) => {
7
- const { access_token: token } = useSupabaseSession() || {};
8
- const authedApiFetcher = withToken(apiFetcher)(token);
9
6
  return (
10
7
  <SWRConfig
11
8
  value={{
12
- fetcher: authedApiFetcher,
9
+ fetcher: cfApiFetcher,
13
10
  }}
14
11
  >
15
12
  {props.children}
@@ -1,10 +1,5 @@
1
- import { Photo } from "@/types/photo";
2
- import { ossProcessType } from "@/types/oss";
3
- import { API_CF_ENDPOINT, API_ENDPOINT, OSS_ADDRESS } from "@/constants/routes";
4
- import { DEFAULT_DELAY } from "@/constants";
5
- import useSWR from "swr";
6
- import { toast } from "sonner";
7
- import { FontType } from "@/types/font";
1
+ import { API_ENDPOINT } from '@/constants/routes';
2
+ import { FontType } from '@/types/font';
8
3
 
9
4
  type Fetcher = (resource: string, init?: any) => Promise<any>;
10
5
 
@@ -12,183 +7,25 @@ export const floatNumberToPercentageString = (num: number): string => {
12
7
  return `${num * 100}%`;
13
8
  };
14
9
 
15
- export const delay = (time: number) => {
16
- return new Promise((resolve) => {
17
- setTimeout(resolve, time);
18
- });
19
- };
20
-
21
- export const minDelay = async (
22
- promise: Promise<any>,
23
- time: number = DEFAULT_DELAY
24
- ): Promise<any> => {
25
- const delayPromise = delay(time);
26
- await Promise.all([delayPromise, promise]);
27
- return promise;
28
- };
29
-
30
- export const addOssWebpProcessStyle = (
31
- originUrl: string,
32
- style: ossProcessType
33
- ): string => {
34
- const isInvalidOSSImgUrl = !originUrl.startsWith(OSS_ADDRESS);
35
- const isProcessedOssImg = /x-oss-process=style\/\w+/.test(originUrl);
36
- const isWebpImg = /webp$/.test(originUrl);
37
-
38
- if (
39
- isInvalidOSSImgUrl ||
40
- isProcessedOssImg ||
41
- (isWebpImg && style === ossProcessType.WEBP) ||
42
- style === ossProcessType.NULL
43
- ) {
44
- return originUrl;
45
- }
46
- return `${originUrl}?x-oss-process=style/${style}`;
47
- };
48
-
49
- export const calcDefaultImgSize = (
50
- img: Photo,
51
- defaultWidth?: number,
52
- scale?: number
53
- ): { width: number; height: number } => {
54
- const { width, height } = img;
55
- const whRatio = width / height;
56
- const isHorizontal = width > height;
57
-
58
- const finalWidth =
59
- (defaultWidth || (isHorizontal ? 576 : 384)) * (scale || 1);
60
-
61
- return {
62
- width: finalWidth,
63
- height: finalWidth / whRatio,
64
- };
65
- };
66
-
67
- export const getEnv = () => {
68
- return /^http:\/\/localhost/.test(location.href)
69
- ? "development"
70
- : "production";
71
- };
72
-
73
10
  export const baseFetcher = (resource: string, init: RequestInit = {}) =>
74
- fetch(resource, init).then((res) => {
11
+ fetch(resource, init).then(res => {
75
12
  if (!res.ok) {
76
- throw new Error("An error occurred while fetching the data.");
13
+ throw new Error('An error occurred while fetching the data.');
77
14
  }
78
15
 
79
16
  return res.json();
80
17
  });
81
18
 
82
- export const withToken =
83
- (fetcher: Fetcher) =>
84
- (token?: string) =>
85
- (resource: string, init: RequestInit = {}) => {
86
- const { headers = {} } = init;
87
- const tokenHeaders = token
88
- ? {
89
- "X-Supabase-Auth": token,
90
- }
91
- : {};
92
- const finalHeaders = {
93
- ...headers,
94
- ...tokenHeaders,
95
- };
96
- return fetcher(resource, {
97
- ...init,
98
- headers: finalHeaders,
99
- });
100
- };
101
-
102
19
  export const withBBApi =
103
20
  (fetcher: Fetcher) =>
104
- (apiEndPoint = API_ENDPOINT): Fetcher =>
21
+ (apiEndPoint: string): Fetcher =>
105
22
  async (resource: string, init: RequestInit = {}) =>
106
- fetcher(`${apiEndPoint}/${resource}`, { ...init, mode: "cors" });
107
-
108
- export const apiFetcher = withBBApi(baseFetcher)(API_ENDPOINT);
109
-
110
- export const cfApiFetcher = withBBApi(baseFetcher)(API_CF_ENDPOINT);
111
-
112
- export const getImageFileSize = (
113
- file: File
114
- ): Promise<{ width: number; height: number }> => {
115
- const url = URL.createObjectURL(file);
116
- const img = new Image();
117
- return new Promise((resolve) => {
118
- img.src = url;
119
- img.onload = function (e) {
120
- resolve({
121
- width: img.naturalWidth,
122
- height: img.naturalHeight,
123
- });
124
- };
125
- });
126
- };
127
-
128
- export const buildSimpleApiHooks = (api: string, payloadKey: string) => {
129
- return () => {
130
- const { data, error } = useSWR(api);
131
- return {
132
- [payloadKey]: data,
133
- isError: error,
134
- isLoading: !data && !error,
135
- };
136
- };
137
- };
138
-
139
- export const imageFormatter = (image: any) => {
140
- const { rendered_width, thumbnail_src, avg_color, process_type, ...rest } =
141
- image;
142
- return {
143
- renderedWidth: rendered_width,
144
- thumbnailSrc: thumbnail_src,
145
- avgColor: avg_color,
146
- processType: process_type,
147
- ...rest,
148
- };
149
- };
150
-
151
- export const getRandomInt = (min: number, max: number) => {
152
- min = Math.ceil(min);
153
- max = Math.floor(max);
154
- return Math.floor(Math.random() * (max - min + 1)) + min;
155
- };
156
-
157
- export const copyToClipboard = async (value: string) => {
158
- await navigator.clipboard.writeText(value);
159
- };
160
-
161
- export const confirm = (message: string, exec: () => void) => {
162
- toast("", {
163
- description: message,
164
- actionButtonStyle: {
165
- backgroundColor: "#fff",
166
- color: "rgb(37,99,235)",
167
- },
168
- position: "bottom-right",
169
- action: {
170
- label: "是",
171
- onClick: () => {
172
- exec();
173
- },
174
- },
175
- });
176
- };
177
-
178
- export const splitPost= (wholeString: string ) => {
179
- const firstLine = wholeString ? wholeString .split("\n")[0] : "";
180
- const title = firstLine ? firstLine.trim() : "";
181
-
182
- const restContent = wholeString ? wholeString.slice(title.length).trim() : "";
23
+ fetcher(`${apiEndPoint}/${resource}`, { ...init, mode: 'cors' });
183
24
 
184
- return {
185
- title,
186
- content: restContent,
187
- };
188
- }
25
+ export const cfApiFetcher = withBBApi(baseFetcher)(API_ENDPOINT);
189
26
 
190
27
  export const changeFont = (type: FontType) => {
191
- const rootDiv = document.getElementById("root");
28
+ const rootDiv = document.getElementById('root');
192
29
  if (rootDiv == null) {
193
30
  return;
194
31
  }
@@ -205,5 +42,5 @@ export const changeFont = (type: FontType) => {
205
42
  rootDiv.classList.add(type);
206
43
 
207
44
  // save font type to local storage
208
- localStorage.setItem("font", type);
45
+ localStorage.setItem('font', type);
209
46
  };
package/vite.config.js CHANGED
@@ -31,7 +31,7 @@ const options = {
31
31
  export default defineConfig({
32
32
  server: {
33
33
  proxy: {
34
- '/api/streaming': {
34
+ '/api': {
35
35
  target: 'http://localhost:8787',
36
36
  rewrite: path => path.replace(/^\/api/, ''),
37
37
  },
@@ -71,6 +71,7 @@ export default defineConfig({
71
71
  tailwindcss(),
72
72
  VitePWA({
73
73
  injectRegister: 'auto',
74
+ registerType: 'autoUpdate',
74
75
  includeAssets: ['favicon.svg', 'robots.txt', 'apple-touch-icon.png', 'Logo.svg'],
75
76
  devOptions: {
76
77
  enabled: true,
@@ -1,61 +0,0 @@
1
- ---
2
- title: 与或非禁区
3
- ---
4
-
5
- **1. 古老的小区**
6
-
7
- 我搬进 \
8
- 一个古老的小区的胃里
9
-
10
- 堆积的法国梧桐树叶 \
11
- 和我的过往 \
12
- 加重了它消化不良的症状
13
-
14
- 北方的冷空气穿过冲积平原 \
15
- 匍匐在小区旁的江面休息 \
16
- 玩闹
17
-
18
- 手拉着手的山脉远在南边等候 \
19
- 胃壁从此有水珠渗出
20
-
21
- 我的浴巾不再干燥
22
-
23
- **2. 信**
24
-
25
- 被子折叠成信封 \
26
- 身体干瘪成信纸
27
-
28
- 睡梦邮递心事。
29
-
30
- **3. 梦里的事**
31
-
32
- 梦里一群恒星散落在 \
33
- 外婆家屋后的草丛
34
-
35
- 梦里在摩天大楼上空蛙泳
36
-
37
- 梦里变成凶杀犯被通缉
38
-
39
- 梦里牙齿全部脱落 \
40
- 一点也不痛
41
-
42
- **3. 清明后**
43
-
44
- 左手是右手的手写板 \
45
- 五官不是心脏的显示屏 \
46
- 面无表情 \
47
- 手指划着手心
48
-
49
- 键盘上有飞蛾的卵 \
50
- 地毯下是腐烂的地板 \
51
- 没有把的拖把 \
52
- 已被晒干
53
-
54
- 通话质量不佳 \
55
- 扬声器变得沙哑 \
56
- Siri 把熊仔念成了熊子 \
57
- 乘着吹进车窗的 \
58
- 横风飘过副驾驶 \
59
- 飘进山谷带着 \
60
- 笑声、雾 \
61
- 消失在海拔五百二十米的高度
@@ -1,7 +0,0 @@
1
- ---
2
- title: 说明书
3
- tags:
4
- - bbki.ng
5
- ---
6
-
7
- 301 -> https://docs.bbki.ng
@@ -1,8 +0,0 @@
1
- ---
2
- title: 六月
3
- tags:
4
- - 随笔
5
- - 梦
6
- ---
7
-
8
- 梦见一条巨大、扭曲、干涸的黑色河流,它穿过城市,充斥着整个视野。梦中只有我,和河边篮球场玩滑板的一群陌生孩子。
@@ -1,21 +0,0 @@
1
- ---
2
- title: 庆典
3
- tags:
4
- - 摘抄
5
- - 诗歌
6
- ---
7
-
8
- ```
9
- 我把我的孤伶展在
10
- 桌上,像一张地图。
11
- 我绘制路线
12
- 去往我迎风的住处。
13
- 到达的人遇不见我。
14
- 我等的人不存在。
15
-
16
- 我喝下暴怒的烈酒
17
- 为了把那些面孔变成
18
- 一个天使,变成空杯子
19
-
20
- —— 摘自《夜的命名术》
21
- ```
@@ -1,11 +0,0 @@
1
- ---
2
- title: 衣服
3
- tags:
4
- - 随笔
5
- ---
6
-
7
- 衣服干了,没叠
8
-
9
- 白天我把它堆在床上
10
-
11
- 晚上把它挪到沙发上
@@ -1,7 +0,0 @@
1
- ---
2
- title: 做饭
3
- tags:
4
- - 随笔
5
- ---
6
-
7
- 自己做饭好玩的事情之一是吃饭的时候,面对食物有种熟悉的感觉:原来这是我切菜时犹豫要不要扔掉,最终留下来的辣椒屁股。
@@ -1,12 +0,0 @@
1
- ---
2
- title: 降温
3
- tags:
4
- - 随笔
5
- - 天气
6
- ---
7
-
8
- 跟去年一样,第一时间感受到了降温。不同的是,去年是走在路上突然反应过来,与低温偶遇。今年像被大队人马包围,为首的用喇叭喊话告知,你应该向低温投降。国庆跟朋友还不止一次聊起,记得某年国庆室友结婚我们已经在烤电火炉,为什么现在还是短袖呢?几天后,气温骤降。低温劈头盖脸而来。
9
-
10
- 越来越喜欢描述气候特点的词汇:四季分明,雨热同期,春秋短促,冬夏绵长,低温寡照,阴雨连绵。像朴素的大道理,包罗万象。「越冬作物可安全越冬,缓慢生长」,提到作物的句子也充满关心像母亲的语气。
11
-
12
- 作为湖南人自己会经常忘了这片土地大部分位于洞庭湖以南的事实。湘赣交界诸山和武陵山脉、雪峰山脉、南岭山脉,把东西南三面围住,唯独北边是冲积平原,洞开接收来自北方的寒意。
@@ -1,15 +0,0 @@
1
- ---
2
- title: 堂兄
3
- tags:
4
- - 随笔
5
- ---
6
-
7
- 春节假过完回长沙,堂兄开车送我去高铁站。
8
-
9
- 车内循环播放着《冻结》。这首歌出现在林俊杰首张专辑《乐行者》中,发行于 2003 年。当时我在堂兄家过暑假。那张专辑里堂兄循环最多的歌曲是《就是我》。他循环太多遍,还是小学生的我无意间听会,二十年后还是能想起一些歌词、旋律。
10
-
11
- 2003 年过暑假、堂兄长大的地方叫种蓄场。有大片橘树林,兔舍,猪舍,小鸡孵化厂房。还有只在学校见过的铁门和围墙。在老家,一到天黑小孩会被催着回家;在这里,天黑后大人也放心小孩出门结伴玩耍。他们围着空地烧火,抽烟,吹牛。他们在磨坊里躲迷藏,那里一袋袋玉米在二楼一直堆到天花板上。我记得停在门口的大卡车,和它散发出的橡胶轮胎和灰尘金属味道。
12
-
13
- 种蓄场倒闭,伯父去世,伯母改嫁,堂兄结婚生子、大病入院、妻子失踪。全部发生在这二十年间。「弟弟来你家要把你的菜全吃光了」,二十年前,种蓄场的职员这样逗堂兄玩。晚餐的时候,堂兄用筷子截住我伸出的筷子,不让我夹菜。
14
-
15
- 曾经父母宠爱,无忧无虑的堂兄,现在沉默寡言。
@@ -1,8 +0,0 @@
1
- ---
2
- title: 大雪
3
- tags:
4
- - 随笔
5
- - 天气
6
- ---
7
-
8
- 大雪节气过后,气温更低、起床变得更加困难。去吃粉的途中,发现街道两边的车顶和挡风玻璃上都铺满了落叶,地面上的落叶也铺得很厚。想要把落叶拢到一起,已经不能用常规的扫地动作。店家握着扫把一端,用小腹抵住往前推,人和扫把模拟出一辆简易版推土机。往前推进很短的距离,落叶便隆成一个小丘。地铁口的自动扶梯前有一片落叶,黄叶孤零零躺在不锈钢面板上,听着扶梯传动系统发出的枯燥声音。当代艺术中心大楼前的人工水池里也泡着落叶,有大叔用网兜打捞。不远处的草坪有人和银杏落叶合影。落叶和落雪给人们带来类似的烦恼和喜悦。
@@ -1,104 +0,0 @@
1
- ---
2
- title: 图片展示
3
- tags:
4
- - bbki.ng
5
- - web
6
- - 图片
7
- ---
8
-
9
- import { ImgDemo } from "@/demo/ImgDemo";
10
-
11
- 图片是 BBKi.ng 的主要内容之一,友好地展示图片是必须考虑的问题。
12
-
13
- 先写下第一行代码:
14
-
15
- ```html
16
- <img src="my-awesome-picture.jpg" />
17
- ```
18
-
19
- 这样就完成了图片展示。但是这种极简的做法存在一些问题,还谈不上友好。比如:
20
-
21
- 1. 布局偏移问题。我们没有为 `img` 标签设定尺寸,导致图片加载期间,浏览器没有在文档中为其分配正确的空间。加载完成后,图片突然撑开。
22
- 2. 性能问题。没有惰性渲染、惰性加载,多图页面会有潜在性能问题。
23
- 3. 逐行渲染问题。标准 JPEG 格式压缩的图片,下载时基线 JPEG 算法会逐行渲染图片,影响体验。
24
-
25
- ## 布局偏移
26
-
27
- 对于问题 1. 我们为图片设置好宽高即可:
28
-
29
- ```html
30
- <style>
31
- img {
32
- max-width: 100%;
33
- height: auto;
34
- }
35
- </style>
36
- <img height="853" width="1280" … />
37
- ```
38
-
39
- ## 性能相关
40
-
41
- ### `content-visibility: auto`
42
-
43
- 该 CSS 属性会告知浏览器,在图片接近屏幕前,先不要为其布局。浏览器也不会去解码用户暂时看不见的图片,节省 CPU。
44
-
45
- ### `loading="lazy"`
46
-
47
- 该属性会告知浏览器,在图片接近屏幕前,先不要去请求图片资源。
48
-
49
- ### `decoding="async"`
50
-
51
- 该属性会给浏览器脱离主线程解码图片的权限,避免用来解码图片的 CPU 时间影响用户。
52
-
53
- ## 呈现相关
54
-
55
- ### 渐进加载
56
-
57
- 直观体验:
58
-
59
- - [Medium](https://medium.com/cucumbertown-magazine/the-beginners-guide-to-composition-in-food-photography-how-to-transform-your-food-photos-from-good-39613ab78bf2)
60
- - [Polymer shop project](https://shop.polymer-project.org/)
61
-
62
- 上边两个例子在加载大图时,都用到了低质量图片占位(Low Quality Image Placeholder, LQIP)。在大图加载时,展示模糊的低质量图片,在加载低质量图片时,展示背景色。图片加载成功时,配合 transition 效果,整个过程显得顺滑流畅。
63
- [José M. Pérez](https://jmperezperez.com/about-me/) 在 2015 年写了[一篇文章](https://jmperezperez.com/medium-image-progressive-loading-placeholder/)详细分析了 Medium 图片渐进加载细节。
64
-
65
- 了解基本思路后,实现方式可以有很多种。其中关键,需要准备高清图对应的低质量图片,以及对低质量图片进行模糊处理。
66
-
67
- ### 低质量图片
68
-
69
- BBKi.ng 所有图片均存放在阿里云 OSS 中。OSS 数据处理下的图片处理功能可以创建样式。不同样式对应不同的图片处理细节,如图片格式转换、图片质量、缩略等。我们可以新建名为 `LQIP` 的样式,将原图压缩为低质量图片。假设上传原图后得到的地址为:`url/to/my-awesome-picture.jpg`, 那么通过地址 `url/to/my-awesome-picture.jpg?x-oss-process=style/LQIP` 即能获取低质量图片。
70
-
71
- ### 模糊效果
72
-
73
- 实现图片模糊有很多种方式。比如
74
-
75
- 1. CSS [滤镜](https://developer.mozilla.org/en-US/docs/Web/CSS/filter)
76
- 2. CSS [背景滤镜](https://developer.mozilla.org/en-US/docs/Web/CSS/backdrop)
77
- 3. Canvas 渲染([blurhash](https://blurha.sh/))
78
-
79
- ```
80
- img {
81
- filter: blur(2px)
82
- }
83
-
84
- .cover {
85
- backdrop-filter: blur(2px);
86
- }
87
- ```
88
-
89
- 以上几种方式都各有优缺点。CSS 滤镜优点是写法比较简单,也无需引入新的元素。缺点是,边缘也会模糊掉。可以增加一个包裹元素,设置 `overflow: hidden` 解决,但是模糊半径较大时,图片边缘的模糊效果始终不是很理想([效果对比](https://codepen.io/z-j-h/pen/ZEJPEWq));用 CSS 背景滤镜可以得到锐利清晰的边缘,同时图片内容区域模糊效果也很好,缺点是效果是应用在元素背后的区域,因此我们需要引入额外的元素,同时该样式会影响图层合成时间、默认不兼容火狐浏览器;Blurhash 方法的优点是模糊图片只需要用简洁哈希字符串表示,节省空间。缺点是编码解码过程稍显繁琐。同时客户端解码大尺寸图片时也存在性能问题。
90
-
91
- 综合考虑后,最终使用背景滤镜对图片进行模糊处理。
92
-
93
- **示例**
94
-
95
- <ImgDemo />
96
-
97
- ## 参考链接
98
-
99
- - https://web.dev/optimize-cls/
100
- - https://medium.com/hd-pro/jpeg-formats-progressive-vs-baseline-73b3938c2339
101
- - https://www.industrialempathy.com/posts/image-optimizations/
102
- - https://www.guypo.com/introducing-lqip-low-quality-image-placeholders
103
- - https://jmperezperez.com/medium-image-progressive-loading-placeholder/
104
- - https://github.com/vercel/next.js/blob/canary/packages/next/client/image.tsx