1
This commit is contained in:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
/node_modules/
|
||||||
|
/temp/
|
||||||
|
/docs/.vitepress/dist/
|
||||||
|
/.idea/
|
||||||
43
docs/.vitepress/cache/deps/_metadata.json
vendored
Normal file
43
docs/.vitepress/cache/deps/_metadata.json
vendored
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"hash": "b8a284ed",
|
||||||
|
"configHash": "a23338b4",
|
||||||
|
"lockfileHash": "28c8fbed",
|
||||||
|
"browserHash": "e99f2412",
|
||||||
|
"optimized": {
|
||||||
|
"vue": {
|
||||||
|
"src": "../../../../node_modules/vue/dist/vue.runtime.esm-bundler.js",
|
||||||
|
"file": "vue.js",
|
||||||
|
"fileHash": "5ecf6963",
|
||||||
|
"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": "16a99640",
|
||||||
|
"needsInterop": false
|
||||||
|
},
|
||||||
|
"vitepress > @vueuse/core": {
|
||||||
|
"src": "../../../../node_modules/@vueuse/core/index.mjs",
|
||||||
|
"file": "vitepress___@vueuse_core.js",
|
||||||
|
"fileHash": "530df1da",
|
||||||
|
"needsInterop": false
|
||||||
|
},
|
||||||
|
"pinia": {
|
||||||
|
"src": "../../../../node_modules/pinia/dist/pinia.mjs",
|
||||||
|
"file": "pinia.js",
|
||||||
|
"fileHash": "e90be092",
|
||||||
|
"needsInterop": false
|
||||||
|
},
|
||||||
|
"pinia-plugin-persistedstate": {
|
||||||
|
"src": "../../../../node_modules/pinia-plugin-persistedstate/dist/index.js",
|
||||||
|
"file": "pinia-plugin-persistedstate.js",
|
||||||
|
"fileHash": "ac61966c",
|
||||||
|
"needsInterop": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"chunks": {
|
||||||
|
"chunk-SNNOYR6U": {
|
||||||
|
"file": "chunk-SNNOYR6U.js"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12865
docs/.vitepress/cache/deps/chunk-SNNOYR6U.js
vendored
Normal file
12865
docs/.vitepress/cache/deps/chunk-SNNOYR6U.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
7
docs/.vitepress/cache/deps/chunk-SNNOYR6U.js.map
vendored
Normal file
7
docs/.vitepress/cache/deps/chunk-SNNOYR6U.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
3
docs/.vitepress/cache/deps/package.json
vendored
Normal file
3
docs/.vitepress/cache/deps/package.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"type": "module"
|
||||||
|
}
|
||||||
137
docs/.vitepress/cache/deps/pinia-plugin-persistedstate.js
vendored
Normal file
137
docs/.vitepress/cache/deps/pinia-plugin-persistedstate.js
vendored
Normal 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
|
||||||
7
docs/.vitepress/cache/deps/pinia-plugin-persistedstate.js.map
vendored
Normal file
7
docs/.vitepress/cache/deps/pinia-plugin-persistedstate.js.map
vendored
Normal 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
1715
docs/.vitepress/cache/deps/pinia.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
7
docs/.vitepress/cache/deps/pinia.js.map
vendored
Normal file
7
docs/.vitepress/cache/deps/pinia.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
4505
docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js
vendored
Normal file
4505
docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
7
docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js.map
vendored
Normal file
7
docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
9731
docs/.vitepress/cache/deps/vitepress___@vueuse_core.js
vendored
Normal file
9731
docs/.vitepress/cache/deps/vitepress___@vueuse_core.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
7
docs/.vitepress/cache/deps/vitepress___@vueuse_core.js.map
vendored
Normal file
7
docs/.vitepress/cache/deps/vitepress___@vueuse_core.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
347
docs/.vitepress/cache/deps/vue.js
vendored
Normal file
347
docs/.vitepress/cache/deps/vue.js
vendored
Normal 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
7
docs/.vitepress/cache/deps/vue.js.map
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"sources": [],
|
||||||
|
"sourcesContent": [],
|
||||||
|
"mappings": "",
|
||||||
|
"names": []
|
||||||
|
}
|
||||||
69
docs/.vitepress/config.mts
Normal file
69
docs/.vitepress/config.mts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { defineConfig } from 'vitepress'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
title: "乌仿镇",
|
||||||
|
description: "技术分享笔记",
|
||||||
|
lang: 'zh-CN',
|
||||||
|
vite: {
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'https://openlist.wufangzhen.com',
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
sitemap: {
|
||||||
|
hostname: 'https://wufangzhen.com'
|
||||||
|
},
|
||||||
|
themeConfig: {
|
||||||
|
outline: {
|
||||||
|
level: [2, 3],
|
||||||
|
label: '目录'
|
||||||
|
},
|
||||||
|
docFooter: {
|
||||||
|
prev: '上一页',
|
||||||
|
next: '下一页'
|
||||||
|
},
|
||||||
|
lastUpdated: {
|
||||||
|
text: '最后更新于',
|
||||||
|
formatOptions: {
|
||||||
|
dateStyle: 'short',
|
||||||
|
timeStyle: 'short'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
sidebar: {
|
||||||
|
'/java/': [
|
||||||
|
{
|
||||||
|
text: 'Java 笔记',
|
||||||
|
items: [
|
||||||
|
{ text: '概述', link: '/java/' },
|
||||||
|
{ text: '基础语法', link: '/java/basic' },
|
||||||
|
{ text: '面向对象', link: '/java/oop' },
|
||||||
|
{ text: '集合框架', link: '/java/collection' },
|
||||||
|
{ text: '多线程', link: '/java/thread' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'/vue/': [
|
||||||
|
{
|
||||||
|
text: 'Vue 笔记',
|
||||||
|
items: [
|
||||||
|
{ text: '概述', link: '/vue/' },
|
||||||
|
{ text: 'Vue3基础', link: '/vue/basic' },
|
||||||
|
{ text: '组件开发', link: '/vue/component' },
|
||||||
|
{ text: '组合式API', link: '/vue/composition' },
|
||||||
|
{ text: '状态管理', link: '/vue/pinia' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
nav: [
|
||||||
|
{ text: '首页', link: '/' },
|
||||||
|
{ text: 'Java', link: '/java/' },
|
||||||
|
{ text: 'Vue', link: '/vue/' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
297
docs/.vitepress/theme/Layout.vue
Normal file
297
docs/.vitepress/theme/Layout.vue
Normal file
@@ -0,0 +1,297 @@
|
|||||||
|
<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'
|
||||||
|
|
||||||
|
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 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 href="/java/" class="nav-link">Java</a>
|
||||||
|
<a href="/vue/" class="nav-link">Vue</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>
|
||||||
97
docs/.vitepress/theme/components/Cafe.vue
Normal file
97
docs/.vitepress/theme/components/Cafe.vue
Normal 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>
|
||||||
240
docs/.vitepress/theme/components/Calendar.vue
Normal file
240
docs/.vitepress/theme/components/Calendar.vue
Normal 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>
|
||||||
84
docs/.vitepress/theme/components/DocSidebar.vue
Normal file
84
docs/.vitepress/theme/components/DocSidebar.vue
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useData } from 'vitepress'
|
||||||
|
|
||||||
|
const { page } = useData()
|
||||||
|
|
||||||
|
const sidebarItems = {
|
||||||
|
'/java/': [
|
||||||
|
{ text: '概述', link: '/java/' },
|
||||||
|
{ text: '基础语法', link: '/java/basic' },
|
||||||
|
{ text: '面向对象', link: '/java/oop' },
|
||||||
|
{ text: '集合框架', link: '/java/collection' },
|
||||||
|
{ text: '多线程', link: '/java/thread' }
|
||||||
|
],
|
||||||
|
'/vue/': [
|
||||||
|
{ text: '概述', link: '/vue/' },
|
||||||
|
{ text: 'Vue3基础', link: '/vue/basic' },
|
||||||
|
{ text: '组件开发', link: '/vue/component' },
|
||||||
|
{ text: '组合式API', link: '/vue/composition' },
|
||||||
|
{ text: '状态管理', link: '/vue/pinia' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCurrentSidebar() {
|
||||||
|
const path = page.value.relativePath
|
||||||
|
for (const [prefix, items] of Object.entries(sidebarItems)) {
|
||||||
|
if (path.startsWith(prefix.replace(/^\//, ''))) {
|
||||||
|
return { title: prefix === '/java/' ? 'Java 笔记' : 'Vue 笔记', items }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentSidebar = getCurrentSidebar()
|
||||||
|
</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>
|
||||||
198
docs/.vitepress/theme/components/Dock.vue
Normal file
198
docs/.vitepress/theme/components/Dock.vue
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
<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'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
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>
|
||||||
265
docs/.vitepress/theme/components/DraggableWidget.vue
Normal file
265
docs/.vitepress/theme/components/DraggableWidget.vue
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
<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 }
|
||||||
|
return widgetState.value?.position ?? 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 storedData = localStorage.getItem('wufangzhen-settings')
|
||||||
|
const parsed = storedData ? JSON.parse(storedData) : null
|
||||||
|
const storedWidget = parsed?.widgets?.find((w: { id: string }) => w.id === props.id)
|
||||||
|
|
||||||
|
if (!storedWidget) {
|
||||||
|
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>
|
||||||
460
docs/.vitepress/theme/components/Settings.vue
Normal file
460
docs/.vitepress/theme/components/Settings.vue
Normal file
@@ -0,0 +1,460 @@
|
|||||||
|
<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)
|
||||||
|
|
||||||
|
watch(() => settingsStore.theme, (newTheme) => {
|
||||||
|
primaryColor.value = newTheme.primaryColor
|
||||||
|
backgroundColor.value = newTheme.backgroundColor
|
||||||
|
blurAmount.value = newTheme.blurAmount
|
||||||
|
}, { 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: '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 resetSettings() {
|
||||||
|
settingsStore.resetTheme()
|
||||||
|
primaryColor.value = settingsStore.theme.primaryColor
|
||||||
|
backgroundColor.value = settingsStore.theme.backgroundColor
|
||||||
|
blurAmount.value = settingsStore.theme.blurAmount
|
||||||
|
}
|
||||||
|
|
||||||
|
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 === '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>
|
||||||
107
docs/.vitepress/theme/components/TaoXin.vue
Normal file
107
docs/.vitepress/theme/components/TaoXin.vue
Normal 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>
|
||||||
106
docs/.vitepress/theme/components/VideoBackground.vue
Normal file
106
docs/.vitepress/theme/components/VideoBackground.vue
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch, onMounted } from 'vue'
|
||||||
|
import { useSettingsStore } from '../stores/settings'
|
||||||
|
import { getVideoUrl } from '../utils'
|
||||||
|
|
||||||
|
const settingsStore = useSettingsStore()
|
||||||
|
const videoRef = ref<HTMLVideoElement | null>(null)
|
||||||
|
const isLoading = ref(true)
|
||||||
|
|
||||||
|
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"></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>
|
||||||
365
docs/.vitepress/theme/components/Window.vue
Normal file
365
docs/.vitepress/theme/components/Window.vue
Normal 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>
|
||||||
112
docs/.vitepress/theme/data/quotes.ts
Normal file
112
docs/.vitepress/theme/data/quotes.ts
Normal 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)]
|
||||||
|
}
|
||||||
21
docs/.vitepress/theme/index.ts
Normal file
21
docs/.vitepress/theme/index.ts
Normal 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
|
||||||
219
docs/.vitepress/theme/stores/settings.ts
Normal file
219
docs/.vitepress/theme/stores/settings.ts
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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: 20, y: 20 + index * 120 },
|
||||||
|
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']
|
||||||
|
}
|
||||||
|
})
|
||||||
48
docs/.vitepress/theme/style.css
Normal file
48
docs/.vitepress/theme/style.css
Normal 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;
|
||||||
|
}
|
||||||
50
docs/.vitepress/theme/types/index.ts
Normal file
50
docs/.vitepress/theme/types/index.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SettingsState {
|
||||||
|
currentVideo: VideoItem | null
|
||||||
|
videoList: VideoItem[]
|
||||||
|
theme: ThemeSettings
|
||||||
|
widgets: WidgetState[]
|
||||||
|
windows: WindowState[]
|
||||||
|
isMobile: boolean
|
||||||
|
}
|
||||||
40
docs/.vitepress/theme/utils/index.ts
Normal file
40
docs/.vitepress/theme/utils/index.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
export function getVideoUrl(name: string): string {
|
||||||
|
return `https://openlist.wufangzhen.com/d/139/wufangzhen.com/mp4/${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
|
||||||
|
}
|
||||||
7
docs/index.md
Normal file
7
docs/index.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
home: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# 乌仿镇
|
||||||
|
|
||||||
|
技术分享笔记
|
||||||
109
docs/java/basic.md
Normal file
109
docs/java/basic.md
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
# 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
|
||||||
|
```
|
||||||
175
docs/java/collection.md
Normal file
175
docs/java/collection.md
Normal 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
10
docs/java/index.md
Normal 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
157
docs/java/oop.md
Normal 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
198
docs/java/thread.md
Normal 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
240
docs/vue/basic.md
Normal 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
251
docs/vue/component.md
Normal 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
242
docs/vue/composition.md
Normal 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
10
docs/vue/index.md
Normal 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
224
docs/vue/pinia.md
Normal 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)
|
||||||
|
```
|
||||||
2554
package-lock.json
generated
Normal file
2554
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
14
package.json
Normal file
14
package.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"docs:dev": "vitepress dev docs",
|
||||||
|
"docs:build": "vitepress build docs",
|
||||||
|
"docs:preview": "vitepress preview docs"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"pinia": "^2.1.7",
|
||||||
|
"pinia-plugin-persistedstate": "^3.2.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"vitepress": "^1.3.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user