Initial commit

This commit is contained in:
wfz
2026-05-13 16:24:00 +08:00
commit 5728d3cbda
55 changed files with 37267 additions and 0 deletions

View File

@@ -0,0 +1,43 @@
{
"hash": "0daa58b8",
"configHash": "a23338b4",
"lockfileHash": "179ea88f",
"browserHash": "6f87fcc3",
"optimized": {
"vue": {
"src": "../../../../node_modules/vue/dist/vue.runtime.esm-bundler.js",
"file": "vue.js",
"fileHash": "69e73e5b",
"needsInterop": false
},
"vitepress > @vue/devtools-api": {
"src": "../../../../node_modules/vitepress/node_modules/@vue/devtools-api/dist/index.js",
"file": "vitepress___@vue_devtools-api.js",
"fileHash": "9a76c491",
"needsInterop": false
},
"vitepress > @vueuse/core": {
"src": "../../../../node_modules/@vueuse/core/index.mjs",
"file": "vitepress___@vueuse_core.js",
"fileHash": "4bc31a77",
"needsInterop": false
},
"pinia": {
"src": "../../../../node_modules/pinia/dist/pinia.mjs",
"file": "pinia.js",
"fileHash": "3af78da8",
"needsInterop": false
},
"pinia-plugin-persistedstate": {
"src": "../../../../node_modules/pinia-plugin-persistedstate/dist/index.js",
"file": "pinia-plugin-persistedstate.js",
"fileHash": "d77b5825",
"needsInterop": false
}
},
"chunks": {
"chunk-SNNOYR6U": {
"file": "chunk-SNNOYR6U.js"
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,3 @@
{
"type": "module"
}

View File

@@ -0,0 +1,137 @@
// node_modules/pinia-plugin-persistedstate/dist/index.js
function isObject(v) {
return typeof v === "object" && v !== null;
}
function normalizeOptions(options, factoryOptions) {
options = isObject(options) ? options : /* @__PURE__ */ Object.create(null);
return new Proxy(options, {
get(target, key, receiver) {
if (key === "key")
return Reflect.get(target, key, receiver);
return Reflect.get(target, key, receiver) || Reflect.get(factoryOptions, key, receiver);
}
});
}
function get(state, path) {
return path.reduce((obj, p) => {
return obj == null ? void 0 : obj[p];
}, state);
}
function set(state, path, val) {
return path.slice(0, -1).reduce((obj, p) => {
if (/^(__proto__)$/.test(p))
return {};
else return obj[p] = obj[p] || {};
}, state)[path[path.length - 1]] = val, state;
}
function pick(baseState, paths) {
return paths.reduce((substate, path) => {
const pathArray = path.split(".");
return set(substate, pathArray, get(baseState, pathArray));
}, {});
}
function parsePersistence(factoryOptions, store) {
return (o) => {
var _a;
try {
const {
storage = localStorage,
beforeRestore = void 0,
afterRestore = void 0,
serializer = {
serialize: JSON.stringify,
deserialize: JSON.parse
},
key = store.$id,
paths = null,
debug = false
} = o;
return {
storage,
beforeRestore,
afterRestore,
serializer,
key: ((_a = factoryOptions.key) != null ? _a : (k) => k)(typeof key == "string" ? key : key(store.$id)),
paths,
debug
};
} catch (e) {
if (o.debug)
console.error("[pinia-plugin-persistedstate]", e);
return null;
}
};
}
function hydrateStore(store, { storage, serializer, key, debug }) {
try {
const fromStorage = storage == null ? void 0 : storage.getItem(key);
if (fromStorage)
store.$patch(serializer == null ? void 0 : serializer.deserialize(fromStorage));
} catch (e) {
if (debug)
console.error("[pinia-plugin-persistedstate]", e);
}
}
function persistState(state, { storage, serializer, key, paths, debug }) {
try {
const toStore = Array.isArray(paths) ? pick(state, paths) : state;
storage.setItem(key, serializer.serialize(toStore));
} catch (e) {
if (debug)
console.error("[pinia-plugin-persistedstate]", e);
}
}
function createPersistedState(factoryOptions = {}) {
return (context) => {
const { auto = false } = factoryOptions;
const {
options: { persist = auto },
store,
pinia
} = context;
if (!persist)
return;
if (!(store.$id in pinia.state.value)) {
const original_store = pinia._s.get(store.$id.replace("__hot:", ""));
if (original_store)
Promise.resolve().then(() => original_store.$persist());
return;
}
const persistences = (Array.isArray(persist) ? persist.map((p) => normalizeOptions(p, factoryOptions)) : [normalizeOptions(persist, factoryOptions)]).map(parsePersistence(factoryOptions, store)).filter(Boolean);
store.$persist = () => {
persistences.forEach((persistence) => {
persistState(store.$state, persistence);
});
};
store.$hydrate = ({ runHooks = true } = {}) => {
persistences.forEach((persistence) => {
const { beforeRestore, afterRestore } = persistence;
if (runHooks)
beforeRestore == null ? void 0 : beforeRestore(context);
hydrateStore(store, persistence);
if (runHooks)
afterRestore == null ? void 0 : afterRestore(context);
});
};
persistences.forEach((persistence) => {
const { beforeRestore, afterRestore } = persistence;
beforeRestore == null ? void 0 : beforeRestore(context);
hydrateStore(store, persistence);
afterRestore == null ? void 0 : afterRestore(context);
store.$subscribe(
(_mutation, state) => {
persistState(state, persistence);
},
{
detached: true
}
);
});
};
}
var src_default = createPersistedState();
export {
createPersistedState,
src_default as default
};
//# sourceMappingURL=pinia-plugin-persistedstate.js.map

View File

@@ -0,0 +1,7 @@
{
"version": 3,
"sources": ["../../../../node_modules/pinia-plugin-persistedstate/dist/index.js"],
"sourcesContent": ["// src/normalize.ts\nfunction isObject(v) {\n return typeof v === \"object\" && v !== null;\n}\nfunction normalizeOptions(options, factoryOptions) {\n options = isObject(options) ? options : /* @__PURE__ */ Object.create(null);\n return new Proxy(options, {\n get(target, key, receiver) {\n if (key === \"key\")\n return Reflect.get(target, key, receiver);\n return Reflect.get(target, key, receiver) || Reflect.get(factoryOptions, key, receiver);\n }\n });\n}\n\n// src/pick.ts\nfunction get(state, path) {\n return path.reduce((obj, p) => {\n return obj == null ? void 0 : obj[p];\n }, state);\n}\nfunction set(state, path, val) {\n return path.slice(0, -1).reduce((obj, p) => {\n if (/^(__proto__)$/.test(p))\n return {};\n else return obj[p] = obj[p] || {};\n }, state)[path[path.length - 1]] = val, state;\n}\nfunction pick(baseState, paths) {\n return paths.reduce((substate, path) => {\n const pathArray = path.split(\".\");\n return set(substate, pathArray, get(baseState, pathArray));\n }, {});\n}\n\n// src/plugin.ts\nfunction parsePersistence(factoryOptions, store) {\n return (o) => {\n var _a;\n try {\n const {\n storage = localStorage,\n beforeRestore = void 0,\n afterRestore = void 0,\n serializer = {\n serialize: JSON.stringify,\n deserialize: JSON.parse\n },\n key = store.$id,\n paths = null,\n debug = false\n } = o;\n return {\n storage,\n beforeRestore,\n afterRestore,\n serializer,\n key: ((_a = factoryOptions.key) != null ? _a : (k) => k)(typeof key == \"string\" ? key : key(store.$id)),\n paths,\n debug\n };\n } catch (e) {\n if (o.debug)\n console.error(\"[pinia-plugin-persistedstate]\", e);\n return null;\n }\n };\n}\nfunction hydrateStore(store, { storage, serializer, key, debug }) {\n try {\n const fromStorage = storage == null ? void 0 : storage.getItem(key);\n if (fromStorage)\n store.$patch(serializer == null ? void 0 : serializer.deserialize(fromStorage));\n } catch (e) {\n if (debug)\n console.error(\"[pinia-plugin-persistedstate]\", e);\n }\n}\nfunction persistState(state, { storage, serializer, key, paths, debug }) {\n try {\n const toStore = Array.isArray(paths) ? pick(state, paths) : state;\n storage.setItem(key, serializer.serialize(toStore));\n } catch (e) {\n if (debug)\n console.error(\"[pinia-plugin-persistedstate]\", e);\n }\n}\nfunction createPersistedState(factoryOptions = {}) {\n return (context) => {\n const { auto = false } = factoryOptions;\n const {\n options: { persist = auto },\n store,\n pinia\n } = context;\n if (!persist)\n return;\n if (!(store.$id in pinia.state.value)) {\n const original_store = pinia._s.get(store.$id.replace(\"__hot:\", \"\"));\n if (original_store)\n Promise.resolve().then(() => original_store.$persist());\n return;\n }\n const persistences = (Array.isArray(persist) ? persist.map((p) => normalizeOptions(p, factoryOptions)) : [normalizeOptions(persist, factoryOptions)]).map(parsePersistence(factoryOptions, store)).filter(Boolean);\n store.$persist = () => {\n persistences.forEach((persistence) => {\n persistState(store.$state, persistence);\n });\n };\n store.$hydrate = ({ runHooks = true } = {}) => {\n persistences.forEach((persistence) => {\n const { beforeRestore, afterRestore } = persistence;\n if (runHooks)\n beforeRestore == null ? void 0 : beforeRestore(context);\n hydrateStore(store, persistence);\n if (runHooks)\n afterRestore == null ? void 0 : afterRestore(context);\n });\n };\n persistences.forEach((persistence) => {\n const { beforeRestore, afterRestore } = persistence;\n beforeRestore == null ? void 0 : beforeRestore(context);\n hydrateStore(store, persistence);\n afterRestore == null ? void 0 : afterRestore(context);\n store.$subscribe(\n (_mutation, state) => {\n persistState(state, persistence);\n },\n {\n detached: true\n }\n );\n });\n };\n}\n\n// src/index.ts\nvar src_default = createPersistedState();\nexport {\n createPersistedState,\n src_default as default\n};\n"],
"mappings": ";AACA,SAAS,SAAS,GAAG;AACnB,SAAO,OAAO,MAAM,YAAY,MAAM;AACxC;AACA,SAAS,iBAAiB,SAAS,gBAAgB;AACjD,YAAU,SAAS,OAAO,IAAI,UAA0B,uBAAO,OAAO,IAAI;AAC1E,SAAO,IAAI,MAAM,SAAS;AAAA,IACxB,IAAI,QAAQ,KAAK,UAAU;AACzB,UAAI,QAAQ;AACV,eAAO,QAAQ,IAAI,QAAQ,KAAK,QAAQ;AAC1C,aAAO,QAAQ,IAAI,QAAQ,KAAK,QAAQ,KAAK,QAAQ,IAAI,gBAAgB,KAAK,QAAQ;AAAA,IACxF;AAAA,EACF,CAAC;AACH;AAGA,SAAS,IAAI,OAAO,MAAM;AACxB,SAAO,KAAK,OAAO,CAAC,KAAK,MAAM;AAC7B,WAAO,OAAO,OAAO,SAAS,IAAI,CAAC;AAAA,EACrC,GAAG,KAAK;AACV;AACA,SAAS,IAAI,OAAO,MAAM,KAAK;AAC7B,SAAO,KAAK,MAAM,GAAG,EAAE,EAAE,OAAO,CAAC,KAAK,MAAM;AAC1C,QAAI,gBAAgB,KAAK,CAAC;AACxB,aAAO,CAAC;AAAA,QACL,QAAO,IAAI,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC;AAAA,EAClC,GAAG,KAAK,EAAE,KAAK,KAAK,SAAS,CAAC,CAAC,IAAI,KAAK;AAC1C;AACA,SAAS,KAAK,WAAW,OAAO;AAC9B,SAAO,MAAM,OAAO,CAAC,UAAU,SAAS;AACtC,UAAM,YAAY,KAAK,MAAM,GAAG;AAChC,WAAO,IAAI,UAAU,WAAW,IAAI,WAAW,SAAS,CAAC;AAAA,EAC3D,GAAG,CAAC,CAAC;AACP;AAGA,SAAS,iBAAiB,gBAAgB,OAAO;AAC/C,SAAO,CAAC,MAAM;AACZ,QAAI;AACJ,QAAI;AACF,YAAM;AAAA,QACJ,UAAU;AAAA,QACV,gBAAgB;AAAA,QAChB,eAAe;AAAA,QACf,aAAa;AAAA,UACX,WAAW,KAAK;AAAA,UAChB,aAAa,KAAK;AAAA,QACpB;AAAA,QACA,MAAM,MAAM;AAAA,QACZ,QAAQ;AAAA,QACR,QAAQ;AAAA,MACV,IAAI;AACJ,aAAO;AAAA,QACL;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA,OAAO,KAAK,eAAe,QAAQ,OAAO,KAAK,CAAC,MAAM,GAAG,OAAO,OAAO,WAAW,MAAM,IAAI,MAAM,GAAG,CAAC;AAAA,QACtG;AAAA,QACA;AAAA,MACF;AAAA,IACF,SAAS,GAAG;AACV,UAAI,EAAE;AACJ,gBAAQ,MAAM,iCAAiC,CAAC;AAClD,aAAO;AAAA,IACT;AAAA,EACF;AACF;AACA,SAAS,aAAa,OAAO,EAAE,SAAS,YAAY,KAAK,MAAM,GAAG;AAChE,MAAI;AACF,UAAM,cAAc,WAAW,OAAO,SAAS,QAAQ,QAAQ,GAAG;AAClE,QAAI;AACF,YAAM,OAAO,cAAc,OAAO,SAAS,WAAW,YAAY,WAAW,CAAC;AAAA,EAClF,SAAS,GAAG;AACV,QAAI;AACF,cAAQ,MAAM,iCAAiC,CAAC;AAAA,EACpD;AACF;AACA,SAAS,aAAa,OAAO,EAAE,SAAS,YAAY,KAAK,OAAO,MAAM,GAAG;AACvE,MAAI;AACF,UAAM,UAAU,MAAM,QAAQ,KAAK,IAAI,KAAK,OAAO,KAAK,IAAI;AAC5D,YAAQ,QAAQ,KAAK,WAAW,UAAU,OAAO,CAAC;AAAA,EACpD,SAAS,GAAG;AACV,QAAI;AACF,cAAQ,MAAM,iCAAiC,CAAC;AAAA,EACpD;AACF;AACA,SAAS,qBAAqB,iBAAiB,CAAC,GAAG;AACjD,SAAO,CAAC,YAAY;AAClB,UAAM,EAAE,OAAO,MAAM,IAAI;AACzB,UAAM;AAAA,MACJ,SAAS,EAAE,UAAU,KAAK;AAAA,MAC1B;AAAA,MACA;AAAA,IACF,IAAI;AACJ,QAAI,CAAC;AACH;AACF,QAAI,EAAE,MAAM,OAAO,MAAM,MAAM,QAAQ;AACrC,YAAM,iBAAiB,MAAM,GAAG,IAAI,MAAM,IAAI,QAAQ,UAAU,EAAE,CAAC;AACnE,UAAI;AACF,gBAAQ,QAAQ,EAAE,KAAK,MAAM,eAAe,SAAS,CAAC;AACxD;AAAA,IACF;AACA,UAAM,gBAAgB,MAAM,QAAQ,OAAO,IAAI,QAAQ,IAAI,CAAC,MAAM,iBAAiB,GAAG,cAAc,CAAC,IAAI,CAAC,iBAAiB,SAAS,cAAc,CAAC,GAAG,IAAI,iBAAiB,gBAAgB,KAAK,CAAC,EAAE,OAAO,OAAO;AACjN,UAAM,WAAW,MAAM;AACrB,mBAAa,QAAQ,CAAC,gBAAgB;AACpC,qBAAa,MAAM,QAAQ,WAAW;AAAA,MACxC,CAAC;AAAA,IACH;AACA,UAAM,WAAW,CAAC,EAAE,WAAW,KAAK,IAAI,CAAC,MAAM;AAC7C,mBAAa,QAAQ,CAAC,gBAAgB;AACpC,cAAM,EAAE,eAAe,aAAa,IAAI;AACxC,YAAI;AACF,2BAAiB,OAAO,SAAS,cAAc,OAAO;AACxD,qBAAa,OAAO,WAAW;AAC/B,YAAI;AACF,0BAAgB,OAAO,SAAS,aAAa,OAAO;AAAA,MACxD,CAAC;AAAA,IACH;AACA,iBAAa,QAAQ,CAAC,gBAAgB;AACpC,YAAM,EAAE,eAAe,aAAa,IAAI;AACxC,uBAAiB,OAAO,SAAS,cAAc,OAAO;AACtD,mBAAa,OAAO,WAAW;AAC/B,sBAAgB,OAAO,SAAS,aAAa,OAAO;AACpD,YAAM;AAAA,QACJ,CAAC,WAAW,UAAU;AACpB,uBAAa,OAAO,WAAW;AAAA,QACjC;AAAA,QACA;AAAA,UACE,UAAU;AAAA,QACZ;AAAA,MACF;AAAA,IACF,CAAC;AAAA,EACH;AACF;AAGA,IAAI,cAAc,qBAAqB;",
"names": []
}

1715
docs/.vitepress/cache/deps/pinia.js vendored Normal file

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
docs/.vitepress/cache/deps/vue.js vendored Normal file
View File

@@ -0,0 +1,347 @@
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-SNNOYR6U.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
docs/.vitepress/cache/deps/vue.js.map vendored Normal file
View File

@@ -0,0 +1,7 @@
{
"version": 3,
"sources": [],
"sourcesContent": [],
"mappings": "",
"names": []
}

View File

@@ -0,0 +1,21 @@
import { defineConfig } from 'vitepress'
export default defineConfig({
title: "乌仿镇",
description: "技术分享笔记",
lang: 'zh-CN',
vite: {
server: {
proxy: {
'/api': {
target: 'https://wufangzhen.com',
changeOrigin: true,
secure: false
}
}
}
},
sitemap: {
hostname: 'https://wufangzhen.com'
}
})

View File

@@ -0,0 +1,311 @@
<script setup lang="ts">
import { onMounted, onUnmounted, computed } from 'vue'
import { useData } from 'vitepress'
import { useSettingsStore } from './stores/settings'
import { isMobileDevice } from './utils'
import VideoBackground from './components/VideoBackground.vue'
import Dock from './components/Dock.vue'
import DraggableWidget from './components/DraggableWidget.vue'
import Window from './components/Window.vue'
import Calendar from './components/Calendar.vue'
import TaoXin from './components/TaoXin.vue'
import Cafe from './components/Cafe.vue'
import Settings from './components/Settings.vue'
import DocSidebar from './components/DocSidebar.vue'
import sidebarData from './data/sidebar.json'
const { frontmatter } = useData()
const settingsStore = useSettingsStore()
settingsStore.initWidgets(['calendar', 'taoxin', 'cafe'])
const isHome = computed(() => frontmatter.value.home === true)
const blurAmount = computed(() => settingsStore.theme.blurAmount ?? 20)
const backgroundColor = computed(() => settingsStore.theme.backgroundColor ?? 'rgba(30, 30, 30, 0.7)')
const navItems = computed(() => {
return sidebarData.map(group => ({
text: group.title,
link: group.path
}))
})
const handleResize = () => {
settingsStore.setMobile(isMobileDevice())
}
onMounted(() => {
handleResize()
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
})
</script>
<template>
<div class="app-container">
<VideoBackground />
<template v-if="isHome">
<div class="home-container" :class="{ 'home-mobile': settingsStore.isMobile }">
<DraggableWidget id="calendar" :width="320" :height="400">
<Calendar />
</DraggableWidget>
<DraggableWidget id="taoxin" :width="300" :height="280">
<TaoXin />
</DraggableWidget>
<DraggableWidget id="cafe" :width="300" :height="300">
<Cafe />
</DraggableWidget>
</div>
<Dock />
<Window id="settings" title="设置中心" :default-width="800" :default-height="600">
<Settings />
</Window>
</template>
<template v-else>
<div class="doc-page">
<header class="doc-header">
<a href="/" class="logo">乌仿镇</a>
<nav class="doc-nav">
<a href="/" class="nav-link">首页</a>
<a
v-for="item in navItems"
:key="item.link"
:href="item.link"
class="nav-link"
>
{{ item.text }}
</a>
</nav>
</header>
<div class="doc-layout" :style="{
background: backgroundColor,
backdropFilter: 'blur(' + blurAmount + 'px)',
WebkitBackdropFilter: 'blur(' + blurAmount + 'px)'
}">
<aside class="doc-sidebar">
<DocSidebar />
</aside>
<main class="doc-main">
<Content />
</main>
</div>
</div>
<Dock />
<Window id="settings" title="设置中心" :default-width="800" :default-height="600">
<Settings />
</Window>
</template>
</div>
</template>
<style scoped>
.app-container {
min-height: 100vh;
position: relative;
}
.home-container {
position: relative;
min-height: 100vh;
padding-bottom: 100px;
}
.home-mobile {
display: flex;
flex-direction: column;
padding: 10px;
padding-bottom: 120px;
}
.doc-page {
min-height: 100vh;
}
.doc-header {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 60px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24px;
background: rgba(30, 30, 30, 0.9);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
z-index: 50;
}
.logo {
font-size: 20px;
font-weight: 700;
color: white;
text-decoration: none;
}
.doc-nav {
display: flex;
gap: 24px;
}
.nav-link {
color: rgba(255, 255, 255, 0.8);
text-decoration: none;
font-size: 14px;
transition: color 0.2s ease;
}
.nav-link:hover {
color: white;
}
.doc-layout {
position: fixed;
top: 60px;
left: 0;
right: 0;
bottom: 0;
display: flex;
overflow: hidden;
}
.doc-sidebar {
width: 280px;
padding: 20px;
height: 100%;
overflow-y: auto;
border-right: 1px solid rgba(255, 255, 255, 0.1);
flex-shrink: 0;
}
.doc-main {
flex: 1;
padding: 20px 40px;
padding-bottom: 120px;
overflow-y: auto;
height: 100%;
}
.doc-main :deep(*) {
color: white;
}
.doc-main :deep(a) {
color: #007AFF;
}
.doc-main :deep(h1),
.doc-main :deep(h2),
.doc-main :deep(h3) {
color: white;
border-bottom-color: rgba(255, 255, 255, 0.1);
padding-bottom: 8px;
margin-top: 24px;
}
.doc-main :deep(h1) {
font-size: 28px;
}
.doc-main :deep(h2) {
font-size: 22px;
}
.doc-main :deep(h3) {
font-size: 18px;
}
.doc-main :deep(p) {
line-height: 1.8;
margin: 16px 0;
}
.doc-main :deep(code) {
background: rgba(255, 255, 255, 0.1);
padding: 2px 6px;
border-radius: 4px;
font-family: 'Fira Code', monospace;
}
.doc-main :deep(pre) {
background: rgba(0, 0, 0, 0.4);
border-radius: 8px;
padding: 16px;
overflow-x: auto;
margin: 16px 0;
}
.doc-main :deep(pre code) {
background: transparent;
padding: 0;
}
.doc-main :deep(blockquote) {
border-left: 4px solid #007AFF;
background: rgba(255, 255, 255, 0.05);
padding: 12px 16px;
margin: 16px 0;
border-radius: 0 8px 8px 0;
}
.doc-main :deep(table) {
width: 100%;
border-collapse: collapse;
margin: 16px 0;
}
.doc-main :deep(th),
.doc-main :deep(td) {
border: 1px solid rgba(255, 255, 255, 0.1);
padding: 12px;
text-align: left;
}
.doc-main :deep(th) {
background: rgba(255, 255, 255, 0.1);
}
.doc-main :deep(ul),
.doc-main :deep(ol) {
padding-left: 24px;
margin: 16px 0;
}
.doc-main :deep(li) {
margin: 8px 0;
}
@media (max-width: 768px) {
.doc-sidebar {
display: none;
}
.doc-main {
padding: 16px;
padding-bottom: 120px;
}
.doc-header {
padding: 0 16px;
}
.doc-nav {
gap: 16px;
}
}
</style>

View File

@@ -0,0 +1,97 @@
<script setup lang="ts">
import { ref } from 'vue'
const links = [
{ name: 'Java 笔记', path: '/java/', icon: '☕', description: 'Java技术栈学习笔记' },
{ name: 'Vue 笔记', path: '/vue/', icon: '💚', description: 'Vue框架学习笔记' }
]
</script>
<template>
<div class="cafe">
<div class="cafe-header">
<span class="cafe-icon"></span>
<span class="cafe-title">咖啡厅</span>
</div>
<div class="cafe-links">
<a
v-for="link in links"
:key="link.path"
:href="link.path"
class="cafe-link"
>
<span class="link-icon">{{ link.icon }}</span>
<div class="link-info">
<span class="link-name">{{ link.name }}</span>
<span class="link-desc">{{ link.description }}</span>
</div>
</a>
</div>
</div>
</template>
<style scoped>
.cafe {
height: 100%;
color: white;
}
.cafe-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 16px;
}
.cafe-icon {
font-size: 24px;
}
.cafe-title {
font-size: 18px;
font-weight: 600;
}
.cafe-links {
display: flex;
flex-direction: column;
gap: 12px;
}
.cafe-link {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
border-radius: 12px;
background: rgba(255, 255, 255, 0.1);
text-decoration: none;
color: white;
transition: all 0.2s ease;
}
.cafe-link:hover {
background: rgba(255, 255, 255, 0.2);
transform: translateX(4px);
}
.link-icon {
font-size: 24px;
}
.link-info {
display: flex;
flex-direction: column;
gap: 2px;
}
.link-name {
font-size: 14px;
font-weight: 500;
}
.link-desc {
font-size: 12px;
opacity: 0.7;
}
</style>

View File

@@ -0,0 +1,240 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted, computed } from 'vue'
const currentTime = ref(new Date())
const selectedDate = ref(new Date())
let timer: ReturnType<typeof setInterval>
onMounted(() => {
timer = setInterval(() => {
currentTime.value = new Date()
}, 1000)
})
onUnmounted(() => {
clearInterval(timer)
})
const timeString = computed(() => {
const hours = currentTime.value.getHours().toString().padStart(2, '0')
const minutes = currentTime.value.getMinutes().toString().padStart(2, '0')
const seconds = currentTime.value.getSeconds().toString().padStart(2, '0')
return `${hours}:${minutes}:${seconds}`
})
const dateString = computed(() => {
const year = currentTime.value.getFullYear()
const month = (currentTime.value.getMonth() + 1).toString().padStart(2, '0')
const day = currentTime.value.getDate().toString().padStart(2, '0')
const weekDays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
const weekDay = weekDays[currentTime.value.getDay()]
return `${year}${month}${day}${weekDay}`
})
const currentYear = computed(() => selectedDate.value.getFullYear())
const currentMonth = computed(() => selectedDate.value.getMonth())
const monthNames = [
'一月', '二月', '三月', '四月', '五月', '六月',
'七月', '八月', '九月', '十月', '十一月', '十二月'
]
const weekDayNames = ['日', '一', '二', '三', '四', '五', '六']
const daysInMonth = computed(() => {
return new Date(currentYear.value, currentMonth.value + 1, 0).getDate()
})
const firstDayOfMonth = computed(() => {
return new Date(currentYear.value, currentMonth.value, 1).getDay()
})
const calendarDays = computed(() => {
const days = []
for (let i = 0; i < firstDayOfMonth.value; i++) {
days.push(null)
}
for (let i = 1; i <= daysInMonth.value; i++) {
days.push(i)
}
return days
})
function prevMonth() {
selectedDate.value = new Date(currentYear.value, currentMonth.value - 1, 1)
}
function nextMonth() {
selectedDate.value = new Date(currentYear.value, currentMonth.value + 1, 1)
}
function isToday(day: number | null) {
if (!day) return false
return (
day === currentTime.value.getDate() &&
currentMonth.value === currentTime.value.getMonth() &&
currentYear.value === currentTime.value.getFullYear()
)
}
</script>
<template>
<div class="calendar">
<div class="calendar-header">
<div class="time">{{ timeString }}</div>
<div class="date">{{ dateString }}</div>
</div>
<div class="calendar-body">
<div class="month-nav">
<button class="nav-btn" @click="prevMonth"></button>
<span class="month-title">{{ monthNames[currentMonth] }} {{ currentYear }}</span>
<button class="nav-btn" @click="nextMonth"></button>
</div>
<div class="weekdays">
<div v-for="day in weekDayNames" :key="day" class="weekday">{{ day }}</div>
</div>
<div class="calendar-grid">
<div
v-for="(day, index) in calendarDays"
:key="index"
class="calendar-day"
:class="{ 'calendar-day-today': isToday(day), 'calendar-day-empty': !day }"
>
{{ day }}
</div>
</div>
</div>
</div>
</template>
<style scoped>
.calendar {
color: white;
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
}
.calendar-header {
text-align: center;
padding-bottom: 10px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
margin-bottom: 10px;
flex-shrink: 0;
}
.time {
font-size: 28px;
font-weight: 200;
letter-spacing: 2px;
font-family: 'SF Pro Display', -apple-system, BlinkMacSystemFont, sans-serif;
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
margin-bottom: 2px;
}
.date {
font-size: 12px;
opacity: 0.8;
font-weight: 400;
}
.calendar-body {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
overflow: hidden;
}
.month-nav {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
flex-shrink: 0;
}
.nav-btn {
background: rgba(255, 255, 255, 0.1);
border: none;
color: white;
width: 24px;
height: 24px;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
transition: all 0.2s ease;
}
.nav-btn:hover {
background: rgba(255, 255, 255, 0.2);
}
.month-title {
font-size: 13px;
font-weight: 600;
}
.weekdays {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 2px;
margin-bottom: 4px;
flex-shrink: 0;
}
.weekday {
text-align: center;
font-size: 11px;
font-weight: 600;
opacity: 0.6;
padding: 2px 0;
}
.calendar-grid {
flex: 1;
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 2px;
min-height: 0;
}
.calendar-day {
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
border-radius: 4px;
transition: all 0.2s ease;
cursor: pointer;
background: rgba(255, 255, 255, 0.05);
min-height: 0;
flex: 1;
}
.calendar-day:hover:not(.calendar-day-empty) {
background: rgba(255, 255, 255, 0.15);
}
.calendar-day-today {
background: rgba(0, 122, 255, 0.4);
font-weight: 600;
color: white;
}
.calendar-day-empty {
background: transparent;
cursor: default;
}
</style>

View File

@@ -0,0 +1,63 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useData } from 'vitepress'
import sidebarData from '../data/sidebar.json'
const { page } = useData()
const currentSidebar = computed(() => {
const path = page.value.relativePath
const pathPrefix = path.split('/')[0]
return sidebarData.find(group => group.path === `/${pathPrefix}/`)
})
</script>
<template>
<div class="sidebar" v-if="currentSidebar">
<h2 class="sidebar-title">{{ currentSidebar.title }} 笔记</h2>
<ul class="sidebar-list">
<li v-for="item in currentSidebar.items" :key="item.link">
<a :href="item.link" class="sidebar-link">{{ item.text }}</a>
</li>
</ul>
</div>
</template>
<style scoped>
.sidebar {
color: white;
}
.sidebar-title {
font-size: 14px;
font-weight: 600;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.sidebar-list {
list-style: none;
padding: 0;
margin: 0;
}
.sidebar-list li {
margin: 4px 0;
}
.sidebar-link {
color: rgba(255, 255, 255, 0.7);
text-decoration: none;
font-size: 14px;
display: block;
padding: 8px 12px;
border-radius: 6px;
transition: all 0.2s ease;
}
.sidebar-link:hover {
background: rgba(255, 255, 255, 0.1);
color: white;
}
</style>

View File

@@ -0,0 +1,204 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useSettingsStore } from '../stores/settings'
const settingsStore = useSettingsStore()
const dockItems = [
{
id: 'openlist',
name: 'OpenList',
url: 'https://openlist.wufangzhen.com',
icon: 'M4 6h16M4 12h16M4 18h16'
},
{
id: 'gitea',
name: 'Gitea',
url: 'https://gitea.wufangzhen.com',
icon: 'M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5'
},
{
id: 'baota',
name: '宝塔',
url: 'https://baota.wufangzhen.com',
icon: 'M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zM9 17H7v-7h2v7zm4 0h-2V7h2v10zm4 0h-2v-4h2v4z'
},
{
id: 'jrebel',
name: 'JRebel',
url: 'https://jrebel.wufangzhen.com',
icon: 'M13 10V3L4 14h7v7l9-11h-7z'
},
{
id: 'opencode',
name: 'OpenCode',
url: 'https://opencode.wufangzhen.com',
icon: 'M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z'
}
]
const isMobile = computed(() => settingsStore.isMobile)
function openSettings() {
settingsStore.openWindow('settings')
}
</script>
<template>
<div class="dock" :class="{ 'dock-mobile': isMobile }">
<div class="dock-container">
<a
v-for="item in dockItems"
:key="item.id"
:href="item.url"
target="_blank"
rel="noopener noreferrer"
class="dock-item"
>
<div class="dock-icon">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path :d="item.icon" />
</svg>
</div>
<div class="dock-tooltip">{{ item.name }}</div>
</a>
<button class="dock-item settings-btn" @click="openSettings">
<div class="dock-icon">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<circle cx="12" cy="12" r="3" />
</svg>
</div>
<div class="dock-tooltip">设置</div>
</button>
</div>
</div>
</template>
<style scoped>
.dock {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
z-index: 100;
}
.dock-container {
display: flex;
gap: 8px;
padding: 8px 16px;
background: rgba(40, 40, 40, 0.8);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-radius: 20px;
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}
.dock-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 8px 12px;
border-radius: 12px;
cursor: pointer;
transition: all 0.2s ease;
text-decoration: none;
background: transparent;
border: none;
color: white;
position: relative;
}
.dock-item:hover {
background: rgba(255, 255, 255, 0.1);
transform: scale(1.15);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
}
.dock-icon {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
}
.dock-icon svg {
width: 28px;
height: 28px;
color: white;
}
.dock-tooltip {
position: absolute;
bottom: 100%;
margin-bottom: 8px;
padding: 6px 12px;
background: rgba(0, 0, 0, 0.8);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border-radius: 6px;
font-size: 11px;
white-space: nowrap;
opacity: 0;
visibility: hidden;
transition: all 0.2s ease;
pointer-events: none;
color: white;
}
.dock-item:hover .dock-tooltip {
opacity: 1;
visibility: visible;
}
.dock-mobile {
bottom: 10px;
left: 10px;
right: 10px;
transform: none;
}
.dock-mobile .dock-container {
width: 100%;
justify-content: space-around;
border-radius: 16px;
}
.dock-mobile .dock-item {
padding: 10px;
}
.dock-mobile .dock-tooltip {
display: none;
}
.settings-btn {
cursor: pointer;
}
@media (max-width: 768px) {
.dock {
bottom: 10px;
left: 10px;
right: 10px;
transform: none;
}
.dock-container {
width: 100%;
justify-content: space-around;
border-radius: 16px;
}
.dock-item {
padding: 10px;
}
.dock-tooltip {
display: none;
}
}
</style>

View File

@@ -0,0 +1,266 @@
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, useSlots, nextTick } from 'vue'
import { useSettingsStore } from '../stores/settings'
import { clamp } from '../utils'
interface Props {
id: string
title?: string
width?: number
height?: number
}
const props = withDefaults(defineProps<Props>(), {
title: '',
width: 300,
height: 200
})
const settingsStore = useSettingsStore()
const slots = useSlots()
const elementRef = ref<HTMLElement | null>(null)
const isMounted = ref(false)
const isDragging = ref(false)
const dragStartPos = ref({ x: 0, y: 0 })
const elementStartPos = ref({ x: 0, y: 0 })
const isMobile = computed(() => settingsStore.isMobile)
const widgetState = computed(() => {
return settingsStore.widgets.find(w => w.id === props.id)
})
const position = computed(() => {
if (isMobile.value || !isMounted.value) return { x: 0, y: 0 }
const storedPos = widgetState.value?.position
if (storedPos && storedPos.x >= 0 && storedPos.y >= 0) {
return storedPos
}
return getDefaultPosition()
})
const order = computed(() => {
return widgetState.value?.order ?? 0
})
const blurAmount = computed(() => settingsStore.theme.blurAmount ?? 20)
const backgroundColor = computed(() => settingsStore.theme.backgroundColor ?? 'rgba(30, 30, 30, 0.7)')
function getDefaultPosition() {
const screenWidth = typeof window !== 'undefined' ? window.innerWidth : 1920
const screenHeight = typeof window !== 'undefined' ? window.innerHeight : 1080
const positions: Record<string, { x: number; y: number }> = {
calendar: { x: screenWidth - 340, y: 20 },
taoxin: { x: screenWidth - 320, y: screenHeight - 380 },
cafe: { x: screenWidth - 660, y: 20 }
}
return positions[props.id] ?? { x: 20, y: 20 }
}
onMounted(async () => {
await nextTick()
const storedPos = widgetState.value?.position
if (!storedPos || storedPos.x < 0 || storedPos.y < 0) {
const defaultPos = getDefaultPosition()
settingsStore.updateWidgetPosition(props.id, defaultPos)
}
isMounted.value = true
})
function handleDragStart(e: MouseEvent | TouchEvent, clientX: number, clientY: number) {
if (isMobile.value || !isMounted.value) return
e.preventDefault()
isDragging.value = true
dragStartPos.value = { x: clientX, y: clientY }
elementStartPos.value = { ...position.value }
if (e instanceof MouseEvent) {
document.addEventListener('mousemove', handleMouseMove)
document.addEventListener('mouseup', handleMouseUp)
} else {
document.addEventListener('touchmove', handleTouchMove, { passive: false })
document.addEventListener('touchend', handleTouchEnd)
}
}
function handleMouseDown(e: MouseEvent) {
handleDragStart(e, e.clientX, e.clientY)
}
function handleTouchStart(e: TouchEvent) {
if (e.touches.length !== 1) return
handleDragStart(e, e.touches[0].clientX, e.touches[0].clientY)
}
function handleMouseMove(e: MouseEvent) {
if (!isDragging.value) return
updatePosition(e.clientX, e.clientY)
}
function handleTouchMove(e: TouchEvent) {
if (!isDragging.value || e.touches.length !== 1) return
e.preventDefault()
updatePosition(e.touches[0].clientX, e.touches[0].clientY)
}
function updatePosition(clientX: number, clientY: number) {
const deltaX = clientX - dragStartPos.value.x
const deltaY = clientY - dragStartPos.value.y
const maxX = window.innerWidth - props.width - 20
const maxY = window.innerHeight - props.height - 100
const newX = clamp(elementStartPos.value.x + deltaX, 20, maxX)
const newY = clamp(elementStartPos.value.y + deltaY, 20, maxY)
settingsStore.updateWidgetPosition(props.id, { x: newX, y: newY })
}
function handleMouseUp() {
isDragging.value = false
document.removeEventListener('mousemove', handleMouseMove)
document.removeEventListener('mouseup', handleMouseUp)
}
function handleTouchEnd() {
isDragging.value = false
document.removeEventListener('touchmove', handleTouchMove)
document.removeEventListener('touchend', handleTouchEnd)
}
onUnmounted(() => {
document.removeEventListener('mousemove', handleMouseMove)
document.removeEventListener('mouseup', handleMouseUp)
document.removeEventListener('touchmove', handleTouchMove)
document.removeEventListener('touchend', handleTouchEnd)
})
</script>
<template>
<div
v-if="isMobile"
class="widget-mobile"
:style="{ order: order }"
>
<div class="widget-mobile-header" v-if="title">
<span class="widget-title">{{ title }}</span>
</div>
<div class="widget-mobile-content">
<slot></slot>
</div>
</div>
<div
v-else-if="isMounted"
ref="elementRef"
class="widget"
:class="{ 'widget-dragging': isDragging }"
:style="{
left: position.x + 'px',
top: position.y + 'px',
width: width + 'px',
height: height + 'px'
}"
>
<div
class="widget-drag-handle"
@mousedown="handleMouseDown"
@touchstart="handleTouchStart"
>
<div class="drag-indicator"></div>
<span class="widget-title" v-if="title">{{ title }}</span>
</div>
<div class="widget-content">
<slot></slot>
</div>
</div>
</template>
<style scoped>
.widget {
position: absolute;
border-radius: 16px;
background: v-bind('settingsStore.theme.backgroundColor');
backdrop-filter: blur(v-bind('settingsStore.theme.blurAmount + "px"'));
-webkit-backdrop-filter: blur(v-bind('settingsStore.theme.blurAmount + "px"'));
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
overflow: hidden;
user-select: none;
transition: box-shadow 0.2s ease;
display: flex;
flex-direction: column;
}
.widget:hover {
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.4);
}
.widget-dragging {
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.5);
z-index: 50;
}
.widget-drag-handle {
padding: 12px 16px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
cursor: grab;
background: rgba(255, 255, 255, 0.05);
flex-shrink: 0;
}
.widget-drag-handle:active {
cursor: grabbing;
}
.drag-indicator {
width: 40px;
height: 4px;
background: rgba(255, 255, 255, 0.3);
border-radius: 2px;
}
.widget-title {
font-size: 14px;
font-weight: 600;
color: white;
}
.widget-content {
flex: 1;
padding: 16px;
overflow: hidden;
cursor: default;
min-height: 0;
}
.widget-mobile {
width: calc(100% - 20px);
margin: 10px;
border-radius: 16px;
background: v-bind('settingsStore.theme.backgroundColor');
backdrop-filter: blur(v-bind('settingsStore.theme.blurAmount + "px"'));
-webkit-backdrop-filter: blur(v-bind('settingsStore.theme.blurAmount + "px"'));
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
overflow: hidden;
}
.widget-mobile-header {
padding: 12px 16px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
background: rgba(255, 255, 255, 0.05);
}
.widget-mobile-content {
padding: 16px;
}
</style>

View File

@@ -0,0 +1,491 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { useSettingsStore } from '../stores/settings'
const settingsStore = useSettingsStore()
const primaryColor = ref(settingsStore.theme.primaryColor)
const backgroundColor = ref(settingsStore.theme.backgroundColor)
const blurAmount = ref(settingsStore.theme.blurAmount)
const videoBrightness = ref(settingsStore.theme.videoBrightness)
watch(() => settingsStore.theme, (newTheme) => {
primaryColor.value = newTheme.primaryColor
backgroundColor.value = newTheme.backgroundColor
blurAmount.value = newTheme.blurAmount
videoBrightness.value = newTheme.videoBrightness
}, { deep: true })
const presetColors = [
{ name: '蓝色', value: '#007AFF' },
{ name: '绿色', value: '#34C759' },
{ name: '橙色', value: '#FF9500' },
{ name: '红色', value: '#FF3B30' },
{ name: '紫色', value: '#AF52DE' },
{ name: '粉色', value: '#FF2D55' }
]
const backgroundPresets = [
{ name: '深色', value: 'rgba(30, 30, 30, 0.7)' },
{ name: '灰色', value: 'rgba(100, 100, 100, 0.7)' },
{ name: '深蓝', value: 'rgba(20, 40, 80, 0.7)' }
]
const videoList = computed(() => settingsStore.videoList)
const currentVideo = computed(() => settingsStore.currentVideo)
const activeSection = ref('video')
const sections = [
{ id: 'video', name: '视频背景', icon: 'M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z' },
{ id: 'brightness', name: '视频亮度', icon: 'M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z' },
{ id: 'theme', name: '主题颜色', icon: 'M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01' },
{ id: 'background', name: '窗口背景', icon: 'M4 5a1 1 0 011-1h14a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM4 13a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H5a1 1 0 01-1-1v-6zM16 13a1 1 0 011-1h2a1 1 0 011 1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-6z' },
{ id: 'blur', name: '模糊程度', icon: 'M15 12a3 3 0 11-6 0 3 3 0 016 0z M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z' }
]
function selectVideo(video: typeof currentVideo.value) {
if (video) {
settingsStore.setCurrentVideo(video)
}
}
function updatePrimaryColor(color: string) {
primaryColor.value = color
settingsStore.updateTheme({ primaryColor: color })
}
function updateBackground(bg: string) {
backgroundColor.value = bg
settingsStore.updateTheme({ backgroundColor: bg })
}
function updateBlur(amount: number) {
blurAmount.value = amount
settingsStore.updateTheme({ blurAmount: amount })
}
function updateBrightness(value: number) {
videoBrightness.value = value
settingsStore.updateTheme({ videoBrightness: value })
}
function resetSettings() {
settingsStore.resetTheme()
primaryColor.value = settingsStore.theme.primaryColor
backgroundColor.value = settingsStore.theme.backgroundColor
blurAmount.value = settingsStore.theme.blurAmount
videoBrightness.value = settingsStore.theme.videoBrightness
}
function getVideoThumb(thumb: string) {
return thumb
}
</script>
<template>
<div class="settings">
<div class="settings-sidebar">
<div class="sidebar-header">
<h3>设置</h3>
</div>
<nav class="sidebar-nav">
<button
v-for="section in sections"
:key="section.id"
class="nav-item"
:class="{ 'nav-item-active': activeSection === section.id }"
@click="activeSection = section.id"
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path :d="section.icon" />
</svg>
<span>{{ section.name }}</span>
</button>
</nav>
<div class="sidebar-footer">
<button class="reset-btn" @click="resetSettings">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
<span>恢复默认</span>
</button>
</div>
</div>
<div class="settings-content">
<div v-if="activeSection === 'video'" class="content-section">
<h3 class="section-title">视频背景</h3>
<p class="section-desc">选择喜欢的视频作为背景</p>
<div class="video-list">
<div
v-for="video in videoList"
:key="video.id"
class="video-item"
:class="{ 'video-item-active': currentVideo?.id === video.id }"
@click="selectVideo(video)"
>
<img
v-if="video.thumb"
:src="getVideoThumb(video.thumb)"
:alt="video.name"
class="video-thumb"
/>
<div class="video-name">{{ video.name.replace('.mp4', '') }}</div>
</div>
</div>
</div>
<div v-if="activeSection === 'brightness'" class="content-section">
<h3 class="section-title">视频亮度</h3>
<p class="section-desc">调整视频遮罩亮度</p>
<div class="blur-control">
<div class="blur-preview" :style="{ background: 'rgba(0, 0, 0, ' + videoBrightness + ')' }">
预览效果
</div>
<div class="slider-container">
<input
type="range"
min="0"
max="1"
step="0.05"
:value="videoBrightness"
@input="updateBrightness(Number(($event.target as HTMLInputElement).value))"
class="blur-slider"
/>
<span class="slider-value">{{ Math.round(videoBrightness * 100) }}%</span>
</div>
</div>
</div>
<div v-if="activeSection === 'theme'" class="content-section">
<h3 class="section-title">主题颜色</h3>
<p class="section-desc">自定义主题强调色</p>
<div class="color-grid">
<button
v-for="color in presetColors"
:key="color.value"
class="color-btn"
:class="{ 'color-btn-active': primaryColor === color.value }"
:style="{ backgroundColor: color.value }"
@click="updatePrimaryColor(color.value)"
:title="color.name"
>
<svg v-if="primaryColor === color.value" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white" width="16" height="16">
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/>
</svg>
</button>
</div>
</div>
<div v-if="activeSection === 'background'" class="content-section">
<h3 class="section-title">窗口背景</h3>
<p class="section-desc">选择窗口背景样式</p>
<div class="bg-grid">
<button
v-for="bg in backgroundPresets"
:key="bg.value"
class="bg-btn"
:class="{ 'bg-btn-active': backgroundColor === bg.value }"
:style="{ backgroundColor: bg.value }"
@click="updateBackground(bg.value)"
>
{{ bg.name }}
</button>
</div>
</div>
<div v-if="activeSection === 'blur'" class="content-section">
<h3 class="section-title">模糊程度</h3>
<p class="section-desc">调整背景模糊效果</p>
<div class="blur-control">
<div class="blur-preview" :style="{ backdropFilter: `blur(${blurAmount}px)` }">
预览效果
</div>
<div class="slider-container">
<input
type="range"
min="0"
max="40"
:value="blurAmount"
@input="updateBlur(Number(($event.target as HTMLInputElement).value))"
class="blur-slider"
/>
<span class="slider-value">{{ blurAmount }}px</span>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.settings {
display: flex;
height: 100%;
color: white;
}
.settings-sidebar {
width: 180px;
background: rgba(0, 0, 0, 0.2);
border-right: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
flex-direction: column;
flex-shrink: 0;
}
.sidebar-header {
padding: 20px 16px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.sidebar-header h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
}
.sidebar-nav {
flex: 1;
padding: 12px 8px;
overflow-y: auto;
}
.nav-item {
width: 100%;
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
border: none;
background: transparent;
color: rgba(255, 255, 255, 0.7);
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
font-size: 13px;
text-align: left;
margin-bottom: 4px;
}
.nav-item:hover {
background: rgba(255, 255, 255, 0.1);
color: white;
}
.nav-item-active {
background: v-bind('settingsStore.theme.primaryColor');
color: white;
}
.nav-item svg {
width: 18px;
height: 18px;
flex-shrink: 0;
}
.sidebar-footer {
padding: 12px 8px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
.reset-btn {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 10px;
border: 1px solid rgba(255, 255, 255, 0.2);
background: transparent;
color: rgba(255, 255, 255, 0.7);
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
font-size: 13px;
}
.reset-btn:hover {
background: rgba(255, 255, 255, 0.1);
color: white;
}
.reset-btn svg {
width: 16px;
height: 16px;
}
.settings-content {
flex: 1;
padding: 24px;
overflow-y: auto;
}
.content-section {
animation: fadeIn 0.2s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.section-title {
font-size: 18px;
font-weight: 600;
margin: 0 0 8px 0;
}
.section-desc {
font-size: 13px;
opacity: 0.7;
margin: 0 0 20px 0;
}
.video-list {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.video-item {
border-radius: 8px;
overflow: hidden;
cursor: pointer;
border: 2px solid transparent;
transition: all 0.2s ease;
background: rgba(255, 255, 255, 0.1);
}
.video-item:hover {
border-color: rgba(255, 255, 255, 0.3);
transform: translateY(-2px);
}
.video-item-active {
border-color: v-bind('settingsStore.theme.primaryColor');
}
.video-thumb {
width: 100%;
height: 80px;
object-fit: cover;
}
.video-name {
padding: 8px;
font-size: 12px;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.color-grid {
display: flex;
gap: 16px;
flex-wrap: wrap;
}
.color-btn {
width: 48px;
height: 48px;
border-radius: 50%;
border: 3px solid transparent;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.color-btn:hover {
transform: scale(1.1);
}
.color-btn-active {
border-color: white;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.bg-grid {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.bg-btn {
padding: 12px 24px;
border-radius: 8px;
border: 2px solid transparent;
cursor: pointer;
font-size: 14px;
color: white;
transition: all 0.2s ease;
}
.bg-btn:hover {
border-color: rgba(255, 255, 255, 0.5);
transform: translateY(-2px);
}
.bg-btn-active {
border-color: v-bind('settingsStore.theme.primaryColor');
}
.blur-control {
display: flex;
flex-direction: column;
gap: 20px;
}
.blur-preview {
padding: 40px;
background: rgba(255, 255, 255, 0.1);
border-radius: 12px;
text-align: center;
font-size: 16px;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.slider-container {
display: flex;
align-items: center;
gap: 16px;
}
.blur-slider {
flex: 1;
height: 6px;
-webkit-appearance: none;
appearance: none;
background: rgba(255, 255, 255, 0.2);
border-radius: 3px;
outline: none;
}
.blur-slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 20px;
height: 20px;
border-radius: 50%;
background: v-bind('settingsStore.theme.primaryColor');
cursor: pointer;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
.slider-value {
font-size: 14px;
opacity: 0.8;
min-width: 50px;
text-align: right;
}
</style>

View File

@@ -0,0 +1,107 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { getRandomQuote, type TaoXinQuote } from '../data/quotes'
const quote = ref<TaoXinQuote | null>(null)
function refreshQuote() {
quote.value = getRandomQuote()
}
onMounted(() => {
refreshQuote()
})
</script>
<template>
<div class="taoxin">
<div class="taoxin-header">
<span class="taoxin-icon">🌸</span>
<span class="taoxin-title">桃信</span>
</div>
<div class="quote-container" @click="refreshQuote">
<div class="quote-text" v-if="quote">
"{{ quote.quote }}"
</div>
<div class="quote-meta" v-if="quote">
<div class="quote-author"> {{ quote.character }}</div>
<div class="quote-chapter" v-if="quote?.chapter">{{ quote.chapter }}</div>
</div>
</div>
<div class="refresh-hint">点击换一条</div>
</div>
</template>
<style scoped>
.taoxin {
display: flex;
flex-direction: column;
height: 100%;
color: white;
}
.taoxin-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 16px;
flex-shrink: 0;
}
.taoxin-icon {
font-size: 20px;
}
.taoxin-title {
font-size: 16px;
font-weight: 600;
}
.quote-container {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
text-align: center;
cursor: pointer;
padding: 16px 0;
min-height: 0;
}
.quote-text {
font-size: 15px;
line-height: 1.8;
font-style: italic;
opacity: 0.95;
margin-bottom: 16px;
}
.quote-meta {
display: flex;
flex-direction: column;
gap: 4px;
}
.quote-author {
font-size: 13px;
opacity: 0.8;
}
.quote-chapter {
font-size: 11px;
opacity: 0.6;
}
.refresh-hint {
font-size: 11px;
opacity: 0.4;
text-align: center;
margin-top: 12px;
transition: opacity 0.2s ease;
flex-shrink: 0;
}
.taoxin:hover .refresh-hint {
opacity: 0.7;
}
</style>

View File

@@ -0,0 +1,108 @@
<script setup lang="ts">
import { ref, watch, onMounted, computed } from 'vue'
import { useSettingsStore } from '../stores/settings'
import { getVideoUrl } from '../utils'
const settingsStore = useSettingsStore()
const videoRef = ref<HTMLVideoElement | null>(null)
const isLoading = ref(true)
const brightness = computed(() => settingsStore.theme.videoBrightness ?? 0.3)
onMounted(async () => {
await settingsStore.fetchVideoList()
})
watch(() => settingsStore.currentVideo, () => {
if (videoRef.value) {
videoRef.value.load()
isLoading.value = true
}
})
function onVideoCanPlay() {
isLoading.value = false
if (videoRef.value) {
videoRef.value.play().catch(() => {})
}
}
</script>
<template>
<div class="video-background">
<div v-if="isLoading" class="loading-overlay">
<div class="loading-spinner"></div>
</div>
<video
ref="videoRef"
class="video-element"
autoplay
muted
loop
playsinline
@canplay="onVideoCanPlay"
>
<source
v-if="settingsStore.currentVideo"
:src="getVideoUrl(settingsStore.currentVideo.name)"
type="video/mp4"
/>
</video>
<div class="video-overlay" :style="{ background: 'rgba(0, 0, 0, ' + brightness + ')' }"></div>
</div>
</template>
<style scoped>
.video-background {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
overflow: hidden;
}
.video-element {
width: 100%;
height: 100%;
object-fit: cover;
}
.video-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.3);
pointer-events: none;
}
.loading-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: #1a1a1a;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 3px solid rgba(255, 255, 255, 0.3);
border-top-color: #007AFF;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
</style>

View File

@@ -0,0 +1,365 @@
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useSettingsStore } from '../stores/settings'
import { getOS, clamp } from '../utils'
interface Props {
id: string
title?: string
defaultWidth?: number
defaultHeight?: number
minWidth?: number
minHeight?: number
}
const props = withDefaults(defineProps<Props>(), {
title: '',
defaultWidth: 800,
defaultHeight: 600,
minWidth: 400,
minHeight: 300
})
const settingsStore = useSettingsStore()
type ResizeDirection = 'n' | 's' | 'e' | 'w' | 'ne' | 'nw' | 'se' | 'sw'
const isDragging = ref(false)
const isResizing = ref(false)
const resizeDirection = ref<ResizeDirection | null>(null)
const dragStartPos = ref({ x: 0, y: 0 })
const elementStartPos = ref({ x: 0, y: 0 })
const elementStartSize = ref({ width: 0, height: 0 })
const os = computed(() => getOS())
const isOpen = computed(() => settingsStore.isWindowOpen(props.id))
const windowState = computed(() => settingsStore.getWindowState(props.id))
const position = computed(() => windowState.value?.position ?? { x: 100, y: 100 })
const size = computed(() => windowState.value?.size ?? { width: props.defaultWidth, height: props.defaultHeight })
const zIndex = computed(() => windowState.value?.zIndex ?? 100)
const blurAmount = computed(() => settingsStore.theme.blurAmount ?? 20)
const backgroundColor = computed(() => settingsStore.theme.backgroundColor ?? 'rgba(30, 30, 30, 0.7)')
function close() {
settingsStore.closeWindow(props.id)
}
function bringToFront() {
settingsStore.bringWindowToFront(props.id)
}
function handleDragStart(e: MouseEvent) {
if ((e.target as HTMLElement).closest('.window-close-btn')) return
if ((e.target as HTMLElement).closest('.resize-handle')) return
e.preventDefault()
bringToFront()
isDragging.value = true
dragStartPos.value = { x: e.clientX, y: e.clientY }
elementStartPos.value = { ...position.value }
document.addEventListener('mousemove', handleDragMove)
document.addEventListener('mouseup', handleDragEnd)
}
function handleDragMove(e: MouseEvent) {
if (!isDragging.value) return
const deltaX = e.clientX - dragStartPos.value.x
const deltaY = e.clientY - dragStartPos.value.y
const maxX = window.innerWidth - size.value.width - 20
const maxY = window.innerHeight - size.value.height - 100
const newX = clamp(elementStartPos.value.x + deltaX, 20, maxX)
const newY = clamp(elementStartPos.value.y + deltaY, 20, maxY)
settingsStore.updateWindowPosition(props.id, { x: newX, y: newY })
}
function handleDragEnd() {
isDragging.value = false
document.removeEventListener('mousemove', handleDragMove)
document.removeEventListener('mouseup', handleDragEnd)
}
function handleResizeStart(e: MouseEvent, direction: ResizeDirection) {
e.preventDefault()
e.stopPropagation()
bringToFront()
isResizing.value = true
resizeDirection.value = direction
dragStartPos.value = { x: e.clientX, y: e.clientY }
elementStartPos.value = { ...position.value }
elementStartSize.value = { ...size.value }
document.addEventListener('mousemove', handleResizeMove)
document.addEventListener('mouseup', handleResizeEnd)
}
function handleResizeMove(e: MouseEvent) {
if (!isResizing.value || !resizeDirection.value) return
const deltaX = e.clientX - dragStartPos.value.x
const deltaY = e.clientY - dragStartPos.value.y
const dir = resizeDirection.value
let newWidth = elementStartSize.value.width
let newHeight = elementStartSize.value.height
let newX = elementStartPos.value.x
let newY = elementStartPos.value.y
if (dir.includes('e')) {
newWidth = clamp(elementStartSize.value.width + deltaX, props.minWidth, window.innerWidth - position.value.x - 20)
}
if (dir.includes('w')) {
const maxDeltaX = elementStartSize.value.width - props.minWidth
const actualDeltaX = clamp(deltaX, -maxDeltaX, elementStartPos.value.x - 20)
newWidth = elementStartSize.value.width - actualDeltaX
newX = elementStartPos.value.x + actualDeltaX
}
if (dir.includes('s')) {
newHeight = clamp(elementStartSize.value.height + deltaY, props.minHeight, window.innerHeight - position.value.y - 100)
}
if (dir.includes('n')) {
const maxDeltaY = elementStartSize.value.height - props.minHeight
const actualDeltaY = clamp(deltaY, -maxDeltaY, elementStartPos.value.y - 20)
newHeight = elementStartSize.value.height - actualDeltaY
newY = elementStartPos.value.y + actualDeltaY
}
settingsStore.updateWindowPosition(props.id, { x: newX, y: newY })
settingsStore.updateWindowSize(props.id, { width: newWidth, height: newHeight })
}
function handleResizeEnd() {
isResizing.value = false
resizeDirection.value = null
document.removeEventListener('mousemove', handleResizeMove)
document.removeEventListener('mouseup', handleResizeEnd)
}
onUnmounted(() => {
document.removeEventListener('mousemove', handleDragMove)
document.removeEventListener('mouseup', handleDragEnd)
document.removeEventListener('mousemove', handleResizeMove)
document.removeEventListener('mouseup', handleResizeEnd)
})
</script>
<template>
<Teleport to="body">
<Transition name="window-fade">
<div
v-if="isOpen"
class="window"
:class="{ 'window-dragging': isDragging }"
:style="{
left: position.x + 'px',
top: position.y + 'px',
width: size.width + 'px',
height: size.height + 'px',
zIndex: zIndex,
background: backgroundColor,
backdropFilter: 'blur(' + blurAmount + 'px)',
WebkitBackdropFilter: 'blur(' + blurAmount + 'px)'
}"
@mousedown="bringToFront"
>
<div class="window-header" @mousedown="handleDragStart">
<div class="window-controls" :class="{ 'controls-left': os === 'mac', 'controls-right': os !== 'mac' }">
<button class="window-close-btn" @click="close" title="关闭">
<svg v-if="os === 'mac'" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12">
<path d="M3 3l6 6M9 3l-6 6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>
<svg v-else xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12">
<path d="M2 2l8 8M10 2l-8 8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>
</button>
</div>
<span class="window-title">{{ title }}</span>
<div class="window-controls-placeholder"></div>
</div>
<div class="window-content">
<slot></slot>
</div>
<div class="resize-handle resize-n" @mousedown="(e) => handleResizeStart(e, 'n')"></div>
<div class="resize-handle resize-s" @mousedown="(e) => handleResizeStart(e, 's')"></div>
<div class="resize-handle resize-e" @mousedown="(e) => handleResizeStart(e, 'e')"></div>
<div class="resize-handle resize-w" @mousedown="(e) => handleResizeStart(e, 'w')"></div>
<div class="resize-handle resize-ne" @mousedown="(e) => handleResizeStart(e, 'ne')"></div>
<div class="resize-handle resize-nw" @mousedown="(e) => handleResizeStart(e, 'nw')"></div>
<div class="resize-handle resize-se" @mousedown="(e) => handleResizeStart(e, 'se')"></div>
<div class="resize-handle resize-sw" @mousedown="(e) => handleResizeStart(e, 'sw')"></div>
</div>
</Transition>
</Teleport>
</template>
<style scoped>
.window {
position: fixed;
border-radius: 12px;
background: v-bind('backgroundColor');
backdrop-filter: blur(v-bind('blurAmount + "px"'));
-webkit-backdrop-filter: blur(v-bind('blurAmount + "px"'));
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
overflow: hidden;
display: flex;
flex-direction: column;
}
.window-dragging {
cursor: grabbing;
}
.window-header {
display: flex;
align-items: center;
padding: 12px 16px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
cursor: grab;
user-select: none;
min-height: 20px;
}
.window-controls {
display: flex;
align-items: center;
gap: 8px;
}
.window-controls-left {
order: -1;
margin-right: 12px;
}
.window-controls-right {
order: 1;
margin-left: 12px;
}
.window-close-btn {
width: 12px;
height: 12px;
border-radius: 50%;
background: #ff5f57;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
transition: all 0.2s ease;
}
.window-close-btn:hover {
background: #ff3b30;
}
.window-close-btn svg {
width: 8px;
height: 8px;
color: rgba(0, 0, 0, 0.5);
opacity: 0;
transition: opacity 0.2s ease;
}
.window-close-btn:hover svg {
opacity: 1;
}
.window-title {
flex: 1;
text-align: center;
font-size: 14px;
font-weight: 600;
color: white;
}
.window-controls-placeholder {
width: 12px;
}
.window-content {
flex: 1;
overflow: auto;
padding: 0;
}
.resize-handle {
position: absolute;
z-index: 10;
}
.resize-n, .resize-s {
left: 10px;
right: 10px;
height: 6px;
cursor: ns-resize;
}
.resize-n {
top: -3px;
}
.resize-s {
bottom: -3px;
}
.resize-e, .resize-w {
top: 10px;
bottom: 10px;
width: 6px;
cursor: ew-resize;
}
.resize-e {
right: -3px;
}
.resize-w {
left: -3px;
}
.resize-ne, .resize-nw, .resize-se, .resize-sw {
width: 12px;
height: 12px;
}
.resize-ne {
top: -3px;
right: -3px;
cursor: nesw-resize;
}
.resize-nw {
top: -3px;
left: -3px;
cursor: nwse-resize;
}
.resize-se {
bottom: -3px;
right: -3px;
cursor: nwse-resize;
}
.resize-sw {
bottom: -3px;
left: -3px;
cursor: nesw-resize;
}
.window-fade-enter-active,
.window-fade-leave-active {
transition: all 0.3s ease;
}
.window-fade-enter-from,
.window-fade-leave-to {
opacity: 0;
transform: scale(0.95);
}
</style>

View File

@@ -0,0 +1,112 @@
export interface TaoXinQuote {
character: string
quote: string
chapter?: string
}
export const taoXinQuotes: TaoXinQuote[] = [
{
character: "砂狼白子",
quote: "老师,今天的咖啡很好喝呢。要一起来一杯吗?",
chapter: "阿拜多斯篇"
},
{
character: "小鸟游星野",
quote: "啊~好想睡觉...老师,可以借你的肩膀靠一下吗?",
chapter: "阿拜多斯篇"
},
{
character: "黑见芹香",
quote: "打工好累啊...但是为了阿拜多斯,我会加油的!",
chapter: "阿拜多斯篇"
},
{
character: "奥空绫音",
quote: "虽然我们人数很少,但是只要团结在一起,就没有什么做不到的!",
chapter: "阿拜多斯篇"
},
{
character: "十六夜野宫",
quote: "老师~今天也要一起玩哦!我准备了好多好吃的点心呢!",
chapter: "千年篇"
},
{
character: "早濑优香",
quote: "又是你啊...算了,反正已经习惯了。有什么需要帮忙的吗?",
chapter: "千年篇"
},
{
character: "天童爱丽丝",
quote: "我是爱丽丝!虽然不太懂很多事情,但是我会努力学习的!",
chapter: "千年篇"
},
{
character: "才羽桃井",
quote: "游戏就是人生!人生就是游戏!老师也来一起玩吧!",
chapter: "千年篇"
},
{
character: "才羽绿",
quote: "...姐姐太吵了。不过,老师如果也想玩的话,我可以教你。",
chapter: "千年篇"
},
{
character: "砂狼白子",
quote: "老师,我一直在想...我们真的能拯救阿拜多斯吗?",
chapter: "阿拜多斯篇"
},
{
character: "小鸟游星野",
quote: "过去的事情就让它过去吧。重要的是现在,还有未来。",
chapter: "阿拜多斯篇"
},
{
character: "早濑优香",
quote: "虽然总是被你们麻烦,但是...其实我并不讨厌这种感觉。",
chapter: "千年篇"
},
{
character: "天童爱丽丝",
quote: "光之剑!发射!...开玩笑的啦~",
chapter: "千年篇"
},
{
character: "十六夜野宫",
quote: "大家都是好朋友嘛!有什么困难一定要说出来哦!",
chapter: "千年篇"
},
{
character: "黑见芹香",
quote: "虽然嘴上说着不想做,但是看到大家努力的样子,就觉得自己也不能输。",
chapter: "阿拜多斯篇"
},
{
character: "奥空绫音",
quote: "作为对策委员会的委员长,我要为大家负责到底!",
chapter: "阿拜多斯篇"
},
{
character: "砂狼白子",
quote: "老师是我的恩人。所以,我会一直守护在老师身边。",
chapter: "阿拜多斯篇"
},
{
character: "小鸟游星野",
quote: "其实...我很喜欢现在的生活。和大家在一起,真的很开心。",
chapter: "阿拜多斯篇"
},
{
character: "早濑优香",
quote: "研讨会的工作虽然繁忙,但是看到大家进步,一切都值得。",
chapter: "千年篇"
},
{
character: "才羽桃井",
quote: "绿虽然不爱说话,但是她其实很温柔的!对吧,绿?",
chapter: "千年篇"
}
]
export function getRandomQuote(): TaoXinQuote {
return taoXinQuotes[Math.floor(Math.random() * taoXinQuotes.length)]
}

11
docs/.vitepress/theme/data/sidebar.d.ts vendored Normal file
View File

@@ -0,0 +1,11 @@
declare module '*.json' {
const value: {
title: string
path: string
items: {
text: string
link: string
}[]
}[]
export default value
}

View File

@@ -0,0 +1,68 @@
[
{
"title": "java",
"path": "/java/",
"items": [
{
"text": "概述",
"link": "/java/"
},
{
"text": "Java基础语法",
"link": "/java/basic"
},
{
"text": "集合框架",
"link": "/java/collection"
},
{
"text": "面向对象编程",
"link": "/java/oop"
},
{
"text": "多线程编程",
"link": "/java/thread"
}
]
},
{
"title": "vue",
"path": "/vue/",
"items": [
{
"text": "概述",
"link": "/vue/"
},
{
"text": "Vue3基础",
"link": "/vue/basic"
},
{
"text": "组件开发",
"link": "/vue/component"
},
{
"text": "组合式API",
"link": "/vue/composition"
},
{
"text": "状态管理",
"link": "/vue/pinia"
}
]
},
{
"title": "杂谈",
"path": "/杂谈/",
"items": [
{
"text": "概述",
"link": "/杂谈/"
},
{
"text": "程序员的日常思考",
"link": "/杂谈/thoughts"
}
]
}
]

View File

@@ -0,0 +1,21 @@
import Layout from './Layout.vue'
import type { Theme } from 'vitepress'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import { useSettingsStore } from './stores/settings'
import './style.css'
export default {
Layout,
enhanceApp({ app }) {
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
app.use(pinia)
if (typeof window !== 'undefined') {
const settingsStore = useSettingsStore(pinia)
settingsStore.initTheme()
settingsStore.fetchVideoList()
}
}
} satisfies Theme

View File

@@ -0,0 +1,221 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { VideoItem, VideoListResponse, ThemeSettings, WidgetState, WindowState } from '../types'
const defaultTheme: ThemeSettings = {
primaryColor: '#007AFF',
backgroundColor: 'rgba(30, 30, 30, 0.7)',
glassOpacity: 0.7,
blurAmount: 20,
videoBrightness: 0.3
}
function mergeTheme(savedTheme: Partial<ThemeSettings> | undefined): ThemeSettings {
if (!savedTheme) return { ...defaultTheme }
return {
primaryColor: savedTheme.primaryColor ?? defaultTheme.primaryColor,
backgroundColor: savedTheme.backgroundColor ?? defaultTheme.backgroundColor,
glassOpacity: savedTheme.glassOpacity ?? defaultTheme.glassOpacity,
blurAmount: savedTheme.blurAmount ?? defaultTheme.blurAmount,
videoBrightness: savedTheme.videoBrightness ?? defaultTheme.videoBrightness
}
}
export const useSettingsStore = defineStore('settings', () => {
const currentVideo = ref<VideoItem | null>(null)
const videoList = ref<VideoItem[]>([])
const theme = ref<ThemeSettings>({ ...defaultTheme })
const widgets = ref<WidgetState[]>([])
const windows = ref<WindowState[]>([])
const isMobile = ref(false)
function initTheme() {
theme.value = mergeTheme(theme.value)
}
const isWindowOpen = computed(() => {
return (id: string) => windows.value.find(w => w.id === id)?.isOpen ?? false
})
const getWindowState = computed(() => {
return (id: string) => windows.value.find(w => w.id === id)
})
async function fetchVideoList() {
try {
const response = await fetch('/api/fs/list', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
path: '/mp4',
password: '',
page: 1,
per_page: 0,
refresh: false
})
})
const data: VideoListResponse = await response.json()
if (data.code === 200) {
videoList.value = data.data.content.filter(item => item.name.endsWith('.mp4'))
if (!currentVideo.value && videoList.value.length > 0) {
currentVideo.value = videoList.value[0]
}
}
} catch (error) {
console.error('Failed to fetch video list:', error)
}
}
function setCurrentVideo(video: VideoItem) {
currentVideo.value = video
}
function updateTheme(newTheme: Partial<ThemeSettings>) {
theme.value = { ...theme.value, ...newTheme }
}
function resetTheme() {
theme.value = { ...defaultTheme }
}
function updateWidgetPosition(id: string, position: { x: number; y: number }) {
const widget = widgets.value.find(w => w.id === id)
if (widget) {
widget.position = position
} else {
widgets.value.push({ id, position, order: widgets.value.length })
}
}
function updateWidgetOrder(id: string, newOrder: number) {
const widget = widgets.value.find(w => w.id === id)
if (widget) {
widget.order = newOrder
}
}
function openWindow(id: string, position?: { x: number; y: number }, size?: { width: number; height: number }) {
const existingWindow = windows.value.find(w => w.id === id)
if (existingWindow) {
existingWindow.isOpen = true
existingWindow.zIndex = Math.max(...windows.value.map(w => w.zIndex), 0) + 1
} else {
const defaultWidth = size?.width ?? 800
const defaultHeight = size?.height ?? 600
const defaultX = (window.innerWidth - defaultWidth) / 2
const defaultY = (window.innerHeight - defaultHeight) / 2
windows.value.push({
id,
isOpen: true,
position: position ?? { x: Math.max(20, defaultX), y: Math.max(20, defaultY) },
size: size ?? { width: defaultWidth, height: defaultHeight },
zIndex: Math.max(...windows.value.map(w => w.zIndex), 0) + 1
})
}
}
function closeWindow(id: string) {
const window = windows.value.find(w => w.id === id)
if (window) {
window.isOpen = false
}
}
function updateWindowPosition(id: string, position: { x: number; y: number }) {
const window = windows.value.find(w => w.id === id)
if (window) {
window.position = position
}
}
function updateWindowSize(id: string, size: { width: number; height: number }) {
const window = windows.value.find(w => w.id === id)
if (window) {
window.size = size
}
}
function bringWindowToFront(id: string) {
const window = windows.value.find(w => w.id === id)
if (window) {
window.zIndex = Math.max(...windows.value.map(w => w.zIndex), 0) + 1
}
}
function setMobile(value: boolean) {
isMobile.value = value
}
function initWidgets(widgetIds: string[]) {
const existingIds = widgets.value.map(w => w.id)
widgetIds.forEach((id, index) => {
if (!existingIds.includes(id)) {
widgets.value.push({
id,
position: { x: -1, y: -1 },
order: index
})
}
})
}
function updateWidgetDefaultPosition(id: string, width: number, height: number) {
const widget = widgets.value.find(w => w.id === id)
if (widget) {
const screenWidth = window.innerWidth
const screenHeight = window.innerHeight
const positions: Record<string, { x: number; y: number }> = {
calendar: { x: screenWidth - 340, y: 20 },
taoxin: { x: screenWidth - 320, y: screenHeight - 380 },
cafe: { x: screenWidth - 660, y: 20 }
}
if (positions[id]) {
const newPos = positions[id]
const isDefaultPosition = widget.position.x === 20 && widget.position.y < 500
const isOutOfBounds = widget.position.x < 0 || widget.position.x > screenWidth - 100 ||
widget.position.y < 0 || widget.position.y > screenHeight - 100
if (isDefaultPosition || isOutOfBounds) {
widget.position = newPos
}
}
}
}
return {
currentVideo,
videoList,
theme,
widgets,
windows,
isMobile,
isWindowOpen,
getWindowState,
fetchVideoList,
setCurrentVideo,
updateTheme,
resetTheme,
updateWidgetPosition,
updateWidgetOrder,
openWindow,
closeWindow,
updateWindowPosition,
updateWindowSize,
bringWindowToFront,
setMobile,
initWidgets,
initTheme,
updateWidgetDefaultPosition
}
}, {
persist: {
key: 'wufangzhen-settings',
storage: typeof window !== 'undefined' ? localStorage : undefined,
paths: ['currentVideo', 'theme', 'widgets', 'windows']
}
})

View File

@@ -0,0 +1,48 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
background: #1a1a1a;
color: white;
overflow-x: hidden;
}
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.5);
}
::selection {
background: rgba(0, 122, 255, 0.3);
}
a {
text-decoration: none;
}
button {
font-family: inherit;
}

View File

@@ -0,0 +1,51 @@
export interface VideoItem {
id: string
name: string
size: number
thumb: string
modified: string
}
export interface VideoListResponse {
code: number
message: string
data: {
content: VideoItem[]
}
}
export interface WidgetPosition {
x: number
y: number
}
export interface WidgetState {
id: string
position: WidgetPosition
order: number
}
export interface WindowState {
id: string
isOpen: boolean
position: WidgetPosition
size: { width: number; height: number }
zIndex: number
}
export interface ThemeSettings {
primaryColor: string
backgroundColor: string
glassOpacity: number
blurAmount: number
videoBrightness: number
}
export interface SettingsState {
currentVideo: VideoItem | null
videoList: VideoItem[]
theme: ThemeSettings
widgets: WidgetState[]
windows: WindowState[]
isMobile: boolean
}

View File

@@ -0,0 +1,40 @@
export function getVideoUrl(name: string): string {
return `https://pub-7d4a6640bc77480c99842eea19bb2b69.r2.dev/${name}`
}
export function clamp(value: number, min: number, max: number): number {
return Math.min(Math.max(value, min), max)
}
export function isMobileDevice(): boolean {
if (typeof window === 'undefined') return false
return window.innerWidth < 768 || 'ontouchstart' in window
}
export function getOS(): 'mac' | 'windows' | 'linux' | 'unknown' {
if (typeof window === 'undefined') return 'unknown'
const userAgent = window.navigator.userAgent
if (userAgent.includes('Mac')) return 'mac'
if (userAgent.includes('Windows')) return 'windows'
if (userAgent.includes('Linux')) return 'linux'
return 'unknown'
}
export function debounce<T extends (...args: unknown[]) => void>(fn: T, delay: number): T {
let timeoutId: ReturnType<typeof setTimeout>
return ((...args: Parameters<T>) => {
clearTimeout(timeoutId)
timeoutId = setTimeout(() => fn(...args), delay)
}) as T
}
export function throttle<T extends (...args: unknown[]) => void>(fn: T, delay: number): T {
let lastCall = 0
return ((...args: Parameters<T>) => {
const now = Date.now()
if (now - lastCall >= delay) {
lastCall = now
fn(...args)
}
}) as T
}