fix: 修复博客静态资源兜底与头部结构
@ -8,7 +8,7 @@ KT 博客前台 demo,基于 Argon WordPress 主题的视觉资产重新实现
|
||||
- 组件库:antdv-next。
|
||||
- 样式:SCSS,类名按 BEM 组织。
|
||||
- 路由:hash 模式,便于静态部署。
|
||||
- 静态资产:只从 Argon 主题包抽取 demo 需要的图片,不引入 WordPress PHP 与主题脚本。
|
||||
- 静态资产:默认使用旧线上博客真实资源,`public/blog-assets/` 保留本地备份兜底;不得把 Argon demo 图片作为默认背景、头像或 fallback 封面。
|
||||
|
||||
## 本地运行
|
||||
|
||||
@ -28,14 +28,15 @@ pnpm exec playwright test e2e/argon-parity/interactions.spec.ts --project=chromi
|
||||
pnpm exec playwright test e2e/argon-parity/baseline.spec.ts --project=chromium
|
||||
```
|
||||
|
||||
`baseline.spec.ts` 只在需要重新抓线上 WordPress Argon 基准时运行;常规本地回归跑 `pages.spec.ts` 和 `interactions.spec.ts`。线上 `https://blog.kwitsukasa.top/` 是只读视觉/交互基准,不是 Admin iframe 预览目标。
|
||||
`baseline.spec.ts` 只在需要重新抓旧 WordPress Argon 基准时运行;常规本地回归跑 `pages.spec.ts` 和 `interactions.spec.ts`。公开域名 `https://blog.kwitsukasa.top/` 当前是 KT Blog Web 静态站入口,不是只读基准站;旧 WordPress 端口只作为视觉/交互基准和回滚入口。
|
||||
|
||||
## Argon 还原范围
|
||||
|
||||
- `e2e/argon-parity` 保存与线上 `https://blog.kwitsukasa.top/` 对齐的页面、视口和交互矩阵。
|
||||
- `e2e/argon-parity` 保存与旧 WordPress Argon 基准对齐的页面、视口和交互矩阵;公开域名切到 KT Blog Web 后,基准抓取必须显式使用旧 WordPress 入口。
|
||||
- 本地 hash 路由按语义映射 WordPress query 路由:文章、分类、标签、搜索和月份归档都用同一套矩阵验证。
|
||||
- 页面根节点通过 `kt-blog--home/search/category/tag/archive/post` 暴露 Argon 页面语义,方便样式、测试和 Admin iframe 预览复用。
|
||||
- 公开 Blog API 返回非空文章列表时优先使用 API;API 不可用或返回空列表时保留内置 WordPress 抓取文章种子,避免数据未迁移期间线上静态站变成空站。
|
||||
- 主题接口若仍返回 `/argon/theme/*` 历史 demo 占位图,前端必须映射回旧线上博客资源;本地备份只用于兜底,避免静态站重新露出模板图。
|
||||
- 文章正文以线上 WordPress `#post_content` 渲染结果和 Argon `style.css` / `argontheme.js` 为准;代码块控制条、复制 toast、Fancybox 图片预览、正文链接 hover、分隔线和图片 lazyload 都必须有 Playwright 断言。
|
||||
- `hljs-codeblock` 静态快照需要恢复 Argon `highlightjs-line-numbers` 运行时生成的 `data-line-number` 与 `.hljs-ln-n::before`,否则行号列会坍塌成 0px。
|
||||
- 左栏 overview sticky/relative 切换用 no-headroom 回归用例固定,解除 fixed 后不得重放卡片入场动画或产生缩放闪烁。
|
||||
|
||||
|
Before Width: | Height: | Size: 218 KiB |
|
Before Width: | Height: | Size: 215 KiB |
|
Before Width: | Height: | Size: 134 KiB |
|
Before Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 44 KiB |
BIN
public/blog-assets/avatar-tsukasa-1.jpg
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
public/blog-assets/bg-donggungun.png
Normal file
|
After Width: | Height: | Size: 3.0 MiB |
BIN
public/blog-assets/icons/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
public/blog-assets/icons/mstile-270x270.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
public/blog-assets/icons/site-icon-192x192.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
public/blog-assets/icons/site-icon-32x32.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
@ -126,6 +126,20 @@ describe('App', () => {
|
||||
expect(wrapper.text()).toContain('KwiTsukasa的小站');
|
||||
});
|
||||
|
||||
it('renders Argon header without the old collapse container', async () => {
|
||||
await router.push('/');
|
||||
await router.isReady();
|
||||
|
||||
const wrapper = mount(App, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.find('.kt-blog__header-collapse').exists()).toBe(false);
|
||||
expect(wrapper.find('.kt-blog__header-nav').exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('loads local blog theme config by default', async () => {
|
||||
const fetchMock = vi.fn((url: string | URL | Request) => {
|
||||
const target = `${url}`;
|
||||
@ -225,10 +239,10 @@ describe('App', () => {
|
||||
expect(styleText).toContain('--themecolor: #C3A1ED;');
|
||||
expect(styleText).toContain('--radius: 6px;');
|
||||
expect(styleText).toContain(
|
||||
"--argon-background-dark-image: url('https://s3.kwitsukasa.top/images/bg-冬滚滚.png');",
|
||||
"--argon-background-dark-image: url('https://s3.kwitsukasa.top/images/bg-冬滚滚.png'), url('/blog-assets/bg-donggungun.png');",
|
||||
);
|
||||
expect(styleText).toContain(
|
||||
"--argon-author-avatar: url('http://s3.kwitsukasa.top/images/avatar-tsukasa-1.jpg');",
|
||||
"--argon-author-avatar: url('http://s3.kwitsukasa.top/images/avatar-tsukasa-1.jpg'), url('/blog-assets/avatar-tsukasa-1.jpg');",
|
||||
);
|
||||
});
|
||||
|
||||
@ -359,10 +373,10 @@ describe('App', () => {
|
||||
expect(styleText).toContain('--radius: 10px;');
|
||||
expect(styleText).toContain('--argon-background-opacity: 0.8;');
|
||||
expect(styleText).toContain(
|
||||
"--argon-background-dark-image: url('https://s3.kwitsukasa.top/images/wp-dark.png');",
|
||||
"--argon-background-dark-image: url('https://s3.kwitsukasa.top/images/wp-dark.png'), url('/blog-assets/bg-donggungun.png');",
|
||||
);
|
||||
expect(styleText).toContain(
|
||||
"--argon-author-avatar: url('http://s3.kwitsukasa.top/images/wp-avatar.jpg');",
|
||||
"--argon-author-avatar: url('http://s3.kwitsukasa.top/images/wp-avatar.jpg'), url('/blog-assets/avatar-tsukasa-1.jpg');",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@ -145,6 +145,40 @@ describe('useBlogArticles', () => {
|
||||
expect(blogArticles.articles.value).toEqual(fallbackArticles);
|
||||
});
|
||||
|
||||
it('keeps fallback article covers on previous blog assets instead of Argon demo images', async () => {
|
||||
expect(fallbackArticles.map((article) => article.cover)).not.toContain('/argon/theme/landing.jpg');
|
||||
expect(fallbackArticles.map((article) => article.cover)).not.toContain('/argon/theme/img-2-1200x1000.jpg');
|
||||
expect(fallbackArticles.map((article) => article.cover)).not.toContain('/argon/theme/img-1-1200x1000.jpg');
|
||||
expect(fallbackArticles.map((article) => article.cover)).not.toContain('/argon/theme/promo-1.png');
|
||||
expect(fallbackArticles.every((article) => article.cover.startsWith('https://s3.kwitsukasa.top/images/'))).toBe(true);
|
||||
});
|
||||
|
||||
it('uses the previous blog cover when public API articles have no cover', async () => {
|
||||
mockFetch([
|
||||
{
|
||||
body: {
|
||||
code: 200,
|
||||
data: {
|
||||
list: [
|
||||
{
|
||||
...publicArticle,
|
||||
cover: '',
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
},
|
||||
},
|
||||
status: 200,
|
||||
},
|
||||
]);
|
||||
const { useBlogArticles } = await import('@/hooks/useBlogArticles');
|
||||
const blogArticles = useBlogArticles();
|
||||
|
||||
await blogArticles.loadArticles();
|
||||
|
||||
expect(blogArticles.articles.value[0]?.cover).toBe('https://s3.kwitsukasa.top/images/bg-冬滚滚.png');
|
||||
});
|
||||
|
||||
it('fetches public article detail when cached list data does not include html content', async () => {
|
||||
const fetchMock = mockFetch([
|
||||
{
|
||||
|
||||
@ -140,13 +140,34 @@ describe('useBlogTheme', () => {
|
||||
expect(themeRootClass.value).toContain('kt-blog--shadow-big');
|
||||
expect(document.querySelector('#kt-blog-theme-style')?.textContent).toContain('--themecolor: #C3A1ED;');
|
||||
expect(document.querySelector('#kt-blog-theme-style')?.textContent).toContain("url('https://s3.kwitsukasa.top/images/bg-冬滚滚.png')");
|
||||
expect(document.querySelector('#kt-blog-theme-style')?.textContent).toContain('--argon-background-dark-image: url(\'https://s3.kwitsukasa.top/images/bg-冬滚滚.png\');');
|
||||
expect(document.querySelector('#kt-blog-theme-style')?.textContent).toContain('--argon-background-dark-image: url(\'https://s3.kwitsukasa.top/images/bg-冬滚滚.png\'), url(\'/blog-assets/bg-donggungun.png\');');
|
||||
expect(document.querySelector('#kt-blog-theme-style')?.textContent).toContain('--argon-background-filter: brightness(0.65);');
|
||||
expect(document.querySelector('#kt-blog-theme-style')?.textContent).toContain('--argon-background-opacity: 1;');
|
||||
expect(document.querySelector('#kt-blog-theme-style')?.textContent).toContain('--argon-background-dark-opacity: 1;');
|
||||
expect(document.querySelector('#kt-blog-theme-style')?.textContent).toContain("url('http://s3.kwitsukasa.top/images/avatar-tsukasa-1.jpg')");
|
||||
});
|
||||
|
||||
it('maps legacy Argon placeholder image config back to previous blog static assets', async () => {
|
||||
const { applyWordpressThemeConfig, siteConfig } = useBlogTheme();
|
||||
|
||||
applyWordpressThemeConfig({
|
||||
backgroundDarkImage: '/argon/theme/img-2-1200x1000.jpg',
|
||||
backgroundImage: '/argon/theme/img-2-1200x1000.jpg',
|
||||
site: {
|
||||
authorAvatar: '/argon/theme/profile.jpg',
|
||||
},
|
||||
});
|
||||
await nextTick();
|
||||
|
||||
const styleText = document.querySelector('#kt-blog-theme-style')?.textContent || '';
|
||||
expect(styleText).toContain("url('https://s3.kwitsukasa.top/images/bg-冬滚滚.png')");
|
||||
expect(styleText).toContain("url('/blog-assets/bg-donggungun.png')");
|
||||
expect(styleText).toContain("url('https://s3.kwitsukasa.top/images/avatar-tsukasa-1.jpg')");
|
||||
expect(styleText).toContain("url('/blog-assets/avatar-tsukasa-1.jpg')");
|
||||
expect(styleText).not.toContain('/argon/theme/');
|
||||
expect(siteConfig.value.authorAvatar).toBe('https://s3.kwitsukasa.top/images/avatar-tsukasa-1.jpg');
|
||||
});
|
||||
|
||||
it('mirrors remote layout class switches instead of keeping stale defaults', async () => {
|
||||
const { applyWordpressThemeConfig, themeRootClass, wordpressThemeConfig } = useBlogTheme();
|
||||
|
||||
|
||||
@ -88,9 +88,36 @@ export default defineComponent({
|
||||
</RouterLink>
|
||||
</div>
|
||||
|
||||
<div class="kt-blog__header-collapse">
|
||||
<div class="kt-blog__header-collapse-head">
|
||||
<div class="kt-blog__header-mobile-search">
|
||||
<ul class="kt-blog__header-nav kt-blog__header-nav--hover">
|
||||
{siteConfig.value.headerMenu.map((item) => (
|
||||
<li key={item.label} class="kt-blog__header-nav-item">
|
||||
{item.external ? (
|
||||
<a
|
||||
class="kt-blog__header-nav-link"
|
||||
href={item.href}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
{item.label}
|
||||
</a>
|
||||
) : (
|
||||
<RouterLink class="kt-blog__header-nav-link" to={item.href}>
|
||||
{item.label}
|
||||
</RouterLink>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<ul class="kt-blog__header-nav kt-blog__header-nav--end">
|
||||
<li class="kt-blog__header-search-item">
|
||||
<div
|
||||
class={['kt-blog__header-search', navSearchOpen.value && 'kt-blog__header-search--open']}
|
||||
onClick={() => {
|
||||
navSearchOpen.value = true;
|
||||
nextTick(() => focusInput(navSearchInputRef.value));
|
||||
}}
|
||||
>
|
||||
<div class="kt-blog__input-group">
|
||||
<div class="kt-blog__input-addon-wrap">
|
||||
<span class="kt-blog__input-addon">
|
||||
@ -98,72 +125,26 @@ export default defineComponent({
|
||||
</span>
|
||||
</div>
|
||||
<BlogInput
|
||||
class="kt-blog__header-mobile-search-input kt-blog__input"
|
||||
ref={navSearchInputRef}
|
||||
class="kt-blog__header-search-input kt-blog__input"
|
||||
placeholder="搜索什么..."
|
||||
autocomplete="off"
|
||||
v-model:value={keyword.value}
|
||||
onClick={(event: MouseEvent) => event.stopPropagation()}
|
||||
onBlur={() => {
|
||||
navSearchOpen.value = false;
|
||||
}}
|
||||
onKeydown={(event: KeyboardEvent) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
submitSearch();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="kt-blog__header-nav kt-blog__header-nav--hover">
|
||||
{siteConfig.value.headerMenu.map((item) => (
|
||||
<li key={item.label} class="kt-blog__header-nav-item">
|
||||
{item.external ? (
|
||||
<a
|
||||
class="kt-blog__header-nav-link"
|
||||
href={item.href}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
{item.label}
|
||||
</a>
|
||||
) : (
|
||||
<RouterLink class="kt-blog__header-nav-link" to={item.href}>
|
||||
{item.label}
|
||||
</RouterLink>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<ul class="kt-blog__header-nav kt-blog__header-nav--end">
|
||||
<li class="kt-blog__header-search-item">
|
||||
<div
|
||||
class={['kt-blog__header-search', navSearchOpen.value && 'kt-blog__header-search--open']}
|
||||
onClick={() => {
|
||||
navSearchOpen.value = true;
|
||||
nextTick(() => focusInput(navSearchInputRef.value));
|
||||
}}
|
||||
>
|
||||
<div class="kt-blog__input-group">
|
||||
<div class="kt-blog__input-addon-wrap">
|
||||
<span class="kt-blog__input-addon">
|
||||
<SearchOutlined />
|
||||
</span>
|
||||
</div>
|
||||
<BlogInput
|
||||
ref={navSearchInputRef}
|
||||
class="kt-blog__header-search-input kt-blog__input"
|
||||
placeholder="搜索什么..."
|
||||
autocomplete="off"
|
||||
v-model:value={keyword.value}
|
||||
onClick={(event: MouseEvent) => event.stopPropagation()}
|
||||
onBlur={() => {
|
||||
navSearchOpen.value = false;
|
||||
}}
|
||||
onKeydown={(event: KeyboardEvent) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
submitSearch();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="kt-blog__header-menu-mask" />
|
||||
<BlogButton
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { articleContentHtml } from './blogArticleContent';
|
||||
import { PREVIOUS_BLOG_BACKGROUND_IMAGE } from './blogStaticAssets';
|
||||
|
||||
export interface BlogArticle {
|
||||
id: number;
|
||||
@ -252,7 +253,7 @@ export const articles: BlogArticle[] = [
|
||||
category: 'public',
|
||||
categorySlug: 'public',
|
||||
tags: [],
|
||||
cover: '/argon/theme/landing.jpg',
|
||||
cover: PREVIOUS_BLOG_BACKGROUND_IMAGE,
|
||||
date: '2026-06-18 11:11',
|
||||
readTime: '18 分钟',
|
||||
author: 'KwiTsukasa',
|
||||
@ -274,7 +275,7 @@ export const articles: BlogArticle[] = [
|
||||
category: 'NAS',
|
||||
categorySlug: 'nas',
|
||||
tags: ['NAS'],
|
||||
cover: '/argon/theme/img-2-1200x1000.jpg',
|
||||
cover: PREVIOUS_BLOG_BACKGROUND_IMAGE,
|
||||
date: '2026-05-16 16:43',
|
||||
readTime: '14 分钟',
|
||||
author: 'KwiTsukasa',
|
||||
@ -298,7 +299,7 @@ export const articles: BlogArticle[] = [
|
||||
category: 'NAS',
|
||||
categorySlug: 'nas',
|
||||
tags: ['NAS'],
|
||||
cover: '/argon/theme/img-1-1200x1000.jpg',
|
||||
cover: PREVIOUS_BLOG_BACKGROUND_IMAGE,
|
||||
date: '2026-05-12 12:00',
|
||||
readTime: '16 分钟',
|
||||
author: 'KwiTsukasa',
|
||||
@ -321,7 +322,7 @@ export const articles: BlogArticle[] = [
|
||||
category: 'NAS',
|
||||
categorySlug: 'nas',
|
||||
tags: ['NAS'],
|
||||
cover: '/argon/theme/landing.jpg',
|
||||
cover: PREVIOUS_BLOG_BACKGROUND_IMAGE,
|
||||
date: '2026-05-12 11:58',
|
||||
readTime: '14 分钟',
|
||||
author: 'KwiTsukasa',
|
||||
@ -344,7 +345,7 @@ export const articles: BlogArticle[] = [
|
||||
category: 'Vue',
|
||||
categorySlug: 'vue',
|
||||
tags: ['Vue'],
|
||||
cover: '/argon/theme/promo-1.png',
|
||||
cover: PREVIOUS_BLOG_BACKGROUND_IMAGE,
|
||||
date: '2025-10-31 16:16',
|
||||
readTime: '11 分钟',
|
||||
author: 'KwiTsukasa',
|
||||
@ -369,7 +370,7 @@ export const articles: BlogArticle[] = [
|
||||
{ slug: 'vue', label: 'Vue' },
|
||||
],
|
||||
tags: ['MQTT', 'Vue'],
|
||||
cover: '/argon/theme/img-2-1200x1000.jpg',
|
||||
cover: PREVIOUS_BLOG_BACKGROUND_IMAGE,
|
||||
date: '2025-10-29 16:50',
|
||||
readTime: '17 分钟',
|
||||
author: 'KwiTsukasa',
|
||||
@ -392,7 +393,7 @@ export const articles: BlogArticle[] = [
|
||||
category: 'Node',
|
||||
categorySlug: 'node',
|
||||
tags: ['Node'],
|
||||
cover: '/argon/theme/img-1-1200x1000.jpg',
|
||||
cover: PREVIOUS_BLOG_BACKGROUND_IMAGE,
|
||||
date: '2025-10-29 15:38',
|
||||
readTime: '13 分钟',
|
||||
author: 'KwiTsukasa',
|
||||
|
||||
59
src/data/blogStaticAssets.ts
Normal file
@ -0,0 +1,59 @@
|
||||
export const PREVIOUS_BLOG_BACKGROUND_IMAGE = 'https://s3.kwitsukasa.top/images/bg-冬滚滚.png';
|
||||
export const PREVIOUS_BLOG_AUTHOR_AVATAR = 'https://s3.kwitsukasa.top/images/avatar-tsukasa-1.jpg';
|
||||
export const LOCAL_BLOG_BACKGROUND_IMAGE = '/blog-assets/bg-donggungun.png';
|
||||
export const LOCAL_BLOG_AUTHOR_AVATAR = '/blog-assets/avatar-tsukasa-1.jpg';
|
||||
|
||||
const LEGACY_ARGON_ASSET_REPLACEMENTS: Record<string, string> = {
|
||||
'/argon/theme/img-1-1200x1000.jpg': PREVIOUS_BLOG_BACKGROUND_IMAGE,
|
||||
'/argon/theme/img-2-1200x1000.jpg': PREVIOUS_BLOG_BACKGROUND_IMAGE,
|
||||
'/argon/theme/landing.jpg': PREVIOUS_BLOG_BACKGROUND_IMAGE,
|
||||
'/argon/theme/profile.jpg': PREVIOUS_BLOG_AUTHOR_AVATAR,
|
||||
'/argon/theme/promo-1.png': PREVIOUS_BLOG_BACKGROUND_IMAGE,
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolves theme/article image values while keeping the old real online resources as the primary source.
|
||||
*
|
||||
* @param value Image URL from API data, static captured article data, or a CSS `url(...)` value.
|
||||
* @param fallback Primary fallback to use when the value is empty; callers pass the matching online old asset.
|
||||
* @returns A usable online/local image URL with legacy Argon demo placeholders mapped back to previous blog assets.
|
||||
*/
|
||||
export function resolveBlogStaticAsset(value?: null | string, fallback = PREVIOUS_BLOG_BACKGROUND_IMAGE) {
|
||||
const asset = unwrapBlogCssImage(value);
|
||||
if (!asset) return fallback;
|
||||
|
||||
const replacementKey = getAssetPath(asset);
|
||||
|
||||
return LEGACY_ARGON_ASSET_REPLACEMENTS[replacementKey] || asset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the raw asset URL from plain URLs and CSS `url(...)` tokens.
|
||||
*
|
||||
* @param value Candidate image token from the theme API or generated CSS.
|
||||
* @returns Trimmed URL/path without surrounding `url(...)` syntax, or an empty string for unusable values.
|
||||
*/
|
||||
export function unwrapBlogCssImage(value?: null | string) {
|
||||
const normalized = `${value || ''}`.trim();
|
||||
if (!normalized) return '';
|
||||
|
||||
const cssImage = /^url\((.*)\)$/i.exec(normalized)?.[1]?.trim();
|
||||
|
||||
return (cssImage ? cssImage.replace(/^['"]|['"]$/g, '') : normalized).trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes absolute same-path assets to their pathname so legacy placeholder matching is stable.
|
||||
*
|
||||
* @param asset URL or root-relative path to compare against known Argon demo placeholder files.
|
||||
* @returns Pathname for absolute URLs and the original value for root-relative paths.
|
||||
*/
|
||||
function getAssetPath(asset: string) {
|
||||
if (asset.startsWith('/')) return asset;
|
||||
|
||||
try {
|
||||
return new URL(asset).pathname;
|
||||
} catch {
|
||||
return asset;
|
||||
}
|
||||
}
|
||||
@ -16,14 +16,9 @@ import {
|
||||
type BlogCategory,
|
||||
type BlogTag,
|
||||
} from '@/data/blog';
|
||||
import { PREVIOUS_BLOG_BACKGROUND_IMAGE, resolveBlogStaticAsset } from '@/data/blogStaticAssets';
|
||||
|
||||
const coverPool = [
|
||||
'/argon/theme/img-2-1200x1000.jpg',
|
||||
'/argon/theme/img-1-1200x1000.jpg',
|
||||
'/argon/theme/landing.jpg',
|
||||
'/argon/theme/promo-1.png',
|
||||
];
|
||||
const defaultCover = coverPool[0] || '/argon/theme/img-2-1200x1000.jpg';
|
||||
const defaultCover = PREVIOUS_BLOG_BACKGROUND_IMAGE;
|
||||
const colorPool = ['blue', 'purple', 'green', 'orange', 'geekblue', 'cyan', 'volcano', 'magenta'];
|
||||
const blogArticles = ref<BlogArticle[]>(fallbackArticles);
|
||||
const loading = ref(false);
|
||||
@ -222,7 +217,7 @@ function normalizeWordpressArticle(article: WordpressPublicArticle): BlogArticle
|
||||
comments: 0,
|
||||
content: contentText ? [contentText] : [],
|
||||
contentHtml,
|
||||
cover: article.cover || coverPool[article.id % coverPool.length] || defaultCover,
|
||||
cover: resolveBlogStaticAsset(article.cover, defaultCover),
|
||||
date: formatDate(article.date || article.modified),
|
||||
excerpt,
|
||||
id: article.id,
|
||||
|
||||
@ -1,6 +1,14 @@
|
||||
import { theme } from 'antdv-next';
|
||||
import { computed, reactive, watch } from 'vue';
|
||||
|
||||
import {
|
||||
PREVIOUS_BLOG_AUTHOR_AVATAR,
|
||||
PREVIOUS_BLOG_BACKGROUND_IMAGE,
|
||||
LOCAL_BLOG_AUTHOR_AVATAR,
|
||||
LOCAL_BLOG_BACKGROUND_IMAGE,
|
||||
resolveBlogStaticAsset,
|
||||
unwrapBlogCssImage,
|
||||
} from '@/data/blogStaticAssets';
|
||||
import { createBlogMotionCssVariables } from '@/factories/blogAnimationFactory';
|
||||
import { BLOG_META_NAMES, blogDomId, blogDomSelector, blogMetaSelector } from '@/factories/blogDomFactory';
|
||||
|
||||
@ -107,8 +115,8 @@ const ARGON_SERIF_FONT_FAMILY = 'Georgia, "Times New Roman", "Noto Serif SC", se
|
||||
const ARGON_DEFAULT_COLOR_PRIMARY = '#c3a1ed';
|
||||
const ARGON_PRIMARY_SOFT = '#4a4058';
|
||||
const ARGON_CARD_SHADOW = '0 2px 4px rgba(0, 0, 0, 0.075)';
|
||||
const ARGON_DEFAULT_BACKGROUND = 'https://s3.kwitsukasa.top/images/bg-冬滚滚.png';
|
||||
const ARGON_DEFAULT_AUTHOR_AVATAR = 'https://s3.kwitsukasa.top/images/avatar-tsukasa-1.jpg';
|
||||
const ARGON_DEFAULT_BACKGROUND = PREVIOUS_BLOG_BACKGROUND_IMAGE;
|
||||
const ARGON_DEFAULT_AUTHOR_AVATAR = PREVIOUS_BLOG_AUTHOR_AVATAR;
|
||||
const defaultHeaderMenu: BlogThemeMenuItem[] = [];
|
||||
const defaultSidebarMenu: BlogThemeMenuItem[] = [
|
||||
{ href: '/', icon: 'fa-home', label: '首页' },
|
||||
@ -304,7 +312,8 @@ function applyWordpressThemeConfig(config: WordpressArgonThemeConfig) {
|
||||
typeof nextBackgroundDarkBrightness === 'number'
|
||||
? nextBackgroundDarkBrightness
|
||||
: runtimeConfig.backgroundDarkBrightness;
|
||||
runtimeConfig.backgroundDarkImage = normalizeCssImage(config.backgroundDarkImage) || runtimeConfig.backgroundDarkImage;
|
||||
runtimeConfig.backgroundDarkImage =
|
||||
normalizeCssImage(config.backgroundDarkImage) || runtimeConfig.backgroundDarkImage;
|
||||
runtimeConfig.backgroundDarkOpacity =
|
||||
typeof nextBackgroundDarkOpacity === 'number'
|
||||
? nextBackgroundDarkOpacity
|
||||
@ -432,12 +441,14 @@ function applyCssVariables(currentPreferences: BlogThemePreferences) {
|
||||
const palette = createThemePalette(currentPreferences.colorPrimary, currentPreferences.mode);
|
||||
const fontFamily = getThemeFontFamily(currentPreferences.font);
|
||||
const argonColorVariables = createArgonColorVariables(primaryRgb, currentPreferences.mode);
|
||||
const backgroundImage =
|
||||
const backgroundImage = appendCssImageFallback(
|
||||
normalizeCssImage(
|
||||
currentPreferences.mode === 'dark'
|
||||
? runtimeConfig.backgroundDarkImage || runtimeConfig.backgroundImage
|
||||
: runtimeConfig.backgroundImage,
|
||||
) || `url('${ARGON_DEFAULT_BACKGROUND}')`;
|
||||
) || `url('${ARGON_DEFAULT_BACKGROUND}')`,
|
||||
LOCAL_BLOG_BACKGROUND_IMAGE,
|
||||
);
|
||||
const backgroundOpacity =
|
||||
currentPreferences.mode === 'dark'
|
||||
? runtimeConfig.backgroundDarkOpacity
|
||||
@ -505,13 +516,13 @@ ${serializeCssVariables(argonColorVariables.current)}
|
||||
--radius: ${currentPreferences.radius}px;
|
||||
--card-radius: ${currentPreferences.radius}px;
|
||||
--argon-background-image: ${backgroundImage};
|
||||
--argon-background-light-image: ${normalizeCssImage(runtimeConfig.backgroundImage) || `url('${ARGON_DEFAULT_BACKGROUND}')`};
|
||||
--argon-background-dark-image: ${normalizeCssImage(runtimeConfig.backgroundDarkImage) || backgroundImage};
|
||||
--argon-background-light-image: ${appendCssImageFallback(normalizeCssImage(runtimeConfig.backgroundImage) || `url('${ARGON_DEFAULT_BACKGROUND}')`, LOCAL_BLOG_BACKGROUND_IMAGE)};
|
||||
--argon-background-dark-image: ${appendCssImageFallback(normalizeCssImage(runtimeConfig.backgroundDarkImage) || backgroundImage, LOCAL_BLOG_BACKGROUND_IMAGE)};
|
||||
--argon-background-filter: ${currentPreferences.mode === 'dark' ? `brightness(${runtimeConfig.backgroundDarkBrightness})` : 'none'};
|
||||
--argon-background-opacity: ${backgroundOpacity};
|
||||
--argon-background-light-opacity: ${runtimeConfig.backgroundOpacity};
|
||||
--argon-background-dark-opacity: ${runtimeConfig.backgroundDarkOpacity};
|
||||
--argon-author-avatar: ${normalizeCssImage(runtimeConfig.siteAuthorAvatar) || `url('${ARGON_DEFAULT_AUTHOR_AVATAR}')`};
|
||||
--argon-author-avatar: ${appendCssImageFallback(normalizeCssImage(runtimeConfig.siteAuthorAvatar) || `url('${ARGON_DEFAULT_AUTHOR_AVATAR}')`, LOCAL_BLOG_AUTHOR_AVATAR)};
|
||||
--argon-font-family: ${fontFamily};
|
||||
--argon-shadow: ${ARGON_CARD_SHADOW};
|
||||
--argon-primary-soft: ${ARGON_PRIMARY_SOFT};
|
||||
@ -904,9 +915,12 @@ function normalizePositiveNumber(value?: number | string) {
|
||||
return Number.isFinite(numericValue) ? Math.max(0, numericValue) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param value Theme image URL returned by the API or static defaults.
|
||||
* @returns CSS `url(...)` token after replacing legacy Argon demo placeholders with previous blog assets.
|
||||
*/
|
||||
function normalizeCssImage(value?: string) {
|
||||
if (!value) return '';
|
||||
const normalized = value.trim();
|
||||
const normalized = resolveBlogStaticAsset(value, '');
|
||||
|
||||
if (normalized.startsWith('url(')) return normalized;
|
||||
if (/^https?:\/\//i.test(normalized) || normalized.startsWith('/')) {
|
||||
@ -916,13 +930,27 @@ function normalizeCssImage(value?: string) {
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param primaryCssImage CSS `url(...)` token for the preferred remote image.
|
||||
* @param localFallback Local packaged image path used when the remote image cannot load.
|
||||
* @returns A CSS background-image layer list with the remote image first and local backup second.
|
||||
*/
|
||||
function appendCssImageFallback(primaryCssImage: string, localFallback: string) {
|
||||
const fallbackCssImage = normalizeCssImage(localFallback);
|
||||
|
||||
if (!fallbackCssImage || primaryCssImage.includes(fallbackCssImage)) {
|
||||
return primaryCssImage;
|
||||
}
|
||||
|
||||
return `${primaryCssImage}, ${fallbackCssImage}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param value Theme asset URL returned by WordPress-compatible config.
|
||||
* @returns Plain URL/path after replacing legacy Argon demo placeholders, suitable for component state.
|
||||
*/
|
||||
function normalizeThemeAsset(value?: string) {
|
||||
if (!value) return '';
|
||||
const normalized = value.trim();
|
||||
const cssImage = /^url\((.*)\)$/i.exec(normalized)?.[1]?.trim();
|
||||
const asset = cssImage
|
||||
? cssImage.replace(/^['"]|['"]$/g, '')
|
||||
: normalized;
|
||||
const asset = resolveBlogStaticAsset(unwrapBlogCssImage(value), '');
|
||||
|
||||
return /^https?:\/\//i.test(asset) || asset.startsWith('/') ? asset : '';
|
||||
}
|
||||
|
||||
@ -240,8 +240,11 @@
|
||||
height: 100px;
|
||||
margin: 0 auto;
|
||||
border-radius: 50%;
|
||||
background: var(--argon-author-avatar, url('/argon/theme/profile.jpg')) center / cover no-repeat;
|
||||
background-color: rgba(127, 127, 127, 0.1);
|
||||
background-image: var(--argon-author-avatar, url('/blog-assets/avatar-tsukasa-1.jpg'));
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
box-shadow: var(--argon-shadow);
|
||||
}
|
||||
|
||||
|
||||
@ -46,11 +46,10 @@
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
&__header-collapse {
|
||||
&__header-nav--hover {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&__header-nav {
|
||||
|
||||