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

7
.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
/node_modules/
/temp/
/docs/.vitepress/dist/
/.idea/
/.wrangler/
.wranglerrc
.dev.vars

28
Jenkinsfile vendored Normal file
View File

@@ -0,0 +1,28 @@
pipeline {
agent any
stages {
stage('Build') {
steps {
echo 'Building VitePress site...'
sh 'npm ci && npm run build'
}
}
stage('Deploy') {
steps {
echo 'Deploying to Cloudflare Pages...'
sh 'npx wrangler pages deploy docs/.vitepress/dist --project-name=web-home --branch main'
}
}
}
post {
success {
echo 'Deployment successful!'
}
failure {
echo 'Deployment failed!'
}
}
}

5
README.md Normal file
View File

@@ -0,0 +1,5 @@
# Trigger Build 2026年 05月 07日 星期四 10:11:23 CST
## CI/CD 测试 $(date)
最终流程验证测试。

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
}

12
docs/index.md Normal file
View File

@@ -0,0 +1,12 @@
---
home: true
---
# 乌仿镇
技术分享笔记
测试部署
1
test push

115
docs/java/basic.md Normal file
View File

@@ -0,0 +1,115 @@
# Java基础语法
## 变量与数据类型
Java是一种强类型语言每个变量都必须声明其类型。
### 基本数据类型
Java有8种基本数据类型
| 类型 | 关键字 | 大小 | 取值范围 |
|------|--------|------|----------|
| 字节型 | byte | 1字节 | -128 ~ 127 |
| 短整型 | short | 2字节 | -32768 ~ 32767 |
| 整型 | int | 4字节 | -2^31 ~ 2^31-1 |
| 长整型 | long | 8字节 | -2^63 ~ 2^63-1 |
| 单精度浮点 | float | 4字节 | 约±3.4E38 |
| 双精度浮点 | double | 8字节 | 约±1.7E308 |
| 字符型 | char | 2字节 | 0 ~ 65535 |
| 布尔型 | boolean | 1位 | true/false |
### 变量声明
```java
// 声明变量
int age = 25;
String name = "张三";
double salary = 10000.50;
boolean isActive = true;
// 常量
final double PI = 3.14159;
```
## 运算符
### 算术运算符
```java
int a = 10, b = 3;
System.out.println(a + b); // 13 加法
System.out.println(a - b); // 7 减法
System.out.println(a * b); // 30 乘法
System.out.println(a / b); // 3 除法
System.out.println(a % b); // 1 取余
```
### 比较运算符
```java
int x = 5, y = 10;
System.out.println(x == y); // false
System.out.println(x != y); // true
System.out.println(x > y); // false
System.out.println(x < y); // true
```
## 流程控制
### 条件语句
```java
int score = 85;
if (score >= 90) {
System.out.println("优秀");
} else if (score >= 60) {
System.out.println("及格");
} else {
System.out.println("不及格");
}
```
### 循环语句
```java
// for循环
for (int i = 0; i < 5; i++) {
System.out.println(i);
}
// while循环
int i = 0;
while (i < 5) {
System.out.println(i);
i++;
}
// 增强for循环
int[] arr = {1, 2, 3, 4, 5};
for (int num : arr) {
System.out.println(num);
}
```
## 数组
```java
// 声明数组
int[] numbers = new int[5];
String[] names = {"张三", "李四", "王五"};
// 访问数组元素
numbers[0] = 10;
System.out.println(names[1]); // 李四
// 数组长度
System.out.println(names.length); // 3
```
## 验证自动化部署
> 2026-05-07: 此文档已通过 CI/CD 自动化部署验证
>
> 第四次验证 - webhook 已改为内网地址

175
docs/java/collection.md Normal file
View File

@@ -0,0 +1,175 @@
# 集合框架
## 集合体系结构
Java集合框架主要分为两大类
1. **Collection接口**:存储单值
- List有序、可重复
- Set无序、不可重复
- Queue队列
2. **Map接口**:存储键值对
## List集合
### ArrayList
基于动态数组实现,查询快,增删慢。
```java
import java.util.ArrayList;
import java.util.List;
List<String> list = new ArrayList<>();
// 添加元素
list.add("Java");
list.add("Python");
list.add("JavaScript");
// 获取元素
System.out.println(list.get(0)); // Java
// 遍历
for (String item : list) {
System.out.println(item);
}
// 删除元素
list.remove("Python");
// 集合大小
System.out.println(list.size()); // 2
```
### LinkedList
基于链表实现,增删快,查询慢。
```java
import java.util.LinkedList;
LinkedList<String> linkedList = new LinkedList<>();
linkedList.addFirst("第一");
linkedList.addLast("最后");
linkedList.add("中间");
System.out.println(linkedList.getFirst()); // 第一
System.out.println(linkedList.getLast()); // 最后
```
## Set集合
### HashSet
基于哈希表实现,无序、不可重复。
```java
import java.util.HashSet;
import java.util.Set;
Set<String> set = new HashSet<>();
set.add("Apple");
set.add("Banana");
set.add("Apple"); // 重复元素不会被添加
System.out.println(set.size()); // 2
System.out.println(set.contains("Apple")); // true
```
### TreeSet
基于红黑树实现,自动排序。
```java
import java.util.TreeSet;
Set<Integer> treeSet = new TreeSet<>();
treeSet.add(5);
treeSet.add(1);
treeSet.add(3);
// 输出1, 3, 5自动排序
for (Integer num : treeSet) {
System.out.println(num);
}
```
## Map集合
### HashMap
基于哈希表实现,键不可重复。
```java
import java.util.HashMap;
import java.util.Map;
Map<String, Integer> map = new HashMap<>();
// 添加键值对
map.put("Java", 1);
map.put("Python", 2);
map.put("JavaScript", 3);
// 获取值
System.out.println(map.get("Java")); // 1
// 遍历
for (Map.Entry<String, Integer> entry : map.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
// 判断键是否存在
System.out.println(map.containsKey("Java")); // true
// 删除
map.remove("Python");
```
### TreeMap
基于红黑树实现,键自动排序。
```java
import java.util.TreeMap;
Map<Integer, String> treeMap = new TreeMap<>();
treeMap.put(3, "C");
treeMap.put(1, "A");
treeMap.put(2, "B");
// 按键排序输出1=A, 2=B, 3=C
treeMap.forEach((k, v) -> System.out.println(k + "=" + v));
```
## 集合工具类
```java
import java.util.Collections;
import java.util.ArrayList;
import java.util.List;
List<Integer> numbers = new ArrayList<>();
numbers.add(3);
numbers.add(1);
numbers.add(2);
// 排序
Collections.sort(numbers); // [1, 2, 3]
// 反转
Collections.reverse(numbers); // [3, 2, 1]
// 打乱
Collections.shuffle(numbers);
// 最大最小值
System.out.println(Collections.max(numbers));
System.out.println(Collections.min(numbers));
```

10
docs/java/index.md Normal file
View File

@@ -0,0 +1,10 @@
# Java 笔记
Java技术栈学习笔记涵盖基础语法、面向对象、集合框架、多线程等内容。
## 目录
- [Java基础语法](/java/basic.html)
- [面向对象编程](/java/oop.html)
- [集合框架](/java/collection.html)
- [多线程编程](/java/thread.html)

157
docs/java/oop.md Normal file
View File

@@ -0,0 +1,157 @@
# 面向对象编程
## 类与对象
### 类的定义
```java
public class Person {
// 属性(字段)
private String name;
private int age;
// 构造方法
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// 方法
public void sayHello() {
System.out.println("你好,我是" + name);
}
// Getter和Setter
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
```
### 创建对象
```java
Person person = new Person("张三", 25);
person.sayHello(); // 你好,我是张三
person.setName("李四");
System.out.println(person.getName()); // 李四
```
## 封装
封装是面向对象的三大特性之一,通过访问修饰符控制类的成员访问权限。
| 修饰符 | 同一类 | 同一包 | 子类 | 其他 |
|--------|--------|--------|------|------|
| public | ✓ | ✓ | ✓ | ✓ |
| protected | ✓ | ✓ | ✓ | ✗ |
| default | ✓ | ✓ | ✗ | ✗ |
| private | ✓ | ✗ | ✗ | ✗ |
## 继承
```java
// 父类
public class Animal {
protected String name;
public Animal(String name) {
this.name = name;
}
public void eat() {
System.out.println(name + "正在吃东西");
}
}
// 子类
public class Dog extends Animal {
public Dog(String name) {
super(name);
}
public void bark() {
System.out.println(name + "正在汪汪叫");
}
}
// 使用
Dog dog = new Dog("旺财");
dog.eat(); // 旺财正在吃东西
dog.bark(); // 旺财正在汪汪叫
```
## 多态
```java
public class Shape {
public void draw() {
System.out.println("绘制图形");
}
}
public class Circle extends Shape {
@Override
public void draw() {
System.out.println("绘制圆形");
}
}
public class Rectangle extends Shape {
@Override
public void draw() {
System.out.println("绘制矩形");
}
}
// 多态使用
Shape shape1 = new Circle();
Shape shape2 = new Rectangle();
shape1.draw(); // 绘制圆形
shape2.draw(); // 绘制矩形
```
## 抽象类与接口
### 抽象类
```java
public abstract class Vehicle {
protected String brand;
public Vehicle(String brand) {
this.brand = brand;
}
// 抽象方法
public abstract void start();
// 具体方法
public void showBrand() {
System.out.println("品牌:" + brand);
}
}
```
### 接口
```java
public interface Flyable {
void fly();
default void show() {
System.out.println("可以飞行");
}
}
public class Bird implements Flyable {
@Override
public void fly() {
System.out.println("鸟在飞翔");
}
}
```

198
docs/java/thread.md Normal file
View File

@@ -0,0 +1,198 @@
# 多线程编程
## 线程基础
### 创建线程
#### 方式一继承Thread类
```java
public class MyThread extends Thread {
@Override
public void run() {
System.out.println("线程运行中:" + Thread.currentThread().getName());
}
}
// 使用
MyThread thread = new MyThread();
thread.start();
```
#### 方式二实现Runnable接口
```java
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("线程运行中:" + Thread.currentThread().getName());
}
}
// 使用
Thread thread = new Thread(new MyRunnable());
thread.start();
// 使用Lambda表达式
Thread thread2 = new Thread(() -> {
System.out.println("Lambda线程运行中");
});
thread2.start();
```
#### 方式三实现Callable接口
```java
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
public class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 1; i <= 100; i++) {
sum += i;
}
return sum;
}
}
// 使用
FutureTask<Integer> futureTask = new FutureTask<>(new MyCallable());
Thread thread = new Thread(futureTask);
thread.start();
// 获取返回值
Integer result = futureTask.get();
System.out.println("1到100的和" + result); // 5050
```
## 线程同步
### synchronized关键字
```java
public class Counter {
private int count = 0;
// 同步方法
public synchronized void increment() {
count++;
}
// 同步代码块
public void decrement() {
synchronized (this) {
count--;
}
}
public int getCount() {
return count;
}
}
```
### Lock接口
```java
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class SafeCounter {
private int count = 0;
private final Lock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public int getCount() {
return count;
}
}
```
## 线程通信
### wait/notify
```java
public class ProducerConsumer {
private final Object lock = new Object();
private boolean hasData = false;
public void produce() throws InterruptedException {
synchronized (lock) {
while (hasData) {
lock.wait(); // 等待消费
}
// 生产数据
hasData = true;
System.out.println("生产数据");
lock.notify(); // 通知消费者
}
}
public void consume() throws InterruptedException {
synchronized (lock) {
while (!hasData) {
lock.wait(); // 等待生产
}
// 消费数据
hasData = false;
System.out.println("消费数据");
lock.notify(); // 通知生产者
}
}
}
```
## 线程池
```java
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
// 创建固定大小线程池
ExecutorService executor = Executors.newFixedThreadPool(5);
// 提交任务
for (int i = 0; i < 10; i++) {
final int taskId = i;
executor.submit(() -> {
System.out.println("任务 " + taskId + "" +
Thread.currentThread().getName() + " 执行");
});
}
// 关闭线程池
executor.shutdown();
```
### ThreadPoolExecutor
```java
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.TimeUnit;
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, // 核心线程数
5, // 最大线程数
60L, // 空闲线程存活时间
TimeUnit.SECONDS, // 时间单位
new ArrayBlockingQueue<>(10) // 任务队列
);
executor.execute(() -> {
System.out.println("执行任务");
});
executor.shutdown();
```

240
docs/vue/basic.md Normal file
View File

@@ -0,0 +1,240 @@
# Vue3基础
## 创建应用
### 使用Vite创建项目
```bash
npm create vite@latest my-vue-app -- --template vue
cd my-vue-app
npm install
npm run dev
```
### 应用实例
```vue
<script setup>
import { ref } from 'vue'
const message = ref('Hello Vue 3!')
</script>
<template>
<div>
<h1>{{ message }}</h1>
</div>
</template>
<style scoped>
h1 {
color: #42b883;
}
</style>
```
## 模板语法
### 文本插值
```vue
<template>
<p>{{ message }}</p>
<p>{{ number + 1 }}</p>
<p>{{ ok ? 'YES' : 'NO' }}</p>
</template>
<script setup>
import { ref } from 'vue'
const message = ref('Hello')
const number = ref(10)
const ok = ref(true)
</script>
```
### 属性绑定
```vue
<template>
<div v-bind:id="dynamicId"></div>
<div :class="activeClass"></div>
<button :disabled="isDisabled">按钮</button>
</template>
<script setup>
import { ref } from 'vue'
const dynamicId = ref('my-id')
const activeClass = ref('active')
const isDisabled = ref(false)
</script>
```
### 条件渲染
```vue
<template>
<div v-if="type === 'A'">类型A</div>
<div v-else-if="type === 'B'">类型B</div>
<div v-else>其他类型</div>
<div v-show="isVisible">显示/隐藏</div>
</template>
<script setup>
import { ref } from 'vue'
const type = ref('A')
const isVisible = ref(true)
</script>
```
### 列表渲染
```vue
<template>
<ul>
<li v-for="item in items" :key="item.id">
{{ item.name }}
</li>
</ul>
<ul>
<li v-for="(item, index) in items" :key="item.id">
{{ index }}: {{ item.name }}
</li>
</ul>
</template>
<script setup>
import { ref } from 'vue'
const items = ref([
{ id: 1, name: 'Vue' },
{ id: 2, name: 'React' },
{ id: 3, name: 'Angular' }
])
</script>
```
## 事件处理
```vue
<template>
<button v-on:click="count++">点击次数: {{ count }}</button>
<button @click="handleClick">点击</button>
<button @click="handleClick($event, '参数')">传参</button>
<form @submit.prevent="onSubmit">
<button type="submit">提交</button>
</form>
</template>
<script setup>
import { ref } from 'vue'
const count = ref(0)
function handleClick(event) {
console.log('点击事件', event)
}
function onSubmit() {
console.log('表单提交')
}
</script>
```
## 双向绑定
```vue
<template>
<input v-model="text" placeholder="输入文本">
<p>输入的内容: {{ text }}</p>
<textarea v-model="content"></textarea>
<select v-model="selected">
<option value="">请选择</option>
<option value="a">选项A</option>
<option value="b">选项B</option>
</select>
<input type="checkbox" v-model="checked">
<input type="radio" v-model="picked" value="one">
</template>
<script setup>
import { ref } from 'vue'
const text = ref('')
const content = ref('')
const selected = ref('')
const checked = ref(false)
const picked = ref('')
</script>
```
## 计算属性
```vue
<template>
<p>原始: {{ message }}</p>
<p>反转: {{ reversedMessage }}</p>
<p>全名: {{ fullName }}</p>
</template>
<script setup>
import { ref, computed } from 'vue'
const message = ref('Hello Vue')
const firstName = ref('张')
const lastName = ref('三')
const reversedMessage = computed(() => {
return message.value.split('').reverse().join('')
})
const fullName = computed({
get() {
return firstName.value + lastName.value
},
set(newValue) {
firstName.value = newValue[0]
lastName.value = newValue.slice(1)
}
})
</script>
```
## 侦听器
```vue
<template>
<input v-model="question">
<p>{{ answer }}</p>
</template>
<script setup>
import { ref, watch } from 'vue'
const question = ref('')
const answer = ref('请输入问题')
watch(question, async (newQuestion) => {
if (newQuestion.includes('?')) {
answer.value = '思考中...'
// 模拟API调用
setTimeout(() => {
answer.value = '这是一个好问题!'
}, 1000)
}
})
// 监听多个源
watch([question, answer], ([newQuestion, newAnswer], [oldQuestion, oldAnswer]) => {
console.log('问题或答案改变了')
})
</script>
```

251
docs/vue/component.md Normal file
View File

@@ -0,0 +1,251 @@
# 组件开发
## 组件基础
### 定义组件
```vue
<!-- MyButton.vue -->
<template>
<button :class="['btn', `btn-${type}`]" @click="handleClick">
<slot></slot>
</button>
</template>
<script setup>
const props = defineProps({
type: {
type: String,
default: 'default',
validator: (value) => ['default', 'primary', 'danger'].includes(value)
}
})
const emit = defineEmits(['click'])
function handleClick(event) {
emit('click', event)
}
</script>
<style scoped>
.btn {
padding: 8px 16px;
border-radius: 4px;
border: none;
cursor: pointer;
}
.btn-default {
background: #f0f0f0;
color: #333;
}
.btn-primary {
background: #42b883;
color: white;
}
.btn-danger {
background: #ff4d4f;
color: white;
}
</style>
```
### 使用组件
```vue
<template>
<MyButton type="primary" @click="handleButtonClick">
点击我
</MyButton>
</template>
<script setup>
import MyButton from './MyButton.vue'
function handleButtonClick() {
console.log('按钮被点击')
}
</script>
```
## Props
### Props声明
```vue
<script setup>
// 简单声明
const props = defineProps(['title', 'content'])
// 带类型的声明
const props = defineProps({
title: String,
content: {
type: String,
required: true
},
count: {
type: Number,
default: 0
},
items: {
type: Array,
default: () => []
}
})
</script>
```
### Props验证
```vue
<script setup>
const props = defineProps({
status: {
type: String,
required: true,
validator: (value) => ['active', 'inactive', 'pending'].includes(value)
},
callback: {
type: Function,
default: () => {}
}
})
</script>
```
## 事件
### 定义事件
```vue
<script setup>
const emit = defineEmits(['update', 'delete', 'change'])
function handleUpdate() {
emit('update', { id: 1, name: '更新数据' })
}
function handleDelete(id) {
emit('delete', id)
}
</script>
```
### v-model
```vue
<!-- CustomInput.vue -->
<template>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
>
</template>
<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>
<!-- 使用 -->
<CustomInput v-model="text" />
```
## 插槽
### 默认插槽
```vue
<!-- Card.vue -->
<template>
<div class="card">
<slot></slot>
</div>
</template>
<!-- 使用 -->
<Card>
<h2>标题</h2>
<p>内容</p>
</Card>
```
### 具名插槽
```vue
<!-- Layout.vue -->
<template>
<div class="layout">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
</template>
<!-- 使用 -->
<Layout>
<template #header>
<h1>页面标题</h1>
</template>
<p>主要内容</p>
<template #footer>
<p>页脚信息</p>
</template>
</Layout>
```
### 作用域插槽
```vue
<!-- List.vue -->
<template>
<ul>
<li v-for="item in items" :key="item.id">
<slot :item="item" :index="index"></slot>
</li>
</ul>
</template>
<script setup>
defineProps(['items'])
</script>
<!-- 使用 -->
<List :items="users">
<template #default="{ item, index }">
<span>{{ index }}. {{ item.name }}</span>
</template>
</List>
```
## 生命周期
```vue
<script setup>
import { onMounted, onUpdated, onUnmounted } from 'vue'
onMounted(() => {
console.log('组件已挂载')
})
onUpdated(() => {
console.log('组件已更新')
})
onUnmounted(() => {
console.log('组件已卸载')
})
</script>
```

242
docs/vue/composition.md Normal file
View File

@@ -0,0 +1,242 @@
# 组合式API
## setup函数
### 基本用法
```vue
<script setup>
import { ref, reactive, computed, watch, onMounted } from 'vue'
// 响应式数据
const count = ref(0)
const state = reactive({
name: 'Vue',
version: 3
})
// 计算属性
const doubleCount = computed(() => count.value * 2)
// 方法
function increment() {
count.value++
}
// 生命周期
onMounted(() => {
console.log('组件已挂载')
})
</script>
<template>
<div>
<p>Count: {{ count }}</p>
<p>Double: {{ doubleCount }}</p>
<button @click="increment">+1</button>
</div>
</template>
```
## ref与reactive
### ref
用于基本类型和需要替换整个对象的场景。
```vue
<script setup>
import { ref } from 'vue'
const count = ref(0)
const user = ref({ name: '张三' })
// 访问需要 .value
console.log(count.value)
console.log(user.value.name)
// 重新赋值
user.value = { name: '李四' }
</script>
```
### reactive
用于对象类型,返回原始对象的代理。
```vue
<script setup>
import { reactive } from 'vue'
const state = reactive({
count: 0,
user: {
name: '张三',
age: 25
}
})
// 直接访问,不需要 .value
console.log(state.count)
console.log(state.user.name)
// 修改属性
state.count++
state.user.age = 26
</script>
```
## 组合式函数
### 创建组合式函数
```javascript
// useCounter.js
import { ref, computed } from 'vue'
export function useCounter(initialValue = 0) {
const count = ref(initialValue)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
function decrement() {
count.value--
}
function reset() {
count.value = initialValue
}
return {
count,
doubleCount,
increment,
decrement,
reset
}
}
```
### 使用组合式函数
```vue
<script setup>
import { useCounter } from './useCounter'
const { count, doubleCount, increment, decrement, reset } = useCounter(10)
</script>
<template>
<div>
<p>Count: {{ count }}</p>
<p>Double: {{ doubleCount }}</p>
<button @click="increment">+1</button>
<button @click="decrement">-1</button>
<button @click="reset">Reset</button>
</div>
</template>
```
### 鼠标位置追踪
```javascript
// useMouse.js
import { ref, onMounted, onUnmounted } from 'vue'
export function useMouse() {
const x = ref(0)
const y = ref(0)
function update(event) {
x.value = event.pageX
y.value = event.pageY
}
onMounted(() => {
window.addEventListener('mousemove', update)
})
onUnmounted(() => {
window.removeEventListener('mousemove', update)
})
return { x, y }
}
```
```vue
<script setup>
import { useMouse } from './useMouse'
const { x, y } = useMouse()
</script>
<template>
<p>鼠标位置: {{ x }}, {{ y }}</p>
</template>
```
## provide/inject
### 提供数据
```vue
<!-- 祖先组件 -->
<script setup>
import { provide, ref } from 'vue'
const theme = ref('dark')
const user = ref({ name: '张三' })
provide('theme', theme)
provide('user', user)
provide('updateTheme', (newTheme) => {
theme.value = newTheme
})
</script>
```
### 注入数据
```vue
<!-- 后代组件 -->
<script setup>
import { inject } from 'vue'
const theme = inject('theme', 'light') // 默认值
const user = inject('user')
const updateTheme = inject('updateTheme')
function toggleTheme() {
updateTheme(theme.value === 'dark' ? 'light' : 'dark')
}
</script>
```
## toRef与toRefs
```vue
<script setup>
import { reactive, toRef, toRefs } from 'vue'
const state = reactive({
name: '张三',
age: 25,
city: '北京'
})
// toRef - 单个属性
const nameRef = toRef(state, 'name')
// toRefs - 所有属性
const { name, age, city } = toRefs(state)
// 解构后的ref保持响应性
name.value = '李四'
console.log(state.name) // 李四
</script>
```

10
docs/vue/index.md Normal file
View File

@@ -0,0 +1,10 @@
# Vue 笔记
Vue.js框架学习笔记涵盖Vue3基础、组件开发、状态管理等内容。
## 目录
- [Vue3基础](/vue/basic.html)
- [组件开发](/vue/component.html)
- [组合式API](/vue/composition.html)
- [状态管理](/vue/pinia.html)

224
docs/vue/pinia.md Normal file
View File

@@ -0,0 +1,224 @@
# 状态管理
## Pinia基础
### 安装Pinia
```bash
npm install pinia
```
### 创建Store
```javascript
// stores/counter.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useCounterStore = defineStore('counter', () => {
// state
const count = ref(0)
// getters
const doubleCount = computed(() => count.value * 2)
// actions
function increment() {
count.value++
}
function decrement() {
count.value--
}
async function fetchCount() {
// 异步操作
const response = await fetch('/api/count')
const data = await response.json()
count.value = data.count
}
return { count, doubleCount, increment, decrement, fetchCount }
})
```
### 使用Store
```vue
<script setup>
import { useCounterStore } from '@/stores/counter'
const counter = useCounterStore()
// 读取state
console.log(counter.count)
// 读取getters
console.log(counter.doubleCount)
// 调用actions
counter.increment()
</script>
<template>
<div>
<p>Count: {{ counter.count }}</p>
<p>Double: {{ counter.doubleCount }}</p>
<button @click="counter.increment">+1</button>
</div>
</template>
```
## 解构Store
### storeToRefs
```vue
<script setup>
import { storeToRefs } from 'pinia'
import { useCounterStore } from '@/stores/counter'
const counter = useCounterStore()
// 解构state和getters保持响应性
const { count, doubleCount } = storeToRefs(counter)
// 解构actions直接解构
const { increment, decrement } = counter
</script>
```
## 持久化
### 安装插件
```bash
npm install pinia-plugin-persistedstate
```
### 配置持久化
```javascript
// stores/index.js
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
export default pinia
```
### 使用持久化
```javascript
// stores/user.js
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useUserStore = defineStore('user', () => {
const token = ref('')
const userInfo = ref(null)
function setToken(newToken) {
token.value = newToken
}
function setUserInfo(info) {
userInfo.value = info
}
function logout() {
token.value = ''
userInfo.value = null
}
return { token, userInfo, setToken, setUserInfo, logout }
}, {
persist: {
key: 'my-user-store',
storage: localStorage,
paths: ['token', 'userInfo']
}
})
```
## 组合多个Store
```javascript
// stores/index.js
import { useCounterStore } from './counter'
import { useUserStore } from './user'
export function useRootStore() {
const counter = useCounterStore()
const user = useUserStore()
function resetAll() {
counter.$reset()
user.$reset()
}
return {
counter,
user,
resetAll
}
}
```
## 订阅状态变化
```vue
<script setup>
import { useCounterStore } from '@/stores/counter'
const counter = useCounterStore()
// 订阅state变化
counter.$subscribe((mutation, state) => {
console.log('类型:', mutation.type)
console.log('storeId:', mutation.storeId)
console.log('新状态:', state)
// 持久化到本地存储
localStorage.setItem('counter', JSON.stringify(state))
})
// 订阅actions
counter.$onAction(({ name, args, after, onError }) => {
console.log(`Action ${name} 被调用,参数:`, args)
after((result) => {
console.log(`Action ${name} 完成,结果:`, result)
})
onError((error) => {
console.error(`Action ${name} 出错:`, error)
})
})
</script>
```
## 插件开发
```javascript
// plugins/piniaLogger.js
export function piniaLogger({ store }) {
store.$onAction(({ name, args, after, onError }) => {
console.log(`[${store.$id}] ${name} 开始`, args)
after((result) => {
console.log(`[${store.$id}] ${name} 完成`, result)
})
onError((error) => {
console.error(`[${store.$id}] ${name} 失败`, error)
})
})
}
// 使用插件
const pinia = createPinia()
pinia.use(piniaLogger)
```

32
docs/杂谈/index.md Normal file
View File

@@ -0,0 +1,32 @@
# 杂谈
这里是随心所欲的记录角落。
## 关于学习
学习是一件很有趣的事情,尤其是当你发现知识之间的联系时。
> 学而不思则罔,思而不学则殆。——《论语》
## 编程心得
编程不仅仅是写代码,更是一种思维方式:
1. **分解问题** - 把复杂问题拆成小问题
2. **抽象思维** - 找到事物的共同特征
3. **持续学习** - 技术更新很快,保持好奇心
4. **CI/CD 自动化** - 让机器做重复的事,节省时间
## 一些想法
生活就像写代码,有时候需要重构,有时候需要添加新功能,但最重要的是保持清晰的结构。
## 自动化部署测试
通过 Jenkins + Gitea Webhook + Cloudflare Pages 实现全自动部署流程!
测试时间:$(date)
---
*持续更新中...*

27
docs/杂谈/thoughts.md Normal file
View File

@@ -0,0 +1,27 @@
# 程序员的日常思考
## 代码与生活
写代码就像烹饪,你需要:
- 好的食材(清晰的需求)
- 合适的工具(合适的框架和库)
- 耐心和细心(调试和测试)
## 保持学习
技术更新很快,但基础永远重要:
- 数据结构与算法
- 设计模式
- 计算机网络
- 操作系统原理
## 工作心得
1. 代码要写得让六个月后的自己能看懂
2. 注释不是越详细越好,而是恰到好处
3. 重构是持续的,不是一次性的
4. 测试是最好的文档
## 结语
每天进步一点点,积少成多。

72
functions/api/fs/list.ts Normal file
View File

@@ -0,0 +1,72 @@
interface Env {
R2_BUCKET: R2Bucket;
}
export async function onRequest(context: { env: Env; request: Request }) {
const url = new URL(context.request.url);
if (url.pathname === '/api/fs/list' && context.request.method === 'POST') {
try {
const files: any[] = [];
let cursor: string | undefined;
do {
const listing = await context.env.R2_BUCKET.list({
cursor: cursor,
limit: 1000
});
for (const obj of listing.objects) {
if (obj.key.endsWith('.mp4') && !obj.key.includes('/')) {
const nameWithoutExt = obj.key.replace('.mp4', '');
files.push({
name: obj.key,
size: obj.size,
modified: obj.lastModified,
thumb: `https://pub-7d4a6640bc77480c99842eea19bb2b69.r2.dev/thumbs/${nameWithoutExt}.jpg`
});
}
}
cursor = listing.cursor;
} while (cursor);
const response = {
code: 200,
message: "success",
data: {
content: files.map((f, i) => ({
id: `r2_${i}`,
path: "",
name: f.name,
size: f.size,
is_dir: false,
modified: f.modified,
created: f.modified,
sign: "",
thumb: f.thumb,
type: 2,
hashinfo: "null",
hash_info: null
})),
total: files.length,
readme: "",
header: "",
write: false,
provider: "r2"
}
};
return new Response(JSON.stringify(response), {
headers: { 'Content-Type': 'application/json' }
});
} catch (e) {
return new Response(JSON.stringify({ error: e.message }), {
status: 500,
headers: { 'Content-Type': 'application/json' }
});
}
}
return new Response('Not Found', { status: 404 });
}

3082
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

21
package.json Normal file
View File

@@ -0,0 +1,21 @@
{
"scripts": {
"docs:dev": "vitepress dev docs",
"docs:build": "vitepress build docs",
"docs:preview": "vitepress preview docs",
"gen:sidebar": "npx tsx scripts/generate-sidebar.ts",
"predev": "npm run gen:sidebar",
"prebuild": "npm run gen:sidebar",
"dev": "npm run docs:dev",
"build": "npm run docs:build",
"preview": "npm run docs:preview"
},
"dependencies": {
"pinia": "^2.1.7",
"pinia-plugin-persistedstate": "^3.2.1"
},
"devDependencies": {
"tsx": "^4.7.0",
"vitepress": "^1.3.4"
}
}

120
scripts/generate-sidebar.ts Normal file
View File

@@ -0,0 +1,120 @@
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const docsDir = path.resolve(__dirname, '../docs')
interface SidebarItem {
text: string
link: string
}
interface SidebarGroup {
title: string
path: string
items: SidebarItem[]
}
function getTitleFromFile(filePath: string): string {
const content = fs.readFileSync(filePath, 'utf-8')
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/)
if (frontmatterMatch) {
const titleMatch = frontmatterMatch[1].match(/title:\s*["']?([^"'\n]+)["']?/)
if (titleMatch) {
return titleMatch[1].trim()
}
}
const h1Match = content.match(/^#\s+(.+)$/m)
if (h1Match) {
return h1Match[1].trim()
}
return path.basename(filePath, '.md')
}
function getSidebarOrder(filePath: string, fileName: string): number {
if (fileName === 'index.md') {
return 0
}
const content = fs.readFileSync(filePath, 'utf-8')
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/)
if (frontmatterMatch) {
const orderMatch = frontmatterMatch[1].match(/sidebarOrder:\s*(\d+)/)
if (orderMatch) {
return parseInt(orderMatch[1], 10)
}
}
return 999
}
function generateSidebar(): SidebarGroup[] {
const sidebar: SidebarGroup[] = []
const entries = fs.readdirSync(docsDir, { withFileTypes: true })
for (const entry of entries) {
if (!entry.isDirectory() || entry.name.startsWith('.') || entry.name === '.vitepress') {
continue
}
const dirPath = path.join(docsDir, entry.name)
const files = fs.readdirSync(dirPath)
.filter(f => f.endsWith('.md'))
.map(f => ({
name: f,
path: path.join(dirPath, f),
order: getSidebarOrder(path.join(dirPath, f), f)
}))
.sort((a, b) => a.order - b.order)
if (files.length === 0) continue
const items: SidebarItem[] = files.map(file => {
let title = getTitleFromFile(file.path)
if (file.name === 'index.md') {
title = '概述'
}
const linkPath = file.name === 'index.md'
? `/${entry.name}/`
: `/${entry.name}/${file.name.replace('.md', '')}`
return {
text: title,
link: linkPath
}
})
sidebar.push({
title: entry.name,
path: `/${entry.name}/`,
items
})
}
return sidebar
}
function updateConfig() {
const sidebar = generateSidebar()
const outputPath = path.resolve(__dirname, '../docs/.vitepress/theme/data/sidebar.json')
const outputDir = path.dirname(outputPath)
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true })
}
fs.writeFileSync(outputPath, JSON.stringify(sidebar, null, 2))
console.log('Sidebar JSON generated:')
console.log(JSON.stringify(sidebar, null, 2))
}
updateConfig()

2
test.txt Normal file
View File

@@ -0,0 +1,2 @@
webhook test 2026年 05月 07日 星期四 10:25:24 CST
ci test 1778121645

4
wrangler.toml Normal file
View File

@@ -0,0 +1,4 @@
name = "web-home"
account_id = "0a0b9995a00648aadf505e235e66458a"
compatibility_date = "2024-01-01"
pages_build_output_dir = "docs/.vitepress/dist"