diff --git a/ui/vhtml/axios.min.js b/ui/vhtml/axios.min.js
deleted file mode 100644
index c0d0b27..0000000
--- a/ui/vhtml/axios.min.js
+++ /dev/null
@@ -1,3 +0,0 @@
-/*! Axios v1.10.0 Copyright (c) 2025 Matt Zabriskie and contributors */
-function e(e,t){return function(){return e.apply(t,arguments)}}const{toString:t}=Object.prototype,{getPrototypeOf:n}=Object,{iterator:r,toStringTag:o}=Symbol,s=(i=Object.create(null),e=>{const n=t.call(e);return i[n]||(i[n]=n.slice(8,-1).toLowerCase())});var i;const a=e=>(e=e.toLowerCase(),t=>s(t)===e),c=e=>t=>typeof t===e,{isArray:l}=Array,u=c("undefined");const f=a("ArrayBuffer");const d=c("string"),p=c("function"),h=c("number"),m=e=>null!==e&&"object"==typeof e,y=e=>{if("object"!==s(e))return!1;const t=n(e);return!(null!==t&&t!==Object.prototype&&null!==Object.getPrototypeOf(t)||o in e||r in e)},b=a("Date"),g=a("File"),w=a("Blob"),E=a("FileList"),O=a("URLSearchParams"),[R,S,T,A]=["ReadableStream","Request","Response","Headers"].map(a);function v(e,t,{allOwnKeys:n=!1}={}){if(null==e)return;let r,o;if("object"!=typeof e&&(e=[e]),l(e))for(r=0,o=e.length;r0;)if(r=n[o],t===r.toLowerCase())return r;return null}const C="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:"undefined"!=typeof window?window:global,j=e=>!u(e)&&e!==C;const N=(U="undefined"!=typeof Uint8Array&&n(Uint8Array),e=>U&&e instanceof U);var U;const P=a("HTMLFormElement"),_=(({hasOwnProperty:e})=>(t,n)=>e.call(t,n))(Object.prototype),F=a("RegExp"),L=(e,t)=>{const n=Object.getOwnPropertyDescriptors(e),r={};v(n,((n,o)=>{let s;!1!==(s=t(n,o,e))&&(r[o]=s||n)})),Object.defineProperties(e,r)};const B=a("AsyncFunction"),k=(D="function"==typeof setImmediate,q=p(C.postMessage),D?setImmediate:q?(I=`axios@${Math.random()}`,M=[],C.addEventListener("message",(({source:e,data:t})=>{e===C&&t===I&&M.length&&M.shift()()}),!1),e=>{M.push(e),C.postMessage(I,"*")}):e=>setTimeout(e));var D,q,I,M;const z="undefined"!=typeof queueMicrotask?queueMicrotask.bind(C):"undefined"!=typeof process&&process.nextTick||k,H={isArray:l,isArrayBuffer:f,isBuffer:function(e){return null!==e&&!u(e)&&null!==e.constructor&&!u(e.constructor)&&p(e.constructor.isBuffer)&&e.constructor.isBuffer(e)},isFormData:e=>{let t;return e&&("function"==typeof FormData&&e instanceof FormData||p(e.append)&&("formdata"===(t=s(e))||"object"===t&&p(e.toString)&&"[object FormData]"===e.toString()))},isArrayBufferView:function(e){let t;return t="undefined"!=typeof ArrayBuffer&&ArrayBuffer.isView?ArrayBuffer.isView(e):e&&e.buffer&&f(e.buffer),t},isString:d,isNumber:h,isBoolean:e=>!0===e||!1===e,isObject:m,isPlainObject:y,isReadableStream:R,isRequest:S,isResponse:T,isHeaders:A,isUndefined:u,isDate:b,isFile:g,isBlob:w,isRegExp:F,isFunction:p,isStream:e=>m(e)&&p(e.pipe),isURLSearchParams:O,isTypedArray:N,isFileList:E,forEach:v,merge:function e(){const{caseless:t}=j(this)&&this||{},n={},r=(r,o)=>{const s=t&&x(n,o)||o;y(n[s])&&y(r)?n[s]=e(n[s],r):y(r)?n[s]=e({},r):l(r)?n[s]=r.slice():n[s]=r};for(let e=0,t=arguments.length;e(v(n,((n,o)=>{r&&p(n)?t[o]=e(n,r):t[o]=n}),{allOwnKeys:o}),t),trim:e=>e.trim?e.trim():e.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,""),stripBOM:e=>(65279===e.charCodeAt(0)&&(e=e.slice(1)),e),inherits:(e,t,n,r)=>{e.prototype=Object.create(t.prototype,r),e.prototype.constructor=e,Object.defineProperty(e,"super",{value:t.prototype}),n&&Object.assign(e.prototype,n)},toFlatObject:(e,t,r,o)=>{let s,i,a;const c={};if(t=t||{},null==e)return t;do{for(s=Object.getOwnPropertyNames(e),i=s.length;i-- >0;)a=s[i],o&&!o(a,e,t)||c[a]||(t[a]=e[a],c[a]=!0);e=!1!==r&&n(e)}while(e&&(!r||r(e,t))&&e!==Object.prototype);return t},kindOf:s,kindOfTest:a,endsWith:(e,t,n)=>{e=String(e),(void 0===n||n>e.length)&&(n=e.length),n-=t.length;const r=e.indexOf(t,n);return-1!==r&&r===n},toArray:e=>{if(!e)return null;if(l(e))return e;let t=e.length;if(!h(t))return null;const n=new Array(t);for(;t-- >0;)n[t]=e[t];return n},forEachEntry:(e,t)=>{const n=(e&&e[r]).call(e);let o;for(;(o=n.next())&&!o.done;){const n=o.value;t.call(e,n[0],n[1])}},matchAll:(e,t)=>{let n;const r=[];for(;null!==(n=e.exec(t));)r.push(n);return r},isHTMLForm:P,hasOwnProperty:_,hasOwnProp:_,reduceDescriptors:L,freezeMethods:e=>{L(e,((t,n)=>{if(p(e)&&-1!==["arguments","caller","callee"].indexOf(n))return!1;const r=e[n];p(r)&&(t.enumerable=!1,"writable"in t?t.writable=!1:t.set||(t.set=()=>{throw Error("Can not rewrite read-only method '"+n+"'")}))}))},toObjectSet:(e,t)=>{const n={},r=e=>{e.forEach((e=>{n[e]=!0}))};return l(e)?r(e):r(String(e).split(t)),n},toCamelCase:e=>e.toLowerCase().replace(/[-_\s]([a-z\d])(\w*)/g,(function(e,t,n){return t.toUpperCase()+n})),noop:()=>{},toFiniteNumber:(e,t)=>null!=e&&Number.isFinite(e=+e)?e:t,findKey:x,global:C,isContextDefined:j,isSpecCompliantForm:function(e){return!!(e&&p(e.append)&&"FormData"===e[o]&&e[r])},toJSONObject:e=>{const t=new Array(10),n=(e,r)=>{if(m(e)){if(t.indexOf(e)>=0)return;if(!("toJSON"in e)){t[r]=e;const o=l(e)?[]:{};return v(e,((e,t)=>{const s=n(e,r+1);!u(s)&&(o[t]=s)})),t[r]=void 0,o}}return e};return n(e,0)},isAsyncFn:B,isThenable:e=>e&&(m(e)||p(e))&&p(e.then)&&p(e.catch),setImmediate:k,asap:z,isIterable:e=>null!=e&&p(e[r])};function J(e,t,n,r,o){Error.call(this),Error.captureStackTrace?Error.captureStackTrace(this,this.constructor):this.stack=(new Error).stack,this.message=e,this.name="AxiosError",t&&(this.code=t),n&&(this.config=n),r&&(this.request=r),o&&(this.response=o,this.status=o.status?o.status:null)}H.inherits(J,Error,{toJSON:function(){return{message:this.message,name:this.name,description:this.description,number:this.number,fileName:this.fileName,lineNumber:this.lineNumber,columnNumber:this.columnNumber,stack:this.stack,config:H.toJSONObject(this.config),code:this.code,status:this.status}}});const W=J.prototype,K={};["ERR_BAD_OPTION_VALUE","ERR_BAD_OPTION","ECONNABORTED","ETIMEDOUT","ERR_NETWORK","ERR_FR_TOO_MANY_REDIRECTS","ERR_DEPRECATED","ERR_BAD_RESPONSE","ERR_BAD_REQUEST","ERR_CANCELED","ERR_NOT_SUPPORT","ERR_INVALID_URL"].forEach((e=>{K[e]={value:e}})),Object.defineProperties(J,K),Object.defineProperty(W,"isAxiosError",{value:!0}),J.from=(e,t,n,r,o,s)=>{const i=Object.create(W);return H.toFlatObject(e,i,(function(e){return e!==Error.prototype}),(e=>"isAxiosError"!==e)),J.call(i,e.message,t,n,r,o),i.cause=e,i.name=e.name,s&&Object.assign(i,s),i};function V(e){return H.isPlainObject(e)||H.isArray(e)}function $(e){return H.endsWith(e,"[]")?e.slice(0,-2):e}function X(e,t,n){return e?e.concat(t).map((function(e,t){return e=$(e),!n&&t?"["+e+"]":e})).join(n?".":""):t}const G=H.toFlatObject(H,{},null,(function(e){return/^is[A-Z]/.test(e)}));function Q(e,t,n){if(!H.isObject(e))throw new TypeError("target must be an object");t=t||new FormData;const r=(n=H.toFlatObject(n,{metaTokens:!0,dots:!1,indexes:!1},!1,(function(e,t){return!H.isUndefined(t[e])}))).metaTokens,o=n.visitor||l,s=n.dots,i=n.indexes,a=(n.Blob||"undefined"!=typeof Blob&&Blob)&&H.isSpecCompliantForm(t);if(!H.isFunction(o))throw new TypeError("visitor must be a function");function c(e){if(null===e)return"";if(H.isDate(e))return e.toISOString();if(H.isBoolean(e))return e.toString();if(!a&&H.isBlob(e))throw new J("Blob is not supported. Use a Buffer instead.");return H.isArrayBuffer(e)||H.isTypedArray(e)?a&&"function"==typeof Blob?new Blob([e]):Buffer.from(e):e}function l(e,n,o){let a=e;if(e&&!o&&"object"==typeof e)if(H.endsWith(n,"{}"))n=r?n:n.slice(0,-2),e=JSON.stringify(e);else if(H.isArray(e)&&function(e){return H.isArray(e)&&!e.some(V)}(e)||(H.isFileList(e)||H.endsWith(n,"[]"))&&(a=H.toArray(e)))return n=$(n),a.forEach((function(e,r){!H.isUndefined(e)&&null!==e&&t.append(!0===i?X([n],r,s):null===i?n:n+"[]",c(e))})),!1;return!!V(e)||(t.append(X(o,n,s),c(e)),!1)}const u=[],f=Object.assign(G,{defaultVisitor:l,convertValue:c,isVisitable:V});if(!H.isObject(e))throw new TypeError("data must be an object");return function e(n,r){if(!H.isUndefined(n)){if(-1!==u.indexOf(n))throw Error("Circular reference detected in "+r.join("."));u.push(n),H.forEach(n,(function(n,s){!0===(!(H.isUndefined(n)||null===n)&&o.call(t,n,H.isString(s)?s.trim():s,r,f))&&e(n,r?r.concat(s):[s])})),u.pop()}}(e),t}function Z(e){const t={"!":"%21","'":"%27","(":"%28",")":"%29","~":"%7E","%20":"+","%00":"\0"};return encodeURIComponent(e).replace(/[!'()~]|%20|%00/g,(function(e){return t[e]}))}function Y(e,t){this._pairs=[],e&&Q(e,this,t)}const ee=Y.prototype;function te(e){return encodeURIComponent(e).replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(/%20/g,"+").replace(/%5B/gi,"[").replace(/%5D/gi,"]")}function ne(e,t,n){if(!t)return e;const r=n&&n.encode||te;H.isFunction(n)&&(n={serialize:n});const o=n&&n.serialize;let s;if(s=o?o(t,n):H.isURLSearchParams(t)?t.toString():new Y(t,n).toString(r),s){const t=e.indexOf("#");-1!==t&&(e=e.slice(0,t)),e+=(-1===e.indexOf("?")?"?":"&")+s}return e}ee.append=function(e,t){this._pairs.push([e,t])},ee.toString=function(e){const t=e?function(t){return e.call(this,t,Z)}:Z;return this._pairs.map((function(e){return t(e[0])+"="+t(e[1])}),"").join("&")};const re=class{constructor(){this.handlers=[]}use(e,t,n){return this.handlers.push({fulfilled:e,rejected:t,synchronous:!!n&&n.synchronous,runWhen:n?n.runWhen:null}),this.handlers.length-1}eject(e){this.handlers[e]&&(this.handlers[e]=null)}clear(){this.handlers&&(this.handlers=[])}forEach(e){H.forEach(this.handlers,(function(t){null!==t&&e(t)}))}},oe={silentJSONParsing:!0,forcedJSONParsing:!0,clarifyTimeoutError:!1},se={isBrowser:!0,classes:{URLSearchParams:"undefined"!=typeof URLSearchParams?URLSearchParams:Y,FormData:"undefined"!=typeof FormData?FormData:null,Blob:"undefined"!=typeof Blob?Blob:null},protocols:["http","https","file","blob","url","data"]},ie="undefined"!=typeof window&&"undefined"!=typeof document,ae="object"==typeof navigator&&navigator||void 0,ce=ie&&(!ae||["ReactNative","NativeScript","NS"].indexOf(ae.product)<0),le="undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope&&"function"==typeof self.importScripts,ue=ie&&window.location.href||"http://localhost",fe={...Object.freeze({__proto__:null,hasBrowserEnv:ie,hasStandardBrowserWebWorkerEnv:le,hasStandardBrowserEnv:ce,navigator:ae,origin:ue}),...se};function de(e){function t(e,n,r,o){let s=e[o++];if("__proto__"===s)return!0;const i=Number.isFinite(+s),a=o>=e.length;if(s=!s&&H.isArray(r)?r.length:s,a)return H.hasOwnProp(r,s)?r[s]=[r[s],n]:r[s]=n,!i;r[s]&&H.isObject(r[s])||(r[s]=[]);return t(e,n,r[s],o)&&H.isArray(r[s])&&(r[s]=function(e){const t={},n=Object.keys(e);let r;const o=n.length;let s;for(r=0;r{t(function(e){return H.matchAll(/\w+|\[(\w*)]/g,e).map((e=>"[]"===e[0]?"":e[1]||e[0]))}(e),r,n,0)})),n}return null}const pe={transitional:oe,adapter:["xhr","http","fetch"],transformRequest:[function(e,t){const n=t.getContentType()||"",r=n.indexOf("application/json")>-1,o=H.isObject(e);o&&H.isHTMLForm(e)&&(e=new FormData(e));if(H.isFormData(e))return r?JSON.stringify(de(e)):e;if(H.isArrayBuffer(e)||H.isBuffer(e)||H.isStream(e)||H.isFile(e)||H.isBlob(e)||H.isReadableStream(e))return e;if(H.isArrayBufferView(e))return e.buffer;if(H.isURLSearchParams(e))return t.setContentType("application/x-www-form-urlencoded;charset=utf-8",!1),e.toString();let s;if(o){if(n.indexOf("application/x-www-form-urlencoded")>-1)return function(e,t){return Q(e,new fe.classes.URLSearchParams,Object.assign({visitor:function(e,t,n,r){return fe.isNode&&H.isBuffer(e)?(this.append(t,e.toString("base64")),!1):r.defaultVisitor.apply(this,arguments)}},t))}(e,this.formSerializer).toString();if((s=H.isFileList(e))||n.indexOf("multipart/form-data")>-1){const t=this.env&&this.env.FormData;return Q(s?{"files[]":e}:e,t&&new t,this.formSerializer)}}return o||r?(t.setContentType("application/json",!1),function(e,t,n){if(H.isString(e))try{return(t||JSON.parse)(e),H.trim(e)}catch(e){if("SyntaxError"!==e.name)throw e}return(n||JSON.stringify)(e)}(e)):e}],transformResponse:[function(e){const t=this.transitional||pe.transitional,n=t&&t.forcedJSONParsing,r="json"===this.responseType;if(H.isResponse(e)||H.isReadableStream(e))return e;if(e&&H.isString(e)&&(n&&!this.responseType||r)){const n=!(t&&t.silentJSONParsing)&&r;try{return JSON.parse(e)}catch(e){if(n){if("SyntaxError"===e.name)throw J.from(e,J.ERR_BAD_RESPONSE,this,null,this.response);throw e}}}return e}],timeout:0,xsrfCookieName:"XSRF-TOKEN",xsrfHeaderName:"X-XSRF-TOKEN",maxContentLength:-1,maxBodyLength:-1,env:{FormData:fe.classes.FormData,Blob:fe.classes.Blob},validateStatus:function(e){return e>=200&&e<300},headers:{common:{Accept:"application/json, text/plain, */*","Content-Type":void 0}}};H.forEach(["delete","get","head","post","put","patch"],(e=>{pe.headers[e]={}}));const he=pe,me=H.toObjectSet(["age","authorization","content-length","content-type","etag","expires","from","host","if-modified-since","if-unmodified-since","last-modified","location","max-forwards","proxy-authorization","referer","retry-after","user-agent"]),ye=Symbol("internals");function be(e){return e&&String(e).trim().toLowerCase()}function ge(e){return!1===e||null==e?e:H.isArray(e)?e.map(ge):String(e)}function we(e,t,n,r,o){return H.isFunction(r)?r.call(this,t,n):(o&&(t=n),H.isString(t)?H.isString(r)?-1!==t.indexOf(r):H.isRegExp(r)?r.test(t):void 0:void 0)}class Ee{constructor(e){e&&this.set(e)}set(e,t,n){const r=this;function o(e,t,n){const o=be(t);if(!o)throw new Error("header name must be a non-empty string");const s=H.findKey(r,o);(!s||void 0===r[s]||!0===n||void 0===n&&!1!==r[s])&&(r[s||t]=ge(e))}const s=(e,t)=>H.forEach(e,((e,n)=>o(e,n,t)));if(H.isPlainObject(e)||e instanceof this.constructor)s(e,t);else if(H.isString(e)&&(e=e.trim())&&!/^[-_a-zA-Z0-9^`|~,!#$%&'*+.]+$/.test(e.trim()))s((e=>{const t={};let n,r,o;return e&&e.split("\n").forEach((function(e){o=e.indexOf(":"),n=e.substring(0,o).trim().toLowerCase(),r=e.substring(o+1).trim(),!n||t[n]&&me[n]||("set-cookie"===n?t[n]?t[n].push(r):t[n]=[r]:t[n]=t[n]?t[n]+", "+r:r)})),t})(e),t);else if(H.isObject(e)&&H.isIterable(e)){let n,r,o={};for(const t of e){if(!H.isArray(t))throw TypeError("Object iterator must return a key-value pair");o[r=t[0]]=(n=o[r])?H.isArray(n)?[...n,t[1]]:[n,t[1]]:t[1]}s(o,t)}else null!=e&&o(t,e,n);return this}get(e,t){if(e=be(e)){const n=H.findKey(this,e);if(n){const e=this[n];if(!t)return e;if(!0===t)return function(e){const t=Object.create(null),n=/([^\s,;=]+)\s*(?:=\s*([^,;]+))?/g;let r;for(;r=n.exec(e);)t[r[1]]=r[2];return t}(e);if(H.isFunction(t))return t.call(this,e,n);if(H.isRegExp(t))return t.exec(e);throw new TypeError("parser must be boolean|regexp|function")}}}has(e,t){if(e=be(e)){const n=H.findKey(this,e);return!(!n||void 0===this[n]||t&&!we(0,this[n],n,t))}return!1}delete(e,t){const n=this;let r=!1;function o(e){if(e=be(e)){const o=H.findKey(n,e);!o||t&&!we(0,n[o],o,t)||(delete n[o],r=!0)}}return H.isArray(e)?e.forEach(o):o(e),r}clear(e){const t=Object.keys(this);let n=t.length,r=!1;for(;n--;){const o=t[n];e&&!we(0,this[o],o,e,!0)||(delete this[o],r=!0)}return r}normalize(e){const t=this,n={};return H.forEach(this,((r,o)=>{const s=H.findKey(n,o);if(s)return t[s]=ge(r),void delete t[o];const i=e?function(e){return e.trim().toLowerCase().replace(/([a-z\d])(\w*)/g,((e,t,n)=>t.toUpperCase()+n))}(o):String(o).trim();i!==o&&delete t[o],t[i]=ge(r),n[i]=!0})),this}concat(...e){return this.constructor.concat(this,...e)}toJSON(e){const t=Object.create(null);return H.forEach(this,((n,r)=>{null!=n&&!1!==n&&(t[r]=e&&H.isArray(n)?n.join(", "):n)})),t}[Symbol.iterator](){return Object.entries(this.toJSON())[Symbol.iterator]()}toString(){return Object.entries(this.toJSON()).map((([e,t])=>e+": "+t)).join("\n")}getSetCookie(){return this.get("set-cookie")||[]}get[Symbol.toStringTag](){return"AxiosHeaders"}static from(e){return e instanceof this?e:new this(e)}static concat(e,...t){const n=new this(e);return t.forEach((e=>n.set(e))),n}static accessor(e){const t=(this[ye]=this[ye]={accessors:{}}).accessors,n=this.prototype;function r(e){const r=be(e);t[r]||(!function(e,t){const n=H.toCamelCase(" "+t);["get","set","has"].forEach((r=>{Object.defineProperty(e,r+n,{value:function(e,n,o){return this[r].call(this,t,e,n,o)},configurable:!0})}))}(n,e),t[r]=!0)}return H.isArray(e)?e.forEach(r):r(e),this}}Ee.accessor(["Content-Type","Content-Length","Accept","Accept-Encoding","User-Agent","Authorization"]),H.reduceDescriptors(Ee.prototype,(({value:e},t)=>{let n=t[0].toUpperCase()+t.slice(1);return{get:()=>e,set(e){this[n]=e}}})),H.freezeMethods(Ee);const Oe=Ee;function Re(e,t){const n=this||he,r=t||n,o=Oe.from(r.headers);let s=r.data;return H.forEach(e,(function(e){s=e.call(n,s,o.normalize(),t?t.status:void 0)})),o.normalize(),s}function Se(e){return!(!e||!e.__CANCEL__)}function Te(e,t,n){J.call(this,null==e?"canceled":e,J.ERR_CANCELED,t,n),this.name="CanceledError"}function Ae(e,t,n){const r=n.config.validateStatus;n.status&&r&&!r(n.status)?t(new J("Request failed with status code "+n.status,[J.ERR_BAD_REQUEST,J.ERR_BAD_RESPONSE][Math.floor(n.status/100)-4],n.config,n.request,n)):e(n)}H.inherits(Te,J,{__CANCEL__:!0});const ve=(e,t,n=3)=>{let r=0;const o=function(e,t){e=e||10;const n=new Array(e),r=new Array(e);let o,s=0,i=0;return t=void 0!==t?t:1e3,function(a){const c=Date.now(),l=r[i];o||(o=c),n[s]=a,r[s]=c;let u=i,f=0;for(;u!==s;)f+=n[u++],u%=e;if(s=(s+1)%e,s===i&&(i=(i+1)%e),c-o{o=s,n=null,r&&(clearTimeout(r),r=null),e.apply(null,t)};return[(...e)=>{const t=Date.now(),a=t-o;a>=s?i(e,t):(n=e,r||(r=setTimeout((()=>{r=null,i(n)}),s-a)))},()=>n&&i(n)]}((n=>{const s=n.loaded,i=n.lengthComputable?n.total:void 0,a=s-r,c=o(a);r=s;e({loaded:s,total:i,progress:i?s/i:void 0,bytes:a,rate:c||void 0,estimated:c&&i&&s<=i?(i-s)/c:void 0,event:n,lengthComputable:null!=i,[t?"download":"upload"]:!0})}),n)},xe=(e,t)=>{const n=null!=e;return[r=>t[0]({lengthComputable:n,total:e,loaded:r}),t[1]]},Ce=e=>(...t)=>H.asap((()=>e(...t))),je=fe.hasStandardBrowserEnv?((e,t)=>n=>(n=new URL(n,fe.origin),e.protocol===n.protocol&&e.host===n.host&&(t||e.port===n.port)))(new URL(fe.origin),fe.navigator&&/(msie|trident)/i.test(fe.navigator.userAgent)):()=>!0,Ne=fe.hasStandardBrowserEnv?{write(e,t,n,r,o,s){const i=[e+"="+encodeURIComponent(t)];H.isNumber(n)&&i.push("expires="+new Date(n).toGMTString()),H.isString(r)&&i.push("path="+r),H.isString(o)&&i.push("domain="+o),!0===s&&i.push("secure"),document.cookie=i.join("; ")},read(e){const t=document.cookie.match(new RegExp("(^|;\\s*)("+e+")=([^;]*)"));return t?decodeURIComponent(t[3]):null},remove(e){this.write(e,"",Date.now()-864e5)}}:{write(){},read:()=>null,remove(){}};function Ue(e,t,n){let r=!/^([a-z][a-z\d+\-.]*:)?\/\//i.test(t);return e&&(r||0==n)?function(e,t){return t?e.replace(/\/?\/$/,"")+"/"+t.replace(/^\/+/,""):e}(e,t):t}const Pe=e=>e instanceof Oe?{...e}:e;function _e(e,t){t=t||{};const n={};function r(e,t,n,r){return H.isPlainObject(e)&&H.isPlainObject(t)?H.merge.call({caseless:r},e,t):H.isPlainObject(t)?H.merge({},t):H.isArray(t)?t.slice():t}function o(e,t,n,o){return H.isUndefined(t)?H.isUndefined(e)?void 0:r(void 0,e,0,o):r(e,t,0,o)}function s(e,t){if(!H.isUndefined(t))return r(void 0,t)}function i(e,t){return H.isUndefined(t)?H.isUndefined(e)?void 0:r(void 0,e):r(void 0,t)}function a(n,o,s){return s in t?r(n,o):s in e?r(void 0,n):void 0}const c={url:s,method:s,data:s,baseURL:i,transformRequest:i,transformResponse:i,paramsSerializer:i,timeout:i,timeoutMessage:i,withCredentials:i,withXSRFToken:i,adapter:i,responseType:i,xsrfCookieName:i,xsrfHeaderName:i,onUploadProgress:i,onDownloadProgress:i,decompress:i,maxContentLength:i,maxBodyLength:i,beforeRedirect:i,transport:i,httpAgent:i,httpsAgent:i,cancelToken:i,socketPath:i,responseEncoding:i,validateStatus:a,headers:(e,t,n)=>o(Pe(e),Pe(t),0,!0)};return H.forEach(Object.keys(Object.assign({},e,t)),(function(r){const s=c[r]||o,i=s(e[r],t[r],r);H.isUndefined(i)&&s!==a||(n[r]=i)})),n}const Fe=e=>{const t=_e({},e);let n,{data:r,withXSRFToken:o,xsrfHeaderName:s,xsrfCookieName:i,headers:a,auth:c}=t;if(t.headers=a=Oe.from(a),t.url=ne(Ue(t.baseURL,t.url,t.allowAbsoluteUrls),e.params,e.paramsSerializer),c&&a.set("Authorization","Basic "+btoa((c.username||"")+":"+(c.password?unescape(encodeURIComponent(c.password)):""))),H.isFormData(r))if(fe.hasStandardBrowserEnv||fe.hasStandardBrowserWebWorkerEnv)a.setContentType(void 0);else if(!1!==(n=a.getContentType())){const[e,...t]=n?n.split(";").map((e=>e.trim())).filter(Boolean):[];a.setContentType([e||"multipart/form-data",...t].join("; "))}if(fe.hasStandardBrowserEnv&&(o&&H.isFunction(o)&&(o=o(t)),o||!1!==o&&je(t.url))){const e=s&&i&&Ne.read(i);e&&a.set(s,e)}return t},Le="undefined"!=typeof XMLHttpRequest&&function(e){return new Promise((function(t,n){const r=Fe(e);let o=r.data;const s=Oe.from(r.headers).normalize();let i,a,c,l,u,{responseType:f,onUploadProgress:d,onDownloadProgress:p}=r;function h(){l&&l(),u&&u(),r.cancelToken&&r.cancelToken.unsubscribe(i),r.signal&&r.signal.removeEventListener("abort",i)}let m=new XMLHttpRequest;function y(){if(!m)return;const r=Oe.from("getAllResponseHeaders"in m&&m.getAllResponseHeaders());Ae((function(e){t(e),h()}),(function(e){n(e),h()}),{data:f&&"text"!==f&&"json"!==f?m.response:m.responseText,status:m.status,statusText:m.statusText,headers:r,config:e,request:m}),m=null}m.open(r.method.toUpperCase(),r.url,!0),m.timeout=r.timeout,"onloadend"in m?m.onloadend=y:m.onreadystatechange=function(){m&&4===m.readyState&&(0!==m.status||m.responseURL&&0===m.responseURL.indexOf("file:"))&&setTimeout(y)},m.onabort=function(){m&&(n(new J("Request aborted",J.ECONNABORTED,e,m)),m=null)},m.onerror=function(){n(new J("Network Error",J.ERR_NETWORK,e,m)),m=null},m.ontimeout=function(){let t=r.timeout?"timeout of "+r.timeout+"ms exceeded":"timeout exceeded";const o=r.transitional||oe;r.timeoutErrorMessage&&(t=r.timeoutErrorMessage),n(new J(t,o.clarifyTimeoutError?J.ETIMEDOUT:J.ECONNABORTED,e,m)),m=null},void 0===o&&s.setContentType(null),"setRequestHeader"in m&&H.forEach(s.toJSON(),(function(e,t){m.setRequestHeader(t,e)})),H.isUndefined(r.withCredentials)||(m.withCredentials=!!r.withCredentials),f&&"json"!==f&&(m.responseType=r.responseType),p&&([c,u]=ve(p,!0),m.addEventListener("progress",c)),d&&m.upload&&([a,l]=ve(d),m.upload.addEventListener("progress",a),m.upload.addEventListener("loadend",l)),(r.cancelToken||r.signal)&&(i=t=>{m&&(n(!t||t.type?new Te(null,e,m):t),m.abort(),m=null)},r.cancelToken&&r.cancelToken.subscribe(i),r.signal&&(r.signal.aborted?i():r.signal.addEventListener("abort",i)));const b=function(e){const t=/^([-+\w]{1,25})(:?\/\/|:)/.exec(e);return t&&t[1]||""}(r.url);b&&-1===fe.protocols.indexOf(b)?n(new J("Unsupported protocol "+b+":",J.ERR_BAD_REQUEST,e)):m.send(o||null)}))},Be=(e,t)=>{const{length:n}=e=e?e.filter(Boolean):[];if(t||n){let n,r=new AbortController;const o=function(e){if(!n){n=!0,i();const t=e instanceof Error?e:this.reason;r.abort(t instanceof J?t:new Te(t instanceof Error?t.message:t))}};let s=t&&setTimeout((()=>{s=null,o(new J(`timeout ${t} of ms exceeded`,J.ETIMEDOUT))}),t);const i=()=>{e&&(s&&clearTimeout(s),s=null,e.forEach((e=>{e.unsubscribe?e.unsubscribe(o):e.removeEventListener("abort",o)})),e=null)};e.forEach((e=>e.addEventListener("abort",o)));const{signal:a}=r;return a.unsubscribe=()=>H.asap(i),a}},ke=function*(e,t){let n=e.byteLength;if(!t||n{const o=async function*(e,t){for await(const n of De(e))yield*ke(n,t)}(e,t);let s,i=0,a=e=>{s||(s=!0,r&&r(e))};return new ReadableStream({async pull(e){try{const{done:t,value:r}=await o.next();if(t)return a(),void e.close();let s=r.byteLength;if(n){let e=i+=s;n(e)}e.enqueue(new Uint8Array(r))}catch(e){throw a(e),e}},cancel:e=>(a(e),o.return())},{highWaterMark:2})},Ie="function"==typeof fetch&&"function"==typeof Request&&"function"==typeof Response,Me=Ie&&"function"==typeof ReadableStream,ze=Ie&&("function"==typeof TextEncoder?(He=new TextEncoder,e=>He.encode(e)):async e=>new Uint8Array(await new Response(e).arrayBuffer()));var He;const Je=(e,...t)=>{try{return!!e(...t)}catch(e){return!1}},We=Me&&Je((()=>{let e=!1;const t=new Request(fe.origin,{body:new ReadableStream,method:"POST",get duplex(){return e=!0,"half"}}).headers.has("Content-Type");return e&&!t})),Ke=Me&&Je((()=>H.isReadableStream(new Response("").body))),Ve={stream:Ke&&(e=>e.body)};var $e;Ie&&($e=new Response,["text","arrayBuffer","blob","formData","stream"].forEach((e=>{!Ve[e]&&(Ve[e]=H.isFunction($e[e])?t=>t[e]():(t,n)=>{throw new J(`Response type '${e}' is not supported`,J.ERR_NOT_SUPPORT,n)})})));const Xe=async(e,t)=>{const n=H.toFiniteNumber(e.getContentLength());return null==n?(async e=>{if(null==e)return 0;if(H.isBlob(e))return e.size;if(H.isSpecCompliantForm(e)){const t=new Request(fe.origin,{method:"POST",body:e});return(await t.arrayBuffer()).byteLength}return H.isArrayBufferView(e)||H.isArrayBuffer(e)?e.byteLength:(H.isURLSearchParams(e)&&(e+=""),H.isString(e)?(await ze(e)).byteLength:void 0)})(t):n},Ge={http:null,xhr:Le,fetch:Ie&&(async e=>{let{url:t,method:n,data:r,signal:o,cancelToken:s,timeout:i,onDownloadProgress:a,onUploadProgress:c,responseType:l,headers:u,withCredentials:f="same-origin",fetchOptions:d}=Fe(e);l=l?(l+"").toLowerCase():"text";let p,h=Be([o,s&&s.toAbortSignal()],i);const m=h&&h.unsubscribe&&(()=>{h.unsubscribe()});let y;try{if(c&&We&&"get"!==n&&"head"!==n&&0!==(y=await Xe(u,r))){let e,n=new Request(t,{method:"POST",body:r,duplex:"half"});if(H.isFormData(r)&&(e=n.headers.get("content-type"))&&u.setContentType(e),n.body){const[e,t]=xe(y,ve(Ce(c)));r=qe(n.body,65536,e,t)}}H.isString(f)||(f=f?"include":"omit");const o="credentials"in Request.prototype;p=new Request(t,{...d,signal:h,method:n.toUpperCase(),headers:u.normalize().toJSON(),body:r,duplex:"half",credentials:o?f:void 0});let s=await fetch(p,d);const i=Ke&&("stream"===l||"response"===l);if(Ke&&(a||i&&m)){const e={};["status","statusText","headers"].forEach((t=>{e[t]=s[t]}));const t=H.toFiniteNumber(s.headers.get("content-length")),[n,r]=a&&xe(t,ve(Ce(a),!0))||[];s=new Response(qe(s.body,65536,n,(()=>{r&&r(),m&&m()})),e)}l=l||"text";let b=await Ve[H.findKey(Ve,l)||"text"](s,e);return!i&&m&&m(),await new Promise(((t,n)=>{Ae(t,n,{data:b,headers:Oe.from(s.headers),status:s.status,statusText:s.statusText,config:e,request:p})}))}catch(t){if(m&&m(),t&&"TypeError"===t.name&&/Load failed|fetch/i.test(t.message))throw Object.assign(new J("Network Error",J.ERR_NETWORK,e,p),{cause:t.cause||t});throw J.from(t,t&&t.code,e,p)}})};H.forEach(Ge,((e,t)=>{if(e){try{Object.defineProperty(e,"name",{value:t})}catch(e){}Object.defineProperty(e,"adapterName",{value:t})}}));const Qe=e=>`- ${e}`,Ze=e=>H.isFunction(e)||null===e||!1===e,Ye=e=>{e=H.isArray(e)?e:[e];const{length:t}=e;let n,r;const o={};for(let s=0;s`adapter ${e} `+(!1===t?"is not supported by the environment":"is not available in the build")));throw new J("There is no suitable adapter to dispatch the request "+(t?e.length>1?"since :\n"+e.map(Qe).join("\n"):" "+Qe(e[0]):"as no adapter specified"),"ERR_NOT_SUPPORT")}return r};function et(e){if(e.cancelToken&&e.cancelToken.throwIfRequested(),e.signal&&e.signal.aborted)throw new Te(null,e)}function tt(e){et(e),e.headers=Oe.from(e.headers),e.data=Re.call(e,e.transformRequest),-1!==["post","put","patch"].indexOf(e.method)&&e.headers.setContentType("application/x-www-form-urlencoded",!1);return Ye(e.adapter||he.adapter)(e).then((function(t){return et(e),t.data=Re.call(e,e.transformResponse,t),t.headers=Oe.from(t.headers),t}),(function(t){return Se(t)||(et(e),t&&t.response&&(t.response.data=Re.call(e,e.transformResponse,t.response),t.response.headers=Oe.from(t.response.headers))),Promise.reject(t)}))}const nt={};["object","boolean","number","function","string","symbol"].forEach(((e,t)=>{nt[e]=function(n){return typeof n===e||"a"+(t<1?"n ":" ")+e}}));const rt={};nt.transitional=function(e,t,n){function r(e,t){return"[Axios v1.10.0] Transitional option '"+e+"'"+t+(n?". "+n:"")}return(n,o,s)=>{if(!1===e)throw new J(r(o," has been removed"+(t?" in "+t:"")),J.ERR_DEPRECATED);return t&&!rt[o]&&(rt[o]=!0,console.warn(r(o," has been deprecated since v"+t+" and will be removed in the near future"))),!e||e(n,o,s)}},nt.spelling=function(e){return(t,n)=>(console.warn(`${n} is likely a misspelling of ${e}`),!0)};const ot={assertOptions:function(e,t,n){if("object"!=typeof e)throw new J("options must be an object",J.ERR_BAD_OPTION_VALUE);const r=Object.keys(e);let o=r.length;for(;o-- >0;){const s=r[o],i=t[s];if(i){const t=e[s],n=void 0===t||i(t,s,e);if(!0!==n)throw new J("option "+s+" must be "+n,J.ERR_BAD_OPTION_VALUE)}else if(!0!==n)throw new J("Unknown option "+s,J.ERR_BAD_OPTION)}},validators:nt},st=ot.validators;class it{constructor(e){this.defaults=e||{},this.interceptors={request:new re,response:new re}}async request(e,t){try{return await this._request(e,t)}catch(e){if(e instanceof Error){let t={};Error.captureStackTrace?Error.captureStackTrace(t):t=new Error;const n=t.stack?t.stack.replace(/^.+\n/,""):"";try{e.stack?n&&!String(e.stack).endsWith(n.replace(/^.+\n.+\n/,""))&&(e.stack+="\n"+n):e.stack=n}catch(e){}}throw e}}_request(e,t){"string"==typeof e?(t=t||{}).url=e:t=e||{},t=_e(this.defaults,t);const{transitional:n,paramsSerializer:r,headers:o}=t;void 0!==n&&ot.assertOptions(n,{silentJSONParsing:st.transitional(st.boolean),forcedJSONParsing:st.transitional(st.boolean),clarifyTimeoutError:st.transitional(st.boolean)},!1),null!=r&&(H.isFunction(r)?t.paramsSerializer={serialize:r}:ot.assertOptions(r,{encode:st.function,serialize:st.function},!0)),void 0!==t.allowAbsoluteUrls||(void 0!==this.defaults.allowAbsoluteUrls?t.allowAbsoluteUrls=this.defaults.allowAbsoluteUrls:t.allowAbsoluteUrls=!0),ot.assertOptions(t,{baseUrl:st.spelling("baseURL"),withXsrfToken:st.spelling("withXSRFToken")},!0),t.method=(t.method||this.defaults.method||"get").toLowerCase();let s=o&&H.merge(o.common,o[t.method]);o&&H.forEach(["delete","get","head","post","put","patch","common"],(e=>{delete o[e]})),t.headers=Oe.concat(s,o);const i=[];let a=!0;this.interceptors.request.forEach((function(e){"function"==typeof e.runWhen&&!1===e.runWhen(t)||(a=a&&e.synchronous,i.unshift(e.fulfilled,e.rejected))}));const c=[];let l;this.interceptors.response.forEach((function(e){c.push(e.fulfilled,e.rejected)}));let u,f=0;if(!a){const e=[tt.bind(this),void 0];for(e.unshift.apply(e,i),e.push.apply(e,c),u=e.length,l=Promise.resolve(t);f{if(!n._listeners)return;let t=n._listeners.length;for(;t-- >0;)n._listeners[t](e);n._listeners=null})),this.promise.then=e=>{let t;const r=new Promise((e=>{n.subscribe(e),t=e})).then(e);return r.cancel=function(){n.unsubscribe(t)},r},e((function(e,r,o){n.reason||(n.reason=new Te(e,r,o),t(n.reason))}))}throwIfRequested(){if(this.reason)throw this.reason}subscribe(e){this.reason?e(this.reason):this._listeners?this._listeners.push(e):this._listeners=[e]}unsubscribe(e){if(!this._listeners)return;const t=this._listeners.indexOf(e);-1!==t&&this._listeners.splice(t,1)}toAbortSignal(){const e=new AbortController,t=t=>{e.abort(t)};return this.subscribe(t),e.signal.unsubscribe=()=>this.unsubscribe(t),e.signal}static source(){let e;return{token:new ct((function(t){e=t})),cancel:e}}}const lt=ct;const ut={Continue:100,SwitchingProtocols:101,Processing:102,EarlyHints:103,Ok:200,Created:201,Accepted:202,NonAuthoritativeInformation:203,NoContent:204,ResetContent:205,PartialContent:206,MultiStatus:207,AlreadyReported:208,ImUsed:226,MultipleChoices:300,MovedPermanently:301,Found:302,SeeOther:303,NotModified:304,UseProxy:305,Unused:306,TemporaryRedirect:307,PermanentRedirect:308,BadRequest:400,Unauthorized:401,PaymentRequired:402,Forbidden:403,NotFound:404,MethodNotAllowed:405,NotAcceptable:406,ProxyAuthenticationRequired:407,RequestTimeout:408,Conflict:409,Gone:410,LengthRequired:411,PreconditionFailed:412,PayloadTooLarge:413,UriTooLong:414,UnsupportedMediaType:415,RangeNotSatisfiable:416,ExpectationFailed:417,ImATeapot:418,MisdirectedRequest:421,UnprocessableEntity:422,Locked:423,FailedDependency:424,TooEarly:425,UpgradeRequired:426,PreconditionRequired:428,TooManyRequests:429,RequestHeaderFieldsTooLarge:431,UnavailableForLegalReasons:451,InternalServerError:500,NotImplemented:501,BadGateway:502,ServiceUnavailable:503,GatewayTimeout:504,HttpVersionNotSupported:505,VariantAlsoNegotiates:506,InsufficientStorage:507,LoopDetected:508,NotExtended:510,NetworkAuthenticationRequired:511};Object.entries(ut).forEach((([e,t])=>{ut[t]=e}));const ft=ut;const dt=function t(n){const r=new at(n),o=e(at.prototype.request,r);return H.extend(o,at.prototype,r,{allOwnKeys:!0}),H.extend(o,r,null,{allOwnKeys:!0}),o.create=function(e){return t(_e(n,e))},o}(he);dt.Axios=at,dt.CanceledError=Te,dt.CancelToken=lt,dt.isCancel=Se,dt.VERSION="1.10.0",dt.toFormData=Q,dt.AxiosError=J,dt.Cancel=dt.CanceledError,dt.all=function(e){return Promise.all(e)},dt.spread=function(e){return function(t){return e.apply(null,t)}},dt.isAxiosError=function(e){return H.isObject(e)&&!0===e.isAxiosError},dt.mergeConfig=_e,dt.AxiosHeaders=Oe,dt.formToJSON=e=>de(H.isHTMLForm(e)?new FormData(e):e),dt.getAdapter=Ye,dt.HttpStatusCode=ft,dt.default=dt;const pt=dt,{Axios:ht,AxiosError:mt,CanceledError:yt,isCancel:bt,CancelToken:gt,VERSION:wt,all:Et,Cancel:Ot,isAxiosError:Rt,spread:St,toFormData:Tt,AxiosHeaders:At,HttpStatusCode:vt,formToJSON:xt,getAdapter:Ct,mergeConfig:jt}=pt;export{ht as Axios,mt as AxiosError,At as AxiosHeaders,Ot as Cancel,gt as CancelToken,yt as CanceledError,vt as HttpStatusCode,wt as VERSION,Et as all,pt as default,xt as formToJSON,Ct as getAdapter,Rt as isAxiosError,bt as isCancel,jt as mergeConfig,St as spread,Tt as toFormData};
-//# sourceMappingURL=axios.min.js.map
diff --git a/ui/vhtml/utils.js b/ui/vhtml/utils.js
deleted file mode 100644
index 6847e2d..0000000
--- a/ui/vhtml/utils.js
+++ /dev/null
@@ -1,293 +0,0 @@
-
-
-function CamelToKebabCase(str) {
- // 首先将字符串的第一个字符转换为小写,避免在首字符前加上'-'
- if (str.length === 0) return '';
- let firstChar = str.charAt(0).toLowerCase();
-
- // 对剩余部分应用原逻辑:找到每个大写字母,并替换为连字符加上该字母的小写形式
- let rest = str.slice(1).replace(/([A-Z])/g, function(match, p1) {
- return '-' + p1.toLowerCase();
- });
-
- return firstChar + rest;
-}
-
-const outerClickList = []
-const globalClick = document.addEventListener('click', (event) => {
- outerClickList.forEach((item) => {
- if (item?.dom instanceof Element && typeof item?.callback === 'function') {
- if (!item.dom.contains(event.target)) {
- item.callback(event)
- }
- }
- })
-})
-const AddClicker = (dom, typ, callback) => {
- if (typ === 'outer') {
- let idx = outerClickList.length
- outerClickList.push({ dom, callback })
- return () => {
- outerClickList[idx] = null
- }
- }
-}
-
-const EventsList = [
- // 窗口和框架事件
- 'load',
- 'unload',
- 'beforeunload',
- 'resize',
- 'scroll',
-
- // 表单事件
- 'submit',
- 'reset',
- 'input',
- 'change',
- 'focus',
- 'blur',
-
- // 键盘事件
- 'keydown',
- 'keypress',
- 'keyup',
-
- // 鼠标事件
- 'click',
- 'dblclick',
- 'contextmenu',
- 'mousedown',
- 'mouseup',
- 'mousemove',
- 'mouseover',
- 'mouseout',
- 'mouseenter',
- 'mouseleave',
-
- // 触摸事件
- 'touchstart',
- 'touchmove',
- 'touchend',
- 'touchcancel',
-
- // 拖拽事件
- 'drag',
- 'dragstart',
- 'dragend',
- 'dragover',
- 'dragenter',
- 'dragleave',
- 'drop',
-
- // 剪贴板事件
- 'copy',
- 'cut',
- 'paste',
-
- // 动画事件
- 'animationstart',
- 'animationend',
- 'animationiteration',
-
- // 过渡事件
- 'transitionend',
-
- // 文件操作事件
- 'abort',
- 'error',
- 'loadstart',
- 'progress',
-
- // 音视频事件
- 'play',
- 'pause',
- 'ended',
- 'volumechange',
- 'timeupdate',
- 'loadeddata',
- 'waiting',
- 'playing',
-
- // 网络状态事件
- 'online',
- 'offline',
-
- // 存储事件
- 'storage',
-
- // 页面可见性事件
- 'visibilitychange'
-];
-
-function BindInputDomValue(dom, data, key, watch) {
- const element = typeof dom === 'string' ? document.querySelector(dom) : dom;
-
- if (!element) {
- console.error('DOM元素未找到');
- return;
- }
- // 根据元素类型进行双向绑定
- const elementType = element.type || element.tagName.toLowerCase();
- switch (elementType) {
- // 文本输入类
- case 'text':
- case 'password':
- case 'email':
- case 'tel':
- case 'url':
- case 'search':
- case 'number':
- case 'range':
- case 'color':
- case 'date':
- case 'time':
- case 'datetime-local':
- case 'month':
- case 'week':
- case 'hidden':
- case 'textarea':
- watch(() => data[key], (value) => {
- if (value === undefined) {
- element.value = ''
- } else {
- element.value = value
- }
- })
- element.addEventListener('input', function() {
- data[key] = this.value;
- });
- break;
- case 'checkbox':
- watch(function() {
- element.checked = !!data[key];
- });
- element.addEventListener('change', function() {
- data[key] = this.checked;
- console.log(data, data[key])
- });
- break;
- // 单选框
- case 'radio':
- // 初始化
- watch(() => {
- element.checked = element.value === data[key];
- })
- element.addEventListener('change', function() {
- if (this.checked) {
- data[key] = this.value;
- }
- });
- break;
-
- // 下拉选择框
- case 'select-one':
- case 'select-multiple':
- watch(() => {
- let newValue = data[key]
- if (element.multiple) {
- const values = Array.isArray(newValue) ? newValue : [];
- for (let i = 0; i < element.options.length; i++) {
- element.options[i].selected = values.includes(element.options[i].value);
- }
- } else {
- element.value = newValue || '';
- }
- });
- // 监听变化
- element.addEventListener('change', function() {
- if (this.multiple) {
- // 多选
- const selectedValues = [];
- for (let i = 0; i < this.options.length; i++) {
- if (this.options[i].selected) {
- selectedValues.push(this.options[i].value);
- }
- }
- data[key] = selectedValues;
- } else {
- // 单选
- data[key] = this.value;
- }
- });
- break;
-
- default:
- console.warn(`${elementType} not support v!bind only for input element`, element);
- return false
- }
- return true
-}
-
-function SetAttr(dom, key, value) {
- // 属性名映射表
- const propertyMap = {
- 'htmlfor': 'htmlFor',
- 'readonly': 'readOnly',
- 'maxlength': 'maxLength',
- 'minlength': 'minLength',
- 'cellspacing': 'cellSpacing',
- 'cellpadding': 'cellPadding',
- 'rowspan': 'rowSpan',
- 'colspan': 'colSpan',
- 'tabindex': 'tabIndex',
- 'usemap': 'useMap',
- 'frameborder': 'frameBorder',
- 'contenteditable': 'contentEditable',
- 'spellcheck': 'spellcheck',
- 'innerhtml': 'innerHTML',
- 'innertext': 'innerText',
- 'autocapitalize': 'autocapitalize',
- };
-
- // 需要使用 DOM 属性设置的属性
- const domProperties = new Set([
- 'innerHTML', 'innerText', 'outerHTML', 'textContent',
- 'value', 'checked', 'selected', 'disabled', 'readOnly',
- 'maxLength', 'minLength', 'htmlFor',
- 'tabIndex', 'scrollTop', 'scrollLeft', 'scrollWidth', 'scrollHeight',
- 'clientWidth', 'clientHeight', 'offsetWidth', 'offsetHeight',
- 'style', 'dataset'
- ]);
-
- // 布尔属性
- const booleanAttributes = new Set([
- 'checked', 'selected', 'disabled', 'readonly', 'required',
- 'hidden', 'autofocus', 'multiple', 'novalidate'
- ]);
-
- // 转换属性名
- const lowerKey = key.toLowerCase();
- const mappedKey = propertyMap[lowerKey] || key;
-
-
-
- // 设置属性的策略:
- if (domProperties.has(mappedKey)) {
- // DOM 属性
- if (value === undefined) {
- dom[mappedKey] = ''
- } else {
- dom[mappedKey] = value;
- }
- } else if (booleanAttributes.has(lowerKey)) {
- // 布尔属性
- if (value) {
- dom.setAttribute(lowerKey, '');
- } else {
- dom.removeAttribute(lowerKey);
- }
- } else {
- // 其他属性使用 setAttribute
- if (value === undefined) {
- dom.removeAttribute(key);
- } else {
- dom.setAttribute(key, value);
- }
- }
-}
-
-
-export default { CamelToKebabCase, EventsList, BindInputDomValue, SetAttr, AddClicker }
-
diff --git a/ui/vhtml/v.js b/ui/vhtml/v.js
deleted file mode 100644
index caf4ebe..0000000
--- a/ui/vhtml/v.js
+++ /dev/null
@@ -1,1128 +0,0 @@
-/*
- * v.js
- * Copyright (C) 2024 veypi
- *
- * Distributed under terms of the GPL license.
- */
-import vproxy from './vproxy.js'
-import vget from './vget.js'
-import vrouter from './vrouter.js'
-import utils from './utils.js'
-import setupVdev from './vdev.js'
-
-(async function() {
-
- const globalStyle = document.createElement('style')
- globalStyle.innerHTML = `
- [vref] {
- display: block;
- }
- [vparsing] {
- display: none;
- -webkit-text-fill-color: transparent;
- }
- vslot, vrouter {
- display: block;
- }
-`
- if (document.head.firstChild) {
- document.head.insertBefore(globalStyle, document.head.firstChild)
- } else {
- document.head.appendChild(globalStyle)
- }
- const DelayCache = []
- const config = { attributes: false, childList: true, subtree: true, characterData: false }
- const runVdelay = (d) => {
- if (!d.isConnected) {
- return
- }
- let delay = d.getAttribute('vdelay')
- if (delay) {
- let fc = DelayCache[delay]
- if (fc) {
- fc(d)
- } else {
- console.error('delay not found:', delay, d)
- }
- }
- }
- const callback = function(mutationsList, observer) {
- mutationsList.forEach(function(mutation) {
- for (let node of mutation.addedNodes) {
- if (node.nodeType === 1) { // 元素节点
- runVdelay(node)
- node.querySelectorAll('*[vdelay]').forEach(runVdelay)
- }
- }
- })
- }
- const observer = new MutationObserver(callback);
- observer.observe(document.body, config);
-
- function recordGet(code, data) {
- code = `with (sandbox) { ${code} }`
- const fn = new Function('sandbox', code);
- let res = vproxy.Wrap({})
- let keys = []
- const proxy = new Proxy(data, {
- // 拦截所有属性,防止到 Proxy 对象以外的作用域链查找。
- has(target, key) {
- return true;
- },
- get(target, key, receiver) {
- if (key === Symbol.unscopables) {
- return undefined;
- }
- let v = Reflect.get(target, key, receiver);
- if (keys.indexOf(key) === -1) {
- keys.push(key)
- }
- return v
- },
- set(target, key, newValue, receiver) {
- return false
- }
- });
- fn(proxy)
- vproxy.Watch(() => {
- keys.forEach(k => {
- if (res[k] !== data[k]) {
- res[k] = data[k]
- }
- })
- })
- return res
- }
-
- function findLastAccess(code, data) {
- code = `with (sandbox) { ${code} }`
- const fn = new Function('sandbox', code);
- let res = {
- data: null,
- key: null,
- }
- const wrap = (tmp) => {
- return new Proxy(tmp, {
- // 拦截所有属性,防止到 Proxy 对象以外的作用域链查找。
- has(target, key) {
- return true;
- },
- get(target, key, receiver) {
- if (key === Symbol.unscopables) {
- return undefined;
- }
- let v = Reflect.get(target, key, receiver);
- res.data = target
- res.key = key
- if (typeof v === 'function') {
- console.warn('vhtml not support function with "v:" variables bind')
- }
- if (typeof v === 'object' && v) {
- return wrap(v)
- }
- return v
- },
- set(target, key, newValue, receiver) {
- // console.log('set', target, key, newValue)
- // return Reflect.set(target, key, newValue, receiver);
- return false
- }
- })
- }
- fn(wrap(data))
- return res
- }
-
- const varRegex = /{{|}}/g;
- const vforRegex = /^(\s*(\w+)\s+in\s+|\((\w+),\s*(\w+)\)\s+in\s+)([\w\?\$\.\[\]\(\)'"]+)$/
- class vhtml {
- /** @type {HTMLElement} */
- app = null
- scoped = null
- vget = vget
- vproxy = vproxy
- __noproxy = true
- $router = vrouter.$router
- constructor(id) {
- if (typeof id === 'string') {
- this.app = document.getElementById(id)
- } else if (id instanceof HTMLElement) {
- this.app = id
- } else {
- this.app = document.body
- }
- if (!this.app) {
- console.error(`Can't find element by id: ${id}`)
- return
- }
- let init = async () => {
- // vget.SetBaseFile(await vget.FetchFile(window.location.pathname))
- let mainParser = await vget.FetchUI(window.location.pathname, {}, true)
- this.scoped = mainParser.env?.scoped || ''
- if (mainParser.env?.vdev && window.self !== window.top) {
- setupVdev()
- }
- this.parseRef('root', this.app, {}, mainParser.env || {}, mainParser, true)
- }
- init()
- }
- /**
- * @param{HTMLElement} dom
- * @param {Object} scopedData
- * */
- async parseDom(dom, scopedData = {}, env) {
- if (env instanceof HTMLElement) {
- console.log(env)
- throw new Error('env error')
- }
- let nodeName = dom.nodeName.toLowerCase()
- if (dom.nodeType === 3) {
- this.parseTextNode(dom, scopedData, env)
- return
- } else if (dom.nodeType === 8) {
- // comment node
- return
- } else if (dom.nodeType !== 1) {
- console.log('Other Node Type:', dom.nodeType, dom);
- return
- }
- if (dom.hasAttribute('no-vhtml') || dom.vparsed) {
- return
- }
-
- let vfortxt = dom.getAttribute('v-for')
- if (vfortxt !== null) {
- this.parseVfor(vfortxt, dom, scopedData, env)
- return
- }
- if (nodeName.indexOf('-') !== -1) {
- let url = '/' + nodeName.split('-').join('/')
- let singleMode = dom.hasAttribute('single')
- this.parseRef(url, dom, scopedData, env, null, singleMode)
- dom.vparsed = true
- return
- }
- if (dom.getAttribute(':vsrc')) {
- let code = dom.getAttribute(':vsrc')
- dom.removeAttribute(':vsrc')
- let attrs = Array.from(dom.attributes).map(a => {
- let res = { name: a.name, value: a.value }
- return res
- })
- let oldChilds = Array.from(dom.childNodes)
- vproxy.Watch(() => {
- delete dom.vparsed
- dom.setAttribute('vparsing', '')
- let vsrc = vproxy.Run(code, scopedData, env)
- if (!vsrc) {
- return
- }
- Array.from(dom.attributes).forEach(a => { dom.removeAttribute(a.name) })
- dom.innerHTML = ''
- attrs.forEach(a => { dom.setAttribute(a.name, a.value) })
- oldChilds.forEach(c => { dom.appendChild(c.cloneNode(true)) })
- this.parseRef(vsrc, dom, scopedData, env, null, false)
- dom.vparsed = true
- })
- return
- }
- if (dom.getAttribute('vsrc')) {
- let singleMode = dom.hasAttribute('single')
- this.parseRef(dom.getAttribute('vsrc'), dom, scopedData, env, null, singleMode)
- dom.vparsed = true
- return
- }
- if (nodeName === 'div' && dom.getAttribute('v-html')) {
- let vhtmlCode = dom.getAttribute('v-html')
- dom.removeAttribute('v-html')
- dom.innerHTML = ''
- this.parseAttrs(dom, scopedData, env)
- dom.vparsed = true
- vproxy.Watch(() => {
- let innerHTML = vproxy.Run(vhtmlCode, scopedData, env)
- dom.innerHTML = innerHTML
- let childs = this.parseVif(Array.from(dom.childNodes), scopedData, env)
- for (let n of childs) {
- this.parseDom(n, scopedData, env)
- }
- })
- return
- }
- if (nodeName === 'vslot') {
- this.parseSlots(dom, scopedData, env)
- dom.vparsed = true
- return
- }
- if (nodeName === 'vrouter') {
- this.parseAttrs(dom, scopedData, env)
- vrouter.$router.ParseVrouter(this, dom, env)
- return
- }
- this.parseAttrs(dom, scopedData, env)
- let childs = this.parseVif(Array.from(dom.childNodes), scopedData, env)
- for (let n of childs) {
- this.parseDom(n, scopedData, env)
- }
- dom.vparsed = true
- }
-
- onMountedRun(dom, cb, once = true) {
- if (once) {
- if (dom.isConnected) {
- cb(dom)
- return
- }
- let did = DelayCache.push((dom) => {
- dom.removeAttribute('vdelay')
- cb(dom)
- })
- dom.setAttribute('vdelay', did - 1)
- return
- }
- if (dom.isConnected) {
- cb(dom)
- }
- let did = DelayCache.push(cb)
- dom.setAttribute('vdelay', did - 1)
- }
-
- /**
- * @param{string} name
- * @param{HTMLElement} dom
- * */
- AllENVs = {}
- async parseRef(vsrc, dom, data, env, target, singleMode = false) {
- dom.setAttribute('vparsing', '')
- let oldEnv = env
- let vrefof = dom.getAttribute('vrefof')
- let parentRef = dom.closest(`*[vref='${vrefof}']`)
- if (parentRef) {
- env = parentRef.$env
- } else {
- // console.log('parentRef not found:', vrefof,vsrc)
- }
- if (!target && vsrc) {
- if (!vsrc.endsWith('.html')) {
- vsrc = vsrc + '.html'
- }
- target = await vget.FetchUI(vsrc, env, dom.hasAttribute('scoped'))
- }
- env = Object.assign({}, env, target?.env || {})
- // env = target.env
- dom.$env = env
- dom.$vsrc = vsrc
- env.$router = vrouter.$router
- env.$emit = (evt, ...args) => {
- evt = evt.toLowerCase()
- if (!dom.$vevent) {
- return
- }
- let fc = dom.$vevent[evt]
- if (fc && typeof fc === 'function') {
- fc(...args)
- }
- }
- let originData = await this.setupRef(dom, data, oldEnv, target, singleMode)
-
- if (singleMode) {
- this.parseAttrs(dom, originData, env, target?.customAttrs)
- } else {
- this.parseAttrs(dom, data, oldEnv, target?.customAttrs)
- }
- let childs = this.parseVif(Array.from(dom.childNodes), originData, env)
- for (let n of childs) {
- this.parseDom(n, originData, env)
- }
- dom.removeAttribute('vparsing')
- this.mountRef(dom, originData, env, target)
- }
-
- /**
- * @param {HTMLElement} dom
- * @parm {Object} data
- * @param {{heads:HTMLElement[],body:HTMLElement,scripts:HTMLElement[],setup:HTMLElement}} target
- * */
- async setupRef(dom, data, env, target, singleMode = false) {
- let originData = vproxy.Wrap({})
- if (target.setup) {
- let s = target.setup.innerHTML
- s = await vproxy.ParseImport(s, originData, dom.$env, dom.$vsrc)
- await vproxy.AsyncRun(s, originData, dom.$env, {
- $node: dom
- })
- }
- dom.$refScope = data
- dom.$refData = originData
- if (singleMode) {
- return originData
- }
- if (!dom.$refSlots) {
- // dom 预定义好内容
- let slots = vproxy.Wrap({})
- dom.childNodes.forEach((n) => {
- let nName = n.getAttribute ? n.getAttribute('vslot') : ''
- nName = nName || ''
- if (!slots[nName]) {
- slots[nName] = []
- }
- slots[nName].push(n)
- })
- dom.$refSlots = slots
- }
- dom.innerHTML = ''
- let now = target.body.cloneNode(true)
- dom.append(...now.childNodes)
-
- // 处理调用方attr 传参
- Object.keys(originData).forEach(k => {
- const localK = utils.CamelToKebabCase(k)
- if (typeof originData[k] === 'boolean') {
- if (dom.hasAttribute(k) || dom.hasAttribute(localK)) {
- originData[k] = true
- }
- } else if (dom.hasAttribute(k)) {
- originData[k] = dom.getAttribute(k)
- dom.removeAttribute(k)
- } else if (dom.hasAttribute(localK)) {
- originData[k] = dom.getAttribute(localK)
- dom.removeAttribute(localK)
- }
- if (dom.hasAttribute(':' + k) || dom.hasAttribute(':' + localK)) {
- let val = dom.getAttribute(':' + k) || dom.getAttribute(':' + localK)
- dom.removeAttribute(':' + k)
- dom.removeAttribute(':' + localK)
- // 此时数据还没有监听,删除旧对象,使得外部的对象直接绑定在内部$data上
- delete originData[k]
- if (val) {
- vproxy.Watch(() => vproxy.Run(val, data, env), () => {
- originData[k] = vproxy.Run(val, data, env)
- }, { deep: true })
- } else {
- vproxy.Watch(() => data[k], () => {
- originData[k] = data[k]
- }, { deep: true })
- }
- }
- if (dom.hasAttribute('v:' + k) || dom.hasAttribute('v:' + localK)) {
- let val = dom.getAttribute('v:' + k) || dom.getAttribute('v:' + localK)
- dom.removeAttribute('v:' + k)
- dom.removeAttribute('v:' + localK)
- if (!val) {
- val = k
- }
- delete originData[k]
- vproxy.Watch(() => findLastAccess(val, data), (args) => {
- if (args && args.data && args.key) {
- originData[k] = args.data[args.key]
- } else {
- console.warn('not found variables in:' + val)
- }
- })
- vproxy.Watch(() => originData[k], () => {
- let args = findLastAccess(val, data)
- if (args && args.data && args.key) {
- args.data[args.key] = originData[k]
- } else {
- console.warn('not found variables in:' + val)
- }
- })
- }
- })
-
- // handle local component attr
- let attrs = Array.from(now.attributes)
- attrs = attrs.filter(a => {
- if (this.parseAttr(dom, a.name, a.value, originData, env)) {
- now.removeAttribute(a.name)
- return false
- }
- return true
- })
- attrs.forEach((a) => {
- if (a.name === 'class') {
- dom.classList.add(...a.value.trim().split(/\s+/))
- } else if (a.name === 'style') {
- let styles = a.value.split(';')
- for (let s of styles) {
- let ss = s.split(':')
- if (ss.length === 2 && !dom.style[ss[0]]) {
- let skey = ss[0].trim()
- if (skey.startsWith('--')) {
- dom.style.setProperty(skey, ss[1].trim())
- } else {
- dom.style[skey] = ss[1].trim()
- }
- }
- }
- } else if (!dom.getAttribute(a.name)) {
- dom.setAttribute(a.name, a.value)
- }
- })
- return originData
- }
-
- async mountRef(dom, scopedData, env, target) {
- for (let s of target.scripts) {
- if (s.hasAttribute('active')) {
- this.onMountedRun(dom, () => {
- vproxy.AsyncRun(s.innerHTML, scopedData, env, { $node: dom, $watch: vproxy.Watch })
- }, false)
- } else {
- vproxy.AsyncRun(s.innerHTML, scopedData, env, { $node: dom, $watch: vproxy.Watch })
- }
- }
- }
-
- parseAttrs(dom, data, env, attrs) {
- if (dom.nodeName === 'A') {
- this.parseAHref(dom, data, env)
- }
- Array.from(dom.attributes).forEach(a => {
- if (this.parseAttr(dom, a.name, a.value, data, env)) {
- dom.removeAttribute(a.name)
- }
- })
- if (attrs) {
- // just for body element
- Object.keys(attrs).forEach(k => {
- this.parseAttr(dom, k, attrs[k], dom.$refData, env)
- })
- }
- if (dom.hasAttribute('v-show')) {
- let code = dom.getAttribute('v-show')
- let oldDisplay = dom.style.display
- vproxy.Watch(() => {
- let res = vproxy.Run(code, data, env)
- if (res) {
- dom.style.display = oldDisplay
- } else {
- dom.style.display = 'none'
- }
- })
- }
- }
-
- parseAHref(dom, data, env) {
- if (!dom.hasAttribute("href") && !dom.hasAttribute(":href")) {
- return
- }
- if (dom.hasAttribute(":href")) {
- let code = dom.getAttribute(":href")
- dom.removeAttribute(":href")
- vproxy.Watch(() => {
- let href = vproxy.Run(code, data, env)
- console.log(code, href, env)
- if (!href || href.startsWith('#') || href.startsWith('http')) {
- return
- } else if (href.startsWith('@')) {
- dom.setAttribute('href', href.slice(1))
- } else if (href) {
- let scoped = env?.scoped
- if (scoped) {
- href = scoped + href
- }
- dom.setAttribute('href', href)
- }
- })
- }
- const fc = (to) => {
- let url = to?.fullPath
- if (dom.getAttribute('href') === url) {
- dom.setAttribute('active', '')
- } else {
- dom.removeAttribute('active')
- }
- }
- fc(vrouter.$router.current)
- vrouter.$router.onChange(fc)
- }
-
- /**
- * @param {HTMLElement} dom
- * */
- parseAttr(dom, name, value, data, env) {
- if (name.startsWith(':')) {
- let attrName = name.slice(1)
- if (attrName === 'class' || attrName === 'style') {
- this.handleStyle(dom, attrName, value, data, env)
- } else {
- vproxy.Watch(() => {
- let res
- if (value) {
- res = vproxy.Run(value, data, env)
- } else {
- res = data[attrName]
- }
- utils.SetAttr(dom, attrName, res)
- })
- }
- return true
- } else if (name.startsWith('@')) {
- this.handleEvent(dom, name, value, data, env)
- return true
- } else if (name.indexOf('!') > -1) {
- console.warn('! prefix is deprecated, use : instead:', name, value, dom)
- } else if (name.startsWith('v:')) {
- let args = findLastAccess(value, data)
- if (args && args.data && args.key) {
- let vkey = args.key
- let vdata = args.data
- return utils.BindInputDomValue(dom, vdata, vkey, vproxy.Watch)
- } else {
- console.warn('not found variables in:' + value)
- }
- } else if (name === 'vdom') {
- let vbind = findLastAccess(value, data)
- if (vbind && vbind.data && vbind.key) {
- vbind.data[vbind.key] = dom
- } else {
- console.warn('not found variables in:' + value)
- }
- return true
- }
- return false
- }
- handleStyle(dom, attrName, value, data, env) {
- let oldValue = ''
- vproxy.Watch(() => {
- let res = vproxy.Run(value, data, env)
- if (typeof res === 'function') {
- res = res()
- }
- if (attrName === 'class') {
- if (oldValue) {
- dom.classList.remove(...oldValue.split(/\s+/))
- oldValue = ''
- }
- if (res instanceof Array) {
- oldValue = ''
- res.forEach(r => {
- if (typeof r === 'string' && r.length) {
- oldValue += ' ' + r
- } else if (typeof r === 'object') {
- for (let k in r) {
- if (r[k]) {
- oldValue += ' ' + k
- }
- }
- }
- })
- } else if (typeof res === 'string' && res.length) {
- oldValue = res.trim()
- } else if (typeof res === 'object') {
- oldValue = ''
- for (let k in res) {
- if (res[k]) {
- oldValue += ' ' + k
- }
- }
- } else if (res) {
- console.warn('class value error:', res)
- }
- oldValue = oldValue.trim()
- if (oldValue) {
- dom.classList.add(...oldValue.split(/\s+/))
- }
- } else if (attrName === 'style') {
- if (oldValue) {
- if (typeof oldValue === 'object') {
- for (let k in oldValue) {
- if (k.startsWith('--')) {
- dom.style.removeProperty(k)
- } else {
- dom.style[k] = ''
- }
- }
- } else if (typeof oldValue === 'string') {
- let styles = oldValue.split(';')
- for (let s of styles) {
- let ss = s.split(':')
- if (ss.length === 2) {
- if (ss[0].trim().startsWith('--')) {
- dom.style.removeProperty(ss[0].trim())
- } else {
- dom.style[ss[0].trim()] = ''
- }
- }
- }
- }
- }
- if (typeof res === 'object') {
- for (let k in res) {
- if (k.startsWith('--')) {
- dom.style.setProperty(k, res[k])
- } else {
- dom.style[k] = res[k]
- }
- }
- } else if (typeof res === 'string') {
- let styles = res.split(';')
- for (let s of styles) {
- let ss = s.split(':')
- if (ss.length === 2) {
- if (ss[0].trim().startsWith('--')) {
- dom.style.setProperty(ss[0].trim(), ss[1].trim())
- } else {
- dom.style[ss[0].trim()] = ss[1].trim()
- }
- }
- }
- }
- oldValue = res
- }
- })
- }
- handleEvent(dom, name, value, data, env) {
- let actionName = name.slice(1).split('.')
- let evtMap = { 'self': false, 'prevent': false, 'stop': false }
- let evt = actionName[0]
- if (evt === 'mounted') {
- this.onMountedRun(dom, (d) => {
- let cb = vproxy.Run(value, data, env)
- if (typeof cb === 'function') {
- (function() {
- })();
- cb(d)
- }
- }, false)
- } else if (evt === 'outerclick') {
- let func = (e) => {
- let cb = vproxy.Run(value, data, env, { $event: e })
- if (typeof cb === 'function') {
- cb(e)
- }
- }
- utils.AddClicker(dom, 'outer', func)
- } else if (utils.EventsList.indexOf(evt) !== -1) {
- if (evt === 'keydown' || evt === 'keyup' || evt === 'keypress') {
- if (dom.tagName !== 'INPUT' && dom.tagName !== 'TEXTAREA') {
- dom.setAttribute('tabindex', '0')
- }
- }
- let func = (e) => {
- let cb = vproxy.Run(value, data, env, { $event: e })
- if (typeof cb === 'function') {
- cb(e)
- }
- }
- actionName.slice(1).forEach(k => {
- if (k.startsWith('delay')) {
- let delay = k.slice(5)
- if (!delay) {
- delay = 1000
- } else if (delay.endsWith('ms')) {
- delay = Number(delay.slice(0, -2))
- } else if (delay.endsWith('s')) {
- delay = Number(delay.slice(0, -1)) * 1000
- } else {
- delay = Number(delay)
- }
- if (isNaN(delay)) {
- delay = 1000
- }
- func = (e) => {
- let fc = dom['_' + evt]
- if (fc && typeof fc === 'number') {
- clearTimeout(fc)
- }
- dom['_' + evt] = setTimeout(() => {
- let cb = vproxy.Run(value, data, env, { $event: e })
- if (typeof cb === 'function') {
- cb(e)
- }
- }, delay)
- }
- }
- evtMap[k] = true
- })
- dom.addEventListener(evt, (e) => {
- if (actionName.length > 1 && (evt === 'keydown' || evt == 'keyup' || evt == 'keypress')) {
- let btn = actionName[1]
- if (btn !== e.key?.toLowerCase()) {
- return
- }
- }
- if (evtMap['self'] && e.currentTarget !== e.target) {
- return
- }
- if (evtMap['prevent']) {
- e.preventDefault()
- }
- if (evtMap['stop']) {
- e.stopPropagation()
- }
- func(e)
- })
- } else {
- dom.$vevent = dom.$vevent || {}
- dom.$vevent[evt] = (...arg) => {
- let cb = vproxy.Run(value, data, env, {})
- if (typeof cb === 'function') {
- cb(...arg)
- }
- }
- }
- }
-
- parseTextNode(dom, data, env) {
- // text node
- let txt = dom.nodeValue.trim()
- if (!txt) {
- return
- }
- let match
- let nstart = 0
- let start = -1;
- let txtItems = []
- while ((match = varRegex.exec(txt)) !== null) {
- if (match[0] === '{{') {
- start = match.index
- } else if (match[0] === '}}' && start >= 0) {
- if (nstart !== start) {
- txtItems.push(txt.slice(nstart, start))
- }
- txtItems.push('')
- let valStr = txt.slice(start + 2, match.index)
- let valIdx = txtItems.length
- start = -1
- nstart = match.index + 2
- vproxy.Watch(() => {
- txtItems[valIdx - 1] = vproxy.Run(valStr, data, env)
- if (typeof txtItems[valIdx - 1] === 'object') {
- txtItems[valIdx - 1] = JSON.stringify(txtItems[valIdx - 1])
- }
- dom.nodeValue = txtItems.join('')
- })
- }
- }
- txtItems.push(txt.slice(nstart))
- dom.nodeValue = txtItems.join('')
- }
-
- vforDomCache = {}
- parseVfor(vfortxt, dom, data, env) {
- dom.removeAttribute('v-for')
- let matches = vforRegex.exec(vfortxt)
- if (matches?.length === 6) {
- let vforTag = document.createElement('div')
- vforTag.style.display = 'none'
- let vforTagID = vproxy.GenUniqueID()
- this.vforDomCache[vforTagID] = {}
- dom.parentNode.replaceChild(vforTag, dom)
- vproxy.Watch(() => {
- let value = matches[3] || matches[2]
- let key = matches[4]
- let iters = vproxy.Run(matches[5], data, env)
- let cache = this.vforDomCache[vforTagID]
- let rendereds = new Set()
-
- if (typeof iters === 'function') {
- iters = iters()
- } else if (typeof iters === 'number') {
- iters = Array.from({ length: iters }, (_, i) => i)
- }
- if (iters === undefined || iters === null) {
- iters = []
- }
- // 访问长度,触发监听
- let _ = iters.length
- if (typeof iters === 'object') {
- let keys = Object.keys(iters)
- let items = []
- for (let kid in keys) {
- let k = keys[kid]
- let vfk = ''
- if (iters[k] && iters[k][vproxy.DataID]) {
- vfk = iters[k][vproxy.DataID]
- } else {
- vfk = k + '.' + iters[k]
- }
- vfk = vforTagID + "." + vfk
- rendereds.add(vfk)
- items.push({ k, vfk, val: iters[k] })
- }
-
- // Cleanup removed nodes
- for (let k of Object.keys(cache)) {
- if (!rendereds.has(k)) {
- if (cache[k] instanceof Array) {
- cache[k].forEach(d => d.remove())
- } else {
- cache[k].remove()
- }
- delete cache[k]
- }
- }
-
- // Update and Reorder (Backwards)
- let refNode = vforTag
- for (let i = items.length - 1; i >= 0; i--) {
- let { k, vfk, val } = items[i]
- let curDom = cache[vfk]
-
- if (curDom) {
- if (key) {
- curDom.$vforData[key] = k === '0' ? 0 : (Number(k) || k)
- }
- if (curDom.isConnected) {
- if (curDom.nextSibling !== refNode) {
- vforTag.parentNode.insertBefore(curDom, refNode)
- }
- refNode = curDom
- }
- continue
- }
-
- let newDom = dom.cloneNode(true)
- cache[vfk] = newDom
- let tmpData = { [value]: val }
- if (key) {
- tmpData[key] = k === '0' ? 0 : (Number(k) || k)
- }
- tmpData = vproxy.Wrap(tmpData, data)
- newDom.$vforData = tmpData
-
- vforTag.parentNode.insertBefore(newDom, refNode)
-
- let vif = dom.getAttribute('v-if')
- if (!vif) {
- this.parseDom(newDom, tmpData, env)
- refNode = newDom
- continue
- }
-
- newDom.removeAttribute('v-if')
- let watchid = -1
- watchid = vproxy.Watch(() => {
- let dom = cache[vfk]
- if (!dom) {
- vproxy.Cancel(watchid)
- return
- }
- let res = vproxy.Run(vif, tmpData, env)
- if (res) {
- if (!dom.vparsed) {
- this.parseDom(dom, tmpData, env)
- }
- if (!dom.isConnected) {
- let founded = false
- let before = vforTag
- for (let tmpvfk in cache) {
- if (tmpvfk === vfk) {
- founded = true
- continue
- }
- if (founded && cache[tmpvfk].isConnected) {
- before = cache[tmpvfk]
- break
- }
- }
- vforTag.parentNode.insertBefore(dom, before)
- }
- } else {
- if (dom.isConnected) {
- dom.remove()
- } else {
- this.onMountedRun(dom, (d) => {
- dom.remove()
- })
- }
- }
- })
-
- if (newDom.isConnected) {
- refNode = newDom
- }
- }
- } else {
- console.error('vfor iter object error:', [matches, iters, vfortxt, data])
- }
- })
- } else {
- console.error('vfor error:', vfortxt)
- }
- }
-
- parseVif(nodes, data, env) {
- let ifCache = { now: document.createElement('div'), conds: [], doms: [] }
- const handleIf = (cache) => {
- let ifData = { now: cache.now, conds: cache.conds, doms: cache.doms }
- let ifList = []
- for (let cid in ifData.conds) {
- let c = ifData.conds[cid]
- if (c === '') {
- c = 'true'
- } else {
- c = 'Boolean(' + c + ')'
- }
- ifList.push(c)
- }
- let ifFc = `let res = [${ifList.join(',')}]\n return res.indexOf(true)`
- vproxy.Watch(() => {
- let res = vproxy.Run(ifFc, data, env)
- let tmpDom = ifData.doms[res]
- if (!tmpDom) {
- tmpDom = document.createElement('div')
- tmpDom.style.display = 'none'
- }
- return tmpDom
- }, (tmpDom) => {
- if (!tmpDom) {
- return
- }
- this.onMountedRun(ifData.now, (d) => {
- d.replaceWith(tmpDom)
- ifData.now = tmpDom
- })
- if (!tmpDom?.vparsed) {
- this.parseDom(tmpDom, data, env)
- }
- })
- }
- let childs = nodes.filter(d => {
- if (!d.getAttribute || d.getAttribute('v-for')) {
- return true
- }
- if (d.getAttribute('v-if') !== null) {
- if (ifCache.conds.length > 0) {
- handleIf(ifCache)
- ifCache = { now: document.createElement('div'), conds: [], doms: [] }
- }
- d.replaceWith(ifCache.now)
- // dom.replaceChild(ifCache.now, d)
- ifCache.conds.push(d.getAttribute('v-if'))
- d.removeAttribute('v-if')
- ifCache.doms.push(d)
- return false
- } else if (d.getAttribute('v-else-if') !== null) {
- ifCache.conds.push(d.getAttribute('v-else-if'))
- d.removeAttribute('v-else-if')
- ifCache.doms.push(d)
- d.remove()
- return false
- } else if (d.getAttribute('v-else') !== null) {
- ifCache.conds.push('')
- d.removeAttribute('v-else')
- ifCache.doms.push(d)
- d.remove()
- return false
- }
- return true
- })
- if (ifCache.conds.length > 0) {
- handleIf(ifCache)
- }
- return childs
- }
-
-
- /**
- * @param {HTMLElement} dom
- * */
- parseSlots(dom, data, env) {
- // 先插入dom,后解析,避免slot搭配v-for,v-if使用出现bug
- let slotof = dom.getAttribute('vrefof')
- let refDom = dom.closest(`*[vref='${slotof}']`)
- if (!refDom) {
- // TODO
- this.onMountedRun(dom, (d) => {
- this.parseSlots(d, data, env)
- })
- return
- }
- while (true) {
- let tmp = refDom?.parentNode.closest('*[vref]')
- if (!tmp) {
- break
- }
- if (tmp.getAttribute('vref') === slotof) {
- refDom = tmp
- } else {
- break
- }
- }
- let sName = dom.getAttribute('name') || ''
- if (dom.getAttribute(':name')) {
- let nameVal = dom.getAttribute(':name')
- dom.removeAttribute(':name')
- sName = vproxy.Run(nameVal, data, env)
- }
- //
- if (!dom.originContent) {
- dom.$originContent = Array.from(dom.childNodes)
- dom.innerHTML = ''
- }
- // slot模板
- dom.$slotCache = {}
- vproxy.Watch(() => {
- let slots = refDom.$refSlots || {}
- // slot数据域
- let slotsData = refDom.$refScope || {}
- let sNodes = slots[sName]
- if (sNodes && sNodes.length > 0) {
- let hashID = sNodes[0].hashID
- if (!hashID) {
- hashID = vproxy.GenUniqueID()
- sNodes[0].hashID = hashID
- } else if (dom.$slotCache[hashID]) {
- dom.innerHTML = ''
- dom.append(...dom.$slotCache[hashID])
- return
- }
- dom.innerHTML = ''
- sNodes = sNodes.map(n => n.cloneNode(true))
- dom.append(...sNodes)
- let tmpSlotsData = slotsData
- if (dom.getAttribute('vbind') !== null) {
- let vbindAttrs = dom.getAttribute('vbind').split(',').map(a => a.trim())
- tmpSlotsData = vproxy.Wrap({})
- vbindAttrs.forEach(a => {
- if (data.hasOwnProperty(a)) {
- tmpSlotsData[a] = data[a]
- }
- })
- // tmpSlotsData = recordGet(dom.getAttribute('vbind'), data)
- vproxy.SetDataRoot(tmpSlotsData, slotsData)
- }
- let sNodeVrefof = ''
- sNodes.find(n => {
- if (n.getAttribute && n.getAttribute('vrefof')) {
- sNodeVrefof = n.getAttribute('vrefof')
- return true
- }
- return false
- })
- let sNodesEnv = env
- if (sNodeVrefof) {
- let sNodeVref = dom.closest(`*[vref='${sNodeVrefof}']`)
- sNodesEnv = sNodeVref?.$env || env
- }
- sNodes = this.parseVif(sNodes, tmpSlotsData, sNodesEnv)
- sNodes.forEach((n) => this.parseDom(n, tmpSlotsData, sNodesEnv))
- dom.$slotCache[hashID] = sNodes
- } else {
- dom.innerHTML = ''
- dom.append(...dom.$originContent)
- let parsed = false
- dom.$originContent.forEach((n) => {
- if (n.hasAttribute && n.vparsed) {
- parsed = true
- }
- })
- if (!parsed) {
- dom.$originContent = this.parseVif(dom.$originContent, data, env)
- dom.$originContent.forEach((n) => this.parseDom(n, data, env))
- }
- }
- })
- this.parseAttrs(dom, data, env)
- return dom
- }
- }
- if (window.$vhtml) {
- console.error('vhtml already exists.')
- } else {
- window.$vhtml = new vhtml(document.body)
- console.log('vhtml loaded.')
- }
-})();
diff --git a/ui/vhtml/vbus.js b/ui/vhtml/vbus.js
deleted file mode 100644
index 045c8ef..0000000
--- a/ui/vhtml/vbus.js
+++ /dev/null
@@ -1,138 +0,0 @@
-/*
- * vbus.js
- * Copyright (C) 2025 veypi
- *
- * Distributed under terms of the MIT license.
- */
-
-
-class EventBus {
- constructor() {
- // 存储事件监听器的对象
- this.events = {};
- }
-
- /**
- * 订阅事件
- * @param {string} eventName - 事件名称
- * @param {Function} callback - 回调函数
- * @param {Object} context - 执行上下文(可选)
- * @returns {Function} 取消订阅的函数
- */
- on(eventName, callback, context = null) {
- if (typeof callback !== 'function') {
- throw new Error('回调函数必须是一个函数');
- }
-
- if (!this.events[eventName]) {
- this.events[eventName] = [];
- }
-
- const listener = { callback, context };
- this.events[eventName].push(listener);
-
- // 返回取消订阅的函数
- return () => this.off(eventName, callback, context);
- }
-
- /**
- * 一次性事件监听
- * @param {string} eventName - 事件名称
- * @param {Function} callback - 回调函数
- * @param {Object} context - 执行上下文(可选)
- * @returns {Function} 取消订阅的函数
- */
- once(eventName, callback, context = null) {
- const onceWrapper = (...args) => {
- this.off(eventName, onceWrapper, context);
- callback.apply(context, args);
- };
-
- return this.on(eventName, onceWrapper, context);
- }
-
- /**
- * 取消事件订阅
- * @param {string} eventName - 事件名称
- * @param {Function} callback - 要移除的回调函数(可选)
- * @param {Object} context - 执行上下文(可选)
- */
- off(eventName, callback = null, context = null) {
- if (!this.events[eventName]) {
- return;
- }
-
- // 如果没有指定回调函数,移除该事件的所有监听器
- if (!callback) {
- delete this.events[eventName];
- return;
- }
-
- // 移除特定的监听器
- this.events[eventName] = this.events[eventName].filter(listener => {
- return !(listener.callback === callback && listener.context === context);
- });
-
- // 如果该事件没有监听器了,删除该事件
- if (this.events[eventName].length === 0) {
- delete this.events[eventName];
- }
- }
-
- /**
- * 触发事件
- * @param {string} eventName - 事件名称
- * @param {...any} args - 传递给回调函数的参数
- */
- emit(eventName, ...args) {
- if (!this.events[eventName]) {
- return;
- }
-
- // 复制监听器数组,避免在执行过程中修改原数组导致的问题
- const listeners = [...this.events[eventName]];
-
- listeners.forEach(listener => {
- try {
- listener.callback.apply(listener.context, args);
- } catch (error) {
- console.error(`事件 "${eventName}" 的监听器执行出错:`, error);
- }
- });
- }
-
- /**
- * 获取事件的监听器数量
- * @param {string} eventName - 事件名称
- * @returns {number} 监听器数量
- */
- listenerCount(eventName) {
- return this.events[eventName] ? this.events[eventName].length : 0;
- }
-
- /**
- * 获取所有事件名称
- * @returns {string[]} 事件名称数组
- */
- eventNames() {
- return Object.keys(this.events);
- }
-
- /**
- * 移除所有事件监听器
- */
- removeAllListeners() {
- this.events = {};
- }
-
- /**
- * 检查是否有某个事件的监听器
- * @param {string} eventName - 事件名称
- * @returns {boolean} 是否有监听器
- */
- hasListeners(eventName) {
- return this.listenerCount(eventName) > 0;
- }
-}
-
-export default EventBus;
diff --git a/ui/vhtml/vcss.js b/ui/vhtml/vcss.js
deleted file mode 100644
index 3369554..0000000
--- a/ui/vhtml/vcss.js
+++ /dev/null
@@ -1,566 +0,0 @@
-/*
- * vcss.js
- * Copyright (C) 2025 veypi
- *
- * Distributed under terms of the GPL license.
- * simple css parser
- */
-
-
-
-class CSSParser {
- constructor() {
- this.scopeAttribute = '';
- this.scopeBody = ''
- this.scopedKeyframes = new Map(); // 存储已处理的keyframe映射
- }
-
- /**
- * 解析CSS文本并添加作用域
- * @param {string} cssText - CSS文本
- * @param {string} scope - 作用域标识符
- * @returns {string} - 处理后的CSS文本
- */
- parse(cssText, scope) {
- this.scopeAttribute = `[vrefof="${scope}"]`;
- this.scopeBody = `[vref="${scope}"]`
- this.scopedKeyframes.clear();
- this.scopeSuffix = scope.replace(/[^a-zA-Z0-9]/g, ''); // 清理作用域后缀
-
- // 移除注释
- cssText = this.removeComments(cssText);
-
- // 第一遍:收集所有keyframes名称
- this.collectKeyframes(cssText);
-
- // 第二遍:解析CSS规则并替换动画名称
- return this.parseRules(cssText);
- }
-
- /**
- * 收集所有keyframes名称
- * @param {string} cssText
- */
- collectKeyframes(cssText) {
- const keyframeRegex = /@keyframes\s+([^\s{]+)/gi;
- let match;
-
- while ((match = keyframeRegex.exec(cssText)) !== null) {
- const originalName = match[1];
- const scopedName = originalName + '-' + this.scopeSuffix;
- this.scopedKeyframes.set(originalName, scopedName);
- }
- }
-
- /**
- * 移除CSS注释
- * @param {string} cssText
- * @returns {string}
- */
- removeComments(cssText) {
- return cssText.replace(/\/\*[\s\S]*?\*\//g, '');
- }
-
- /**
- * 解析CSS规则
- * @param {string} cssText
- * @returns {string}
- */
- parseRules(cssText) {
- let result = '';
- let i = 0;
-
- while (i < cssText.length) {
- // 跳过空白字符
- while (i < cssText.length && /\s/.test(cssText[i])) {
- result += cssText[i];
- i++;
- }
-
- if (i >= cssText.length) break;
-
- // 检查是否是@规则
- if (cssText[i] === '@') {
- const atRule = this.parseAtRule(cssText, i);
- result += atRule.content;
- i = atRule.endIndex;
- } else {
- // 普通CSS规则
- const rule = this.parseNormalRule(cssText, i);
- result += rule.content;
- i = rule.endIndex;
- }
- }
-
- return result;
- }
-
- /**
- * 解析@规则(如@media, @keyframes等)
- * @param {string} cssText
- * @param {number} startIndex
- * @returns {object}
- */
- parseAtRule(cssText, startIndex) {
- let i = startIndex;
- let atRuleContent = '';
-
- // 读取@规则名称和参数
- while (i < cssText.length && cssText[i] !== '{') {
- atRuleContent += cssText[i];
- i++;
- }
-
- if (i >= cssText.length) {
- return { content: atRuleContent, endIndex: i };
- }
-
- // 检查是否是@keyframes或@media
- const atRuleName = atRuleContent.toLowerCase().trim();
-
- if (atRuleName.startsWith('@keyframes')) {
- return this.parseKeyframes(cssText, startIndex);
- } else if (atRuleName.startsWith('@media')) {
- return this.parseMedia(cssText, startIndex);
- } else if (atRuleName.startsWith('@supports')) {
- return this.parseSupports(cssText, startIndex);
- } else {
- // 其他@规则,查找匹配的大括号
- const braceContent = this.findMatchingBrace(cssText, i);
- return {
- content: atRuleContent + braceContent.content,
- endIndex: braceContent.endIndex
- };
- }
- }
-
- /**
- * 解析@keyframes规则
- * @param {string} cssText
- * @param {number} startIndex
- * @returns {object}
- */
- parseKeyframes(cssText, startIndex) {
- let i = startIndex;
- let result = '';
-
- // 读取@keyframes声明
- while (i < cssText.length && cssText[i] !== '{') {
- result += cssText[i];
- i++;
- }
-
- if (i >= cssText.length) {
- return { content: result, endIndex: i };
- }
-
- // 处理keyframes内容
- const braceContent = this.findMatchingBrace(cssText, i);
- const keyframesContent = braceContent.content;
-
- // 为keyframes名称添加作用域
- const keyframeName = this.extractKeyframeName(result);
- const scopedKeyframeName = this.scopedKeyframes.get(keyframeName);
- if (scopedKeyframeName) {
- result = result.replace(keyframeName, scopedKeyframeName);
- }
-
- result += keyframesContent;
-
- return {
- content: result,
- endIndex: braceContent.endIndex
- };
- }
-
- /**
- * 提取keyframe名称
- * @param {string} keyframeDeclaration
- * @returns {string}
- */
- extractKeyframeName(keyframeDeclaration) {
- const match = keyframeDeclaration.match(/@keyframes\s+([^\s{]+)/i);
- return match ? match[1] : '';
- }
-
- /**
- * 解析@media规则
- * @param {string} cssText
- * @param {number} startIndex
- * @returns {object}
- */
- parseMedia(cssText, startIndex) {
- let i = startIndex;
- let result = '';
-
- // 读取@media声明
- while (i < cssText.length && cssText[i] !== '{') {
- result += cssText[i];
- i++;
- }
-
- if (i >= cssText.length) {
- return { content: result, endIndex: i };
- }
-
- result += '{';
- i++; // 跳过开始的 '{'
-
- // 解析@media内部的CSS规则
- let braceLevel = 1;
- let innerCss = '';
-
- while (i < cssText.length && braceLevel > 0) {
- if (cssText[i] === '{') {
- braceLevel++;
- } else if (cssText[i] === '}') {
- braceLevel--;
- if (braceLevel === 0) {
- break;
- }
- }
- innerCss += cssText[i];
- i++;
- }
-
- // 递归处理@media内部的CSS规则
- const processedInnerCss = this.parseRules(innerCss);
- result += processedInnerCss;
-
- if (i < cssText.length && cssText[i] === '}') {
- result += '}';
- i++;
- }
-
- return {
- content: result,
- endIndex: i
- };
- }
-
- /**
- * 解析@supports规则
- * @param {string} cssText
- * @param {number} startIndex
- * @returns {object}
- */
- parseSupports(cssText, startIndex) {
- return this.parseMedia(cssText, startIndex);
- }
-
- /**
- * 解析普通CSS规则
- * @param {string} cssText
- * @param {number} startIndex
- * @returns {object}
- */
- parseNormalRule(cssText, startIndex) {
- let i = startIndex;
- let selector = '';
-
- // 读取选择器
- while (i < cssText.length && cssText[i] !== '{') {
- selector += cssText[i];
- i++;
- }
-
- if (i >= cssText.length) {
- return { content: selector, endIndex: i };
- }
-
- // 处理选择器添加作用域
- const scopedSelector = this.addScopeToSelector(selector.trim());
-
- // 读取CSS规则体并处理动画名称
- const braceContent = this.findMatchingBrace(cssText, i);
- const processedContent = this.processRuleContent(braceContent.content);
-
- return {
- content: scopedSelector + processedContent,
- endIndex: braceContent.endIndex
- };
- }
-
- /**
- * 处理CSS规则内容,替换动画名称
- * @param {string} content
- * @returns {string}
- */
- processRuleContent(content) {
- let processedContent = content;
-
- // 处理 animation 属性
- processedContent = processedContent.replace(
- /animation\s*:\s*([^;]+);/gi,
- (match, animationValue) => {
- const processedValue = this.processAnimationValue(animationValue);
- return `animation: ${processedValue};`;
- }
- );
-
- // 处理 animation-name 属性
- processedContent = processedContent.replace(
- /animation-name\s*:\s*([^;]+);/gi,
- (match, animationNames) => {
- const processedNames = this.processAnimationNames(animationNames);
- return `animation-name: ${processedNames};`;
- }
- );
-
- return processedContent;
- }
-
- /**
- * 处理animation属性值
- * @param {string} animationValue
- * @returns {string}
- */
- processAnimationValue(animationValue) {
- // animation 可能包含多个动画,用逗号分隔
- const animations = animationValue.split(',').map(anim => anim.trim());
-
- return animations.map(animation => {
- const parts = animation.split(/\s+/);
-
- // 第一个非时间、非数字、非关键字的值通常是动画名称
- for (let i = 0; i < parts.length; i++) {
- const part = parts[i];
-
- // 跳过时间值 (如 0.3s, 200ms)
- if (/^\d+(\.\d+)?(s|ms)$/.test(part)) continue;
-
- // 跳过数字值 (如 iteration count)
- if (/^\d+(\.\d+)?$/.test(part)) continue;
-
- // 跳过CSS关键字
- if (['ease', 'ease-in', 'ease-out', 'ease-in-out', 'linear', 'infinite',
- 'normal', 'reverse', 'alternate', 'alternate-reverse', 'forwards',
- 'backwards', 'both', 'running', 'paused'].includes(part)) continue;
-
- // 跳过贝塞尔曲线
- if (part.startsWith('cubic-bezier(')) continue;
-
- // 这应该是动画名称
- const scopedName = this.scopedKeyframes.get(part);
- if (scopedName) {
- parts[i] = scopedName;
- }
- break;
- }
-
- return parts.join(' ');
- }).join(', ');
- }
-
- /**
- * 处理animation-name属性值
- * @param {string} animationNames
- * @returns {string}
- */
- processAnimationNames(animationNames) {
- return animationNames.split(',').map(name => {
- const trimmedName = name.trim();
- const scopedName = this.scopedKeyframes.get(trimmedName);
- return scopedName || trimmedName;
- }).join(', ');
- }
-
- /**
- * 为选择器添加作用域
- * @param {string} selector
- * @returns {string}
- */
- addScopeToSelector(selector) {
- if (!selector.trim()) return selector;
-
- // 分割多个选择器(用逗号分隔)
- const selectors = selector.split(',').map(sel => sel.trim());
-
- const scopedSelectors = selectors.map(sel => {
- return this.addScopeToSingleSelector(sel);
- });
-
- return scopedSelectors.join(', ');
- }
-
- /**
- * 为单个选择器添加作用域
- * @param {string} selector
- * @returns {string}
- */
- addScopeToSingleSelector(selector) {
- if (!selector.trim()) return selector;
-
- // 处理伪元素选择器 - 在伪元素前添加作用域
- if (selector.includes('::')) {
- const parts = selector.split('::');
- const mainPart = parts[0];
- const pseudoElement = '::' + parts.slice(1).join('::');
-
- // 为主要部分添加作用域
- const scopedMain = this.addScopeToSelectorPart(mainPart);
- return scopedMain + pseudoElement;
- }
-
- // 处理伪类选择器 - 在伪类前添加作用域
- if (selector.includes(':') && !selector.includes('::')) {
- const pseudoMatch = selector.match(/^([^:]+)(:.+)$/);
- if (pseudoMatch) {
- const mainPart = pseudoMatch[1];
- const pseudoClass = pseudoMatch[2];
-
- // 为主要部分添加作用域
- const scopedMain = this.addScopeToSelectorPart(mainPart);
- return scopedMain + pseudoClass;
- }
- }
-
- // 处理特殊选择器
- if (selector === '*' || selector.startsWith('@')) {
- return selector;
- }
-
- // 处理复合选择器
- return this.addScopeToSelectorPart(selector);
- }
-
- /**
- * 为选择器部分添加作用域
- * @param {string} selectorPart
- * @returns {string}
- */
- addScopeToSelectorPart(selectorPart) {
- // 处理组合器选择器(>、+、~、空格)
- const combinatorRegex = /(\s*[>+~]\s*|\s+)/;
-
- if (combinatorRegex.test(selectorPart)) {
- // 分割选择器,但保留组合器
- const parts = selectorPart.split(combinatorRegex);
- if (/^body(?:$|[:\[ ])/.test(parts[0])) {
- parts[0] = this.scopeBody + parts[0].slice(4)
- return parts.join('');
- }
- if (/^:root(?:$|[:\[ ])/.test(parts[0])) {
- parts[0] = this.scopeBody + parts[0].slice(5)
- return parts.join('');
- }
-
- // 只在最后一个选择器部分添加作用域
- for (let i = parts.length - 1; i >= 0; i--) {
- if (parts[i].trim() && !combinatorRegex.test(parts[i])) {
- let tag = parts[i].trim()
- if (/^body(?:$|[:\[ ])/.test(tag)) {
- parts[i] = this.scopeBody + tag.slice(4)
- } else if (/^:root(?:$:[$:\[ ])/.test(tag)) {
- parts[i] = this.scopeBody + tag.slice(5)
- } else {
- parts[i] = parts[i].trim() + this.scopeAttribute;
- }
- break;
- }
- }
-
- return parts.join('');
- }
- selectorPart = selectorPart.trim()
- let tag = selectorPart.trim()
- if (/^body(?:$|[:\[ ])/.test(tag)) {
- return this.scopeBody + tag.slice(4)
- } else if (/^:root(?:$:[$:\[ ])/.test(tag)) {
- return this.scopeBody + tag.slice(5)
- } else {
- return tag + this.scopeAttribute;
- }
- }
-
- /**
- * 查找匹配的大括号
- * @param {string} cssText
- * @param {number} startIndex
- * @returns {object}
- */
- findMatchingBrace(cssText, startIndex) {
- let i = startIndex;
- let content = '';
- let braceLevel = 0;
-
- while (i < cssText.length) {
- content += cssText[i];
-
- if (cssText[i] === '{') {
- braceLevel++;
- } else if (cssText[i] === '}') {
- braceLevel--;
- if (braceLevel === 0) {
- i++;
- break;
- }
- }
- i++;
- }
-
- return {
- content: content,
- endIndex: i
- };
- }
-}
-
-// 使用示例
-const parser = new CSSParser();
-
-// 示例CSS
-const cssText = `
- body{}
- .container::before, .a {
- content: "";
- animation-name: fadeIn, slideUp;
- animation: slideIn 0.5s infinite alternate;
- }
-
- @keyframes slideIn {
- from {
- transform: translateX(-100%);
- }
- to {
- transform: translateX(0);
- }
- }
-
- @keyframes fadeIn {
- from { opacity: 0; }
- to { opacity: 1; }
- }
-
- @keyframes slideUp {
- from { transform: translateY(100%); }
- to { transform: translateY(0); }
- }
-
- @media (max-width: 768px) {
- .container {
- font-size: 14px;
- animation: slideIn 0.2s ease-out;
- }
-
- .item {
- margin: 5px;
- }
- }
-
- .box > .child {
- color: green;
- }
-
- .box + .sibling {
- margin-top: 20px;
- }
-`;
-
-// 解析并添加作用域
-// console.log(parser.parse(cssText, 'comasd-123'));
-
-// 导出类
-export default parser;
-
-export { CSSParser }
diff --git a/ui/vhtml/vdev.js b/ui/vhtml/vdev.js
deleted file mode 100644
index 0052b3b..0000000
--- a/ui/vhtml/vdev.js
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * vdev.js
- * Copyright (C) 2025 veypi
- *
- * Distributed under terms of the MIT license.
- */
-
-import DivSelectorPlugin from './vdevselect.js'
-
-let isSetup = false;
-let divSelector = null;
-
-const postMessage = (typ, args) => {
- if (!isSetup) return; // 未初始化前不发送消息
- if (typeof args === 'string' || typeof args === 'number') {
- args = { value: args }
- }
- window.parent.postMessage(Object.assign({ type: typ, from: 'vdev' }, args), '*')
-}
-
-// 延迟执行的初始化函数
-const initializeVdev = () => {
- // 在初始化时创建 divSelector 实例
- divSelector = new DivSelectorPlugin();
-
- postMessage('iframe-loaded')
- divSelector.postMessage = postMessage
-
- window.addEventListener('keyup', (event) => {
- if (event.key === 'Escape') {
- postMessage('key-esc')
- }
- })
-
- setTimeout(() => {
- if (window.$vhtml && window.$vhtml.$router) {
- $vhtml.$router.onChange((url) => {
- postMessage('url-change', url.fullPath)
- })
- }
- }, 100)
-
- window.addEventListener('message', (event) => {
- const data = event.data;
- console.log(data)
- if (data.from != 'vhtml') {
- return
- }
- switch (data.type) {
- case 'reload':
- window.location.reload()
- break;
- case 'magic':
- if (divSelector && divSelector.isActive) {
- divSelector.deactivate()
- } else if (divSelector) {
- divSelector.activate()
- }
- break
- }
- });
-}
-
-function setup() {
- if (isSetup) return; // 防止重复初始化
- isSetup = true;
- initializeVdev();
-}
-
-export default setup
diff --git a/ui/vhtml/vdevselect.js b/ui/vhtml/vdevselect.js
deleted file mode 100644
index c59f518..0000000
--- a/ui/vhtml/vdevselect.js
+++ /dev/null
@@ -1,620 +0,0 @@
-/*
- * vdev-select.js
- * Copyright (C) 2025 veypi
- *
- * Distributed under terms of the MIT license.
- */
-
-const copyToClipboard = async (text) => {
- if (navigator.clipboard && navigator.clipboard.writeText) {
- return navigator.clipboard.writeText(text)
- }
- prompt('http环境无法自动复制,请手动复制内容到剪贴板:', text)
- return new Promise((resolve) => { })
-}
-
-/**
- * Div Selector Plugin
- * 用于选择页面div元素并高亮显示的插件
- */
-class DivSelectorPlugin {
- postMessage = (typ, args) => { }
- constructor(options = {}) {
- this.options = {
- highlightColor: '#007bff',
- overlayColor: 'rgba(0, 123, 255, 0.1)',
- borderWidth: '2px',
- zIndex: 10000,
- showTagName: true,
- ...options
- };
-
- this.isActive = false;
- this.selectedElement = null;
- this.currentHoverElement = null;
- this.overlay = null;
- this.selectedOverlay = null;
- this.tooltip = null;
- this.selectedTooltip = null;
- this.actionPanel = null;
- this.originalCursor = '';
-
- this.init();
- }
-
- init() {
- this.createStyles();
- this.bindEvents();
- }
-
- createStyles() {
- const style = document.createElement('style');
- style.textContent = `
- .div-selector-overlay {
- position: fixed;
- pointer-events: none;
- border: ${this.options.borderWidth} solid ${this.options.highlightColor};
- background: ${this.options.overlayColor};
- z-index: ${this.options.zIndex};
- transition: all 0.2s ease;
- box-sizing: border-box;
- }
-
- .div-selector-selected {
- position: fixed;
- pointer-events: none;
- border: ${this.options.borderWidth} solid #28a745;
- background: rgba(40, 167, 69, 0.1);
- z-index: ${this.options.zIndex - 1};
- box-sizing: border-box;
- }
-
- .div-selector-tooltip {
- position: fixed;
- background: rgba(51, 51, 51, 0.95);
- cursor:pointer;
- color: white;
- padding: 8px 12px;
- border-radius: 6px;
- font-size: 12px;
- font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
- z-index: ${this.options.zIndex + 1};
- user-select: none;
- box-shadow: 0 2px 8px rgba(0,0,0,0.3);
- border: 1px solid rgba(255,255,255,0.1);
- backdrop-filter: blur(4px);
- }
-
- .div-selector-tooltip.hover {
- background: rgba(0, 123, 255, 0.9);
- }
-
- .div-selector-tooltip.selected {
- background: rgba(40, 167, 69, 0.9);
- border-color: rgba(40, 167, 69, 0.3);
- }
-
- .div-selector-actions {
- position: fixed;
- background: white;
- border: 1px solid #ddd;
- border-radius: 8px;
- padding: 8px;
- z-index: ${this.options.zIndex + 2};
- box-shadow: 0 4px 16px rgba(0,0,0,0.15);
- display: flex;
- gap: 6px;
- min-width: 30px;
- backdrop-filter: blur(8px);
- }
-
- .div-selector-btn {
- padding: 4px 6px;
- border: 1px solid #ddd;
- background: white;
- border-radius: 6px;
- cursor: pointer;
- font-size: 12px;
- transition: all 0.2s ease;
- text-align: left;
- white-space: nowrap;
- }
-
- .div-selector-btn:hover {
- background: #f8f9fa;
- transform: translateX(2px);
- }
-
- .div-selector-btn.danger {
- color: #dc3545;
- border-color: #dc3545;
- }
-
- .div-selector-btn.danger:hover {
- background: #dc3545;
- color: white;
- }
-
- .div-selector-btn.primary {
- background: #007bff;
- color: white;
- border-color: #007bff;
- }
-
- .div-selector-btn.primary:hover {
- background: #0056b3;
- }
-
- .div-selector-active {
- cursor: crosshair !important;
- }
- `;
- document.head.appendChild(style);
- }
-
- bindEvents() {
- this.handleMouseMove = this.handleMouseMove.bind(this);
- this.handleClick = this.handleClick.bind(this);
- this.handleScroll = this.handleScroll.bind(this);
- this.handleResize = this.handleResize.bind(this);
- }
-
- activate() {
- if (this.isActive) return;
-
- this.isActive = true;
- this.originalCursor = document.body.style.cursor;
- document.body.classList.add('div-selector-active');
-
- document.addEventListener('mousemove', this.handleMouseMove);
- document.addEventListener('click', this.handleClick);
- window.addEventListener('scroll', this.handleScroll);
- window.addEventListener('resize', this.handleResize);
-
- this.createOverlay();
- this.createTooltip();
-
- }
-
- deactivate() {
- if (!this.isActive) return;
-
- this.isActive = false;
- document.body.style.cursor = this.originalCursor;
- document.body.classList.remove('div-selector-active');
-
- document.removeEventListener('mousemove', this.handleMouseMove);
- document.removeEventListener('click', this.handleClick);
- window.removeEventListener('scroll', this.handleScroll);
- window.removeEventListener('resize', this.handleResize);
-
- this.removeOverlay();
- this.removeTooltip();
- // this.clearSelection();
-
- console.log('Div Selector Plugin deactivated.');
- }
-
- handleMouseMove(e) {
- if (!this.isActive) return;
-
- const element = document.elementFromPoint(e.clientX, e.clientY);
- if (!element) return;
-
- // 忽略插件自身的元素
- if (this.isPluginElement(element)) return;
-
- // 查找最近的div元素
- const divElement = element.tagName === 'DIV' ? element : element.closest('div,[vref]');
- if (!divElement || this.isPluginElement(divElement)) return;
-
- // 如果已选中元素,不要高亮显示它
- if (this.selectedElement === divElement) return;
-
- this.currentHoverElement = divElement;
- this.highlightElement(divElement);
- this.updateHoverTooltip(divElement);
- }
-
- handleClick(e) {
- if (!this.isActive) return;
-
- const element = document.elementFromPoint(e.clientX, e.clientY);
- if (!element) return;
-
- // 忽略插件自身的元素
- if (this.isPluginElement(element)) return;
-
- e.preventDefault();
- e.stopPropagation();
-
- const divElement = element.tagName === 'DIV' ? element : element.closest('div');
- if (!divElement || this.isPluginElement(divElement)) return;
-
- this.selectElement(divElement);
- this.deactivate()
- }
-
- handleScroll() {
- if (this.selectedElement) {
- this.updateSelectedOverlay();
- this.updateSelectedTooltip();
- this.updateActionPanelPosition();
- }
- if (this.currentHoverElement && !this.selectedElement) {
- this.highlightElement(this.currentHoverElement);
- this.updateHoverTooltip(this.currentHoverElement);
- }
- }
-
- handleResize() {
- if (this.selectedElement) {
- this.updateSelectedOverlay();
- this.updateSelectedTooltip();
- this.updateActionPanelPosition();
- }
- }
-
- isPluginElement(element) {
- return element.closest('.div-selector-overlay, .div-selector-selected, .div-selector-tooltip, .div-selector-actions');
- }
-
- createOverlay() {
- this.overlay = document.createElement('div');
- this.overlay.className = 'div-selector-overlay';
- this.overlay.style.display = 'none';
- document.body.appendChild(this.overlay);
- }
-
- createSelectedOverlay() {
- this.selectedOverlay = document.createElement('div');
- this.selectedOverlay.className = 'div-selector-selected';
- document.body.appendChild(this.selectedOverlay);
- }
-
- createTooltip() {
- this.tooltip = document.createElement('div');
- this.tooltip.className = 'div-selector-tooltip hover';
- this.tooltip.style.display = 'none';
- document.body.appendChild(this.tooltip);
- }
-
- createSelectedTooltip() {
- this.selectedTooltip = document.createElement('div');
- this.selectedTooltip.className = 'div-selector-tooltip selected';
- document.body.appendChild(this.selectedTooltip);
- }
-
- highlightElement(element) {
- if (!this.overlay) return;
-
- const rect = element.getBoundingClientRect();
-
- this.overlay.style.display = 'block';
- this.overlay.style.left = rect.left + 'px';
- this.overlay.style.top = rect.top + 'px';
- this.overlay.style.width = rect.width + 'px';
- this.overlay.style.height = rect.height + 'px';
- }
-
- updateHoverTooltip(element) {
- if (!this.tooltip) return;
-
- const elementInfo = this.getElementInfo(element);
- const rect = element.getBoundingClientRect();
-
- this.tooltip.textContent = elementInfo;
- this.tooltip.style.display = 'block';
-
- // 计算tooltip位置 - 左上角,稍微偏移避免遮挡
- let left = rect.left - 8;
- let top = rect.top - this.tooltip.offsetHeight - 8;
-
- // 确保不超出视窗
- if (left < 0) left = 8;
- if (top < 0) top = rect.top + 8;
-
- this.tooltip.style.left = left + 'px';
- this.tooltip.style.top = top + 'px';
- }
-
- selectElement(element) {
- // 清除之前的选择
- this.clearSelection();
-
- this.selectedElement = element;
-
- // 创建选中状态的覆盖层和tooltip
- this.createSelectedOverlay();
- this.createSelectedTooltip();
- this.updateSelectedOverlay();
- this.updateSelectedTooltip();
-
- // 隐藏悬停效果
- if (this.overlay) {
- this.overlay.style.display = 'none';
- }
- if (this.tooltip) {
- this.tooltip.style.display = 'none';
- }
-
- // 显示操作面板
- this.showActionPanel();
-
- console.log('Element selected:', element);
- }
-
- updateSelectedOverlay() {
- if (!this.selectedOverlay || !this.selectedElement) return;
-
- const rect = this.selectedElement.getBoundingClientRect();
-
- this.selectedOverlay.style.left = rect.left + 'px';
- this.selectedOverlay.style.top = rect.top + 'px';
- this.selectedOverlay.style.width = rect.width + 'px';
- this.selectedOverlay.style.height = rect.height + 'px';
- }
-
- updateSelectedTooltip() {
- if (!this.selectedTooltip || !this.selectedElement) return;
-
- const elementInfo = this.getElementInfo(this.selectedElement);
- let vref = this.getFilePath(this.selectedElement)
- const rect = this.selectedElement.getBoundingClientRect();
- this.selectedTooltip.addEventListener('click', e => {
- if (vref) {
- this.postMessage('fs-open', vref)
- }
- e.preventDefault()
- e.stopPropagation()
- })
-
- this.selectedTooltip.textContent = `✓ ${elementInfo}`;
-
- // 计算tooltip位置 - 选中框的左上角
- let left = rect.left - 8;
- let top = rect.top - this.selectedTooltip.offsetHeight - 8;
-
- // 确保不超出视窗
- if (left < 0) left = 8;
- if (top < 0) top = rect.top + 8;
-
- this.selectedTooltip.style.left = left + 'px';
- this.selectedTooltip.style.top = top + 'px';
- }
-
- showActionPanel() {
- this.removeActionPanel();
-
- const rect = this.selectedElement.getBoundingClientRect();
-
- this.actionPanel = document.createElement('div');
- this.actionPanel.className = 'div-selector-actions';
-
- // 计算位置 - 放在右上角
- const panelWidth = 180; // 预估宽度
- const panelHeight = 40; // 预估高度
-
- let left = rect.right - panelWidth;
- let top = rect.top + 6;
-
- // 确保不超出视窗
- if (left + panelWidth > window.innerWidth) {
- left = rect.left - panelWidth - 8;
- }
- if (left < 0) left = 8;
-
- if (top + panelHeight > window.innerHeight) {
- top = window.innerHeight - panelHeight - 8;
- }
- if (top < 0) top = 8;
-
- this.actionPanel.style.left = left + 'px';
- this.actionPanel.style.top = top + 'px';
-
- // 创建操作按钮
- const buttons = [
- { text: '📋', action: () => this.copySelector(this.selectedElement) },
- // { text: '🔍', action: () => this.copyXPath(this.selectedElement) },
- // { text: '👁️', action: () => this.viewStyles(this.selectedElement) },
- // { text: '📍', action: () => this.clearSelection() },
- { text: 'x', action: () => this.clearSelection() },
- ];
-
- buttons.forEach(btn => {
- const button = document.createElement('button');
- button.className = `div-selector-btn ${btn.class || ''}`;
- button.textContent = btn.text;
- button.onclick = btn.action;
- this.actionPanel.appendChild(button);
- });
-
- document.body.appendChild(this.actionPanel);
- }
-
- updateActionPanelPosition() {
- if (!this.actionPanel || !this.selectedElement) return;
-
- const rect = this.selectedElement.getBoundingClientRect();
- const panelWidth = this.actionPanel.offsetWidth;
- const panelHeight = this.actionPanel.offsetHeight;
-
- let left = rect.right - panelWidth + 8;
- let top = rect.top - 8;
-
- // 确保不超出视窗
- if (left + panelWidth > window.innerWidth) {
- left = rect.left - panelWidth - 8;
- }
- if (left < 0) left = 8;
-
- if (top + panelHeight > window.innerHeight) {
- top = window.innerHeight - panelHeight - 8;
- }
- if (top < 0) top = 8;
-
- this.actionPanel.style.left = left + 'px';
- this.actionPanel.style.top = top + 'px';
- }
-
- clearSelection() {
- this.selectedElement = null;
-
- if (this.selectedOverlay) {
- this.selectedOverlay.remove();
- this.selectedOverlay = null;
- }
-
- if (this.selectedTooltip) {
- this.selectedTooltip.remove();
- this.selectedTooltip = null;
- }
-
- this.removeActionPanel();
-
- console.log('Selection cleared');
- }
-
- getElementInfo(element) {
- const rect = element.getBoundingClientRect();
- let vref = element.getAttribute('vref')
- if (!vref) {
- vref = element.closest('[vref]').getAttribute('vref')
- vref += "." + element.tagName.toLowerCase();
- }
- return `${vref} (${Math.round(rect.width)}×${Math.round(rect.height)})`;
- }
-
- getFilePath(element) {
- let vref = element.getAttribute('vref')
- if (!vref) {
- vref = element.closest('[vref]').getAttribute('vref')
- }
- if (vref) {
- vref = '/ui' + vref + '.html'
- }
- return vref
- }
-
- copySelector(element) {
- let vref = this.getFilePath(element)
- copyToClipboard(vref).then(() => {
- console.log('CSS Selector copied:', selector);
- this.showNotification('📋 CSS选择器已复制到剪贴板');
- });
- }
-
- copyXPath(element) {
- const xpath = this.generateXPath(element);
- copyToClipboard(xpath).then(() => {
- console.log('XPath copied:', xpath);
- this.showNotification('🔍 XPath已复制到剪贴板');
- });
- }
-
- viewStyles(element) {
- const styles = window.getComputedStyle(element);
- const importantStyles = [
- 'display', 'position', 'width', 'height', 'margin', 'padding',
- 'background-color', 'color', 'font-size', 'border', 'z-index', 'opacity'
- ];
-
- let styleInfo = '📊 重要样式属性:\n\n';
- importantStyles.forEach(prop => {
- styleInfo += `${prop}: ${styles.getPropertyValue(prop)}\n`;
- });
-
- alert(styleInfo);
- }
-
-
- showNotification(message) {
- const notification = document.createElement('div');
- notification.style.cssText = `
- position: fixed;
- top: 20px;
- right: 20px;
- background: linear-gradient(135deg, #28a745, #20c997);
- color: white;
- padding: 12px 18px;
- border-radius: 8px;
- z-index: ${this.options.zIndex + 10};
- font-size: 14px;
- box-shadow: 0 4px 12px rgba(0,0,0,0.2);
- transform: translateX(100%);
- transition: transform 0.3s ease;
- `;
- notification.textContent = message;
- document.body.appendChild(notification);
-
- // 动画显示
- setTimeout(() => {
- notification.style.transform = 'translateX(0)';
- }, 100);
-
- // 自动隐藏
- setTimeout(() => {
- notification.style.transform = 'translateX(100%)';
- setTimeout(() => {
- notification.remove();
- }, 300);
- }, 2500);
- }
-
- generateXPath(element) {
- if (element.id) {
- return `//*[@id="${element.id}"]`;
- }
-
- const path = [];
- while (element && element.nodeType === Node.ELEMENT_NODE) {
- let index = 0;
- let hasFollowingSiblings = false;
- let hasPrecedingSiblings = false;
-
- for (let sibling = element.previousSibling; sibling; sibling = sibling.previousSibling) {
- if (sibling.nodeType === Node.ELEMENT_NODE && sibling.nodeName === element.nodeName) {
- hasPrecedingSiblings = true;
- index++;
- }
- }
-
- for (let sibling = element.nextSibling; sibling && !hasFollowingSiblings; sibling = sibling.nextSibling) {
- if (sibling.nodeType === Node.ELEMENT_NODE && sibling.nodeName === element.nodeName) {
- hasFollowingSiblings = true;
- }
- }
-
- const tagName = element.nodeName.toLowerCase();
- const pathIndex = (hasPrecedingSiblings || hasFollowingSiblings) ? `[${index + 1}]` : '';
- path.unshift(tagName + pathIndex);
- element = element.parentNode;
- if (element === document.body) break;
- }
-
- return path.length ? '/' + path.join('/') : null;
- }
-
- removeOverlay() {
- if (this.overlay) {
- this.overlay.remove();
- this.overlay = null;
- }
- }
-
- removeTooltip() {
- if (this.tooltip) {
- this.tooltip.remove();
- this.tooltip = null;
- }
- }
-
- removeActionPanel() {
- if (this.actionPanel) {
- this.actionPanel.remove();
- this.actionPanel = null;
- }
- }
-}
-
-export default DivSelectorPlugin
diff --git a/ui/vhtml/verror.js b/ui/vhtml/verror.js
deleted file mode 100644
index ddd74aa..0000000
--- a/ui/vhtml/verror.js
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * verror.js
- * Copyright (C) 2025 veypi
- *
- * Distributed under terms of the MIT license.
- */
-class ErrorCollector {
- constructor() {
- this.errorQueue = [];
- this.init();
- }
-
- init() {
- // 监听未处理的 Promise 拒绝
- window.addEventListener('unhandledrejection', (event) => {
- this.handleError({
- type: 'unhandledrejection',
- message: event.reason.message || String(event.reason),
- stack: event.reason.stack,
- timestamp: new Date().toISOString(),
- userAgent: navigator.userAgent,
- url: window.location.href,
- promiseRejection: true
- });
- });
-
- // 监听 JavaScript 错误
- window.addEventListener('error', (event) => {
- if (event.target === window) {
- this.handleError({
- type: 'javascript-error',
- message: event.message,
- source: event.filename,
- line: event.lineno,
- column: event.colno,
- stack: event.error ? event.error.stack : null,
- timestamp: new Date().toISOString(),
- userAgent: navigator.userAgent,
- url: window.location.href
- });
- }
- });
-
- }
-
- handleError(errorInfo) {
- console.error('Error collected:', errorInfo);
- this.queueError(errorInfo);
- }
-
- queueError(errorInfo) {
- this.errorQueue.push(errorInfo);
-
- // 限制队列大小
- if (this.errorQueue.length > 100) {
- this.errorQueue.shift();
- }
-
- // 尝试存储到 localStorage
- try {
- localStorage.setItem('errorQueue', JSON.stringify(this.errorQueue));
- } catch (e) {
- console.warn('Failed to store error queue in localStorage');
- }
- }
-
- // 手动报告错误
- reportError(error, context = {}) {
- this.handleError({
- type: 'manual-report',
- message: error.message,
- stack: error.stack,
- context: context,
- timestamp: new Date().toISOString(),
- userAgent: navigator.userAgent,
- url: window.location.href
- });
- }
-}
-
-// 初始化错误收集器
-const errorCollector = new ErrorCollector();
-
-// 导出实例供其他模块使用
-window.errorCollector = errorCollector;
-
-export default errorCollector
-
-
diff --git a/ui/vhtml/vget.js b/ui/vhtml/vget.js
deleted file mode 100644
index b5c6df3..0000000
--- a/ui/vhtml/vget.js
+++ /dev/null
@@ -1,322 +0,0 @@
-/*
- * vget.js
- * Copyright (C) 2024 veypi
- *
- * Distributed under terms of the MIT license.
- */
-import EventBus from './vbus.js';
-import axios from './axios.min.js'
-import vcss from './vcss.js'
-import vproxy from './vproxy.js';
-import vmessage from './vmessage.js'
-
-async function FetchFile(url) {
- return fetch(url).then((response) => {
- if (!response.ok) {
- throw new Error(`HTTP error! status: ${response.status}`);
- }
- return response.text();
- })
-}
-
-var cacheUrl = {}
-var pendingRequests = {};
-let baseFile = ''
-
-const envMap = {}
-async function getEnv(scoped, temp) {
- scoped = scoped || ''
- if (!envMap[scoped]) {
- let baseURL = scoped.startsWith('http') ? scoped : window.location.origin + scoped
- envMap[scoped] = Object.assign({}, temp, {
- scoped: scoped,
- $G: vproxy.Wrap({}),
- $bus: new EventBus(),
- $axios: axios.create({
- baseURL: baseURL,
- }),
- $message: vmessage,
- $router: null,
- $emit: null,
- })
- if (scoped === $vhtml.scoped || $vhtml.scoped === null) {
- envMap[scoped].$router = $vhtml.$router
- } else {
- // 对于第三方组件,不配置路由
- envMap[scoped].$router = { addRoutes: () => { }, beforeEnter: () => { } }
- }
- try {
- await (await import(baseURL + '/env.js')).default(envMap[scoped])
- } catch (e) {
- console.warn('error loading ' + baseURL + '/env.js: ' + e)
- }
- }
- return envMap[scoped]
-}
-
-/**
-* @param {string} url
-* @return {Promise<{heads:HTMLCollection, body: HTMLElement, setup?:Element, scripts:Element[]}, scripts:Element>}
-*/
-async function FetchUI(url, env, ignorescoped) {
- if (!url || url === '/') {
- url = '/'
- }
- if (!url.startsWith('http') && !url.startsWith('@')) {
- if (!url.startsWith('/')) {
- url = '/' + url
- }
- }
- let scoped = env?.scoped
- if (scoped && url.startsWith('/')) {
- url = scoped + url
- }
- if (url.startsWith('@')) {
- url = url.slice(1)
- }
- if (cacheUrl[url]) {
- return Promise.resolve(cacheUrl[url])
- }
- if (pendingRequests[url]) {
- return pendingRequests[url];
- }
- let tempenv = {}
- const promise = fetch(url + "?random=" + Math.random())
- .then(async (response) => {
- if (!response.ok) {
- throw new Error(`HTTP error! status: ${response.status}`);
- }
- for (const [key, value] of response.headers.entries()) {
- if (key.startsWith('vhtml-')) {
- tempenv[key.slice(6)] = value
- }
- }
- let scoped = tempenv.scoped || ''
- if (url.startsWith('http')) {
- scoped = new URL(url).origin + scoped
- tempenv.scoped = scoped
- }
- let packEnv = await getEnv(scoped, tempenv)
- Object.assign(tempenv, packEnv)
- // Object.seal(tempenv)
- return response.text()
- })
- .then(txt => {
- // if (baseFile === txt) {
- // throw new Error(`HTTP error! status: 404`);
- // }
- if (baseFile == '') {
- baseFile = txt
- }
- return ParseUI(txt, tempenv, url, ignorescoped)
- }).then((parser) => {
- cacheUrl[url] = parser
- return parser
- })
- .catch(err => {
- let errmsg = '404'
- if (err.message !== 'HTTP error! status: 404') {
- console.warn(err)
- }
- let dom404 = document.createElement('div')
- dom404.style.cssText = `
- backgound:#aaa;
- height:100%;
- width: 100%;
- display:grid;
- place-items: center;
-`
- dom404.innerHTML = `
-
-`
- let parser = {
- heads: [],
- body: dom404,
- setup: '',
- scripts: [],
- styles: '',
- txt: '',
- tmp: '',
- env: tempenv,
- err: err,
- }
- cacheUrl[url] = parser
- return parser
- })
- .finally(() => {
- delete pendingRequests[url];
- });
- pendingRequests[url] = promise;
- return promise;
-}
-
-function generateCompactUniqueString() {
- // 获取当前时间戳,精确到毫秒
- const timestamp = new Date().getTime();
- let shortenedTimestamp = timestamp.toString(36);
- if (shortenedTimestamp.length > 4) {
- shortenedTimestamp = shortenedTimestamp.substring(shortenedTimestamp.length - 4);
- }
- const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
- let randomPart = '';
- for (let i = 0; i < 4; i++) {
- randomPart += characters.charAt(Math.floor(Math.random() * characters.length));
- }
- // 组合随机部分和时间戳部分,保证总长度为8位
- return randomPart + shortenedTimestamp.padStart(4, '0');
-}
-
-function sync_ref_owner_id(dom, id) {
- // 子组件根节点不设置data-v, 所以style class 不生效于body,只能通过body {}设置样式
- // dom.setAttribute('data-v-' + id, '')
- Array.from(dom.childNodes).forEach((n) => {
- if (n.nodeType === 1) {
- n.setAttribute('vrefof', id)
- sync_ref_owner_id(n, id)
- }
- })
-}
-
-async function ParseUI(txt, env, turl, ignorescoped) {
- if (turl === undefined) {
- turl = '#' + generateCompactUniqueString()
- }
- if (turl.endsWith('.html')) {
- turl = turl.slice(0, -5)
- }
- let tmp = new DOMParser().parseFromString(txt, 'text/html')
- if (tmp.body.hasAttribute('scoped') && !ignorescoped) {
- throw new Error(`HTTP error! status: 404`);
- }
- let target = {
- url: turl,
- heads: [],
- body: document.createElement('div'),
- setup: undefined,
- scripts: [],
- styles: '',
- txt: txt,
- env: env,
- tmp: tmp,
- customAttrs: {},
- }
- target.heads = Array.from(tmp.querySelector('head')?.children)
- // target.heads.forEach(h => {
- // })
- if (turl) {
- tmp.querySelectorAll('style').forEach((s) => {
- if (s.getAttribute('unscoped') === null) {
- target.styles += vcss.parse(s.innerHTML, turl)
- } else {
- target.styles += s.innerHTML
- }
- })
- if (target.styles) {
- const style = document.createElement('style')
- style.innerHTML = target.styles
- style.setAttribute('vref', turl)
- document.head.appendChild(style)
- }
- }
- target.body.append(...tmp.querySelector('body').childNodes)
- // target.body = tmp.querySelector('body')
- target.body.querySelectorAll('script').forEach((s) => {
- let sinner = s.innerHTML.trim()
- if (sinner == '') {
- s.remove()
- return
- }
- if (s.hasAttribute('setup')) {
- target.setup = s
- } else if (!s.hasAttribute('no-vhtml')) {
- target.scripts.push(s)
- }
- s.remove()
- })
- // target.body.classList = tmp.body.classList
- Array.from(tmp.body.attributes).forEach((e) => {
- if (/^[a-zA-Z]/.test(e.name)) {
- target.body.setAttribute(e.name, e.value)
- } else {
- target.customAttrs[e.name] = e.value
- }
- })
- target.body.setAttribute('vref', turl)
- sync_ref_owner_id(target.body, turl)
- if (!ignorescoped) {
- await loadHeaders(target, env)
- }
- return target
-}
-
-async function loadHeaders(target, env) {
- for (let h of target.heads) {
- let nodeName = h.nodeName.toLowerCase()
- if (nodeName === 'link') {
- LoadLink(h, env)
- } else if (nodeName === 'script') {
- await LoadScript(h, env)
- } else if (nodeName === 'title') {
- target.title = h.innerText
- } else {
- }
- }
-}
-/**
-* @param {HTMLElement} dom
-*/
-function LoadScript(dom, env) {
- let src = dom.getAttribute('src')
- let key = dom.getAttribute('key')
- let scoped = env?.scoped
- if (scoped && src.startsWith('/')) {
- src = scoped + src
- }
- if (src.startsWith('@')) {
- src = src.slice(1)
- }
- if (src && document.querySelector(`script[src="${src}"]`)) {
- return
- }
- if (key && document.querySelector(`script[key="${key}"]`)) {
- return
- }
- let newDom = document.createElement('script')
- newDom.src = src
- newDom.key = key
- newDom.type = dom.getAttribute('type') || 'text/javascript'
- return new Promise((resolve, reject) => {
- newDom.onload = () => {
- resolve(newDom)
- };
- newDom.onerror = () => reject(new Error(`Failed to load script ${src}`));
- document.head.appendChild(newDom)
- })
-}
-
-async function LoadLink(dom, env) {
- let src = dom.getAttribute('href')
- let key = dom.getAttribute('key')
- let scoped = env?.scoped
- if (scoped && src.startsWith('/')) {
- src = scoped + src
- }
- if (src.startsWith('@')) {
- src = src.slice(1)
- }
- if (src && document.querySelector(`link[href="${src}"]`)) {
- return
- }
- if (key && document.querySelector(`link[key="${key}"]`)) {
- return
- }
- dom.setAttribute('href', src)
- document.head.append(dom)
-}
-
-
-
-export default { FetchUI, FetchFile, LoadScript, LoadLink, ParseUI }
diff --git a/ui/vhtml/vmessage.js b/ui/vhtml/vmessage.js
deleted file mode 100644
index 2d2a5cf..0000000
--- a/ui/vhtml/vmessage.js
+++ /dev/null
@@ -1,531 +0,0 @@
-/*
- * vmessage.js
- * Copyright (C) 2025 veypi
- *
- * Distributed under terms of the MIT license.
- */
-
-class Message {
- constructor() {
- this.container = null;
- this.init();
- }
-
- /**
- * 初始化容器
- */
- init() {
- this.container = document.createElement('div');
- this.container.className = 'message-container';
- document.body.appendChild(this.container);
-
- // 添加基础样式
- this.addBaseStyles();
- }
-
- /**
- * 添加基础样式
- */
- addBaseStyles() {
- const style = document.createElement('style');
- style.textContent = `
- .message-container {
- position: fixed;
- top: 60px;
- left: 50%;
- transform: translateX(-50%);
- z-index: 9999;
- width: 300px;
- }
-
- .message-item {
- margin-bottom: 10px;
- padding: 15px;
- border-radius: 4px;
- box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
- transform: translateY(100%);
- opacity: 0;
- transition: all 0.3s ease;
- display: flex;
- align-items: flex-start;
- }
-
- .message-item.show {
- transform: translateY(0);
- opacity: 1;
- }
-
- .message-icon {
- margin-right: 10px;
- font-size: 16px;
- line-height: 1;
- }
-
- .message-content {
- flex: 1;
- font-size: 14px;
- line-height: 1.4;
- }
-
- .message-close {
- margin-left: 10px;
- cursor: pointer;
- font-size: 16px;
- opacity: 0.7;
- transition: opacity 0.2s;
- }
-
- .message-close:hover {
- opacity: 1;
- }
-
- .message-success {
- background-color: #f0f9eb;
- border: 1px solid #e1f3d8;
- color: var(--color-success, #67c23a);
- }
-
- .message-warning {
- background-color: #fdf6ec;
- border: 1px solid #faecd8;
- color: #e6a23c;
- }
-
- .message-error {
- background-color: #fef0f0;
- border: 1px solid #fde2e2;
- color: #f56c6c;
- }
-
- .message-info {
- background-color: #edf2fc;
- border: 1px solid #ebeef5;
- color: #409eff;
- }
-
- .prompt-overlay {
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background-color: rgba(0, 0, 0, 0.5);
- z-index: 10000;
- display: flex;
- justify-content: center;
- align-items: center;
- opacity: 0;
- transition: opacity 0.3s ease;
- }
-
- .prompt-overlay.show {
- opacity: 1;
- }
-
- .prompt-dialog {
- background: white;
- border-radius: 8px;
- box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
- min-width: 400px;
- max-width: 500px;
- transform: scale(0.8);
- transition: transform 0.3s ease;
- }
-
- .prompt-overlay.show .prompt-dialog {
- transform: scale(1);
- }
-
- .prompt-header {
- padding: 20px 20px 10px;
- display: flex;
- justify-content: space-between;
- align-items: center;
- border-bottom: 1px solid #eee;
- }
-
- .prompt-title {
- font-size: 18px;
- font-weight: 500;
- margin: 0;
- }
-
- .prompt-close {
- cursor: pointer;
- font-size: 20px;
- color: #999;
- border: none;
- background: none;
- padding: 0;
- width: 20px;
- height: 20px;
- display: flex;
- align-items: center;
- justify-content: center;
- }
-
- .prompt-close:hover {
- color: #666;
- }
-
- .prompt-body {
- padding: 20px;
- }
-
- .prompt-content {
- margin-bottom: 20px;
- font-size: 14px;
- line-height: 1.5;
- color: #333;
- }
-
- .prompt-input {
- width: 100%;
- padding: 10px 12px;
- border: 1px solid #dcdfe6;
- border-radius: 4px;
- font-size: 14px;
- box-sizing: border-box;
- transition: border-color 0.2s;
- }
-
- .prompt-input:focus {
- outline: none;
- border-color: #409eff;
- }
-
- .prompt-footer {
- padding: 15px 20px;
- text-align: right;
- border-top: 1px solid #eee;
- }
-
- .prompt-btn {
- padding: 8px 16px;
- border: 1px solid #dcdfe6;
- border-radius: 4px;
- font-size: 14px;
- cursor: pointer;
- margin-left: 10px;
- transition: all 0.2s;
- }
-
- .prompt-btn-cancel {
- background: white;
- color: #606266;
- }
-
- .prompt-btn-cancel:hover {
- background: #f5f7fa;
- border-color: #c0c4cc;
- }
-
- .prompt-btn-confirm {
- background: #409eff;
- color: white;
- border-color: #409eff;
- }
-
- .prompt-btn-confirm:hover {
- background: #66b1ff;
- border-color: #66b1ff;
- }
- `;
- document.head.appendChild(style);
- }
-
- /**
- * 创建消息元素
- * @param {string} type - 消息类型 (success, warning, error, info)
- * @param {string} content - 消息内容
- * @param {Object} options - 配置选项
- */
- createMessage(type, content, options = {}) {
- const {
- duration = 3000,
- showClose = true,
- onClose = null
- } = options;
-
- const messageItem = document.createElement('div');
- messageItem.className = `message-item message-${type}`;
-
- // 消息图标
- const icons = {
- success: '✓',
- warning: '⚠',
- error: '✕',
- info: 'ℹ'
- };
-
- const icon = document.createElement('span');
- icon.className = 'message-icon';
- icon.textContent = icons[type] || icons.info;
-
- // 消息内容
- const contentEl = document.createElement('div');
- contentEl.className = 'message-content';
- contentEl.textContent = content;
-
- messageItem.appendChild(icon);
- messageItem.appendChild(contentEl);
-
- // 关闭按钮
- if (showClose) {
- const closeBtn = document.createElement('span');
- closeBtn.className = 'message-close';
- closeBtn.innerHTML = '×';
- closeBtn.onclick = () => {
- this.removeMessage(messageItem);
- if (onClose) onClose();
- };
- messageItem.appendChild(closeBtn);
- }
-
- this.container.appendChild(messageItem);
-
- // 显示动画
- setTimeout(() => {
- messageItem.classList.add('show');
- }, 10);
-
- // 自动关闭
- if (duration > 0) {
- setTimeout(() => {
- this.removeMessage(messageItem);
- if (onClose) onClose();
- }, duration);
- }
-
- return messageItem;
- }
-
- /**
- * 移除消息
- * @param {HTMLElement} messageItem - 消息元素
- */
- removeMessage(messageItem) {
- if (messageItem && messageItem.parentNode) {
- messageItem.classList.remove('show');
- setTimeout(() => {
- if (messageItem.parentNode) {
- messageItem.parentNode.removeChild(messageItem);
- }
- }, 300);
- }
- }
-
- /**
- * 成功消息
- * @param {string} content - 消息内容
- * @param {Object} options - 配置选项
- */
- success(content, options = {}) {
- return this.createMessage('success', content, options);
- }
-
- /**
- * 警告消息
- * @param {string} content - 消息内容
- * @param {Object} options - 配置选项
- */
- warning(content, options = {}) {
- return this.createMessage('warning', content, options);
- }
-
- /**
- * 错误消息
- * @param {string} content - 消息内容
- * @param {Object} options - 配置选项
- */
- error(content, options = {}) {
- return this.createMessage('error', content, options);
- }
-
- /**
- * 信息消息
- * @param {string} content - 消息内容
- * @param {Object} options - 配置选项
- */
- info(content, options = {}) {
- return this.createMessage('info', content, options);
- }
-
- /**
- * 显示提示框
- * @param {string} content - 提示内容
- * @param {Object} options - 配置选项
- */
- _prompt(content, options = {}) {
- return new Promise((resolve, reject) => {
- const {
- title = '提示',
- type = 'confirm', // confirm, input
- inputValue = '',
- confirmText = '确定',
- cancelText = '取消',
- onConfirm = null,
- onCancel = () => { resolve('') }
- } = options;
-
- // 创建遮罩层
- const overlay = document.createElement('div');
- overlay.className = 'prompt-overlay';
-
- // 创建对话框
- const dialog = document.createElement('div');
- dialog.className = 'prompt-dialog';
-
- // 头部
- const header = document.createElement('div');
- header.className = 'prompt-header';
-
- const titleEl = document.createElement('h3');
- titleEl.className = 'prompt-title';
- titleEl.textContent = title;
-
- const closeBtn = document.createElement('button');
- closeBtn.className = 'prompt-close';
- closeBtn.innerHTML = '×';
-
- header.appendChild(titleEl);
- header.appendChild(closeBtn);
-
- // 主体
- const body = document.createElement('div');
- body.className = 'prompt-body';
-
- const contentEl = document.createElement('div');
- contentEl.className = 'prompt-content';
- contentEl.textContent = content;
-
- body.appendChild(contentEl);
-
- // 输入框(如果是input类型)
- let inputEl = null;
- if (type === 'input') {
- inputEl = document.createElement('input');
- inputEl.className = 'prompt-input';
- inputEl.type = 'text';
- inputEl.value = inputValue;
- body.appendChild(inputEl);
- }
-
- // 底部按钮
- const footer = document.createElement('div');
- footer.className = 'prompt-footer';
-
- const cancelBtn = document.createElement('button');
- cancelBtn.className = 'prompt-btn prompt-btn-cancel';
- cancelBtn.textContent = cancelText;
-
- const confirmBtn = document.createElement('button');
- confirmBtn.className = 'prompt-btn prompt-btn-confirm';
- confirmBtn.textContent = confirmText;
-
- footer.appendChild(cancelBtn);
- footer.appendChild(confirmBtn);
-
- dialog.appendChild(header);
- dialog.appendChild(body);
- dialog.appendChild(footer);
- overlay.appendChild(dialog);
-
- document.body.appendChild(overlay);
-
- // 显示动画
- setTimeout(() => {
- overlay.classList.add('show');
- }, 10);
-
- // 如果是input类型,自动聚焦
- if (inputEl) {
- setTimeout(() => {
- inputEl.focus();
- }, 300);
- }
-
- // 事件处理
- const closeDialog = (result = null) => {
- overlay.classList.remove('show');
- setTimeout(() => {
- if (overlay.parentNode) {
- overlay.parentNode.removeChild(overlay);
- }
- }, 300);
- return result;
- };
-
- // 关闭按钮
- closeBtn.onclick = () => {
- closeDialog();
- onCancel ? onCancel() : reject(new Error('cancelled'))
- };
-
- // 取消按钮
- cancelBtn.onclick = () => {
- closeDialog();
- onCancel ? onCancel() : reject(new Error('cancelled'))
- };
-
- // 确认按钮
- confirmBtn.onclick = () => {
- const value = inputEl ? inputEl.value : true;
- closeDialog();
- resolve(value);
- if (onConfirm) onConfirm(value);
- };
-
- // ESC键关闭
- const handleEsc = (e) => {
- if (e.key === 'Escape') {
- closeDialog();
- onCancel ? onCancel() : reject(new Error('cancelled'))
- document.removeEventListener('keydown', handleEsc);
- }
- };
-
- document.addEventListener('keydown', handleEsc);
-
- // 点击遮罩层关闭
- overlay.onclick = (e) => {
- if (e.target === overlay) {
- closeDialog();
- onCancel ? onCancel() : reject(new Error('cancelled'))
- }
- };
- });
- }
-
- /**
- * 确认框
- * @param {string} content - 确认内容
- * @param {Object} options - 配置选项
- */
- confirm(content, options = {}) {
- return this._prompt(content, {
- ...options,
- type: 'confirm'
- });
- }
-
- /**
- * 输入框
- * @param {string} content - 提示内容
- * @param {Object} options - 配置选项
- */
- prompt(message, content, options = {}) {
- return this._prompt(message, {
- ...options,
- type: 'input',
- inputValue: content,
- });
- }
-}
-
-// 创建单例实例
-const message = new Message();
-
-// 导出默认实例和类
-export default message;
-export { Message };
diff --git a/ui/vhtml/vproxy.js b/ui/vhtml/vproxy.js
deleted file mode 100644
index 0a35ba6..0000000
--- a/ui/vhtml/vproxy.js
+++ /dev/null
@@ -1,553 +0,0 @@
-/*
- * proxy.js
- * Copyright (C) 2024 veypi
- *
- * Distributed under terms of the MIT license.
- */
-
-
-/** @type {([boolean, ()=>void])[]} */
-const callbackList = []
-/** @type {number[]} */
-const cacheUpdateList = []
-// 界面更新响应频率40hz
-const sync = () => {
- let list = new Set(cacheUpdateList.splice(0))
- let c = 0
- for (let l of list) {
- if (callbackList[l]) {
- callbackList[l]()
- c++
- }
- }
- if (c > 0) {
- console.log(`update ${c}`)
- // sync()
- }
- return c
-}
-setInterval(sync, 25)
-
-function GenUniqueID() {
- const timestamp = performance.now().toString(36);
- const random = Math.random().toString(36).substring(2, 5);
- return `${timestamp}-${random}`;
-}
-
-function ForceUpdate() {
- for (let c of callbackList) {
- if (c) {
- c()
- }
- }
-}
-
-window.$vupdate = (id) => {
- console.log('update', id)
- callbackList[id]()
-}
-
-function deepAccess(obj, seen = new Set()) {
- if (obj && typeof obj === 'object' && !seen.has(obj)) {
- seen.add(obj)
- for (let key in obj) {
- deepAccess(obj[key], seen)
- }
- }
- return obj
-}
-
-/** @type {number[]} */
-var listen_tags = []
-/**
-* @param {()=>void} callback
-* @returns number
-*/
-function Watch(target, callback, options) {
- let idx = callbackList.length
- listen_tags.push(idx)
- if (typeof callback === 'function') {
- callbackList.push(() => {
- callback(target())
- })
- } else {
- callbackList.push(target)
- }
- let res
- try {
- res = target()
- if (options && options.deep) {
- deepAccess(res)
- }
- } catch (e) {
- console.warn('running \n%s\n failed:', target, e)
- } finally {
- listen_tags.pop()
- }
- if (typeof callback === 'function') {
- callback(res)
- }
- return idx
-}
-
-function Cancel(idx) {
- if (idx < 0 || idx >= callbackList.length) {
- return
- }
- callbackList[idx] = null
-}
-
-const isProxy = Symbol("isProxy")
-const DataID = Symbol("DataID")
-const DataBind = Symbol("bind")
-const rootObj = Symbol("root")
-const rootArg = Symbol("root arg")
-
-
-function SetDataRoot(data, root) {
- data[rootObj] = root
- Object.keys(root).forEach(k => {
- if (k in data) {
- } else {
- data[k] = rootArg
- }
- })
-}
-
-function isProxyType(v) {
- if (!v || typeof v !== 'object') {
- return false
- }
- if (v instanceof Node || v instanceof Date || v instanceof RegExp || v instanceof Event) {
- return false
- }
- if (v.__noproxy) {
- return false
- }
- if (v.constructor !== Object && v.constructor !== Array) {
- return false
- }
- return true
-}
-
-// oldValue只集成值, 不继承事件
-function copyBind(oldValue, newValue) {
- if (!oldValue || !oldValue[isProxy] || !isProxyType(newValue)) {
- return newValue
- }
- let binds = oldValue[DataBind]
- if (newValue[isProxy]) {
- // 新值也是代理对象,继承旧值的事件绑定, 使用新的代理对象
- if (newValue[DataID] === oldValue[DataID]) {
- return newValue
- }
- for (let k in binds) {
- if (newValue[DataBind][k]?.indexOf) {
- const currentBinds = newValue[DataBind][k]
- const bindSet = new Set(currentBinds)
- for (let i of binds[k]) {
- if (!bindSet.has(i)) {
- currentBinds.push(i)
- bindSet.add(i)
- }
- }
- } else {
- newValue[DataBind][k] = binds[k]
- }
- }
- } else {
- // 新值不是代理对象,继承值,使用旧的代理对象
- if (Array.isArray(newValue) && Array.isArray(oldValue)) {
- oldValue.length = 0
- for (let i = 0; i < newValue.length; i++) {
- oldValue.push(newValue[i])
- }
- return oldValue
- }
- Object.keys(oldValue).forEach(k => {
- if (!newValue.hasOwnProperty(k)) {
- delete oldValue[k]
- }
- })
- Object.keys(newValue).forEach(k => {
- if (oldValue[k]?.[isProxy]) {
- oldValue[k] = copyBind(oldValue[k], newValue[k])
- } else {
- oldValue[k] = newValue[k]
- }
- })
- return oldValue
- }
- for (let k in newValue) {
- if (k in oldValue && oldValue[k]?.[isProxy]) {
- newValue[k] = copyBind(oldValue[k], newValue[k])
- }
- }
- return newValue
-}
-
-
-let stopChecking = false
-function Wrap(data, root = undefined) {
- const did = GenUniqueID()
- let isArray = false
- if (Object.prototype.toString.call(data) === '[object Array]') {
- isArray = true
- }
- if (root) {
- SetDataRoot(data, root)
- }
- // data[DataID] = did
- const listeners = {}
- const handler = {
- /**
- * @param {Object} target
- * @param {string|symbol} key
- *
- * */
- get(target, key, receiver) {
- if (key === DataID) {
- return did
- } else if (key === isProxy) {
- return true
- } else if (key === DataBind) {
- return listeners
- }
- const value = Reflect.get(target, key, receiver)
- if (value === rootArg) {
- return target[rootObj][key]
- }
- if (typeof key === 'symbol' && stopChecking) {
- return value
- } else if (typeof value === 'function') {
- return value
- }
- let idx = -1
- if (listen_tags.length > 0) {
- let lkey = key
- idx = listen_tags[listen_tags.length - 1]
- if (isArray) {
- lkey = ''
- }
- if (!listeners.hasOwnProperty(lkey)) {
- listeners[lkey] = [idx]
- } else if (listeners[lkey].indexOf(idx) == -1) {
- listeners[lkey].push(idx)
- }
- }
- if (window.vdev) {
- console.log(`${did} get ${key.toString()}:|${value}| `)
- }
- if (isProxyType(value) && !value[isProxy]) {
- let newValue = Wrap(value, undefined)
- Reflect.set(target, key, newValue, receiver)
- return newValue
- }
- return value;
- },
- set(target, key, newValue, receiver) {
- const oldValue = Reflect.get(target, key, receiver)
- if (oldValue === rootArg) {
- target[rootObj][key] = newValue
- return true
- }
- if (oldValue === newValue) {
- return true
- } else if (stopChecking) {
- return Reflect.set(target, key, newValue, receiver)
- }
- let result = true
- if (Array.isArray(newValue) && Array.isArray(oldValue)) {
- stopChecking = true
- oldValue.length = 0
- for (let i = 0; i < newValue.length; i++) {
- oldValue.push(newValue[i])
- }
- stopChecking = false
- } else if (oldValue && oldValue[isProxy] && isProxyType(newValue)) {
- // 监听对象只赋值可迭代属性
- newValue = copyBind(oldValue, newValue)
- result = Reflect.set(target, key, newValue, receiver);
- } else {
- result = Reflect.set(target, key, newValue, receiver);
- }
- if (result && listen_tags.length === 0) {
- let lkey = key
- if (isArray) {
- lkey = ''
- }
- if (listeners[lkey]) {
- let i = 0
- if (window.vdev) {
- console.log(`before set ${key} listeners:`, listeners[lkey], target)
- }
- while (i < listeners[lkey].length) {
- let cb = listeners[lkey][i]
- if (!callbackList[cb]) {
- listeners[lkey].splice(i, 1);
- } else {
- i++
- if (window.vdev) {
- console.log(`${did} set ${key}:`, '\n', callbackList[cb], '\n', oldValue, newValue)
- }
- cacheUpdateList.push(cb)
- }
- }
- }
- }
- return result;
- },
- deleteProperty(target, key) {
- if (window.vdev) {
- console.log(`del ${key}`)
- }
- const result = Reflect.deleteProperty(target, key);
- if (result && listen_tags.length === 0) {
- let lkey = key
- if (isArray) {
- lkey = ''
- }
- if (listeners[lkey]) {
- let i = 0
- while (i < listeners[lkey].length) {
- let cb = listeners[lkey][i]
- if (!callbackList[cb]) {
- listeners[lkey].splice(i, 1);
- } else {
- i++
- cacheUpdateList.push(cb)
- if (window.vdev) {
- console.log(`${did} del ${key}:`, '\n', callbackList[cb], '\n')
- }
- }
- }
- }
- }
- return result
- },
- };
-
- let res = new Proxy(data, handler);
- return res
-}
-
-const expose = {
- 'console': console,
- 'window': window,
- 'prompt': prompt.bind(window),
- 'alert': alert.bind(window),
- 'confirm': confirm.bind(window),
- 'RegExp': RegExp,
- 'document': document,
- 'Array': Array,
- 'Object': Object,
- 'Math': Math,
- 'Date': Date,
- 'JSON': JSON,
- 'Symbol': Symbol,
- 'Number': Number,
- 'eval': eval,
- 'isNaN': isNaN,
- 'parseInt': parseInt,
- 'parseFloat': parseFloat,
- 'setTimeout': setTimeout.bind(window),
- 'setInterval': setInterval.bind(window),
- 'clearTimeout': clearTimeout.bind(window),
- 'clearInterval': clearInterval.bind(window),
- 'encodeURIComponent': encodeURIComponent,
- 'btoa': btoa.bind(window),
- 'fetch': fetch.bind(window),
- 'TextDecoder': TextDecoder,
- 'history': history,
- 'requestAnimationFrame': requestAnimationFrame.bind(window),
-}
-
-function newProxy(data, env, tmpenv) {
- const proxy = new Proxy(data, {
- // 拦截所有属性,防止到 Proxy 对象以外的作用域链查找。
- has(target, key) {
- return true;
- },
- get(target, key, receiver) {
- let v
- if (key === '$data') {
- v = data
- } else if (key === '$env') {
- v = env
- } else if (key in target) {
- v = Reflect.get(target, key, receiver);
- } else if (key in env) {
- v = env[key]
- } else if (tmpenv && key in tmpenv) {
- v = tmpenv[key]
- } else if (key in expose) {
- v = expose[key]
- } else if (key in window) {
- v = window[key]
- }
- return v
- },
- set(target, key, newValue, receiver) {
- // code global variable set will work on "data"
- return Reflect.set(target, key, newValue, receiver);
- }
- });
- return proxy
-}
-// 运行dom属性绑定等小代码语句
-// for code snapshot
-function Run(originCode, data, env, tmpenv) {
- let code = originCode.trim()
- const cleanCode = code.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, '').trim()
- const isStatement = /^(var|let|const|if|for|while|switch|try|throw|class|function|return|debugger)\b/.test(cleanCode)
- if (!isStatement && (code.indexOf('\n') === -1 || !cleanCode.includes(';'))) {
- code = 'return ' + code
- }
- code = `
-with (sandbox) {
-${code}
-}`
- let res
- try {
- const fn = new Function('sandbox', code);
- res = fn(newProxy(data, env, tmpenv))
- } catch (error) {
- console.warn(`Run error:`, originCode, '\n', error)
- }
- return res
-}
-
-const AsyncFunction = Object.getPrototypeOf(async function() { }).constructor
-
-// 运行大段代码库
-async function AsyncRun(originCode, data, env, tmpenv) {
- let code = originCode.trim()
- if (code.indexOf('\n') === -1) {
- code = 'return ' + code
- }
- code = `
-with (sandbox) {
-${code}
-}`
- // try {
- const fn = new AsyncFunction('sandbox', code);
- return await fn(newProxy(data, env, tmpenv))
- // } catch (error) {
- // console.warn('AsyncRun error:', error, '\n', originCode)
- // }
-}
-
-function resolvePath(relativePath, currentPath) {
- // 如果相对路径已经是绝对路径,直接返回
- if (relativePath.startsWith('/')) {
- return relativePath;
- }
-
- // 获取当前路径的目录部分(去掉文件名)
- const currentDir = currentPath.substring(0, currentPath.lastIndexOf('/'));
-
- // 分割路径段
- const currentSegments = currentDir.split('/').filter(segment => segment !== '');
- const relativeSegments = relativePath.split('/').filter(segment => segment !== '');
-
- // 处理相对路径段
- for (const segment of relativeSegments) {
- if (segment === '..') {
- // 返回上一级目录
- if (currentSegments.length > 0) {
- currentSegments.pop();
- }
- } else if (segment === '.') {
- // 当前目录,不做任何操作
- continue;
- } else {
- // 普通目录或文件名
- currentSegments.push(segment);
- }
- }
-
- // 构建绝对路径
- return '/' + currentSegments.join('/');
-}
-
-
-async function ParseImport(code, data, env, src) {
- data = data || {}
- let scoped = env.scoped || ''
- let codeCopy = code
- let match;
- src = src.startsWith('http') ? src : scoped + src
-
- const awaitImportRegex = /await import\(['"]([^'"]+)['"]\)/gm;
- while ((match = awaitImportRegex.exec(code)) !== null) {
- let url = match[1]
- if (!url.startsWith('http')) {
- url = resolvePath(url, src)
- url = window.location.origin + url
- }
- codeCopy = codeCopy.replace(match[0], `await import('${url}')`)
- }
-
- const importRegex = /^[\s/]*import\s+([\w{},\s]+)\s+from\s+['"]([^'"]+)['"][;\s]*$/gm;
-
- // 提取所有匹配的模块路径
- while ((match = importRegex.exec(code)) !== null) {
- codeCopy = codeCopy.replace(match[0], '')
- if (match[0].trim().startsWith('//')) {
- continue
- }
- let url = match[2]
- if (!url.startsWith('http') && !url.startsWith('@')) {
- if (url.startsWith('/') && scoped) {
- url = scoped + url
- } else {
- url = resolvePath(url, src)
- }
- }
- if (url.startsWith('@')) {
- url = url.slice(1)
- }
- if (!url.endsWith('.js')) {
- url += '.js'
- }
- if (!url.startsWith('http')) {
- url = window.location.origin + url
- }
- let packname = match[1].trim()
- try {
- let packs = null
- if (/^\w+$/.test(packname)) {
- packs = packname
- } else if (/^{[\w\s,]+}$/.test(packname)) {
- packs = packname.slice(1, -1).split(',').map(p => p.trim())
- } else {
- throw new Error('unsupported import: ' + match[0])
- }
- // window.$env = env
- const module = await import(url)
- if (typeof packs === 'string') {
- if (module.default) {
- data[packs] = module.default
- } else {
- data[packs] = module
- }
- } else {
- packs.forEach((p) => {
- if (p in module) {
- data[p] = module[p]
- } else if (p in module.default) {
- data[p] = module.default[p]
- }
- })
- }
- } catch (error) {
- console.error(`模块加载失败 (${match[0]}):`, error.message);
- }
- }
- return codeCopy.trim()
-}
-
-export default {
- Wrap, Watch, Cancel, ForceUpdate, SetDataRoot,
- DataID, GenUniqueID, Run, AsyncRun, ParseImport
-}
diff --git a/ui/vhtml/vrouter.js b/ui/vhtml/vrouter.js
deleted file mode 100644
index c331276..0000000
--- a/ui/vhtml/vrouter.js
+++ /dev/null
@@ -1,624 +0,0 @@
-import vproxy from './vproxy.js'
-import vget from './vget.js'
-
-// 解析URL字符串,提取路径、查询参数和hash
-function parseUrlString(urlString, scoped) {
- let url
-
- let path
- // 判断是否为完整URL(包含协议)
- if (urlString.startsWith('http://') || urlString.startsWith('https://')) {
- url = new URL(urlString)
- // 如果是外部URL,返回null(不处理外部链接)
- if (url.origin !== window.location.origin) {
- return null
- }
- if (url.pathname.startsWith(scoped)) {
- path = url.pathname.slice(scoped.length) // 去掉根路径
- }
- } else {
- // 相对路径,基于当前origin构建完整URL
- url = new URL(urlString, window.location.href)
- path = url.pathname
- }
-
- // 解析查询参数
- const query = {}
- url.searchParams.forEach((value, key) => {
- query[key] = value
- })
-
- return {
- path: path,
- query,
- hash: url.hash
- }
-}
-
-
-class VRouter {
- #routes = []
- #history = []
- #current = null
- #scoped = ''
- #listeners = []
- #pageCache = new Map()
- #node = null
- #env = null
- #originContent = []
- #loaded = false
- #vhtml = null
- #routesByName = new Map() // 添加按名称索引的路由缓存
-
- constructor() {
- this.init()
- }
-
- get routes() { return this.#routes.slice() }
- get history() { return this.#history.slice() }
- get current() { return this.#current }
- get query() { return this.#current?.query || {} }
- get params() { return this.#current?.params || {} }
- get scoped() { return this.#scoped }
-
- onChange(fc) {
- this.#listeners.push(fc)
- }
-
- addRoute(route) {
- if (!route.path) throw new Error('Route must have a path')
- if (route.path != '/' && route.path.endsWith('/')) {
- route.path = route.path.slice(0, -1)
- }
-
- const routeConfig = {
- path: route.path,
- component: route.component,
- name: route.name,
- meta: route.meta || {},
- children: route.children || [],
- matcher: new RouteMatcher(route.path, route.name),
- description: route.description || '',
- layout: route.layout || '',
- }
-
- this.#routes.push(routeConfig)
-
- // 如果有名称,添加到名称索引中
- if (route.name) {
- this.#routesByName.set(route.name, routeConfig)
- }
-
- // 递归处理子路由
- if (route.children?.length > 0) {
- route.children.forEach(child => {
- const childPath = route.path + (child.path.startsWith('/') ? child.path : '/' + child.path)
- const layout = child.layout || route.layout || ''
- const meta = { ...route.meta, ...child.meta }
- this.addRoute({ ...child, path: childPath, parent: routeConfig, layout, meta })
- })
- }
- }
-
- addRoutes(routes) {
- routes.forEach(route => this.addRoute(route))
- }
-
- #notifyListeners(to, from) {
- this.#listeners.forEach(listener => {
- if (typeof listener === 'function') {
- try {
- listener(to, from)
- } catch (error) {
- console.error('Error in router listener:', error)
- }
- }
- })
- }
-
- #setRouterPath(matchedRoute) {
- const oldRoute = this.#current
-
- this.#current = {
- path: matchedRoute.path,
- fullPath: matchedRoute.fullPath,
- params: matchedRoute.params || {},
- query: matchedRoute.query || {},
- hash: new URL(matchedRoute.fullPath, window.location.origin).hash,
- meta: matchedRoute.route?.meta || {},
- description: matchedRoute.route?.description || '',
- layout: matchedRoute.route?.layout || '',
- name: matchedRoute.route?.name,
- matched: matchedRoute.route ? [matchedRoute.route] : []
- }
-
- this.#history.push(this.#current)
- if (this.#scoped && !matchedRoute.fullPath.startsWith('http')) {
- history.pushState({}, '', this.#scoped + matchedRoute.fullPath)
- } else {
- history.pushState({}, '', matchedRoute.fullPath)
- }
- this.#notifyListeners(this.#current, oldRoute)
- }
-
- // 优化后的路由匹配方法,支持多种参数类型
- matchRoute(to) {
- // 处理不同类型的路由参数
- const routeInfo = this.normalizeRouteTarget(to)
- if (!routeInfo) return null
-
- const { path, query, params, name } = routeInfo
-
- // 如果是按名称匹配
- if (name) {
- const route = this.#routesByName.get(name)
- if (!route) return null
-
- // 构建带参数的路径
- let resolvedPath = route.path
- Object.entries(params).forEach(([key, value]) => {
- resolvedPath = resolvedPath.replace(`:${key}`, value)
- })
-
- const match = route.matcher.match(resolvedPath)
- if (match) {
- return {
- route,
- params: { ...match.params, ...params },
- matched: match.matched,
- path: resolvedPath,
- query,
- name
- }
- }
- return null
- }
-
- // 按路径匹配
- for (const route of this.#routes) {
- const match = route.matcher.match(path)
- if (match && route.component) {
- return {
- route,
- params: { ...match.params, ...params },
- matched: match.matched,
- description: route.description,
- layout: route.layout,
- path,
- query,
- name: route.name
- }
- }
- }
- return null
- }
-
- // 标准化路由目标参数
- normalizeRouteTarget(to) {
- let path, query = {}, params = {}, hash = '', name
-
- if (typeof to === 'string') {
- // 字符串类型:解析可能包含的URL、query、hash
- const parsed = parseUrlString(to, this.#scoped)
- if (!parsed) return null // 外部URL或解析失败
-
- path = parsed.path
- query = { ...parsed.query }
- hash = parsed.hash
- } else if (to && typeof to === 'object') {
- if (to.path) {
- // {path} 类型:path可能也包含query和hash
- const parsed = parseUrlString(to.path, this.#scoped)
- if (!parsed) return null
-
- path = parsed.path
- // 合并query参数,对象中的query优先级更高
- query = { ...parsed.query, ...(to.query || {}) }
- hash = to.hash || parsed.hash
- params = to.params || {}
- } else if (to.name) {
- // {name} 类型
- name = to.name
- query = to.query || {}
- params = to.params || {}
- hash = to.hash || ''
- } else {
- return null
- }
- } else {
- return null
- }
-
- // 标准化路径
- if (path && !path.startsWith('/')) {
- path = '/' + path
- }
- if (this.#scoped) {
- path = path.startsWith(this.#scoped) ? path.slice(this.#scoped.length) : path
- }
-
- if (!path.startsWith('/')) {
- path = '/' + path
- }
-
- if (path != '/' && path.endsWith('/')) {
- path = path.slice(0, -1)
- }
-
- return { path, query, params, hash, name }
- }
-
- matchTo(to) {
- const matchResult = this.matchRoute(to)
- if (!matchResult) return null
-
- const { route, params, query, path, name } = matchResult
-
- // 构建查询字符串
- let search = ''
- if (query && Object.keys(query).length > 0) {
- search = '?' + Object.entries(query)
- .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
- .join('&')
- }
-
- const fullPath = (path || matchResult.path) + search
-
- return {
- route,
- params,
- query,
- name: name || route.name,
- path: path || matchResult.path,
- fullPath,
- matched: [route]
- }
- }
-
- buildUrl(baseUrl, additionalQuery = {}) {
- const url = new URL(baseUrl, window.location.origin)
- Object.entries(additionalQuery).forEach(([key, value]) => {
- url.searchParams.append(key, value)
- })
- return url
- }
-
- resolveRoutePath(route, params = {}) {
- let path = route.component || route.path
-
- Object.entries(params).forEach(([key, value]) => {
- path = path.replace(`:${key}`, value)
- })
-
- if (path === '/' || path === '') path = '/index'
- if (!path.startsWith('/')) path = '/' + path
- if (path.endsWith('.html')) path = path.slice(0, -5)
- if (path.endsWith('/')) path = path.slice(0, -1)
-
- return path
- }
-
- async #navigateTo(matchedRoute) {
- if (!matchedRoute) {
- console.warn(`No route matched`)
- return
- }
-
- const { route, params, query } = matchedRoute
-
- const to = {
- path: matchedRoute.path,
- fullPath: matchedRoute.fullPath,
- params,
- query,
- hash: new URL(matchedRoute.fullPath, window.location.origin).hash,
- meta: route.meta,
- description: route.description,
- layout: route.layout,
- name: route.name,
- matched: [route]
- }
-
- if (this.beforeEnter) {
- try {
- let shouldContinue = true
- const result = await this.beforeEnter(to, this.#current, (next) => {
- if (next) {
- shouldContinue = false
- this.push(next)
- }
- })
- if (result === false || !shouldContinue) return
- } catch (error) {
- console.error('Error in beforeEnter guard:', error)
- return
- }
- }
-
- const cacheKey = matchedRoute.fullPath
- let page = this.#pageCache.get(cacheKey)
-
- this.#setRouterPath(matchedRoute)
- if (page) {
- page.activate()
- } else {
- page = new Page(this.#vhtml, this.#node, matchedRoute)
- await page.mount(this.#env, this.#originContent, to.layout)
- this.#pageCache.set(cacheKey, page)
- }
-
- }
-
- async push(to) {
- const matchedRoute = this.matchTo(to)
-
- if (!matchedRoute) {
- const target = typeof to === 'string' ? to : (to.path || `name: ${to.name}`)
- console.warn(`No route matched for ${target}`)
- return
- }
-
- await this.#navigateTo(matchedRoute)
- }
-
- replace(to) {
- this.push(to)
- if (this.#history.length > 1) {
- this.#history.splice(-2, 1)
- }
- }
-
- go(n) { history.go(n) }
- back() { history.back() }
- forward() { history.forward() }
-
- init() {
- if (this.#loaded) return
- this.#loaded = true
-
- document.body.addEventListener('click', (event) => {
- const linkElement = event.target.closest('a')
- if (!linkElement) return
-
- const href = linkElement.getAttribute('href')
- if (!href || href.startsWith('http') || href.startsWith('#')) return
-
-
- event.preventDefault()
- const reload = linkElement.hasAttribute('reload')
- if (reload) {
- window.location.href = href
- } else {
- this.push(href)
- }
- }, true)
-
- window.addEventListener('popstate', () => {
- this.push(window.location.href)
- })
- }
-
- ParseVrouter($vhtml, $node, env) {
- this.#node = $node
- this.#env = env
- this.#scoped = env.scoped || ''
- this.#originContent = Array.from($node.childNodes)
- this.#vhtml = $vhtml
- this.push(window.location.href)
- }
-}
-
-// 优化后的路由匹配器
-class RouteMatcher {
- constructor(path, name) {
- this.originalPath = path
- this.name = name
- this.keys = []
- this.regexp = this.pathToRegexp(path)
- }
-
- pathToRegexp(path) {
- const paramPattern = /:([^(/]+)/g
- let regexpStr = path.replace(paramPattern, (match, key) => {
- this.keys.push(key)
- return `(?<${key}>[^/]+)`
- })
-
- // 处理 *path 形式的通配符
- regexpStr = regexpStr.replace(/\*(\w+)/g, (match, key) => {
- this.keys.push(key)
- return `(?<${key}>.*)`
- });
-
- // 如果有未处理的*号,将其替换为允许匹配任意数量字符的正则表达式
- regexpStr = regexpStr.replace(/\*/g, '.*')
- return new RegExp(`^${regexpStr}$`)
- }
-
- // 优化的匹配方法,支持多种参数类型
- match(target) {
- let path
-
- // 处理不同类型的输入
- if (typeof target === 'string') {
- path = target
- } else if (target && typeof target === 'object') {
- if (target.path) {
- path = target.path
- } else if (target.name && target.name === this.name) {
- // 如果按名称匹配且名称相符,返回基本匹配
- return {
- path: this.originalPath,
- params: target.params || {},
- matched: this.originalPath
- }
- } else {
- return null
- }
- } else {
- return null
- }
-
- const match = this.regexp.exec(path)
- if (!match) return null
-
- const params = {}
- this.keys.forEach(key => {
- if (match.groups?.[key]) {
- params[key] = match.groups[key]
- }
- })
-
- return {
- path: this.originalPath,
- params,
- matched: match[0]
- }
- }
-}
-
-const layoutCache = new Map()
-
-class Page {
- constructor(vhtml, node, matchedRoute) {
- this.vhtml = vhtml
- this.node = node
- this.layoutDom = undefined
- this.matchedRoute = matchedRoute
- this.htmlPath = this.resolveHtmlPath(matchedRoute)
- }
-
- resolveHtmlPath(matchedRoute) {
- let path = matchedRoute.route.component || matchedRoute.route.path
- if (typeof path === 'function') {
- path = path(matchedRoute.path)
- }
-
- Object.entries(matchedRoute.params).forEach(([key, value]) => {
- path = path.replace(`:${key}`, value)
- })
-
- if (!path.startsWith('/')) path = '/' + path
- if (path.endsWith('/')) path = path.slice(0, -1)
- if (!path.endsWith('.html')) path = path + '.html'
-
- return path
- }
-
- async mount(env, originContent, layout) {
-
- const parser = await vget.FetchUI(this.htmlPath, env)
- if (parser.err) {
- console.warn(parser.err)
- let dom = document.createElement('div')
- Object.assign(dom.style, { width: '100%', height: '100%' })
- dom.append(...originContent)
- this.node.innerHTML = ''
- this.node.append(dom)
- this.vhtml.parseRef(this.htmlPath, dom, {}, env, null, true)
- return
- }
- this.title = parser.title || ''
-
-
- const slots = {}
- const dom = document.createElement("div")
- dom.setAttribute('vsrc', this.htmlPath)
- slots[''] = [dom]
- this.slots = slots
-
- if (!layout) {
- this.node.innerHTML = ''
- this.node.append(dom)
- this.vhtml.parseRef(this.htmlPath, dom, {}, env, null)
- return
- }
-
- let layoutDom = layoutCache.get(layout)
- if (!layoutDom) {
- let layoutUrl = layout
- if (!layoutUrl.startsWith('/')) {
- layoutUrl = '/' + layout
- }
- if (!layoutUrl.endsWith('.html')) {
- layoutUrl += '.html'
- }
- if (!layoutUrl.startsWith('/layout')) {
- layoutUrl = '/layout' + layoutUrl
- }
- const layoutParser = await vget.FetchUI(layoutUrl, env)
- if (layoutParser.err) {
- console.warn(`get layout ${layoutUrl} failed.`, layoutParser.err)
- this.node.innerHTML = ''
- this.node.append(dom)
- this.vhtml.parseRef(this.htmlPath, dom, {}, env, null)
- return
- }
- layoutDom = layoutParser.body.cloneNode(true)
- layoutCache.set(layout, layoutDom)
- dom.$refData = vproxy.Wrap({})
- layoutDom.$refSlots = vproxy.Wrap({ ...slots })
- this.node.innerHTML = ''
- this.node.append(layoutDom)
- this.layoutDom = layoutDom
- this.vhtml.parseRef('/layout/' + layout, layoutDom, {}, env, null, true)
- } else {
-
- this.layoutDom = layoutDom
- this.activate()
- }
- }
-
- activate() {
- if (this.title) document.title = this.title
- const layoutDom = this.layoutDom
-
- if (layoutDom) {
- layoutDom.querySelectorAll("vslot").forEach(e => {
- if (e.closest('[vref]') === layoutDom && this.slots[e.getAttribute('name') || '']) {
- e.innerHTML = ''
- }
- })
- Object.keys(layoutDom.$refSlots).forEach(key => {
- delete layoutDom.$refSlots[key]
- })
- Object.assign(layoutDom.$refSlots, this.slots)
- if (!layoutDom.isConnected) {
- this.node.innerHTML = ''
- }
- this.node.append(layoutDom)
- } else {
- this.node.innerHTML = ''
- const dom = this.slots['']
- if (dom instanceof Array) {
- this.node.append(...dom)
- } else {
- this.node.append(dom)
- }
- }
- }
-}
-
-const $router = new VRouter()
-
-const DefaultRoutes = [
- {
- path: '/',
- component: '/page/index.html',
- name: 'home',
- },
- {
- path: '/404',
- component: '/page/404.html',
- name: '404'
- },
- {
- path: '*',
- component: (path) => {
- if (path.endsWith('.html')) return path
- return '/page' + path + '.html'
- },
- }
-]
-
-export default { $router, DefaultRoutes }