feat: 完善主题功能并适配插件 (v1.1.5)

功能新增
- 新增页面布局契约 templates/layout.html,插件前台页面可复用主题外观,避免回退到 Halo fallback 布局
- 新增微信读书页面模板 (weread.html),集成 halo-weread-plugin 书架
- 新增订阅页面模板 (page_subscriptions.html)
- 新增欢迎弹窗组件(IP 地理定位,多 API 容错)
- 新增点击特效组件(9 种效果)
- 页脚新增全站统计,支持开关与统计项配置,默认开启

插件适配
- 适配 extra-api:文章字数改服务端统计、正文可选中英混排与服务端代码高亮
- 修正各页面插件应用市场链接与插件名

修复
- 修复文章正文图片/视频无法居中(正文样式移至全局,scoped 样式对 Thymeleaf 注入内容无效)
- 修复正文 H1-H6 字号过大,重排层级与移动端断点
- 修复 th: 属性中文字符串缺少引号导致的 Thymeleaf 解析异常(/links、/moments 500)
- 修复 JS 内联表达式与 Thymeleaf 内联语法冲突,全部内联脚本加 th:inline="none"
- 修复移除 i18n 后 steam 页 Elvis 表达式回归问题
- 修正订阅页 th:attr 引号嵌套错误

优化
- Hero 移除灰色遮罩,下滑箭头改为可点击平滑滚动
- 优化目录样式:移除圆点与下拉箭头,支持两行显示,修复长目录截断
- 移除 i18n 多语言支持

清理
- 移除未使用的 Vue/图标依赖与废弃脚本、重复资源
This commit is contained in:
2026-09-26 11:18:52 +08:00
parent 9d0bfcfb4f
commit 430d107d44
78 changed files with 2959 additions and 36075 deletions
+11 -2
View File
@@ -1,18 +1,24 @@
# build output
dist/
templates/
# generated types
.astro/
# dependencies
node_modules/
# docs site
docs/.vitepress/cache/
docs/.vitepress/dist/
docs/node_modules/
# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# environment variables
.env
.env.production
@@ -23,4 +29,7 @@ pnpm-debug.log*
# jetbrains setting folder
.idea/
templates/
# AI working artifacts
llms.txt
.workbuddy/
-91
View File
@@ -1,91 +0,0 @@
const http = require('http');
function apiCall(method, path, data) {
return new Promise((resolve, reject) => {
const options = {
hostname: 'localhost',
port: 8090,
path: path,
method: method,
headers: {
'Content-Type': 'application/json',
}
};
const req = http.request(options, (res) => {
let body = '';
res.on('data', chunk => body += chunk);
res.on('end', () => {
try {
const cookies = res.headers['set-cookie'];
resolve({ status: res.statusCode, data: JSON.parse(body), cookies });
} catch(e) {
resolve({ status: res.statusCode, data: body, cookies: res.headers['set-cookie'] });
}
});
});
req.on('error', reject);
if (data) req.write(JSON.stringify(data));
req.end();
});
}
async function main() {
// Login first
const loginRes = await apiCall('POST', '/api/auth/signin', { username: 'admin', password: 'admin' });
console.log('Login status:', loginRes.status);
// Get the session cookie
const cookies = loginRes.cookies || [];
const cookieStr = cookies.map(c => c.split(';')[0]).join('; ');
console.log('Cookies:', cookieStr);
// Now get theme settings using the cookie
const http2 = require('http');
function apiWithCookie(method, path, data) {
return new Promise((resolve, reject) => {
const options = {
hostname: 'localhost',
port: 8090,
path: path,
method: method,
headers: {
'Content-Type': 'application/json',
'Cookie': cookieStr
}
};
const req = http2.request(options, (res) => {
let body = '';
res.on('data', chunk => body += chunk);
res.on('end', () => {
try {
resolve({ status: res.statusCode, data: JSON.parse(body) });
} catch(e) {
resolve({ status: res.statusCode, data: body });
}
});
});
req.on('error', reject);
if (data) req.write(JSON.stringify(data));
req.end();
});
}
// Get activated theme
const themeRes = await apiWithCookie('GET', '/apis/api.console.halo.run/v1alpha1/themes?sort=creationTimestamp%2Cdesc');
console.log('Themes status:', themeRes.status);
if (themeRes.data && themeRes.data.items) {
const activeTheme = themeRes.data.items.find(t => t.spec && t.spec.activationTime);
if (activeTheme) {
console.log('Active theme:', activeTheme.metadata.name);
// Get theme settings
const settingsRes = await apiWithCookie('GET', `/apis/api.console.halo.run/v1alpha1/themes/${activeTheme.metadata.name}/setting`);
console.log('Settings status:', settingsRes.status);
console.log('Settings:', JSON.stringify(settingsRes.data).substring(0, 500));
}
}
}
main().catch(console.error);
-52
View File
@@ -1,52 +0,0 @@
{
"hash": "0ed7ed0d",
"configHash": "c707d7b6",
"lockfileHash": "4f412479",
"browserHash": "3aefc297",
"optimized": {
"vue": {
"src": "../../../node_modules/vue/dist/vue.runtime.esm-bundler.js",
"file": "vue.js",
"fileHash": "9027419d",
"needsInterop": false
},
"vitepress > @vue/devtools-api": {
"src": "../../../node_modules/@vue/devtools-api/dist/index.js",
"file": "vitepress___@vue_devtools-api.js",
"fileHash": "3cd67bf3",
"needsInterop": false
},
"vitepress > @vueuse/core": {
"src": "../../../node_modules/@vueuse/core/index.mjs",
"file": "vitepress___@vueuse_core.js",
"fileHash": "cda81787",
"needsInterop": false
},
"vitepress > @vueuse/integrations/useFocusTrap": {
"src": "../../../node_modules/@vueuse/integrations/useFocusTrap.mjs",
"file": "vitepress___@vueuse_integrations_useFocusTrap.js",
"fileHash": "7b4ec920",
"needsInterop": false
},
"vitepress > mark.js/src/vanilla.js": {
"src": "../../../node_modules/mark.js/src/vanilla.js",
"file": "vitepress___mark__js_src_vanilla__js.js",
"fileHash": "3efbcd2b",
"needsInterop": false
},
"vitepress > minisearch": {
"src": "../../../node_modules/minisearch/dist/es/index.js",
"file": "vitepress___minisearch.js",
"fileHash": "8ca06377",
"needsInterop": false
}
},
"chunks": {
"chunk-4USRTV5H": {
"file": "chunk-4USRTV5H.js"
},
"chunk-XY26LBYQ": {
"file": "chunk-XY26LBYQ.js"
}
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
-3
View File
@@ -1,3 +0,0 @@
{
"type": "module"
}
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
-583
View File
@@ -1,583 +0,0 @@
import {
DefaultMagicKeysAliasMap,
StorageSerializers,
TransitionPresets,
assert,
breakpointsAntDesign,
breakpointsBootstrapV5,
breakpointsElement,
breakpointsMasterCss,
breakpointsPrimeFlex,
breakpointsQuasar,
breakpointsSematic,
breakpointsTailwind,
breakpointsVuetify,
breakpointsVuetifyV2,
breakpointsVuetifyV3,
bypassFilter,
camelize,
clamp,
cloneFnJSON,
computedAsync,
computedEager,
computedInject,
computedWithControl,
containsProp,
controlledRef,
createEventHook,
createFetch,
createFilterWrapper,
createGlobalState,
createInjectionState,
createRef,
createReusableTemplate,
createSharedComposable,
createSingletonPromise,
createTemplatePromise,
createUnrefFn,
customStorageEventName,
debounceFilter,
defaultDocument,
defaultLocation,
defaultNavigator,
defaultWindow,
executeTransition,
extendRef,
formatDate,
formatTimeAgo,
get,
getLifeCycleTarget,
getSSRHandler,
hasOwn,
hyphenate,
identity,
increaseWithUnit,
injectLocal,
invoke,
isClient,
isDef,
isDefined,
isIOS,
isObject,
isWorker,
makeDestructurable,
mapGamepadToXbox360Controller,
noop,
normalizeDate,
notNullish,
now,
objectEntries,
objectOmit,
objectPick,
onClickOutside,
onElementRemoval,
onKeyDown,
onKeyPressed,
onKeyStroke,
onKeyUp,
onLongPress,
onStartTyping,
pausableFilter,
promiseTimeout,
provideLocal,
provideSSRWidth,
pxValue,
rand,
reactify,
reactifyObject,
reactiveComputed,
reactiveOmit,
reactivePick,
refAutoReset,
refDebounced,
refDefault,
refThrottled,
refWithControl,
resolveRef,
resolveUnref,
set,
setSSRHandler,
syncRef,
syncRefs,
templateRef,
throttleFilter,
timestamp,
toArray,
toReactive,
toRef,
toRefs,
toValue,
tryOnBeforeMount,
tryOnBeforeUnmount,
tryOnMounted,
tryOnScopeDispose,
tryOnUnmounted,
unrefElement,
until,
useActiveElement,
useAnimate,
useArrayDifference,
useArrayEvery,
useArrayFilter,
useArrayFind,
useArrayFindIndex,
useArrayFindLast,
useArrayIncludes,
useArrayJoin,
useArrayMap,
useArrayReduce,
useArraySome,
useArrayUnique,
useAsyncQueue,
useAsyncState,
useBase64,
useBattery,
useBluetooth,
useBreakpoints,
useBroadcastChannel,
useBrowserLocation,
useCached,
useClipboard,
useClipboardItems,
useCloned,
useColorMode,
useConfirmDialog,
useCountdown,
useCounter,
useCssVar,
useCurrentElement,
useCycleList,
useDark,
useDateFormat,
useDebounceFn,
useDebouncedRefHistory,
useDeviceMotion,
useDeviceOrientation,
useDevicePixelRatio,
useDevicesList,
useDisplayMedia,
useDocumentVisibility,
useDraggable,
useDropZone,
useElementBounding,
useElementByPoint,
useElementHover,
useElementSize,
useElementVisibility,
useEventBus,
useEventListener,
useEventSource,
useEyeDropper,
useFavicon,
useFetch,
useFileDialog,
useFileSystemAccess,
useFocus,
useFocusWithin,
useFps,
useFullscreen,
useGamepad,
useGeolocation,
useIdle,
useImage,
useInfiniteScroll,
useIntersectionObserver,
useInterval,
useIntervalFn,
useKeyModifier,
useLastChanged,
useLocalStorage,
useMagicKeys,
useManualRefHistory,
useMediaControls,
useMediaQuery,
useMemoize,
useMemory,
useMounted,
useMouse,
useMouseInElement,
useMousePressed,
useMutationObserver,
useNavigatorLanguage,
useNetwork,
useNow,
useObjectUrl,
useOffsetPagination,
useOnline,
usePageLeave,
useParallax,
useParentElement,
usePerformanceObserver,
usePermission,
usePointer,
usePointerLock,
usePointerSwipe,
usePreferredColorScheme,
usePreferredContrast,
usePreferredDark,
usePreferredLanguages,
usePreferredReducedMotion,
usePreferredReducedTransparency,
usePrevious,
useRafFn,
useRefHistory,
useResizeObserver,
useSSRWidth,
useScreenOrientation,
useScreenSafeArea,
useScriptTag,
useScroll,
useScrollLock,
useSessionStorage,
useShare,
useSorted,
useSpeechRecognition,
useSpeechSynthesis,
useStepper,
useStorage,
useStorageAsync,
useStyleTag,
useSupported,
useSwipe,
useTemplateRefsList,
useTextDirection,
useTextSelection,
useTextareaAutosize,
useThrottleFn,
useThrottledRefHistory,
useTimeAgo,
useTimeout,
useTimeoutFn,
useTimeoutPoll,
useTimestamp,
useTitle,
useToNumber,
useToString,
useToggle,
useTransition,
useUrlSearchParams,
useUserMedia,
useVModel,
useVModels,
useVibrate,
useVirtualList,
useWakeLock,
useWebNotification,
useWebSocket,
useWebWorker,
useWebWorkerFn,
useWindowFocus,
useWindowScroll,
useWindowSize,
watchArray,
watchAtMost,
watchDebounced,
watchDeep,
watchIgnorable,
watchImmediate,
watchOnce,
watchPausable,
watchThrottled,
watchTriggerable,
watchWithFilter,
whenever
} from "./chunk-4USRTV5H.js";
import "./chunk-XY26LBYQ.js";
export {
DefaultMagicKeysAliasMap,
StorageSerializers,
TransitionPresets,
assert,
computedAsync as asyncComputed,
refAutoReset as autoResetRef,
breakpointsAntDesign,
breakpointsBootstrapV5,
breakpointsElement,
breakpointsMasterCss,
breakpointsPrimeFlex,
breakpointsQuasar,
breakpointsSematic,
breakpointsTailwind,
breakpointsVuetify,
breakpointsVuetifyV2,
breakpointsVuetifyV3,
bypassFilter,
camelize,
clamp,
cloneFnJSON,
computedAsync,
computedEager,
computedInject,
computedWithControl,
containsProp,
computedWithControl as controlledComputed,
controlledRef,
createEventHook,
createFetch,
createFilterWrapper,
createGlobalState,
createInjectionState,
reactify as createReactiveFn,
createRef,
createReusableTemplate,
createSharedComposable,
createSingletonPromise,
createTemplatePromise,
createUnrefFn,
customStorageEventName,
debounceFilter,
refDebounced as debouncedRef,
watchDebounced as debouncedWatch,
defaultDocument,
defaultLocation,
defaultNavigator,
defaultWindow,
computedEager as eagerComputed,
executeTransition,
extendRef,
formatDate,
formatTimeAgo,
get,
getLifeCycleTarget,
getSSRHandler,
hasOwn,
hyphenate,
identity,
watchIgnorable as ignorableWatch,
increaseWithUnit,
injectLocal,
invoke,
isClient,
isDef,
isDefined,
isIOS,
isObject,
isWorker,
makeDestructurable,
mapGamepadToXbox360Controller,
noop,
normalizeDate,
notNullish,
now,
objectEntries,
objectOmit,
objectPick,
onClickOutside,
onElementRemoval,
onKeyDown,
onKeyPressed,
onKeyStroke,
onKeyUp,
onLongPress,
onStartTyping,
pausableFilter,
watchPausable as pausableWatch,
promiseTimeout,
provideLocal,
provideSSRWidth,
pxValue,
rand,
reactify,
reactifyObject,
reactiveComputed,
reactiveOmit,
reactivePick,
refAutoReset,
refDebounced,
refDefault,
refThrottled,
refWithControl,
resolveRef,
resolveUnref,
set,
setSSRHandler,
syncRef,
syncRefs,
templateRef,
throttleFilter,
refThrottled as throttledRef,
watchThrottled as throttledWatch,
timestamp,
toArray,
toReactive,
toRef,
toRefs,
toValue,
tryOnBeforeMount,
tryOnBeforeUnmount,
tryOnMounted,
tryOnScopeDispose,
tryOnUnmounted,
unrefElement,
until,
useActiveElement,
useAnimate,
useArrayDifference,
useArrayEvery,
useArrayFilter,
useArrayFind,
useArrayFindIndex,
useArrayFindLast,
useArrayIncludes,
useArrayJoin,
useArrayMap,
useArrayReduce,
useArraySome,
useArrayUnique,
useAsyncQueue,
useAsyncState,
useBase64,
useBattery,
useBluetooth,
useBreakpoints,
useBroadcastChannel,
useBrowserLocation,
useCached,
useClipboard,
useClipboardItems,
useCloned,
useColorMode,
useConfirmDialog,
useCountdown,
useCounter,
useCssVar,
useCurrentElement,
useCycleList,
useDark,
useDateFormat,
refDebounced as useDebounce,
useDebounceFn,
useDebouncedRefHistory,
useDeviceMotion,
useDeviceOrientation,
useDevicePixelRatio,
useDevicesList,
useDisplayMedia,
useDocumentVisibility,
useDraggable,
useDropZone,
useElementBounding,
useElementByPoint,
useElementHover,
useElementSize,
useElementVisibility,
useEventBus,
useEventListener,
useEventSource,
useEyeDropper,
useFavicon,
useFetch,
useFileDialog,
useFileSystemAccess,
useFocus,
useFocusWithin,
useFps,
useFullscreen,
useGamepad,
useGeolocation,
useIdle,
useImage,
useInfiniteScroll,
useIntersectionObserver,
useInterval,
useIntervalFn,
useKeyModifier,
useLastChanged,
useLocalStorage,
useMagicKeys,
useManualRefHistory,
useMediaControls,
useMediaQuery,
useMemoize,
useMemory,
useMounted,
useMouse,
useMouseInElement,
useMousePressed,
useMutationObserver,
useNavigatorLanguage,
useNetwork,
useNow,
useObjectUrl,
useOffsetPagination,
useOnline,
usePageLeave,
useParallax,
useParentElement,
usePerformanceObserver,
usePermission,
usePointer,
usePointerLock,
usePointerSwipe,
usePreferredColorScheme,
usePreferredContrast,
usePreferredDark,
usePreferredLanguages,
usePreferredReducedMotion,
usePreferredReducedTransparency,
usePrevious,
useRafFn,
useRefHistory,
useResizeObserver,
useSSRWidth,
useScreenOrientation,
useScreenSafeArea,
useScriptTag,
useScroll,
useScrollLock,
useSessionStorage,
useShare,
useSorted,
useSpeechRecognition,
useSpeechSynthesis,
useStepper,
useStorage,
useStorageAsync,
useStyleTag,
useSupported,
useSwipe,
useTemplateRefsList,
useTextDirection,
useTextSelection,
useTextareaAutosize,
refThrottled as useThrottle,
useThrottleFn,
useThrottledRefHistory,
useTimeAgo,
useTimeout,
useTimeoutFn,
useTimeoutPoll,
useTimestamp,
useTitle,
useToNumber,
useToString,
useToggle,
useTransition,
useUrlSearchParams,
useUserMedia,
useVModel,
useVModels,
useVibrate,
useVirtualList,
useWakeLock,
useWebNotification,
useWebSocket,
useWebWorker,
useWebWorkerFn,
useWindowFocus,
useWindowScroll,
useWindowSize,
watchArray,
watchAtMost,
watchDebounced,
watchDeep,
watchIgnorable,
watchImmediate,
watchOnce,
watchPausable,
watchThrottled,
watchTriggerable,
watchWithFilter,
whenever
};
//# sourceMappingURL=vitepress___@vueuse_core.js.map
@@ -1,7 +0,0 @@
{
"version": 3,
"sources": [],
"sourcesContent": [],
"mappings": "",
"names": []
}
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
-347
View File
@@ -1,347 +0,0 @@
import {
BaseTransition,
BaseTransitionPropsValidators,
Comment,
DeprecationTypes,
EffectScope,
ErrorCodes,
ErrorTypeStrings,
Fragment,
KeepAlive,
ReactiveEffect,
Static,
Suspense,
Teleport,
Text,
TrackOpTypes,
Transition,
TransitionGroup,
TriggerOpTypes,
VueElement,
assertNumber,
callWithAsyncErrorHandling,
callWithErrorHandling,
camelize,
capitalize,
cloneVNode,
compatUtils,
compile,
computed,
createApp,
createBaseVNode,
createBlock,
createCommentVNode,
createElementBlock,
createHydrationRenderer,
createPropsRestProxy,
createRenderer,
createSSRApp,
createSlots,
createStaticVNode,
createTextVNode,
createVNode,
customRef,
defineAsyncComponent,
defineComponent,
defineCustomElement,
defineEmits,
defineExpose,
defineModel,
defineOptions,
defineProps,
defineSSRCustomElement,
defineSlots,
devtools,
effect,
effectScope,
getCurrentInstance,
getCurrentScope,
getCurrentWatcher,
getTransitionRawChildren,
guardReactiveProps,
h,
handleError,
hasInjectionContext,
hydrate,
hydrateOnIdle,
hydrateOnInteraction,
hydrateOnMediaQuery,
hydrateOnVisible,
initCustomFormatter,
initDirectivesForSSR,
inject,
isMemoSame,
isProxy,
isReactive,
isReadonly,
isRef,
isRuntimeOnly,
isShallow,
isVNode,
markRaw,
mergeDefaults,
mergeModels,
mergeProps,
nextTick,
nodeOps,
normalizeClass,
normalizeProps,
normalizeStyle,
onActivated,
onBeforeMount,
onBeforeUnmount,
onBeforeUpdate,
onDeactivated,
onErrorCaptured,
onMounted,
onRenderTracked,
onRenderTriggered,
onScopeDispose,
onServerPrefetch,
onUnmounted,
onUpdated,
onWatcherCleanup,
openBlock,
patchProp,
popScopeId,
provide,
proxyRefs,
pushScopeId,
queuePostFlushCb,
reactive,
readonly,
ref,
registerRuntimeCompiler,
render,
renderList,
renderSlot,
resolveComponent,
resolveDirective,
resolveDynamicComponent,
resolveFilter,
resolveTransitionHooks,
setBlockTracking,
setDevtoolsHook,
setTransitionHooks,
shallowReactive,
shallowReadonly,
shallowRef,
ssrContextKey,
ssrUtils,
stop,
toDisplayString,
toHandlerKey,
toHandlers,
toRaw,
toRef,
toRefs,
toValue,
transformVNodeArgs,
triggerRef,
unref,
useAttrs,
useCssModule,
useCssVars,
useHost,
useId,
useModel,
useSSRContext,
useShadowRoot,
useSlots,
useTemplateRef,
useTransitionState,
vModelCheckbox,
vModelDynamic,
vModelRadio,
vModelSelect,
vModelText,
vShow,
version,
warn,
watch,
watchEffect,
watchPostEffect,
watchSyncEffect,
withAsyncContext,
withCtx,
withDefaults,
withDirectives,
withKeys,
withMemo,
withModifiers,
withScopeId
} from "./chunk-XY26LBYQ.js";
export {
BaseTransition,
BaseTransitionPropsValidators,
Comment,
DeprecationTypes,
EffectScope,
ErrorCodes,
ErrorTypeStrings,
Fragment,
KeepAlive,
ReactiveEffect,
Static,
Suspense,
Teleport,
Text,
TrackOpTypes,
Transition,
TransitionGroup,
TriggerOpTypes,
VueElement,
assertNumber,
callWithAsyncErrorHandling,
callWithErrorHandling,
camelize,
capitalize,
cloneVNode,
compatUtils,
compile,
computed,
createApp,
createBlock,
createCommentVNode,
createElementBlock,
createBaseVNode as createElementVNode,
createHydrationRenderer,
createPropsRestProxy,
createRenderer,
createSSRApp,
createSlots,
createStaticVNode,
createTextVNode,
createVNode,
customRef,
defineAsyncComponent,
defineComponent,
defineCustomElement,
defineEmits,
defineExpose,
defineModel,
defineOptions,
defineProps,
defineSSRCustomElement,
defineSlots,
devtools,
effect,
effectScope,
getCurrentInstance,
getCurrentScope,
getCurrentWatcher,
getTransitionRawChildren,
guardReactiveProps,
h,
handleError,
hasInjectionContext,
hydrate,
hydrateOnIdle,
hydrateOnInteraction,
hydrateOnMediaQuery,
hydrateOnVisible,
initCustomFormatter,
initDirectivesForSSR,
inject,
isMemoSame,
isProxy,
isReactive,
isReadonly,
isRef,
isRuntimeOnly,
isShallow,
isVNode,
markRaw,
mergeDefaults,
mergeModels,
mergeProps,
nextTick,
nodeOps,
normalizeClass,
normalizeProps,
normalizeStyle,
onActivated,
onBeforeMount,
onBeforeUnmount,
onBeforeUpdate,
onDeactivated,
onErrorCaptured,
onMounted,
onRenderTracked,
onRenderTriggered,
onScopeDispose,
onServerPrefetch,
onUnmounted,
onUpdated,
onWatcherCleanup,
openBlock,
patchProp,
popScopeId,
provide,
proxyRefs,
pushScopeId,
queuePostFlushCb,
reactive,
readonly,
ref,
registerRuntimeCompiler,
render,
renderList,
renderSlot,
resolveComponent,
resolveDirective,
resolveDynamicComponent,
resolveFilter,
resolveTransitionHooks,
setBlockTracking,
setDevtoolsHook,
setTransitionHooks,
shallowReactive,
shallowReadonly,
shallowRef,
ssrContextKey,
ssrUtils,
stop,
toDisplayString,
toHandlerKey,
toHandlers,
toRaw,
toRef,
toRefs,
toValue,
transformVNodeArgs,
triggerRef,
unref,
useAttrs,
useCssModule,
useCssVars,
useHost,
useId,
useModel,
useSSRContext,
useShadowRoot,
useSlots,
useTemplateRef,
useTransitionState,
vModelCheckbox,
vModelDynamic,
vModelRadio,
vModelSelect,
vModelText,
vShow,
version,
warn,
watch,
watchEffect,
watchPostEffect,
watchSyncEffect,
withAsyncContext,
withCtx,
withDefaults,
withDirectives,
withKeys,
withMemo,
withModifiers,
withScopeId
};
//# sourceMappingURL=vue.js.map
-7
View File
@@ -1,7 +0,0 @@
{
"version": 3,
"sources": [],
"sourcesContent": [],
"mappings": "",
"names": []
}
-2
View File
@@ -1,2 +0,0 @@
const js = `(function(){var h=document.querySelector(".hero");if(!h)return "No .hero element";var s=getComputedStyle(h);return JSON.stringify({width:s.width,marginLeft:s.marginLeft,marginTop:s.marginTop,paddingTop:s.paddingTop})})()`;
console.log(Buffer.from(js).toString('base64'));
-130
View File
@@ -1,130 +0,0 @@
common.home=Home
common.search=Search
common.themeSwitch=Switch Theme
common.menu=Menu
common.close=Close
common.toc=Table of Contents
common.openToc=Open Table of Contents
common.closeToc=Close Table of Contents
common.newer=Newer
common.older=Older
common.loading=Loading...
common.allLoaded=All posts loaded
common.allMomentsLoaded=All moments loaded
common.noPosts=No posts yet.
common.postCount=Total {0} posts
common.categoryCount=Total {0} categories
common.tagCount=Total {0} tags
common.readMore=Read more
common.words=words
common.readTime=min read
common.views=views
common.like=Like
common.pagination=Pagination
common.toggleMenu=Toggle menu
common.all=All
common.noPhotos=No photos yet.
common.noPhotosInGroup=No photos in this group.
common.pluginError=Plugin loading error, please check plugin status.
common.installPlugin=This feature requires the {0} plugin, <a href="{1}">go to the marketplace to install</a>
common.featured=Featured
common.latestPosts=Latest Posts
common.timeline=Timeline
common.viewAll=View all
navbar.search=Search
navbar.themeSwitch=Switch Theme
hero.pinned=Pinned
hero.posts=Posts
article.previousPost=Previous
article.nextPost=Next
article.toc=Table of Contents
article.wordCount=words
article.readTime=min read
article.views=views
article.relatedPosts=Related Posts
share.title=Share
share.qq=QQ
share.wechat=WeChat
share.weibo=Weibo
share.twitter=X (Twitter)
share.facebook=Facebook
share.telegram=Telegram
share.copyLink=Copy Link
share.copied=Copied!
share.qrcode=QR Code
share.wechatQR=Scan to Share
share.wechatHint=Open WeChat and scan the QR code to share
archives.title=Archives
categories.title=Categories
tags.title=Tags
links.title=Friends
links.subtitle=Mountains and rivers, a journey of a thousand miles
moments.title=Moments
moments.noMoments=No moments yet, go record your mood \U0001f319
photos.title=Gallery
friends.title=Friends Circle
equipment.title=Equipment
steam.title=Steam
steam.homepage=Steam Homepage
steam.level=Level
steam.ownedGames=Owned Games
steam.totalPlaytime=Total Playtime
steam.recentPlaytime=Recent 2 Weeks
steam.badges=Badges
steam.hours=hours
steam.recentlyPlayed=Recently Played
steam.gameLibrary=Game Library
steam.lastPlayed=Last played {0}
douban.title=Douban
douban.wish=Wish
douban.done=Done
douban.wishRead=Wish
douban.reading=Reading
douban.listened=Listened
douban.wishPlay=Wish
douban.played=Played
douban.wishListen=Wish
douban.listening=Listening
douban.playing=Playing
douban.myRating=My:
messageboard.title=Message Board
messageboard.subtitle=Leave your footprints here \U0001f319
footer.poweredBy=Powered by Halo
error.404.title=Page Not Found
error.404.desc=The page you are looking for seems to have wandered off this island
error.404.home=Back to Home
error.404.back=Go Back
error.500.title=Server Error
error.500.desc=The island encountered some issues, please try again later
error.500.home=Back to Home
error.500.back=Go Back
sidebar.authorInfo=About Author
sidebar.latestPosts=Latest Posts
sidebar.recommendedPosts=Recommended
sidebar.timeProgress=Time Progress
sidebar.dayProgress=Day
sidebar.monthProgress=Month
sidebar.yearProgress=Year
sidebar.categories=Categories
sidebar.tags=Tags
comment.style=Warm
comment.showAvatar=Show Avatar
-130
View File
@@ -1,130 +0,0 @@
common.home=Home
common.search=Search
common.themeSwitch=Switch Theme
common.menu=Menu
common.close=Close
common.toc=Table of Contents
common.openToc=Open Table of Contents
common.closeToc=Close Table of Contents
common.newer=Newer
common.older=Older
common.loading=Loading...
common.allLoaded=All posts loaded
common.noPosts=No posts yet.
common.postCount=Total {0} posts
common.readMore=Read more
common.words=words
common.readTime=min read
common.views=views
common.like=Like
common.pagination=Pagination
common.toggleMenu=Toggle menu
common.allMomentsLoaded=All moments loaded
common.categoryCount=Total {0} categories
common.tagCount=Total {0} tags
common.all=All
common.noPhotos=No photos yet.
common.noPhotosInGroup=No photos in this group.
common.pluginError=Plugin loading error, please check plugin status.
common.installPlugin=This feature requires the {0} plugin, <a href="{1}">go to the marketplace to install</a>
common.featured=Featured
common.latestPosts=Latest Posts
common.timeline=Timeline
common.viewAll=View all
navbar.search=Search
navbar.themeSwitch=Switch Theme
hero.pinned=Pinned
hero.posts=Posts
article.previousPost=Previous
article.nextPost=Next
article.toc=Table of Contents
article.wordCount=words
article.readTime=min read
article.views=views
article.relatedPosts=Related Posts
share.title=Share
share.qq=QQ
share.wechat=WeChat
share.weibo=Weibo
share.twitter=X (Twitter)
share.facebook=Facebook
share.telegram=Telegram
share.copyLink=Copy Link
share.copied=Copied!
share.qrcode=QR Code
share.wechatQR=Scan to Share
share.wechatHint=Open WeChat and scan the QR code to share
archives.title=Archives
categories.title=Categories
tags.title=Tags
links.title=Friends
links.subtitle=Mountains and rivers, a journey of a thousand miles
moments.title=Moments
moments.noMoments=No moments yet, go record your mood \U0001f319
photos.title=Gallery
friends.title=Friends Circle
equipment.title=Equipment
steam.title=Steam
steam.homepage=Steam Homepage
steam.level=Level
steam.ownedGames=Owned Games
steam.totalPlaytime=Total Playtime
steam.recentPlaytime=Recent 2 Weeks
steam.badges=Badges
steam.hours=hours
steam.recentlyPlayed=Recently Played
steam.gameLibrary=Game Library
steam.lastPlayed=Last played {0}
douban.title=Douban
douban.wish=Wish
douban.done=Done
douban.wishRead=Wish
douban.reading=Reading
douban.listened=Listened
douban.wishPlay=Wish
douban.played=Played
douban.wishListen=Wish
douban.listening=Listening
douban.playing=Playing
douban.myRating=My:
messageboard.title=Message Board
messageboard.subtitle=Leave your footprints here \U0001f319
footer.poweredBy=Powered by Halo
error.404.title=Page Not Found
error.404.desc=The page you are looking for seems to have wandered off this island
error.404.home=Back to Home
error.404.back=Go Back
error.500.title=Server Error
error.500.desc=The island encountered some issues, please try again later
error.500.home=Back to Home
error.500.back=Go Back
sidebar.authorInfo=About Author
sidebar.latestPosts=Latest Posts
sidebar.recommendedPosts=Recommended
sidebar.timeProgress=Time Progress
sidebar.dayProgress=Day
sidebar.monthProgress=Month
sidebar.yearProgress=Year
sidebar.categories=Categories
sidebar.tags=Tags
comment.style=Warm
comment.showAvatar=Show Avatar
-130
View File
@@ -1,130 +0,0 @@
common.home=\u9996\u9875
common.search=\u641c\u7d22
common.themeSwitch=\u5207\u6362\u4e3b\u9898
common.menu=\u83dc\u5355
common.close=\u5173\u95ed
common.toc=\u76ee\u5f55
common.openToc=\u6253\u5f00\u76ee\u5f55
common.closeToc=\u5173\u95ed\u76ee\u5f55
common.newer=\u8f83\u65b0
common.older=\u8f83\u65e7
common.loading=\u52a0\u8f7d\u4e2d...
common.allLoaded=\u5df2\u52a0\u8f7d\u5168\u90e8\u6587\u7ae0
common.noPosts=\u6682\u65e0\u6587\u7ae0
common.postCount=\u5171 {0} \u7bc7\u6587\u7ae0
common.readMore=\u9605\u8bfb\u66f4\u591a
common.words=\u5b57
common.readTime=\u5206\u949f
common.views=\u9605\u8bfb
common.like=\u70b9\u8d5e
common.pagination=\u5206\u9875
common.toggleMenu=\u5207\u6362\u83dc\u5355
common.allMomentsLoaded=\u5df2\u52a0\u8f7d\u5168\u90e8\u77ac\u95f4
common.categoryCount=\u5171 {0} \u4e2a\u5206\u7c7b
common.tagCount=\u5171 {0} \u4e2a\u6807\u7b7e
common.all=\u5168\u90e8
common.noPhotos=\u6682\u65e0\u56fe\u7247
common.noPhotosInGroup=\u8be5\u5206\u7ec4\u6682\u65e0\u56fe\u7247
common.pluginError=\u63d2\u4ef6\u52a0\u8f7d\u5f02\u5e38\uff0c\u8bf7\u68c0\u67e5\u63d2\u4ef6\u72b6\u6001
common.installPlugin=\u8be5\u529f\u80fd\u9700\u8981\u5b89\u88c5\u300c{0}\u300d\u63d2\u4ef6\uff0c<a href="{1}">\u524d\u5f80\u5e94\u7528\u5e02\u573a\u5b89\u88c5</a>
common.featured=\u7cbe\u9009
common.latestPosts=\u6700\u65b0\u6587\u7ae0
common.timeline=\u65f6\u95f4\u7ebf
common.viewAll=\u67e5\u770b\u5168\u90e8
navbar.search=\u641c\u7d22
navbar.themeSwitch=\u5207\u6362\u4e3b\u9898
hero.pinned=\u7f6e\u9876
hero.posts=\u6587\u7ae0
article.previousPost=\u4e0a\u4e00\u7bc7
article.nextPost=\u4e0b\u4e00\u7bc7
article.toc=\u76ee\u5f55
article.wordCount=\u5b57
article.readTime=\u5206\u949f
article.views=\u9605\u8bfb
article.relatedPosts=\u76f8\u5173\u63a8\u8350
share.title=\u5206\u4eab
share.qq=QQ
share.wechat=\u5fae\u4fe1
share.weibo=\u5fae\u535a
share.twitter=X
share.facebook=Facebook
share.telegram=Telegram
share.copyLink=\u590d\u5236\u94fe\u63a5
share.copied=\u5df2\u590d\u5236
share.qrcode=\u4e8c\u7ef4\u7801
share.wechatQR=\u5fae\u4fe1\u626b\u7801\u5206\u4eab
share.wechatHint=\u6253\u5f00\u5fae\u4fe1\uff0c\u626b\u63cf\u4e8c\u7ef4\u7801\u5373\u53ef\u5206\u4eab
archives.title=\u5f52\u6863
categories.title=\u5206\u7c7b
tags.title=\u6807\u7b7e
links.title=\u53cb\u94fe
links.subtitle=\u5c71\u6c34\u4e00\u7a0b\uff0c\u4e09\u751f\u6709\u5e78
moments.title=\u52a8\u6001
moments.noMoments=\u8fd8\u6ca1\u6709\u77ac\u95f4\uff0c\u5feb\u53bb\u8bb0\u5f55\u5f53\u4e0b\u7684\u5fc3\u60c5\u5427 \U0001f319
photos.title=\u76f8\u518c
friends.title=\u53cb\u4eba\u5708
equipment.title=\u88c5\u5907
steam.title=Steam
steam.homepage=Steam \u4e3b\u9875
steam.level=\u7b49\u7ea7
steam.ownedGames=\u62e5\u6709\u6e38\u620f
steam.totalPlaytime=\u603b\u6e38\u73a9\u65f6\u957f
steam.recentPlaytime=\u8fd1\u4e24\u5468
steam.badges=\u5fbd\u7ae0
steam.hours=\u5c0f\u65f6
steam.recentlyPlayed=\u6700\u8fd1\u6e38\u73a9
steam.gameLibrary=\u6e38\u620f\u5e93
steam.lastPlayed=\u6700\u540e\u6e38\u73a9 {0}
douban.title=\u8c46\u74e3
douban.wish=\u60f3\u770b
douban.done=\u770b\u8fc7
douban.wishRead=\u60f3\u8bfb
douban.reading=\u5728\u8bfb
douban.listened=\u542c\u8fc7
douban.wishPlay=\u60f3\u73a9
douban.played=\u73a9\u8fc7
douban.wishListen=\u60f3\u542c
douban.listening=\u5728\u542c
douban.playing=\u5728\u73a9
douban.myRating=\u6211\u7684\uff1a
messageboard.title=\u7559\u8a00\u677f
messageboard.subtitle=\u5728\u8fd9\u91cc\u7559\u4e0b\u4f60\u7684\u8db3\u8ff9 \U0001f319
footer.poweredBy=Powered by Halo
error.404.title=\u9875\u9762\u672a\u627e\u5230
error.404.desc=\u4f60\u5bfb\u627e\u7684\u9875\u9762\u4f3c\u4e4e\u5df2\u7ecf\u8ff7\u8def\u4e86
error.404.home=\u8fd4\u56de\u9996\u9875
error.404.back=\u8fd4\u56de\u4e0a\u9875
error.500.title=\u670d\u52a1\u5668\u9519\u8bef
error.500.desc=\u5c0f\u5c9b\u9047\u5230\u4e86\u4e00\u4e9b\u95ee\u9898\uff0c\u8bf7\u7a0d\u540e\u518d\u8bd5
error.500.home=\u8fd4\u56de\u9996\u9875
error.500.back=\u8fd4\u56de\u4e0a\u9875
sidebar.authorInfo=\u535a\u4e3b\u4fe1\u606f
sidebar.latestPosts=\u6700\u65b0\u6587\u7ae0
sidebar.recommendedPosts=\u63a8\u8350\u6587\u7ae0
sidebar.timeProgress=\u65f6\u5149\u8fdb\u5ea6
sidebar.dayProgress=\u65e5
sidebar.monthProgress=\u6708
sidebar.yearProgress=\u5e74
sidebar.categories=\u5206\u7c7b
sidebar.tags=\u6807\u7b7e
comment.style=\u6e29\u6696
comment.showAvatar=\u663e\u793a\u5934\u50cf
-130
View File
@@ -1,130 +0,0 @@
common.home=\u9996\u9801
common.search=\u641c\u5c0b
common.themeSwitch=\u5207\u63db\u4e3b\u984c
common.menu=\u9078\u55ae
common.close=\u95dc\u9589
common.toc=\u76ee\u9304
common.openToc=\u6253\u958b\u76ee\u9304
common.closeToc=\u95dc\u9589\u76ee\u9304
common.newer=\u8f03\u65b0
common.older=\u8f03\u820a
common.loading=\u8f09\u5165\u4e2d...
common.allLoaded=\u5df2\u8f09\u5165\u5168\u90e8\u6587\u7ae0
common.noPosts=\u66ab\u7121\u6587\u7ae0
common.postCount=\u5171 {0} \u7bc7\u6587\u7ae0
common.readMore=\u95b1\u8b80\u66f4\u591a
common.words=\u5b57
common.readTime=\u5206\u9418
common.views=\u95b1\u8b80
common.like=\u6309\u8b9a
common.pagination=\u5206\u9801
common.toggleMenu=\u5207\u63db\u9078\u55ae
common.allMomentsLoaded=\u5df2\u8f09\u5165\u5168\u90e8\u77ac\u9593
common.categoryCount=\u5171 {0} \u500b\u5206\u985e
common.tagCount=\u5171 {0} \u500b\u6a19\u7c64
common.all=\u5168\u90e8
common.noPhotos=\u66ab\u7121\u5716\u7247
common.noPhotosInGroup=\u8a72\u5206\u7d44\u66ab\u7121\u5716\u7247
common.pluginError=\u5916\u639b\u7a0b\u5f0f\u8f09\u5165\u7570\u5e38\uff0c\u8acb\u6aa2\u67e5\u5916\u639b\u7a0b\u5f0f\u72c0\u614b
common.installPlugin=\u8a72\u529f\u80fd\u9700\u8981\u5b89\u88dd\u300c{0}\u300d\u5916\u639b\u7a0b\u5f0f\uff0c<a href="{1}">\u524d\u5f80\u61c9\u7528\u5e02\u5834\u5b89\u88dd</a>
common.featured=\u7cbe\u9078
common.latestPosts=\u6700\u65b0\u6587\u7ae0
common.timeline=\u6642\u9593\u7dda
common.viewAll=\u67e5\u770b\u5168\u90e8
navbar.search=\u641c\u5c0b
navbar.themeSwitch=\u5207\u63db\u4e3b\u984c
hero.pinned=\u7f6e\u9802
hero.posts=\u6587\u7ae0
article.previousPost=\u4e0a\u4e00\u7bc7
article.nextPost=\u4e0b\u4e00\u7bc7
article.toc=\u76ee\u9304
article.wordCount=\u5b57
article.readTime=\u5206\u9418
article.views=\u95b1\u8b80
article.relatedPosts=\u76f8\u95dc\u63a8\u85a6
share.title=\u5206\u4eab
share.qq=QQ
share.wechat=\u5fae\u4fe1
share.weibo=\u5fae\u535a
share.twitter=X
share.facebook=Facebook
share.telegram=Telegram
share.copyLink=\u8907\u88fd\u9023\u7d50
share.copied=\u5df2\u8907\u88fd
share.qrcode=\u4e8c\u7dad\u78bc
share.wechatQR=\u5fae\u4fe1\u6383\u78bc\u5206\u4eab
share.wechatHint=\u6253\u958b\u5fae\u4fe1\uff0c\u6383\u63cf\u4e8c\u7dad\u78bc\u5373\u53ef\u5206\u4eab
archives.title=\u6b78\u6a94
categories.title=\u5206\u985e
tags.title=\u6a19\u7c64
links.title=\u53cb\u93c8
links.subtitle=\u5c71\u6c34\u4e00\u7a0b\uff0c\u4e09\u751f\u6709\u5e78
moments.title=\u52d5\u614b
moments.noMoments=\u9084\u6c92\u6709\u77ac\u9593\uff0c\u5feb\u53bb\u8a18\u9304\u7576\u4e0b\u7684\u5fc3\u60c5\u5427 \U0001f319
photos.title=\u76f8\u7c3f
friends.title=\u53cb\u4eba\u5708
equipment.title=\u88dd\u5099
steam.title=Steam
steam.homepage=Steam \u4e3b\u9801
steam.level=\u7b49\u7d1a
steam.ownedGames=\u64c1\u6709\u904a\u6232
steam.totalPlaytime=\u7e3d\u904a\u73a9\u6642\u9577
steam.recentPlaytime=\u8fd1\u5169\u9031
steam.badges=\u5fbd\u7ae0
steam.hours=\u5c0f\u6642
steam.recentlyPlayed=\u6700\u8fd1\u904a\u73a9
steam.gameLibrary=\u904a\u6232\u5eab
steam.lastPlayed=\u6700\u5f8c\u904a\u73a9 {0}
douban.title=\u8c46\u74e3
douban.wish=\u60f3\u770b
douban.done=\u770b\u904e
douban.wishRead=\u60f3\u8b80
douban.reading=\u5728\u8b80
douban.listened=\u807d\u904e
douban.wishPlay=\u60f3\u73a9
douban.played=\u73a9\u904e
douban.wishListen=\u60f3\u807d
douban.listening=\u5728\u807d
douban.playing=\u5728\u73a9
douban.myRating=\u6211\u7684\uff1a
messageboard.title=\u7559\u8a00\u677f
messageboard.subtitle=\u5728\u9019\u88cf\u7559\u4e0b\u4f60\u7684\u8db3\u8de1 \U0001f319
footer.poweredBy=Powered by Halo
error.404.title=\u9801\u9762\u672a\u627e\u5230
error.404.desc=\u4f60\u5c0b\u627e\u7684\u9801\u9762\u4f3c\u4e4e\u5df2\u7d93\u8ff7\u8def\u4e86
error.404.home=\u8fd4\u56de\u9996\u9801
error.404.back=\u8fd4\u56de\u4e0a\u9801
error.500.title=\u4f3a\u670d\u5668\u932f\u8aa4
error.500.desc=\u5c0f\u5cf6\u9047\u5230\u4e86\u4e00\u4e9b\u554f\u984c\uff0c\u8acb\u7a0d\u5f8c\u518d\u8a66
error.500.home=\u8fd4\u56de\u9996\u9801
error.500.back=\u8fd4\u56de\u4e0a\u9801
sidebar.authorInfo=\u535a\u4e3b\u8cc7\u8a0a
sidebar.latestPosts=\u6700\u65b0\u6587\u7ae0
sidebar.recommendedPosts=\u63a8\u85a6\u6587\u7ae0
sidebar.timeProgress=\u6642\u5149\u9032\u5ea6
sidebar.dayProgress=\u65e5
sidebar.monthProgress=\u6708
sidebar.yearProgress=\u5e74
sidebar.categories=\u5206\u985e
sidebar.tags=\u6a19\u7c64
comment.style=\u6eab\u6696
comment.showAvatar=\u986f\u793a\u982d\u50cf
+4 -13
View File
@@ -1,5 +1,5 @@
{
"version": "1.1.1",
"version": "1.1.5",
"type": "module",
"engines": {
"node": ">=22.12.0"
@@ -23,24 +23,15 @@
}
]
},
"pnpm": {
"onlyBuiltDependencies": [
"@parcel/watcher",
"sharp"
]
},
"dependencies": {
"@astrojs/vue": "^6.0.1",
"astro": "^6.1.4",
"lightgallery": "^2.9.0",
"vue": "^3.5.32"
"lightgallery": "^2.9.0"
},
"devDependencies": {
"@iconify-json/mingcute": "^1.2.7",
"@halo-dev/theme-package-cli": "^1.1.1",
"nodemon": "^3.1.14",
"prettier": "3.8.1",
"prettier-plugin-astro": "0.14.1",
"sass": "^1.87.0",
"unplugin-icons": "^23.0.1"
"sass": "^1.87.0"
}
}
+682 -1061
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,5 +1,5 @@
allowBuilds:
'@parcel/watcher': true
"@parcel/watcher": true
esbuild: true
sharp: true
onlyBuiltDependencies:
-455
View File
@@ -1,455 +0,0 @@
<!doctype html>
<html lang="zh-cn">
<head>
<meta charset="UTF-8" />
<title>字体分包构建报告</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<link
rel="stylesheet"
href="https://fastly.jsdelivr.net/npm/vant@4.9.15/lib/index.css"
/>
<link
href="https://unpkg.com/vue3-easy-data-table@1.5.47/dist/style.css"
rel="stylesheet"
/>
<link rel="stylesheet" href="./result.css" />
<style>
body {
overflow-wrap: break-word;
font-size: 18px;
background-color: #edf0f3;
--easy-table-border: 0;
}
main {
padding: 20px;
margin-top: var(--van-nav-bar-height);
margin-bottom: var(--van-tabbar-height);
}
.light-blue {
--easy-table-body-row-font-color: #427cb7;
}
.light-red {
--easy-table-body-row-font-color: #b74273;
}
.hint {
display: block;
text-align: center;
color: gray;
margin: 1rem;
}
.scroller {
height: 100%;
}
.user {
height: 32%;
padding: 0 12px;
display: flex;
align-items: center;
}
</style>
</head>
<body>
<div id="app">
<van-nav-bar
:title="reporter.state?.css?.family +' 分包报告'"
right-text="Github"
@click-right="toGithub"
style="position: fixed; width: 100%; top: 0"
></van-nav-bar>
<div v-if="reporter.isLoading">加载中</div>
<div v-if="reporter.isError">加载错误</div>
<main :style="style">
<van-cell-group inset title="样式控制">
<van-field
v-model="fontSize"
label="字体大小"
placeholder="请输入字体大小"
></van-field>
<van-field
v-model="fontWeight"
label="字体重量"
placeholder="请输入字重"
></van-field>
</van-cell-group>
<!-- 汇总信息 -->
<display-chart
v-if="reporter.isOk() && activePage === 0"
:reporter="reporter.state"
></display-chart>
<!-- 字体信息表格 -->
<name-table
v-if="reporter.isOk() && activePage === 1"
:reporter="reporter.state"
></name-table>
<!-- 单包查询 -->
<pkg-list
v-if="reporter.isOk() && activePage === 2"
:reporter="reporter.state"
></pkg-list>
<div class="hint">
<a href="https://chinese-font.netlify.app">中文网字计划</a>
<span> cn-font-split 生成 </span>
</div>
</main>
<!-- 底部导航栏 -->
<van-tabbar v-model="activePage">
<van-tabbar-item icon="home-o">汇总</van-tabbar-item>
<van-tabbar-item icon="home-o">字体信息</van-tabbar-item>
<van-tabbar-item icon="search">分包</van-tabbar-item>
</van-tabbar>
</div>
</body>
<script src="https://unpkg.com/vue@3.4.33"></script>
<script src="https://unpkg.com/vant@4/lib/vant.min.js"></script>
<script src="https://unpkg.com/vue3-easy-data-table@1.5.47/dist/vue3-easy-data-table.umd.js"></script>
<script src="https://unpkg.com/echarts@5.5.1"></script>
<script src="https://unpkg.com/vue-echarts@7.0.3"></script>
<script src="https://unpkg.com/protobufjs@7.4.0/dist/protobuf.min.js"></script>
<script>
const { createApp } = Vue;
const app = createApp({
data() {
return {
reporter: PromiseToState(loadReport),
activePage: 0,
fontWeight: '',
fontSize: 16,
};
},
methods: {
toGithub() {
window.open('https://github.com/KonghaYao/cn-font-split');
},
},
async mounted() {
const state = await this.reporter.refetch();
this.fontWeight = state.css.weight;
},
computed: {
style() {
const size = this.fontSize < 12 ? 12 : this.fontSize;
return (
`font-family: '${this.reporter.state?.css?.family}';` +
`font-weight: ${this.fontWeight};` +
`--van-cell-font-size: ${size}px;`
);
},
},
});
/** 封装Promise为状态对象 **/
function PromiseToState(fn) {
return {
state: {},
isOk() {
return !this.isLoading && !this.isError && this.state;
},
isLoading: false,
isError: false,
async refetch() {
this.isLoading = true;
this.isError = false;
try {
this.state = await fn();
} catch (e) {
console.error(e);
this.isError = true;
}
this.isLoading = false;
return this.state;
},
};
}
/** 载入构建出来的二进制报告文件 **/
async function loadReport() {
return new Promise((resolve, reject) => {
protobuf.load('./index.proto', (err, root) => {
if (err) return reject(err);
fetch('./reporter.bin')
.then((res) => res.arrayBuffer())
.then((buf) => {
const OutputReport = root.lookup('OutputReport');
const data = OutputReport.decode(
new Uint8Array(buf),
);
console.log(data);
resolve(data);
})
.catch(reject);
});
});
}
globalThis.app = app;
app.use(vant);
app.component('v-chart', VueECharts);
app.component('easy-data-table', window['vue3-easy-data-table']);
</script>
<!-- 汇总数据 -->
<template id="display-chart">
<van-cell-group inset title="构建信息">
<van-cell title="构建工具" value="cn-font-split"></van-cell>
<van-cell title="构建版本" :value="reporter.version"></van-cell>
<van-cell title="构建平台" :value="reporter.platform"></van-cell>
</van-cell-group>
<van-cell-group inset title="构建详情">
<div style="height: 300px; width: 400px">
<v-chart :option="pieOptions"></v-chart>
</div>
<div style="height: 400px; width: 100%">
<v-chart :option="option"></v-chart>
</div>
</van-cell-group>
</template>
<script>
const tooltip = {
trigger: 'axis',
axisPointer: {
type: 'cross',
crossStyle: {
color: '#999',
},
},
};
app.component('display-chart', {
template: '#display-chart',
props: {
reporter: Object,
},
computed: {
option() {
const data = this.reporter.subsetDetail ?? [];
return {
title: {
text: '构建耗时',
x: 'center',
},
xAxis: { type: 'category' },
tooltip,
yAxis: [
{
type: 'value',
name: 'time / ms',
},
{
type: 'value',
name: 'bytes / KB',
},
],
series: [
{
name: 'time',
type: 'bar',
data: data.map((i) => i.duration),
},
{
name: 'bytes',
type: 'line',
data: data.map((i) =>
(i.bytes / 1024).toFixed(2),
),
},
],
};
},
pieOptions() {
const message = this.reporter.bundleMessage;
if (!message) return {};
return {
title: {
text: '构建大小对比',
x: 'center',
},
series: [
{
type: 'pie',
radius: ['45%', '60%'],
label: {
formatter(b, c) {
return `${b.name}\n${(
b.value /
(1024 * 1024)
).toFixed(2)}MB`;
},
},
data: [
{
value: message.bundledBytes,
name: 'bundled',
},
{
value: message.originBytes,
name: 'origin',
},
],
},
{
type: 'pie',
radius: [0, '30%'],
label: {
formatter(b, c) {
return `${b.name}\n${b.value}`;
},
},
data: [
{
value: message.bundledSize,
name: 'bundled',
},
{
value: message.originSize,
name: 'origin',
},
],
},
],
};
},
},
});
</script>
<!-- 字体信息表格 -->
<template id="name-table">
<van-cell-group inset title="CSS 信息">
<van-cell
title="font-family"
:value="reporter.css?.family"
></van-cell>
<van-cell
title="font-weight"
:value="reporter.css?.weight"
></van-cell>
<van-cell
title="font-style"
:value="reporter.css?.style"
></van-cell>
<van-cell
title="font-display"
:value="reporter.css?.display"
></van-cell>
</van-cell-group>
<van-cell-group inset title="字体信息">
<easy-data-table
:headers="columns"
:items="reporter.nameTable??[]"
:rows-per-page="1000"
:body-row-class-name="useColor"
></easy-data-table>
</van-cell-group>
</template>
<script>
app.component('name-table', {
template: '#name-table',
props: {
reporter: Object,
},
data() {
return {
color: {
Windows: 'light-blue',
Macintosh: 'light-red',
},
columns: [
{
text: '语言',
value: 'language',
sortable: true,
minWidth: '36px',
},
{
text: '平台',
value: 'platform',
sortable: true,
minWidth: '20px',
},
{
text: '名称',
value: 'name',
sortable: true,
minWidth: '50px',
},
{
text: '值',
value: 'value',
sortable: true,
width: '200px',
},
],
};
},
methods: {
useColor(item) {
return this.color[item.platform];
},
},
});
</script>
<!-- 每个包的数据 -->
<template id="pkg-list">
<van-cell-group inset>
<van-field
v-model="search"
label="搜索"
placeholder="请输入关键字"
></van-field>
</van-cell-group>
<van-cell-group inset title="字体分包详情">
<v-list
ref="list"
:items="items"
:first-render="10"
style="height: 80vh"
>
<template #default="{ item, index }">
<div style="padding: 0 1rem">
<h2 :style="{ width: item.width }">
ITEM: {{ index }} - {{ item.hash }}
</h2>
<span> {{String.fromCodePoint(...item.chars)}} </span>
</div>
</template>
</v-list>
</van-cell-group>
</template>
<script src="https://unpkg.com/@virtual-list/vue/dist/v3/index.iife.js"></script>
<script>
app.component('v-list', virtualListVue);
app.component('pkg-list', {
template: '#pkg-list',
props: {
reporter: Object,
},
data() {
return {
activeNames: [],
search: '',
};
},
mounted() {},
computed: {
items() {
const list = this.reporter.subsetDetail ?? [];
if (this.search) {
return list.filter((i) => i.hash.includes(this.search));
}
return list;
},
},
});
</script>
<script>
app.mount('#app');
</script>
</html>
-117
View File
@@ -1,117 +0,0 @@
syntax = "proto3";
package api_interface;
message InputTemplate {
bytes input = 1; // ttf/woff2 文件的 buffer
optional string out_dir = 2; // 切割后放置文件的文件夹
// ====== 构建产物配置 ======
optional CssProperties css = 5; // CSS 配置
optional string target_type = 6; // 目标类型
repeated bytes subsets = 7; // 子集
optional int32 chunk_size = 9; // 包大小
optional float chunk_size_tolerance = 10; // 包大小容差
optional int32 max_allow_subsets_count = 11; // 最大允许子集数量
optional bool test_html = 13; // 是否生成测试 HTML
optional bool reporter = 14; // 是否生成 reporter.bin
optional PreviewImage preview_image = 15; // 预览图像
optional string rename_output_font = 18; // 重命名输出字体
optional string build_mode = 20; // TODO 构建模式
// ====== 预分包优化项配置 =======
optional bool language_areas = 8; // 是否进行语言区域优化
optional bool multi_threads = 21; // TODO 是否使用多线程
optional bool font_feature = 22; // 是否启用字体特性
optional bool reduce_mins = 23; // 是否减少最小分包,
optional bool auto_subset = 24; // 是否自动子集化
optional bool subset_remain_chars = 25; // 是否自动添加没有声明的字符
// CSS 属性配置
message CssProperties {
optional string font_family = 1; // 字体家族名称
optional string font_weight = 2; // 字体粗细
optional string font_style = 3; // 字体样式
optional string font_display = 4; // 字体显示方式
repeated string local_family = 5; // 本地字体家族名称
repeated PolyfillType polyfill = 6; // Polyfill 类型
// 注释属性
optional bool comment_base = 11; // 基础注释
optional bool comment_name_table = 12; // 名称表注释
optional bool comment_unicodes = 13; // Unicode 注释
optional bool compress = 8; // 是否压缩
optional string file_name = 9; // 文件名
}
// Polyfill 类型
message PolyfillType {
string name = 1; // 名称
string format = 2; // 格式
}
// 预览图像
message PreviewImage {
string text = 1; // 会显示在 svg 中的文本
string name = 2; // svg 文件的名称
}
}
enum EventName {
UNSPECIFIED = 0;
OUTPUT_DATA = 1; // "output_data"
END = 2; // "end"
}
message EventMessage {
EventName event = 1;
string message = 2;
optional bytes data = 3;
}
message MultiMessages {
repeated EventMessage messages = 1;
}
message OutputReport {
string version = 1; // 版本号
Css css = 2; // CSS相关信息,可以直接用
string platform = 3; // 平台信息,rust 的构建平台
BundleMessage bundle_message = 24; // 构建消息
repeated NameTable name_table = 25; // name 表信息
repeated SubsetDetail subset_detail = 26; // 子集详情
message NameTable {
string platform = 1; // 平台
string language = 2; // 语言
string name = 3; // 名称
string value = 4; // 值
}
message SubsetDetail {
uint32 id = 1; // ID
string hash = 2; // 哈希值
string file_name = 6; // 文件名称
uint32 bytes = 3; // 字节数
repeated uint32 chars = 4; // 字符数组
uint32 duration = 5; // 构建时间
}
message BundleMessage {
uint32 origin_size = 1; // 原始大小
uint32 bundled_size = 2; // 产物大小
uint32 origin_bytes = 3; // 原始字节数
uint32 bundled_bytes = 4; // 产物字节数
}
message Css {
string family = 1; // 字体家族
string style = 2; // 样式
string weight = 3; // 字重
string display = 4; // 显示方式
}
}
Binary file not shown.
+1 -2
View File
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="zh">
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
@@ -126,6 +126,5 @@
<a class="wi-error__back" href="javascript:history.back()">返回上页</a>
</div>
</div>
<script>(function(){var lang=navigator.language||'';var locale=lang.startsWith('zh-TW')||lang.startsWith('zh-HK')?'zh_TW':lang.startsWith('zh')?'zh_CN':'en';var i18n={zh_CN:{title:'页面走丢了',desc:'你寻找的页面似乎不在这个小岛上',home:'回到首页',back:'返回上页',pageTitle:'页面走丢了'},zh_TW:{title:'頁面走丟了',desc:'你尋找的頁面似乎不在這個小島上',home:'回到首頁',back:'返回上頁',pageTitle:'頁面走丟了'},en:{title:'Page Not Found',desc:'The page you are looking for seems to have wandered off this island',home:'Back to Home',back:'Go Back',pageTitle:'Page Not Found'}};var t=i18n[locale];var el=document.querySelector('.wi-error__title');if(el)el.textContent=t.title;var desc=document.querySelector('.wi-error__desc');if(desc)desc.textContent=t.desc;var home=document.querySelector('.wi-error__home');if(home)home.textContent=t.home;var back=document.querySelector('.wi-error__back');if(back)back.textContent=t.back;document.title=t.pageTitle+' - WarmIsland';})();</script>
</body>
</html>
+1 -2
View File
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="zh">
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
@@ -126,6 +126,5 @@
<a class="wi-error__back" href="javascript:history.back()">返回上页</a>
</div>
</div>
<script>(function(){var lang=navigator.language||'';var locale=lang.startsWith('zh-TW')||lang.startsWith('zh-HK')?'zh_TW':lang.startsWith('zh')?'zh_CN':'en';var i18n={zh_CN:{title:'服务器开小差了',desc:'小岛遇到了一些问题,稍后再来看看吧',home:'回到首页',back:'返回上页',pageTitle:'服务器开小差了'},zh_TW:{title:'伺服器開小差了',desc:'小島遇到了一些問題,稍後再來看看吧',home:'回到首頁',back:'返回上頁',pageTitle:'伺服器開小差了'},en:{title:'Server Error',desc:'The island encountered some issues, please try again later',home:'Back to Home',back:'Go Back',pageTitle:'Server Error'}};var t=i18n[locale];var el=document.querySelector('.wi-error__title');if(el)el.textContent=t.title;var desc=document.querySelector('.wi-error__desc');if(desc)desc.textContent=t.desc;var home=document.querySelector('.wi-error__home');if(home)home.textContent=t.home;var back=document.querySelector('.wi-error__back');if(back)back.textContent=t.back;document.title=t.pageTitle+' - WarmIsland';})();</script>
</body>
</html>
-21
View File
@@ -1,21 +0,0 @@
<ul class="wi-post-feed" role="list">
<li th:each="post : ${posts.items}" class="wi-post-feed__item">
<a class="wi-post-feed__link" th:href="@{${post.status.permalink}}">
<div class="wi-post-feed__image-wrap" th:unless="${#strings.isEmpty(post.spec.cover)}">
<img
class="wi-post-feed__image"
th:src="${post.spec.cover}"
th:srcset="|${thumbnail.gen(post.spec.cover, 's')} 400w,
${thumbnail.gen(post.spec.cover, 'm')} 800w,
${thumbnail.gen(post.spec.cover, 'l')} 1200w|"
sizes="(max-width: 768px) 100vw, 400px"
/>
</div>
<div class="wi-post-feed__body">
<p class="wi-post-feed__date" th:text="${#dates.format(post.spec.publishTime, 'yyyy-MM-dd')}"></p>
<h2 class="wi-post-feed__title" th:text="${post.spec.title}"></h2>
<p class="wi-post-feed__excerpt" th:text="${post.status.excerpt}"></p>
</div>
</a>
</li>
</ul>
+288 -12
View File
@@ -28,20 +28,63 @@ spec:
- $formkit: text
name: label_archives_title
label: 归档页标题
- group: welcome_popup
label: 欢迎弹窗
formSchema:
- $formkit: switch
name: welcome_popup_enabled
id: welcome_popup_enabled
label: 启用欢迎弹窗
value: false
help: 首页显示"欢迎来自 XX·XX·XX 的朋友"的浮动提示弹窗,基于访客 IP 地理定位
- $formkit: select
name: language
label: 语言
name: welcome_popup_position
id: welcome_popup_position
label: 弹窗位置
options:
- label: 跟随浏览器
value: auto
- label: 简体中文
value: zh_CN
- label: 繁體中文
value: zh_TW
- label: English
value: en
value: auto
help: 设置主题界面语言,选择"跟随浏览器"将根据浏览器语言自动匹配
- label: 顶端漂窗(顶部居中)
value: top-center
- label: 右上
value: top-right
- label: 左上
value: top-left
- label: 右下
value: bottom-right
- label: 左下
value: bottom-left
value: top-center
if: "$get(welcome_popup_enabled).value === true"
- $formkit: number
name: welcome_popup_duration
id: welcome_popup_duration
label: 自动消失时间(秒)
value: 5
min: 3
max: 60
step: 1
help: 弹窗自动消失的秒数,范围 3-60
if: "$get(welcome_popup_enabled).value === true"
- $formkit: url
name: welcome_popup_api
id: welcome_popup_api
label: IP 定位主 API
value: "https://myip.ipip.net/json"
help: 返回 JSON 的 IP 定位主接口,默认 IPIP;留空使用默认值。支持 IPIP/腾讯/ipapi.co 等常见格式
if: "$get(welcome_popup_enabled).value === true"
- $formkit: url
name: welcome_popup_fallback_api
id: welcome_popup_fallback_api
label: IP 定位备选 API
value: "https://r.inews.qq.com/api/ip2city?otype=json"
help: 主接口失败时尝试的备选接口,默认腾讯 ip2city;留空则不使用备选
if: "$get(welcome_popup_enabled).value === true"
- $formkit: switch
name: welcome_popup_show_once
id: welcome_popup_show_once
label: 同会话仅显示一次
value: true
help: 开启后同一浏览器会话内不再重复弹出;关闭则每次访问首页都显示
if: "$get(welcome_popup_enabled).value === true"
- group: style
label: 总体样式
formSchema:
@@ -102,6 +145,37 @@ spec:
name: animation_cursor_glow
label: 光感跟随
value: false
- $formkit: switch
name: click_effect_enabled
id: click_effect_enabled
label: 鼠标点击效果
value: false
help: 在页面上点击鼠标时触发的视觉特效
- $formkit: select
name: click_effect_type
id: click_effect_type
label: 点击效果类型
value: off
if: "$get(click_effect_enabled).value === true"
options:
- label: 关闭
value: off
- label: 社会主义核心价值观
value: values
- label: 随机彩色爱心
value: hearts
- label: 烟花
value: fireworks
- label: 烟花波纹
value: ripples
- label: 粒子
value: particles
- label: 星星
value: stars
- label: 星星拖尾
value: star-trail
- label: 粒子拖尾
value: particle-trail
- group: navbar
label: 导航栏
formSchema:
@@ -116,6 +190,35 @@ spec:
- label: Float
value: float
value: glass
- $formkit: select
name: navbar_brand_type
id: navbar_brand_type
label: 左侧显示
options:
- label: 站点名称
value: title
- label: Logo
value: logo
value: title
- $formkit: attachment
name: navbar_logo
id: navbar_logo
label: 导航栏 Logo
help: 选择"Logo"模式后在此上传图片;留空则使用基础设置中的自定义 Logo
if: "$get(navbar_brand_type).value === 'logo'"
- $formkit: select
name: navbar_logo_radius
id: navbar_logo_radius
label: Logo 圆角
options:
- label: 直角
value: square
- label: 圆角
value: rounded
- label: 圆形
value: circle
value: rounded
if: "$get(navbar_brand_type).value === 'logo'"
- $formkit: switch
name: navbar_show_search
label: 显示搜索按钮
@@ -188,9 +291,53 @@ spec:
label: 自定义描述文案
key: hero_custom_description
if: "$get(hero_description_mode).value === 'custom'"
- $formkit: switch
name: hero_hitokoto_auto_refresh
id: hero_hitokoto_auto_refresh
label: 一言自动刷新
value: false
help: 开启后按设定间隔自动刷新一言内容
- $formkit: select
name: hero_hitokoto_refresh_interval
label: 一言刷新间隔
if: "$get(hero_hitokoto_auto_refresh).value === true"
options:
- label: 5 秒
value: 5000
- label: 10 秒
value: 10000
- label: 15 秒
value: 15000
- label: 20 秒
value: 20000
- label: 25 秒
value: 25000
- label: 30 秒
value: 30000
value: 10000
- $formkit: attachment
name: hero_background_image
label: 背景图片
- $formkit: attachment
name: hero_background_image_dark
label: 深色模式背景图片
help: 深色模式下显示的背景图;留空则深色模式也使用上方背景图
- $formkit: color
name: hero_title_color_light
label: 浅色模式大标题颜色
help: Hero 大标题文字颜色(浅色模式);留空使用主题默认
- $formkit: color
name: hero_title_color_dark
label: 深色模式大标题颜色
help: Hero 大标题文字颜色(深色模式);留空使用主题默认
- $formkit: color
name: hero_subtitle_color_light
label: 浅色模式副标题颜色
help: Hero 副标题和一言文字颜色(浅色模式);留空使用主题默认
- $formkit: color
name: hero_subtitle_color_dark
label: 深色模式副标题颜色
help: Hero 副标题和一言文字颜色(深色模式);留空使用主题默认
- group: home
label: 首页
formSchema:
@@ -304,6 +451,16 @@ spec:
- label: Cold
value: cold
value: warm
- $formkit: switch
name: article_extra_spacing
label: 中英文混排自动加空格
value: true
help: 依赖「API 扩展包」插件,在服务端为正文自动添加中英文间距;插件未安装时自动跳过
- $formkit: switch
name: article_extra_highlight
label: 使用服务端代码高亮
value: false
help: 依赖「API 扩展包」全量版(含 JS 运行时),启用后由服务端渲染代码高亮并替代主题自带样式;如环境不支持请勿开启
- $formkit: select
name: article_toc_expand
label: 目录展开模式
@@ -371,6 +528,10 @@ spec:
- label: Masonry
value: masonry
value: cards
- $formkit: attachment
name: moments_cover
label: 顶部封面图
help: 朋友圈样式顶部的封面图片,建议尺寸 1500×500
- $formkit: select
name: moments_loading_mode
label: 加载方式
@@ -432,12 +593,75 @@ spec:
- label: Cards
value: cards
value: cards
- $formkit: switch
name: links_show_comment
label: 显示评论
value: false
help: 默认关闭评论功能,开启后在友情链接页面底部显示评论区
- $formkit: switch
name: links_random_order
label: 随机排列
value: false
help: 开启后友情链接卡片随机排列顺序
- $formkit: switch
name: links_random_portal
label: 随机传送门按钮
value: true
help: 开启后页面右上角显示"随机传送"按钮,点击随机访问一位博友
- group: subscriptions
label: 订阅
formSchema:
- $formkit: text
name: subscriptions_page_title
label: 页面标题
placeholder: "订阅"
- $formkit: text
name: subscriptions_subtitle
label: 页面副标题
placeholder: "博友们的最新文章"
- $formkit: number
name: subscriptions_page_size
label: 每页显示数量
value: 20
min: 1
max: 100
help: 单页展示的订阅动态条数,范围 1-100
- $formkit: select
name: subscriptions_loading_mode
label: 加载方式
options:
- label: 分页
value: pagination
- label: 下滑加载
value: infinite_scroll
value: pagination
- $formkit: select
name: subscriptions_layout
label: 布局样式
options:
- label: 列表卡片
value: list
- label: 网格卡片
value: grid
value: list
help: 网格卡片布局一行显示3个,内容区域更宽
- group: friends
label: 朋友圈
formSchema:
- $formkit: text
name: friends_page_title
label: 页面标题
- group: weread
label: 微信读书
formSchema:
- $formkit: text
name: weread_page_title
label: 页面标题
placeholder: "书架"
- $formkit: text
name: weread_page_subtitle
label: 页面副标题
placeholder: "我在读的书"
- group: messageboard
label: 留言板
formSchema:
@@ -469,9 +693,61 @@ spec:
- $formkit: text
name: footer_copyright
label: 版权信息(留空则使用默认格式 © 年份 站点标题)
- $formkit: switch
name: footer_show_stats
id: footer_show_stats
label: 显示全站统计
value: true
help: 在页脚展示文章数、分类数、评论数、访问量等站点统计数据
- $formkit: checkbox
name: footer_stats_items
id: footer_stats_items
label: 统计项
if: "$get(footer_show_stats).value !== false"
options:
- label: 文章数
value: post
- label: 分类数
value: category
- label: 评论数
value: comment
- label: 访问量
value: visit
- label: 点赞数
value: upvote
- label: 总字数
value: word
value:
- post
- category
- comment
- visit
help: 总字数依赖「API 扩展包」插件,未安装时自动隐藏该项
- $formkit: switch
name: footer_show_icp
label: 显示 ICP 备案号
value: false
help: 开启后页脚显示 ICP 备案链接
- $formkit: text
name: footer_icp
label: ICP 备案号
help: 工信部ICP备案号,填写后页脚显示备案链接(指向 beian.miit.gov.cn)
if: "$get(footer_show_icp).value === true"
- $formkit: switch
name: footer_show_mps
label: 显示公网安备号
value: false
help: 开启后页脚显示公网安备图标和链接
- $formkit: text
name: footer_mps
label: 公网安备号
help: 公安部联网备案号,填写后页脚显示备案图标和链接(指向 beian.mps.gov.cn)
if: "$get(footer_show_mps).value === true"
- $formkit: text
name: footer_mps_url
label: 公网安备跳转链接
help: 留空时默认指向 https://beian.mps.gov.cn/ ,可填写其他备案查询链接
if: "$get(footer_show_mps).value === true"
- $formkit: repeater
name: footer_socials
label: 社交链接
+243
View File
@@ -0,0 +1,243 @@
---
---
<div id="wi-click-effect" th:if="${theme.config?.style?.click_effect_enabled == true}" th:attr="data-effect=${theme.config?.style?.click_effect_type}">
<script is:inline th:inline="none">
(function () {
var container = document.getElementById("wi-click-effect");
if (!container) return;
var effect = container.getAttribute("data-effect") || "off";
if (effect === "off" || !effect) return;
/* 社会主义核心价值观 */
var CORE_VALUES = [
"富强", "民主", "文明", "和谐",
"自由", "平等", "公正", "法治",
"爱国", "敬业", "诚信", "友善"
];
var RANDOM_COLORS = [
"#ff6b6b", "#f06595", "#cc5de8", "#845ef7",
"#5c7cfa", "#339af0", "#22b8cf", "#20c997",
"#51cf66", "#94d82d", "#fcc419", "#ff922b"
];
/* 创建单个 DOM 元素并加入舞台 */
function spawn(el, x, y) {
el.style.left = x + "px";
el.style.top = y + "px";
container.appendChild(el);
}
/* ========== 1. 社会主义核心价值观 ========== */
function spawnValueText(x, y) {
var el = document.createElement("span");
el.className = "wi-click-fx wi-click-fx--text";
el.textContent = CORE_VALUES[Math.floor(Math.random() * CORE_VALUES.length)];
el.style.color = RANDOM_COLORS[Math.floor(Math.random() * RANDOM_COLORS.length)];
spawn(el, x, y);
setTimeout(function () {
if (el.parentNode) el.parentNode.removeChild(el);
}, 1200);
}
/* ========== 2. 随机彩色爱心 ========== */
function spawnHeart(x, y) {
var el = document.createElement("span");
el.className = "wi-click-fx wi-click-fx--heart";
el.innerHTML = "&#10084;";
el.style.color = RANDOM_COLORS[Math.floor(Math.random() * RANDOM_COLORS.length)];
var dx = (Math.random() - 0.5) * 80;
spawn(el, x, y);
requestAnimationFrame(function () {
el.style.transform = "translate(" + dx + "px, -80px) scale(0.2)";
el.style.opacity = "0";
});
setTimeout(function () {
if (el.parentNode) el.parentNode.removeChild(el);
}, 1000);
}
/* ========== 3. 烟花 ========== */
function spawnFirework(x, y) {
var count = 16;
var color = RANDOM_COLORS[Math.floor(Math.random() * RANDOM_COLORS.length)];
for (var i = 0; i < count; i++) {
var el = document.createElement("span");
el.className = "wi-click-fx wi-click-fx--spark";
el.style.background = color;
el.style.boxShadow = "0 0 6px " + color;
var angle = (Math.PI * 2 * i) / count;
var dist = 40 + Math.random() * 30;
var dx = Math.cos(angle) * dist;
var dy = Math.sin(angle) * dist;
spawn(el, x, y);
requestAnimationFrame(function (e, tx, ty) {
return function () {
e.style.transform = "translate(" + tx + "px, " + ty + "px) scale(0)";
e.style.opacity = "0";
};
}(el, dx, dy));
(function (e) {
setTimeout(function () {
if (e.parentNode) e.parentNode.removeChild(e);
}, 800);
})(el);
}
}
/* ========== 4. 烟花波纹 ========== */
function spawnRipple(x, y) {
var color = RANDOM_COLORS[Math.floor(Math.random() * RANDOM_COLORS.length)];
for (var r = 0; r < 2; r++) {
var el = document.createElement("span");
el.className = "wi-click-fx wi-click-fx--ripple";
el.style.borderColor = color;
spawn(el, x, y);
(function (e, delay) {
setTimeout(function () {
requestAnimationFrame(function () {
e.style.transform = "translate(-50%, -50%) scale(4)";
e.style.opacity = "0";
});
}, delay);
setTimeout(function () {
if (e.parentNode) e.parentNode.removeChild(e);
}, delay + 700);
})(el, r * 150);
}
}
/* ========== 5. 粒子 ========== */
function spawnParticles(x, y, n) {
n = n || 8;
for (var i = 0; i < n; i++) {
var el = document.createElement("span");
el.className = "wi-click-fx wi-click-fx--particle";
el.style.background = RANDOM_COLORS[Math.floor(Math.random() * RANDOM_COLORS.length)];
var dx = (Math.random() - 0.5) * 60;
var dy = (Math.random() - 0.5) * 60 - 20;
spawn(el, x, y);
requestAnimationFrame(function (e, tx, ty) {
return function () {
e.style.transform = "translate(" + tx + "px, " + ty + "px) scale(0)";
e.style.opacity = "0";
};
}(el, dx, dy));
(function (e) {
setTimeout(function () {
if (e.parentNode) e.parentNode.removeChild(e);
}, 700);
})(el);
}
}
/* ========== 6. 星星 ========== */
function spawnStar(x, y) {
var el = document.createElement("span");
el.className = "wi-click-fx wi-click-fx--star";
el.innerHTML = "&#9733;";
el.style.color = RANDOM_COLORS[Math.floor(Math.random() * RANDOM_COLORS.length)];
var dx = (Math.random() - 0.5) * 50;
var dy = (Math.random() - 0.5) * 50 - 30;
spawn(el, x, y);
requestAnimationFrame(function () {
el.style.transform = "translate(" + dx + "px, " + dy + "px) scale(1.6)";
el.style.opacity = "0";
});
setTimeout(function () {
if (el.parentNode) el.parentNode.removeChild(el);
}, 800);
}
/* 点击分发 */
var clickHandlers = {
"values": spawnValueText,
"hearts": spawnHeart,
"fireworks": spawnFirework,
"ripples": spawnRipple,
"particles": function (x, y) { spawnParticles(x, y, 10); },
"stars": spawnStar
};
/* 拖尾效果 */
var trailHandlers = {
"star-trail": function (x, y) {
var el = document.createElement("span");
el.className = "wi-click-fx wi-click-fx--trail-star";
el.innerHTML = "&#9733;";
el.style.color = RANDOM_COLORS[Math.floor(Math.random() * RANDOM_COLORS.length)];
spawn(el, x, y);
setTimeout(function () {
if (el.parentNode) el.parentNode.removeChild(el);
}, 600);
},
"particle-trail": function (x, y) {
var el = document.createElement("span");
el.className = "wi-click-fx wi-click-fx--trail-particle";
el.style.background = RANDOM_COLORS[Math.floor(Math.random() * RANDOM_COLORS.length)];
spawn(el, x, y);
setTimeout(function () {
if (el.parentNode) el.parentNode.removeChild(el);
}, 500);
}
};
var isTrail = effect.indexOf("trail") !== -1;
if (isTrail) {
var handler = trailHandlers[effect];
if (!handler) return;
var lastX = 0, lastY = 0;
var throttle = false;
document.addEventListener("mousemove", function (e) {
lastX = e.clientX;
lastY = e.clientY;
if (throttle) return;
throttle = true;
requestAnimationFrame(function () {
throttle = false;
handler(lastX, lastY);
});
}, { passive: true });
} else {
var handler = clickHandlers[effect];
if (!handler) return;
document.addEventListener("click", function (e) {
handler(e.clientX, e.clientY);
}, { passive: true });
}
})();
</script>
<style is:global>
.wi-click-fx{position:fixed;pointer-events:none;will-change:transform,opacity
}
.wi-click-fx--text{font-size:18px;font-weight:700;transform:translate(-50%,-50%);opacity:0;animation:wi-fx-text 1.2s ease-out forwards
}
.wi-click-fx--heart{font-size:20px;transform:translate(-50%,-50%) scale(1);opacity:1;transition:transform 1s cubic-bezier(.16,1,.3,1),opacity 1s ease-out
}
.wi-click-fx--spark{width:6px;height:6px;border-radius:50%;transform:translate(-50%,-50%) scale(1);opacity:1;transition:transform .8s cubic-bezier(.16,1,.3,1),opacity .8s ease-out
}
.wi-click-fx--ripple{width:30px;height:30px;border:2px solid;border-radius:50%;transform:translate(-50%,-50%) scale(.5);opacity:1;transition:transform .7s ease-out,opacity .7s ease-out
}
.wi-click-fx--particle{width:8px;height:8px;border-radius:50%;transform:translate(-50%,-50%) scale(1);opacity:1;transition:transform .7s cubic-bezier(.16,1,.3,1),opacity .7s ease-out
}
.wi-click-fx--star{font-size:16px;transform:translate(-50%,-50%) scale(1);opacity:1;transition:transform .8s cubic-bezier(.16,1,.3,1),opacity .8s ease-out
}
.wi-click-fx--trail-star{font-size:12px;transform:translate(-50%,-50%) scale(1);opacity:.8;transition:opacity .6s ease-out,transform .6s ease-out
}
.wi-click-fx--trail-particle{width:6px;height:6px;border-radius:50%;transform:translate(-50%,-50%) scale(1);opacity:.8;transition:opacity .5s ease-out,transform .5s ease-out
}
@keyframes wi-fx-text{0%{opacity:0;transform:translate(-50%,-50%) scale(.5)}20%{opacity:1;transform:translate(-50%,-80%) scale(1.1)}to{opacity:0;transform:translate(-50%,-120%) scale(.9)}}
</style>
+1 -1
View File
@@ -3,7 +3,7 @@
<div class="wi-cursor-glow" id="wi-cursor-glow"></div>
<script is:inline>
<script is:inline th:inline="none">
(function(){
var glowEl = document.getElementById("wi-cursor-glow");
if (!glowEl) return;
+2 -2
View File
@@ -8,7 +8,7 @@
>
<div class="wi-container">
<div class="wi-section__header">
<h2 class="wi-section__title" th:text="${theme.config?.home?.home_featured_title ?: #messages.msg('common.featured')}">精选</h2>
<h2 class="wi-section__title" th:text="${theme.config?.home?.home_featured_title ?: '精选'}">精选</h2>
</div>
<div class="wi-featured">
<a
@@ -32,7 +32,7 @@
<span
class="wi-featured__pinned"
th:if="${post.spec.pinned}"
th:text="#{hero.pinned}">Pinned</span>
th:text="'置顶'">置顶</span>
</div>
<h3
class="wi-featured__title"
+107 -6
View File
@@ -1,19 +1,42 @@
---
const THEME_VERSION = "1.0.0";
const today = new Date();
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
// 版本号直接读取 theme.yaml,避免与包版本不同步
function readThemeVersion() {
try {
const yaml = readFileSync(resolve(process.cwd(), "theme.yaml"), "utf-8");
return yaml.match(/^\s*version:\s*"?([^"\s]+)"?/m)?.[1] ?? "";
} catch {
return "";
}
}
const THEME_VERSION = readThemeVersion();
---
<footer th:with="showPowered=${theme.config?.footer?.footer_show_powered != false}, showTheme=${theme.config?.footer?.footer_show_theme != false}, hideRight=${showPowered == false and showTheme == false}">
<footer
th:with="showPowered=${theme.config?.footer?.footer_show_powered != false}, showTheme=${theme.config?.footer?.footer_show_theme != false}, hideRight=${showPowered == false and showTheme == false}, icpText=${theme.config?.footer?.footer_icp}, mpsText=${theme.config?.footer?.footer_mps}, showIcp=${theme.config?.footer?.footer_show_icp == true and not #strings.isEmpty(#strings.trim(icpText ?: ''))}, showMps=${theme.config?.footer?.footer_show_mps == true and not #strings.isEmpty(#strings.trim(mpsText ?: ''))}"
>
<div class="footer-inner">
<div class="footer-bottom" th:classappend="${hideRight} ? 'footer-bottom--center' : ''">
<div class="footer-bottom__left">
<span class="footer-copyright" th:if="${theme.config?.footer?.footer_copyright}" th:text="${theme.config?.footer?.footer_copyright}"></span>
<span class="footer-copyright" th:unless="${theme.config?.footer?.footer_copyright}">
© {today.getFullYear()}
© <th:block th:text="${#dates.format(#dates.createNow(), 'yyyy')}">2026</th:block>
<th:block th:text="${theme.config?.basic?.owner_name ?: site.title}"></th:block>
</span>
<span class="footer-icp" th:if="${theme.config?.footer?.footer_icp}">
<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener noreferrer" th:text="${theme.config?.footer?.footer_icp}"></a>
<span class="footer-icp" th:if="${showIcp}">
<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener noreferrer" th:text="${#strings.trim(icpText)}"></a>
</span>
<span class="footer-mps" th:if="${showMps}" th:with="mpsUrl=${theme.config?.footer?.footer_mps_url ?: 'https://beian.mps.gov.cn/'}">
<a th:href="${mpsUrl}" target="_blank" rel="noopener noreferrer">
<svg class="footer-mps__icon" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M12 2L2 7v6c0 5.5 3.8 10.7 10 12 6.2-1.3 10-6.5 10-12V7l-10-5z"></path>
<path d="M9.5 12.5l2 2 3.5-4" stroke="#fff" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round"></path>
</svg>
<span th:text="${#strings.trim(mpsText)}"></span>
</a>
</span>
</div>
<div class="footer-socials" th:if="${theme.config?.footer?.footer_socials}">
@@ -34,6 +57,39 @@ const today = new Date();
<span th:if="${showTheme}">WarmIsland v{THEME_VERSION}</span>
</div>
</div>
<div
class="footer-stats"
th:if="${theme.config?.footer?.footer_show_stats != false}"
th:with="stats=${siteStatsFinder.getStats()}, items=${theme.config?.footer?.footer_stats_items ?: {'post','category','comment','visit'}}"
>
<span class="footer-stat" th:if="${#lists.contains(items, 'post')}">
<span class="footer-stat__value" th:text="${stats.post ?: 0}">0</span>
<span class="footer-stat__label">文章</span>
</span>
<span class="footer-stat" th:if="${#lists.contains(items, 'category')}">
<span class="footer-stat__value" th:text="${stats.category ?: 0}">0</span>
<span class="footer-stat__label">分类</span>
</span>
<span class="footer-stat" th:if="${#lists.contains(items, 'comment')}">
<span class="footer-stat__value" th:text="${stats.comment ?: 0}">0</span>
<span class="footer-stat__label">评论</span>
</span>
<span class="footer-stat" th:if="${#lists.contains(items, 'visit')}">
<span class="footer-stat__value" th:text="${stats.visit ?: 0}">0</span>
<span class="footer-stat__label">访问</span>
</span>
<span class="footer-stat" th:if="${#lists.contains(items, 'upvote')}">
<span class="footer-stat__value" th:text="${stats.upvote ?: 0}">0</span>
<span class="footer-stat__label">点赞</span>
</span>
<span
class="footer-stat"
th:if="${#lists.contains(items, 'word') and pluginFinder.available('extra-api')}"
>
<span class="footer-stat__value" th:text="${extraApiStatsFinder.getPostWordCount()}">0</span>
<span class="footer-stat__label">字数</span>
</span>
</div>
<div th:if="${theme.config?.footer?.footer_custom_html}" th:utext="${theme.config?.footer?.footer_custom_html}" class="footer-custom"></div>
<div class="footer-injected">
<halo:footer />
@@ -104,6 +160,51 @@ const today = new Date();
color: var(--ink-2);
}
.footer-mps a {
display: inline-flex;
align-items: center;
gap: 0.3rem;
color: var(--ink-3);
text-decoration: none;
transition: color 0.15s ease;
}
.footer-mps a:hover {
color: var(--ink-2);
}
.footer-mps__icon {
flex: none;
vertical-align: middle;
}
.footer-stats {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
gap: 0.5rem 1.25rem;
width: 100%;
font-size: 0.8rem;
color: var(--ink-3);
}
.footer-stat {
display: inline-flex;
align-items: baseline;
gap: 0.3rem;
}
.footer-stat__value {
font-weight: 600;
color: var(--ink-2);
font-variant-numeric: tabular-nums;
}
.footer-stat__label {
opacity: 0.8;
}
.footer-socials {
display: inline-flex;
align-items: center;
+119 -11
View File
@@ -1,6 +1,11 @@
---
---
<style is:inline th:if="${theme.config?.hero?.hero_title_color_light != null and theme.config?.hero?.hero_title_color_light != ''}" th:utext="${' :root { --hero-title-text: ' + theme.config?.hero?.hero_title_color_light + '; } '}">/*hero-title-light*/</style>
<style is:inline th:if="${theme.config?.hero?.hero_title_color_dark != null and theme.config?.hero?.hero_title_color_dark != ''}" th:utext="${' html.dark { --hero-title-text: ' + theme.config?.hero?.hero_title_color_dark + '; } '}">/*hero-title-dark*/</style>
<style is:inline th:if="${theme.config?.hero?.hero_subtitle_color_light != null and theme.config?.hero?.hero_subtitle_color_light != ''}" th:utext="${' :root { --hero-subtitle-text: ' + theme.config?.hero?.hero_subtitle_color_light + '; } '}">/*hero-subtitle-light*/</style>
<style is:inline th:if="${theme.config?.hero?.hero_subtitle_color_dark != null and theme.config?.hero?.hero_subtitle_color_dark != ''}" th:utext="${' html.dark { --hero-subtitle-text: ' + theme.config?.hero?.hero_subtitle_color_dark + '; } '}">/*hero-subtitle-dark*/</style>
<section
class="hero"
id="wi-hero"
@@ -13,9 +18,24 @@
<div class="hero__orb hero__orb--3"></div>
<div
class="hero__bg-image"
th:if="${theme.config?.hero?.hero_background_image}"
th:if="${!#strings.isEmpty(theme.config?.hero?.hero_background_image) and #strings.isEmpty(theme.config?.hero?.hero_background_image_dark)}"
th:style="'background-image: url(' + ${theme.config?.hero?.hero_background_image} + ')'"
></div>
<div
class="hero__bg-image"
th:if="${#strings.isEmpty(theme.config?.hero?.hero_background_image) and !#strings.isEmpty(theme.config?.hero?.hero_background_image_dark)}"
th:style="'background-image: url(' + ${theme.config?.hero?.hero_background_image_dark} + ')'"
></div>
<div
class="hero__bg-image hero__bg-image--light"
th:if="${!#strings.isEmpty(theme.config?.hero?.hero_background_image) and !#strings.isEmpty(theme.config?.hero?.hero_background_image_dark)}"
th:style="'background-image: url(' + ${theme.config?.hero?.hero_background_image} + ')'"
></div>
<div
class="hero__bg-image hero__bg-image--dark"
th:if="${!#strings.isEmpty(theme.config?.hero?.hero_background_image) and !#strings.isEmpty(theme.config?.hero?.hero_background_image_dark)}"
th:style="'background-image: url(' + ${theme.config?.hero?.hero_background_image_dark} + ')'"
></div>
</div>
<div class="hero__blur-overlay" id="wi-hero-blur-overlay"></div>
@@ -37,7 +57,7 @@
class="hero__description"
th:if="${theme.config?.hero?.hero_description_mode == 'hitokoto'}"
id="wi-hitokoto"
th:attr="data-api=${theme.config?.hero?.hero_hitokoto_api ?: 'https://v1.hitokoto.cn/'},data-categories=${theme.config?.hero?.hero_hitokoto_categories}"
th:attr="data-api=${theme.config?.hero?.hero_hitokoto_api ?: 'https://v1.hitokoto.cn/'},data-categories=${theme.config?.hero?.hero_hitokoto_categories},data-auto-refresh=${theme.config?.hero?.hero_hitokoto_auto_refresh == true ? 'true' : 'false'},data-refresh-interval=${theme.config?.hero?.hero_hitokoto_refresh_interval ?: 10000}"
></p>
<p
class="hero__description"
@@ -46,7 +66,12 @@
></p>
</div>
<div class="hero__scroll-indicator" id="wi-hero-scroll-indicator" aria-hidden="true">
<button
class="hero__scroll-indicator"
id="wi-hero-scroll-indicator"
type="button"
aria-label="向下浏览"
>
<svg
width="24"
height="24"
@@ -56,21 +81,58 @@
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</div>
</button>
</section>
<script is:inline>
<script is:inline th:inline="none">
/* Hero 下滑箭头:平滑滚动到首屏内容区 */
(function () {
var indicator = document.getElementById("wi-hero-scroll-indicator");
if (!indicator) return;
indicator.addEventListener("click", function () {
var content = document.querySelector(".wi-home-content");
var hero = document.getElementById("wi-hero");
var top;
if (content) {
top = content.getBoundingClientRect().top + window.pageYOffset;
} else if (hero) {
top = hero.offsetTop + hero.offsetHeight;
} else {
top = window.innerHeight;
}
/* 预留固定导航栏的高度,避免内容被顶栏遮住 */
var navbar = document.getElementById("wi-navbar");
var offset = navbar ? navbar.offsetHeight + 8 : 8;
var reduce =
window.matchMedia &&
window.matchMedia("(prefers-reduced-motion: reduce)").matches;
window.scrollTo({
top: Math.max(0, top - offset),
behavior: reduce ? "auto" : "smooth",
});
});
})();
(function () {
var el = document.getElementById("wi-hitokoto");
if (!el) return;
var api = el.getAttribute("data-api") || "https://v1.hitokoto.cn/";
var categories = el.getAttribute("data-categories");
var url = api + "?encode=json";
var autoRefresh = el.getAttribute("data-auto-refresh") === "true";
var refreshInterval = parseInt(el.getAttribute("data-refresh-interval"), 10) || 10000;
function buildUrl() {
var url = api + "?encode=json";
if (categories) {
try {
var cats = JSON.parse(categories);
@@ -81,11 +143,16 @@
}
} catch (e) {}
}
return url;
}
function load(initial) {
if (initial) {
el.style.opacity = "0.5";
el.textContent = "\u2026";
}
fetch(url)
return fetch(buildUrl())
.then(function (res) {
if (!res.ok) throw new Error("HTTP " + res.status);
return res.json();
@@ -101,8 +168,19 @@
})
.catch(function () {
el.style.opacity = "1";
el.textContent = "";
// 自动刷新失败时保留原文案,避免闪烁成空白
if (initial) el.textContent = "";
});
}
load(true);
// 自动刷新:仅在开启且间隔不小于 3 秒时启用,避免请求过于频繁
if (autoRefresh && refreshInterval >= 3000) {
setInterval(function () {
load(false);
}, refreshInterval);
}
})();
</script>
@@ -199,6 +277,16 @@
opacity: 0.15;
}
/* 深色模式背景图切换:未配置双图时不生效,保持单一背景图 */
.hero__bg-image--dark,
html.dark .hero__bg-image--light {
display: none;
}
html.dark .hero__bg-image--dark {
display: block;
}
.hero__content {
position: relative;
z-index: 2;
@@ -216,14 +304,14 @@
font-size: clamp(3rem, 8vw, 6rem);
font-weight: 700;
letter-spacing: -0.03em;
color: var(--ink);
color: var(--hero-title-text, var(--ink));
line-height: var(--leading-tight);
}
.hero__subtitle {
font-family: var(--font-body);
font-size: var(--text-xl);
color: var(--ink-2);
color: var(--hero-subtitle-text, var(--ink-2));
letter-spacing: 0.05em;
line-height: var(--leading-normal);
}
@@ -243,9 +331,29 @@
left: 50%;
transform: translateX(-50%);
z-index: 2;
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
padding: 0;
border: none;
border-radius: 50%;
background: transparent;
color: var(--ink-3);
cursor: pointer;
animation: scrollHint 2s ease-in-out infinite;
transition: opacity 0.4s ease;
transition: opacity 0.4s ease, color 0.2s ease, background 0.2s ease;
}
.hero__scroll-indicator:hover {
color: var(--accent);
background: color-mix(in srgb, var(--accent) 10%, transparent);
}
.hero__scroll-indicator:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.hero--no-breath .hero__orb {
+3 -3
View File
@@ -7,7 +7,7 @@
>
<div class="wi-container">
<div class="wi-section__header">
<h2 class="wi-section__title" th:text="${theme.config?.home?.home_latest_title ?: #messages.msg('common.latestPosts')}">Latest Posts</h2>
<h2 class="wi-section__title" th:text="${theme.config?.home?.home_latest_title ?: '最新文章'}">最新文章</h2>
</div>
<div
class="wi-latest"
@@ -58,7 +58,7 @@
th:if="${posts.hasPrevious()}"
th:href="@{${posts.prevUrl}}"
class="wi-pagination__prev"
th:text="${theme.config?.home?.home_label_newer ?: #messages.msg('common.newer')}"
th:text="${theme.config?.home?.home_label_newer ?: '较新'}"
>&larr; Newer</a>
<span
class="wi-pagination__info"
@@ -68,7 +68,7 @@
th:if="${posts.hasNext()}"
th:href="@{${posts.nextUrl}}"
class="wi-pagination__next"
th:text="${theme.config?.home?.home_label_older ?: #messages.msg('common.older')}"
th:text="${theme.config?.home?.home_label_older ?: '较旧'}"
>Older &rarr;</a>
</nav>
</div>
+1 -1
View File
@@ -6,7 +6,7 @@
>
<div class="wi-container">
<div class="wi-section__header">
<h2 class="wi-section__title" th:text="${theme.config?.home?.home_message_wall_title ?: #messages.msg('messageboard.title')}">留言墙</h2>
<h2 class="wi-section__title" th:text="${theme.config?.home?.home_message_wall_title ?: '留言板'}">留言墙</h2>
<span class="wi-section__accent"></span>
</div>
+3 -3
View File
@@ -35,19 +35,19 @@
onclick="SearchWidget.open()"
>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
<span th:text="${theme.config?.basic?.label_search ?: #messages.msg('common.search')}">Search</span>
<span th:text="${theme.config?.basic?.label_search ?: '搜索'}">搜索</span>
</button>
<button class="wi-mobile-menu__action-btn wi-mobile-menu__theme-btn" type="button" aria-label="Toggle theme">
<svg class="wi-mobile-menu__icon-moon" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/></svg>
<svg class="wi-mobile-menu__icon-sun" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/></svg>
<span class="wi-mobile-menu__theme-label" th:text="${theme.config?.basic?.label_theme_switch ?: #messages.msg('common.themeSwitch')}">Switch Theme</span>
<span class="wi-mobile-menu__theme-label" th:text="${theme.config?.basic?.label_theme_switch ?: '切换主题'}">切换主题</span>
</button>
</div>
</div>
</div>
<script is:inline>
<script is:inline th:inline="none">
const menu = document.getElementById("wi-mobile-menu");
const backdrop = menu?.querySelector(".wi-mobile-menu__backdrop");
const closeBtn = menu?.querySelector(".wi-mobile-menu__close");
+2 -2
View File
@@ -9,7 +9,7 @@ const pageConfig = findPageBySlug("moments");
<th:block th:if="${pluginFinder.available('PluginMoments')}">
<section class="wi-moments-section">
<div class="wi-container">
<h2 class="wi-moments-section__title" th:text="${pageConfig?.title ?: theme.config?.moments?.moments_title ?: #messages.msg('moments.title')}">瞬间</h2>
<h2 class="wi-moments-section__title" th:text="${pageConfig?.title ?: theme.config?.moments?.moments_title ?: '动态'}">瞬间</h2>
<div
class="wi-moments-section__list"
th:attr="data-style=${theme.config?.moments?.moments_style ?: 'timeline'}"
@@ -29,7 +29,7 @@ const pageConfig = findPageBySlug("moments");
</th:block>
</div>
<a th:href="@{'/moments'}" class="wi-moments-section__more">
<span th:text="${theme.config?.home?.home_label_view_all ?: #messages.msg('common.viewAll')}">查看全部</span>
<span th:text="${theme.config?.home?.home_label_view_all ?: '查看全部'}">查看全部</span>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M5 12h14M12 5l7 7-7 7"/>
</svg>
+25 -9
View File
@@ -4,18 +4,22 @@ import ThemeSwitcher from "./ThemeSwitcher.astro";
<header class="wi-navbar-wrapper" id="wi-navbar" th:classappend="${theme.config?.navbar?.navbar_style == 'minimal'} ? 'wi-navbar--minimal' : (${theme.config?.navbar?.navbar_style == 'float'} ? 'wi-navbar--float' : 'wi-navbar--glass')">
<div class="wi-navbar__capsule">
<a class="wi-navbar__brand" th:href="@{/}">
<a
class="wi-navbar__brand"
th:href="@{/}"
th:with="brandType=${theme.config?.navbar?.navbar_brand_type}, logoUrl=${!#strings.isEmpty(theme.config?.navbar?.navbar_logo) ? theme.config?.navbar?.navbar_logo : theme.config?.basic?.logo}, logoRadius=${theme.config?.navbar?.navbar_logo_radius}"
>
<img
th:if="${theme.config?.basic?.logo}"
th:src="${theme.config?.basic?.logo}"
th:if="${brandType == 'logo' and !#strings.isEmpty(logoUrl)}"
th:src="${logoUrl}"
alt=""
class="wi-navbar__logo"
th:class="${'wi-navbar__logo wi-navbar__logo--' + (logoRadius != null ? logoRadius : 'rounded')}"
loading="eager"
decoding="async"
onerror="this.style.display='none';this.nextElementSibling&&this.nextElementSibling.classList.add('wi-navbar__title')"
onerror="this.style.display='none'"
/>
<span
th:if="${#strings.isEmpty(theme.config?.basic?.logo)}"
th:if="${brandType != 'logo' or #strings.isEmpty(logoUrl)}"
class="wi-navbar__title"
th:text="${site.title}"
></span>
@@ -47,14 +51,14 @@ import ThemeSwitcher from "./ThemeSwitcher.astro";
th:if="${pluginFinder.available('PluginSearchWidget') and theme.config?.navbar?.navbar_show_search != false}"
class="wi-navbar__action-btn wi-navbar__search-btn"
type="button"
th:aria-label="${theme.config?.basic?.label_search ?: #messages.msg('common.search')}"
th:aria-label="${theme.config?.basic?.label_search ?: '搜索'}"
onclick="SearchWidget.open()"
>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
</button>
<th:block th:if="${theme.config?.navbar?.navbar_show_theme_switch != false}">
<span th:attr="data-label=${theme.config?.basic?.label_theme_switch ?: #messages.msg('common.themeSwitch')}" style="display:inline-flex">
<span th:attr="data-label=${theme.config?.basic?.label_theme_switch ?: '切换主题'}" style="display:inline-flex">
<ThemeSwitcher />
</span>
</th:block>
@@ -70,7 +74,7 @@ import ThemeSwitcher from "./ThemeSwitcher.astro";
</div>
</header>
<script is:inline>
<script is:inline th:inline="none">
const navbar = document.getElementById("wi-navbar");
let lastScrollY = window.scrollY;
let ticking = false;
@@ -218,6 +222,18 @@ import ThemeSwitcher from "./ThemeSwitcher.astro";
object-fit: contain;
}
.wi-navbar__logo--square {
border-radius: 0;
}
.wi-navbar__logo--rounded {
border-radius: 8px;
}
.wi-navbar__logo--circle {
border-radius: 50%;
}
.wi-navbar__title {
font-family: var(--font-sans);
font-weight: 700;
+2 -2
View File
@@ -9,7 +9,7 @@ const pageConfig = findPageBySlug("photos");
<th:block th:if="${pluginFinder.available('PluginPhotos')}">
<section class="wi-photos-section">
<div class="wi-container">
<h2 class="wi-photos-section__title" th:text="${pageConfig?.title ?: theme.config?.photos?.photos_title ?: #messages.msg('photos.title')}">图库</h2>
<h2 class="wi-photos-section__title" th:text="${pageConfig?.title ?: theme.config?.photos?.photos_title ?: '相册'}">图库</h2>
<div
class="wi-photos-section__grid"
th:attr="data-style=${theme.config?.photos?.photos_style ?: 'masonry'}, data-columns=${theme.config?.photos?.photos_columns ?: 3}"
@@ -37,7 +37,7 @@ const pageConfig = findPageBySlug("photos");
</th:block>
</div>
<a th:href="@{'/photos'}" class="wi-photos-section__more">
<span th:text="${theme.config?.home?.home_label_view_all ?: #messages.msg('common.viewAll')}">查看全部</span>
<span th:text="${theme.config?.home?.home_label_view_all ?: '查看全部'}">查看全部</span>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M5 12h14M12 5l7 7-7 7"/>
</svg>
+1 -1
View File
@@ -1,7 +1,7 @@
---
---
<script is:inline>
<script is:inline th:inline="none">
(function(){
var enabled = !document.documentElement.classList.contains("wi-no-animations");
if (!enabled) {
-56
View File
@@ -1,56 +0,0 @@
<script setup lang="ts">
import { onMounted, onUnmounted } from "vue";
function handleKeydown(e: KeyboardEvent) {
if ((e.metaKey || e.ctrlKey) && e.key === "k") {
e.preventDefault();
const searchWidget = (window as any).SearchWidget;
if (searchWidget && typeof searchWidget.open === "function") {
searchWidget.open();
}
}
}
onMounted(() => {
document.addEventListener("keydown", handleKeydown);
});
onUnmounted(() => {
document.removeEventListener("keydown", handleKeydown);
});
</script>
<template>
<span class="wi-search-hint" aria-hidden="true">
<kbd class="wi-search-hint__key">⌘</kbd>
<kbd class="wi-search-hint__key">K</kbd>
</span>
</template>
<style scoped>
.wi-search-hint {
display: inline-flex;
align-items: center;
gap: 4px;
pointer-events: none;
user-select: none;
}
.wi-search-hint__key {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 22px;
height: 22px;
padding: 0 5px;
font-family: var(--font-body);
font-size: 11px;
font-weight: 500;
line-height: 1;
color: var(--ink-3);
background: var(--bg-raised);
border: 1px solid var(--rule);
border-radius: 6px;
box-shadow: 0 1px 0 var(--rule);
}
</style>
+1 -1
View File
@@ -24,7 +24,7 @@
</svg>
</button>
<script is:inline>
<script is:inline th:inline="none">
(function(){
var btn = document.getElementById("wi-theme-toggle");
if (!btn) return;
+1 -1
View File
@@ -6,7 +6,7 @@
>
<div class="wi-container">
<div class="wi-section__header">
<h2 class="wi-section__title" th:text="${theme.config?.home?.home_timeline_title ?: #messages.msg('common.timeline')}">时间线</h2>
<h2 class="wi-section__title" th:text="${theme.config?.home?.home_timeline_title ?: '时间线'}">时间线</h2>
<span class="wi-section__accent"></span>
</div>
+443
View File
@@ -0,0 +1,443 @@
---
---
<section
class="wi-welcome-popup"
id="wi-welcome-popup"
th:if="${theme.config?.welcome_popup?.welcome_popup_enabled == true}"
th:attr="data-position=${theme.config?.welcome_popup?.welcome_popup_position ?: 'top-center'},data-duration=${theme.config?.welcome_popup?.welcome_popup_duration ?: 5},data-api=${theme.config?.welcome_popup?.welcome_popup_api},data-fallback-api=${theme.config?.welcome_popup?.welcome_popup_fallback_api},data-show-once=${theme.config?.welcome_popup?.welcome_popup_show_once != false ? 'true' : 'false'}"
aria-live="polite"
aria-atomic="true"
>
<div class="wi-welcome-popup__inner">
<div class="wi-welcome-popup__icon" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z"></path>
<circle cx="12" cy="10" r="3"></circle>
</svg>
</div>
<div class="wi-welcome-popup__content" id="wi-welcome-popup-text">加载中…</div>
<button class="wi-welcome-popup__close" type="button" aria-label="关闭">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round">
<path d="M18 6 6 18"></path>
<path d="m6 6 12 12"></path>
</svg>
</button>
</div>
<div class="wi-welcome-popup__progress" id="wi-welcome-popup-progress" aria-hidden="true"></div>
</section>
<script is:inline th:inline="none">
(function () {
var popup = document.getElementById("wi-welcome-popup");
if (!popup) return;
/* 同会话已显示过则不再弹出(可通过"同会话仅显示一次"开关关闭) */
var sessionKey = "wi-welcome-shown";
var showOnce = popup.getAttribute("data-show-once") !== "false";
try {
if (showOnce && sessionStorage.getItem(sessionKey)) {
popup.style.display = "none";
return;
}
} catch (e) {}
var textEl = document.getElementById("wi-welcome-popup-text");
var closeBtn = popup.querySelector(".wi-welcome-popup__close");
var progress = document.getElementById("wi-welcome-popup-progress");
var duration = parseInt(popup.getAttribute("data-duration"), 10) || 5;
var api = popup.getAttribute("data-api") || "https://myip.ipip.net/json";
var fallbackApi = popup.getAttribute("data-fallback-api") || "https://r.inews.qq.com/api/ip2city?otype=json";
var hideTimer = null;
var progressRaf = null;
var progressStart = 0;
var isHidden = false;
function hidePopup() {
if (isHidden) return;
isHidden = true;
popup.classList.remove("wi-welcome-popup--visible");
popup.classList.add("wi-welcome-popup--leaving");
if (hideTimer) {
clearTimeout(hideTimer);
hideTimer = null;
}
if (progressRaf) {
cancelAnimationFrame(progressRaf);
progressRaf = null;
}
try {
if (showOnce) sessionStorage.setItem(sessionKey, "1");
} catch (e) {}
setTimeout(function () {
popup.style.display = "none";
}, 400);
}
function startAutoHide() {
progressStart = performance.now();
var step = function (now) {
var elapsed = (now - progressStart) / 1000;
var ratio = Math.min(elapsed / duration, 1);
if (progress) {
progress.style.transform = "scaleX(" + (1 - ratio) + ")";
}
if (ratio < 1 && !isHidden) {
progressRaf = requestAnimationFrame(step);
}
};
progressRaf = requestAnimationFrame(step);
hideTimer = setTimeout(hidePopup, duration * 1000);
}
function renderMessage(location) {
var loc = (location || "").trim();
if (!loc) loc = "未知星球";
if (textEl) {
textEl.textContent = "欢迎来自 " + loc + " 的朋友";
}
popup.classList.add("wi-welcome-popup--visible");
startAutoHide();
}
function fetchLocation() {
if (textEl) {
textEl.textContent = "加载中…";
}
popup.classList.add("wi-welcome-popup--visible");
/* 按顺序尝试主 API -> 备选 API */
var endpoints = [];
if (api) endpoints.push(api);
if (fallbackApi && fallbackApi !== api) endpoints.push(fallbackApi);
if (endpoints.length === 0) {
renderMessage("");
return;
}
var idx = 0;
function tryNext() {
if (idx >= endpoints.length) {
renderMessage("");
return;
}
var url = endpoints[idx++];
fetchSingle(url, 6000)
.then(function (location) {
if (location) {
renderMessage(location);
} else {
tryNext();
}
})
.catch(function () {
tryNext();
});
}
tryNext();
}
function fetchSingle(url, timeoutMs) {
var controller = null;
var timeoutId = null;
if (window.AbortController) {
controller = new AbortController();
timeoutId = setTimeout(function () {
controller.abort();
}, timeoutMs);
}
return fetch(url, controller ? { signal: controller.signal } : {})
.then(function (res) {
if (!res.ok) throw new Error("HTTP " + res.status);
return res.json();
})
.then(function (data) {
if (timeoutId) clearTimeout(timeoutId);
return parseLocation(data);
})
.catch(function (err) {
if (timeoutId) clearTimeout(timeoutId);
throw err;
});
}
/* 解析多种 IP API 的返回格式:
- IPIP: {"ret":"ok","data":{"location":["中国","河北","张家口","","联通"]}}
- 腾讯 ip2city: {"ret":0,"province":"河北","city":"张家口",...}
- ipapi.co / ip-api.com: {"country":"China","regionName":"Hebei","city":"Zhangjiakou"}
- 太平洋: {"pro":"广东省","city":"广州市",...} */
function parseLocation(data) {
if (!data) return "";
/* IPIP: data.data.location 数组 */
if (data.data && Array.isArray(data.data.location)) {
var loc = data.data.location
.filter(function (s) {
return s && String(s).trim();
})
.map(function (s) {
return String(s).trim();
});
/* 去掉末尾的运营商字段(如"联通""电信""移动") */
var ispKeywords = ["联通", "电信", "移动", "铁通", "广电", "教育网", "长城"];
while (loc.length > 1 && ispKeywords.indexOf(loc[loc.length - 1]) !== -1) {
loc.pop();
}
return loc.length ? loc.join("·") : "";
}
var country = data.country || data.country_name || data.nation || "";
var region = data.regionName || data.region || data.province || data.pro || "";
var city = data.city || data.cityName || "";
var district = data.district || data.area || data.info3 || "";
/* 短英文国家代码(如 CN)舍弃,避免显示无意义的两字母 */
if (country && country.length <= 3 && !/[一-龥]/.test(country)) {
country = "";
}
var parts = [];
if (country) parts.push(country);
if (region && region !== country) parts.push(region);
if (city && city !== region) parts.push(city);
if (district && district !== city) parts.push(district);
return parts.length ? parts.join("·") : "";
}
/* 交互:悬停/触摸暂停倒计时,离开恢复 */
function pauseTimer() {
if (hideTimer) {
clearTimeout(hideTimer);
hideTimer = null;
}
if (progressRaf) {
cancelAnimationFrame(progressRaf);
progressRaf = null;
}
}
function resumeTimer() {
var remaining = duration;
if (progress) {
var matrix = progress.style.transform;
var match = matrix && matrix.match(/scaleX\(([\d.]+)\)/);
if (match) {
var ratio = parseFloat(match[1]);
remaining = duration * ratio;
}
}
progressStart = performance.now() - (duration - remaining) * 1000;
var step = function (now) {
var elapsed = (now - progressStart) / 1000;
var ratio = Math.min(elapsed / duration, 1);
if (progress) {
progress.style.transform = "scaleX(" + (1 - ratio) + ")";
}
if (ratio < 1 && !isHidden) {
progressRaf = requestAnimationFrame(step);
}
};
progressRaf = requestAnimationFrame(step);
hideTimer = setTimeout(hidePopup, remaining * 1000);
}
popup.addEventListener("mouseenter", pauseTimer);
popup.addEventListener("mouseleave", resumeTimer);
popup.addEventListener("touchstart", pauseTimer, { passive: true });
popup.addEventListener("touchend", resumeTimer, { passive: true });
if (closeBtn) closeBtn.addEventListener("click", hidePopup);
/* 延迟 800ms 显示,避免与首屏渲染争抢资源 */
setTimeout(fetchLocation, 800);
})();
</script>
<style>
.wi-welcome-popup {
position: fixed;
z-index: 9999;
max-width: min(380px, calc(100vw - 32px));
pointer-events: none;
opacity: 0;
transform: translateY(-12px);
transition: opacity 0.4s var(--ease-out-expo), transform 0.4s var(--ease-out-expo);
will-change: opacity, transform;
}
.wi-welcome-popup[data-position="top-left"] {
top: 88px;
left: 24px;
}
.wi-welcome-popup[data-position="top-right"] {
top: 88px;
right: 24px;
}
.wi-welcome-popup[data-position="bottom-left"] {
bottom: 24px;
left: 24px;
transform: translateY(12px);
}
.wi-welcome-popup[data-position="bottom-right"] {
bottom: 24px;
right: 24px;
transform: translateY(12px);
}
.wi-welcome-popup[data-position="top-center"] {
top: 88px;
left: 50%;
transform: translate(-50%) translateY(-12px);
}
.wi-welcome-popup--visible {
opacity: 1;
pointer-events: auto;
}
.wi-welcome-popup--visible[data-position="top-left"],
.wi-welcome-popup--visible[data-position="top-right"],
.wi-welcome-popup--visible[data-position="bottom-left"],
.wi-welcome-popup--visible[data-position="bottom-right"] {
transform: translateY(0);
}
.wi-welcome-popup--visible[data-position="top-center"] {
transform: translate(-50%) translateY(0);
}
.wi-welcome-popup--leaving {
opacity: 0;
pointer-events: none;
}
.wi-welcome-popup--leaving[data-position^="top"] {
transform: translateY(-12px);
}
.wi-welcome-popup--leaving[data-position="bottom-left"],
.wi-welcome-popup--leaving[data-position="bottom-right"] {
transform: translateY(12px);
}
.wi-welcome-popup--leaving[data-position="top-center"] {
transform: translate(-50%) translateY(-12px);
}
.wi-welcome-popup__inner {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 14px;
border-radius: 9999px;
background: color-mix(in srgb, var(--bg-raised) 92%, transparent);
backdrop-filter: blur(20px) saturate(180%);
-webkit-backdrop-filter: blur(20px) saturate(180%);
border: 1px solid var(--rule);
box-shadow: 0 8px 24px -8px #0000001f, 0 2px 8px -2px #0000000f;
color: var(--ink);
font-size: var(--text-sm);
line-height: 1.4;
}
.wi-welcome-popup__icon {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
flex-shrink: 0;
border-radius: 50%;
background: color-mix(in srgb, var(--accent) 12%, transparent);
color: var(--accent);
}
.wi-welcome-popup__icon svg {
animation: wi-welcome-popup-pulse 2s ease-in-out infinite;
}
.wi-welcome-popup__content {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.wi-welcome-popup__close {
display: flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
flex-shrink: 0;
padding: 0;
border: none;
border-radius: 50%;
background: transparent;
color: var(--ink-3);
cursor: pointer;
opacity: 0.6;
transition: opacity 0.2s ease, background 0.2s ease, color 0.2s ease;
}
.wi-welcome-popup__close:hover {
opacity: 1;
background: color-mix(in srgb, var(--ink-3) 12%, transparent);
color: var(--ink);
}
.wi-welcome-popup__progress {
position: absolute;
left: 14px;
right: 14px;
bottom: 4px;
height: 2px;
border-radius: 1px;
background: color-mix(in srgb, var(--accent) 30%, transparent);
transform-origin: left center;
transform: scaleX(1);
opacity: 0.7;
}
.wi-welcome-popup--visible .wi-welcome-popup__progress {
opacity: 1;
}
@keyframes wi-welcome-popup-pulse {
0%,
to {
transform: scale(1);
}
50% {
transform: scale(1.12);
}
}
@media (max-width: 480px) {
.wi-welcome-popup[data-position] {
max-width: calc(100vw - 24px);
}
}
@media (prefers-reduced-motion: reduce) {
.wi-welcome-popup,
.wi-welcome-popup--visible,
.wi-welcome-popup--leaving,
.wi-welcome-popup__icon svg {
transition: opacity 0.2s ease !important;
transform: none !important;
animation: none !important;
}
}
</style>
+28 -38
View File
@@ -5,19 +5,30 @@ import Footer from "../components/Footer.astro";
import ScrollReveal from "../components/ScrollReveal.astro";
import CursorGlow from "../components/CursorGlow.astro";
import MobileMenu from "../components/MobileMenu.astro";
import ClickEffect from "../components/ClickEffect.astro";
interface Props {
pageTitle?: string;
contentClass?: string;
wide?: boolean;
home?: boolean;
/** 渲染为 Halo 页面布局契约(templates/layout.html 的 html(head, content) 片段) */
layoutFragment?: boolean;
}
const { pageTitle, contentClass, wide, home } = Astro.props;
const { pageTitle, contentClass, wide, home, layoutFragment = false } = Astro.props;
const fragmentAttr = layoutFragment ? "html (head, content)" : undefined;
---
<!doctype html>
<html class={home ? 'wi-home-snap' : ''} style="overflow-x:hidden" th:lang="${#locale.toLanguageTag()}" th:classappend="${theme.config?.animation?.animation_enabled == false} ? 'wi-no-animations'" th:attr="data-lang=${theme.config?.basic?.language}">
<html
lang="zh-CN"
class={home ? 'wi-home-snap' : ''}
style="overflow-x:hidden"
th:classappend="${theme.config?.animation?.animation_enabled == false} ? 'wi-no-animations'"
xmlns:th={layoutFragment ? "https://www.thymeleaf.org" : undefined}
th:fragment={fragmentAttr}
>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
@@ -73,47 +84,31 @@ const { pageTitle, contentClass, wide, home } = Astro.props;
<style is:inline th:if="${theme.config?.style?.layout_container_width == 'medium' or theme.config?.style?.layout_container_width == null}" th:utext="${'.wi-content-wrap { max-width: 800px; } .wi-content-wrap--wide { max-width: 1200px; } .wi-main--wide { max-width: 1400px; }'}">/*width*/</style>
<style is:inline th:if="${theme.config?.style?.layout_container_width == 'wide'}" th:utext="${'.wi-content-wrap { max-width: 900px; } .wi-content-wrap--wide { max-width: 1400px; } .wi-main--wide { max-width: 1600px; }'}">/*width*/</style>
<style is:inline th:if="${theme.config?.style?.custom_css}" th:utext="${theme.config?.style?.custom_css}">/*custom*/</style>
{
layoutFragment && (
<title th:if="${head == null}" th:text="${site.title}"></title>
<th:block th:if="${head != null}">
<th:block th:replace="${head}" />
</th:block>
)
}
<slot name="head" />
</head>
<body class="wi-body">
<script is:inline>
<script is:inline th:inline="none">
window.addEventListener("unhandledrejection", function (e) {
if (e.reason && e.reason.message && e.reason.message.indexOf("Could not establish connection") !== -1) {
e.preventDefault();
}
});
</script>
<script is:inline>
(function(){
try{
var lang=document.documentElement.getAttribute("data-lang");
if(!lang||lang==="auto"||lang==="null"||lang===""){
var cookies=document.cookie.split(";");
for(var i=0;i<cookies.length;i++){
var c=cookies[i].trim();
if(c.indexOf("language=")===0){
document.cookie="language=;path=/;max-age=0;SameSite=Lax";
break;
}
}
return;
}
var tag=lang.replace(/_/g,"-");
var cookies=document.cookie.split(";");
var current="";
for(var i=0;i<cookies.length;i++){
var c=cookies[i].trim();
if(c.indexOf("language=")===0){current=c.substring(9);break;}
}
if(current!==tag){
document.cookie="language="+tag+";path=/;max-age=31536000;SameSite=Lax";
}
}catch(e){}
})();
</script>
<Navbar />
<main class:list={["wi-main", { "wi-main--wide": wide || home, "wi-main--home": home }]}>
{home ? (
{layoutFragment ? (
<div class:list={["wi-content-wrap", { "wi-content-wrap--wide": wide }]}>
<th:block th:replace="${content}" />
</div>
) : home ? (
<slot />
) : (
<div class:list={["wi-content-wrap", { "wi-content-wrap--wide": wide }]}>
@@ -125,6 +120,7 @@ const { pageTitle, contentClass, wide, home } = Astro.props;
<MobileMenu />
<ScrollReveal th:if="${theme.config?.animation?.animation_enabled != false and theme.config?.animation?.animation_scroll_reveal != false}" />
<CursorGlow th:if="${theme.config?.animation?.animation_enabled != false and theme.config?.animation?.animation_cursor_glow == true}" />
<ClickEffect />
</body>
</html>
@@ -169,10 +165,4 @@ html.wi-no-animations .wi-reveal {
transition: none;
}
.wi-post__content img {
max-width: 100%;
height: auto;
}
</style>
+6 -6
View File
@@ -4,14 +4,14 @@ import Layout from "../layouts/Layout.astro";
<Layout>
<Fragment slot="head">
<title th:text="|${theme.config?.basic?.label_archives_title ?: #messages.msg('archives.title')} - ${site.title}|"></title>
<title th:text="|${theme.config?.basic?.label_archives_title ?: '归档'} - ${site.title}|"></title>
</Fragment>
<div class="wi-archives">
<header class="wi-archives__header">
<h1 class="wi-archives__title" th:text="${theme.config?.basic?.label_archives_title ?: #messages.msg('archives.title')}">归档</h1>
<h1 class="wi-archives__title" th:text="${theme.config?.basic?.label_archives_title ?: '归档'}">归档</h1>
<span class="wi-page-accent"></span>
<p class="wi-archives__subtitle" th:text="${#strings.replace(#messages.msg('common.postCount'), '{0}', #strings.toString(archives.total))}"></p>
<p class="wi-archives__subtitle" th:text="|共 ${archives.total} 篇文章|">共 0 篇文章</p>
</header>
<section class="wi-archives__timeline" aria-label="Archive timeline">
@@ -57,7 +57,7 @@ import Layout from "../layouts/Layout.astro";
<div
th:if="${archives.total == 0}"
class="wi-archives__empty"
th:text="${theme.config?.home?.home_label_no_posts ?: #messages.msg('common.noPosts')}"
th:text="${theme.config?.home?.home_label_no_posts ?: '暂无文章'}"
>No posts yet.</div>
</section>
@@ -70,7 +70,7 @@ import Layout from "../layouts/Layout.astro";
th:if="${archives.hasPrevious()}"
th:href="@{${archives.prevUrl}}"
class="wi-archives__page-link wi-archives__page-link--prev"
th:text="${theme.config?.home?.home_label_newer ?: #messages.msg('common.newer')}"
th:text="${theme.config?.home?.home_label_newer ?: '较新'}"
>&larr; Newer</a>
<span
class="wi-archives__page-info"
@@ -80,7 +80,7 @@ import Layout from "../layouts/Layout.astro";
th:if="${archives.hasNext()}"
th:href="@{${archives.nextUrl}}"
class="wi-archives__page-link wi-archives__page-link--next"
th:text="${theme.config?.home?.home_label_older ?: #messages.msg('common.older')}"
th:text="${theme.config?.home?.home_label_older ?: '较旧'}"
>Older &rarr;</a>
</nav>
</div>
+3 -3
View File
@@ -4,14 +4,14 @@ import Layout from "../layouts/Layout.astro";
<Layout>
<Fragment slot="head">
<title th:text="|#{categories.title} - ${site.title}|"></title>
<title th:text="|分类 - ${site.title}|"></title>
</Fragment>
<div class="wi-categories">
<header class="wi-categories__header">
<h1 class="wi-categories__title" th:text="#{categories.title}">分类</h1>
<h1 class="wi-categories__title" th:text="'分类'">分类</h1>
<span class="wi-page-accent"></span>
<p class="wi-categories__subtitle" th:text="${#messages.msg('common.categoryCount', categories.size())}"></p>
<p class="wi-categories__subtitle" th:text="|共 ${categories.size()} 个分类|">共 0 个分类</p>
</header>
<nav class="wi-categories__list" aria-label="Category list">
+4 -4
View File
@@ -14,7 +14,7 @@ import Layout from "../layouts/Layout.astro";
th:text="${category.spec.displayName}"
></h1>
<span class="wi-page-accent"></span>
<p class="wi-category__subtitle" th:text="#{common.postCount(${posts.total})}"></p>
<p class="wi-category__subtitle" th:text="|共 ${posts.total} 篇文章|">共 0 篇文章</p>
<p
th:if="${not #strings.isEmpty(category.spec.description)}"
class="wi-category__description"
@@ -58,7 +58,7 @@ import Layout from "../layouts/Layout.astro";
<div
th:if="${posts.total == 0}"
class="wi-category__empty"
th:text="${theme.config?.home?.home_label_no_posts ?: #messages.msg('common.noPosts')}"
th:text="${theme.config?.home?.home_label_no_posts ?: '暂无文章'}"
>No posts yet.</div>
<nav
@@ -70,7 +70,7 @@ import Layout from "../layouts/Layout.astro";
th:if="${posts.hasPrevious()}"
th:href="@{${posts.prevUrl}}"
class="wi-category__page-link wi-category__page-link--prev"
th:text="${theme.config?.home?.home_label_newer ?: #messages.msg('common.newer')}"
th:text="${theme.config?.home?.home_label_newer ?: '较新'}"
>&larr; Newer</a>
<span
class="wi-category__page-info"
@@ -80,7 +80,7 @@ import Layout from "../layouts/Layout.astro";
th:if="${posts.hasNext()}"
th:href="@{${posts.nextUrl}}"
class="wi-category__page-link wi-category__page-link--next"
th:text="${theme.config?.home?.home_label_older ?: #messages.msg('common.older')}"
th:text="${theme.config?.home?.home_label_older ?: '较旧'}"
>Older &rarr;</a>
</nav>
</div>
+13 -13
View File
@@ -10,7 +10,7 @@ import Layout from "../layouts/Layout.astro";
<section class="wi-douban">
<div class="wi-container">
<div class="wi-douban__header">
<h1 class="wi-douban__title" th:text="${title ?: #messages.msg('douban.title')}">Douban</h1>
<h1 class="wi-douban__title" th:text="${title ?: '豆瓣'}">豆瓣</h1>
<span class="wi-page-accent"></span>
</div>
@@ -32,63 +32,63 @@ import Layout from "../layouts/Layout.astro";
class="wi-douban__filter-tag wi-douban__filter-tag--sm"
th:href="@{/douban(type=${currentType},status='wish')}"
th:classappend="${hasStatus and currentStatus == 'wish'} ? 'wi-douban__filter-tag--active' : ''"
th:text="#{douban.wish}">Wish</a>
th:text="'想看'">想看</a>
<a
class="wi-douban__filter-tag wi-douban__filter-tag--sm"
th:href="@{/douban(type=${currentType},status='done')}"
th:classappend="${(!hasStatus and isDefaultType) or (hasStatus and currentStatus == 'done')} ? 'wi-douban__filter-tag--active' : ''"
th:text="#{douban.done}">Done</a>
th:text="'看过'">看过</a>
</th:block>
<th:block th:if="${currentType == 'book'}">
<a
class="wi-douban__filter-tag wi-douban__filter-tag--sm"
th:href="@{/douban(type='book',status='wish')}"
th:classappend="${hasStatus and currentStatus == 'wish'} ? 'wi-douban__filter-tag--active' : ''"
th:text="#{douban.wishRead}">Wish</a>
th:text="'想读'">想读</a>
<a
class="wi-douban__filter-tag wi-douban__filter-tag--sm"
th:href="@{/douban(type='book',status='doing')}"
th:classappend="${hasStatus and currentStatus == 'doing'} ? 'wi-douban__filter-tag--active' : ''"
th:text="#{douban.reading}">Reading</a>
th:text="'在读'">在读</a>
<a
class="wi-douban__filter-tag wi-douban__filter-tag--sm"
th:href="@{/douban(type='book',status='done')}"
th:classappend="${(!hasStatus and isDefaultType) or (hasStatus and currentStatus == 'done')} ? 'wi-douban__filter-tag--active' : ''"
th:text="#{douban.done}">Done</a>
th:text="'看过'">看过</a>
</th:block>
<th:block th:if="${currentType == 'music'}">
<a
class="wi-douban__filter-tag wi-douban__filter-tag--sm"
th:href="@{/douban(type='music',status='wish')}"
th:classappend="${hasStatus and currentStatus == 'wish'} ? 'wi-douban__filter-tag--active' : ''"
th:text="#{douban.wishListen}">Wish</a>
th:text="'想听'">想听</a>
<a
class="wi-douban__filter-tag wi-douban__filter-tag--sm"
th:href="@{/douban(type='music',status='doing')}"
th:classappend="${hasStatus and currentStatus == 'doing'} ? 'wi-douban__filter-tag--active' : ''"
th:text="#{douban.listening}">Listening</a>
th:text="'在听'">在听</a>
<a
class="wi-douban__filter-tag wi-douban__filter-tag--sm"
th:href="@{/douban(type='music',status='done')}"
th:classappend="${(!hasStatus and isDefaultType) or (hasStatus and currentStatus == 'done')} ? 'wi-douban__filter-tag--active' : ''"
th:text="#{douban.listened}">Listened</a>
th:text="'听过'">听过</a>
</th:block>
<th:block th:if="${currentType == 'game'}">
<a
class="wi-douban__filter-tag wi-douban__filter-tag--sm"
th:href="@{/douban(type='game',status='wish')}"
th:classappend="${hasStatus and currentStatus == 'wish'} ? 'wi-douban__filter-tag--active' : ''"
th:text="#{douban.wishPlay}">Wish</a>
th:text="'想玩'">想玩</a>
<a
class="wi-douban__filter-tag wi-douban__filter-tag--sm"
th:href="@{/douban(type='game',status='doing')}"
th:classappend="${hasStatus and currentStatus == 'doing'} ? 'wi-douban__filter-tag--active' : ''"
th:text="#{douban.playing}">Playing</a>
th:text="'在玩'">在玩</a>
<a
class="wi-douban__filter-tag wi-douban__filter-tag--sm"
th:href="@{/douban(type='game',status='done')}"
th:classappend="${(!hasStatus and isDefaultType) or (hasStatus and currentStatus == 'done')} ? 'wi-douban__filter-tag--active' : ''"
th:text="#{douban.played}">Played</a>
th:text="'玩过'">玩过</a>
</th:block>
</div>
@@ -162,7 +162,7 @@ import Layout from "../layouts/Layout.astro";
class="wi-douban__my-score"
th:if="${!#strings.isEmpty(item.faves?.score)}"
>
<span th:text="#{douban.myRating}">My:</span><th:block th:text="${item.faves?.score}"></th:block>
<span th:text="'我的:'">My:</span><th:block th:text="${item.faves?.score}"></th:block>
</span>
<span
class="wi-douban__year"
+6 -6
View File
@@ -4,7 +4,7 @@ import Layout from "../layouts/Layout.astro";
<Layout wide>
<Fragment slot="head">
<title th:text="|${theme.config?.friends?.friends_page_title ?: #messages.msg('friends.title')} - ${site.title}|"></title>
<title th:text="|${theme.config?.friends?.friends_page_title ?: '友人圈'} - ${site.title}|"></title>
</Fragment>
<section
@@ -15,7 +15,7 @@ import Layout from "../layouts/Layout.astro";
<div class="wi-friends-page__header">
<h1
class="wi-friends-page__title"
th:text="${theme.config?.friends?.friends_page_title ?: #messages.msg('friends.title')}"
th:text="${theme.config?.friends?.friends_page_title ?: '友人圈'}"
>
Friends Circle
</h1>
@@ -87,7 +87,7 @@ import Layout from "../layouts/Layout.astro";
th:if="${friends.hasPrevious()}"
th:href="@{${friends.prevUrl}}"
class="wi-friends-page__page-link"
th:text="#{common.newer}"
th:text="'较新'"
>
&larr; Newer
</a>
@@ -99,7 +99,7 @@ import Layout from "../layouts/Layout.astro";
th:if="${friends.hasNext()}"
th:href="@{${friends.nextUrl}}"
class="wi-friends-page__page-link"
th:text="#{common.older}"
th:text="'较旧'"
>
Older &rarr;
</a>
@@ -113,11 +113,11 @@ import Layout from "../layouts/Layout.astro";
>
<div class="wi-container">
<div class="wi-friends-page__header">
<h1 class="wi-friends-page__title" th:text="#{friends.title}">Friends Circle</h1>
<h1 class="wi-friends-page__title" th:text="'友人圈'">友人圈</h1>
<span class="wi-page-accent"></span>
</div>
<div class="wi-friends-page__empty">
<p th:utext="#{common.installPlugin('Friends Circle', 'https://www.halo.run/store/apps/app-friends')}">This feature requires the Friends Circle plugin</p>
<p>该功能需要安装「朋友圈」插件,<a href="https://www.halo.run/store/apps/app-yISsV" target="_blank" rel="noopener noreferrer">前往应用市场安装</a></p>
</div>
</div>
</section>
+18 -15
View File
@@ -1,6 +1,7 @@
---
import Layout from "../layouts/Layout.astro";
import HeroSection from "../components/HeroSection.astro";
import WelcomePopup from "../components/WelcomePopup.astro";
---
<Layout home>
@@ -8,12 +9,13 @@ import HeroSection from "../components/HeroSection.astro";
<title th:text="${site.title}"></title>
</Fragment>
<WelcomePopup />
<HeroSection />
<div
class="wi-home-content"
th:classappend="${theme.config?.hero?.hero_enabled != false} ? 'wi-home-content--with-hero'"
th:attr="data-i18n-all-loaded=#{common.allLoaded},data-i18n-loading=#{common.loading},data-i18n-words=#{common.words},data-i18n-read-time=#{common.readTime},data-i18n-views=#{common.views}"
>
<section
class="wi-home-pinned"
@@ -21,7 +23,7 @@ import HeroSection from "../components/HeroSection.astro";
>
<div class="wi-container">
<div class="wi-section__header">
<h2 class="wi-section__title" th:text="${theme.config?.home?.home_pinned_title ?: #messages.msg('hero.pinned')}">置顶</h2>
<h2 class="wi-section__title" th:text="${theme.config?.home?.home_pinned_title ?: '置顶'}">置顶</h2>
</div>
<div class="wi-pinned">
<a
@@ -66,7 +68,7 @@ import HeroSection from "../components/HeroSection.astro";
<section class="wi-home-posts" id="wi-home-posts">
<div class="wi-container">
<div class="wi-section__header">
<h2 class="wi-section__title" th:text="${theme.config?.home?.home_posts_title ?: #messages.msg('hero.posts')}">文章</h2>
<h2 class="wi-section__title" th:text="${theme.config?.home?.home_posts_title ?: '文章'}">文章</h2>
</div>
<div class="wi-flow" id="wi-post-grid">
<article
@@ -129,7 +131,7 @@ import HeroSection from "../components/HeroSection.astro";
class="wi-flow__stat"
th:if="${theme.config?.home?.home_card_show_visit ?: true}"
>
<th:block th:text="${post.stats?.visit ?: 0}"></th:block> <th:block th:text="#{common.views}">阅读</th:block>
<th:block th:text="${post.stats?.visit ?: 0}"></th:block> <th:block th:text="'阅读'">阅读</th:block>
</span>
</div>
</div>
@@ -153,7 +155,7 @@ import HeroSection from "../components/HeroSection.astro";
th:if="${posts.hasPrevious()}"
th:href="@{${posts.prevUrl}}"
class="wi-pagination__prev"
th:text="${theme.config?.home?.home_label_newer ?: #messages.msg('common.newer')}"
th:text="${theme.config?.home?.home_label_newer ?: '较新'}"
>&larr; 较新</a>
<span
class="wi-pagination__info"
@@ -163,18 +165,18 @@ import HeroSection from "../components/HeroSection.astro";
th:if="${posts.hasNext()}"
th:href="@{${posts.nextUrl}}"
class="wi-pagination__next"
th:text="${theme.config?.home?.home_label_older ?: #messages.msg('common.older')}"
th:text="${theme.config?.home?.home_label_older ?: '较旧'}"
>较旧 &rarr;</a>
</nav>
<div
th:if="${posts.hasNext() and theme.config?.home?.home_post_loading == 'infinite_scroll'}"
id="wi-infinite-scroll-sentinel"
th:attr="data-next-url=${posts.nextUrl}, data-all-loaded-text=${theme.config?.home?.home_label_all_loaded ?: #messages.msg('common.allLoaded')}"
th:attr="data-next-url=${posts.nextUrl}, data-all-loaded-text=${theme.config?.home?.home_label_all_loaded ?: '已加载全部文章'}"
>
<div class="wi-infinite-scroll__loader">
<div class="wi-infinite-scroll__spinner"></div>
<span th:text="${theme.config?.home?.home_label_loading ?: #messages.msg('common.loading')}">加载中...</span>
<span th:text="${theme.config?.home?.home_label_loading ?: '加载中...'}">加载中...</span>
</div>
</div>
@@ -182,23 +184,22 @@ import HeroSection from "../components/HeroSection.astro";
th:if="${!posts.hasNext() and posts.totalPages gt 1 and theme.config?.home?.home_post_loading == 'infinite_scroll'}"
class="wi-infinite-scroll__end"
>
<span th:text="${theme.config?.home?.home_label_all_loaded ?: #messages.msg('common.allLoaded')}">已加载全部文章</span>
<span th:text="${theme.config?.home?.home_label_all_loaded ?: '已加载全部文章'}">已加载全部文章</span>
</div>
</div>
</section>
</div>
</Layout>
<script is:inline>
<script is:inline th:inline="none">
(function () {
var sentinel = document.getElementById("wi-infinite-scroll-sentinel");
if (!sentinel) return;
var grid = document.getElementById("wi-post-grid");
var i18nEl = document.querySelector(".wi-home-content");
var loading = false;
var nextUrl = sentinel.getAttribute("data-next-url");
var allLoadedText = sentinel.getAttribute("data-all-loaded-text") || (i18nEl && i18nEl.getAttribute("data-i18n-all-loaded")) || "All posts loaded";
var allLoadedText = sentinel.getAttribute("data-all-loaded-text") || "已加载全部文章";
var observer = new IntersectionObserver(
function (entries) {
@@ -240,7 +241,9 @@ import HeroSection from "../components/HeroSection.astro";
sentinel.style.display = "none";
var endMsg = document.createElement("div");
endMsg.className = "wi-infinite-scroll__end";
endMsg.innerHTML = "<span>" + allLoadedText + "</span>";
var endText = document.createElement("span");
endText.textContent = allLoadedText;
endMsg.appendChild(endText);
sentinel.parentNode.appendChild(endMsg);
}
@@ -260,7 +263,7 @@ import HeroSection from "../components/HeroSection.astro";
})();
</script>
<script is:inline>
<script is:inline th:inline="none">
(function () {
var hero = document.getElementById("wi-hero");
var blurOverlay = document.getElementById("wi-hero-blur-overlay");
@@ -304,7 +307,7 @@ import HeroSection from "../components/HeroSection.astro";
})();
</script>
<script is:inline>
<script is:inline th:inline="none">
(function () {
var hero = document.getElementById("wi-hero");
var content = document.querySelector(".wi-home-content--with-hero");
+13
View File
@@ -0,0 +1,13 @@
---
import Layout from "../layouts/Layout.astro";
---
<!--
页面布局契约(Halo 2.26+)
供插件前台页面通过 ~{layout :: html(head = ..., content = ...)} 复用主题外观,
避免这些页面回退到 Halo 内置的 fallback 布局导致风格不一致。
本文件只负责声明契约:实际结构、配色、导航、页脚均由 Layout 组件提供,
因此修改主题外观时无需同步改这里。
-->
<Layout layoutFragment wide />
+108 -4
View File
@@ -4,24 +4,39 @@ import Layout from "../layouts/Layout.astro";
<Layout wide>
<Fragment slot="head">
<title th:text="|${theme.config?.links?.links_page_title ?: #messages.msg('links.title')} - ${site.title}|"></title>
<title th:text="|${theme.config?.links?.links_page_title ?: '友链'} - ${site.title}|"></title>
</Fragment>
<section
class="wi-links"
th:if="${pluginFinder.available('PluginLinks')}"
th:attr="data-style=${theme.config?.links?.links_style ?: 'cards'}"
th:attr="data-style=${theme.config?.links?.links_style ?: 'cards'},data-random=${theme.config?.links?.links_random_order == true ? 'true' : 'false'}"
>
<div class="wi-container">
<div class="wi-links__header">
<h1
class="wi-links__title"
th:text="${theme.config?.links?.links_page_title ?: #messages.msg('links.title')}"
th:text="${theme.config?.links?.links_page_title ?: '友链'}"
>
友情链接
</h1>
<p class="wi-links__subtitle" th:text="#{links.subtitle}">Mountains and rivers, a journey of a thousand miles</p>
<p class="wi-links__subtitle" th:text="'山水一程,三生有幸'">山水一程,三生有幸</p>
<span class="wi-page-accent"></span>
<button
type="button"
class="wi-links__portal"
id="wi-links-portal"
th:if="${theme.config?.links?.links_random_portal != false}"
title="随机传送"
aria-label="随机传送"
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 16V8a2 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>
<polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline>
<polyline points="3.27 6.96 12 12.01 20.73 6.96" transform="translate(0 8)"></polyline>
</svg>
<span class="wi-links__portal-text">随机传送</span>
</button>
</div>
<th:block th:each="group : ${groups}">
@@ -71,8 +86,50 @@ import Layout from "../layouts/Layout.astro";
</a>
</div>
</th:block>
<div class="wi-links__comment" th:if="${theme.config?.links?.links_show_comment == true}">
<halo:comment group="plugin.halo.run" kind="Plugin" name="PluginLinks"></halo:comment>
</div>
</div>
</section>
<script is:inline th:inline="none">
(function () {
var section = document.querySelector(".wi-links");
if (!section) return;
var cards = Array.prototype.slice.call(section.querySelectorAll(".wi-links__card"));
if (!cards.length) return;
// 随机排列:Fisher-Yates 打乱每个分组内的卡片顺序
if (section.getAttribute("data-random") === "true") {
Array.prototype.forEach.call(section.querySelectorAll(".wi-links__grid"), function (grid) {
for (var i = grid.children.length; i >= 0; i--) {
var j = Math.floor(Math.random() * i);
grid.appendChild(grid.children[j]);
}
});
}
// 随机传送:从当前页所有友链中随机打开一个
var portal = document.getElementById("wi-links-portal");
if (portal) {
portal.addEventListener("click", function () {
var links = Array.prototype.slice
.call(section.querySelectorAll(".wi-links__card"))
.map(function (a) {
return a.getAttribute("href");
})
.filter(function (h) {
return h && h !== "#";
});
if (!links.length) return;
var target = links[Math.floor(Math.random() * links.length)];
window.open(target, "_blank", "noopener");
});
}
})();
</script>
</Layout>
<style>
@@ -85,6 +142,53 @@ import Layout from "../layouts/Layout.astro";
margin-bottom: var(--space-3xl);
}
.wi-links__portal {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.6rem 1.1rem;
min-height: 44px;
margin-top: var(--space-md);
border-radius: 9999px;
font-size: var(--text-sm);
color: #fff;
background: linear-gradient(
135deg,
var(--accent),
color-mix(in srgb, var(--accent) 70%, #000)
);
border: none;
cursor: pointer;
font-family: inherit;
font-weight: 600;
box-shadow: 0 2px 8px color-mix(in srgb, var(--accent) 30%, transparent);
transition: transform var(--duration-fast) var(--ease-out-quart),
box-shadow var(--duration-fast) var(--ease-out-quart),
opacity var(--duration-fast) var(--ease-out-quart);
}
.wi-links__portal:hover {
transform: translateY(-1px);
box-shadow: 0 4px 14px color-mix(in srgb, var(--accent) 40%, transparent);
opacity: 0.95;
}
.wi-links__portal:active {
transform: translateY(0) scale(0.97);
}
.wi-links__portal svg {
transition: transform var(--duration-normal) var(--ease-out-expo);
}
.wi-links__portal:hover svg {
transform: rotate(180deg) scale(1.1);
}
.wi-links__comment {
margin-top: var(--space-3xl);
}
.wi-links__title {
font-size: var(--text-4xl);
margin-bottom: var(--space-xs);
+40 -14
View File
@@ -5,13 +5,22 @@ import LightGallery from "../components/LightGallery.astro";
<Layout>
<Fragment slot="head">
<title th:text="|${theme.config?.moments?.moments_page_title ?: #messages.msg('moments.title')} - ${site.title}|"></title>
<title th:text="|${theme.config?.moments?.moments_page_title ?: '动态'} - ${site.title}|"></title>
</Fragment>
<section class="wi-moments-page" th:attr="data-i18n-all-moments-loaded=#{common.allMomentsLoaded}">
<section class="wi-moments-page">
<div class="wi-moments-page__hero" th:if="${theme.config?.moments?.moments_cover}">
<img
class="wi-moments-page__cover"
th:src="${theme.config?.moments?.moments_cover}"
alt=""
loading="eager"
decoding="async"
/>
</div>
<div class="wi-container">
<div class="wi-moments-page__header">
<h1 class="wi-moments-page__title" th:text="${theme.config?.moments?.moments_page_title ?: #messages.msg('moments.title')}">瞬间</h1>
<div class="wi-moments-page__header" th:unless="${theme.config?.moments?.moments_cover}">
<h1 class="wi-moments-page__title" th:text="${theme.config?.moments?.moments_page_title ?: '动态'}">瞬间</h1>
<span class="wi-page-accent"></span>
</div>
@@ -70,7 +79,7 @@ import LightGallery from "../components/LightGallery.astro";
</th:block>
<th:block th:if="${momentsResult == null or #lists.isEmpty(momentsResult.items)}">
<div class="wi-moments-page__empty">
<p th:text="#{moments.noMoments}">No moments yet</p>
<p th:text="'还没有瞬间,快去记录当下的心情吧 🌙'">还没有瞬间,快去记录当下的心情吧 🌙</p>
</div>
</th:block>
</div>
@@ -126,7 +135,7 @@ import LightGallery from "../components/LightGallery.astro";
</th:block>
<th:block th:if="${momentsResult == null or #lists.isEmpty(momentsResult.items)}">
<div class="wi-moments-page__empty">
<p th:text="#{moments.noMoments}">No moments yet</p>
<p th:text="'还没有瞬间,快去记录当下的心情吧 🌙'">还没有瞬间,快去记录当下的心情吧 🌙</p>
</div>
</th:block>
</div>
@@ -141,13 +150,13 @@ import LightGallery from "../components/LightGallery.astro";
class="wi-moments-page__page-btn"
th:if="${momentsResult.hasPrevious()}"
th:href="${momentsResult.prevUrl}"
th:text="#{common.newer}">Newer</a>
th:text="'较新'">较新</a>
<span class="wi-moments-page__page-spacer" th:if="${momentsResult.hasPrevious() and momentsResult.hasNext()}"></span>
<a
class="wi-moments-page__page-btn"
th:if="${momentsResult.hasNext()}"
th:href="${momentsResult.nextUrl}"
th:text="#{common.older}">Older</a>
th:text="'较旧'">较旧</a>
</nav>
<div
@@ -157,7 +166,7 @@ import LightGallery from "../components/LightGallery.astro";
></div>
<th:block th:if="${momentFinder == null}">
<div class="wi-moments-page__empty">
<p th:text="#{common.pluginError}">Plugin error</p>
<p th:text="'插件加载异常,请检查插件状态'">插件加载异常,请检查插件状态</p>
</div>
</th:block>
</div>
@@ -165,7 +174,7 @@ import LightGallery from "../components/LightGallery.astro";
<th:block th:unless="${pluginFinder.available('PluginMoments')}">
<div class="wi-moments-page__empty">
<p th:utext="#{common.installPlugin('Moments', 'https://www.halo.run/store/apps/app-hqbe')}">This feature requires the Moments plugin</p>
<p>该功能需要安装「瞬间」插件,<a href="https://www.halo.run/store/apps/app-SnwWD" target="_blank" rel="noopener noreferrer">前往应用市场安装</a></p>
</div>
</th:block>
</div>
@@ -174,7 +183,7 @@ import LightGallery from "../components/LightGallery.astro";
<LightGallery />
</Layout>
<script is:inline>
<script is:inline th:inline="none">
(function () {
document.addEventListener("click", function (e) {
var likeBtn = e.target.closest(".wi-moments-page__like-btn");
@@ -350,9 +359,7 @@ import LightGallery from "../components/LightGallery.astro";
sentinel.style.display = "none";
var endMsg = document.createElement("div");
endMsg.className = "wi-moments-page__empty";
var momentsSection = document.querySelector(".wi-moments-page");
var allMomentsText = momentsSection ? momentsSection.getAttribute("data-i18n-all-moments-loaded") : "All moments loaded";
endMsg.innerHTML = "<p>" + allMomentsText + "</p>";
endMsg.innerHTML = "<p>已加载全部瞬间</p>";
sentinel.parentNode.appendChild(endMsg);
}
@@ -376,6 +383,21 @@ import LightGallery from "../components/LightGallery.astro";
padding-block: 1rem;
}
.wi-moments-page__hero {
position: relative;
width: 100%;
height: clamp(220px, 30vw, 380px);
overflow: hidden;
background: var(--bg-raised);
}
.wi-moments-page__cover {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.wi-moments-page__header {
text-align: center;
margin-bottom: var(--space-3xl);
@@ -678,6 +700,10 @@ import LightGallery from "../components/LightGallery.astro";
}
@media (max-width: 768px) {
.wi-moments-page__hero {
height: clamp(180px, 36vw, 260px);
}
.wi-moments-page__masonry {
columns: 1;
}
-15
View File
@@ -92,21 +92,6 @@ import LightGallery from "../components/LightGallery.astro";
color: var(--ink);
}
.wi-page__body :is(h1, h2, h3, h4, h5, h6) {
font-family: var(--font-sans);
margin-top: 2em;
margin-bottom: 0.6em;
}
.wi-page__body p {
margin-block-end: 1.2em;
}
.wi-page__body img {
border-radius: 12px;
margin-block: var(--space-lg);
}
.wi-page__body a {
text-decoration: underline;
text-decoration-thickness: 1px;
+3 -3
View File
@@ -5,7 +5,7 @@ import Layout from "../layouts/Layout.astro";
<Layout>
<Fragment slot="head">
<title
th:text="|${theme.config?.messageboard?.messageboard_title ?: #messages.msg('messageboard.title')} - ${site.title}|"
th:text="|${theme.config?.messageboard?.messageboard_title ?: '留言板'} - ${site.title}|"
></title>
</Fragment>
@@ -14,12 +14,12 @@ import Layout from "../layouts/Layout.astro";
<span class="wi-messageboard__deco">"</span>
<h1
class="wi-messageboard__title"
th:text="${theme.config?.messageboard?.messageboard_title ?: #messages.msg('messageboard.title')}"
th:text="${theme.config?.messageboard?.messageboard_title ?: '留言板'}"
></h1>
<span class="wi-page-accent"></span>
<p
class="wi-messageboard__subtitle"
th:text="${theme.config?.messageboard?.messageboard_subtitle ?: #messages.msg('messageboard.subtitle')}"
th:text="${theme.config?.messageboard?.messageboard_subtitle ?: '在这里留下你的足迹 🌙'}"
></p>
</header>
File diff suppressed because one or more lines are too long
+9 -9
View File
@@ -5,7 +5,7 @@ import LightGallery from "../components/LightGallery.astro";
<Layout wide>
<Fragment slot="head">
<title th:text="|${title ?: (theme.config?.photos?.photos_page_title ?: #messages.msg('photos.title'))} - ${site.title}|"></title>
<title th:text="|${title ?: (theme.config?.photos?.photos_page_title ?: '相册')} - ${site.title}|"></title>
</Fragment>
<section class="wi-photos-page">
@@ -13,7 +13,7 @@ import LightGallery from "../components/LightGallery.astro";
<div class="wi-photos-page__header">
<h1
class="wi-photos-page__title"
th:text="${theme.config?.photos?.photos_page_title ?: #messages.msg('photos.title')}"
th:text="${theme.config?.photos?.photos_page_title ?: '相册'}"
>
Gallery
</h1>
@@ -30,7 +30,7 @@ import LightGallery from "../components/LightGallery.astro";
th:href="@{${photoUrl.list()}}"
class="wi-photos-page__group-link"
th:classappend="${#strings.isEmpty(currentGroup)} ? 'wi-photos-page__group-link--active' : ''"
th:text="#{common.all}"
th:text="'全部'"
>
All
</a>
@@ -49,7 +49,7 @@ import LightGallery from "../components/LightGallery.astro";
<div class="wi-photos-page__all-groups">
<th:block th:if="${showUngrouped and photos != null and !#lists.isEmpty(photos.items)}">
<div class="wi-photos-page__section">
<h2 class="wi-photos-page__section-title" th:text="#{common.all}">All</h2>
<h2 class="wi-photos-page__section-title" th:text="'全部'">全部</h2>
<div
class="wi-photos-page__grid"
th:attr="data-style=${theme.config?.photos?.photos_style ?: 'masonry'}, data-columns=${theme.config?.photos?.photos_columns ?: 3}"
@@ -241,7 +241,7 @@ import LightGallery from "../components/LightGallery.astro";
</div>
</div>
<div class="wi-photos-page__empty" th:if="${#lists.isEmpty(group.photos)}">
<p th:text="#{common.noPhotosInGroup}">No photos in this group.</p>
<p th:text="'该分组暂无图片'">该分组暂无图片</p>
</div>
</th:block>
</th:block>
@@ -249,20 +249,20 @@ import LightGallery from "../components/LightGallery.astro";
</th:block>
<div class="wi-photos-page__empty" th:if="${#lists.isEmpty(groups) and !(showUngrouped and photos != null and !#lists.isEmpty(photos.items))}">
<p th:text="#{common.noPhotos}">No photos yet.</p>
<p th:text="'暂无图片'">暂无图片</p>
</div>
</th:block>
<th:block th:if="${groups == null}">
<div class="wi-photos-page__empty">
<p th:text="#{common.pluginError}">Plugin error</p>
<p th:text="'插件加载异常,请检查插件状态'">插件加载异常,请检查插件状态</p>
</div>
</th:block>
</th:block>
<th:block th:unless="${pluginFinder.available('PluginPhotos')}">
<div class="wi-photos-page__empty">
<p th:utext="#{common.installPlugin('Gallery', 'https://www.halo.run/store/apps/app-PgHgw')}">This feature requires the Gallery plugin</p>
<p>该功能需要安装「相册」插件,<a href="https://www.halo.run/store/apps/app-BmQJW" target="_blank" rel="noopener noreferrer">前往应用市场安装</a></p>
</div>
</th:block>
</div>
@@ -547,7 +547,7 @@ import LightGallery from "../components/LightGallery.astro";
}
</style>
<script is:inline>
<script is:inline th:inline="none">
(function () {
var rafPending = false;
+88 -237
View File
@@ -38,7 +38,7 @@ import LightGallery from "../components/LightGallery.astro";
<div class="wi-reading-progress" id="wi-reading-progress"></div>
<article class="wi-post" th:attr="data-i18n-words=#{common.words},data-i18n-read-time=#{common.readTime}">
<article class="wi-post">
<header class="wi-post__header">
<div
th:if="${theme.config?.article?.article_show_cover != false and !#strings.isEmpty(post.spec.cover)}"
@@ -111,12 +111,14 @@ import LightGallery from "../components/LightGallery.astro";
th:if="${theme.config?.article?.article_show_word_count ?: true}"
class="wi-post__stat-item"
id="wi-word-count"
>-- words</span>
th:with="serverWords=${pluginFinder.available('extra-api') ? extraApiStatsFinder.getPostWordCount({name: post.metadata.name}) : 0}"
th:text="${serverWords > 0 ? serverWords + ' 字' : ''}"
></span>
<span
th:if="${theme.config?.article?.article_show_visit_count ?: true}"
class="wi-post__stat-item"
>
<th:block th:text="${post.stats?.visit ?: 0}"></th:block> <th:block th:text="#{common.views}"></th:block>
<th:block th:text="${post.stats?.visit ?: 0}"></th:block> <th:block th:text="'阅读'"></th:block>
</span>
<span
th:if="${theme.config?.article?.article_show_read_time ?: false}"
@@ -131,7 +133,10 @@ import LightGallery from "../components/LightGallery.astro";
<div
class="wi-post__body"
id="wi-post-body"
th:utext="${post.content.content}"
th:with="hasExtra=${pluginFinder.available('extra-api')},
spaced=${hasExtra and (theme.config?.article?.article_extra_spacing ?: true) ? extraApiRenderFinder.applySpacingInHtml(post.content.content) : post.content.content},
rendered=${hasExtra and (theme.config?.article?.article_extra_highlight ?: false) and extraApiPluginInfoFinder.isFullVersion() ? extraApiJsRenderFinder.highlightCodeInHtml(spaced) : spaced}"
th:utext="${rendered}"
></div>
<aside
@@ -141,7 +146,7 @@ import LightGallery from "../components/LightGallery.astro";
th:attr="data-position=${theme.config?.article?.article_toc_position ?: 'left'},data-expand=${theme.config?.article?.article_toc_expand ?: 'active'}"
>
<div class="wi-toc__header">
<span class="wi-toc__title" th:text="#{article.toc}">目录</span>
<span class="wi-toc__title" th:text="'目录'">目录</span>
</div>
<nav class="wi-toc__nav" id="wi-toc-nav"></nav>
</aside>
@@ -158,7 +163,7 @@ import LightGallery from "../components/LightGallery.astro";
</div>
<div class="wi-post__share" th:if="${theme.config?.article?.article_show_share != false}" th:with="shareUrl=${post.status.permalink}, shareTitle=${post.spec.title}, shareChannels=${theme.config?.article?.article_share_channels ?: 'qq,wechat,weibo'}">
<span class="wi-post__share-label" th:text="${theme.config?.article?.article_share_title ?: #messages.msg('share.title')}">分享</span>
<span class="wi-post__share-label" th:text="${theme.config?.article?.article_share_title ?: '分享'}">分享</span>
<div class="wi-post__share-buttons">
<button th:if="${#strings.contains(shareChannels, 'qq')}" class="wi-post__share-btn wi-post__share-btn--qq" type="button" th:attr="data-url=${shareUrl},data-title=${shareTitle}" title="QQ">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" width="18" height="18" fill="currentColor"><!--!Font Awesome Free v7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M530.1 484.4C518.6 485.8 485.2 431.7 485.2 431.7C485.2 463 469.1 503.9 434.2 533.5C451 538.7 489 552.7 480 567.9C472.7 580.2 354.5 575.8 320.4 571.9C286.3 575.7 168.1 580.2 160.8 567.9C151.8 552.7 189.7 538.7 206.6 533.5C171.7 504 155.5 463.1 155.5 431.7C155.5 431.7 122.2 485.8 110.6 484.4C105.2 483.8 98.2 454.8 119.9 384.7C130.2 351.7 141.9 324.2 160 278.9C156.9 162 205.2 63.9 320.3 63.9C434 63.9 483.5 160 480.6 278.9C498.7 324.1 510.5 351.8 520.7 384.7C542.5 454.8 535.4 483.8 530 484.4z"/></svg>
@@ -189,9 +194,9 @@ import LightGallery from "../components/LightGallery.astro";
<button class="wi-wechat-qr__close" type="button" aria-label="Close">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
</button>
<div class="wi-wechat-qr__title" th:text="#{share.wechatQR ?: '微信扫码分享'}">微信扫码分享</div>
<div class="wi-wechat-qr__title" th:text="'微信扫码分享'">微信扫码分享</div>
<canvas class="wi-wechat-qr__canvas" id="wi-wechat-qr-canvas"></canvas>
<div class="wi-wechat-qr__hint" th:text="#{share.wechatHint ?: '打开微信,扫描二维码即可分享'}">打开微信,扫描二维码即可分享</div>
<div class="wi-wechat-qr__hint" th:text="'打开微信,扫描二维码即可分享'">打开微信,扫描二维码即可分享</div>
</div>
</div>
@@ -205,7 +210,7 @@ import LightGallery from "../components/LightGallery.astro";
th:href="@{${cursor.previous.status.permalink}}"
class="wi-post__nav-link wi-post__nav-link--prev"
>
<span class="wi-post__nav-label" th:text="#{article.previousPost}">上一篇</span>
<span class="wi-post__nav-label" th:text="'上一篇'">上一篇</span>
<span
class="wi-post__nav-title"
th:text="${cursor.previous.spec.title}"
@@ -220,7 +225,7 @@ import LightGallery from "../components/LightGallery.astro";
th:href="@{${cursor.next.status.permalink}}"
class="wi-post__nav-link wi-post__nav-link--next"
>
<span class="wi-post__nav-label" th:text="#{article.nextPost}">下一篇</span>
<span class="wi-post__nav-label" th:text="'下一篇'">下一篇</span>
<span
class="wi-post__nav-title"
th:text="${cursor.next.spec.title}"
@@ -236,7 +241,7 @@ import LightGallery from "../components/LightGallery.astro";
<th:block th:if="${theme.config?.article?.article_show_related == true}">
<section class="wi-post__related" th:with="relatedCat=${post.categories != null and !post.categories.isEmpty()} ? ${post.categories[0].metadata.name} : null, relatedPosts=${relatedCat != null} ? ${postFinder.list({size: 3, categoryName: relatedCat})} : ${postFinder.list({size: 3})}">
<h2 class="wi-post__related-title" th:text="${theme.config?.article?.article_related_title ?: #messages.msg('article.relatedPosts')}">相关推荐</h2>
<h2 class="wi-post__related-title" th:text="${theme.config?.article?.article_related_title ?: '相关推荐'}">相关推荐</h2>
<div class="wi-post__related-grid">
<a th:each="rp : ${relatedPosts.items}" th:if="${rp.metadata.name != post.metadata.name}" th:href="@{${rp.status.permalink}}" class="wi-post__related-card">
<div class="wi-post__related-cover" th:if="${!#strings.isEmpty(rp.spec.cover)}">
@@ -266,7 +271,7 @@ import LightGallery from "../components/LightGallery.astro";
class="wi-toc-fab"
id="wi-toc-fab"
type="button"
th:aria-label="#{common.openToc}"
th:aria-label="'打开目录'"
>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="8" x2="21" y1="6" y2="6"/><line x1="8" x2="21" y1="12" y2="12"/><line x1="8" x2="21" y1="18" y2="18"/><line x1="3" x2="3.01" y1="6" y2="6"/><line x1="3" x2="3.01" y1="12" y2="12"/><line x1="3" x2="3.01" y1="18" y2="18"/></svg>
</button>
@@ -279,8 +284,8 @@ import LightGallery from "../components/LightGallery.astro";
<div class="wi-toc-drawer__overlay"></div>
<div class="wi-toc-drawer__panel">
<div class="wi-toc-drawer__header">
<span class="wi-toc-drawer__title" th:text="#{article.toc}">目录</span>
<button class="wi-toc-drawer__close" type="button" th:aria-label="#{common.closeToc}">
<span class="wi-toc-drawer__title" th:text="'目录'">目录</span>
<button class="wi-toc-drawer__close" type="button" th:aria-label="'关闭目录'">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
</button>
</div>
@@ -305,7 +310,7 @@ import LightGallery from "../components/LightGallery.astro";
<script is:inline defer src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/prism.min.js"></script>
<script is:inline defer src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/plugins/autoloader/prism-autoloader.min.js"></script>
<script is:inline>
<script is:inline th:inline="none">
(function () {
if (typeof Prism !== "undefined") {
Prism.plugins.autoloader.languages_path = "https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/";
@@ -321,7 +326,7 @@ import LightGallery from "../components/LightGallery.astro";
})();
</script>
<script is:inline>
<script is:inline th:inline="none">
(function () {
var progressBar = document.getElementById("wi-reading-progress");
if (progressBar) {
@@ -347,6 +352,7 @@ import LightGallery from "../components/LightGallery.astro";
var body = document.getElementById("wi-post-body");
if (!body) return;
/* 图片:补齐懒加载属性 + 加载前的模糊占位 */
body.querySelectorAll("img").forEach(function(img) {
if (!img.hasAttribute("loading")) {
img.setAttribute("loading", "lazy");
@@ -354,22 +360,64 @@ import LightGallery from "../components/LightGallery.astro";
if (!img.hasAttribute("decoding")) {
img.setAttribute("decoding", "async");
}
if (img.complete && img.naturalWidth > 0) {
img.classList.add("wi-media-loaded");
} else {
img.classList.add("wi-media-loading");
img.addEventListener("load", function () {
img.classList.remove("wi-media-loading");
img.classList.add("wi-media-loaded");
});
img.addEventListener("error", function () {
img.classList.remove("wi-media-loading");
img.classList.add("wi-media-loaded");
});
}
});
/* 嵌入媒体(B站 / 腾讯视频 / YouTube 等 iframe):
按原始 width/height 生成 aspect-ratio 容器,保证居中且自适应不形变 */
body.querySelectorAll("iframe").forEach(function(frame) {
if (frame.closest(".wi-embed")) return;
var w = parseInt(frame.getAttribute("width"), 10);
var h = parseInt(frame.getAttribute("height"), 10);
var wrap = document.createElement("div");
wrap.className = "wi-embed";
if (w > 0 && h > 0) {
wrap.style.aspectRatio = w + " / " + h;
} else {
wrap.style.aspectRatio = "16 / 9";
}
frame.parentNode.insertBefore(wrap, frame);
wrap.appendChild(frame);
frame.removeAttribute("width");
frame.removeAttribute("height");
});
var showWordCount = document.getElementById("wi-word-count");
var showReadTime = document.getElementById("wi-read-time");
var articleEl = document.querySelector(".wi-post");
var wordsLabel = articleEl ? articleEl.getAttribute("data-i18n-words") : "words";
var readTimeLabel = articleEl ? articleEl.getAttribute("data-i18n-read-time") : "min read";
var wordsLabel = "字";
var readTimeLabel = "分钟";
if (showWordCount || showReadTime) {
var text = (body.textContent || body.innerText || "").trim();
var charCount = text.replace(/\s/g, "").length;
var readMinutes = Math.max(1, Math.ceil(charCount / 500));
/* 服务端已统计字数时(API 扩展包插件)直接复用,避免重复计算 */
if (showWordCount) {
if (showWordCount.textContent.trim()) {
var servered = parseInt(showWordCount.textContent.replace(/\D/g, ""), 10);
if (servered > 0) {
charCount = servered;
readMinutes = Math.max(1, Math.ceil(charCount / 500));
} else {
showWordCount.textContent = charCount + " " + wordsLabel;
}
} else {
showWordCount.textContent = charCount + " " + wordsLabel;
}
}
if (showReadTime) {
showReadTime.textContent = readMinutes + " " + readTimeLabel;
}
@@ -727,20 +775,6 @@ import LightGallery from "../components/LightGallery.astro";
});
});
var lazyImgs = document.querySelectorAll(".wi-post__content img[loading='lazy']");
lazyImgs.forEach(function (img) {
if (img.complete && img.naturalWidth > 0) {
img.setAttribute("data-blur", "loaded");
} else {
img.setAttribute("data-blur", "true");
img.addEventListener("load", function () {
img.setAttribute("data-blur", "loaded");
});
img.addEventListener("error", function () {
img.setAttribute("data-blur", "loaded");
});
}
});
})();
</script>
</Layout>
@@ -874,109 +908,9 @@ import LightGallery from "../components/LightGallery.astro";
color: #d4cdc4;
}
.wi-post__body * {
max-width: 100%;
}
.wi-post__body :is(pre, code, table) {
overflow-x: auto;
}
.wi-post__body pre {
margin-block: 1.5em;
padding: 1.25em;
border-radius: 12px;
border: 1px solid var(--rule);
background: var(--bg-raised);
font-size: 0.875em;
line-height: 1.6;
}
.wi-post__body pre code {
background: none;
padding: 0;
border: none;
border-radius: 0;
font-size: inherit;
}
.wi-post__body code {
background: var(--bg-raised);
padding: 0.15em 0.4em;
border-radius: 4px;
font-size: 0.875em;
border: 1px solid var(--rule);
}
.wi-post__body :is(h1, h2, h3, h4, h5, h6) {
font-family: var(--font-sans);
margin-top: 1.8em;
margin-bottom: 0.5em;
scroll-margin-top: 80px;
}
.wi-post__body h2 {
font-size: 1.5rem;
margin-top: 2em;
margin-bottom: 0.5em;
}
.wi-post__body h3 {
font-size: 1.25rem;
}
.wi-post__body .ai-summary,
.wi-post__body [class*="ai-summary"],
.wi-post__body [class*="ai-abstract"],
.wi-post__body .abstract,
.wi-post__body [class*="likcc-article-summary"] {
margin-bottom: 15px;
}
.wi-post__body p {
margin-block-end: 1.2em;
}
.wi-post__body img {
border-radius: 8px;
margin-block: 1.5em;
}
.wi-post__content img {
transition: filter 0.4s ease, opacity 0.4s ease;
}
.wi-post__content img[data-blur="true"] {
filter: blur(20px);
opacity: 0.6;
transform: scale(1.05);
}
.wi-post__content img[data-blur="loaded"] {
filter: blur(0);
opacity: 1;
transform: scale(1);
}
.wi-post__body a {
text-decoration: underline;
text-decoration-thickness: 1px;
text-underline-offset: 0.2em;
}
.wi-post__body ul,
.wi-post__body ol {
padding-inline-start: 1.5em;
margin-block-end: 1.2em;
}
.wi-post__body li {
margin-block-end: 0.6em;
}
.wi-post__body blockquote {
margin-block: 2em;
}
/* 正文本身及内部元素样式统一放在全局样式 src/styles/_content.scss,
因为正文由 Thymeleaf 在运行时注入,不会带 Astro 的 data-astro-cid 作用域属性,
写在组件 scoped 样式里对正文内容完全无效。 */
.wi-toc {
display: none;
@@ -1135,12 +1069,6 @@ import LightGallery from "../components/LightGallery.astro";
gap: 2px;
}
.wi-toc-drawer__nav {
display: flex;
flex-direction: column;
gap: 2px;
}
.wi-post__like {
display: flex;
justify-content: center;
@@ -1558,51 +1486,10 @@ import LightGallery from "../components/LightGallery.astro";
line-height: 1.82;
}
.wi-post__body p {
margin-block-end: 1.4em;
}
.wi-post__body h2 {
font-size: 1.6rem;
}
.wi-post__body h3 {
font-size: 1.35rem;
}
.wi-post__body h4 {
font-size: 1.2rem;
}
.wi-post__body pre {
font-size: 0.9em;
padding: 1em;
}
.wi-post__body code {
font-size: 0.9em;
}
}
</style>
<style is:global>
.wi-post__body figure {
margin-block: 1.5em;
}
.wi-post__body figcaption {
color: #545164;
font-size: 0.875rem;
text-align: center;
margin-top: 0.5em;
margin-bottom: 1.2em;
line-height: 1.5;
}
html.dark .wi-post__body figcaption {
color: #8a8494;
}
.wi-toc__group {
position: relative;
}
@@ -1615,36 +1502,38 @@ import LightGallery from "../components/LightGallery.astro";
}
.wi-toc__group--active .wi-toc__sub {
max-height: 600px;
max-height: 1600px;
opacity: 1;
}
.wi-toc__group--expanded .wi-toc__sub {
max-height: 600px;
max-height: 1600px;
opacity: 1;
}
.wi-toc[data-expand="all"] .wi-toc__sub {
max-height: 600px;
max-height: 1600px;
opacity: 1;
}
.wi-toc__link {
display: block;
padding: 4px 0;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
padding: 5px 8px;
font-size: var(--text-xs);
line-height: var(--leading-snug);
color: var(--ink-3);
text-decoration: none;
transition: color 0.15s ease, background 0.15s ease, border-color 0.15s ease;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
border-radius: 6px;
border-left: 2px solid transparent;
transition: color 0.15s ease, background 0.15s ease, border-color 0.15s ease;
}
.wi-toc__link:hover {
color: var(--accent);
background: color-mix(in srgb, var(--accent) 5%, transparent);
}
.wi-toc__link--active {
@@ -1655,56 +1544,23 @@ import LightGallery from "../components/LightGallery.astro";
}
.wi-toc__link--group {
padding-left: 0;
padding-right: 4px;
font-weight: 500;
padding-left: 8px;
padding-right: 8px;
font-weight: 600;
color: var(--ink-2);
cursor: pointer;
}
.wi-toc__link--group::before {
content: "";
display: inline-block;
width: 4px;
height: 4px;
border-radius: 50%;
background: var(--ink-3);
margin-right: 6px;
vertical-align: middle;
transition: background 0.15s ease;
}
.wi-toc__link--group::after {
content: '';
display: inline-block;
width: 5px;
height: 5px;
border-right: 1.5px solid var(--ink-3);
border-bottom: 1.5px solid var(--ink-3);
transform: rotate(-45deg);
margin-left: 6px;
vertical-align: middle;
transition: transform 0.2s ease, border-color 0.15s ease;
}
.wi-toc__group--expanded > .wi-toc__link--group::after,
.wi-toc__group--active > .wi-toc__link--group::after {
transform: rotate(45deg);
}
.wi-toc__link--active.wi-toc__link--group::before {
background: var(--accent);
}
.wi-toc__link--h3 {
padding-left: 16px;
padding-left: 22px;
}
.wi-toc__link--h4 {
padding-left: 32px;
padding-left: 36px;
}
.wi-toc__link--h5 {
padding-left: 48px;
padding-left: 50px;
}
.wi-toc__link--h6 {
@@ -1715,11 +1571,6 @@ import LightGallery from "../components/LightGallery.astro";
border-left-color: var(--accent);
}
.wi-toc__sub .wi-toc__link--h3.wi-toc__link--active { padding-left: 14px; }
.wi-toc__sub .wi-toc__link--h4.wi-toc__link--active { padding-left: 30px; }
.wi-toc__sub .wi-toc__link--h5.wi-toc__link--active { padding-left: 46px; }
.wi-toc__sub .wi-toc__link--h6.wi-toc__link--active { padding-left: 62px; }
.wi-toc-drawer__nav .wi-toc__link {
font-size: var(--text-sm);
padding: 6px 0;
+13 -13
View File
@@ -55,35 +55,35 @@ import Layout from "../layouts/Layout.astro";
th:href="${profile.summary?.profileUrl}"
target="_blank"
rel="noopener noreferrer"
th:text="#{steam.homepage}">Steam Homepage &rarr;</a>
th:text="'Steam 主页'">Steam Homepage &rarr;</a>
</div>
<div class="wi-steam__level" th:if="${profile.steamLevel != null}">
<span class="wi-steam__level-num" th:text="${profile.steamLevel}"></span>
<span class="wi-steam__level-label" th:text="#{steam.level}">Level</span>
<span class="wi-steam__level-label" th:text="'等级'">等级</span>
</div>
</div>
<div class="wi-steam__stats" th:if="${stats != null}">
<div class="wi-steam__stat-card">
<span class="wi-steam__stat-value" th:text="${stats.totalGames} ?: 0">0</span>
<span class="wi-steam__stat-label" th:text="#{steam.ownedGames}">Owned Games</span>
<span class="wi-steam__stat-label" th:text="'拥有游戏'">拥有游戏</span>
</div>
<div class="wi-steam__stat-card">
<span class="wi-steam__stat-value" th:text="${stats.totalPlaytimeFormatted} ?: '0 hours'">0 hours</span>
<span class="wi-steam__stat-label" th:text="#{steam.totalPlaytime}">Total Playtime</span>
<span class="wi-steam__stat-label" th:text="'总游玩时长'">总游玩时长</span>
</div>
<div class="wi-steam__stat-card">
<span class="wi-steam__stat-value" th:text="${stats.recentPlaytimeFormatted} ?: '0 hours'">0 hours</span>
<span class="wi-steam__stat-label" th:text="#{steam.recentPlaytime}">Recent 2 Weeks</span>
<span class="wi-steam__stat-label" th:text="'近两周'">近两周</span>
</div>
<div class="wi-steam__stat-card">
<span class="wi-steam__stat-value" th:text="${badges != null and badges.totalBadges != null ? badges.totalBadges : 0}">0</span>
<span class="wi-steam__stat-label" th:text="#{steam.badges}">Badges</span>
<span class="wi-steam__stat-label" th:text="'徽章'">徽章</span>
</div>
</div>
<div class="wi-steam__section" th:if="${recentGames != null and !recentGames.isEmpty()}">
<h3 class="wi-steam__section-title" th:text="#{steam.recentlyPlayed}">Recently Played</h3>
<h3 class="wi-steam__section-title" th:text="'最近游玩'">最近游玩</h3>
<div class="wi-steam__recent-scroll">
<article class="wi-steam__recent-card" th:each="game : ${recentGames}">
<a
@@ -107,7 +107,7 @@ import Layout from "../layouts/Layout.astro";
<div class="wi-steam__recent-body">
<h4 class="wi-steam__recent-name" th:text="${game.name}"></h4>
<div class="wi-steam__recent-meta">
<span class="wi-steam__recent-time" th:text="${game.playtime2WeeksFormatted} ?: #{steam.hours}"></span>
<span class="wi-steam__recent-time" th:text="${game.playtime2WeeksFormatted} ?: '小时'"></span>
<span
class="wi-steam__recent-achievements"
th:if="${game.achievementProgressText != null}"
@@ -122,7 +122,7 @@ import Layout from "../layouts/Layout.astro";
</th:block>
<div class="wi-steam__section">
<h3 class="wi-steam__section-title" th:text="#{steam.gameLibrary}">Game Library</h3>
<h3 class="wi-steam__section-title" th:text="'游戏库'">游戏库</h3>
<div class="wi-steam__games-grid">
<article class="wi-steam__game-card" th:each="game : ${games.items}">
<a
@@ -146,11 +146,11 @@ import Layout from "../layouts/Layout.astro";
<div class="wi-steam__game-body">
<h4 class="wi-steam__game-name" th:text="${game.name}"></h4>
<div class="wi-steam__game-meta">
<span class="wi-steam__game-time" th:text="${game.playtimeFormatted} ?: #{steam.hours}"></span>
<span class="wi-steam__game-time" th:text="${game.playtimeFormatted} ?: '小时'"></span>
<span
class="wi-steam__game-last"
th:if="${!#strings.isEmpty(game.lastPlayedFormatted)}"
th:text="#{steam.lastPlayed(${game.lastPlayedFormatted})}"
th:text="|最后游玩 ${game.lastPlayedFormatted}|"
></span>
</div>
</div>
@@ -166,13 +166,13 @@ import Layout from "../layouts/Layout.astro";
class="wi-steam__page-btn"
th:if="${games.hasPrevious()}"
th:href="${games.prevUrl}"
th:text="#{common.newer}">Previous</a>
th:text="'较新'">较新</a>
<span class="wi-steam__page-spacer" th:if="${games.hasPrevious() and games.hasNext()}"></span>
<a
class="wi-steam__page-btn"
th:if="${games.hasNext()}"
th:href="${games.nextUrl}"
th:text="#{common.older}">Next</a>
th:text="'较旧'">较旧</a>
</nav>
</div>
</section>
+4 -4
View File
@@ -11,7 +11,7 @@ import Layout from "../layouts/Layout.astro";
<header class="wi-tag__header">
<h1 class="wi-tag__title" th:text="${tag.spec.displayName}"></h1>
<span class="wi-page-accent"></span>
<p class="wi-tag__subtitle" th:text="#{common.postCount(${posts.total})}"></p>
<p class="wi-tag__subtitle" th:text="|共 ${posts.total} 篇文章|">共 0 篇文章</p>
<p
th:if="${not #strings.isEmpty(tag.spec.description)}"
class="wi-tag__description"
@@ -55,7 +55,7 @@ import Layout from "../layouts/Layout.astro";
<div
th:if="${posts.total == 0}"
class="wi-tag__empty"
th:text="${theme.config?.home?.home_label_no_posts ?: #messages.msg('common.noPosts')}"
th:text="${theme.config?.home?.home_label_no_posts ?: '暂无文章'}"
>No posts yet.</div>
<nav
@@ -67,7 +67,7 @@ import Layout from "../layouts/Layout.astro";
th:if="${posts.hasPrevious()}"
th:href="@{${posts.prevUrl}}"
class="wi-tag__page-link wi-tag__page-link--prev"
th:text="${theme.config?.home?.home_label_newer ?: #messages.msg('common.newer')}"
th:text="${theme.config?.home?.home_label_newer ?: '较新'}"
>&larr; Newer</a>
<span
class="wi-tag__page-info"
@@ -77,7 +77,7 @@ import Layout from "../layouts/Layout.astro";
th:if="${posts.hasNext()}"
th:href="@{${posts.nextUrl}}"
class="wi-tag__page-link wi-tag__page-link--next"
th:text="${theme.config?.home?.home_label_older ?: #messages.msg('common.older')}"
th:text="${theme.config?.home?.home_label_older ?: '较旧'}"
>Older &rarr;</a>
</nav>
</div>
+3 -3
View File
@@ -4,14 +4,14 @@ import Layout from "../layouts/Layout.astro";
<Layout>
<Fragment slot="head">
<title th:text="|#{tags.title} - ${site.title}|"></title>
<title th:text="|标签 - ${site.title}|"></title>
</Fragment>
<div class="wi-tags">
<header class="wi-tags__header">
<h1 class="wi-tags__title" th:text="#{tags.title}">标签</h1>
<h1 class="wi-tags__title" th:text="'标签'">标签</h1>
<span class="wi-page-accent"></span>
<p class="wi-tags__subtitle" th:text="${#messages.msg('common.tagCount', tags.size())}"></p>
<p class="wi-tags__subtitle" th:text="|共 ${tags.size()} 个标签|">共 0 个标签</p>
</header>
<div class="wi-tags__cloud" role="list" aria-label="Tag list">
+158
View File
@@ -0,0 +1,158 @@
---
import Layout from "../layouts/Layout.astro";
---
<Layout wide>
<Fragment slot="head">
<title th:text="|${theme.config?.weread?.weread_page_title ?: '书架'} - ${site.title}|"></title>
</Fragment>
<section class="wi-weread">
<div class="wi-container">
<div class="wi-weread__header">
<h1
class="wi-weread__title"
th:text="${theme.config?.weread?.weread_page_title ?: '书架'}"
>
书架
</h1>
<p
class="wi-weread__subtitle"
th:if="${theme.config?.weread?.weread_page_subtitle}"
th:text="${theme.config?.weread?.weread_page_subtitle}"
>
我在读的书
</p>
<span class="wi-page-accent"></span>
</div>
<th:block th:if="${pluginFinder.available('halo-weread-plugin')}">
<div
class="wi-weread__shelf"
id="weread-shelf-wrapper"
th:attr="data-endpoint=@{/halo-weread-plugin/shelf-html}"
>
<p class="wi-weread__loading">正在展开书架…</p>
</div>
</th:block>
<div
class="wi-weread__empty"
th:unless="${pluginFinder.available('halo-weread-plugin')}"
>
<p>
该功能需要安装「微信读书(WeRead)信息展示」插件,<a
href="https://github.com/yeora26/halo-weread-plugin"
target="_blank"
rel="noopener noreferrer"
>前往插件仓库了解</a
>
</p>
</div>
</div>
</section>
<script is:inline th:inline="none">
(function () {
var container = document.getElementById("weread-shelf-wrapper");
if (!container) return;
var endpoint =
container.getAttribute("data-endpoint") ||
"/halo-weread-plugin/shelf-html";
fetch(endpoint)
.then(function (res) {
if (!res.ok) throw new Error("HTTP " + res.status);
return res.text();
})
.then(function (html) {
container.innerHTML = html;
/* innerHTML 插入的脚本不会执行,需要重建后才会运行 */
var oldScripts = container.querySelectorAll("script");
Array.prototype.forEach.call(oldScripts, function (oldScript) {
var script = document.createElement("script");
script.textContent = oldScript.textContent;
oldScript.parentNode.replaceChild(script, oldScript);
});
})
.catch(function () {
container.innerHTML =
'<p class="wi-weread__loading">书架暂时无法展开,请稍后再试。</p>';
});
})();
</script>
</Layout>
<style>
.wi-weread {
padding-block: 2rem 3rem;
}
.wi-weread__header {
text-align: center;
margin-bottom: var(--space-2xl);
}
.wi-weread__title {
font-family: var(--font-sans);
font-size: var(--text-3xl);
font-weight: 700;
color: var(--ink);
letter-spacing: var(--tracking-tight);
margin-bottom: var(--space-sm);
}
.wi-weread__subtitle {
font-size: var(--text-base);
color: var(--ink-3);
line-height: var(--leading-relaxed);
}
.wi-weread__shelf {
min-height: 200px;
}
.wi-weread__loading {
text-align: center;
color: var(--ink-3);
font-size: var(--text-sm);
padding: var(--space-2xl) 0;
}
.wi-weread__empty {
text-align: center;
padding: var(--space-2xl) var(--space-lg);
border: 1px dashed var(--rule);
border-radius: var(--radius-lg, 16px);
color: var(--ink-3);
font-size: var(--text-sm);
}
.wi-weread__empty a {
color: var(--accent);
text-decoration: none;
}
.wi-weread__empty a:hover {
text-decoration: underline;
}
/* 插件返回的书架内容:收敛为与主题一致的卡片观感 */
.wi-weread__shelf :global(img) {
max-width: 100%;
height: auto;
border-radius: 8px;
}
.wi-weread__shelf :global(a) {
color: var(--accent);
}
@media (max-width: 768px) {
.wi-weread__title {
font-size: var(--text-2xl);
}
}
</style>
+331
View File
@@ -0,0 +1,331 @@
@use 'variables' as *;
/* ==========================================================================
正文排版(文章页 .wi-post__body / 单页 .wi-page__body 共用)
--------------------------------------------------------------------------
目标:
1. 标题层级清晰、字号克制,移动端逐级收敛(而不是反向放大)
2. 图片 / 视频 / 嵌入 iframe / figure 一律水平居中且不破坏响应式
========================================================================== */
/* 正文容器:运行时由 Thymeleaf 注入内容,因此这里的规则必须是全局的,
不能写进组件的 scoped <style>(那样不会命中注入的内容)。 */
.wi-post__body,
.wi-page__body,
.wi-messageboard__body {
/* ---------- 标题层级 ---------- */
:is(h1, h2, h3, h4, h5, h6) {
font-family: var(--font-sans);
font-weight: 700;
line-height: 1.35;
letter-spacing: -0.01em;
color: var(--ink);
scroll-margin-top: 88px;
text-wrap: balance;
/* 覆盖全局标题装饰(左侧竖线 / 圆点),正文内保持干净 */
padding-left: 0;
border-left: 0;
&::before,
&::after {
content: none;
}
}
h1 {
font-size: clamp(1.45rem, 1.3rem + 0.7vw, 1.75rem);
margin-top: 2em;
margin-bottom: 0.7em;
}
h2 {
font-size: clamp(1.3rem, 1.2rem + 0.5vw, 1.5rem);
margin-top: 1.9em;
margin-bottom: 0.65em;
}
h3 {
font-size: clamp(1.15rem, 1.08rem + 0.35vw, 1.28rem);
margin-top: 1.7em;
margin-bottom: 0.6em;
}
h4 {
font-size: clamp(1.05rem, 1rem + 0.25vw, 1.13rem);
margin-top: 1.5em;
margin-bottom: 0.5em;
}
h5 {
font-size: 1rem;
margin-top: 1.4em;
margin-bottom: 0.45em;
}
h6 {
font-size: 0.95rem;
font-weight: 600;
color: var(--ink-3);
margin-top: 1.4em;
margin-bottom: 0.45em;
}
/* 标题紧跟在另一个标题后时去掉多余上间距 */
:is(h1, h2, h3, h4, h5, h6) + :is(h1, h2, h3, h4, h5, h6) {
margin-top: 0.6em;
}
/* 段落内首个元素不需要上间距 */
> :is(h1, h2, h3, h4, h5, h6):first-child {
margin-top: 0;
}
p {
margin-block-end: 1.2em;
}
/* ---------- 代码块 / 引用 / 列表 ---------- */
* {
max-width: 100%;
}
:is(pre, code, table) {
overflow-x: auto;
}
pre {
margin-block: 1.5em;
padding: 1.25em;
border-radius: $border-radius-md;
border: 1px solid var(--rule);
background: var(--bg-raised);
font-size: 0.875em;
line-height: 1.6;
}
pre code {
background: none;
padding: 0;
border: none;
border-radius: 0;
font-size: inherit;
}
code {
background: var(--bg-raised);
padding: 0.15em 0.4em;
border-radius: $border-radius-sm;
font-size: 0.875em;
border: 1px solid var(--rule);
}
blockquote {
margin-block: 2em;
}
:is(ul, ol) {
padding-inline-start: 1.5em;
margin-block-end: 1.2em;
}
li {
margin-block-end: 0.6em;
}
a {
text-decoration: underline;
text-decoration-thickness: 1px;
text-underline-offset: 0.2em;
}
/* 第三方摘要插件容器 */
:is(.ai-summary, .abstract),
[class*='ai-summary'],
[class*='ai-abstract'],
[class*='likcc-article-summary'] {
margin-bottom: 15px;
}
/* ---------- 媒体居中 ---------- */
/* 图片 / 视频:块级 + 自动外边距实现居中,max-width 保证不溢出容器 */
img,
video {
display: block;
max-width: 100%;
height: auto;
margin-inline: auto;
}
img,
video {
border-radius: $border-radius-md;
margin-block: 1.5em;
}
/* 行内小图标 / 表情不受居中规则影响 */
img[role='presentation'],
img[aria-hidden='true'],
:is(.emoji, .icon, .avatar, .badge) {
display: inline;
margin-inline: 0;
border-radius: 0;
}
/* 编辑器可能输出 align="center|left|right" 或 text-align 样式,保持一致居中 */
[align='center'],
[style*='text-align:center'],
[style*='text-align: center'] {
text-align: center;
}
iframe {
display: block;
max-width: 100%;
margin-inline: auto;
border: none;
}
figure {
display: block;
max-width: 100%;
margin-inline: auto;
margin-block: 1.5em;
text-align: center;
}
figure :is(img, video) {
margin-block: 0;
}
figcaption {
color: var(--ink-3);
font-size: 0.875rem;
line-height: 1.6;
text-align: center;
margin-top: 0.6em;
}
/* lightgallery 注入的 <a> 包裹层:收缩到图片宽度并居中 */
.wi-lightgallery-item {
display: block;
width: fit-content;
max-width: 100%;
margin-inline: auto;
border-radius: $border-radius-md;
line-height: 0;
}
.wi-lightgallery-item > img {
margin-block: 0;
}
/* 响应式嵌入容器(由脚本按 width/height 生成 aspect-ratio) */
.wi-embed {
position: relative;
width: 100%;
max-width: 100%;
margin-inline: auto;
margin-block: 1.5em;
overflow: hidden;
border-radius: $border-radius-md;
background: var(--bg-raised);
}
.wi-embed > :is(iframe, video, embed, object) {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
margin: 0;
border-radius: 0;
border: 0;
}
/* 图片懒加载占位模糊 */
.wi-media-loading {
filter: blur(20px);
opacity: 0.6;
}
.wi-media-loaded {
filter: blur(0);
opacity: 1;
}
}
/* 移动端:标题逐级收敛,正文行高放宽 */
@media (max-width: 768px) {
.wi-post__body,
.wi-page__body,
.wi-messageboard__body {
h1 {
font-size: 1.4rem;
}
h2 {
font-size: 1.28rem;
}
h3 {
font-size: 1.16rem;
}
h4 {
font-size: 1.05rem;
}
h5 {
font-size: 1rem;
}
h6 {
font-size: 0.95rem;
}
p {
margin-block-end: 1.35em;
}
pre {
font-size: 0.9em;
padding: 1em;
}
code {
font-size: 0.9em;
}
img,
video {
margin-block: 1.2em;
}
/* 窄屏图片允许撑满内容宽度,避免留白过大 */
img,
video,
.wi-lightgallery-item,
figure {
max-width: 100%;
}
}
}
@media (max-width: 480px) {
.wi-post__body,
.wi-page__body,
.wi-messageboard__body {
h1 {
font-size: 1.32rem;
}
h2 {
font-size: 1.22rem;
}
h3 {
font-size: 1.12rem;
}
}
}
+6 -6
View File
@@ -39,19 +39,19 @@ h1, h2, h3, h4, h5, h6 {
}
h1 {
font-size: var(--text-4xl);
font-size: var(--text-3xl);
margin-bottom: var(--space-lg);
}
h2 {
font-size: var(--text-3xl);
font-size: var(--text-2xl);
padding-left: var(--space-md);
border-left: 3px solid var(--accent);
margin-bottom: var(--space-sm);
}
h3 {
font-size: var(--text-2xl);
font-size: var(--text-xl);
position: relative;
padding-left: var(--space-lg);
@@ -71,16 +71,16 @@ h3 {
}
h4 {
font-size: var(--text-xl);
font-size: var(--text-md);
color: var(--ink-2);
}
h5 {
font-size: var(--text-lg);
font-size: var(--text-base);
color: var(--ink-2);
}
h6 {
font-size: var(--text-base);
font-size: var(--text-sm);
color: var(--ink-3);
}
+1
View File
@@ -2,6 +2,7 @@
@use 'mixins';
@use 'colors';
@use 'typography';
@use 'content';
@use 'spacing';
@use 'animations';
@use 'comment';
-8
View File
@@ -1,8 +0,0 @@
(function(){
return fetch('/apis/api.halo.run/v1alpha1/posts/5152aea5-c2e8-4717-8bba-2263d46e19d5/likes', {method: 'POST'})
.then(function(r){
return r.text().then(function(t){
return JSON.stringify({status: r.status, body: t.substring(0, 500)});
});
});
})()
+9 -1
View File
@@ -20,7 +20,15 @@ spec:
description: 留言板
screenshot:
file: page_messageboard.html
version: "1.1.1"
- name: Subscriptions
description: 订阅页面(展示博友文章订阅动态,依赖链接管理插件)
screenshot:
file: page_subscriptions.html
- name: WeRead
description: 微信读书书架(依赖微信读书插件)
screenshot:
file: weread.html
version: "1.1.5"
requires: ">=2.24.0"
license:
- name: "GPL-3.0"
+3 -4
View File
@@ -1,10 +1,9 @@
{
"extends": "astro/tsconfigs/strict",
"include": [".astro/types.d.ts", "**/*"],
"exclude": ["dist"],
"include": [".astro/types.d.ts", "src/**/*"],
"exclude": ["dist", "templates", "node_modules", "docs"],
"compilerOptions": {
"strictNullChecks": true,
"jsx": "preserve",
"types": ["unplugin-icons/types/vue"]
"jsx": "preserve"
}
}
-60
View File
@@ -1,60 +0,0 @@
const path = require('path');
const pageCdpUrl = process.argv[2];
const filePath = process.argv[3];
async function uploadFile() {
const ws = new WebSocket(pageCdpUrl);
let msgId = 1;
const pending = new Map();
ws.addEventListener('message', function(event) {
const msg = JSON.parse(event.data);
if (msg.id && pending.has(msg.id)) {
const { resolve, reject } = pending.get(msg.id);
pending.delete(msg.id);
if (msg.error) reject(msg.error);
else resolve(msg.result);
}
});
function send(method, params = {}) {
return new Promise((resolve, reject) => {
const id = msgId++;
pending.set(id, { resolve, reject });
ws.send(JSON.stringify({ id, method, params }));
});
}
await new Promise(resolve => ws.addEventListener('open', resolve, { once: true }));
const docResult = await send('DOM.getDocument');
const { nodeId } = await send('DOM.querySelector', {
nodeId: docResult.root.nodeId,
selector: 'input[type="file"]'
});
if (!nodeId) {
console.error('File input not found');
process.exit(1);
}
const absolutePath = path.resolve(filePath);
console.log('Setting file:', absolutePath);
await send('DOM.setFileInputFiles', {
nodeId,
files: [absolutePath]
});
console.log('File upload triggered successfully');
await new Promise(resolve => setTimeout(resolve, 3000));
ws.close();
}
uploadFile().catch(err => {
console.error('Error:', err);
process.exit(1);
});
-53
View File
@@ -1,53 +0,0 @@
const CDP_URL = 'ws://127.0.0.1:62529/devtools/browser/a36c1b63-8ff4-41d5-93fa-5c228657ef70';
const ZIP_FILE = 'c:\\Users\\Zhang\\Documents\\Halo\\WarmIsland\\dist\\warm-island-1.0.0.zip';
async function main() {
const ws = new WebSocket(CDP_URL);
let msgId = 1;
const pending = new Map();
ws.addEventListener('message', (event) => {
const msg = JSON.parse(event.data);
if (msg.id && pending.has(msg.id)) {
const { resolve, reject } = pending.get(msg.id);
pending.delete(msg.id);
if (msg.error) reject(msg.error);
else resolve(msg.result);
}
});
const send = (method, params = {}, sessionId) => new Promise((resolve, reject) => {
const id = msgId++;
const msg = { id, method, params };
if (sessionId) msg.sessionId = sessionId;
pending.set(id, { resolve, reject });
ws.send(JSON.stringify(msg));
});
await new Promise(r => ws.addEventListener('open', r, { once: true }));
try {
const { targetInfos } = await send('Target.getTargets');
const pageTarget = targetInfos.find(t => t.type === 'page' && t.url.includes('localhost:8090'));
if (!pageTarget) { console.error('No page target found'); process.exit(1); }
const { sessionId } = await send('Target.attachToTarget', { targetId: pageTarget.targetId, flatten: true });
const doc = await send('DOM.getDocument', {}, sessionId);
const { nodeId } = await send('DOM.querySelector', { nodeId: doc.root.nodeId, selector: 'input[type="file"][accept=".zip"]' }, sessionId);
if (!nodeId) { console.error('File input not found'); process.exit(1); }
console.log('Found file input node:', nodeId);
await send('DOM.setFileInputFiles', { nodeId, files: [ZIP_FILE] }, sessionId);
console.log('File upload triggered successfully!');
ws.close();
} catch (err) {
console.error('Error:', JSON.stringify(err));
ws.close();
process.exit(1);
}
}
main();