e instanceof xo?e.toJSON():e;function Fs(e,t){t=t||{};const n={};function o(c,f,d){return Be.isPlainObject(c)&&Be.isPlainObject(f)?Be.merge.call({caseless:d},c,f):Be.isPlainObject(f)?Be.merge({},f):Be.isArray(f)?f.slice():f}function r(c,f,d){if(Be.isUndefined(f)){if(!Be.isUndefined(c))return o(void 0,c,d)}else return o(c,f,d)}function a(c,f){if(!Be.isUndefined(f))return o(void 0,f)}function l(c,f){if(Be.isUndefined(f)){if(!Be.isUndefined(c))return o(void 0,c)}else return o(void 0,f)}function s(c,f,d){if(d in t)return o(c,f);if(d in e)return o(void 0,c)}const u={url:a,method:a,data:a,baseURL:l,transformRequest:l,transformResponse:l,paramsSerializer:l,timeout:l,timeoutMessage:l,withCredentials:l,adapter:l,responseType:l,xsrfCookieName:l,xsrfHeaderName:l,onUploadProgress:l,onDownloadProgress:l,decompress:l,maxContentLength:l,maxBodyLength:l,beforeRedirect:l,transport:l,httpAgent:l,httpsAgent:l,cancelToken:l,socketPath:l,responseEncoding:l,validateStatus:s,headers:(c,f)=>r(U1(c),U1(f),!0)};return Be.forEach(Object.keys(Object.assign({},e,t)),function(f){const d=u[f]||r,p=d(e[f],t[f],f);Be.isUndefined(p)&&d!==s||(n[f]=p)}),n}const bT="1.4.0",Ug={};["object","boolean","number","function","string","symbol"].forEach((e,t)=>{Ug[e]=function(o){return typeof o===e||"a"+(t<1?"n ":" ")+e}});const q1={};Ug.transitional=function(t,n,o){function r(a,l){return"[Axios v"+bT+"] Transitional option '"+a+"'"+l+(o?". "+o:"")}return(a,l,s)=>{if(t===!1)throw new jt(r(l," has been removed"+(n?" in "+n:"")),jt.ERR_DEPRECATED);return n&&!q1[l]&&(q1[l]=!0,console.warn(r(l," has been deprecated since v"+n+" and will be removed in the near future"))),t?t(a,l,s):!0}};function qoe(e,t,n){if(typeof e!="object")throw new jt("options must be an object",jt.ERR_BAD_OPTION_VALUE);const o=Object.keys(e);let r=o.length;for(;r-- >0;){const a=o[r],l=t[a];if(l){const s=e[a],u=s===void 0||l(s,a,e);if(u!==!0)throw new jt("option "+a+" must be "+u,jt.ERR_BAD_OPTION_VALUE);continue}if(n!==!0)throw new jt("Unknown option "+a,jt.ERR_BAD_OPTION)}}const Cm={assertOptions:qoe,validators:Ug},ua=Cm.validators;let fl=class{constructor(t){this.defaults=t,this.interceptors={request:new z1,response:new z1}}request(t,n){typeof t=="string"?(n=n||{},n.url=t):n=t||{},n=Fs(this.defaults,n);const{transitional:o,paramsSerializer:r,headers:a}=n;o!==void 0&&Cm.assertOptions(o,{silentJSONParsing:ua.transitional(ua.boolean),forcedJSONParsing:ua.transitional(ua.boolean),clarifyTimeoutError:ua.transitional(ua.boolean)},!1),r!=null&&(Be.isFunction(r)?n.paramsSerializer={serialize:r}:Cm.assertOptions(r,{encode:ua.function,serialize:ua.function},!0)),n.method=(n.method||this.defaults.method||"get").toLowerCase();let l;l=a&&Be.merge(a.common,a[n.method]),l&&Be.forEach(["delete","get","head","post","put","patch","common"],v=>{delete a[v]}),n.headers=xo.concat(l,a);const s=[];let u=!0;this.interceptors.request.forEach(function(h){typeof h.runWhen=="function"&&h.runWhen(n)===!1||(u=u&&h.synchronous,s.unshift(h.fulfilled,h.rejected))});const c=[];this.interceptors.response.forEach(function(h){c.push(h.fulfilled,h.rejected)});let f,d=0,p;if(!u){const v=[K1.bind(this),void 0];for(v.unshift.apply(v,s),v.push.apply(v,c),p=v.length,f=Promise.resolve(n);d{if(!o._listeners)return;let a=o._listeners.length;for(;a-- >0;)o._listeners[a](r);o._listeners=null}),this.promise.then=r=>{let a;const l=new Promise(s=>{o.subscribe(s),a=s}).then(r);return l.cancel=function(){o.unsubscribe(a)},l},t(function(a,l,s){o.reason||(o.reason=new zu(a,l,s),n(o.reason))})}throwIfRequested(){if(this.reason)throw this.reason}subscribe(t){if(this.reason){t(this.reason);return}this._listeners?this._listeners.push(t):this._listeners=[t]}unsubscribe(t){if(!this._listeners)return;const n=this._listeners.indexOf(t);n!==-1&&this._listeners.splice(n,1)}static source(){let t;return{token:new qg(function(r){t=r}),cancel:t}}}function Yoe(e){return function(n){return e.apply(null,n)}}function Goe(e){return Be.isObject(e)&&e.isAxiosError===!0}const Sm={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(Sm).forEach(([e,t])=>{Sm[t]=e});function yT(e){const t=new fl(e),n=tT(fl.prototype.request,t);return Be.extend(n,fl.prototype,t,{allOwnKeys:!0}),Be.extend(n,t,null,{allOwnKeys:!0}),n.create=function(r){return yT(Fs(e,r))},n}const Nn=yT(ai);Nn.Axios=fl;Nn.CanceledError=zu;Nn.CancelToken=qg;Nn.isCancel=vT;Nn.VERSION=bT;Nn.toFormData=Lf;Nn.AxiosError=jt;Nn.Cancel=Nn.CanceledError;Nn.all=function(t){return Promise.all(t)};Nn.spread=Yoe;Nn.isAxiosError=Goe;Nn.mergeConfig=Fs;Nn.AxiosHeaders=xo;Nn.formToJSON=e=>mT(Be.isHTMLForm(e)?new FormData(e):e);Nn.HttpStatusCode=Sm;Nn.default=Nn;const Xoe={Terminal:"终端","Command run log":"命令运行日志","No mission yet":"还没有任务...","Test command":"测试命令","Install dependent packages":"安装依赖包",Republish:"重新发布","Clean up task list":"清理任务列表",unknown:"未知","Waiting for execution":"等待执行",Connecting:"连接中...",Executing:"执行中...","Successful execution":"执行成功","Execution failed":"执行失败","Unknown execution result":"执行结果未知","Are you sure you want to republish?":"确认要重新发布吗?","Failure to execute this command will block the execution of the queue":"本命令执行失败会阻断队列执行","Do not refresh the browser":"请勿刷新浏览器"},Joe=Object.freeze(Object.defineProperty({__proto__:null,default:Xoe},Symbol.toStringTag,{value:"Module"})),Zoe={Terminal:"Terminal","Command run log":"Command run log","No mission yet":"No mission yet","Test command":"Test command","Install dependent packages":"Install the dependent packages",Republish:"Republish","Clean up task list":"Clean up the task list",unknown:"Unknown","Waiting for execution":"Waiting for execution",Connecting:"Connecting",Executing:"Executing","Successful execution":"Execution successful","Execution failed":"Execution failed","Unknown execution result":"Unknown execution result","Are you sure you want to republish?":"Are you sure you want to republish?","Failure to execute this command will block the execution of the queue":"Failure to execute this command will block the execution of the queue.","Do not refresh the browser":"Please do not refresh your browser."},Qoe=Object.freeze(Object.defineProperty({__proto__:null,default:Zoe},Symbol.toStringTag,{value:"Module"}));var Pr={},wT={exports:{}},Ot={},_T={exports:{}},jo={},CT={exports:{}},Xt={};/*!
+ * shared v9.2.2
+ * (c) 2022 kazuya kawaguchi
+ * Released under the MIT License.
+ */Object.defineProperty(Xt,"__esModule",{value:!0});const ere=typeof window<"u";let tre,nre;const ore=/\{([0-9a-zA-Z]+)\}/g;function rre(e,...t){return t.length===1&&Yg(t[0])&&(t=t[0]),(!t||!t.hasOwnProperty)&&(t={}),e.replace(ore,(n,o)=>t.hasOwnProperty(o)?t[o]:"")}const are=typeof Symbol=="function"&&typeof Symbol.toStringTag=="symbol",lre=e=>are?Symbol(e):e,sre=(e,t,n)=>ST({l:e,k:t,s:n}),ST=e=>JSON.stringify(e).replace(/\u2028/g,"\\u2028").replace(/\u2029/g,"\\u2029").replace(/\u0027/g,"\\u0027"),ire=e=>typeof e=="number"&&isFinite(e),ure=e=>xf(e)==="[object Date]",cre=e=>xf(e)==="[object RegExp]",dre=e=>Xg(e)&&Object.keys(e).length===0;function fre(e,t){typeof console<"u"&&(console.warn("[intlify] "+e),t&&console.warn(t.stack))}const pre=Object.assign;let Y1;const hre=()=>Y1||(Y1=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:typeof rr<"u"?rr:{});function mre(e){return e.replace(//g,">").replace(/"/g,""").replace(/'/g,"'")}const vre=Object.prototype.hasOwnProperty;function gre(e,t){return vre.call(e,t)}const kT=Array.isArray,km=e=>typeof e=="function",bre=e=>typeof e=="string",yre=e=>typeof e=="boolean",wre=e=>typeof e=="symbol",Yg=e=>e!==null&&typeof e=="object",_re=e=>Yg(e)&&km(e.then)&&km(e.catch),Gg=Object.prototype.toString,xf=e=>Gg.call(e),Xg=e=>xf(e)==="[object Object]",Cre=e=>e==null?"":kT(e)||Xg(e)&&e.toString===Gg?JSON.stringify(e,null,2):String(e),G1=2;function Sre(e,t=0,n=e.length){const o=e.split(/\r?\n/);let r=0;const a=[];for(let l=0;l=t){for(let s=l-G1;s<=l+G1||n>r;s++){if(s<0||s>=o.length)continue;const u=s+1;a.push(`${u}${" ".repeat(3-String(u).length)}| ${o[s]}`);const c=o[s].length;if(s===l){const f=t-(r-c)+1,d=Math.max(1,n>r?c-f:n-t);a.push(" | "+" ".repeat(f)+"^".repeat(d))}else if(s>l){if(n>r){const f=Math.max(Math.min(n-r,c),1);a.push(" | "+"^".repeat(f))}r+=c+1}}break}return a.join(`
+`)}function kre(){const e=new Map;return{events:e,on(n,o){const r=e.get(n);r&&r.push(o)||e.set(n,[o])},off(n,o){const r=e.get(n);r&&r.splice(r.indexOf(o)>>>0,1)},emit(n,o){(e.get(n)||[]).slice().map(r=>r(o)),(e.get("*")||[]).slice().map(r=>r(n,o))}}}Xt.assign=pre;Xt.createEmitter=kre;Xt.escapeHtml=mre;Xt.format=rre;Xt.friendlyJSONstringify=ST;Xt.generateCodeFrame=Sre;Xt.generateFormatCacheKey=sre;Xt.getGlobalThis=hre;Xt.hasOwn=gre;Xt.inBrowser=ere;Xt.isArray=kT;Xt.isBoolean=yre;Xt.isDate=ure;Xt.isEmptyObject=dre;Xt.isFunction=km;Xt.isNumber=ire;Xt.isObject=Yg;Xt.isPlainObject=Xg;Xt.isPromise=_re;Xt.isRegExp=cre;Xt.isString=bre;Xt.isSymbol=wre;Xt.makeSymbol=lre;Xt.mark=tre;Xt.measure=nre;Xt.objectToString=Gg;Xt.toDisplayString=Cre;Xt.toTypeString=xf;Xt.warn=fre;CT.exports=Xt;var Jg=CT.exports,Df={},Zg={},Ff={},Qg={},X1="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".split("");Qg.encode=function(e){if(0<=e&&e>1;return t?-n:n}Ff.encode=function(t){var n="",o,r=Ere(t);do o=r&$T,r>>>=eb,r>0&&(o|=OT),n+=ET.encode(o);while(r>0);return n};Ff.decode=function(t,n,o){var r=t.length,a=0,l=0,s,u;do{if(n>=r)throw new Error("Expected more digits in base 64 VLQ value.");if(u=ET.decode(t.charCodeAt(n++)),u===-1)throw new Error("Invalid base64 digit: "+t.charAt(n-1));s=!!(u&OT),u&=$T,a=a+(u<=0;M--)O=$[M],O==="."?$.splice(M,1):O===".."?A++:A>0&&(O===""?($.splice(M+1,A),A=0):($.splice(M,2),A--));return w=$.join("/"),w===""&&(w=E?"/":"."),S?(S.path=w,a(S)):w}e.normalize=l;function s(b,w){b===""&&(b="."),w===""&&(w=".");var S=r(w),E=r(b);if(E&&(b=E.path||"/"),S&&!S.scheme)return E&&(S.scheme=E.scheme),a(S);if(S||w.match(o))return w;if(E&&!E.host&&!E.path)return E.host=w,a(E);var $=w.charAt(0)==="/"?w:l(b.replace(/\/+$/,"")+"/"+w);return E?(E.path=$,a(E)):$}e.join=s,e.isAbsolute=function(b){return b.charAt(0)==="/"||n.test(b)};function u(b,w){b===""&&(b="."),b=b.replace(/\/$/,"");for(var S=0;w.indexOf(b+"/")!==0;){var E=b.lastIndexOf("/");if(E<0||(b=b.slice(0,E),b.match(/^([^\/]+:\/)?\/*$/)))return w;++S}return Array(S+1).join("../")+w.substr(b.length+1)}e.relative=u;var c=function(){var b=Object.create(null);return!("__proto__"in b)}();function f(b){return b}function d(b){return m(b)?"$"+b:b}e.toSetString=c?f:d;function p(b){return m(b)?b.slice(1):b}e.fromSetString=c?f:p;function m(b){if(!b)return!1;var w=b.length;if(w<9||b.charCodeAt(w-1)!==95||b.charCodeAt(w-2)!==95||b.charCodeAt(w-3)!==111||b.charCodeAt(w-4)!==116||b.charCodeAt(w-5)!==111||b.charCodeAt(w-6)!==114||b.charCodeAt(w-7)!==112||b.charCodeAt(w-8)!==95||b.charCodeAt(w-9)!==95)return!1;for(var S=w-10;S>=0;S--)if(b.charCodeAt(S)!==36)return!1;return!0}function v(b,w,S){var E=C(b.source,w.source);return E!==0||(E=b.originalLine-w.originalLine,E!==0)||(E=b.originalColumn-w.originalColumn,E!==0||S)||(E=b.generatedColumn-w.generatedColumn,E!==0)||(E=b.generatedLine-w.generatedLine,E!==0)?E:C(b.name,w.name)}e.compareByOriginalPositions=v;function h(b,w,S){var E=b.generatedLine-w.generatedLine;return E!==0||(E=b.generatedColumn-w.generatedColumn,E!==0||S)||(E=C(b.source,w.source),E!==0)||(E=b.originalLine-w.originalLine,E!==0)||(E=b.originalColumn-w.originalColumn,E!==0)?E:C(b.name,w.name)}e.compareByGeneratedPositionsDeflated=h;function C(b,w){return b===w?0:b===null?1:w===null?-1:b>w?1:-1}function g(b,w){var S=b.generatedLine-w.generatedLine;return S!==0||(S=b.generatedColumn-w.generatedColumn,S!==0)||(S=C(b.source,w.source),S!==0)||(S=b.originalLine-w.originalLine,S!==0)||(S=b.originalColumn-w.originalColumn,S!==0)?S:C(b.name,w.name)}e.compareByGeneratedPositionsInflated=g;function y(b){return JSON.parse(b.replace(/^\)]}'[^\n]*\n/,""))}e.parseSourceMapInput=y;function _(b,w,S){if(w=w||"",b&&(b[b.length-1]!=="/"&&w[0]!=="/"&&(b+="/"),w=b+w),S){var E=r(S);if(!E)throw new Error("sourceMapURL could not be parsed");if(E.path){var $=E.path.lastIndexOf("/");$>=0&&(E.path=E.path.substring(0,$+1))}w=s(a(E),w)}return l(w)}e.computeSourceURL=_})(li);var tb={},nb=li,ob=Object.prototype.hasOwnProperty,pl=typeof Map<"u";function Gr(){this._array=[],this._set=pl?new Map:Object.create(null)}Gr.fromArray=function(t,n){for(var o=new Gr,r=0,a=t.length;r=0)return n}else{var o=nb.toSetString(t);if(ob.call(this._set,o))return this._set[o]}throw new Error('"'+t+'" is not in the set.')};Gr.prototype.at=function(t){if(t>=0&&tn||o==n&&a>=r||IT.compareByGeneratedPositionsInflated(e,t)<=0}function Bf(){this._array=[],this._sorted=!0,this._last={generatedLine:-1,generatedColumn:0}}Bf.prototype.unsortedForEach=function(t,n){this._array.forEach(t,n)};Bf.prototype.add=function(t){$re(this._last,t)?(this._last=t,this._array.push(t)):(this._sorted=!1,this._array.push(t))};Bf.prototype.toArray=function(){return this._sorted||(this._array.sort(IT.compareByGeneratedPositionsInflated),this._sorted=!0),this._array};NT.MappingList=Bf;var wi=Ff,yn=li,Ad=tb.ArraySet,Ore=NT.MappingList;function zo(e){e||(e={}),this._file=yn.getArg(e,"file",null),this._sourceRoot=yn.getArg(e,"sourceRoot",null),this._skipValidation=yn.getArg(e,"skipValidation",!1),this._sources=new Ad,this._names=new Ad,this._mappings=new Ore,this._sourcesContents=null}zo.prototype._version=3;zo.fromSourceMap=function(t){var n=t.sourceRoot,o=new zo({file:t.file,sourceRoot:n});return t.eachMapping(function(r){var a={generated:{line:r.generatedLine,column:r.generatedColumn}};r.source!=null&&(a.source=r.source,n!=null&&(a.source=yn.relative(n,a.source)),a.original={line:r.originalLine,column:r.originalColumn},r.name!=null&&(a.name=r.name)),o.addMapping(a)}),t.sources.forEach(function(r){var a=r;n!==null&&(a=yn.relative(n,r)),o._sources.has(a)||o._sources.add(a);var l=t.sourceContentFor(r);l!=null&&o.setSourceContent(r,l)}),o};zo.prototype.addMapping=function(t){var n=yn.getArg(t,"generated"),o=yn.getArg(t,"original",null),r=yn.getArg(t,"source",null),a=yn.getArg(t,"name",null);this._skipValidation||this._validateMapping(n,o,r,a),r!=null&&(r=String(r),this._sources.has(r)||this._sources.add(r)),a!=null&&(a=String(a),this._names.has(a)||this._names.add(a)),this._mappings.add({generatedLine:n.line,generatedColumn:n.column,originalLine:o!=null&&o.line,originalColumn:o!=null&&o.column,source:r,name:a})};zo.prototype.setSourceContent=function(t,n){var o=t;this._sourceRoot!=null&&(o=yn.relative(this._sourceRoot,o)),n!=null?(this._sourcesContents||(this._sourcesContents=Object.create(null)),this._sourcesContents[yn.toSetString(o)]=n):this._sourcesContents&&(delete this._sourcesContents[yn.toSetString(o)],Object.keys(this._sourcesContents).length===0&&(this._sourcesContents=null))};zo.prototype.applySourceMap=function(t,n,o){var r=n;if(n==null){if(t.file==null)throw new Error(`SourceMapGenerator.prototype.applySourceMap requires either an explicit source file, or the source map's "file" property. Both were omitted.`);r=t.file}var a=this._sourceRoot;a!=null&&(r=yn.relative(a,r));var l=new Ad,s=new Ad;this._mappings.unsortedForEach(function(u){if(u.source===r&&u.originalLine!=null){var c=t.originalPositionFor({line:u.originalLine,column:u.originalColumn});c.source!=null&&(u.source=c.source,o!=null&&(u.source=yn.join(o,u.source)),a!=null&&(u.source=yn.relative(a,u.source)),u.originalLine=c.line,u.originalColumn=c.column,c.name!=null&&(u.name=c.name))}var f=u.source;f!=null&&!l.has(f)&&l.add(f);var d=u.name;d!=null&&!s.has(d)&&s.add(d)},this),this._sources=l,this._names=s,t.sources.forEach(function(u){var c=t.sourceContentFor(u);c!=null&&(o!=null&&(u=yn.join(o,u)),a!=null&&(u=yn.relative(a,u)),this.setSourceContent(u,c))},this)};zo.prototype._validateMapping=function(t,n,o,r){if(n&&typeof n.line!="number"&&typeof n.column!="number")throw new Error("original.line and original.column are not numbers -- you probably meant to omit the original mapping entirely and only map the generated position. If so, pass null for the original mapping instead of an object with empty or null values.");if(!(t&&"line"in t&&"column"in t&&t.line>0&&t.column>=0&&!n&&!o&&!r)){if(t&&"line"in t&&"column"in t&&n&&"line"in n&&"column"in n&&t.line>0&&t.column>=0&&n.line>0&&n.column>=0&&o)return;throw new Error("Invalid mapping: "+JSON.stringify({generated:t,source:o,original:n,name:r}))}};zo.prototype._serializeMappings=function(){for(var t=0,n=1,o=0,r=0,a=0,l=0,s="",u,c,f,d,p=this._mappings.toArray(),m=0,v=p.length;m0){if(!yn.compareByGeneratedPositionsInflated(c,p[m-1]))continue;u+=","}u+=wi.encode(c.generatedColumn-t),t=c.generatedColumn,c.source!=null&&(d=this._sources.indexOf(c.source),u+=wi.encode(d-l),l=d,u+=wi.encode(c.originalLine-1-r),r=c.originalLine-1,u+=wi.encode(c.originalColumn-o),o=c.originalColumn,c.name!=null&&(f=this._names.indexOf(c.name),u+=wi.encode(f-a),a=f)),s+=u}return s};zo.prototype._generateSourcesContent=function(t,n){return t.map(function(o){if(!this._sourcesContents)return null;n!=null&&(o=yn.relative(n,o));var r=yn.toSetString(o);return Object.prototype.hasOwnProperty.call(this._sourcesContents,r)?this._sourcesContents[r]:null},this)};zo.prototype.toJSON=function(){var t={version:this._version,sources:this._sources.toArray(),names:this._names.toArray(),mappings:this._serializeMappings()};return this._file!=null&&(t.file=this._file),this._sourceRoot!=null&&(t.sourceRoot=this._sourceRoot),this._sourcesContents&&(t.sourcesContent=this._generateSourcesContent(t.sources,t.sourceRoot)),t};zo.prototype.toString=function(){return JSON.stringify(this.toJSON())};Zg.SourceMapGenerator=zo;var Vf={},MT={};(function(e){e.GREATEST_LOWER_BOUND=1,e.LEAST_UPPER_BOUND=2;function t(n,o,r,a,l,s){var u=Math.floor((o-n)/2)+n,c=l(r,a[u],!0);return c===0?u:c>0?o-u>1?t(u,o,r,a,l,s):s==e.LEAST_UPPER_BOUND?o1?t(n,u,r,a,l,s):s==e.LEAST_UPPER_BOUND?u:n<0?-1:n}e.search=function(o,r,a,l){if(r.length===0)return-1;var s=t(-1,r.length,o,r,a,l||e.GREATEST_LOWER_BOUND);if(s<0)return-1;for(;s-1>=0&&a(r[s],r[s-1],!0)===0;)--s;return s}})(MT);var AT={};function Bp(e,t,n){var o=e[t];e[t]=e[n],e[n]=o}function Nre(e,t){return Math.round(e+Math.random()*(t-e))}function Em(e,t,n,o){if(n=0){var l=this._originalMappings[a];if(t.column===void 0)for(var s=l.originalLine;l&&l.originalLine===s;)r.push({line:ht.getArg(l,"generatedLine",null),column:ht.getArg(l,"generatedColumn",null),lastColumn:ht.getArg(l,"lastGeneratedColumn",null)}),l=this._originalMappings[++a];else for(var u=l.originalColumn;l&&l.originalLine===n&&l.originalColumn==u;)r.push({line:ht.getArg(l,"generatedLine",null),column:ht.getArg(l,"generatedColumn",null),lastColumn:ht.getArg(l,"lastGeneratedColumn",null)}),l=this._originalMappings[++a]}return r};Vf.SourceMapConsumer=cn;function Dn(e,t){var n=e;typeof e=="string"&&(n=ht.parseSourceMapInput(e));var o=ht.getArg(n,"version"),r=ht.getArg(n,"sources"),a=ht.getArg(n,"names",[]),l=ht.getArg(n,"sourceRoot",null),s=ht.getArg(n,"sourcesContent",null),u=ht.getArg(n,"mappings"),c=ht.getArg(n,"file",null);if(o!=this._version)throw new Error("Unsupported version: "+o);l&&(l=ht.normalize(l)),r=r.map(String).map(ht.normalize).map(function(f){return l&&ht.isAbsolute(l)&&ht.isAbsolute(f)?ht.relative(l,f):f}),this._names=Bs.fromArray(a.map(String),!0),this._sources=Bs.fromArray(r,!0),this._absoluteSources=this._sources.toArray().map(function(f){return ht.computeSourceURL(l,f,t)}),this.sourceRoot=l,this.sourcesContent=s,this._mappings=u,this._sourceMapURL=t,this.file=c}Dn.prototype=Object.create(cn.prototype);Dn.prototype.consumer=cn;Dn.prototype._findSourceIndex=function(e){var t=e;if(this.sourceRoot!=null&&(t=ht.relative(this.sourceRoot,t)),this._sources.has(t))return this._sources.indexOf(t);var n;for(n=0;n1&&(h.source=s+g[1],s+=g[1],h.originalLine=a+g[2],a=h.originalLine,h.originalLine+=1,h.originalColumn=l+g[3],l=h.originalColumn,g.length>4&&(h.name=u+g[4],u+=g[4])),v.push(h),typeof h.originalLine=="number"&&m.push(h)}bu(v,ht.compareByGeneratedPositionsDeflated),this.__generatedMappings=v,bu(m,ht.compareByOriginalPositions),this.__originalMappings=m};Dn.prototype._findMapping=function(t,n,o,r,a,l){if(t[o]<=0)throw new TypeError("Line must be greater than or equal to 1, got "+t[o]);if(t[r]<0)throw new TypeError("Column must be greater than or equal to 0, got "+t[r]);return rb.search(t,n,a,l)};Dn.prototype.computeColumnSpans=function(){for(var t=0;t=0){var r=this._generatedMappings[o];if(r.generatedLine===n.generatedLine){var a=ht.getArg(r,"source",null);a!==null&&(a=this._sources.at(a),a=ht.computeSourceURL(this.sourceRoot,a,this._sourceMapURL));var l=ht.getArg(r,"name",null);return l!==null&&(l=this._names.at(l)),{source:a,line:ht.getArg(r,"originalLine",null),column:ht.getArg(r,"originalColumn",null),name:l}}}return{source:null,line:null,column:null,name:null}};Dn.prototype.hasContentsOfAllSources=function(){return this.sourcesContent?this.sourcesContent.length>=this._sources.size()&&!this.sourcesContent.some(function(t){return t==null}):!1};Dn.prototype.sourceContentFor=function(t,n){if(!this.sourcesContent)return null;var o=this._findSourceIndex(t);if(o>=0)return this.sourcesContent[o];var r=t;this.sourceRoot!=null&&(r=ht.relative(this.sourceRoot,r));var a;if(this.sourceRoot!=null&&(a=ht.urlParse(this.sourceRoot))){var l=r.replace(/^file:\/\//,"");if(a.scheme=="file"&&this._sources.has(l))return this.sourcesContent[this._sources.indexOf(l)];if((!a.path||a.path=="/")&&this._sources.has("/"+r))return this.sourcesContent[this._sources.indexOf("/"+r)]}if(n)return null;throw new Error('"'+r+'" is not in the SourceMap.')};Dn.prototype.generatedPositionFor=function(t){var n=ht.getArg(t,"source");if(n=this._findSourceIndex(n),n<0)return{line:null,column:null,lastColumn:null};var o={source:n,originalLine:ht.getArg(t,"line"),originalColumn:ht.getArg(t,"column")},r=this._findMapping(o,this._originalMappings,"originalLine","originalColumn",ht.compareByOriginalPositions,ht.getArg(t,"bias",cn.GREATEST_LOWER_BOUND));if(r>=0){var a=this._originalMappings[r];if(a.source===o.source)return{line:ht.getArg(a,"generatedLine",null),column:ht.getArg(a,"generatedColumn",null),lastColumn:ht.getArg(a,"lastGeneratedColumn",null)}}return{line:null,column:null,lastColumn:null}};Vf.BasicSourceMapConsumer=Dn;function dr(e,t){var n=e;typeof e=="string"&&(n=ht.parseSourceMapInput(e));var o=ht.getArg(n,"version"),r=ht.getArg(n,"sections");if(o!=this._version)throw new Error("Unsupported version: "+o);this._sources=new Bs,this._names=new Bs;var a={line:-1,column:0};this._sections=r.map(function(l){if(l.url)throw new Error("Support for url field in sections not implemented.");var s=ht.getArg(l,"offset"),u=ht.getArg(s,"line"),c=ht.getArg(s,"column");if(u=0;n--)this.prepend(t[n]);else if(t[si]||typeof t=="string")this.children.unshift(t);else throw new TypeError("Expected a SourceNode, string, or an array of SourceNodes and strings. Got "+t);return this};Co.prototype.walk=function(t){for(var n,o=0,r=this.children.length;o0){for(n=[],o=0;ot[E]===Dre&&t[E+1]===Xn,s=E=>t[E]===Xn,u=E=>t[E]===Bre,c=E=>t[E]===Fre,f=E=>l(E)||s(E)||u(E)||c(E),d=()=>n,p=()=>o,m=()=>r,v=()=>a,h=E=>l(E)||u(E)||c(E)?Xn:t[E],C=()=>h(n),g=()=>h(n+a);function y(){return a=0,f(n)&&(o++,r=0),l(n)&&n++,n++,r++,t[n]}function _(){return l(n+a)&&a++,a++,t[n+a]}function b(){n=0,o=1,r=1,a=0}function w(E=0){a=E}function S(){const E=n+a;for(;E!==n;)y();a=0}return{index:d,line:p,column:m,peekOffset:v,charAt:h,currentChar:C,currentPeek:g,next:y,peek:_,reset:b,resetPeek:w,skipToPeek:S}}const ca=void 0,J1="'",Hre="tokenizer";function zre(e,t={}){const n=t.location!==!1,o=Vre(e),r=()=>o.index(),a=()=>xT(o.line(),o.column(),o.index()),l=a(),s=r(),u={currentType:14,offset:s,startLoc:l,endLoc:l,lastType:14,lastOffset:s,lastStartLoc:l,lastEndLoc:l,braceNest:0,inLinked:!1,text:""},c=()=>u,{onError:f}=t;function d(B,z,Z,...ue){const se=c();if(z.column+=Z,z.offset+=Z,f){const me=Ld(se.startLoc,z),_e=ab(B,me,{domain:Hre,args:ue});f(_e)}}function p(B,z,Z){B.endLoc=a(),B.currentType=z;const ue={type:z};return n&&(ue.loc=Ld(B.startLoc,B.endLoc)),Z!=null&&(ue.value=Z),ue}const m=B=>p(B,14);function v(B,z){return B.currentChar()===z?(B.next(),z):(d(Nt.EXPECTED_TOKEN,a(),0,z),"")}function h(B){let z="";for(;B.currentPeek()===Dr||B.currentPeek()===Xn;)z+=B.currentPeek(),B.peek();return z}function C(B){const z=h(B);return B.skipToPeek(),z}function g(B){if(B===ca)return!1;const z=B.charCodeAt(0);return z>=97&&z<=122||z>=65&&z<=90||z===95}function y(B){if(B===ca)return!1;const z=B.charCodeAt(0);return z>=48&&z<=57}function _(B,z){const{currentType:Z}=z;if(Z!==2)return!1;h(B);const ue=g(B.currentPeek());return B.resetPeek(),ue}function b(B,z){const{currentType:Z}=z;if(Z!==2)return!1;h(B);const ue=B.currentPeek()==="-"?B.peek():B.currentPeek(),se=y(ue);return B.resetPeek(),se}function w(B,z){const{currentType:Z}=z;if(Z!==2)return!1;h(B);const ue=B.currentPeek()===J1;return B.resetPeek(),ue}function S(B,z){const{currentType:Z}=z;if(Z!==8)return!1;h(B);const ue=B.currentPeek()===".";return B.resetPeek(),ue}function E(B,z){const{currentType:Z}=z;if(Z!==9)return!1;h(B);const ue=g(B.currentPeek());return B.resetPeek(),ue}function $(B,z){const{currentType:Z}=z;if(!(Z===8||Z===12))return!1;h(B);const ue=B.currentPeek()===":";return B.resetPeek(),ue}function O(B,z){const{currentType:Z}=z;if(Z!==10)return!1;const ue=()=>{const me=B.currentPeek();return me==="{"?g(B.peek()):me==="@"||me==="%"||me==="|"||me===":"||me==="."||me===Dr||!me?!1:me===Xn?(B.peek(),ue()):g(me)},se=ue();return B.resetPeek(),se}function A(B){h(B);const z=B.currentPeek()==="|";return B.resetPeek(),z}function M(B){const z=h(B),Z=B.currentPeek()==="%"&&B.peek()==="{";return B.resetPeek(),{isModulo:Z,hasSpace:z.length>0}}function D(B,z=!0){const Z=(se=!1,me="",_e=!1)=>{const $e=B.currentPeek();return $e==="{"?me==="%"?!1:se:$e==="@"||!$e?me==="%"?!0:se:$e==="%"?(B.peek(),Z(se,"%",!0)):$e==="|"?me==="%"||_e?!0:!(me===Dr||me===Xn):$e===Dr?(B.peek(),Z(!0,Dr,_e)):$e===Xn?(B.peek(),Z(!0,Xn,_e)):!0},ue=Z();return z&&B.resetPeek(),ue}function U(B,z){const Z=B.currentChar();return Z===ca?ca:z(Z)?(B.next(),Z):null}function j(B){return U(B,Z=>{const ue=Z.charCodeAt(0);return ue>=97&&ue<=122||ue>=65&&ue<=90||ue>=48&&ue<=57||ue===95||ue===36})}function W(B){return U(B,Z=>{const ue=Z.charCodeAt(0);return ue>=48&&ue<=57})}function L(B){return U(B,Z=>{const ue=Z.charCodeAt(0);return ue>=48&&ue<=57||ue>=65&&ue<=70||ue>=97&&ue<=102})}function P(B){let z="",Z="";for(;z=W(B);)Z+=z;return Z}function x(B){C(B);const z=B.currentChar();return z!=="%"&&d(Nt.EXPECTED_TOKEN,a(),0,z),B.next(),"%"}function I(B){let z="";for(;;){const Z=B.currentChar();if(Z==="{"||Z==="}"||Z==="@"||Z==="|"||!Z)break;if(Z==="%")if(D(B))z+=Z,B.next();else break;else if(Z===Dr||Z===Xn)if(D(B))z+=Z,B.next();else{if(A(B))break;z+=Z,B.next()}else z+=Z,B.next()}return z}function H(B){C(B);let z="",Z="";for(;z=j(B);)Z+=z;return B.currentChar()===ca&&d(Nt.UNTERMINATED_CLOSING_BRACE,a(),0),Z}function G(B){C(B);let z="";return B.currentChar()==="-"?(B.next(),z+=`-${P(B)}`):z+=P(B),B.currentChar()===ca&&d(Nt.UNTERMINATED_CLOSING_BRACE,a(),0),z}function J(B){C(B),v(B,"'");let z="",Z="";const ue=me=>me!==J1&&me!==Xn;for(;z=U(B,ue);)z==="\\"?Z+=ee(B):Z+=z;const se=B.currentChar();return se===Xn||se===ca?(d(Nt.UNTERMINATED_SINGLE_QUOTE_IN_PLACEHOLDER,a(),0),se===Xn&&(B.next(),v(B,"'")),Z):(v(B,"'"),Z)}function ee(B){const z=B.currentChar();switch(z){case"\\":case"'":return B.next(),`\\${z}`;case"u":return fe(B,z,4);case"U":return fe(B,z,6);default:return d(Nt.UNKNOWN_ESCAPE_SEQUENCE,a(),0,z),""}}function fe(B,z,Z){v(B,z);let ue="";for(let se=0;sese!=="{"&&se!=="}"&&se!==Dr&&se!==Xn;for(;z=U(B,ue);)Z+=z;return Z}function oe(B){let z="",Z="";for(;z=j(B);)Z+=z;return Z}function ke(B){const z=(Z=!1,ue)=>{const se=B.currentChar();return se==="{"||se==="%"||se==="@"||se==="|"||!se||se===Dr?ue:se===Xn?(ue+=se,B.next(),z(Z,ue)):(ue+=se,B.next(),z(!0,ue))};return z(!1,"")}function ae(B){C(B);const z=v(B,"|");return C(B),z}function Oe(B,z){let Z=null;switch(B.currentChar()){case"{":return z.braceNest>=1&&d(Nt.NOT_ALLOW_NEST_PLACEHOLDER,a(),0),B.next(),Z=p(z,2,"{"),C(B),z.braceNest++,Z;case"}":return z.braceNest>0&&z.currentType===2&&d(Nt.EMPTY_PLACEHOLDER,a(),0),B.next(),Z=p(z,3,"}"),z.braceNest--,z.braceNest>0&&C(B),z.inLinked&&z.braceNest===0&&(z.inLinked=!1),Z;case"@":return z.braceNest>0&&d(Nt.UNTERMINATED_CLOSING_BRACE,a(),0),Z=we(B,z)||m(z),z.braceNest=0,Z;default:let se=!0,me=!0,_e=!0;if(A(B))return z.braceNest>0&&d(Nt.UNTERMINATED_CLOSING_BRACE,a(),0),Z=p(z,1,ae(B)),z.braceNest=0,z.inLinked=!1,Z;if(z.braceNest>0&&(z.currentType===5||z.currentType===6||z.currentType===7))return d(Nt.UNTERMINATED_CLOSING_BRACE,a(),0),z.braceNest=0,ge(B,z);if(se=_(B,z))return Z=p(z,5,H(B)),C(B),Z;if(me=b(B,z))return Z=p(z,6,G(B)),C(B),Z;if(_e=w(B,z))return Z=p(z,7,J(B)),C(B),Z;if(!se&&!me&&!_e)return Z=p(z,13,Te(B)),d(Nt.INVALID_TOKEN_IN_PLACEHOLDER,a(),0,Z.value),C(B),Z;break}return Z}function we(B,z){const{currentType:Z}=z;let ue=null;const se=B.currentChar();switch((Z===8||Z===9||Z===12||Z===10)&&(se===Xn||se===Dr)&&d(Nt.INVALID_LINKED_FORMAT,a(),0),se){case"@":return B.next(),ue=p(z,8,"@"),z.inLinked=!0,ue;case".":return C(B),B.next(),p(z,9,".");case":":return C(B),B.next(),p(z,10,":");default:return A(B)?(ue=p(z,1,ae(B)),z.braceNest=0,z.inLinked=!1,ue):S(B,z)||$(B,z)?(C(B),we(B,z)):E(B,z)?(C(B),p(z,12,oe(B))):O(B,z)?(C(B),se==="{"?Oe(B,z)||ue:p(z,11,ke(B))):(Z===8&&d(Nt.INVALID_LINKED_FORMAT,a(),0),z.braceNest=0,z.inLinked=!1,ge(B,z))}}function ge(B,z){let Z={type:14};if(z.braceNest>0)return Oe(B,z)||m(z);if(z.inLinked)return we(B,z)||m(z);switch(B.currentChar()){case"{":return Oe(B,z)||m(z);case"}":return d(Nt.UNBALANCED_CLOSING_BRACE,a(),0),B.next(),p(z,3,"}");case"@":return we(B,z)||m(z);default:if(A(B))return Z=p(z,1,ae(B)),z.braceNest=0,z.inLinked=!1,Z;const{isModulo:se,hasSpace:me}=M(B);if(se)return me?p(z,0,I(B)):p(z,4,x(B));if(D(B))return p(z,0,I(B));break}return Z}function q(){const{currentType:B,offset:z,startLoc:Z,endLoc:ue}=u;return u.lastType=B,u.lastOffset=z,u.lastStartLoc=Z,u.lastEndLoc=ue,u.offset=r(),u.startLoc=a(),o.currentChar()===ca?p(u,14):ge(o,u)}return{nextToken:q,currentOffset:r,currentPosition:a,context:c}}const DT="parser",jre=/(?:\\\\|\\'|\\u([0-9a-fA-F]{4})|\\U([0-9a-fA-F]{6}))/g;function Wre(e,t,n){switch(e){case"\\\\":return"\\";case"\\'":return"'";default:{const o=parseInt(t||n,16);return o<=55295||o>=57344?String.fromCodePoint(o):"�"}}}function FT(e={}){const t=e.location!==!1,{onError:n}=e;function o(g,y,_,b,...w){const S=g.currentPosition();if(S.offset+=b,S.column+=b,n){const E=Ld(_,S),$=ab(y,E,{domain:DT,args:w});n($)}}function r(g,y,_){const b={type:g,start:y,end:y};return t&&(b.loc={start:_,end:_}),b}function a(g,y,_,b){g.end=y,t&&g.loc&&(g.loc.end=_)}function l(g,y){const _=g.context(),b=r(3,_.offset,_.startLoc);return b.value=y,a(b,g.currentOffset(),g.currentPosition()),b}function s(g,y){const _=g.context(),{lastOffset:b,lastStartLoc:w}=_,S=r(5,b,w);return S.index=parseInt(y,10),g.nextToken(),a(S,g.currentOffset(),g.currentPosition()),S}function u(g,y){const _=g.context(),{lastOffset:b,lastStartLoc:w}=_,S=r(4,b,w);return S.key=y,g.nextToken(),a(S,g.currentOffset(),g.currentPosition()),S}function c(g,y){const _=g.context(),{lastOffset:b,lastStartLoc:w}=_,S=r(9,b,w);return S.value=y.replace(jre,Wre),g.nextToken(),a(S,g.currentOffset(),g.currentPosition()),S}function f(g){const y=g.nextToken(),_=g.context(),{lastOffset:b,lastStartLoc:w}=_,S=r(8,b,w);return y.type!==12?(o(g,Nt.UNEXPECTED_EMPTY_LINKED_MODIFIER,_.lastStartLoc,0),S.value="",a(S,b,w),{nextConsumeToken:y,node:S}):(y.value==null&&o(g,Nt.UNEXPECTED_LEXICAL_ANALYSIS,_.lastStartLoc,0,fr(y)),S.value=y.value||"",a(S,g.currentOffset(),g.currentPosition()),{node:S})}function d(g,y){const _=g.context(),b=r(7,_.offset,_.startLoc);return b.value=y,a(b,g.currentOffset(),g.currentPosition()),b}function p(g){const y=g.context(),_=r(6,y.offset,y.startLoc);let b=g.nextToken();if(b.type===9){const w=f(g);_.modifier=w.node,b=w.nextConsumeToken||g.nextToken()}switch(b.type!==10&&o(g,Nt.UNEXPECTED_LEXICAL_ANALYSIS,y.lastStartLoc,0,fr(b)),b=g.nextToken(),b.type===2&&(b=g.nextToken()),b.type){case 11:b.value==null&&o(g,Nt.UNEXPECTED_LEXICAL_ANALYSIS,y.lastStartLoc,0,fr(b)),_.key=d(g,b.value||"");break;case 5:b.value==null&&o(g,Nt.UNEXPECTED_LEXICAL_ANALYSIS,y.lastStartLoc,0,fr(b)),_.key=u(g,b.value||"");break;case 6:b.value==null&&o(g,Nt.UNEXPECTED_LEXICAL_ANALYSIS,y.lastStartLoc,0,fr(b)),_.key=s(g,b.value||"");break;case 7:b.value==null&&o(g,Nt.UNEXPECTED_LEXICAL_ANALYSIS,y.lastStartLoc,0,fr(b)),_.key=c(g,b.value||"");break;default:o(g,Nt.UNEXPECTED_EMPTY_LINKED_KEY,y.lastStartLoc,0);const w=g.context(),S=r(7,w.offset,w.startLoc);return S.value="",a(S,w.offset,w.startLoc),_.key=S,a(_,w.offset,w.startLoc),{nextConsumeToken:b,node:_}}return a(_,g.currentOffset(),g.currentPosition()),{node:_}}function m(g){const y=g.context(),_=y.currentType===1?g.currentOffset():y.offset,b=y.currentType===1?y.endLoc:y.startLoc,w=r(2,_,b);w.items=[];let S=null;do{const O=S||g.nextToken();switch(S=null,O.type){case 0:O.value==null&&o(g,Nt.UNEXPECTED_LEXICAL_ANALYSIS,y.lastStartLoc,0,fr(O)),w.items.push(l(g,O.value||""));break;case 6:O.value==null&&o(g,Nt.UNEXPECTED_LEXICAL_ANALYSIS,y.lastStartLoc,0,fr(O)),w.items.push(s(g,O.value||""));break;case 5:O.value==null&&o(g,Nt.UNEXPECTED_LEXICAL_ANALYSIS,y.lastStartLoc,0,fr(O)),w.items.push(u(g,O.value||""));break;case 7:O.value==null&&o(g,Nt.UNEXPECTED_LEXICAL_ANALYSIS,y.lastStartLoc,0,fr(O)),w.items.push(c(g,O.value||""));break;case 8:const A=p(g);w.items.push(A.node),S=A.nextConsumeToken||null;break}}while(y.currentType!==14&&y.currentType!==1);const E=y.currentType===1?y.lastOffset:g.currentOffset(),$=y.currentType===1?y.lastEndLoc:g.currentPosition();return a(w,E,$),w}function v(g,y,_,b){const w=g.context();let S=b.items.length===0;const E=r(1,y,_);E.cases=[],E.cases.push(b);do{const $=m(g);S||(S=$.items.length===0),E.cases.push($)}while(w.currentType!==14);return S&&o(g,Nt.MUST_HAVE_MESSAGES_IN_PLURAL,_,0),a(E,g.currentOffset(),g.currentPosition()),E}function h(g){const y=g.context(),{offset:_,startLoc:b}=y,w=m(g);return y.currentType===14?w:v(g,_,b,w)}function C(g){const y=zre(g,Rd.assign({},e)),_=y.context(),b=r(0,_.offset,_.startLoc);return t&&b.loc&&(b.loc.source=g),b.body=h(y),_.currentType!==14&&o(y,Nt.UNEXPECTED_LEXICAL_ANALYSIS,_.lastStartLoc,0,g[_.offset]||""),a(b,y.currentOffset(),y.currentPosition()),b}return{parse:C}}function fr(e){if(e.type===14)return"EOF";const t=(e.value||"").replace(/\r?\n/gu,"\\n");return t.length>10?t.slice(0,9)+"…":t}function Kre(e,t={}){const n={ast:e,helpers:new Set};return{context:()=>n,helper:a=>(n.helpers.add(a),a)}}function Z1(e,t){for(let n=0;nl;function u(C,g){l.code+=C,l.map&&(g&&g.loc&&g.loc!==LT&&h(g.loc.start,Qre(g)),eae(l,C))}function c(C,g=!0){const y=g?r:"";u(a?y+" ".repeat(C):y)}function f(C=!0){const g=++l.indentLevel;C&&c(g)}function d(C=!0){const g=--l.indentLevel;C&&c(g)}function p(){c(l.indentLevel)}const m=C=>`_${C}`,v=()=>l.needIndent;function h(C,g){l.map.addMapping({name:g,source:l.filename,original:{line:C.line,column:C.column-1},generated:{line:l.line,column:l.column-1}})}return n&&(l.map=new Rre.SourceMapGenerator,l.map.setSourceContent(o,l.source)),{context:s,push:u,indent:f,deindent:d,newline:p,helper:m,needIndent:v}}function Yre(e,t){const{helper:n}=e;e.push(`${n("linked")}(`),Vs(e,t.key),t.modifier?(e.push(", "),Vs(e,t.modifier),e.push(", _type")):e.push(", undefined, _type"),e.push(")")}function Gre(e,t){const{helper:n,needIndent:o}=e;e.push(`${n("normalize")}([`),e.indent(o());const r=t.items.length;for(let a=0;a1){e.push(`${n("plural")}([`),e.indent(o());const r=t.cases.length;for(let a=0;a{const n=Rd.isString(t.mode)?t.mode:"normal",o=Rd.isString(t.filename)?t.filename:"message.intl",r=!!t.sourceMap,a=t.breakLineCode!=null?t.breakLineCode:n==="arrow"?";":`
+`,l=t.needIndent?t.needIndent:n!=="arrow",s=e.helpers||[],u=qre(e,{mode:n,filename:o,sourceMap:r,breakLineCode:a,needIndent:l});u.push(n==="normal"?"function __msg__ (ctx) {":"(ctx) => {"),u.indent(l),s.length>0&&(u.push(`const { ${s.map(d=>`${d}: _${d}`).join(", ")} } = ctx`),u.newline()),u.push("return "),Vs(u,e),u.deindent(l),u.push("}");const{code:c,map:f}=u.context();return{ast:e,code:c,map:f?f.toJSON():void 0}};function Qre(e){switch(e.type){case 3:case 9:case 8:case 7:return e.value;case 5:return e.index.toString();case 4:return e.key;default:return}}function eae(e,t,n=t.length){let o=0,r=-1;for(let a=0;a{l===void 0?l=s:l+=s},p[1]=()=>{l!==void 0&&(t.push(l),l=void 0)},p[2]=()=>{p[0](),r++},p[3]=()=>{if(r>0)r--,o=4,p[0]();else{if(r=0,l===void 0||(l=uae(l),l===!1))return!1;p[1]()}};function m(){const v=e[n+1];if(o===5&&v==="'"||o===6&&v==='"')return n++,s="\\"+v,p[0](),!0}for(;o!==null;)if(n++,a=e[n],!(a==="\\"&&m())){if(u=iae(a),d=Va[o],c=d[u]||d.l||8,c===8||(o=c[0],c[1]!==void 0&&(f=p[c[1]],f&&(s=a,f()===!1))))return;if(o===7)return t}}const Q1=new Map;function zT(e,t){return Ae.isObject(e)?e[t]:null}function cae(e,t){if(!Ae.isObject(e))return null;let n=Q1.get(t);if(n||(n=HT(t),n&&Q1.set(t,n)),!n)return null;const o=n.length;let r=e,a=0;for(;ae,fae=e=>"",jT="text",pae=e=>e.length===0?"":e.join(""),hae=Ae.toDisplayString;function ew(e,t){return e=Math.abs(e),t===2?e?e>1?1:0:1:e?Math.min(e,2):0}function mae(e){const t=Ae.isNumber(e.pluralIndex)?e.pluralIndex:-1;return e.named&&(Ae.isNumber(e.named.count)||Ae.isNumber(e.named.n))?Ae.isNumber(e.named.count)?e.named.count:Ae.isNumber(e.named.n)?e.named.n:t:t}function vae(e,t){t.count||(t.count=e),t.n||(t.n=e)}function WT(e={}){const t=e.locale,n=mae(e),o=Ae.isObject(e.pluralRules)&&Ae.isString(t)&&Ae.isFunction(e.pluralRules[t])?e.pluralRules[t]:ew,r=Ae.isObject(e.pluralRules)&&Ae.isString(t)&&Ae.isFunction(e.pluralRules[t])?ew:void 0,a=g=>g[o(n,g.length,r)],l=e.list||[],s=g=>l[g],u=e.named||{};Ae.isNumber(e.pluralIndex)&&vae(n,u);const c=g=>u[g];function f(g){const y=Ae.isFunction(e.messages)?e.messages(g):Ae.isObject(e.messages)?e.messages[g]:!1;return y||(e.parent?e.parent.message(g):fae)}const d=g=>e.modifiers?e.modifiers[g]:dae,p=Ae.isPlainObject(e.processor)&&Ae.isFunction(e.processor.normalize)?e.processor.normalize:pae,m=Ae.isPlainObject(e.processor)&&Ae.isFunction(e.processor.interpolate)?e.processor.interpolate:hae,v=Ae.isPlainObject(e.processor)&&Ae.isString(e.processor.type)?e.processor.type:jT,C={list:s,named:c,plural:a,linked:(g,...y)=>{const[_,b]=y;let w="text",S="";y.length===1?Ae.isObject(_)?(S=_.modifier||S,w=_.type||w):Ae.isString(_)&&(S=_||S):y.length===2&&(Ae.isString(_)&&(S=_||S),Ae.isString(b)&&(w=b||w));let E=f(g)(C);return w==="vnode"&&Ae.isArray(E)&&S&&(E=E[0]),S?d(S)(E,w):E},message:f,type:v,interpolate:m,normalize:p};return C}let zs=null;function gae(e){zs=e}function bae(){return zs}function yae(e,t,n){zs&&zs.emit(VT.IntlifyDevToolsHooks.I18nInit,{timestamp:Date.now(),i18n:e,version:t,meta:n})}const wae=_ae(VT.IntlifyDevToolsHooks.FunctionTranslate);function _ae(e){return t=>zs&&zs.emit(e,t)}const Ka={NOT_FOUND_KEY:1,FALLBACK_TO_TRANSLATE:2,CANNOT_FORMAT_NUMBER:3,FALLBACK_TO_NUMBER_FORMAT:4,CANNOT_FORMAT_DATE:5,FALLBACK_TO_DATE_FORMAT:6,__EXTEND_POINT__:7},Cae={[Ka.NOT_FOUND_KEY]:"Not found '{key}' key in '{locale}' locale messages.",[Ka.FALLBACK_TO_TRANSLATE]:"Fall back to translate '{key}' key with '{target}' locale.",[Ka.CANNOT_FORMAT_NUMBER]:"Cannot format a number value due to not supported Intl.NumberFormat.",[Ka.FALLBACK_TO_NUMBER_FORMAT]:"Fall back to number format '{key}' key with '{target}' locale.",[Ka.CANNOT_FORMAT_DATE]:"Cannot format a date value due to not supported Intl.DateTimeFormat.",[Ka.FALLBACK_TO_DATE_FORMAT]:"Fall back to datetime format '{key}' key with '{target}' locale."};function Sae(e,...t){return Ae.format(Cae[e],...t)}function KT(e,t,n){return[...new Set([n,...Ae.isArray(t)?t:Ae.isObject(t)?Object.keys(t):Ae.isString(t)?[t]:[n]])]}function kae(e,t,n){const o=Ae.isString(n)?n:ib,r=e;r.__localeChainCache||(r.__localeChainCache=new Map);let a=r.__localeChainCache.get(o);if(!a){a=[];let l=[n];for(;Ae.isArray(l);)l=tw(a,l,t);const s=Ae.isArray(t)||!Ae.isPlainObject(t)?t:t.default?t.default:null;l=Ae.isString(s)?[s]:s,Ae.isArray(l)&&tw(a,l,!1),r.__localeChainCache.set(o,a)}return a}function tw(e,t,n){let o=!0;for(let r=0;r`${e.charAt(0).toLocaleUpperCase()}${e.substr(1)}`;function Oae(){return{upper:(e,t)=>t==="text"&&Ae.isString(e)?e.toUpperCase():t==="vnode"&&Ae.isObject(e)&&"__v_isVNode"in e?e.children.toUpperCase():e,lower:(e,t)=>t==="text"&&Ae.isString(e)?e.toLowerCase():t==="vnode"&&Ae.isObject(e)&&"__v_isVNode"in e?e.children.toLowerCase():e,capitalize:(e,t)=>t==="text"&&Ae.isString(e)?nw(e):t==="vnode"&&Ae.isObject(e)&&"__v_isVNode"in e?nw(e.children):e}}let qT;function Nae(e){qT=e}let YT;function Iae(e){YT=e}let GT;function Mae(e){GT=e}let XT=null;const Aae=e=>{XT=e},Pae=()=>XT;let JT=null;const Rae=e=>{JT=e},Lae=()=>JT;let ow=0;function xae(e={}){const t=Ae.isString(e.version)?e.version:UT,n=Ae.isString(e.locale)?e.locale:ib,o=Ae.isArray(e.fallbackLocale)||Ae.isPlainObject(e.fallbackLocale)||Ae.isString(e.fallbackLocale)||e.fallbackLocale===!1?e.fallbackLocale:n,r=Ae.isPlainObject(e.messages)?e.messages:{[n]:{}},a=Ae.isPlainObject(e.datetimeFormats)?e.datetimeFormats:{[n]:{}},l=Ae.isPlainObject(e.numberFormats)?e.numberFormats:{[n]:{}},s=Ae.assign({},e.modifiers||{},Oae()),u=e.pluralRules||{},c=Ae.isFunction(e.missing)?e.missing:null,f=Ae.isBoolean(e.missingWarn)||Ae.isRegExp(e.missingWarn)?e.missingWarn:!0,d=Ae.isBoolean(e.fallbackWarn)||Ae.isRegExp(e.fallbackWarn)?e.fallbackWarn:!0,p=!!e.fallbackFormat,m=!!e.unresolving,v=Ae.isFunction(e.postTranslation)?e.postTranslation:null,h=Ae.isPlainObject(e.processor)?e.processor:null,C=Ae.isBoolean(e.warnHtmlMessage)?e.warnHtmlMessage:!0,g=!!e.escapeParameter,y=Ae.isFunction(e.messageCompiler)?e.messageCompiler:qT,_=Ae.isFunction(e.messageResolver)?e.messageResolver:YT||zT,b=Ae.isFunction(e.localeFallbacker)?e.localeFallbacker:GT||KT,w=Ae.isObject(e.fallbackContext)?e.fallbackContext:void 0,S=Ae.isFunction(e.onWarn)?e.onWarn:Ae.warn,E=e,$=Ae.isObject(E.__datetimeFormatters)?E.__datetimeFormatters:new Map,O=Ae.isObject(E.__numberFormatters)?E.__numberFormatters:new Map,A=Ae.isObject(E.__meta)?E.__meta:{};ow++;const M={version:t,cid:ow,locale:n,fallbackLocale:o,messages:r,modifiers:s,pluralRules:u,missing:c,missingWarn:f,fallbackWarn:d,fallbackFormat:p,unresolving:m,postTranslation:v,processor:h,warnHtmlMessage:C,escapeParameter:g,messageCompiler:y,messageResolver:_,localeFallbacker:b,fallbackContext:w,onWarn:S,__meta:A};return M.datetimeFormats=a,M.numberFormats=l,M.__datetimeFormatters=$,M.__numberFormatters=O,M}function Dae(e,t){return e instanceof RegExp?e.test(t):e}function Fae(e,t){return e instanceof RegExp?e.test(t):e}function zf(e,t,n,o,r){const{missing:a,onWarn:l}=e;if(a!==null){const s=a(e,n,t,r);return Ae.isString(s)?s:t}else return t}function Bae(e,t,n){const o=e;o.__localeChainCache=new Map,e.localeFallbacker(e,n,t)}const Vae=e=>e;let Tm=Object.create(null);function Hae(){Tm=Object.create(null)}function zae(e,t={}){{const o=(t.onCacheKey||Vae)(e),r=Tm[o];if(r)return r;let a=!1;const l=t.onError||Hs.defaultOnError;t.onError=c=>{a=!0,l(c)};const{code:s}=Hs.baseCompile(e,t),u=new Function(`return ${s}`)();return a?u:Tm[o]=u}}let ZT=Hs.CompileErrorCodes.__EXTEND_POINT__;const Vp=()=>++ZT,wr={INVALID_ARGUMENT:ZT,INVALID_DATE_ARGUMENT:Vp(),INVALID_ISO_DATE_ARGUMENT:Vp(),__EXTEND_POINT__:Vp()};function rl(e){return Hs.createCompileError(e,null,void 0)}wr.INVALID_ARGUMENT+"",wr.INVALID_DATE_ARGUMENT+"",wr.INVALID_ISO_DATE_ARGUMENT+"";const rw=()=>"",ka=e=>Ae.isFunction(e);function jae(e,...t){const{fallbackFormat:n,postTranslation:o,unresolving:r,messageCompiler:a,fallbackLocale:l,messages:s}=e,[u,c]=t$(...t),f=Ae.isBoolean(c.missingWarn)?c.missingWarn:e.missingWarn,d=Ae.isBoolean(c.fallbackWarn)?c.fallbackWarn:e.fallbackWarn,p=Ae.isBoolean(c.escapeParameter)?c.escapeParameter:e.escapeParameter,m=!!c.resolvedMessage,v=Ae.isString(c.default)||Ae.isBoolean(c.default)?Ae.isBoolean(c.default)?a?u:()=>u:c.default:n?a?u:()=>u:"",h=n||v!=="",C=Ae.isString(c.locale)?c.locale:e.locale;p&&Wae(c);let[g,y,_]=m?[u,C,s[C]||{}]:QT(e,u,C,l,d,f),b=g,w=u;if(!m&&!(Ae.isString(b)||ka(b))&&h&&(b=v,w=b),!m&&(!(Ae.isString(b)||ka(b))||!Ae.isString(y)))return r?Hf:u;let S=!1;const E=()=>{S=!0},$=ka(b)?b:e$(e,u,y,b,w,E);if(S)return b;const O=qae(e,y,_,c),A=WT(O),M=Kae(e,$,A);return o?o(M,u):M}function Wae(e){Ae.isArray(e.list)?e.list=e.list.map(t=>Ae.isString(t)?Ae.escapeHtml(t):t):Ae.isObject(e.named)&&Object.keys(e.named).forEach(t=>{Ae.isString(e.named[t])&&(e.named[t]=Ae.escapeHtml(e.named[t]))})}function QT(e,t,n,o,r,a){const{messages:l,onWarn:s,messageResolver:u,localeFallbacker:c}=e,f=c(e,o,n);let d={},p,m=null;const v="translate";for(let h=0;ho;return c.locale=n,c.key=t,c}const u=l(o,Uae(e,n,r,o,s,a));return u.locale=n,u.key=t,u.source=o,u}function Kae(e,t,n){return t(n)}function t$(...e){const[t,n,o]=e,r={};if(!Ae.isString(t)&&!Ae.isNumber(t)&&!ka(t))throw rl(wr.INVALID_ARGUMENT);const a=Ae.isNumber(t)?String(t):(ka(t),t);return Ae.isNumber(n)?r.plural=n:Ae.isString(n)?r.default=n:Ae.isPlainObject(n)&&!Ae.isEmptyObject(n)?r.named=n:Ae.isArray(n)&&(r.list=n),Ae.isNumber(o)?r.plural=o:Ae.isString(o)?r.default=o:Ae.isPlainObject(o)&&Ae.assign(r,o),[a,r]}function Uae(e,t,n,o,r,a){return{warnHtmlMessage:r,onError:l=>{throw a&&a(l),l},onCacheKey:l=>Ae.generateFormatCacheKey(t,n,l)}}function qae(e,t,n,o){const{modifiers:r,pluralRules:a,messageResolver:l,fallbackLocale:s,fallbackWarn:u,missingWarn:c,fallbackContext:f}=e,p={locale:t,modifiers:r,pluralRules:a,messages:m=>{let v=l(n,m);if(v==null&&f){const[,,h]=QT(f,m,t,s,u,c);v=l(h,m)}if(Ae.isString(v)){let h=!1;const g=e$(e,m,t,v,m,()=>{h=!0});return h?rw:g}else return ka(v)?v:rw}};return e.processor&&(p.processor=e.processor),o.list&&(p.list=o.list),o.named&&(p.named=o.named),Ae.isNumber(o.plural)&&(p.pluralIndex=o.plural),p}function Yae(e,...t){const{datetimeFormats:n,unresolving:o,fallbackLocale:r,onWarn:a,localeFallbacker:l}=e,{__datetimeFormatters:s}=e,[u,c,f,d]=o$(...t),p=Ae.isBoolean(f.missingWarn)?f.missingWarn:e.missingWarn;Ae.isBoolean(f.fallbackWarn)?f.fallbackWarn:e.fallbackWarn;const m=!!f.part,v=Ae.isString(f.locale)?f.locale:e.locale,h=l(e,r,v);if(!Ae.isString(u)||u==="")return new Intl.DateTimeFormat(v,d).format(c);let C={},g,y=null;const _="datetime format";for(let S=0;S{n$.includes(u)?l[u]=n[u]:a[u]=n[u]}),Ae.isString(o)?a.locale=o:Ae.isPlainObject(o)&&(l=o),Ae.isPlainObject(r)&&(l=r),[a.key||"",s,a,l]}function Gae(e,t,n){const o=e;for(const r in n){const a=`${t}__${r}`;o.__datetimeFormatters.has(a)&&o.__datetimeFormatters.delete(a)}}function Xae(e,...t){const{numberFormats:n,unresolving:o,fallbackLocale:r,onWarn:a,localeFallbacker:l}=e,{__numberFormatters:s}=e,[u,c,f,d]=a$(...t),p=Ae.isBoolean(f.missingWarn)?f.missingWarn:e.missingWarn;Ae.isBoolean(f.fallbackWarn)?f.fallbackWarn:e.fallbackWarn;const m=!!f.part,v=Ae.isString(f.locale)?f.locale:e.locale,h=l(e,r,v);if(!Ae.isString(u)||u==="")return new Intl.NumberFormat(v,d).format(c);let C={},g,y=null;const _="number format";for(let S=0;S{r$.includes(u)?l[u]=n[u]:a[u]=n[u]}),Ae.isString(o)?a.locale=o:Ae.isPlainObject(o)&&(l=o),Ae.isPlainObject(r)&&(l=r),[a.key||"",s,a,l]}function Jae(e,t,n){const o=e;for(const r in n){const a=`${t}__${r}`;o.__numberFormatters.has(a)&&o.__numberFormatters.delete(a)}}Ot.CompileErrorCodes=Hs.CompileErrorCodes;Ot.createCompileError=Hs.createCompileError;Ot.CoreErrorCodes=wr;Ot.CoreWarnCodes=Ka;Ot.DATETIME_FORMAT_OPTIONS_KEYS=n$;Ot.DEFAULT_LOCALE=ib;Ot.DEFAULT_MESSAGE_DATA_TYPE=jT;Ot.MISSING_RESOLVE_VALUE=$ae;Ot.NOT_REOSLVED=Hf;Ot.NUMBER_FORMAT_OPTIONS_KEYS=r$;Ot.VERSION=UT;Ot.clearCompileCache=Hae;Ot.clearDateTimeFormat=Gae;Ot.clearNumberFormat=Jae;Ot.compileToFunction=zae;Ot.createCoreContext=xae;Ot.createCoreError=rl;Ot.createMessageContext=WT;Ot.datetime=Yae;Ot.fallbackWithLocaleChain=kae;Ot.fallbackWithSimple=KT;Ot.getAdditionalMeta=Pae;Ot.getDevToolsHook=bae;Ot.getFallbackContext=Lae;Ot.getWarnMessage=Sae;Ot.handleMissing=zf;Ot.initI18nDevTools=yae;Ot.isMessageFunction=ka;Ot.isTranslateFallbackWarn=Dae;Ot.isTranslateMissingWarn=Fae;Ot.number=Xae;Ot.parse=HT;Ot.parseDateTimeArgs=o$;Ot.parseNumberArgs=a$;Ot.parseTranslateArgs=t$;Ot.registerLocaleFallbacker=Mae;Ot.registerMessageCompiler=Nae;Ot.registerMessageResolver=Iae;Ot.resolveValue=cae;Ot.resolveWithKeyValue=zT;Ot.setAdditionalMeta=Aae;Ot.setDevToolsHook=gae;Ot.setFallbackContext=Rae;Ot.translate=jae;Ot.translateDevTools=wae;Ot.updateFallbackLocale=Bae;wT.exports=Ot;var Zae=wT.exports;const Qae=mV(kI);/*!
+ * vue-i18n v9.2.2
+ * (c) 2022 kazuya kawaguchi
+ * Released under the MIT License.
+ */Object.defineProperty(Pr,"__esModule",{value:!0});var It=Zae,Rt=Qae,Me=Jg;const l$="9.2.2";let s$=It.CompileErrorCodes.__EXTEND_POINT__;const oo=()=>++s$,mn={UNEXPECTED_RETURN_TYPE:s$,INVALID_ARGUMENT:oo(),MUST_BE_CALL_SETUP_TOP:oo(),NOT_INSLALLED:oo(),NOT_AVAILABLE_IN_LEGACY_MODE:oo(),REQUIRED_VALUE:oo(),INVALID_VALUE:oo(),CANNOT_SETUP_VUE_DEVTOOLS_PLUGIN:oo(),NOT_INSLALLED_WITH_PROVIDE:oo(),UNEXPECTED_ERROR:oo(),NOT_COMPATIBLE_LEGACY_VUE_I18N:oo(),BRIDGE_SUPPORT_VUE_2_ONLY:oo(),MUST_DEFINE_I18N_OPTION_IN_ALLOW_COMPOSITION:oo(),NOT_AVAILABLE_COMPOSITION_IN_LEGACY:oo(),__EXTEND_POINT__:oo()};function _n(e,...t){return It.createCompileError(e,null,void 0)}const $m=Me.makeSymbol("__transrateVNode"),Om=Me.makeSymbol("__datetimeParts"),Nm=Me.makeSymbol("__numberParts"),i$=Me.makeSymbol("__setPluralRules");Me.makeSymbol("__intlifyMeta");const u$=Me.makeSymbol("__injectWithOption"),ele="__VUE_I18N_BRIDGE__";function Im(e){if(!Me.isObject(e))return e;for(const t in e)if(Me.hasOwn(e,t))if(!t.includes("."))Me.isObject(e[t])&&Im(e[t]);else{const n=t.split("."),o=n.length-1;let r=e;for(let a=0;a{if("locale"in s&&"resource"in s){const{locale:u,resource:c}=s;u?(l[u]=l[u]||{},Hi(c,l[u])):Hi(c,l)}else Me.isString(s)&&Hi(JSON.parse(s),l)}),r==null&&a)for(const s in l)Me.hasOwn(l,s)&&Im(l[s]);return l}const kc=e=>!Me.isObject(e)||Me.isArray(e);function Hi(e,t){if(kc(e)||kc(t))throw _n(mn.INVALID_VALUE);for(const n in e)Me.hasOwn(e,n)&&(kc(e[n])||kc(t[n])?t[n]=e[n]:Hi(e[n],t[n]))}function tle(e){return e.type}function c$(e,t,n){let o=Me.isObject(t.messages)?t.messages:{};"__i18nGlobal"in n&&(o=jf(e.locale.value,{messages:o,__i18n:n.__i18nGlobal}));const r=Object.keys(o);r.length&&r.forEach(a=>{e.mergeLocaleMessage(a,o[a])});{if(Me.isObject(t.datetimeFormats)){const a=Object.keys(t.datetimeFormats);a.length&&a.forEach(l=>{e.mergeDateTimeFormat(l,t.datetimeFormats[l])})}if(Me.isObject(t.numberFormats)){const a=Object.keys(t.numberFormats);a.length&&a.forEach(l=>{e.mergeNumberFormat(l,t.numberFormats[l])})}}}function aw(e){return Rt.createVNode(Rt.Text,null,e,0)}let lw=0;function sw(e){return(t,n,o,r)=>e(n,o,Rt.getCurrentInstance()||void 0,r)}function ub(e={},t){const{__root:n}=e,o=n===void 0;let r=Me.isBoolean(e.inheritLocale)?e.inheritLocale:!0;const a=Rt.ref(n&&r?n.locale.value:Me.isString(e.locale)?e.locale:It.DEFAULT_LOCALE),l=Rt.ref(n&&r?n.fallbackLocale.value:Me.isString(e.fallbackLocale)||Me.isArray(e.fallbackLocale)||Me.isPlainObject(e.fallbackLocale)||e.fallbackLocale===!1?e.fallbackLocale:a.value),s=Rt.ref(jf(a.value,e)),u=Rt.ref(Me.isPlainObject(e.datetimeFormats)?e.datetimeFormats:{[a.value]:{}}),c=Rt.ref(Me.isPlainObject(e.numberFormats)?e.numberFormats:{[a.value]:{}});let f=n?n.missingWarn:Me.isBoolean(e.missingWarn)||Me.isRegExp(e.missingWarn)?e.missingWarn:!0,d=n?n.fallbackWarn:Me.isBoolean(e.fallbackWarn)||Me.isRegExp(e.fallbackWarn)?e.fallbackWarn:!0,p=n?n.fallbackRoot:Me.isBoolean(e.fallbackRoot)?e.fallbackRoot:!0,m=!!e.fallbackFormat,v=Me.isFunction(e.missing)?e.missing:null,h=Me.isFunction(e.missing)?sw(e.missing):null,C=Me.isFunction(e.postTranslation)?e.postTranslation:null,g=n?n.warnHtmlMessage:Me.isBoolean(e.warnHtmlMessage)?e.warnHtmlMessage:!0,y=!!e.escapeParameter;const _=n?n.modifiers:Me.isPlainObject(e.modifiers)?e.modifiers:{};let b=e.pluralRules||n&&n.pluralRules,w;w=(()=>{o&&It.setFallbackContext(null);const ce={version:l$,locale:a.value,fallbackLocale:l.value,messages:s.value,modifiers:_,pluralRules:b,missing:h===null?void 0:h,missingWarn:f,fallbackWarn:d,fallbackFormat:m,unresolving:!0,postTranslation:C===null?void 0:C,warnHtmlMessage:g,escapeParameter:y,messageResolver:e.messageResolver,__meta:{framework:"vue"}};ce.datetimeFormats=u.value,ce.numberFormats=c.value,ce.__datetimeFormatters=Me.isPlainObject(w)?w.__datetimeFormatters:void 0,ce.__numberFormatters=Me.isPlainObject(w)?w.__numberFormatters:void 0;const de=It.createCoreContext(ce);return o&&It.setFallbackContext(de),de})(),It.updateFallbackLocale(w,a.value,l.value);function E(){return[a.value,l.value,s.value,u.value,c.value]}const $=Rt.computed({get:()=>a.value,set:ce=>{a.value=ce,w.locale=a.value}}),O=Rt.computed({get:()=>l.value,set:ce=>{l.value=ce,w.fallbackLocale=l.value,It.updateFallbackLocale(w,a.value,ce)}}),A=Rt.computed(()=>s.value),M=Rt.computed(()=>u.value),D=Rt.computed(()=>c.value);function U(){return Me.isFunction(C)?C:null}function j(ce){C=ce,w.postTranslation=ce}function W(){return v}function L(ce){ce!==null&&(h=sw(ce)),v=ce,w.missing=h}const P=(ce,de,xe,he,He,et)=>{E();let rt;if(rt=ce(w),Me.isNumber(rt)&&rt===It.NOT_REOSLVED){const[wt,Ze]=de();return n&&p?he(n):He(wt)}else{if(et(rt))return rt;throw _n(mn.UNEXPECTED_RETURN_TYPE)}};function x(...ce){return P(de=>Reflect.apply(It.translate,null,[de,...ce]),()=>It.parseTranslateArgs(...ce),"translate",de=>Reflect.apply(de.t,de,[...ce]),de=>de,de=>Me.isString(de))}function I(...ce){const[de,xe,he]=ce;if(he&&!Me.isObject(he))throw _n(mn.INVALID_ARGUMENT);return x(de,xe,Me.assign({resolvedMessage:!0},he||{}))}function H(...ce){return P(de=>Reflect.apply(It.datetime,null,[de,...ce]),()=>It.parseDateTimeArgs(...ce),"datetime format",de=>Reflect.apply(de.d,de,[...ce]),()=>It.MISSING_RESOLVE_VALUE,de=>Me.isString(de))}function G(...ce){return P(de=>Reflect.apply(It.number,null,[de,...ce]),()=>It.parseNumberArgs(...ce),"number format",de=>Reflect.apply(de.n,de,[...ce]),()=>It.MISSING_RESOLVE_VALUE,de=>Me.isString(de))}function J(ce){return ce.map(de=>Me.isString(de)||Me.isNumber(de)||Me.isBoolean(de)?aw(String(de)):de)}const fe={normalize:J,interpolate:ce=>ce,type:"vnode"};function Te(...ce){return P(de=>{let xe;const he=de;try{he.processor=fe,xe=Reflect.apply(It.translate,null,[he,...ce])}finally{he.processor=null}return xe},()=>It.parseTranslateArgs(...ce),"translate",de=>de[$m](...ce),de=>[aw(de)],de=>Me.isArray(de))}function oe(...ce){return P(de=>Reflect.apply(It.number,null,[de,...ce]),()=>It.parseNumberArgs(...ce),"number format",de=>de[Nm](...ce),()=>[],de=>Me.isString(de)||Me.isArray(de))}function ke(...ce){return P(de=>Reflect.apply(It.datetime,null,[de,...ce]),()=>It.parseDateTimeArgs(...ce),"datetime format",de=>de[Om](...ce),()=>[],de=>Me.isString(de)||Me.isArray(de))}function ae(ce){b=ce,w.pluralRules=b}function Oe(ce,de){const xe=Me.isString(de)?de:a.value,he=q(xe);return w.messageResolver(he,ce)!==null}function we(ce){let de=null;const xe=It.fallbackWithLocaleChain(w,l.value,a.value);for(let he=0;he{r&&(a.value=ce,w.locale=ce,It.updateFallbackLocale(w,a.value,l.value))}),Rt.watch(n.fallbackLocale,ce=>{r&&(l.value=ce,w.fallbackLocale=ce,It.updateFallbackLocale(w,a.value,l.value))}));const Ce={id:lw,locale:$,fallbackLocale:O,get inheritLocale(){return r},set inheritLocale(ce){r=ce,ce&&n&&(a.value=n.locale.value,l.value=n.fallbackLocale.value,It.updateFallbackLocale(w,a.value,l.value))},get availableLocales(){return Object.keys(s.value).sort()},messages:A,get modifiers(){return _},get pluralRules(){return b||{}},get isGlobal(){return o},get missingWarn(){return f},set missingWarn(ce){f=ce,w.missingWarn=f},get fallbackWarn(){return d},set fallbackWarn(ce){d=ce,w.fallbackWarn=d},get fallbackRoot(){return p},set fallbackRoot(ce){p=ce},get fallbackFormat(){return m},set fallbackFormat(ce){m=ce,w.fallbackFormat=m},get warnHtmlMessage(){return g},set warnHtmlMessage(ce){g=ce,w.warnHtmlMessage=ce},get escapeParameter(){return y},set escapeParameter(ce){y=ce,w.escapeParameter=ce},t:x,getLocaleMessage:q,setLocaleMessage:B,mergeLocaleMessage:z,getPostTranslationHandler:U,setPostTranslationHandler:j,getMissingHandler:W,setMissingHandler:L,[i$]:ae};return Ce.datetimeFormats=M,Ce.numberFormats=D,Ce.rt=I,Ce.te=Oe,Ce.tm=ge,Ce.d=H,Ce.n=G,Ce.getDateTimeFormat=Z,Ce.setDateTimeFormat=ue,Ce.mergeDateTimeFormat=se,Ce.getNumberFormat=me,Ce.setNumberFormat=_e,Ce.mergeNumberFormat=$e,Ce[u$]=e.__injectWithOption,Ce[$m]=Te,Ce[Om]=ke,Ce[Nm]=oe,Ce}function nle(e){const t=Me.isString(e.locale)?e.locale:It.DEFAULT_LOCALE,n=Me.isString(e.fallbackLocale)||Me.isArray(e.fallbackLocale)||Me.isPlainObject(e.fallbackLocale)||e.fallbackLocale===!1?e.fallbackLocale:t,o=Me.isFunction(e.missing)?e.missing:void 0,r=Me.isBoolean(e.silentTranslationWarn)||Me.isRegExp(e.silentTranslationWarn)?!e.silentTranslationWarn:!0,a=Me.isBoolean(e.silentFallbackWarn)||Me.isRegExp(e.silentFallbackWarn)?!e.silentFallbackWarn:!0,l=Me.isBoolean(e.fallbackRoot)?e.fallbackRoot:!0,s=!!e.formatFallbackMessages,u=Me.isPlainObject(e.modifiers)?e.modifiers:{},c=e.pluralizationRules,f=Me.isFunction(e.postTranslation)?e.postTranslation:void 0,d=Me.isString(e.warnHtmlInMessage)?e.warnHtmlInMessage!=="off":!0,p=!!e.escapeParameterHtml,m=Me.isBoolean(e.sync)?e.sync:!0;let v=e.messages;if(Me.isPlainObject(e.sharedMessages)){const w=e.sharedMessages;v=Object.keys(w).reduce((E,$)=>{const O=E[$]||(E[$]={});return Me.assign(O,w[$]),E},v||{})}const{__i18n:h,__root:C,__injectWithOption:g}=e,y=e.datetimeFormats,_=e.numberFormats,b=e.flatJson;return{locale:t,fallbackLocale:n,messages:v,flatJson:b,datetimeFormats:y,numberFormats:_,missing:o,missingWarn:r,fallbackWarn:a,fallbackRoot:l,fallbackFormat:s,modifiers:u,pluralRules:c,postTranslation:f,warnHtmlMessage:d,escapeParameter:p,messageResolver:e.messageResolver,inheritLocale:m,__i18n:h,__root:C,__injectWithOption:g}}function Mm(e={},t){{const n=ub(nle(e)),o={id:n.id,get locale(){return n.locale.value},set locale(r){n.locale.value=r},get fallbackLocale(){return n.fallbackLocale.value},set fallbackLocale(r){n.fallbackLocale.value=r},get messages(){return n.messages.value},get datetimeFormats(){return n.datetimeFormats.value},get numberFormats(){return n.numberFormats.value},get availableLocales(){return n.availableLocales},get formatter(){return{interpolate(){return[]}}},set formatter(r){},get missing(){return n.getMissingHandler()},set missing(r){n.setMissingHandler(r)},get silentTranslationWarn(){return Me.isBoolean(n.missingWarn)?!n.missingWarn:n.missingWarn},set silentTranslationWarn(r){n.missingWarn=Me.isBoolean(r)?!r:r},get silentFallbackWarn(){return Me.isBoolean(n.fallbackWarn)?!n.fallbackWarn:n.fallbackWarn},set silentFallbackWarn(r){n.fallbackWarn=Me.isBoolean(r)?!r:r},get modifiers(){return n.modifiers},get formatFallbackMessages(){return n.fallbackFormat},set formatFallbackMessages(r){n.fallbackFormat=r},get postTranslation(){return n.getPostTranslationHandler()},set postTranslation(r){n.setPostTranslationHandler(r)},get sync(){return n.inheritLocale},set sync(r){n.inheritLocale=r},get warnHtmlInMessage(){return n.warnHtmlMessage?"warn":"off"},set warnHtmlInMessage(r){n.warnHtmlMessage=r!=="off"},get escapeParameterHtml(){return n.escapeParameter},set escapeParameterHtml(r){n.escapeParameter=r},get preserveDirectiveContent(){return!0},set preserveDirectiveContent(r){},get pluralizationRules(){return n.pluralRules||{}},__composer:n,t(...r){const[a,l,s]=r,u={};let c=null,f=null;if(!Me.isString(a))throw _n(mn.INVALID_ARGUMENT);const d=a;return Me.isString(l)?u.locale=l:Me.isArray(l)?c=l:Me.isPlainObject(l)&&(f=l),Me.isArray(s)?c=s:Me.isPlainObject(s)&&(f=s),Reflect.apply(n.t,n,[d,c||f||{},u])},rt(...r){return Reflect.apply(n.rt,n,[...r])},tc(...r){const[a,l,s]=r,u={plural:1};let c=null,f=null;if(!Me.isString(a))throw _n(mn.INVALID_ARGUMENT);const d=a;return Me.isString(l)?u.locale=l:Me.isNumber(l)?u.plural=l:Me.isArray(l)?c=l:Me.isPlainObject(l)&&(f=l),Me.isString(s)?u.locale=s:Me.isArray(s)?c=s:Me.isPlainObject(s)&&(f=s),Reflect.apply(n.t,n,[d,c||f||{},u])},te(r,a){return n.te(r,a)},tm(r){return n.tm(r)},getLocaleMessage(r){return n.getLocaleMessage(r)},setLocaleMessage(r,a){n.setLocaleMessage(r,a)},mergeLocaleMessage(r,a){n.mergeLocaleMessage(r,a)},d(...r){return Reflect.apply(n.d,n,[...r])},getDateTimeFormat(r){return n.getDateTimeFormat(r)},setDateTimeFormat(r,a){n.setDateTimeFormat(r,a)},mergeDateTimeFormat(r,a){n.mergeDateTimeFormat(r,a)},n(...r){return Reflect.apply(n.n,n,[...r])},getNumberFormat(r){return n.getNumberFormat(r)},setNumberFormat(r,a){n.setNumberFormat(r,a)},mergeNumberFormat(r,a){n.mergeNumberFormat(r,a)},getChoiceIndex(r,a){return-1},__onComponentInstanceCreated(r){const{componentInstanceCreatedListener:a}=e;a&&a(r,o)}};return o}}const cb={tag:{type:[String,Object]},locale:{type:String},scope:{type:String,validator:e=>e==="parent"||e==="global",default:"parent"},i18n:{type:Object}};function ole({slots:e},t){return t.length===1&&t[0]==="default"?(e.default?e.default():[]).reduce((o,r)=>o=[...o,...Me.isArray(r.children)?r.children:[r]],[]):t.reduce((n,o)=>{const r=e[o];return r&&(n[o]=r()),n},{})}function d$(e){return Rt.Fragment}const Am={name:"i18n-t",props:Me.assign({keypath:{type:String,required:!0},plural:{type:[Number,String],validator:e=>Me.isNumber(e)||!isNaN(e)}},cb),setup(e,t){const{slots:n,attrs:o}=t,r=e.i18n||Wf({useScope:e.scope,__useComponent:!0});return()=>{const a=Object.keys(n).filter(d=>d!=="_"),l={};e.locale&&(l.locale=e.locale),e.plural!==void 0&&(l.plural=Me.isString(e.plural)?+e.plural:e.plural);const s=ole(t,a),u=r[$m](e.keypath,s,l),c=Me.assign({},o),f=Me.isString(e.tag)||Me.isObject(e.tag)?e.tag:d$();return Rt.h(f,c,u)}}};function rle(e){return Me.isArray(e)&&!Me.isString(e[0])}function f$(e,t,n,o){const{slots:r,attrs:a}=t;return()=>{const l={part:!0};let s={};e.locale&&(l.locale=e.locale),Me.isString(e.format)?l.key=e.format:Me.isObject(e.format)&&(Me.isString(e.format.key)&&(l.key=e.format.key),s=Object.keys(e.format).reduce((p,m)=>n.includes(m)?Me.assign({},p,{[m]:e.format[m]}):p,{}));const u=o(e.value,l,s);let c=[l.key];Me.isArray(u)?c=u.map((p,m)=>{const v=r[p.type],h=v?v({[p.type]:p.value,index:m,parts:u}):[p.value];return rle(h)&&(h[0].key=`${p.type}-${m}`),h}):Me.isString(u)&&(c=[u]);const f=Me.assign({},a),d=Me.isString(e.tag)||Me.isObject(e.tag)?e.tag:d$();return Rt.h(d,f,c)}}const Pm={name:"i18n-n",props:Me.assign({value:{type:Number,required:!0},format:{type:[String,Object]}},cb),setup(e,t){const n=e.i18n||Wf({useScope:"parent",__useComponent:!0});return f$(e,t,It.NUMBER_FORMAT_OPTIONS_KEYS,(...o)=>n[Nm](...o))}},Rm={name:"i18n-d",props:Me.assign({value:{type:[Number,Date],required:!0},format:{type:[String,Object]}},cb),setup(e,t){const n=e.i18n||Wf({useScope:"parent",__useComponent:!0});return f$(e,t,It.DATETIME_FORMAT_OPTIONS_KEYS,(...o)=>n[Om](...o))}};function ale(e,t){const n=e;if(e.mode==="composition")return n.__getInstance(t)||e.global;{const o=n.__getInstance(t);return o!=null?o.__composer:e.global.__composer}}function p$(e){const t=l=>{const{instance:s,modifiers:u,value:c}=l;if(!s||!s.$)throw _n(mn.UNEXPECTED_ERROR);const f=ale(e,s.$),d=iw(c);return[Reflect.apply(f.t,f,[...uw(d)]),f]};return{created:(l,s)=>{const[u,c]=t(s);Me.inBrowser&&e.global===c&&(l.__i18nWatcher=Rt.watch(c.locale,()=>{s.instance&&s.instance.$forceUpdate()})),l.__composer=c,l.textContent=u},unmounted:l=>{Me.inBrowser&&l.__i18nWatcher&&(l.__i18nWatcher(),l.__i18nWatcher=void 0,delete l.__i18nWatcher),l.__composer&&(l.__composer=void 0,delete l.__composer)},beforeUpdate:(l,{value:s})=>{if(l.__composer){const u=l.__composer,c=iw(s);l.textContent=Reflect.apply(u.t,u,[...uw(c)])}},getSSRProps:l=>{const[s]=t(l);return{textContent:s}}}}function iw(e){if(Me.isString(e))return{path:e};if(Me.isPlainObject(e)){if(!("path"in e))throw _n(mn.REQUIRED_VALUE,"path");return e}else throw _n(mn.INVALID_VALUE)}function uw(e){const{path:t,locale:n,args:o,choice:r,plural:a}=e,l={},s=o||{};return Me.isString(n)&&(l.locale=n),Me.isNumber(r)&&(l.plural=r),Me.isNumber(a)&&(l.plural=a),[t,s,l]}function lle(e,t,...n){const o=Me.isPlainObject(n[0])?n[0]:{},r=!!o.useI18nComponentName;(Me.isBoolean(o.globalInstall)?o.globalInstall:!0)&&(e.component(r?"i18n":Am.name,Am),e.component(Pm.name,Pm),e.component(Rm.name,Rm)),e.directive("t",p$(t))}function sle(e,t,n){return{beforeCreate(){const o=Rt.getCurrentInstance();if(!o)throw _n(mn.UNEXPECTED_ERROR);const r=this.$options;if(r.i18n){const a=r.i18n;r.__i18n&&(a.__i18n=r.__i18n),a.__root=t,this===this.$root?this.$i18n=cw(e,a):(a.__injectWithOption=!0,this.$i18n=Mm(a))}else r.__i18n?this===this.$root?this.$i18n=cw(e,r):this.$i18n=Mm({__i18n:r.__i18n,__injectWithOption:!0,__root:t}):this.$i18n=e;r.__i18nGlobal&&c$(t,r,r),e.__onComponentInstanceCreated(this.$i18n),n.__setInstance(o,this.$i18n),this.$t=(...a)=>this.$i18n.t(...a),this.$rt=(...a)=>this.$i18n.rt(...a),this.$tc=(...a)=>this.$i18n.tc(...a),this.$te=(a,l)=>this.$i18n.te(a,l),this.$d=(...a)=>this.$i18n.d(...a),this.$n=(...a)=>this.$i18n.n(...a),this.$tm=a=>this.$i18n.tm(a)},mounted(){},unmounted(){const o=Rt.getCurrentInstance();if(!o)throw _n(mn.UNEXPECTED_ERROR);delete this.$t,delete this.$rt,delete this.$tc,delete this.$te,delete this.$d,delete this.$n,delete this.$tm,n.__deleteInstance(o),delete this.$i18n}}}function cw(e,t){e.locale=t.locale||e.locale,e.fallbackLocale=t.fallbackLocale||e.fallbackLocale,e.missing=t.missing||e.missing,e.silentTranslationWarn=t.silentTranslationWarn||e.silentFallbackWarn,e.silentFallbackWarn=t.silentFallbackWarn||e.silentFallbackWarn,e.formatFallbackMessages=t.formatFallbackMessages||e.formatFallbackMessages,e.postTranslation=t.postTranslation||e.postTranslation,e.warnHtmlInMessage=t.warnHtmlInMessage||e.warnHtmlInMessage,e.escapeParameterHtml=t.escapeParameterHtml||e.escapeParameterHtml,e.sync=t.sync||e.sync,e.__composer[i$](t.pluralizationRules||e.pluralizationRules);const n=jf(e.locale,{messages:t.messages,__i18n:t.__i18n});return Object.keys(n).forEach(o=>e.mergeLocaleMessage(o,n[o])),t.datetimeFormats&&Object.keys(t.datetimeFormats).forEach(o=>e.mergeDateTimeFormat(o,t.datetimeFormats[o])),t.numberFormats&&Object.keys(t.numberFormats).forEach(o=>e.mergeNumberFormat(o,t.numberFormats[o])),e}const h$=Me.makeSymbol("global-vue-i18n");function ile(e={},t){const n=Me.isBoolean(e.legacy)?e.legacy:!0,o=Me.isBoolean(e.globalInjection)?e.globalInjection:!0,r=n?!!e.allowComposition:!0,a=new Map,[l,s]=cle(e,n),u=Me.makeSymbol("");function c(p){return a.get(p)||null}function f(p,m){a.set(p,m)}function d(p){a.delete(p)}{const p={get mode(){return n?"legacy":"composition"},get allowComposition(){return r},async install(m,...v){m.__VUE_I18N_SYMBOL__=u,m.provide(m.__VUE_I18N_SYMBOL__,p),!n&&o&&yle(m,p.global),lle(m,p,...v),n&&m.mixin(sle(s,s.__composer,p));const h=m.unmount;m.unmount=()=>{p.dispose(),h()}},get global(){return s},dispose(){l.stop()},__instances:a,__getInstance:c,__setInstance:f,__deleteInstance:d};return p}}function Wf(e={}){const t=Rt.getCurrentInstance();if(t==null)throw _n(mn.MUST_BE_CALL_SETUP_TOP);if(!t.isCE&&t.appContext.app!=null&&!t.appContext.app.__VUE_I18N_SYMBOL__)throw _n(mn.NOT_INSLALLED);const n=dle(t),o=ple(n),r=tle(t),a=fle(e,r);if(n.mode==="legacy"&&!e.__useComponent){if(!n.allowComposition)throw _n(mn.NOT_AVAILABLE_IN_LEGACY_MODE);return vle(t,a,o,e)}if(a==="global")return c$(o,e,r),o;if(a==="parent"){let u=hle(n,t,e.__useComponent);return u==null&&(u=o),u}const l=n;let s=l.__getInstance(t);if(s==null){const u=Me.assign({},e);"__i18n"in r&&(u.__i18n=r.__i18n),o&&(u.__root=o),s=ub(u),mle(l,t),l.__setInstance(t,s)}return s}const ule=e=>{if(!(ele in e))throw _n(mn.NOT_COMPATIBLE_LEGACY_VUE_I18N);return e};function cle(e,t,n){const o=Rt.effectScope();{const r=t?o.run(()=>Mm(e)):o.run(()=>ub(e));if(r==null)throw _n(mn.UNEXPECTED_ERROR);return[o,r]}}function dle(e){{const t=Rt.inject(e.isCE?h$:e.appContext.app.__VUE_I18N_SYMBOL__);if(!t)throw _n(e.isCE?mn.NOT_INSLALLED_WITH_PROVIDE:mn.UNEXPECTED_ERROR);return t}}function fle(e,t){return Me.isEmptyObject(e)?"__i18n"in t?"local":"global":e.useScope?e.useScope:"local"}function ple(e){return e.mode==="composition"?e.global:e.global.__composer}function hle(e,t,n=!1){let o=null;const r=t.root;let a=t.parent;for(;a!=null;){const l=e;if(e.mode==="composition")o=l.__getInstance(a);else{const s=l.__getInstance(a);s!=null&&(o=s.__composer,n&&o&&!o[u$]&&(o=null))}if(o!=null||r===a)break;a=a.parent}return o}function mle(e,t,n){Rt.onMounted(()=>{},t),Rt.onUnmounted(()=>{e.__deleteInstance(t)},t)}function vle(e,t,n,o={}){const r=t==="local",a=Rt.shallowRef(null);if(r&&e.proxy&&!(e.proxy.$options.i18n||e.proxy.$options.__i18n))throw _n(mn.MUST_DEFINE_I18N_OPTION_IN_ALLOW_COMPOSITION);const l=Me.isBoolean(o.inheritLocale)?o.inheritLocale:!0,s=Rt.ref(r&&l?n.locale.value:Me.isString(o.locale)?o.locale:It.DEFAULT_LOCALE),u=Rt.ref(r&&l?n.fallbackLocale.value:Me.isString(o.fallbackLocale)||Me.isArray(o.fallbackLocale)||Me.isPlainObject(o.fallbackLocale)||o.fallbackLocale===!1?o.fallbackLocale:s.value),c=Rt.ref(jf(s.value,o)),f=Rt.ref(Me.isPlainObject(o.datetimeFormats)?o.datetimeFormats:{[s.value]:{}}),d=Rt.ref(Me.isPlainObject(o.numberFormats)?o.numberFormats:{[s.value]:{}}),p=r?n.missingWarn:Me.isBoolean(o.missingWarn)||Me.isRegExp(o.missingWarn)?o.missingWarn:!0,m=r?n.fallbackWarn:Me.isBoolean(o.fallbackWarn)||Me.isRegExp(o.fallbackWarn)?o.fallbackWarn:!0,v=r?n.fallbackRoot:Me.isBoolean(o.fallbackRoot)?o.fallbackRoot:!0,h=!!o.fallbackFormat,C=Me.isFunction(o.missing)?o.missing:null,g=Me.isFunction(o.postTranslation)?o.postTranslation:null,y=r?n.warnHtmlMessage:Me.isBoolean(o.warnHtmlMessage)?o.warnHtmlMessage:!0,_=!!o.escapeParameter,b=r?n.modifiers:Me.isPlainObject(o.modifiers)?o.modifiers:{},w=o.pluralRules||r&&n.pluralRules;function S(){return[s.value,u.value,c.value,f.value,d.value]}const E=Rt.computed({get:()=>a.value?a.value.locale.value:s.value,set:z=>{a.value&&(a.value.locale.value=z),s.value=z}}),$=Rt.computed({get:()=>a.value?a.value.fallbackLocale.value:u.value,set:z=>{a.value&&(a.value.fallbackLocale.value=z),u.value=z}}),O=Rt.computed(()=>a.value?a.value.messages.value:c.value),A=Rt.computed(()=>f.value),M=Rt.computed(()=>d.value);function D(){return a.value?a.value.getPostTranslationHandler():g}function U(z){a.value&&a.value.setPostTranslationHandler(z)}function j(){return a.value?a.value.getMissingHandler():C}function W(z){a.value&&a.value.setMissingHandler(z)}function L(z){return S(),z()}function P(...z){return a.value?L(()=>Reflect.apply(a.value.t,null,[...z])):L(()=>"")}function x(...z){return a.value?Reflect.apply(a.value.rt,null,[...z]):""}function I(...z){return a.value?L(()=>Reflect.apply(a.value.d,null,[...z])):L(()=>"")}function H(...z){return a.value?L(()=>Reflect.apply(a.value.n,null,[...z])):L(()=>"")}function G(z){return a.value?a.value.tm(z):{}}function J(z,Z){return a.value?a.value.te(z,Z):!1}function ee(z){return a.value?a.value.getLocaleMessage(z):{}}function fe(z,Z){a.value&&(a.value.setLocaleMessage(z,Z),c.value[z]=Z)}function Te(z,Z){a.value&&a.value.mergeLocaleMessage(z,Z)}function oe(z){return a.value?a.value.getDateTimeFormat(z):{}}function ke(z,Z){a.value&&(a.value.setDateTimeFormat(z,Z),f.value[z]=Z)}function ae(z,Z){a.value&&a.value.mergeDateTimeFormat(z,Z)}function Oe(z){return a.value?a.value.getNumberFormat(z):{}}function we(z,Z){a.value&&(a.value.setNumberFormat(z,Z),d.value[z]=Z)}function ge(z,Z){a.value&&a.value.mergeNumberFormat(z,Z)}const q={get id(){return a.value?a.value.id:-1},locale:E,fallbackLocale:$,messages:O,datetimeFormats:A,numberFormats:M,get inheritLocale(){return a.value?a.value.inheritLocale:l},set inheritLocale(z){a.value&&(a.value.inheritLocale=z)},get availableLocales(){return a.value?a.value.availableLocales:Object.keys(c.value)},get modifiers(){return a.value?a.value.modifiers:b},get pluralRules(){return a.value?a.value.pluralRules:w},get isGlobal(){return a.value?a.value.isGlobal:!1},get missingWarn(){return a.value?a.value.missingWarn:p},set missingWarn(z){a.value&&(a.value.missingWarn=z)},get fallbackWarn(){return a.value?a.value.fallbackWarn:m},set fallbackWarn(z){a.value&&(a.value.missingWarn=z)},get fallbackRoot(){return a.value?a.value.fallbackRoot:v},set fallbackRoot(z){a.value&&(a.value.fallbackRoot=z)},get fallbackFormat(){return a.value?a.value.fallbackFormat:h},set fallbackFormat(z){a.value&&(a.value.fallbackFormat=z)},get warnHtmlMessage(){return a.value?a.value.warnHtmlMessage:y},set warnHtmlMessage(z){a.value&&(a.value.warnHtmlMessage=z)},get escapeParameter(){return a.value?a.value.escapeParameter:_},set escapeParameter(z){a.value&&(a.value.escapeParameter=z)},t:P,getPostTranslationHandler:D,setPostTranslationHandler:U,getMissingHandler:j,setMissingHandler:W,rt:x,d:I,n:H,tm:G,te:J,getLocaleMessage:ee,setLocaleMessage:fe,mergeLocaleMessage:Te,getDateTimeFormat:oe,setDateTimeFormat:ke,mergeDateTimeFormat:ae,getNumberFormat:Oe,setNumberFormat:we,mergeNumberFormat:ge};function B(z){z.locale.value=s.value,z.fallbackLocale.value=u.value,Object.keys(c.value).forEach(Z=>{z.mergeLocaleMessage(Z,c.value[Z])}),Object.keys(f.value).forEach(Z=>{z.mergeDateTimeFormat(Z,f.value[Z])}),Object.keys(d.value).forEach(Z=>{z.mergeNumberFormat(Z,d.value[Z])}),z.escapeParameter=_,z.fallbackFormat=h,z.fallbackRoot=v,z.fallbackWarn=m,z.missingWarn=p,z.warnHtmlMessage=y}return Rt.onBeforeMount(()=>{if(e.proxy==null||e.proxy.$i18n==null)throw _n(mn.NOT_AVAILABLE_COMPOSITION_IN_LEGACY);const z=a.value=e.proxy.$i18n.__composer;t==="global"?(s.value=z.locale.value,u.value=z.fallbackLocale.value,c.value=z.messages.value,f.value=z.datetimeFormats.value,d.value=z.numberFormats.value):r&&B(z)}),q}const gle=["locale","fallbackLocale","availableLocales"],ble=["t","rt","d","n","tm"];function yle(e,t){const n=Object.create(null);gle.forEach(o=>{const r=Object.getOwnPropertyDescriptor(t,o);if(!r)throw _n(mn.UNEXPECTED_ERROR);const a=Rt.isRef(r.value)?{get(){return r.value.value},set(l){r.value.value=l}}:{get(){return r.get&&r.get()}};Object.defineProperty(n,o,a)}),e.config.globalProperties.$i18n=n,ble.forEach(o=>{const r=Object.getOwnPropertyDescriptor(t,o);if(!r||!r.value)throw _n(mn.UNEXPECTED_ERROR);Object.defineProperty(e.config.globalProperties,`$${o}`,r)})}It.registerMessageCompiler(It.compileToFunction);It.registerMessageResolver(It.resolveValue);It.registerLocaleFallbacker(It.fallbackWithLocaleChain);Pr.DatetimeFormat=Rm;Pr.I18nInjectionKey=h$;Pr.NumberFormat=Pm;Pr.Translation=Am;Pr.VERSION=l$;Pr.castToVueI18n=ule;var wle=Pr.createI18n=ile,Bl=Pr.useI18n=Wf;Pr.vTDirective=p$;var _le={name:"zh-cn",el:{breadcrumb:{label:"面包屑"},colorpicker:{confirm:"确定",clear:"清空"},datepicker:{now:"此刻",today:"今天",cancel:"取消",clear:"清空",confirm:"确定",selectDate:"选择日期",selectTime:"选择时间",startDate:"开始日期",startTime:"开始时间",endDate:"结束日期",endTime:"结束时间",prevYear:"前一年",nextYear:"后一年",prevMonth:"上个月",nextMonth:"下个月",year:"年",month1:"1 月",month2:"2 月",month3:"3 月",month4:"4 月",month5:"5 月",month6:"6 月",month7:"7 月",month8:"8 月",month9:"9 月",month10:"10 月",month11:"11 月",month12:"12 月",weeks:{sun:"日",mon:"一",tue:"二",wed:"三",thu:"四",fri:"五",sat:"六"},months:{jan:"一月",feb:"二月",mar:"三月",apr:"四月",may:"五月",jun:"六月",jul:"七月",aug:"八月",sep:"九月",oct:"十月",nov:"十一月",dec:"十二月"}},select:{loading:"加载中",noMatch:"无匹配数据",noData:"无数据",placeholder:"请选择"},cascader:{noMatch:"无匹配数据",loading:"加载中",placeholder:"请选择",noData:"暂无数据"},pagination:{goto:"前往",pagesize:"条/页",total:"共 {total} 条",pageClassifier:"页",page:"页",prev:"上一页",next:"下一页",currentPage:"第 {pager} 页",prevPages:"向前 {pager} 页",nextPages:"向后 {pager} 页",deprecationWarning:"你使用了一些已被废弃的用法,请参考 el-pagination 的官方文档"},messagebox:{title:"提示",confirm:"确定",cancel:"取消",error:"输入的数据不合法!"},upload:{deleteTip:"按 delete 键可删除",delete:"删除",preview:"查看图片",continue:"继续上传"},table:{emptyText:"暂无数据",confirmFilter:"筛选",resetFilter:"重置",clearFilter:"全部",sumText:"合计"},tour:{next:"下一步",previous:"上一步",finish:"结束导览"},tree:{emptyText:"暂无数据"},transfer:{noMatch:"无匹配数据",noData:"无数据",titles:["列表 1","列表 2"],filterPlaceholder:"请输入搜索内容",noCheckedFormat:"共 {total} 项",hasCheckedFormat:"已选 {checked}/{total} 项"},image:{error:"加载失败"},pageHeader:{title:"返回"},popconfirm:{confirmButtonText:"确定",cancelButtonText:"取消"},carousel:{leftArrow:"上一张幻灯片",rightArrow:"下一张幻灯片",indicator:"幻灯片切换至索引 {index}"}}};const Cle={"Install BuildAdmin":"安装 BuildAdmin","Environmental inspection":"环境检查","Checking installation environment":"正在检查安装环境","Current execution to:":"当前执行到:","Step 2 site configuration":"第二步 站点配置","Environmental inspection passed":"环境检查通过","This environmental check failed":"此项环境检查未通过","The environment check failed, but the installation can continue":"环境检查为失败/未确认,但可以继续安装","Basic environment":"基础环境","NPM correlation":"NPM相关","Test npm install":"测试 npm install","Check complete":"检查完成","Congratulations, the installation can continue~":"恭喜,安装可以继续~","Sorry, the necessary installation environment conditions have not been met, please check the above form!":"抱歉,有必要的安装环境条件没有达成,请检查以上表格!","Network Timeout":"网络超时","Network connection error":"网络连接错误","The interface path cannot be found":"接口路径找不到了(404):{url}","unknown error":"未知错误",executing:"",php_version:"PHP 版本",config_is_writable:"配置目录是否可写",public_is_writable:"public 目录是否可写",php_pdo:"PHP pdo_mysql 扩展",php_safe_mode:"PHP安全模式",php_proc:"PHP 程序执行函数(proc)",php_gd2:"PHP gd2 或 freeType",npm_version:"NPM 版本",npm_package_manager:"包管理器",nodejs_version:"node.js 版本",error:"错误",success:"成功","test-npm-install":"测试 npm install","check npm install":"是否测试命令执行?","set-npm-registry":"设置NPM源","Set NPM source":"设置NPM源","Use current source":"使用当前源",recommend:"(推荐)",TaoBao:"淘宝",Tencent:"腾讯","Click to test":"点击进行测试","Can execute":"可以执行","Command execution test failed":"命令执行测试失败","PM is ready!":"npm包管理器已经准备好了!","already installed":"已安装","The installation can continue, and some operations need to be completed manually":"可以继续安装,部分操作需手动完成","Sorry, the automatic installation of package manager failed. Please complete the installation manually!":"抱歉,自动安装包管理器失败,请手动完成安装!","Click to see how to solve it":"点击查看如何解决","How to solve":"如何解决",terminal:"终端",narrow:"缩小",Connecting:"连接中...","No command":"无命令",executed:" 已执行","Waiting for execution":" 等待执行","Connection successful, executing":"连接成功 正在执行 ","Unfinished matters manually":"手动完成未尽事宜","Open terminal (windows PowerShell)":"打开您PC/服务器的终端(Windows PowerShell、cmd等)","Execute command":"执行命令","Execution failed?":"执行失败了?","Move the built file to the specified location of the system":"移动构建好的文件到系统指定位置","Click to try to automatically move the build file":"点击尝试自动移动构建文件","The build output directory is: site":"构建输出目录为:站点","root directory / dist":"根目录/web/dist","You can delete the build output directory directly":"您可以直接删除构建输出目录","Getting full path of root directory / Web":"正在获取 根目录/web 的完整路径","Moving automatically":"正在自动移动...","Please move 1":"请移动构建输出目录中的","Please move 2":"文件夹和","Please move 3":"文件到根目录的","Please move 4":"目录下","During construction, all files in the output directory will be overwritten, so the system is designed to build in the root directory first, and then move to the public directory to prevent other files in the public from being overwritten":"构建时,会覆盖输出目录的所有文件,所以系统设计为先构建,然后移动到public目录,以免public内的其他文件被覆盖掉","Thanks for using buildadmin":"感谢使用 BuildAdmin","Background URL":"后台地址","Access foreground":"访问前台","Access background":"访问后台","Install Tips Title 1":"安装环境检测并没有完全通过,但安装可以继续,只是您后续需要手动进行一些操作,建议您","Install Tips Title 2":",在所有检测通过后再安装,以便您体验到 BuildAdmin 的核心功能之一。","Back to previous page":"回到上一页","If you don't want to open the corresponding permission due to some security factors, please check ":"如果你考虑到一些安全因素而不愿开启相应权限,请查看","how installation services ensure system security":"安装服务如何保障系统安全","If you really can't adjust all the tests to pass, please ":"如果您确实无法将所有检测调整到通过状态,请","click to feed back to us":"点击向我们反馈","continue installation":",并继续安装,安装程序后续将引导您,如何手动完成未尽事宜。","Close the prompt of completing unfinished matters manually":"关闭手动完成未尽事宜提示","Test connection to data server":"测试连接数据服务器...","Install now":"立即安装","Mysql database address":"MySQL 数据库地址","MySQL connection user name":"MySQL 连接用户名","MySQL connection password":"MySQL 连接密码","MySQL connection port number":"MySQL 连接端口号","Mysql database name":"MySQL 数据库名","MySQL data table prefix":"MySQL 数据表前缀","Administrator user name":"管理员用户名","Administrator password":"管理员密码","Duplicate administrator password":"重复管理员密码","Site name":"站点名称","Site configuration":"站点配置","The entered database was not found!":"数据表不存在,安装时将自动建立!","Duplicate passwords do not match":"重复密码不匹配","Command execution failed":"命令执行失败",Installing:"正在安装...","After installation, please complete the unfinished matters manually":"安装完成,请手动完成未尽事宜","Automatically executing the build command on the web side":"正在自动执行 WEB端的 构建命令","Installation complete":"安装完成...","The table prefix can only contain alphanumeric characters and underscores, and starts with a letter":"表前缀只能包含字母数字和下划线,并以字母开头","Manual Install 1":"命令自动执行失败,请手动完成未尽事宜,","Manual Install 2":"{seconds}秒 后自动跳转到操作引导页面...",Retry:"重试",delete:"删除",Confirm:"确认",Cancel:"取消","Request timeout!":"请求超时!","Server internal error!":"服务器内部错误!","The service is temporarily unavailable. Please try again later!":"服务暂时无法访问,请稍后再试!","Abnormal problem, please contact the website administrator!":"异常问题,请联系网站管理员!","You're disconnected!":"您断网了!",Required:"必填","Please enter the correct password":`密码要求6到32位,不能包含 & < > " '`,"It is composed of letters, numbers and underscores, starting with letters (3-15 digits)":"由字母、数字、下划线组成,以字母开头(3-15位)","It is recommended to delete the root directory / public / install folder; This page is only visible on your device.":"建议删除: 根目录/public/install 文件夹;本页仅在您的设备上可见。","Switch package manager":"切换包管理器","Please select package manager":"请选择包管理器","Switch package manager title":"只读WEB终端,可以在CRUD等操作后方便的执行 npm install、npm build 等命令,请在下方选择一个已安装好或您喜欢的的 NPM 包管理器","I want to execute the command manually":"我想手动执行命令",Reminder:"温馨提醒","Ready to start":"准备开始",language:"语言","NPM package manager":"NPM包管理器","The system has a Web terminal. Please select an installed or your favorite NPM package manager":"系统拥有WEB终端,请选择一个已安装好或您喜欢的的NPM包管理器","Start installation":"开始安装","Setup will restart. Are you sure you want to switch package manager?":"将重新开始安装程序,请确定要切换包管理器吗?","None - manual execution":"无-手动执行","Previous step":"上一步","Hide index.html?":"隐藏 index.html?","Sorry, some operations could not be completed automatically You need to manually complete the outstanding matters according to the following guidance":"抱歉,一些操作未能自动完成,需要您根据以下引导手动完成未尽事宜。","Need to reinstall the system?":"需重新安装系统?","Please click on me":"请点击我","Backend login password":"后台登录密码","Port error prompt 1":"当前安装程序站点的端口不是8000,您可能以错误的方式启动了安装服务,请参考","Get started quickly":"快速上手文档","Port error prompt 3":"进行安装。","Table migration failed":"数据表迁移失败","We use Phinx to manage the data table, which can version the data table":"我们使用`Phinx`管理数据表,它可以对数据表进行版本化管理。","Data table automatic migration failed, please manually migrate as follows:":"数据表自动迁移失败了,请按以下方法手动迁移:","If the command fails to be executed, add sudo or press the error message":"若命令执行失败,请尝试加 sudo,或按报错解决即可","Migration check":"迁移检查","When the command is executed successfully, the output is similar to:":"命令执行成功时,输出类似于:","After the command is executed successfully, multiple data tables will be automatically created in the database, and then click below to ":"命令执行成功后,数据库内将自动建立多个数据表,然后请您点击下方的","continue install":"继续安装"},Sle={"Install BuildAdmin":"Install BuildAdmin","Environmental inspection":"Environmental inspection","Checking installation environment":"Checking the installation environment","Current execution to:":"Current execution to:","Step 2 site configuration":"Step 2 site configuration","Environmental inspection passed":"Environmental inspection passed","This environmental check failed":"This environmental inspection was not passed.","The environment check failed, but the installation can continue":"Environment check failed/unconfirmed, but you can continue to install.","Basic environment":"Basic Environment","NPM correlation":"NPM correlation","Test npm install":"Test npm install","Check complete":"Check complete","Congratulations, the installation can continue~":"Congratulations, the installation can continue~","Sorry, the necessary installation environment conditions have not been met, please check the above form!":"Sorry, the necessary installation environmental conditions have not been met, please check the above form!","Network Timeout":"Network timeout","Network connection error":"Network connection error","The interface path cannot be found":"The interface path cannot be found(404):{url}","unknown error":"Unknown error",executing:"",php_version:"PHP Version",config_is_writable:"Is the configuration directory writable?",public_is_writable:"Is the public directory writable?",php_pdo:"PHP pdo_mysql extension",php_safe_mode:"PHP security mode",php_proc:"PHP proc_open and proc_close permission",php_gd2:"PHP gd2 or freeType extensions",npm_version:"NPM Version",npm_package_manager:"NPM package manager",nodejs_version:"node.js Version",error:"error",success:"success","test-npm-install":"Test npm install","check npm install":"Whether to test command execution?","set-npm-registry":"Set NPM source","Set NPM source":"Set NPM source","Use current source":"Use the current source",recommend:"(Recommend)",TaoBao:"TaoBao","Click to test":"Click to test","Can execute":"Can be executed","Command execution test failed":"Command execution test failed","PM is ready!":"The NPM package manager is ready!","already installed":"Already installed","The installation can continue, and some operations need to be completed manually":"You can continue the installation, and some operations need to be completed manually.","Sorry, the automatic installation of package manager failed. Please complete the installation manually!":"Sorry, the automatic installation of the package manager failed. Please complete the installation manually!","Click to see how to solve it":"Click to see how to solve it","How to solve":"How to solve",terminal:"Terminal",narrow:"Narrow",Connecting:"Connecting...","No command":"No command",executed:" Executed","Waiting for execution":" Waiting for execution","Connection successful, executing":"Connection successful, executing ","Unfinished matters manually":"Complete unfinished matters manually","Open terminal (windows PowerShell)":"Open the terminal of your PC/Server (PowerShell, cmd)","Execute command":"Execute the command","Execution failed?":"Failed execution?","Move the built file to the specified location of the system":"Move the built file to the specified location on the system.","Click to try to automatically move the build file":"Click to try to move the build file automatically","The build output directory is: site":"Build the output directory as: site","root directory / dist":"root directory/web/dist","You can delete the build output directory directly":"You can delete the build output directory directly","Getting full path of root directory / Web":"Getting the full path to the root directory/web","Moving automatically":"Moving automatically","Please move 1":"Please move the ","Please move 2":" folder and ","Please move 3":" files from the build output directory to the ","Please move 4":" directory of the root directory.","During construction, all files in the output directory will be overwritten, so the system is designed to build in the root directory first, and then move to the public directory to prevent other files in the public from being overwritten":"When constructing the process, all files in the output directory are overwriting, so the system is designed to build first and then move to the public directory to prevent overwriting other files in the public directory","Thanks for using buildadmin":"Thanks for using BuildAdmin","Background URL":"Background URL","Access foreground":"Access to the foreground","Access background":"Access to the background","Install Tips Title 1":"The installation environment test does not completely passed, but the installation can continue, and you need to do some manual operations to see how to modify it.It is recommended that you go ","Install Tips Title 2":" and install it after all the tests have passed so that you can experience one of the core functions of BuildAdmin.","Back to previous page":"back to previous page","If you don't want to open the corresponding permission due to some security factors, please check ":"If you don't want to open the corresponding permission due to some security factors, please check ","how installation services ensure system security":"how installation services ensure system security","If you really can't adjust all the tests to pass, please ":"If you really can't adjust all the tests to pass, please ","click to feed back to us":"click to feed back to us","continue installation":" and continue the installation. The subsequent installation program will guide you on how to manually complete the outstanding matters.","Close the prompt of completing unfinished matters manually":"Close the prompt of completing unfinished matters manually","Test connection to data server":"Test connection to data server","Install now":"Install now","Mysql database address":"Mysql database address","MySQL connection user name":"MySQL connection username","MySQL connection password":"MySQL connection password","MySQL connection port number":"MySQL connection port number","Mysql database name":"Mysql database name","MySQL data table prefix":"MySQL data table prefix","Administrator user name":"Administrator username","Administrator password":"Administrator password","Duplicate administrator password":"Duplicate administrator password","Site name":"Site Name","Site configuration":"Site Configuration","The entered database was not found!":"The data table does not exist and will be created automatically during installation","Duplicate passwords do not match":"Duplicate passwords mismatch","Command execution failed":"Command execution failed",Installing:"Installing","After installation, please complete the unfinished matters manually":"Installation is complete, please complete the unfinished matters manually.","Automatically executing the build command on the web side":"Automatically executing the build command on the web side","Installation complete":"Installation completed","The table prefix can only contain alphanumeric characters and underscores, and starts with a letter":"The table prefix can only contain alphanumeric characters and underscores, and starts with letters","Manual Install 1":"Failed to execute Command automatically. Please complete the unfinished matters manually.","Manual Install 2":"Automatically jump to the operation boot page after {seconds} seconds...",Retry:"Retry",delete:"Delete",Confirm:"Confirm",Cancel:"Cancel","Request timeout!":"Request timeout!","Server internal error!":"Internal server error!","The service is temporarily unavailable. Please try again later!":"The service is temporarily unavailable. Please try again later!","Abnormal problem, please contact the website administrator!":"Abnormal problem, please contact the website administrator!","You're disconnected!":"You're disconnected!",Required:"Required","Please enter the correct password":`The password requires 6 to 32 bits and cannot contain & < > " '`,"It is composed of letters, numbers and underscores, starting with letters (3-15 digits)":"Composed of letters, numbers and underscores, start with letters (3-15 digits)","It is recommended to delete the root directory / public / install folder; This page is only visible on your device.":"It is recommended to delete the root directory / public / install folder; This page is only visible on your device.","Switch package manager":"Switch package manager","Please select package manager":"Please select the package manager","Switch package manager title":"Read-only Web terminal, you can easily execute NPM install, NPM builds, and other commands after crud and other operations. Please select an installed or your favorite NPM package manager below","I want to execute the command manually":"I want to execute the command manually",Reminder:"Reminder","Ready to start":"Ready to start",language:"Language","NPM package manager":"NPM package manager","The system has a Web terminal. Please select an installed or your favorite NPM package manager":"The system has a Web terminal. Please select an installed or your favorite NPM package manager","Start installation":"Start installation","Setup will restart. Are you sure you want to switch package manager?":"The install will restart. Are you sure you want to switch the package manager?","None - manual execution":"None - manual execution","Previous step":"Previous step","Hide index.html?":"Hide index.html?","Sorry, some operations could not be completed automatically You need to manually complete the outstanding matters according to the following guidance":"Sorry, some operations could not be completed automatically. You need to outstanding matters according to the following guidance manually.","Need to reinstall the system?":"Need to reinstall the system?","Please click on me":"Please click on me","Backend login password":"Backend login password","Port error prompt 1":"The current installation site port is not 8000. You may have started the installation service in the wrong way. Please refer to the","Get started quickly":" Quick Start documentation ","Port error prompt 3":" for installation.","Table migration failed":"Table migration failed","We use Phinx to manage the data table, which can version the data table":"We use 'Phinx' to manage the data table, which can version the data table","Data table automatic migration failed, please manually migrate as follows:":"Data table automatic migration failed, please manually migrate as follows:","If the command fails to be executed, add sudo or press the error message":"If the command fails to be executed, add sudo or press the error message","Migration check":"Migration check","When the command is executed successfully, the output is similar to:":"When the command is executed successfully, the output is similar to:","After the command is executed successfully, multiple data tables will be automatically created in the database, and then click below to ":"After the command is executed successfully, multiple data tables will be automatically created in the database, and then click below to ","continue install":"continue the installation"},dw={"zh-cn":fw(Object.assign({"./pages/zh-cn/terminal.ts":Joe}),"zh-cn"),en:fw(Object.assign({"./pages/en/terminal.ts":Qoe}),"en")},kle={"zh-cn":{...Cle,..._le,...dw["zh-cn"]},en:{...Sle,...oS,...dw.en}},$o=wle({locale:"zh-cn",legacy:!1,fallbackLocale:"en",messages:kle});function fw(e,t){const n={};for(const o in e)if(e[o].default){const r=o.slice(o.lastIndexOf(t)+(t.length+1),o.lastIndexOf("."));if(r.indexOf("/")>0){const a=r.split("/");for(const l in a)typeof n[a[l]]>"u"&&(n[a[l]]=[]);a.length==2?n[a[0]][a[1]]=Ec(e[o].default):a.length==3?n[a[0]][a[1]][a[2]]=Ec(e[o].default):n[r]=Ec(e[o].default)}else n[r]=Ec(e[o].default)}return n}function Ec(e){const t=[];for(const n in e)if(n.indexOf(".")>0){const o=n.split(".");typeof t[o[0]]>"u"?t[o[0]]=[]:t[o[0]][o[1]]=e[n]}else t[n]=e[n];return t}const Ele=window.localStorage.getItem("ba-lang")||"zh-cn",m$=()=>window.location.protocol+"//"+window.location.host,Rr=Nn.create({baseURL:m$(),timeout:1e3*10,headers:{"Content-Type":"application/json","think-lang":Ele}});Rr.interceptors.response.use(e=>e,e=>(Tle(e),Promise.reject(e)));function Tle(e){let t="";if(e&&e.response)switch(e.response.status){case 404:t=$o.global.t("The interface path cannot be found",{url:e.response.config.url});break;case 408:t=$o.global.t("Request timeout!");break;case 500:t=$o.global.t("Server internal error!");break;case 504:t=$o.global.t("The service is temporarily unavailable. Please try again later!");break;default:t=$o.global.t("Abnormal problem, please contact the website administrator!");break}e.message.includes("timeout")&&(t=$o.global.t("Network Timeout")),e.message.includes("Network Error")&&(t=$o.global.t("Network connection error")),e.message.includes("Network")&&(t=window.navigator.onLine?$o.global.t("Abnormal problem, please contact the website administrator!"):$o.global.t("You're disconnected!")),t||(t=$o.global.t("unknown error")),Cr({type:"error",message:t,center:!0})}const na="/index.php",$le=na+"/api/install/envBaseCheck",Ole=na+"/api/install/envNpmCheck",Nle=na+"/api/install/testDatabase",v$=na+"/api/install/baseConfig",Ile=na+"/api/install/commandExecComplete",Mle=na+"/api/install/mvDist",Ale=na+"/api/install/manualInstall",Ple=na+"/api/install/terminal",Rle=na+"/api/install/changePackageManager",Lle=()=>Rr.get($le),xle=()=>{const e=ii();return Rr.post(Ole,{manager:e.state.packageManager})},Dle=e=>Rr.post(Nle,e),Fle=()=>Rr.get(v$),Ble=e=>Rr.post(v$,e),pw=e=>Rr.post(Ile,e).then(t=>{t.data.code!=1&&Cr({type:"error",message:t.data.msg,center:!0})}),Ua=e=>{const t=ii();Rr.post(Rle,{manager:e}).then(n=>{n.data.code==1?t.changePackageManager(n.data.data.manager):n.data.msg&&Cr({type:"error",message:n.data.msg,center:!0})})},Vle=()=>Rr.post(Mle),Hle=()=>Rr.get(Ale),zle=(e,t,n)=>m$()+Ple+"?command="+e+"&uuid="+t+"&extend="+n,gt={Waiting:0,Connecting:1,Executing:2,Success:3,Failed:4,Unknown:5},ju=Y_("common",()=>{const e=Et({step:"check",showStartDialog:!0});function t(o){e.step=o}function n(o){e.showStartDialog=o}return{state:e,setStep:t,toggleStartDialog:n}},{persist:{key:AI}}),ii=Y_("terminal",()=>{const e=Et({show:!1,showDot:!1,taskList:[],packageManager:"pnpm",showPackageManagerDialog:!1});function t(){for(const b in e.taskList)(e.taskList[b].status==gt.Connecting||e.taskList[b].status==gt.Executing)&&(e.taskList[b].status=gt.Unknown)}function n(b=!e.show){e.show=b,b&&o(!1)}function o(b=!e.showDot){e.showDot=b}function r(b=!e.showPackageManagerDialog){n(!b),e.showPackageManagerDialog=b}function a(b){e.packageManager=b}function l(b,w){e.taskList[b]&&(e.taskList[b].status=w,(w==gt.Failed||w==gt.Unknown)&&e.taskList[b].blockOnFailure&&u(b,!0))}function s(b){if(!e.taskList[b]||typeof e.taskList[b].callback!="function")return;const w=e.taskList[b].status;if(w==gt.Failed||w==gt.Unknown)e.taskList[b].callback(gt.Failed,b);else if(w==gt.Success&&(e.taskList[b].callback(gt.Success,b),e.taskList[b].command=="web-build."+e.packageManager)){const S=ju();S.state.step=="manualInstall"&&(n(!1),S.setStep("done"))}}function u(b,w=!e.taskList[b].showMessage){e.taskList[b].showMessage=w}function c(b,w){e.show||o(!0),e.taskList[b].message=e.taskList[b].message.concat(w),We(()=>{_(e.taskList[b].uuid)})}function f(b,w=!0,S="",E=()=>{}){e.show||o(!0),e.taskList=e.taskList.concat({uuid:LI(),createtime:RI(),status:gt.Waiting,command:b,message:[],showMessage:!1,blockOnFailure:w,extend:S,callback:E}),m()}function d(b,w=!0,S="",E=()=>{}){f(b+"."+e.packageManager,w,S,E)}function p(b){e.taskList[b].status!=gt.Connecting&&e.taskList[b].status!=gt.Executing&&e.taskList.splice(b,1),m()}function m(){let b=null;for(const w in e.taskList){if(e.taskList[w].status==gt.Waiting){b=parseInt(w);break}if(e.taskList[w].status==gt.Connecting||e.taskList[w].status==gt.Executing)break;if(e.taskList[w].status!=gt.Success&&(e.taskList[w].status==gt.Failed||e.taskList[w].status==gt.Unknown)){if(e.taskList[w].blockOnFailure)break;continue}}b!==null&&(l(b,gt.Connecting),v(b))}function v(b){window.eventSource=new EventSource(zle(e.taskList[b].command,e.taskList[b].uuid,e.taskList[b].extend)),window.eventSource.onmessage=function(w){const S=JSON.parse(w.data);if(!S||!S.data)return;const E=g(S.uuid);E!==!1&&(S.data=="command-exec-error"?(l(E,gt.Failed),window.eventSource.close(),s(E),m()):S.data=="command-exec-completed"?(window.eventSource.close(),e.taskList[E].status!=gt.Success&&l(E,gt.Failed),s(E),m()):S.data=="command-link-success"?l(E,gt.Executing):S.data=="command-exec-success"?l(E,gt.Success):c(E,S.data))},window.eventSource.onerror=function(){window.eventSource.close();const w=y(b);w!==!1&&(l(w,gt.Failed),s(w))}}function h(b){e.taskList[b].message=[],l(b,gt.Waiting),m()}function C(){e.taskList=e.taskList.filter(b=>b.status!=gt.Success)}function g(b){for(const w in e.taskList)if(e.taskList[w].uuid==b)return parseInt(w);return!1}function y(b){if(e.taskList[b])return b;{let w=-1;for(const S in e.taskList)(e.taskList[S].status==gt.Connecting||e.taskList[S].status==gt.Executing)&&(w=parseInt(S));return w===-1?!1:w}}function _(b){const w=document.querySelector(".exec-message-"+b);w&&w.scrollHeight&&(w.scrollTop=w.scrollHeight)}return{state:e,init:t,toggle:n,toggleDot:o,setTaskStatus:l,setTaskShowMessage:u,addTaskMessage:c,addTask:f,addTaskPM:d,delTask:p,startTask:m,retryTask:h,clearSuccessTask:C,togglePackageManagerDialog:r,changePackageManager:a}},{persist:{key:PI}}),jle={class:"command"},Wle={class:"task-opt"},Kle=["onClick"],Ule={class:"indent-2"},qle={class:"package-manager-dialog-footer"},Yle=Y({__name:"index",setup(e){const{t}=Bl(),n=ii(),o=l=>{let s=t("terminal.unknown"),u="info";switch(l){case gt.Waiting:s=t("terminal.Waiting for execution"),u="info";break;case gt.Connecting:s=t("terminal.Connecting"),u="warning";break;case gt.Executing:s=t("terminal.Executing"),u="warning";break;case gt.Success:s=t("terminal.Successful execution"),u="success";break;case gt.Failed:s=t("terminal.Execution failed"),u="danger";break;case gt.Unknown:s=t("terminal.Unknown execution result"),u="danger";break}return{statusText:s,statusType:u}},r=()=>{bm.confirm(t("terminal.Are you sure you want to republish?"),t("Reminder"),{confirmButtonText:t("Confirm"),cancelButtonText:t("Cancel"),type:"warning"}).then(()=>{n.addTaskPM("web-build")})},a=()=>{bm.confirm(t("Setup will restart. Are you sure you want to switch package manager?"),t("Reminder"),{confirmButtonText:t("Confirm"),cancelButtonText:t("Cancel"),type:"warning"}).then(()=>{window.localStorage.clear(),location.reload()})};return(l,s)=>{const u=qe("el-tag"),c=qe("el-button"),f=qe("el-icon"),d=qe("el-card"),p=qe("el-timeline-item"),m=qe("el-timeline"),v=qe("el-empty"),h=qe("el-button-group"),C=qe("el-dialog"),g=qs("blur");return T(),V(Ve,null,[K(C,mt(l.$attrs,{modelValue:i(n).state.show,"onUpdate:modelValue":s[5]||(s[5]=y=>i(n).state.show=y),title:i(t)("terminal.Terminal")+" - "+i(n).state.packageManager,class:"ba-terminal-dialog","append-to-body":!0,"close-on-click-modal":!1}),{default:X(()=>[i(n).state.taskList.length?(T(),re(m,{key:0},{default:X(()=>[(T(!0),V(Ve,null,bt(i(n).state.taskList,(y,_)=>(T(),re(p,{key:_,class:N(["task-item","task-status-"+y.status]),type:o(y.status).statusType,center:"",timestamp:y.createtime,placement:"top"},{default:X(()=>[K(d,null,{default:X(()=>[F("div",null,[K(u,{type:o(y.status).statusType},{default:X(()=>[Ge(le(o(y.status).statusText),1)]),_:2},1032,["type"]),(y.status==i(gt).Failed||y.status==i(gt).Unknown)&&y.blockOnFailure?(T(),re(u,{key:0,class:"block-on-failure-tag",type:"warning"},{default:X(()=>[Ge(le(i(t)("terminal.Failure to execute this command will block the execution of the queue")),1)]),_:1})):te("",!0),y.status==i(gt).Connecting||y.status==i(gt).Executing?(T(),re(u,{key:1,class:"block-on-failure-tag",type:"danger"},{default:X(()=>[Ge(le(i(t)("terminal.Do not refresh the browser")),1)]),_:1})):te("",!0),F("span",jle,le(y.command),1),F("div",Wle,[y.status==i(gt).Failed||y.status==i(gt).Unknown?tt((T(),re(c,{key:0,title:i(t)("Retry"),size:"small",type:"warning",icon:i(XC),circle:"",onClick:b=>i(n).retryTask(_)},null,8,["title","icon","onClick"])),[[g]]):te("",!0),tt(K(c,{onClick:b=>i(n).delTask(_),title:i(t)("delete"),size:"small",type:"danger",icon:i(YC),circle:""},null,8,["onClick","title","icon"]),[[g]])])]),y.status!=i(gt).Waiting?(T(),V(Ve,{key:0},[y.status!=i(gt).Connecting&&y.status!=i(gt).Executing?(T(),V("div",{key:0,onClick:b=>i(n).setTaskShowMessage(_),class:"toggle-message-display"},[F("span",null,le(i(t)("terminal.Command run log")),1),K(f,{size:"16",color:"#909399"},{default:X(()=>[y.showMessage?(T(),re(i(pf),{key:0})):(T(),re(i(Nr),{key:1}))]),_:2},1024)],8,Kle)):te("",!0),y.status==i(gt).Connecting||y.status==i(gt).Executing||y.status>i(gt).Executing&&y.showMessage?(T(),V("div",{key:1,class:N(["exec-message","exec-message-"+y.uuid])},[(T(!0),V(Ve,null,bt(y.message,(b,w)=>(T(),V("pre",{key:w,class:"message-item"},le(b),1))),128))],2)):te("",!0)],64)):te("",!0)]),_:2},1024)]),_:2},1032,["class","type","timestamp"]))),128))]),_:1})):(T(),re(v,{key:1,"image-size":80,description:i(t)("terminal.No mission yet")},null,8,["description"])),K(h,null,{default:X(()=>[tt((T(),re(c,{class:"terminal-menu-item",onClick:s[0]||(s[0]=y=>i(n).addTaskPM("test",!1))},{default:X(()=>[Ge(le(i(t)("terminal.Test command")),1)]),_:1})),[[g]]),tt((T(),re(c,{class:"terminal-menu-item",onClick:s[1]||(s[1]=y=>i(n).addTaskPM("web-install"))},{default:X(()=>[Ge(le(i(t)("terminal.Install dependent packages")),1)]),_:1})),[[g]]),tt((T(),re(c,{class:"terminal-menu-item",onClick:s[2]||(s[2]=y=>r())},{default:X(()=>[Ge(le(i(t)("terminal.Republish")),1)]),_:1})),[[g]]),tt((T(),re(c,{class:"terminal-menu-item",onClick:s[3]||(s[3]=y=>i(n).addTask("version.npm",!1))},{default:X(()=>[Ge("npm -v")]),_:1})),[[g]]),tt((T(),re(c,{class:"terminal-menu-item",onClick:a},{default:X(()=>[Ge(le(i(t)("Switch package manager")),1)]),_:1})),[[g]]),tt((T(),re(c,{class:"terminal-menu-item",onClick:s[4]||(s[4]=y=>i(n).clearSuccessTask())},{default:X(()=>[Ge(le(i(t)("terminal.Clean up task list")),1)]),_:1})),[[g]])]),_:1})]),_:1},16,["modelValue","title"]),K(C,{onClose:s[12]||(s[12]=y=>i(n).togglePackageManagerDialog(!1)),modelValue:i(n).state.showPackageManagerDialog,"onUpdate:modelValue":s[13]||(s[13]=y=>i(n).state.showPackageManagerDialog=y),class:"ba-terminal-dialog",title:i(t)("Please select package manager"),center:""},{footer:X(()=>[F("div",qle,[K(c,{onClick:s[6]||(s[6]=y=>i(Ua)("npm"))},{default:X(()=>[Ge("npm")]),_:1}),K(c,{onClick:s[7]||(s[7]=y=>i(Ua)("cnpm"))},{default:X(()=>[Ge("cnpm")]),_:1}),K(c,{onClick:s[8]||(s[8]=y=>i(Ua)("pnpm"))},{default:X(()=>[Ge("pnpm")]),_:1}),K(c,{onClick:s[9]||(s[9]=y=>i(Ua)("yarn"))},{default:X(()=>[Ge("yarn")]),_:1}),K(c,{onClick:s[10]||(s[10]=y=>i(Ua)("ni"))},{default:X(()=>[Ge("ni")]),_:1}),K(c,{onClick:s[11]||(s[11]=y=>i(Ua)("none"))},{default:X(()=>[Ge(le(i(t)("I want to execute the command manually")),1)]),_:1})])]),default:X(()=>[F("div",Ule,le(i(t)("Switch package manager title")),1)]),_:1},8,["modelValue","title"])],64)}}}),ui=(e,t)=>{const n=e.__vccOpts||e;for(const[o,r]of t)n[o]=r;return n},Gle=ui(Yle,[["__scopeId","data-v-c55edab5"]]),g$="/install/assets/logo.svg",Xle="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAADIAgMAAADQNkYNAAAACVBMVEUAAADc3eTc3+YFvQraAAAAAnRSTlMA9btEy6sAAAGFSURBVGje7dixbcNAEERR24EBuwSX4CpYimNVoRJYggIF1lTpAr6xPxgo2wkpHMEnUXe7+7LZbDabzWazeVo+/rv4My75Pnjt9XdckpPX3jOteMudF79yHZZ85sGLl9wmSnKAkkyYJCcpyURJ7qQk14GSPEhJbhMlOUCZMEmSk5RkoiR3UAbMa0LMJXxafmy34UOcpPA14ue8i2L4rIbhTQyjFGKUQoxTiFEKMUohRinEOIUYpxDjFGJIUQwphiFFMaQoBhTHkOIYUBwDimNAcQwpjgHFMaA4BhTHgOIYUBwDimNAcQwoggHFMaQ4hhTHkOIYUhxDimNIcczRL+kfzPn9l+w/Zf/C+GvZv/z+F+v/yL5d9JuSb339Bttv435Y9EeSH3z98eqHeF8qeEHSlz1eXPUlnBeKRTnaF71eWvcFvLcJfTPiLU/VWPXtmzeJfSvqDW/fVnvz3o8IfBBRjDv6oYqPbvoBkY+h+mGXj9T6wZ2PB/shpI86N5vNZrPZbDbPyh8nhMFbtczXEQAAAABJRU5ErkJggg==",Jle=Object.freeze(Object.defineProperty({__proto__:null,default:Xle},Symbol.toStringTag,{value:"Module"})),b$="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAADIAgMAAADQNkYNAAAADFBMVEUAAAD2bGz1bGz1bGxNefxdAAAAA3RSTlMAVaoLuSc5AAABi0lEQVRo3u3YwXHCQBBEUeOLy+UgHIJTUCjOBDJzCgqBIDhwQe0APrX/0MVt+ihqVXogdmfmbTKZTCaTyWTysnw+u/i7XPKz8drpvlzyt/Pax7Fa8Z4bL37nsljylQcvnnNdUZINlOS+oiQ7KMmxoiQ3UpLLgpI8SEmuK0qygZLcV5RkByU5VpTkBsoCc0qIOYdPy4/tNnyIHZTwNeLnvIti+KyG4U0MoxRilEKMU4hRCjFKIUYpxDiFGKcQ4xRiSFEMKYYhRTGkKAYUx5DiGFAcA4pjQHEMKY4BxTGgOAYUx4DiGFAcA4pjQHEMKIIBxTGkOIYUx5DiGFIcQ4pjSHHM1i/pH8z5/ZfsP2X/wvhr2b/8/hfr/8i+XfSbkm99/Qbbb+N+WPRHkh98/fHqh3hfKnhB0pc9Xlz1JZwXikU52he9Xlr3Bby3CX0z4i1P1Vj17Zs3iX0r6g1v31Z7896PCHwQUYw7+qGKj276AZGPofphl4/U+sGdjwf7IaSPOieTyWQymUwmr8o/BXYQUa5D7j4AAAAASUVORK5CYII=",Zle=Object.freeze(Object.defineProperty({__proto__:null,default:b$},Symbol.toStringTag,{value:"Module"})),Qle="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAMAAACahl6sAAAAmVBMVEUAAABnwjpmwjlnwjpnwztnwjpnwjpnwjpmwzpowzpnwzlnwTtnwjpnwjpmwjlnwzpnwzpnwjpnwjlnwzpnxDltuTZnwjlnwjpnwzpnwzpnwzpnwjpnwjpnwjpnwjhlwDdixDtnwjpnwzlnwjpnwjlowjpnwjpnwzlnwjpnwjpnwjlmwjpnwjpnwjpnwjpmwTpmwjtnwjlnwjrJKS+EAAAAMnRSTlMA7GidKOfFulROSBng2c5vYVs7Mw8Fv7WqfnX1o0MiEwr61dKwlpOJh3o/LvHKgjYdj7mKH34AAANoSURBVHja7dzpUuJQFATgEwhhR1BAZBdwYRu13//hpqxxSsVziQEh98T+fltSVAHp7nARIiIiIiIiIiIiIiIiIiIiIqJfYdJcIKhH/Y7Y9jTDP9uSWFbBu6bY1ccHg7VYNcInRTFqgh1lsekWO/Ji0ha7ArHoDl9sxKAcvqqLPU9QRGJOB5qlmBNAUW+LNUVoLsSaB2iuxZoWNLOxGBNCM1iJNXhnOsTXobkRaxZ4ZT8u3kDTnYoxZWhqoRjzDFVFrNlAkxNrGtA8iDVDaHpizQU024kYs4JmcyXGtKGyN5QWoOmLNV1o7sWa+4xUqSY0s2cx5goqe1XqFpqWWDNjlfLKY0aqVAmajbkqFWalSgUZqVIRNI9iTVaq1BKambkqVYHKXJWqZqVK1TJyN6eY6SpVlJMbh1P5QRfQBKeuUq1eDcDsfiw/5BKqSzmp6R3eFPo/9B8HaVSpKj7odeQHNM5apfRHvW7L0XrQDOW0cvisODlNlZp35LQi7OhWT7H9IJQTC7ArGssR1qlUKT0RNUI5XCGlKrWCYr6WQ3XTqlJVaOqHZu18OsnE3aprl3KIPjRBW84ggqowkuRKaVapCnRBRZIap1ulltANypJQPeUq1YPutiSJXKc+8g4BHB9XX6CZyznl4dBMktlUz3JWdwCOuyKPUqlS+uvimLfqJPDlrtQjHPryHVt/Rt4/cLhJ8HHhxRGdJRz+HPgFv6Aj6ejD4UH2a7mqVFou4PASc4RCNZL05OBwJ27Tmod3pZpwyItTMfVkomnBYZjsWtqQtJVuoet1kryvPDjTUh5At2hrfw2VF6dVKwF01xPZFQ7wytMjnqsCFOp0N3dcQT1xWYNCme4Wvn8x7qoO3TaMPytVEI+Ec+jm69iLTlV8EjaAmOlu5UmVivEcAXunu2rBw2SiqXaxd7qLvEwmmnZx33SX9zSZaKYL6DZlVwvz9NdNOu7pDip/D3cO4eBzMkky3fmdTL5fN3xPJt+d7vxPJgolUXlbpRJOd7onMeAGHxlJJrHTnZlkEjMxGEomqhz2EUOaeGWhSsVpmUwmjukuI2eMygOLyUSf7iwmE82oYKVKxU931o8e/PdUt5hMNOtGNp6HSPsBb7pmX1dvRvfFGqKlb5MiEREREREREREREREREREREdHv8xfQb5TIVcxdIwAAAABJRU5ErkJggg==",ese=Object.freeze(Object.defineProperty({__proto__:null,default:Qle},Symbol.toStringTag,{value:"Module"})),tse="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAMAAACahl6sAAAAllBMVEUAAADnozzkoDzmojzmojzmojznozrnojzmojvmojvnojznojzmojznojzmojzmojznojznojzmojzmojzmojvnojzmojzmojzmpDvmojzmojzmojzmojzmoTvnojznojvlnjjmoDzmojzmojzmojzmojznojznozzmojznojzmoTvmojzmojznpjnmojzmojzmoDzmojxUQ0wVAAAAMXRSTlMAfz+/onQV5+GHevLRybavqJSObl9YUzcP7cRKRTIoGwYj+tedgWROmWks3LsK9s0fysZrJgAABRZJREFUeNrt3Wdv20AMBmDGQ17y3kveM178/3+uQVAUIWO7PuVO4gV8PhZFgdf2G51oqgGllFJKKaWUUkoppdTv1uvB7xCG8Cu8Ib7BLxDhhwj818APDfBeDz/twHchfrqC58r4Vxm8tljiX5cF+CyH/+TAYzv8Ygz+uuIXVfBWGYk8eGpxQaIyAz/lkFmDl8b4zQR8VMVvRuChPN7RBu/cKnjH0L++r/GuPXhmjA9kwC9VfCALXsnjQwfwyGyIDxUC8Mcan2iCNyZINX3t+wiJTUA/aSfwRBuJ929/sgUvzApIdAFg5GPf90jU7rSmBR7gTZ/eS9cH+UZ3X/xZhb1N4rFeFwb3r/UdEC5gTd8+OH0VByBbE4nsw/NwCUTLPDm053zqe/bJbdTt4k/fD0gMA/jkXd+D4vNRw5UeXeT2nTW9CszOk77zpo//N3ucgkwnJHLwTbTEr+og0haJyuL/8/kuCDR4R6IMd8xD/Golse8tJEK4q4fEBsTpI9WD+xrS+17D13p8xH9ErkSwpi+P8MgGCWErUOcVEqWX/2p4BklY01dzeKyLn0SuRPSXJufBOv8UylFHkxP6FD+IXInooNk9U0lo33nTW4aHgOscZCih6VyhI3IlYnpB4mB8+bzI6Hs9xndrfYF9503PmF14xKw8zkMk9rFu76vp9501vTAjN7/ZL+hbtRXW92kFifzju/jMsxvjSgTpavCtmZeDZEStQHWRGj8PwvsuZuVxfuU/Rg2C3ApyVqA2SAwXJkHgIGYF6sia/gYmQfjIe5he3xt8RGoYZCJkJYI3vWsaBPYy+l7lAxHjIIuhhBWoNyQqR/Mg0BbQ92jIBycxgvCViAUkL4dEOI8VZJd633tIdSBWEFinveI84iPSmEGOlXRXoMpILPtxg0A+1ZXHqMAHJ7GDnNlKxA2StEZiFcQPAr0UV6B2SB3AJAiXS6/vWT4i/VGQ6TKtlYgyUhPDIFw5pb4vivwq9sMggzCdvu+RKM6Mg3DdVPrOm54H8yBcI40V5xMfkVoI0sfkVyLySO1sBIFN4ivOtyKfR1kJEqzoCtQMXGsiUYjsBIFOwitQY6TKYCkI1JPt+4mPSK0FySTa9zZSXXtBoJRg32fvfERqLwj/x1e87w6bPpzCc/z7kee2ifV9gj/dthLS9xoS1bPbF6oBjhzQ9QZyK5EV59nK9QvGbxDCAFxoIVHJgH3tBFYeJ8skfqjU+JTJvjom8baPnX98D5jMhbfpuO9BiETdfFseXhLRvl8DsKuExHLiKgjknfY9c+HjAWdB5nRodum7bPrq5i4I9ByuQG2RaoPDIHzc1HXX9Bo4DTKlo/7qwFXTcew2CB+hvllreoWPSB0HGYzoYWgKdjSQKEaug0DXSd8730akzoNAzkHfB1cksvMEgvSHdFgzsL/KhD1IIAhsrD/S0B/GXdnjwwcwEVTZmMP2gkNhCongzVzbXmUqQ1IaVh9pGFTRtHWOrl7Zs81VJuxCckoWPwv9QorL+LOQtvNor+nDPiRpy/pur+kbSFbdUt/PIySqASRrsrTT97LBiNSNlpVHGqaF1B+rXdApbTFe39euR6Tm07R9vKVFCf/VRC3OI0x8KiPQaQ6m8iiScd+PRRSpGBmvMgnVNF1lEmtn1PQTinWa+99040caoncU7D0y+NZItJf7Pkbhxi8fC4SrwUvaKN5LfV+8ecDv38qilFJKKaWUUkoppZRSSimllFIv+wNEulUVw2/KggAAAABJRU5ErkJggg==",nse=Object.freeze(Object.defineProperty({__proto__:null,default:tse},Symbol.toStringTag,{value:"Module"})),ose="/install/assets/loading.gif",rse="/install/assets/lang.svg",ase=e=>(Xd("data-v-631c568f"),e=e(),Jd(),e),lse={class:"lang"},sse=["src"],ise={class:"lang-list"},use=ase(()=>F("span",{class:"lang-list-arrow"},null,-1)),cse={class:"logo-box"},dse=["src"],fse={class:"title"},pse=Y({__name:"index",setup(e){const{t}=Bl(),n=o=>{window.localStorage.setItem("ba-lang",o),location.reload()};return(o,r)=>(T(),V(Ve,null,[F("div",lse,[F("img",{src:i(rse),alt:"lang icon"},null,8,sse),F("div",ise,[use,F("div",{onClick:r[0]||(r[0]=a=>n("zh-cn")),class:"lang-item"},"中文简体"),F("div",{onClick:r[1]||(r[1]=a=>n("en")),class:"lang-item"},"English")])]),F("div",cse,[F("img",{class:"logo",alt:"Build Admin logo",src:i(g$)},null,8,dse),F("div",fse,le(i(t)("Install BuildAdmin")),1)])],64))}}),Kf=ui(pse,[["__scopeId","data-v-631c568f"]]);var hse={VITE_PORT:"2828",VITE_OPEN:"false",VITE_BASE_PATH:"/install/",BASE_URL:"/install/",MODE:"production",DEV:!1,PROD:!0,SSR:!1};const mse={class:"container"},vse={class:"table-title"},gse={class:"table"},bse={key:0,class:"global-warning"},yse={target:"_blank",href:"https://doc.buildadmin.com/guide/install/start.html"},wse={class:"table-label"},_se=["title","onClick"],Cse={class:"table-value"},Sse=["title","src","alt"],kse={key:1,class:"table-item"},Ese={class:"table-label"},Tse={class:"table-value"},$se=["title","alt"],Ose={class:"block-help"},Nse={class:"start-install"},Ise=Y({__name:"check",setup(e){const{t,locale:n}=Bl(),o=ju(),r=ii(),a=Et({envCheckData:[],stateTitle:{ok:"Environmental inspection passed",fail:"This environmental check failed",warn:"The environment check failed, but the installation can continue"},checkType:{base:"Basic environment",npm:"NPM correlation",npminstall:"Test npm install",done:"Check complete"},checkTypeIndex:"base",checkDone:{ok:"Congratulations, the installation can continue~",fail:"Sorry, the necessary installation environment conditions have not been met, please check the above form!",executing:"executing"},checkDoneIndex:"executing",startForm:{lang:n.value,packageManager:r.state.packageManager,setNpmRegistry:"taobao"},showPortErrorPrompt:!1}),l=Object.assign({"../assets/img/install/close.png":Jle,"../assets/img/install/fail.png":Zle,"../assets/img/install/ok.png":ese,"../assets/img/install/warn.png":nse}),s=w=>{const S=`../assets/img/install/${w}.png`;return l[S].default},u=w=>{window.localStorage.setItem("ba-lang",w),location.reload()},c=()=>{o.state.showStartDialog&&Ua(a.startForm.packageManager),o.toggleStartDialog(!1),Lle().then(w=>{if(w.data.code!=1)return p(w.data.msg);m(),a.envCheckData=w.data.data})},f=w=>{w.type=="faq"?window.open(w.url):w.type=="install-package-manager"?(a.checkDoneIndex="executing",v(),r.toggle(!0),r.addTaskPM("install",!0,"",S=>{r.toggle(!1),y(),S==gt.Failed?p(t("Sorry, the automatic installation of package manager failed. Please complete the installation manually!")):S==gt.Success&&(a.envCheckData=Object.assign({},a.envCheckData,{success:{describe:t("PM is ready!"),state:"ok",link:[]}}),a.envCheckData=Object.assign({},a.envCheckData,{npm_package_manager:{describe:t("already installed"),state:"ok",link:[],pm:r.state.packageManager}}),h())})):w.type=="test-npm-install"&&C()},d=()=>{a.checkDoneIndex=="ok"&&o.setStep("config")},p=w=>{y(),a.checkDoneIndex=="fail"&&(a.checkDoneIndex="executing"),Cr({type:"error",message:w,duration:0,center:!0})},m=()=>{a.checkTypeIndex="npm",xle().then(w=>{if(y(),w.data.code==2)return!1;if(w.data.code!=1)return p(w.data.msg);a.envCheckData=Object.assign({},a.envCheckData,w.data.data),w.data.data.npm_package_manager.state=="ok"&&(v(),h())})},v=()=>{if(a.startForm.setNpmRegistry!="none"){let w=!1;for(const S in r.state.taskList)if(r.state.taskList[S].command=="set-npm-registry."+a.startForm.setNpmRegistry&&r.state.taskList[S].status==gt.Success){w=!0;break}w||r.addTask("set-npm-registry."+a.startForm.setNpmRegistry,!1,"",S=>{if(S==gt.Failed){let E={"set-npm-registry":{describe:t("Command execution failed"),state:"fail",link:[{name:t("How to solve"),title:t("Click to see how to solve it"),type:"faq",url:"https://doc.buildadmin.com/guide/install/setNpmRegistryFail.html"}]}};a.envCheckData=Object.assign({},a.envCheckData,E)}})}},h=()=>{a.envCheckData=Object.assign({},a.envCheckData,{"check npm install":{describe:"",state:"warn",link:[{name:t("Click to test"),title:t("Click to test")+" npm install",type:"test-npm-install"}]}})},C=()=>{a.checkDoneIndex="executing",a.checkTypeIndex="npminstall",g("check npm install"),r.toggle(!0),r.addTaskPM("test",!0,"",w=>{if(y(),r.toggle(!1),w==gt.Failed){let S={"test-npm-install":{describe:t("Command execution test failed"),state:"warn",link:[{name:t("How to solve"),title:t("Click to see how to solve it"),type:"faq",url:"https://doc.buildadmin.com/guide/install/npmInstallFail.html"}]}};a.envCheckData=Object.assign({},a.envCheckData,S)}else if(w==gt.Success){let S={"test-npm-install":{describe:t("Can execute"),state:"ok",link:[]}};a.envCheckData=Object.assign({},a.envCheckData,S)}})},g=w=>{delete a.envCheckData[w]},y=()=>{a.checkTypeIndex="done";let w=["php_version","config_is_writable","public_is_writable","php_pdo","php_gd2"];for(const S in w)if(!a.envCheckData[w[S]]||a.envCheckData[w[S]].state!="ok")return a.checkDoneIndex="fail",!1;return a.checkDoneIndex="ok",!0},_=()=>r.state.packageManager=="none"?t("None - manual execution"):r.state.packageManager,b=()=>{let w=hse.VITE_AXIOS_BASE_URL;return w=w=="getCurrentDomain"||!w?window.location.protocol+"//"+window.location.host:w,new URL(w).port};return at(()=>{o.state.showStartDialog||c(),b()!="8000"&&(a.showPortErrorPrompt=!0)}),(w,S)=>{const E=qe("el-alert"),$=qe("el-option"),O=qe("el-select"),A=qe("el-form-item"),M=qe("el-radio"),D=qe("el-radio-group"),U=qe("el-form"),j=qe("el-icon"),W=qe("el-button"),L=qe("el-dialog");return T(),V("div",null,[K(Kf),F("div",mse,[F("div",vse,le(i(t)("Environmental inspection")),1),F("div",gse,[a.showPortErrorPrompt?(T(),V("div",bse,[K(E,{closable:!1,center:"",type:"error"},{default:X(()=>[Ge(le(i(t)("Port error prompt 1"))+" ",1),F("a",yse,le(i(t)("Get started quickly")),1),Ge(" "+le(i(t)("Port error prompt 3")),1)]),_:1})])):te("",!0),K(ku,{name:"slide-bottom"},{default:X(()=>[(T(!0),V(Ve,null,bt(a.envCheckData,(P,x)=>(T(),V("div",{class:N(["table-item",x]),key:x+P.describe+P.state},[F("div",wse,[Ge(le(x.toString()=="npm_package_manager"?i(t)(x)+" "+_():i(t)(x))+" ",1),P.link&&P.link.length>0?(T(!0),V(Ve,{key:0},bt(P.link,(I,H)=>(T(),V("span",{key:H,title:I.title?I.title:"",onClick:G=>f(I),class:N(["label-need",I.type])},le(I.name),11,_se))),128)):te("",!0)]),F("div",Cse,[Ge(le(P.describe)+" ",1),F("img",{title:i(t)(a.stateTitle[P.state]),class:"data-state",src:s(P.state),alt:P.state},null,8,Sse)])],2))),128))]),_:1}),a.checkTypeIndex!="done"?(T(),V("div",kse,[F("div",Ese,le(i(t)("Checking installation environment")),1),F("div",Tse,[Ge(le(i(t)(a.checkType[a.checkTypeIndex]))+" ",1),F("img",{title:i(t)("Current execution to:")+i(t)(a.checkType[a.checkTypeIndex]),class:"data-state",src:ose,alt:i(t)(a.checkType[a.checkTypeIndex])},null,8,$se)])])):te("",!0),F("div",{class:N(["check-done",a.checkDoneIndex])},le(i(t)(a.checkDone[a.checkDoneIndex])),3),F("div",{class:N(["button",a.checkDoneIndex=="ok"?"pass":""]),onClick:d},le(i(t)("Step 2 site configuration")),3)])]),K(L,{modelValue:i(o).state.showStartDialog,"onUpdate:modelValue":S[3]||(S[3]=P=>i(o).state.showStartDialog=P),"close-on-click-modal":!1,"close-on-press-escape":!1,"show-close":!1,"destroy-on-close":!0,class:"ba-terminal-dialog",title:i(t)("Ready to start"),center:""},{footer:X(()=>[K(W,{onClick:c,type:"primary",size:"large",round:""},{default:X(()=>[K(j,null,{default:X(()=>[K(i(D4))]),_:1}),F("span",Nse,le(i(t)("Start installation")),1)]),_:1})]),default:X(()=>[K(U,{onKeyup:Pt(c,["enter"]),class:"start-from","label-position":"left","label-width":"120px",model:a.startForm},{default:X(()=>[K(A,{label:i(t)("language")},{default:X(()=>[K(O,{onChange:u,class:"w100",modelValue:a.startForm.lang,"onUpdate:modelValue":S[0]||(S[0]=P=>a.startForm.lang=P)},{default:X(()=>[K($,{label:"中文简体",value:"zh-cn"}),K($,{label:"English",value:"en"})]),_:1},8,["modelValue"])]),_:1},8,["label"]),K(A,{label:i(t)("NPM package manager")},{default:X(()=>[K(O,{class:"w100",modelValue:a.startForm.packageManager,"onUpdate:modelValue":S[1]||(S[1]=P=>a.startForm.packageManager=P)},{default:X(()=>[K($,{label:"npm",value:"npm"}),K($,{label:"cnpm",value:"cnpm"}),K($,{label:"pnpm"+i(t)("recommend"),value:"pnpm"},null,8,["label"]),K($,{label:"yarn"+i(t)("recommend"),value:"yarn"},null,8,["label"]),K($,{label:"ni",value:"ni"}),K($,{label:i(t)("I want to execute the command manually"),value:"none"},null,8,["label"])]),_:1},8,["modelValue"]),F("div",Ose,le(i(t)("The system has a Web terminal. Please select an installed or your favorite NPM package manager")),1)]),_:1},8,["label"]),K(A,{label:i(t)("Set NPM source")},{default:X(()=>[K(D,{modelValue:a.startForm.setNpmRegistry,"onUpdate:modelValue":S[2]||(S[2]=P=>a.startForm.setNpmRegistry=P),class:"ml-4"},{default:X(()=>[K(M,{label:i(t)("Use current source"),value:"none",size:"large"},null,8,["label"]),K(M,{label:i(t)("TaoBao"),value:"taobao",size:"large"},null,8,["label"]),K(M,{label:"NPM",value:"npm",size:"large"}),K(M,{label:i(t)("Tencent"),value:"tencent",size:"large"},null,8,["label"])]),_:1},8,["modelValue"])]),_:1},8,["label"])]),_:1},8,["onKeyup","model"])]),_:1},8,["modelValue","title"])])}}}),Mse=ui(Ise,[["__scopeId","data-v-e96f3865"]]),y$=e=>(Xd("data-v-efc9ce06"),e=e(),Jd(),e),Ase={class:"container"},Pse={class:"table-title"},Rse={key:0,class:"install-tips-box"},Lse={class:"install-tips"},xse=["alt"],Dse={class:"install-tips-title"},Fse={class:"install-tips-item"},Bse={class:"install-tips-item"},Vse={class:"change-route",href:"https://gitee.com/wonderful-code/buildadmin/issues",target:"_blank"},Hse={class:"table"},zse={key:0,class:"table-item-br"},jse={key:1,class:"table-column table-item"},Wse={key:0,class:"block-help"},Kse={class:"connecting-prompt"},Use={class:"footer-buttons"},qse={class:"phinx-fail-box"},Yse={class:"title"},Gse={class:"content-item"},Xse={class:"content-item"},Jse={class:"command"},Zse={class:"content-item"},Qse=y$(()=>F("div",{class:"command"},"php think migrate:run",-1)),eie={class:"block-help"},tie={class:"content-item"},nie={class:"text"},oie=y$(()=>F("div",{class:"output-box"},[F("div",{class:"output"},"PS E:\\build-admin> php think migrate:run"),F("div",{class:"output mt10"},"== 20230620180908 Install: migrating"),F("div",{class:"output"},"== 20230620180908 Install: migrated 0.0165s"),F("div",{class:"output mt10"},"== 20230620180916 InstallData: migrating"),F("div",{class:"output"},"== 20230620180916 InstallData: migrated 0.0573s"),F("div",{class:"output mt10"},"All Done. Took 0.0898s")],-1)),rie={class:"block-help mt10"},aie={class:"command"},lie={class:"phinx-fail-footer-button"},sie=Y({__name:"config",setup(e){var t,n={hostname:"",username:"",password:"",hostport:""};const{t:o}=Bl(),r=ju(),a=ii(),l=R(),s=Et({formItem:{hostname:{label:o("Mysql database address"),value:"127.0.0.1",name:"hostname",type:"text"},username:{label:o("MySQL connection user name"),value:"root",name:"username",type:"text"},password:{label:o("MySQL connection password"),value:"",name:"password",type:"password"},hostport:{label:o("MySQL connection port number"),value:"3306",name:"hostport",type:"number"},database:{label:o("Mysql database name"),value:"",name:"database",type:"text",blockHelp:""},prefix:{label:o("MySQL data table prefix"),value:"ba_",name:"prefix",type:"text"},br1:{type:"br"},adminname:{label:o("Administrator user name"),value:"admin",name:"adminname",type:"text"},adminpassword:{label:o("Administrator password"),value:"",name:"adminpassword",type:"password",placeholder:o("Backend login password")},repeatadminpassword:{label:o("Duplicate administrator password"),value:"",name:"repeatadminpassword",type:"password"},br2:{type:"br"},sitename:{label:o("Site name"),value:"BuildAdmin",name:"sitename",type:"text"}},showFormItem:!1,showError:"",baseConfigSubmitState:!1,databaseCheck:"wait",databases:[],showInstallTips:!1,autoJumpSeconds:5,maximumCommandFailures:1,commandFailureCount:0,executionWebCommand:!0,execMigrateFail:!1,execMigrateIdx:0,rootPath:""}),u=async()=>{n={hostname:s.formItem.hostname.value,username:s.formItem.username.value,password:s.formItem.password.value,hostport:s.formItem.hostport.value,database:s.formItem.database.value},n.hostname&&n.username&&n.hostport&&(s.databaseCheck="connecting",await Dle(n).then(y=>{if(y.data.code==1)s.databaseCheck="connect-ok",s.databases=y.data.data.databases,s.formItem.database.value&&c.findDatabase(s.formItem.database.value);else throw s.databaseCheck="wait",s.databases=[],Cr({type:"error",message:y.data.msg,center:!0}),new Error(y.data.msg)}))},c={required:(y,_,b)=>_.value==""||!_.value?b(new Error(_.label+o("Required"))):b(),findDatabase:y=>{s.databaseCheck=="connect-ok"&&(y&&s.databases.indexOf(y)===-1?s.formItem.database.blockHelp=o("The entered database was not found!"):s.formItem.database.blockHelp="")},database:(y,_,b)=>(c.findDatabase(_.value),b()),connect:(y,_,b)=>{let w=!1;for(const S in n)n[S]!=s.formItem[S].value&&(w=!0);return w&&u(),b()},prefix:function(y,_,b){if(_.value){var w=new RegExp(/^[a-zA-Z][a-zA-Z0-9_]*$/i);if(!w.test(_.value))return b(new Error(o("The table prefix can only contain alphanumeric characters and underscores, and starts with a letter")))}return b()},adminname:function(y,_,b){return/^[a-zA-Z][a-zA-Z0-9_]{2,15}$/.test(_.value)?b():b(new Error(o("It is composed of letters, numbers and underscores, starting with letters (3-15 digits)")))},adminpassword:function(y,_,b){return/^(?!.*[&<>"'\n\r]).{6,32}$/.test(_.value)?b():b(new Error(o("Please enter the correct password")))},repeatadminpassword:function(y,_,b){return s.formItem.adminpassword.value&&_.value&&s.formItem.adminpassword.value!=_.value?b(new Error(o("Duplicate passwords do not match"))):b()}},f=Et({hostname:[{validator:c.required,trigger:"blur"}],username:[{validator:c.required,trigger:"blur"}],hostport:[{validator:c.required,trigger:"blur"}],database:[{validator:c.required,trigger:"blur"},{validator:c.database,trigger:"blur"}],prefix:[{validator:c.connect,trigger:"blur"},{validator:c.prefix,trigger:"blur"}],adminname:[{validator:c.required,trigger:"blur"},{validator:c.connect,trigger:"blur"},{validator:c.adminname,trigger:"blur"}],adminpassword:[{validator:c.required,trigger:"blur"},{validator:c.connect,trigger:"blur"},{validator:c.adminpassword,trigger:"blur"}],repeatadminpassword:[{validator:c.required,trigger:"blur"},{validator:c.repeatadminpassword,trigger:"blur"}],sitename:[{validator:c.required,trigger:"blur"}]}),d=y=>{window.open(y)},p=y=>{s.showError=y},m=()=>{a.addTask("migrate.run",!0,"",(y,_)=>{y==gt.Success?v():(s.execMigrateIdx=_,s.execMigrateFail=!0)})},v=()=>{s.execMigrateIdx&&a.delTask(s.execMigrateIdx),setTimeout(()=>{pw({type:"migrate",adminname:s.formItem.adminname.value,adminpassword:s.formItem.adminpassword.value,sitename:s.formItem.sitename.value}).then(()=>{h()})},1500)},h=()=>{if(s.execMigrateFail=!1,!s.executionWebCommand){s.showInstallTips=!1,r.setStep("manualInstall");return}a.toggle(!0),a.addTaskPM("web-install",!0,"",(y,_)=>{y==gt.Success?a.addTaskPM("web-build",!0,"",b=>{s.baseConfigSubmitState=!1,b==gt.Success?(pw({type:"web"}),a.toggle(!1),r.setStep("done")):b==gt.Failed&&g()}):y==gt.Failed&&(s.commandFailureCount{y&&(s.baseConfigSubmitState=!0,u().then(()=>{y.validate(_=>{if(_){let b={};for(const w in s.formItem)b=Object.assign(b,{[w]:s.formItem[w].value});Ble(b).then(w=>{w.data.code==1?(s.rootPath=w.data.data.rootPath,s.executionWebCommand=w.data.data.executionWebCommand,m()):(Cr({type:"error",message:w.data.msg,center:!0}),s.baseConfigSubmitState=!1)}).catch(()=>{s.baseConfigSubmitState=!1})}else s.baseConfigSubmitState=!1})}).catch(()=>{s.baseConfigSubmitState=!1}))};Fle().then(y=>{y.data.code==1?(s.rootPath=y.data.data.rootPath,s.showInstallTips=!y.data.data.executionWebCommand,s.executionWebCommand=y.data.data.executionWebCommand):y.data.code==0?Cr({type:"error",message:y.data.msg,center:!0,duration:0}):s.showInstallTips=!0});const g=()=>{a.toggle(!1),t=setInterval(()=>{s.autoJumpSeconds<=0?(clearInterval(t),r.setStep("manualInstall")):(s.autoJumpSeconds--,p(o("Manual Install 1")+o("Manual Install 2",{seconds:s.autoJumpSeconds})))},1e3)};return at(()=>{s.showFormItem=!0}),lr(()=>{clearInterval(t)}),(y,_)=>{const b=qe("el-input"),w=qe("el-form-item"),S=qe("el-button"),E=qe("el-alert"),$=qe("el-dialog");return T(),V(Ve,null,[F("div",null,[K(Kf),F("div",Ase,[F("div",Pse,le(i(o)("Site configuration")),1),s.showInstallTips?(T(),V("div",Rse,[F("div",Lse,[F("img",{class:"install-tips-close",onClick:_[0]||(_[0]=O=>s.showInstallTips=!1),src:b$,alt:i(o)("Close the prompt of completing unfinished matters manually")},null,8,xse),F("div",Dse,[F("span",null,le(i(o)("Install Tips Title 1")),1),F("span",{class:"change-route",onClick:_[1]||(_[1]=O=>i(r).setStep("check"))},le(i(o)("Back to previous page")),1),F("span",null,le(i(o)("Install Tips Title 2")),1)]),F("div",Fse,[Ge(le(i(o)("If you don't want to open the corresponding permission due to some security factors, please check "))+" ",1),F("span",{onClick:_[2]||(_[2]=O=>d("https://doc.buildadmin.com/guide/install/senior.html")),class:"change-route"},le(i(o)("how installation services ensure system security")),1)]),F("div",Bse,[Ge(le(i(o)("If you really can't adjust all the tests to pass, please "))+" ",1),F("a",Vse,le(i(o)("click to feed back to us")),1),Ge(" "+le(i(o)("continue installation")),1)])])])):te("",!0),F("div",Hse,[K(i(zS),{ref_key:"formRef",ref:l,"label-width":"150px",onKeyup:_[5]||(_[5]=Pt(O=>C(l.value),["enter"])),rules:f,model:s.formItem},{default:X(()=>[K(fn,{name:"slide-bottom"},{default:X(()=>[tt(F("div",{class:"table-column table-error"},le(s.showError),513),[[kt,s.showError]])]),_:1}),K(ku,{name:"slide-bottom"},{default:X(()=>[(T(!0),V(Ve,null,bt(s.formItem,(O,A)=>tt((T(),V("div",{key:A},[O.type=="br"?(T(),V("div",zse)):(T(),V("div",jse,[K(w,{prop:O.name,class:"table-label",label:O.label},{default:X(()=>[K(b,{placeholder:O.placeholder?O.placeholder:"",modelValue:O.value,"onUpdate:modelValue":M=>O.value=M,class:"table-input",type:O.type},null,8,["placeholder","modelValue","onUpdate:modelValue","type"]),O.blockHelp?(T(),V("div",Wse,le(O.blockHelp),1)):te("",!0)]),_:2},1032,["prop","label"])]))])),[[kt,s.showFormItem]])),128))]),_:1}),K(fn,{name:"slide-bottom"},{default:X(()=>[tt(F("div",null,[tt(F("div",Kse,[F("span",null,le(i(o)("Test connection to data server")),1)],512),[[kt,s.databaseCheck=="connecting"]]),F("div",Use,[K(S,{class:"button",onClick:_[3]||(_[3]=O=>i(r).setStep("check"))},{default:X(()=>[Ge(le(i(o)("Previous step")),1)]),_:1}),K(S,{type:"primary",class:"button",onClick:_[4]||(_[4]=O=>C(l.value)),loading:s.baseConfigSubmitState},{default:X(()=>[Ge(le(i(o)("Install now")),1)]),_:1},8,["loading"])])],512),[[kt,s.showFormItem]])]),_:1})]),_:1},8,["rules","model"])])])]),K($,{modelValue:s.execMigrateFail,"onUpdate:modelValue":_[6]||(_[6]=O=>s.execMigrateFail=O),top:"5vh","close-on-click-modal":!1,"close-on-press-escape":!1,"show-close":!1,title:i(o)("Table migration failed")},{default:X(()=>[K(E,{title:i(o)("We use Phinx to manage the data table, which can version the data table"),closable:!1,center:"",type:"info"},null,8,["title"]),F("div",qse,[F("div",Yse,le(i(o)("Data table automatic migration failed, please manually migrate as follows:")),1),F("div",Gse,"1、"+le(i(o)("Open terminal (windows PowerShell)")),1),F("div",Xse,[F("div",null,"2、"+le(i(o)("Execute command")),1),F("div",Jse,"cd "+le(s.rootPath),1)]),F("div",Zse,[F("div",null,"3、"+le(i(o)("Execute command")),1),Qse,F("div",eie,le(i(o)("If the command fails to be executed, add sudo or press the error message")),1)]),F("div",tie,[F("div",null,"4、"+le(i(o)("Migration check")),1),F("div",nie,le(i(o)("When the command is executed successfully, the output is similar to:")),1),oie,F("div",rie,[Ge(le(i(o)("After the command is executed successfully, multiple data tables will be automatically created in the database, and then click below to "))+" ",1),F("span",aie,le(i(o)("continue install")),1)])])]),F("div",lie,[K(S,{type:"primary",onClick:v},{default:X(()=>[Ge(le(i(o)("continue install")),1)]),_:1})])]),_:1},8,["modelValue","title"])],64)}}}),iie=ui(sie,[["__scopeId","data-v-efc9ce06"]]),uie={class:"container"},cie={class:"table-title"},die={class:"done-box"},fie={class:"reload-tips"},pie={class:"text-warning"},hie={class:"done-button"},mie=Y({__name:"done",setup(e){const{t}=Bl(),n=window.location.protocol+"//"+window.location.host,o=Et({hideIndexUrl:"https://doc.buildadmin.com/guide/install/hideIndex.html",indexUrl:n+"/index.html/#/",adminUrl:n+"/index.html/#/admin"}),r=l=>{window.open(l)},a=()=>{window.localStorage.clear(),location.reload()};return(l,s)=>{const u=qe("el-alert"),c=qe("el-button");return T(),V("div",null,[K(Kf),F("div",uie,[F("div",cie,"✨ "+le(i(t)("Thanks for using buildadmin"))+" ✨",1),F("div",die,[F("div",null,le(i(t)("Background URL")),1),F("div",{onClick:s[0]||(s[0]=f=>r(o.adminUrl)),class:"admin-url"},le(o.adminUrl),1),F("div",fie,[Ge(le(i(t)("Need to reinstall the system?")),1),F("span",{class:"reload",onClick:a},le(i(t)("Please click on me")),1)])]),F("div",pie,[K(u,{closable:!1,center:"",title:i(t)("It is recommended to delete the root directory / public / install folder; This page is only visible on your device."),type:"error"},null,8,["title"])]),F("div",hie,[K(c,{onClick:s[1]||(s[1]=f=>r(o.hideIndexUrl)),type:"primary",plain:"",size:"large"},{default:X(()=>[Ge(le(i(t)("Hide index.html?")),1)]),_:1}),K(c,{onClick:s[2]||(s[2]=f=>r(o.indexUrl)),type:"primary",plain:"",size:"large"},{default:X(()=>[Ge(le(i(t)("Access foreground")),1)]),_:1}),K(c,{onClick:s[3]||(s[3]=f=>r(o.adminUrl)),type:"primary",size:"large"},{default:X(()=>[Ge(le(i(t)("Access background")),1)]),_:1})])])])}}}),vie=ui(mie,[["__scopeId","data-v-e1e72612"]]),Wu=e=>(Xd("data-v-292784ff"),e=e(),Jd(),e),gie={class:"container"},bie={class:"title"},yie={class:"content"},wie={class:"content-item"},_ie={class:"content-item"},Cie={class:"command"},Sie={class:"content-item"},kie=Wu(()=>F("div",{class:"command"},"npm install",-1)),Eie={class:"content-item"},Tie=Wu(()=>F("div",{class:"command"},"npm run build",-1)),$ie={class:"content-item"},Oie={class:"step-box"},Nie={class:"step"},Iie={class:"text-bold"},Mie={class:"step"},Aie=Wu(()=>F("span",{class:"text-bold"},"assets",-1)),Pie=Wu(()=>F("span",{class:"text-bold"},"index.html",-1)),Rie=Wu(()=>F("span",{class:"text-bold"},"public",-1)),Lie={class:"step"},xie={class:"min-help"},Die={key:0,class:"loading"},Fie={class:"reload-tips"},Bie=Y({__name:"index",setup(e){const{t}=Bl(),n=ju(),o=Et({showLoading:"",webPath:t("Getting full path of root directory / Web")}),r=s=>{window.open(s)},a=()=>{o.showLoading=t("Moving automatically"),Vle().then(s=>{s.data.code==1?n.setStep("done"):Cr({type:"error",message:s.data.msg,center:!0})}).finally(()=>{o.showLoading=""})};Hle().then(s=>{s.data.code==1?o.webPath=s.data.data.webPath:Cr({type:"error",message:s.data.msg,center:!0})});const l=()=>{window.localStorage.clear(),location.reload()};return(s,u)=>{const c=qe("el-alert");return T(),V("div",gie,[F("div",bie,le(i(t)("Unfinished matters manually")),1),F("div",yie,[K(c,{title:i(t)("Sorry, some operations could not be completed automatically You need to manually complete the outstanding matters according to the following guidance"),type:"error"},null,8,["title"]),F("div",wie,"1、"+le(i(t)("Open terminal (windows PowerShell)")),1),F("div",_ie,[F("div",null,"2、"+le(i(t)("Execute command")),1),F("div",Cie,"cd "+le(o.webPath),1)]),F("div",Sie,[F("div",null,"3、"+le(i(t)("Execute command")),1),kie,F("div",{onClick:u[0]||(u[0]=f=>r("https://doc.buildadmin.com/guide/install/npmInstallFail.html")),class:"block-help link"},le(i(t)("Execution failed?")),1)]),F("div",Eie,[F("div",null,"4、"+le(i(t)("Execute command")),1),Tie,F("div",{onClick:u[1]||(u[1]=f=>r("https://doc.buildadmin.com/guide/install/npmBuildFail.html")),class:"block-help link"},le(i(t)("Execution failed?")),1)]),F("div",$ie,[F("div",null,"5、"+le(i(t)("Move the built file to the specified location of the system")),1),F("div",{onClick:a,class:"block-help link size-15"},le(i(t)("Click to try to automatically move the build file")),1),F("div",Oie,[F("div",Nie,[Ge(" 1. "+le(i(t)("The build output directory is: site")),1),F("span",Iie,le(i(t)("root directory / dist")),1)]),F("div",Mie,[Ge(" 2. "+le(i(t)("Please move 1")),1),Aie,Ge(le(i(t)("Please move 2")),1),Pie,Ge(le(i(t)("Please move 3")),1),Rie,Ge(le(i(t)("Please move 4")),1)]),F("div",Lie,"3. "+le(i(t)("You can delete the build output directory directly")),1)]),F("div",xie,le(i(t)("During construction, all files in the output directory will be overwritten, so the system is designed to build in the root directory first, and then move to the public directory to prevent other files in the public from being overwritten")),1)]),o.showLoading?(T(),V("div",Die,le(o.showLoading),1)):te("",!0)]),F("div",Fie,[Ge(le(i(t)("Need to reinstall the system?")),1),F("span",{class:"reload",onClick:l},le(i(t)("Please click on me")),1)])])}}}),Vie=ui(Bie,[["__scopeId","data-v-292784ff"]]),Hie=Y({__name:"manualInstall",setup(e){return(t,n)=>(T(),V("div",null,[K(Kf),K(Vie)]))}}),zie={class:"ba-terminal"},jie=["src"],Wie=Y({__name:"App",setup(e){const t=ju(),n=ii(),{locale:o,getLocaleMessage:r}=Bl();var a=window.localStorage.getItem("ba-lang")||"zh-cn";o.value=a;const l=r(a);return at(()=>{n.init()}),(s,u)=>{const c=qe("el-badge"),f=qe("el-config-provider");return T(),re(f,{locale:i(l)},{default:X(()=>[i(t).state.step=="check"?(T(),re(Mse,{key:0})):te("",!0),i(t).state.step=="config"?(T(),re(iie,{key:1})):te("",!0),i(t).state.step=="done"?(T(),re(vie,{key:2})):te("",!0),i(t).state.step=="manualInstall"?(T(),re(Hie,{key:3})):te("",!0),K(Gle),F("div",zie,[K(c,{"is-dot":i(n).state.showDot},{default:X(()=>[F("img",{onClick:u[0]||(u[0]=d=>i(n).toggle()),class:"terminal-logo",draggable:"false",src:i(g$),alt:"BuildAdmin Logo"},null,8,jie)]),_:1},8,["is-dot"])])]),_:1},8,["locale"])}}});function Kie(e){Yie(e),qie(e),Uie(e)}function Uie(e){e.directive("blur",{mounted(t){t.addEventListener("focus",()=>{t.blur()})}})}function qie(e){e.directive("zoom",{mounted(t,n){if(!n.value)return!1;We(()=>{const o=document.querySelector(n.value),r=document.createElement("div");r.className="zoom-handle",r.onmouseenter=()=>{r.onmousedown=a=>{const l=a.clientX,s=a.clientY,u=o.offsetWidth,c=o.offsetHeight;document.onmousemove=f=>{f.preventDefault();const d=u+(f.clientX-l)*2,p=c+(f.clientY-s);o.style.width=`${d}px`,o.style.height=`${p}px`},document.onmouseup=function(){document.onmousemove=null,document.onmouseup=null}}},o.appendChild(r)})}})}function Yie(e){e.directive("drag",{mounted(t,n){if(!n.value)return!1;const o=document.querySelector(n.value[0]),r=document.querySelector(n.value[1]);if(!r||!o)return!1;r.onmouseover=()=>r.style.cursor="move";function a(s,u){const c=u==="pc"?s.clientX-r.offsetLeft:s.touches[0].clientX-r.offsetLeft,f=u==="pc"?s.clientY-r.offsetTop:s.touches[0].clientY-r.offsetTop,d=document.body.clientWidth,p=document.body.clientHeight||document.documentElement.clientHeight,m=o.offsetWidth,v=o.offsetHeight,h=o.offsetLeft,C=d-o.offsetLeft-m,g=o.offsetTop,y=p-o.offsetTop-v;let _=getComputedStyle(o).left,b=getComputedStyle(o).top;return _.includes("%")?(_=+document.body.clientWidth*(+_.replace(/\%/g,"")/100),b=+document.body.clientHeight*(+b.replace(/\%/g,"")/100)):(_=+_.replace(/\px/g,""),b=+b.replace(/\px/g,"")),{disX:c,disY:f,minDragDomLeft:h,maxDragDomLeft:C,minDragDomTop:g,maxDragDomTop:y,styL:_,styT:b}}function l(s,u,c){const{disX:f,disY:d,minDragDomLeft:p,maxDragDomLeft:m,minDragDomTop:v,maxDragDomTop:h,styL:C,styT:g}=c;let y=u==="pc"?s.clientX-f:s.touches[0].clientX-f,_=u==="pc"?s.clientY-d:s.touches[0].clientY-d;-y>p?y=-p:y>m&&(y=m),-_>v?_=-v:_>h&&(_=h),o.style.cssText+=`;left:${y+C}px;top:${_+g}px;`}r.onmousedown=s=>{const u=a(s,"pc");document.onmousemove=c=>{l(c,"pc",u)},document.onmouseup=()=>{document.onmousemove=null,document.onmouseup=null}},r.ontouchstart=s=>{const u=a(s,"app");document.ontouchmove=c=>{l(c,"app",u)},document.ontouchend=()=>{document.ontouchmove=null,document.ontouchend=null}}}})}function Gie(e){return typeof e=="object"&&e!==null}function hw(e,t){return e=Gie(e)?e:Object.create(null),new Proxy(e,{get(n,o,r){return o==="key"?Reflect.get(n,o,r):Reflect.get(n,o,r)||Reflect.get(t,o,r)}})}function Xie(e,t){return t.reduce((n,o)=>n==null?void 0:n[o],e)}function Jie(e,t,n){return t.slice(0,-1).reduce((o,r)=>/^(__proto__)$/.test(r)?{}:o[r]=o[r]||{},e)[t[t.length-1]]=n,e}function Zie(e,t){return t.reduce((n,o)=>{const r=o.split(".");return Jie(n,r,Xie(e,r))},{})}function mw(e,{storage:t,serializer:n,key:o,debug:r}){try{const a=t==null?void 0:t.getItem(o);a&&e.$patch(n==null?void 0:n.deserialize(a))}catch(a){r&&console.error(a)}}function vw(e,{storage:t,serializer:n,key:o,paths:r,debug:a}){try{const l=Array.isArray(r)?Zie(e,r):e;t.setItem(o,n.serialize(l))}catch(l){a&&console.error(l)}}function Qie(e={}){return t=>{const{auto:n=!1}=e,{options:{persist:o=n},store:r}=t;if(!o)return;const a=(Array.isArray(o)?o.map(l=>hw(l,e)):[hw(o,e)]).map(({storage:l=localStorage,beforeRestore:s=null,afterRestore:u=null,serializer:c={serialize:JSON.stringify,deserialize:JSON.parse},key:f=r.$id,paths:d=null,debug:p=!1})=>{var m;return{storage:l,beforeRestore:s,afterRestore:u,serializer:c,key:((m=e.key)!=null?m:v=>v)(f),paths:d,debug:p}});r.$persist=()=>{a.forEach(l=>{vw(r.$state,l)})},r.$hydrate=({runHooks:l=!0}={})=>{a.forEach(s=>{const{beforeRestore:u,afterRestore:c}=s;l&&(u==null||u(t)),mw(r,s),l&&(c==null||c(t))})},a.forEach(l=>{const{beforeRestore:s,afterRestore:u}=l;s==null||s(t),mw(r,l),u==null||u(t),r.$subscribe((c,f)=>{vw(f,l)},{detached:!0})})}}var eue=Qie();const w$=TI();w$.use(eue);async function tue(){const e=iv(Wie);e.use(w$),e.use($o),e.use(xne,{i18n:$o.global.t}),Kie(e),e.mount("#app")}tue()});export default nue();
diff --git a/public/install/assets/lang.svg b/public/install/assets/lang.svg
new file mode 100644
index 0000000..0d8224c
--- /dev/null
+++ b/public/install/assets/lang.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/install/assets/loading.gif b/public/install/assets/loading.gif
new file mode 100644
index 0000000..ffcb562
Binary files /dev/null and b/public/install/assets/loading.gif differ
diff --git a/public/install/assets/logo.svg b/public/install/assets/logo.svg
new file mode 100644
index 0000000..da77266
--- /dev/null
+++ b/public/install/assets/logo.svg
@@ -0,0 +1,102 @@
+
+
+
+
diff --git a/public/install/favicon.ico b/public/install/favicon.ico
new file mode 100644
index 0000000..df30868
Binary files /dev/null and b/public/install/favicon.ico differ
diff --git a/public/install/index.html b/public/install/index.html
new file mode 100644
index 0000000..373529a
--- /dev/null
+++ b/public/install/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+ BuildAdmin-安装
+
+
+
+
+
+
+
diff --git a/public/npm-install-test/package.json b/public/npm-install-test/package.json
new file mode 100644
index 0000000..72603fc
--- /dev/null
+++ b/public/npm-install-test/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "npm-install-test",
+ "private": true,
+ "version": "0.0.1",
+ "scripts": {
+ "dev": "vite"
+ },
+ "dependencies": {
+ "vue": "^3.2.25"
+ },
+ "devDependencies": {}
+}
diff --git a/public/robots.txt b/public/robots.txt
new file mode 100644
index 0000000..eb05362
--- /dev/null
+++ b/public/robots.txt
@@ -0,0 +1,2 @@
+User-agent: *
+Disallow:
diff --git a/public/router.php b/public/router.php
new file mode 100644
index 0000000..9c43666
--- /dev/null
+++ b/public/router.php
@@ -0,0 +1,21 @@
+
+// +----------------------------------------------------------------------
+// $Id$
+
+@ini_set('zlib.output_compression', 'Off');
+
+if (is_file($_SERVER["DOCUMENT_ROOT"] . $_SERVER["SCRIPT_NAME"])) {
+ return false;
+} else {
+ $_SERVER["SCRIPT_FILENAME"] = __DIR__ . '/index.php';
+
+ require __DIR__ . "/index.php";
+}
diff --git a/public/static/fonts/ttfs/1.ttf b/public/static/fonts/ttfs/1.ttf
new file mode 100644
index 0000000..d4ee155
Binary files /dev/null and b/public/static/fonts/ttfs/1.ttf differ
diff --git a/public/static/fonts/ttfs/2.ttf b/public/static/fonts/ttfs/2.ttf
new file mode 100644
index 0000000..3a452b6
Binary files /dev/null and b/public/static/fonts/ttfs/2.ttf differ
diff --git a/public/static/fonts/ttfs/3.ttf b/public/static/fonts/ttfs/3.ttf
new file mode 100644
index 0000000..d07a4d9
Binary files /dev/null and b/public/static/fonts/ttfs/3.ttf differ
diff --git a/public/static/fonts/ttfs/4.ttf b/public/static/fonts/ttfs/4.ttf
new file mode 100644
index 0000000..54a14ed
Binary files /dev/null and b/public/static/fonts/ttfs/4.ttf differ
diff --git a/public/static/fonts/ttfs/5.ttf b/public/static/fonts/ttfs/5.ttf
new file mode 100644
index 0000000..d672876
Binary files /dev/null and b/public/static/fonts/ttfs/5.ttf differ
diff --git a/public/static/fonts/ttfs/6.ttf b/public/static/fonts/ttfs/6.ttf
new file mode 100644
index 0000000..7f183e2
Binary files /dev/null and b/public/static/fonts/ttfs/6.ttf differ
diff --git a/public/static/fonts/zhttfs/1.ttf b/public/static/fonts/zhttfs/1.ttf
new file mode 100644
index 0000000..1c14f7f
Binary files /dev/null and b/public/static/fonts/zhttfs/1.ttf differ
diff --git a/public/static/fonts/zhttfs/SourceHanSansCN-Normal.ttf b/public/static/fonts/zhttfs/SourceHanSansCN-Normal.ttf
new file mode 100644
index 0000000..c6cc488
Binary files /dev/null and b/public/static/fonts/zhttfs/SourceHanSansCN-Normal.ttf differ
diff --git a/public/static/images/avatar.png b/public/static/images/avatar.png
new file mode 100644
index 0000000..e32edd3
Binary files /dev/null and b/public/static/images/avatar.png differ
diff --git a/public/static/images/captcha/click/bgs/1.png b/public/static/images/captcha/click/bgs/1.png
new file mode 100644
index 0000000..8ba3b34
Binary files /dev/null and b/public/static/images/captcha/click/bgs/1.png differ
diff --git a/public/static/images/captcha/click/bgs/2.png b/public/static/images/captcha/click/bgs/2.png
new file mode 100644
index 0000000..5bff3a9
Binary files /dev/null and b/public/static/images/captcha/click/bgs/2.png differ
diff --git a/public/static/images/captcha/click/bgs/3.png b/public/static/images/captcha/click/bgs/3.png
new file mode 100644
index 0000000..71517d6
Binary files /dev/null and b/public/static/images/captcha/click/bgs/3.png differ
diff --git a/public/static/images/captcha/click/icons/aeroplane.png b/public/static/images/captcha/click/icons/aeroplane.png
new file mode 100644
index 0000000..7b1c062
Binary files /dev/null and b/public/static/images/captcha/click/icons/aeroplane.png differ
diff --git a/public/static/images/captcha/click/icons/apple.png b/public/static/images/captcha/click/icons/apple.png
new file mode 100644
index 0000000..4584367
Binary files /dev/null and b/public/static/images/captcha/click/icons/apple.png differ
diff --git a/public/static/images/captcha/click/icons/banana.png b/public/static/images/captcha/click/icons/banana.png
new file mode 100644
index 0000000..d3f5fb8
Binary files /dev/null and b/public/static/images/captcha/click/icons/banana.png differ
diff --git a/public/static/images/captcha/click/icons/bell.png b/public/static/images/captcha/click/icons/bell.png
new file mode 100644
index 0000000..8ea07a1
Binary files /dev/null and b/public/static/images/captcha/click/icons/bell.png differ
diff --git a/public/static/images/captcha/click/icons/bicycle.png b/public/static/images/captcha/click/icons/bicycle.png
new file mode 100644
index 0000000..5ba9e44
Binary files /dev/null and b/public/static/images/captcha/click/icons/bicycle.png differ
diff --git a/public/static/images/captcha/click/icons/bird.png b/public/static/images/captcha/click/icons/bird.png
new file mode 100644
index 0000000..f8ff493
Binary files /dev/null and b/public/static/images/captcha/click/icons/bird.png differ
diff --git a/public/static/images/captcha/click/icons/bomb.png b/public/static/images/captcha/click/icons/bomb.png
new file mode 100644
index 0000000..f734e11
Binary files /dev/null and b/public/static/images/captcha/click/icons/bomb.png differ
diff --git a/public/static/images/captcha/click/icons/butterfly.png b/public/static/images/captcha/click/icons/butterfly.png
new file mode 100644
index 0000000..1e24b61
Binary files /dev/null and b/public/static/images/captcha/click/icons/butterfly.png differ
diff --git a/public/static/images/captcha/click/icons/candy.png b/public/static/images/captcha/click/icons/candy.png
new file mode 100644
index 0000000..24af81e
Binary files /dev/null and b/public/static/images/captcha/click/icons/candy.png differ
diff --git a/public/static/images/captcha/click/icons/crab.png b/public/static/images/captcha/click/icons/crab.png
new file mode 100644
index 0000000..cef4646
Binary files /dev/null and b/public/static/images/captcha/click/icons/crab.png differ
diff --git a/public/static/images/captcha/click/icons/cup.png b/public/static/images/captcha/click/icons/cup.png
new file mode 100644
index 0000000..89cedf9
Binary files /dev/null and b/public/static/images/captcha/click/icons/cup.png differ
diff --git a/public/static/images/captcha/click/icons/dolphin.png b/public/static/images/captcha/click/icons/dolphin.png
new file mode 100644
index 0000000..bd33f18
Binary files /dev/null and b/public/static/images/captcha/click/icons/dolphin.png differ
diff --git a/public/static/images/captcha/click/icons/fire.png b/public/static/images/captcha/click/icons/fire.png
new file mode 100644
index 0000000..35f9428
Binary files /dev/null and b/public/static/images/captcha/click/icons/fire.png differ
diff --git a/public/static/images/captcha/click/icons/guitar.png b/public/static/images/captcha/click/icons/guitar.png
new file mode 100644
index 0000000..430ae49
Binary files /dev/null and b/public/static/images/captcha/click/icons/guitar.png differ
diff --git a/public/static/images/captcha/click/icons/hexagon.png b/public/static/images/captcha/click/icons/hexagon.png
new file mode 100644
index 0000000..cf4d14b
Binary files /dev/null and b/public/static/images/captcha/click/icons/hexagon.png differ
diff --git a/public/static/images/captcha/click/icons/pear.png b/public/static/images/captcha/click/icons/pear.png
new file mode 100644
index 0000000..45d1688
Binary files /dev/null and b/public/static/images/captcha/click/icons/pear.png differ
diff --git a/public/static/images/captcha/click/icons/rocket.png b/public/static/images/captcha/click/icons/rocket.png
new file mode 100644
index 0000000..60538f5
Binary files /dev/null and b/public/static/images/captcha/click/icons/rocket.png differ
diff --git a/public/static/images/captcha/click/icons/sailboat.png b/public/static/images/captcha/click/icons/sailboat.png
new file mode 100644
index 0000000..e30a3ce
Binary files /dev/null and b/public/static/images/captcha/click/icons/sailboat.png differ
diff --git a/public/static/images/captcha/click/icons/snowflake.png b/public/static/images/captcha/click/icons/snowflake.png
new file mode 100644
index 0000000..32809ae
Binary files /dev/null and b/public/static/images/captcha/click/icons/snowflake.png differ
diff --git a/public/static/images/captcha/click/icons/wolf head.png b/public/static/images/captcha/click/icons/wolf head.png
new file mode 100644
index 0000000..d72dbf2
Binary files /dev/null and b/public/static/images/captcha/click/icons/wolf head.png differ
diff --git a/public/static/images/captcha/image/1.jpg b/public/static/images/captcha/image/1.jpg
new file mode 100644
index 0000000..d417136
Binary files /dev/null and b/public/static/images/captcha/image/1.jpg differ
diff --git a/public/static/images/captcha/image/2.jpg b/public/static/images/captcha/image/2.jpg
new file mode 100644
index 0000000..56640bd
Binary files /dev/null and b/public/static/images/captcha/image/2.jpg differ
diff --git a/public/static/images/captcha/image/3.jpg b/public/static/images/captcha/image/3.jpg
new file mode 100644
index 0000000..83e5bd9
Binary files /dev/null and b/public/static/images/captcha/image/3.jpg differ
diff --git a/public/static/images/captcha/image/4.jpg b/public/static/images/captcha/image/4.jpg
new file mode 100644
index 0000000..97a3721
Binary files /dev/null and b/public/static/images/captcha/image/4.jpg differ
diff --git a/public/static/images/captcha/image/5.jpg b/public/static/images/captcha/image/5.jpg
new file mode 100644
index 0000000..220a17a
Binary files /dev/null and b/public/static/images/captcha/image/5.jpg differ
diff --git a/public/static/images/captcha/image/6.jpg b/public/static/images/captcha/image/6.jpg
new file mode 100644
index 0000000..be53ea0
Binary files /dev/null and b/public/static/images/captcha/image/6.jpg differ
diff --git a/public/static/images/captcha/image/7.jpg b/public/static/images/captcha/image/7.jpg
new file mode 100644
index 0000000..fbf537f
Binary files /dev/null and b/public/static/images/captcha/image/7.jpg differ
diff --git a/public/static/images/captcha/image/8.jpg b/public/static/images/captcha/image/8.jpg
new file mode 100644
index 0000000..e10cf28
Binary files /dev/null and b/public/static/images/captcha/image/8.jpg differ
diff --git a/public/static/images/local-module-logo.png b/public/static/images/local-module-logo.png
new file mode 100644
index 0000000..99cd9fa
Binary files /dev/null and b/public/static/images/local-module-logo.png differ
diff --git a/runtime/.gitignore b/runtime/.gitignore
new file mode 100644
index 0000000..c96a04f
--- /dev/null
+++ b/runtime/.gitignore
@@ -0,0 +1,2 @@
+*
+!.gitignore
\ No newline at end of file
diff --git a/think b/think
new file mode 100644
index 0000000..2429d22
--- /dev/null
+++ b/think
@@ -0,0 +1,10 @@
+#!/usr/bin/env php
+console->run();
\ No newline at end of file
diff --git a/web/.editorconfig b/web/.editorconfig
new file mode 100644
index 0000000..5c83fe8
--- /dev/null
+++ b/web/.editorconfig
@@ -0,0 +1,15 @@
+# https://editorconfig.org
+root = true
+
+[*]
+charset = utf-8
+indent_style = space
+indent_size = 4
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*.md]
+indent_style = tab
+insert_final_newline = false
+trim_trailing_whitespace = false
diff --git a/web/.env b/web/.env
new file mode 100644
index 0000000..e4dd6b0
--- /dev/null
+++ b/web/.env
@@ -0,0 +1,5 @@
+# port 端口号
+VITE_PORT = 1818
+
+# open 运行 npm run dev 时自动打开浏览器
+VITE_OPEN = false
diff --git a/web/.env.development b/web/.env.development
new file mode 100644
index 0000000..3a9fa29
--- /dev/null
+++ b/web/.env.development
@@ -0,0 +1,8 @@
+# 本地环境
+ENV = 'development'
+
+# base路径
+VITE_BASE_PATH = './'
+
+# 本地环境接口地址 - 尾部无需带'/'
+VITE_AXIOS_BASE_URL = 'http://localhost:8000'
diff --git a/web/.env.production b/web/.env.production
new file mode 100644
index 0000000..7ef3784
--- /dev/null
+++ b/web/.env.production
@@ -0,0 +1,11 @@
+# 线上环境
+ENV = 'production'
+
+# base路径
+VITE_BASE_PATH = '/'
+
+# 导出路径
+VITE_OUT_DIR = 'dist'
+
+# 线上环境接口地址 - 'getCurrentDomain:表示获取当前域名'
+VITE_AXIOS_BASE_URL = 'getCurrentDomain'
diff --git a/web/.npmrc b/web/.npmrc
new file mode 100644
index 0000000..49813ee
--- /dev/null
+++ b/web/.npmrc
@@ -0,0 +1,2 @@
+# 若不开启且使用 pnpm 安装依赖后,element-plus 和 vue-i18n(useI18n) 共存时组件的类型定义将丢失
+shamefully-hoist=true
diff --git a/web/.prettierrc.js b/web/.prettierrc.js
new file mode 100644
index 0000000..0075f36
--- /dev/null
+++ b/web/.prettierrc.js
@@ -0,0 +1,36 @@
+export default {
+ printWidth: 150,
+ // 指定每个缩进级别的空格数
+ tabWidth: 4,
+ // 使用制表符而不是空格缩进行
+ useTabs: false,
+ // 在语句末尾打印分号
+ semi: false,
+ // 使用单引号而不是双引号
+ singleQuote: true,
+ // 更改引用对象属性的时间 可选值""
+ quoteProps: 'as-needed',
+ // 在JSX中使用单引号而不是双引号
+ jsxSingleQuote: false,
+ // 多行时尽可能打印尾随逗号。(例如,单行数组永远不会出现逗号结尾。) 可选值"",默认none
+ trailingComma: 'es5',
+ // 在对象文字中的括号之间打印空格
+ bracketSpacing: true,
+ // 在单独的箭头函数参数周围包括括号 always:(x) => x \ avoid:x => x
+ arrowParens: 'always',
+ // 这两个选项可用于格式化以给定字符偏移量(分别包括和不包括)开始和结束的代码
+ rangeStart: 0,
+ rangeEnd: Infinity,
+ // 指定要使用的解析器,不需要写文件开头的 @prettier
+ requirePragma: false,
+ // 不需要自动在文件开头插入 @prettier
+ insertPragma: false,
+ // 使用默认的折行标准 always\never\preserve
+ proseWrap: 'preserve',
+ // 指定HTML文件的全局空格敏感度 css\strict\ignore
+ htmlWhitespaceSensitivity: 'css',
+ // Vue文件脚本和样式标签缩进
+ vueIndentScriptAndStyle: false,
+ // 换行符使用 lf 结尾是 可选值""
+ endOfLine: 'lf',
+}
diff --git a/web/.vscode/extensions.json b/web/.vscode/extensions.json
new file mode 100644
index 0000000..d77da08
--- /dev/null
+++ b/web/.vscode/extensions.json
@@ -0,0 +1,3 @@
+{
+ "recommendations": ["vue.volar", "esbenp.prettier-vscode", "dbaeumer.vscode-eslint"]
+}
diff --git a/web/.vscode/settings.json b/web/.vscode/settings.json
new file mode 100644
index 0000000..dc7a3b8
--- /dev/null
+++ b/web/.vscode/settings.json
@@ -0,0 +1,15 @@
+{
+ "[json]": {
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
+ },
+ "[jsonc]": {
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
+ },
+ "[typescript]": {
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
+ },
+ "[vue]": {
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
+ },
+ "eslint.validate": ["javascript", "vue", "typescript"]
+}
diff --git a/web/eslint.config.js b/web/eslint.config.js
new file mode 100644
index 0000000..76afbbf
--- /dev/null
+++ b/web/eslint.config.js
@@ -0,0 +1,108 @@
+import js from '@eslint/js'
+import eslintConfigPrettier from 'eslint-config-prettier'
+import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'
+import eslintPluginVue from 'eslint-plugin-vue'
+import globals from 'globals'
+import ts from 'typescript-eslint'
+
+export default [
+ // 三大基本推荐规则
+ js.configs.recommended,
+ ...ts.configs.recommended,
+ ...eslintPluginVue.configs['flat/recommended'],
+
+ // 忽略规则
+ {
+ ignores: ['node_modules', 'dist', 'public'],
+ },
+
+ // 全局变量
+ {
+ languageOptions: {
+ globals: {
+ ...globals.browser,
+ },
+ },
+ },
+
+ // vue
+ {
+ files: ['**/*.vue'],
+ languageOptions: {
+ parserOptions: {
+ // ts 解析器
+ parser: ts.parser,
+ // 允许 jsx
+ ecmaFeatures: {
+ jsx: true,
+ },
+ },
+ },
+ },
+
+ // eslint + prettier 的兼容性问题解决规则
+ eslintConfigPrettier,
+ eslintPluginPrettierRecommended,
+
+ // ts
+ {
+ files: ['**/*.{ts,tsx,vue}'],
+ rules: {
+ 'no-empty': 'off',
+ 'no-undef': 'off',
+ 'no-unused-vars': 'off',
+ 'no-useless-escape': 'off',
+ 'no-sparse-arrays': 'off',
+ 'no-prototype-builtins': 'off',
+ 'no-use-before-define': 'off',
+ 'no-case-declarations': 'off',
+ 'no-console': 'off',
+ 'no-control-regex': 'off',
+
+ 'vue/v-on-event-hyphenation': 'off',
+ 'vue/custom-event-name-casing': 'off',
+ 'vue/component-definition-name-casing': 'off',
+ 'vue/attributes-order': 'off',
+ 'vue/one-component-per-file': 'off',
+ 'vue/html-closing-bracket-newline': 'off',
+ 'vue/max-attributes-per-line': 'off',
+ 'vue/multiline-html-element-content-newline': 'off',
+ 'vue/singleline-html-element-content-newline': 'off',
+ 'vue/attribute-hyphenation': 'off',
+ 'vue/html-self-closing': 'off',
+ 'vue/require-default-prop': 'off',
+ 'vue/no-arrow-functions-in-watch': 'off',
+ 'vue/no-v-html': 'off',
+ 'vue/comment-directive': 'off',
+ 'vue/multi-word-component-names': 'off',
+ 'vue/require-prop-types': 'off',
+ 'vue/html-indent': 'off',
+
+ '@typescript-eslint/no-unsafe-function-type': 'off',
+ '@typescript-eslint/no-empty-function': 'off',
+ '@typescript-eslint/no-explicit-any': 'off',
+ '@typescript-eslint/no-non-null-assertion': 'off',
+ '@typescript-eslint/no-unused-expressions': 'off',
+ '@typescript-eslint/no-unused-vars': [
+ 'warn',
+ {
+ argsIgnorePattern: '^_',
+ varsIgnorePattern: '^_',
+ },
+ ],
+ },
+ },
+
+ // prettier 规则
+ {
+ files: ['**/*.{ts,tsx,vue,js}'],
+ rules: {
+ 'prettier/prettier': [
+ 'warn', // 使用警告而不是错误
+ {
+ endOfLine: 'auto', // eslint 无需检查文件换行符
+ },
+ ],
+ },
+ },
+]
diff --git a/web/index.html b/web/index.html
new file mode 100644
index 0000000..badfa36
--- /dev/null
+++ b/web/index.html
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+ Loading...
+
+
+
+
+
+
diff --git a/web/package.json b/web/package.json
new file mode 100644
index 0000000..1885725
--- /dev/null
+++ b/web/package.json
@@ -0,0 +1,63 @@
+{
+ "name": "build-admin",
+ "version": "2.3.6",
+ "license": "Apache-2.0",
+ "type": "module",
+ "scripts": {
+ "dev": "esno ./src/utils/build.ts && vite --force",
+ "build": "vite build && esno ./src/utils/build.ts",
+ "lint": "eslint .",
+ "lint-fix": "eslint --fix .",
+ "format": "npx prettier --write .",
+ "typecheck": "vue-tsc --noEmit"
+ },
+ "dependencies": {
+ "@element-plus/icons-vue": "2.3.1",
+ "@vueuse/core": "12.0.0",
+ "axios": "1.9.0",
+ "echarts": "5.5.1",
+ "element-plus": "2.9.1",
+ "font-awesome": "4.7.0",
+ "lodash-es": "4.17.21",
+ "mitt": "3.0.1",
+ "nprogress": "0.2.0",
+ "pinia": "2.3.0",
+ "pinia-plugin-persistedstate": "4.2.0",
+ "qrcode.vue": "3.6.0",
+ "screenfull": "6.0.2",
+ "sortablejs": "1.15.6",
+ "v-code-diff": "1.13.1",
+ "vue": "3.5.13",
+ "vue-i18n": "11.1.3",
+ "vue-router": "4.5.0"
+ },
+ "devDependencies": {
+ "@eslint/js": "9.17.0",
+ "@types/lodash-es": "4.17.12",
+ "@types/node": "22.10.2",
+ "@types/nprogress": "0.2.3",
+ "@types/sortablejs": "1.15.8",
+ "@vitejs/plugin-vue": "5.2.3",
+ "async-validator": "4.2.5",
+ "eslint": "9.17.0",
+ "eslint-config-prettier": "9.1.0",
+ "eslint-plugin-prettier": "5.2.1",
+ "eslint-plugin-vue": "9.32.0",
+ "esno": "4.8.0",
+ "globals": "15.14.0",
+ "prettier": "3.4.2",
+ "sass": "1.83.0",
+ "typescript": "5.7.2",
+ "typescript-eslint": "8.18.1",
+ "vite": "6.3.5",
+ "vue-tsc": "2.1.10"
+ },
+ "pnpm": {
+ "onlyBuiltDependencies": [
+ "@parcel/watcher",
+ "esbuild",
+ "v-code-diff",
+ "vue-demi"
+ ]
+ }
+}
diff --git a/web/public/favicon.ico b/web/public/favicon.ico
new file mode 100644
index 0000000..df30868
Binary files /dev/null and b/web/public/favicon.ico differ
diff --git a/web/src/App.vue b/web/src/App.vue
new file mode 100644
index 0000000..af0ca20
--- /dev/null
+++ b/web/src/App.vue
@@ -0,0 +1,36 @@
+
+
+
+
+
+
diff --git a/web/src/api/backend/auth/group.ts b/web/src/api/backend/auth/group.ts
new file mode 100644
index 0000000..5612631
--- /dev/null
+++ b/web/src/api/backend/auth/group.ts
@@ -0,0 +1,8 @@
+import createAxios from '/@/utils/axios'
+
+export function getAdminRules() {
+ return createAxios({
+ url: '/admin/auth.Rule/index',
+ method: 'get',
+ })
+}
diff --git a/web/src/api/backend/crud.ts b/web/src/api/backend/crud.ts
new file mode 100644
index 0000000..f2ffeb1
--- /dev/null
+++ b/web/src/api/backend/crud.ts
@@ -0,0 +1,142 @@
+import { useBaAccount } from '/@/stores/baAccount'
+import { useSiteConfig } from '/@/stores/siteConfig'
+import createAxios from '/@/utils/axios'
+
+export const url = '/admin/crud.Crud/'
+
+export function generate(data: anyObj) {
+ return createAxios(
+ {
+ url: url + 'generate',
+ method: 'post',
+ data: data,
+ },
+ {
+ showSuccessMessage: true,
+ }
+ )
+}
+
+export function getFileData(table: string, commonModel = 0) {
+ return createAxios({
+ url: url + 'getFileData',
+ method: 'get',
+ params: {
+ table: table,
+ commonModel: commonModel,
+ },
+ })
+}
+
+export function generateCheck(data: anyObj) {
+ return createAxios(
+ {
+ url: url + 'generateCheck',
+ method: 'post',
+ data: data,
+ },
+ {
+ showCodeMessage: false,
+ }
+ )
+}
+
+export function parseFieldData(data: anyObj) {
+ return createAxios({
+ url: url + 'parseFieldData',
+ method: 'post',
+ data: data,
+ })
+}
+
+export function postLogStart(id: string, type: string) {
+ const data: anyObj = {
+ id,
+ type,
+ }
+
+ if (type == 'Cloud history') {
+ const baAccount = useBaAccount()
+ data['token'] = baAccount.getToken('auth')
+ }
+
+ return createAxios({
+ url: url + 'logStart',
+ method: 'post',
+ data: data,
+ })
+}
+
+export function postDel(id: number) {
+ return createAxios({
+ url: url + 'delete',
+ method: 'post',
+ data: {
+ id: id,
+ },
+ })
+}
+
+export function checkCrudLog(table: string, connection: string) {
+ return createAxios({
+ url: url + 'checkCrudLog',
+ method: 'get',
+ params: {
+ table: table,
+ connection: connection,
+ },
+ })
+}
+
+export function uploadLog(data: anyObj) {
+ const baAccount = useBaAccount()
+ const siteConfig = useSiteConfig()
+ return createAxios(
+ {
+ url: siteConfig.apiUrl + '/api/v6.Crud/uploadLog',
+ data: data,
+ method: 'post',
+ },
+ {
+ anotherToken: baAccount.getToken('auth'),
+ }
+ )
+}
+
+export function uploadCompleted(data: anyObj) {
+ return createAxios({
+ url: url + 'uploadCompleted',
+ data: data,
+ method: 'post',
+ })
+}
+
+export function logs(data: anyObj = {}) {
+ const baAccount = useBaAccount()
+ const siteConfig = useSiteConfig()
+ return createAxios(
+ {
+ url: siteConfig.apiUrl + '/api/v6.Crud/logs',
+ data: data,
+ method: 'post',
+ },
+ {
+ anotherToken: baAccount.getToken('auth'),
+ }
+ )
+}
+
+export function delLog(data: anyObj = {}) {
+ const baAccount = useBaAccount()
+ const siteConfig = useSiteConfig()
+ return createAxios(
+ {
+ url: siteConfig.apiUrl + '/api/v6.Crud/del',
+ data: data,
+ method: 'post',
+ },
+ {
+ anotherToken: baAccount.getToken('auth'),
+ }
+ )
+}
diff --git a/web/src/api/backend/dashboard.ts b/web/src/api/backend/dashboard.ts
new file mode 100644
index 0000000..5caef9d
--- /dev/null
+++ b/web/src/api/backend/dashboard.ts
@@ -0,0 +1,10 @@
+import createAxios from '/@/utils/axios'
+
+export const url = '/admin/Dashboard/'
+
+export function index() {
+ return createAxios({
+ url: url + 'index',
+ method: 'get',
+ })
+}
diff --git a/web/src/api/backend/index.ts b/web/src/api/backend/index.ts
new file mode 100644
index 0000000..9573c2c
--- /dev/null
+++ b/web/src/api/backend/index.ts
@@ -0,0 +1,72 @@
+import { useAdminInfo } from '/@/stores/adminInfo'
+import { useBaAccount } from '/@/stores/baAccount'
+import { useSiteConfig } from '/@/stores/siteConfig'
+import createAxios from '/@/utils/axios'
+
+export const url = '/admin/Index/'
+
+export function index() {
+ return createAxios({
+ url: url + 'index',
+ method: 'get',
+ })
+}
+
+export function login(method: 'get' | 'post', params: object = {}) {
+ return createAxios({
+ url: url + 'login',
+ data: params,
+ method: method,
+ })
+}
+
+export function logout() {
+ const adminInfo = useAdminInfo()
+ return createAxios({
+ url: url + 'logout',
+ method: 'POST',
+ data: {
+ refreshToken: adminInfo.getToken('refresh'),
+ },
+ })
+}
+
+export function baAccountCheckIn(params: object = {}) {
+ const siteConfig = useSiteConfig()
+ return createAxios(
+ {
+ url: siteConfig.apiUrl + '/api/user/checkIn',
+ data: params,
+ method: 'post',
+ },
+ {
+ showSuccessMessage: true,
+ }
+ )
+}
+
+export function baAccountGetUserInfo() {
+ const baAccount = useBaAccount()
+ const siteConfig = useSiteConfig()
+ return createAxios(
+ {
+ url: siteConfig.apiUrl + '/api/user/info',
+ method: 'get',
+ },
+ {
+ anotherToken: baAccount.getToken('auth'),
+ }
+ )
+}
+
+export function baAccountLogout() {
+ const siteConfig = useSiteConfig()
+ const baAccount = useBaAccount()
+ return createAxios({
+ url: siteConfig.apiUrl + '/api/user/logout',
+ method: 'POST',
+ data: {
+ refreshToken: baAccount.getToken('refresh'),
+ },
+ })
+}
diff --git a/web/src/api/backend/module.ts b/web/src/api/backend/module.ts
new file mode 100644
index 0000000..5fc9930
--- /dev/null
+++ b/web/src/api/backend/module.ts
@@ -0,0 +1,190 @@
+import { useBaAccount } from '/@/stores/baAccount'
+import { useSiteConfig } from '/@/stores/siteConfig'
+import createAxios from '/@/utils/axios'
+
+const storeUrl = '/api/v7.store/'
+const moduleControllerUrl = '/admin/module/'
+
+export function index(params: anyObj = {}) {
+ return createAxios({
+ url: moduleControllerUrl + 'index',
+ method: 'get',
+ params: params,
+ })
+}
+
+export function modules(params: anyObj = {}) {
+ const siteConfig = useSiteConfig()
+ return createAxios({
+ url: siteConfig.apiUrl + storeUrl + 'modules',
+ method: 'get',
+ params: params,
+ })
+}
+
+export function info(params: anyObj) {
+ const baAccount = useBaAccount()
+ const siteConfig = useSiteConfig()
+ return createAxios(
+ {
+ url: siteConfig.apiUrl + storeUrl + 'info',
+ method: 'get',
+ params: params,
+ },
+ {
+ anotherToken: baAccount.getToken('auth'),
+ }
+ )
+}
+
+export function createOrder(params: object = {}) {
+ const baAccount = useBaAccount()
+ const siteConfig = useSiteConfig()
+ return createAxios(
+ {
+ url: siteConfig.apiUrl + storeUrl + 'order',
+ method: 'post',
+ params: params,
+ },
+ {
+ anotherToken: baAccount.getToken('auth'),
+ }
+ )
+}
+
+export function payOrder(orderId: number, payType: string) {
+ const baAccount = useBaAccount()
+ const siteConfig = useSiteConfig()
+ return createAxios(
+ {
+ url: siteConfig.apiUrl + storeUrl + 'pay',
+ method: 'post',
+ params: {
+ order_id: orderId,
+ pay_type: payType,
+ },
+ },
+ {
+ anotherToken: baAccount.getToken('auth'),
+ showSuccessMessage: true,
+ }
+ )
+}
+
+export function payCheck(sn: string) {
+ const baAccount = useBaAccount()
+ const siteConfig = useSiteConfig()
+ return createAxios(
+ {
+ url: siteConfig.apiUrl + '/api/pay/check',
+ method: 'get',
+ params: {
+ sn: sn,
+ },
+ },
+ {
+ anotherToken: baAccount.getToken('auth'),
+ showCodeMessage: false,
+ }
+ )
+}
+
+/**
+ * 获取模块的可安装版本列表
+ */
+export function preDownload(data: anyObj) {
+ const baAccount = useBaAccount()
+ const siteConfig = useSiteConfig()
+ return createAxios(
+ {
+ url: siteConfig.apiUrl + storeUrl + 'preDownload',
+ method: 'POST',
+ data,
+ },
+ {
+ anotherToken: baAccount.getToken('auth'),
+ }
+ )
+}
+
+export function getInstallState(uid: string) {
+ return createAxios({
+ url: moduleControllerUrl + 'state',
+ method: 'get',
+ params: {
+ uid: uid,
+ },
+ })
+}
+
+export function postInstallModule(uid: string, orderId: number, version: string, update: boolean, extend: anyObj = {}) {
+ const baAccount = useBaAccount()
+ return createAxios(
+ {
+ url: moduleControllerUrl + 'install',
+ method: 'POST',
+ data: {
+ uid,
+ update,
+ version,
+ orderId,
+ token: baAccount.getToken('auth'),
+ extend,
+ },
+ timeout: 3000 * 10,
+ },
+ {
+ showCodeMessage: false,
+ }
+ )
+}
+
+export function postUninstall(uid: string) {
+ return createAxios(
+ {
+ url: moduleControllerUrl + 'uninstall',
+ method: 'post',
+ params: {
+ uid: uid,
+ },
+ },
+ {
+ showSuccessMessage: true,
+ }
+ )
+}
+
+export function changeState(params: anyObj) {
+ return createAxios(
+ {
+ url: moduleControllerUrl + 'changeState',
+ method: 'post',
+ data: params,
+ },
+ {
+ showCodeMessage: false,
+ }
+ )
+}
+
+export function dependentInstallComplete(uid: string) {
+ return createAxios({
+ url: moduleControllerUrl + 'dependentInstallComplete',
+ method: 'post',
+ params: {
+ uid: uid,
+ },
+ })
+}
+
+export function upload(file: string) {
+ const baAccount = useBaAccount()
+ return createAxios({
+ url: moduleControllerUrl + 'upload',
+ method: 'post',
+ params: {
+ file: file,
+ token: baAccount.getToken('auth'),
+ },
+ })
+}
diff --git a/web/src/api/backend/routine/AdminInfo.ts b/web/src/api/backend/routine/AdminInfo.ts
new file mode 100644
index 0000000..7f08b07
--- /dev/null
+++ b/web/src/api/backend/routine/AdminInfo.ts
@@ -0,0 +1,37 @@
+import createAxios from '/@/utils/axios'
+
+export const url = '/admin/routine.AdminInfo/'
+
+export const actionUrl = new Map([
+ ['index', url + 'index'],
+ ['edit', url + 'edit'],
+ ['log', '/admin/auth.AdminLog/index'],
+])
+
+export function index() {
+ return createAxios({
+ url: actionUrl.get('index'),
+ method: 'get',
+ })
+}
+
+export function log(filter: anyObj = {}) {
+ return createAxios({
+ url: actionUrl.get('log'),
+ method: 'get',
+ params: filter,
+ })
+}
+
+export function postData(data: anyObj) {
+ return createAxios(
+ {
+ url: actionUrl.get('edit'),
+ method: 'post',
+ data: data,
+ },
+ {
+ showSuccessMessage: true,
+ }
+ )
+}
diff --git a/web/src/api/backend/routine/config.ts b/web/src/api/backend/routine/config.ts
new file mode 100644
index 0000000..0adf4b7
--- /dev/null
+++ b/web/src/api/backend/routine/config.ts
@@ -0,0 +1,59 @@
+import createAxios from '/@/utils/axios'
+
+export const url = '/admin/routine.Config/'
+
+export const actionUrl = new Map([
+ ['index', url + 'index'],
+ ['add', url + 'add'],
+ ['edit', url + 'edit'],
+ ['del', url + 'del'],
+ ['sendTestMail', url + 'sendTestMail'],
+])
+
+export function index() {
+ return createAxios({
+ url: actionUrl.get('index'),
+ method: 'get',
+ })
+}
+
+export function postData(action: string, data: anyObj) {
+ return createAxios(
+ {
+ url: actionUrl.get(action),
+ method: 'post',
+ data: data,
+ },
+ {
+ showSuccessMessage: true,
+ }
+ )
+}
+
+export function del(ids: string[]) {
+ return createAxios(
+ {
+ url: actionUrl.get('del'),
+ method: 'DELETE',
+ params: {
+ ids: ids,
+ },
+ },
+ {
+ showSuccessMessage: true,
+ }
+ )
+}
+
+export function postSendTestMail(data: anyObj, mail: string) {
+ return createAxios(
+ {
+ url: actionUrl.get('sendTestMail'),
+ method: 'POST',
+ data: Object.assign({}, data, { testMail: mail }),
+ },
+ {
+ showSuccessMessage: true,
+ }
+ )
+}
diff --git a/web/src/api/backend/security/dataRecycle.ts b/web/src/api/backend/security/dataRecycle.ts
new file mode 100644
index 0000000..8490e33
--- /dev/null
+++ b/web/src/api/backend/security/dataRecycle.ts
@@ -0,0 +1,10 @@
+import createAxios from '/@/utils/axios'
+
+export const url = '/admin/security.DataRecycle/'
+
+export function add() {
+ return createAxios({
+ url: url + 'add',
+ method: 'get',
+ })
+}
diff --git a/web/src/api/backend/security/dataRecycleLog.ts b/web/src/api/backend/security/dataRecycleLog.ts
new file mode 100644
index 0000000..3086a52
--- /dev/null
+++ b/web/src/api/backend/security/dataRecycleLog.ts
@@ -0,0 +1,28 @@
+import createAxios from '/@/utils/axios'
+
+export const url = '/admin/security.DataRecycleLog/'
+
+export function restore(ids: string[]) {
+ return createAxios(
+ {
+ url: url + 'restore',
+ method: 'POST',
+ data: {
+ ids: ids,
+ },
+ },
+ {
+ showSuccessMessage: true,
+ }
+ )
+}
+
+export function info(id: string) {
+ return createAxios({
+ url: url + 'info',
+ method: 'get',
+ params: {
+ id: id,
+ },
+ })
+}
diff --git a/web/src/api/backend/security/sensitiveData.ts b/web/src/api/backend/security/sensitiveData.ts
new file mode 100644
index 0000000..1aeab91
--- /dev/null
+++ b/web/src/api/backend/security/sensitiveData.ts
@@ -0,0 +1,10 @@
+import createAxios from '/@/utils/axios'
+
+export const url = '/admin/security.SensitiveData/'
+
+export function add() {
+ return createAxios({
+ url: url + 'add',
+ method: 'get',
+ })
+}
diff --git a/web/src/api/backend/security/sensitiveDataLog.ts b/web/src/api/backend/security/sensitiveDataLog.ts
new file mode 100644
index 0000000..3dfba25
--- /dev/null
+++ b/web/src/api/backend/security/sensitiveDataLog.ts
@@ -0,0 +1,28 @@
+import createAxios from '/@/utils/axios'
+
+export const url = '/admin/security.SensitiveDataLog/'
+
+export function rollback(ids: string[]) {
+ return createAxios(
+ {
+ url: url + 'rollback',
+ method: 'POST',
+ data: {
+ ids: ids,
+ },
+ },
+ {
+ showSuccessMessage: true,
+ }
+ )
+}
+
+export function info(id: string) {
+ return createAxios({
+ url: url + 'info',
+ method: 'get',
+ params: {
+ id: id,
+ },
+ })
+}
diff --git a/web/src/api/backend/user/group.ts b/web/src/api/backend/user/group.ts
new file mode 100644
index 0000000..010f231
--- /dev/null
+++ b/web/src/api/backend/user/group.ts
@@ -0,0 +1,8 @@
+import createAxios from '/@/utils/axios'
+
+export function getUserRules() {
+ return createAxios({
+ url: '/admin/user.Rule/index',
+ method: 'get',
+ })
+}
diff --git a/web/src/api/backend/user/moneyLog.ts b/web/src/api/backend/user/moneyLog.ts
new file mode 100644
index 0000000..e2078fe
--- /dev/null
+++ b/web/src/api/backend/user/moneyLog.ts
@@ -0,0 +1,13 @@
+import createAxios from '/@/utils/axios'
+
+export const url = '/admin/user.MoneyLog/'
+
+export function add(userId: string) {
+ return createAxios({
+ url: url + 'add',
+ method: 'get',
+ params: {
+ userId: userId,
+ },
+ })
+}
diff --git a/web/src/api/backend/user/scoreLog.ts b/web/src/api/backend/user/scoreLog.ts
new file mode 100644
index 0000000..9262d2a
--- /dev/null
+++ b/web/src/api/backend/user/scoreLog.ts
@@ -0,0 +1,13 @@
+import createAxios from '/@/utils/axios'
+
+export const url = '/admin/user.ScoreLog/'
+
+export function add(userId: string) {
+ return createAxios({
+ url: url + 'add',
+ method: 'get',
+ params: {
+ userId: userId,
+ },
+ })
+}
diff --git a/web/src/api/common.ts b/web/src/api/common.ts
new file mode 100644
index 0000000..ab650ba
--- /dev/null
+++ b/web/src/api/common.ts
@@ -0,0 +1,378 @@
+import createAxios from '/@/utils/axios'
+import { isAdminApp, checkFileMimetype } from '/@/utils/common'
+import { getUrl } from '/@/utils/axios'
+import { useAdminInfo } from '/@/stores/adminInfo'
+import { useUserInfo } from '/@/stores/userInfo'
+import { ElNotification, type UploadRawFile } from 'element-plus'
+import { useSiteConfig } from '/@/stores/siteConfig'
+import { state as uploadExpandState, fileUpload as uploadExpand } from '/@/components/mixins/baUpload'
+import type { AxiosRequestConfig } from 'axios'
+import { uuid } from '/@/utils/random'
+import { i18n } from '../lang'
+import { adminBaseRoutePath } from '/@/router/static/adminBase'
+import { SYSTEM_ZINDEX } from '/@/stores/constant/common'
+
+/*
+ * 公共请求函数和Url定义
+ */
+
+// Admin模块
+export const adminUploadUrl = '/admin/ajax/upload'
+export const adminBuildSuffixSvgUrl = adminBaseRoutePath + '/ajax/buildSuffixSvg'
+export const adminAreaUrl = '/admin/ajax/area'
+export const getTablePkUrl = '/admin/ajax/getTablePk'
+export const getTableListUrl = '/admin/ajax/getTableList'
+export const getTableFieldListUrl = '/admin/ajax/getTableFieldList'
+export const getDatabaseConnectionListUrl = '/admin/ajax/getDatabaseConnectionList'
+export const terminalUrl = adminBaseRoutePath + '/ajax/terminal'
+export const changeTerminalConfigUrl = '/admin/ajax/changeTerminalConfig'
+export const clearCacheUrl = '/admin/ajax/clearCache'
+
+// 公共
+export const captchaUrl = '/api/common/captcha'
+export const clickCaptchaUrl = '/api/common/clickCaptcha'
+export const checkClickCaptchaUrl = '/api/common/checkClickCaptcha'
+export const refreshTokenUrl = '/api/common/refreshToken'
+
+// api模块(前台)
+export const apiUploadUrl = '/api/ajax/upload'
+export const apiBuildSuffixSvgUrl = '/api/ajax/buildSuffixSvg'
+export const apiAreaUrl = '/api/ajax/area'
+export const apiSendSms = '/api/Sms/send'
+export const apiSendEms = '/api/Ems/send'
+
+/**
+ * 上传文件
+ */
+export function fileUpload(fd: FormData, params: anyObj = {}, forceLocal = false, config: AxiosRequestConfig = {}): ApiPromise {
+ let errorMsg = ''
+ const file = fd.get('file') as UploadRawFile
+ const siteConfig = useSiteConfig()
+
+ if (!file.name || typeof file.size == 'undefined') {
+ errorMsg = i18n.global.t('utils.The data of the uploaded file is incomplete!')
+ } else if (!checkFileMimetype(file.name, file.type)) {
+ errorMsg = i18n.global.t('utils.The type of uploaded file is not allowed!')
+ } else if (file.size > siteConfig.upload.maxSize) {
+ errorMsg = i18n.global.t('utils.The size of the uploaded file exceeds the allowed range!')
+ }
+ if (errorMsg) {
+ return new Promise((resolve, reject) => {
+ ElNotification({
+ type: 'error',
+ message: errorMsg,
+ zIndex: SYSTEM_ZINDEX,
+ })
+ reject(errorMsg)
+ })
+ }
+
+ if (!forceLocal && uploadExpandState() == 'enable') {
+ return uploadExpand(fd, params, config)
+ }
+
+ return createAxios({
+ url: isAdminApp() ? adminUploadUrl : apiUploadUrl,
+ method: 'POST',
+ data: fd,
+ params: params,
+ timeout: 0,
+ ...config,
+ })
+}
+
+/**
+ * 生成文件后缀icon的svg图片
+ * @param suffix 后缀名
+ * @param background 背景色,如:rgb(255,255,255)
+ */
+export function buildSuffixSvgUrl(suffix: string, background = '') {
+ const adminInfo = useAdminInfo()
+ return (
+ getUrl() +
+ (isAdminApp() ? adminBuildSuffixSvgUrl : apiBuildSuffixSvgUrl) +
+ '?batoken=' +
+ adminInfo.getToken() +
+ '&suffix=' +
+ suffix +
+ (background ? '&background=' + background : '') +
+ '&server=1'
+ )
+}
+
+/**
+ * 获取地区数据
+ */
+export function getArea(values: number[]) {
+ const params: { province?: number; city?: number; uuid?: string } = {}
+ if (values[0]) {
+ params.province = values[0]
+ }
+ if (values[1]) {
+ params.city = values[1]
+ }
+ params.uuid = uuid()
+ return createAxios({
+ url: isAdminApp() ? adminAreaUrl : apiAreaUrl,
+ method: 'GET',
+ params: params,
+ })
+}
+
+/**
+ * 发送短信
+ */
+export function sendSms(mobile: string, templateCode: string, extend: anyObj = {}) {
+ return createAxios(
+ {
+ url: apiSendSms,
+ method: 'POST',
+ data: {
+ mobile: mobile,
+ template_code: templateCode,
+ ...extend,
+ },
+ },
+ {
+ showSuccessMessage: true,
+ }
+ )
+}
+
+/**
+ * 发送邮件
+ */
+export function sendEms(email: string, event: string, extend: anyObj = {}) {
+ return createAxios(
+ {
+ url: apiSendEms,
+ method: 'POST',
+ data: {
+ email: email,
+ event: event,
+ ...extend,
+ },
+ },
+ {
+ showSuccessMessage: true,
+ }
+ )
+}
+
+/**
+ * 缓存清理接口
+ */
+export function postClearCache(type: string) {
+ return createAxios(
+ {
+ url: clearCacheUrl,
+ method: 'POST',
+ data: {
+ type: type,
+ },
+ },
+ {
+ showSuccessMessage: true,
+ }
+ )
+}
+
+/**
+ * 构建命令执行窗口url
+ */
+export function buildTerminalUrl(commandKey: string, uuid: string, extend: string) {
+ const adminInfo = useAdminInfo()
+ return (
+ getUrl() + terminalUrl + '?command=' + commandKey + '&uuid=' + uuid + '&extend=' + extend + '&batoken=' + adminInfo.getToken() + '&server=1'
+ )
+}
+
+/**
+ * 请求修改终端配置
+ */
+export function postChangeTerminalConfig(data: { manager?: string; port?: string }) {
+ return createAxios({
+ url: changeTerminalConfigUrl,
+ method: 'POST',
+ data: data,
+ })
+}
+
+/**
+ * 远程下拉框数据获取
+ */
+export function getSelectData(remoteUrl: string, q: string, params: anyObj = {}) {
+ return createAxios({
+ url: remoteUrl,
+ method: 'get',
+ params: {
+ select: true,
+ quickSearch: q,
+ ...params,
+ },
+ })
+}
+
+export function buildCaptchaUrl() {
+ return getUrl() + captchaUrl + '?server=1'
+}
+
+export function getCaptchaData(id: string, apiBaseURL: string) {
+ return createAxios({
+ url: apiBaseURL + clickCaptchaUrl,
+ method: 'get',
+ params: {
+ id,
+ },
+ })
+}
+
+export function checkClickCaptcha(id: string, info: string, unset: boolean, apiBaseURL: string) {
+ return createAxios(
+ {
+ url: apiBaseURL + checkClickCaptchaUrl,
+ method: 'post',
+ data: {
+ id,
+ info,
+ unset,
+ },
+ },
+ {
+ showCodeMessage: false,
+ }
+ )
+}
+
+export function getTablePk(table: string, connection = '') {
+ return createAxios({
+ url: getTablePkUrl,
+ method: 'get',
+ params: {
+ table: table,
+ connection: connection,
+ },
+ })
+}
+
+/**
+ * 获取数据表的字段
+ * @param table 数据表名
+ * @param clean 只要干净的字段注释(只要字段标题)
+ */
+export function getTableFieldList(table: string, clean = true, connection = '') {
+ return createAxios({
+ url: getTableFieldListUrl,
+ method: 'get',
+ params: {
+ table: table,
+ clean: clean ? 1 : 0,
+ connection: connection,
+ },
+ })
+}
+
+export function refreshToken() {
+ const adminInfo = useAdminInfo()
+ const userInfo = useUserInfo()
+ return createAxios({
+ url: refreshTokenUrl,
+ method: 'POST',
+ data: {
+ refreshToken: isAdminApp() ? adminInfo.getToken('refresh') : userInfo.getToken('refresh'),
+ },
+ })
+}
+
+/**
+ * 快速生成一个控制器的 增、删、改、查、排序 接口的请求方法
+ * 本 class 实例通常直接传递给 baTable 使用,开发者可重写本类的方法,亦可直接向 baTable 传递自定义的 API 请求类
+ * 表格相关网络请求无需局限于本类,开发者可于 /src/api/ 目录创建自定义的接口请求函数,并于需要的地方导入使用即可
+ */
+export class baTableApi {
+ private controllerUrl
+ public actionUrl
+
+ constructor(controllerUrl: string) {
+ this.controllerUrl = controllerUrl
+ this.actionUrl = new Map([
+ ['index', controllerUrl + 'index'],
+ ['add', controllerUrl + 'add'],
+ ['edit', controllerUrl + 'edit'],
+ ['del', controllerUrl + 'del'],
+ ['sortable', controllerUrl + 'sortable'],
+ ])
+ }
+
+ /**
+ * 表格查看接口的请求方法
+ * @param filter 数据过滤条件
+ */
+ index(filter: BaTable['filter'] = {}) {
+ return createAxios({
+ url: this.actionUrl.get('index'),
+ method: 'get',
+ params: filter,
+ })
+ }
+
+ /**
+ * 获取被编辑行数据
+ * @param params 被编辑行主键等
+ */
+ edit(params: anyObj) {
+ return createAxios({
+ url: this.actionUrl.get('edit'),
+ method: 'get',
+ params,
+ })
+ }
+
+ /**
+ * 表格删除接口的请求方法
+ * @param ids 被删除数据的主键数组
+ */
+ del(ids: string[]) {
+ return createAxios(
+ {
+ url: this.actionUrl.get('del'),
+ method: 'DELETE',
+ params: {
+ ids,
+ },
+ },
+ {
+ showSuccessMessage: true,
+ }
+ )
+ }
+
+ /**
+ * 向指定接口 POST 数据,本方法虽然较为通用,但请不要局限于此,开发者可于 /src/api/ 目录创建自定义的接口请求函数,并于需要的地方导入使用即可
+ * @param action 请求的接口,比如 add、edit
+ * @param data 要 POST 的数据
+ */
+ postData(action: string, data: anyObj) {
+ return createAxios(
+ {
+ url: this.actionUrl.has(action) ? this.actionUrl.get(action) : this.controllerUrl + action,
+ method: 'post',
+ data,
+ },
+ {
+ showSuccessMessage: true,
+ }
+ )
+ }
+
+ /**
+ * 表格行排序接口的请求方法
+ */
+ sortable(data: anyObj) {
+ return createAxios({
+ url: this.actionUrl.get('sortable'),
+ method: 'post',
+ data,
+ })
+ }
+}
diff --git a/web/src/api/frontend/index.ts b/web/src/api/frontend/index.ts
new file mode 100644
index 0000000..051eb67
--- /dev/null
+++ b/web/src/api/frontend/index.ts
@@ -0,0 +1,56 @@
+import createAxios from '/@/utils/axios'
+import { useSiteConfig } from '/@/stores/siteConfig'
+import { useMemberCenter } from '/@/stores/memberCenter'
+import { debounce, setTitleFromRoute } from '/@/utils/common'
+import { handleFrontendRoute } from '/@/utils/router'
+import { useUserInfo } from '/@/stores/userInfo'
+import router from '/@/router/index'
+import { isEmpty } from 'lodash-es'
+
+export const indexUrl = '/api/index/'
+
+/**
+ * 前台初始化请求,获取站点配置信息,动态路由信息等
+ * 1. 首次初始化携带了会员token时,一共只初始化一次
+ * 2. 首次初始化未带会员token,将在登录后再初始化一次
+ */
+export function initialize(callback?: (res: ApiResponse) => void, requiredLogin?: boolean) {
+ debounce(() => {
+ if (router.currentRoute.value.meta.initialize === false) return
+
+ const userInfo = useUserInfo()
+ const siteConfig = useSiteConfig()
+ if (!userInfo.isLogin() && siteConfig.initialize) return
+ if (userInfo.isLogin() && siteConfig.userInitialize) return
+
+ const memberCenter = useMemberCenter()
+
+ createAxios({
+ url: indexUrl + 'index',
+ method: 'get',
+ params: {
+ requiredLogin: requiredLogin ? 1 : 0,
+ },
+ }).then((res) => {
+ handleFrontendRoute(res.data.rules, res.data.menus)
+ siteConfig.dataFill(res.data.site)
+ memberCenter.setStatus(res.data.openMemberCenter)
+
+ if (!isEmpty(res.data.userInfo)) {
+ userInfo.dataFill(res.data.userInfo)
+
+ // 请求到会员信息才设置会员中心初始化是成功的
+ siteConfig.setUserInitialize(true)
+ }
+
+ if (!res.data.openMemberCenter) memberCenter.setLayoutMode('Disable')
+
+ siteConfig.setInitialize(true)
+
+ // 根据当前路由重设页面标题
+ setTitleFromRoute()
+
+ typeof callback == 'function' && callback(res)
+ })
+ }, 200)()
+}
diff --git a/web/src/api/frontend/user/index.ts b/web/src/api/frontend/user/index.ts
new file mode 100644
index 0000000..069a982
--- /dev/null
+++ b/web/src/api/frontend/user/index.ts
@@ -0,0 +1,120 @@
+import createAxios from '/@/utils/axios'
+import { useUserInfo } from '/@/stores/userInfo'
+
+export const userUrl = '/api/user/'
+export const accountUrl = '/api/account/'
+
+export function checkIn(method: 'get' | 'post', params: object = {}) {
+ return createAxios({
+ url: userUrl + 'checkIn',
+ data: params,
+ method: method,
+ })
+}
+
+export function overview() {
+ return createAxios({
+ url: accountUrl + 'overview',
+ method: 'get',
+ })
+}
+
+export function postProfile(params: anyObj) {
+ return createAxios(
+ {
+ url: accountUrl + 'profile',
+ method: 'POST',
+ data: params,
+ },
+ {
+ showSuccessMessage: true,
+ }
+ )
+}
+
+export function getProfile() {
+ return createAxios({
+ url: accountUrl + 'profile',
+ method: 'get',
+ })
+}
+
+export function postVerification(data: anyObj) {
+ return createAxios({
+ url: accountUrl + 'verification',
+ method: 'post',
+ data: data,
+ })
+}
+
+export function postChangeBind(data: anyObj) {
+ return createAxios(
+ {
+ url: accountUrl + 'changeBind',
+ method: 'post',
+ data: data,
+ },
+ {
+ showSuccessMessage: true,
+ }
+ )
+}
+
+export function changePassword(params: anyObj) {
+ return createAxios(
+ {
+ url: accountUrl + 'changePassword',
+ method: 'POST',
+ data: params,
+ },
+ {
+ showSuccessMessage: true,
+ }
+ )
+}
+
+export function getBalanceLog(page: number, pageSize: number) {
+ return createAxios({
+ url: accountUrl + 'balance',
+ method: 'GET',
+ params: {
+ page: page,
+ limit: pageSize,
+ },
+ })
+}
+
+export function getIntegralLog(page: number, pageSize: number) {
+ return createAxios({
+ url: accountUrl + 'integral',
+ method: 'GET',
+ params: {
+ page: page,
+ limit: pageSize,
+ },
+ })
+}
+
+export function postLogout() {
+ const userInfo = useUserInfo()
+ return createAxios({
+ url: userUrl + 'logout',
+ method: 'POST',
+ data: {
+ refreshToken: userInfo.getToken('refresh'),
+ },
+ })
+}
+
+export function retrievePassword(params: anyObj) {
+ return createAxios(
+ {
+ url: accountUrl + 'retrievePassword',
+ method: 'POST',
+ data: params,
+ },
+ {
+ showSuccessMessage: true,
+ }
+ )
+}
diff --git a/web/src/assets/bg-dark.jpg b/web/src/assets/bg-dark.jpg
new file mode 100644
index 0000000..c88faff
Binary files /dev/null and b/web/src/assets/bg-dark.jpg differ
diff --git a/web/src/assets/bg.jpg b/web/src/assets/bg.jpg
new file mode 100644
index 0000000..ddf4655
Binary files /dev/null and b/web/src/assets/bg.jpg differ
diff --git a/web/src/assets/dashboard/coffee.svg b/web/src/assets/dashboard/coffee.svg
new file mode 100644
index 0000000..bd88779
--- /dev/null
+++ b/web/src/assets/dashboard/coffee.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web/src/assets/dashboard/header-1.svg b/web/src/assets/dashboard/header-1.svg
new file mode 100644
index 0000000..f7dc22c
--- /dev/null
+++ b/web/src/assets/dashboard/header-1.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web/src/assets/icons/dark.svg b/web/src/assets/icons/dark.svg
new file mode 100644
index 0000000..3ef1cf0
--- /dev/null
+++ b/web/src/assets/icons/dark.svg
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/web/src/assets/icons/full-screen-cancel.svg b/web/src/assets/icons/full-screen-cancel.svg
new file mode 100644
index 0000000..e08e22b
--- /dev/null
+++ b/web/src/assets/icons/full-screen-cancel.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web/src/assets/icons/lang.svg b/web/src/assets/icons/lang.svg
new file mode 100644
index 0000000..8fe9126
--- /dev/null
+++ b/web/src/assets/icons/lang.svg
@@ -0,0 +1 @@
+
diff --git a/web/src/assets/icons/light.svg b/web/src/assets/icons/light.svg
new file mode 100644
index 0000000..e1cc97c
--- /dev/null
+++ b/web/src/assets/icons/light.svg
@@ -0,0 +1,8 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/web/src/assets/icons/logo.svg b/web/src/assets/icons/logo.svg
new file mode 100644
index 0000000..da77266
--- /dev/null
+++ b/web/src/assets/icons/logo.svg
@@ -0,0 +1,102 @@
+
+
+
+
diff --git a/web/src/assets/icons/terminal.svg b/web/src/assets/icons/terminal.svg
new file mode 100644
index 0000000..b0edca4
--- /dev/null
+++ b/web/src/assets/icons/terminal.svg
@@ -0,0 +1,2 @@
+
\ No newline at end of file
diff --git a/web/src/assets/index/index-cover.svg b/web/src/assets/index/index-cover.svg
new file mode 100644
index 0000000..2abce1c
--- /dev/null
+++ b/web/src/assets/index/index-cover.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web/src/assets/login-header.png b/web/src/assets/login-header.png
new file mode 100644
index 0000000..545b9c0
Binary files /dev/null and b/web/src/assets/login-header.png differ
diff --git a/web/src/assets/logo.png b/web/src/assets/logo.png
new file mode 100644
index 0000000..c39e983
Binary files /dev/null and b/web/src/assets/logo.png differ
diff --git a/web/src/assets/qr.png b/web/src/assets/qr.png
new file mode 100644
index 0000000..c55172d
Binary files /dev/null and b/web/src/assets/qr.png differ
diff --git a/web/src/components/baInput/components/array.vue b/web/src/components/baInput/components/array.vue
new file mode 100644
index 0000000..c9f46de
--- /dev/null
+++ b/web/src/components/baInput/components/array.vue
@@ -0,0 +1,85 @@
+
+
+
+ {{ state.keyTitle }}
+ {{ state.valueTitle }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('Add') }}
+
+
+
+
+
+
+
+
diff --git a/web/src/components/baInput/components/baUpload.vue b/web/src/components/baInput/components/baUpload.vue
new file mode 100644
index 0000000..25d86af
--- /dev/null
+++ b/web/src/components/baInput/components/baUpload.vue
@@ -0,0 +1,518 @@
+
+
+
+
+
+
+ {{ $t('utils.choice') }}
+
+
+
+
+
+
+ {{ $t('Upload') }}
+
+
+
+ {{ $t('utils.choice') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/components/baInput/components/editor.vue b/web/src/components/baInput/components/editor.vue
new file mode 100644
index 0000000..5d52805
--- /dev/null
+++ b/web/src/components/baInput/components/editor.vue
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/components/baInput/components/iconSelector.vue b/web/src/components/baInput/components/iconSelector.vue
new file mode 100644
index 0000000..4c527da
--- /dev/null
+++ b/web/src/components/baInput/components/iconSelector.vue
@@ -0,0 +1,286 @@
+
+
+
+
+
+
+
+
+
{{ state.prependIcon ? state.prependIcon : state.defaultModelValue }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/components/baInput/components/remoteSelect.vue b/web/src/components/baInput/components/remoteSelect.vue
new file mode 100644
index 0000000..ff29b9f
--- /dev/null
+++ b/web/src/components/baInput/components/remoteSelect.vue
@@ -0,0 +1,352 @@
+
+
+
+
+
+
+
+
+
+
+ {{ key }}: {{ item[tooltipParam] }}
+
+ {{ item[field] }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/components/baInput/components/selectFile.vue b/web/src/components/baInput/components/selectFile.vue
new file mode 100644
index 0000000..c1860f6
--- /dev/null
+++ b/web/src/components/baInput/components/selectFile.vue
@@ -0,0 +1,246 @@
+
+
+
+
+
+
+
+
+ {{ t('utils.You can also select') }}
+ {{ limit - baTable.table.selection!.length }}
+ {{ t('utils.items') }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/components/baInput/helper.ts b/web/src/components/baInput/helper.ts
new file mode 100644
index 0000000..7ceb029
--- /dev/null
+++ b/web/src/components/baInput/helper.ts
@@ -0,0 +1,206 @@
+import type { FieldData } from './index'
+
+export const npuaFalse = () => {
+ return {
+ null: false,
+ primaryKey: false,
+ unsigned: false,
+ autoIncrement: false,
+ }
+}
+
+/**
+ * 所有 Input 支持的类型对应的数据字段类型等数据(默认/示例设计)
+ */
+export const fieldData: FieldData = {
+ string: {
+ type: 'varchar',
+ length: 255,
+ precision: 0,
+ defaultType: 'EMPTY STRING',
+ ...npuaFalse(),
+ },
+ password: {
+ type: 'varchar',
+ length: 32,
+ precision: 0,
+ defaultType: 'EMPTY STRING',
+ ...npuaFalse(),
+ },
+ number: {
+ type: 'int',
+ length: 10,
+ precision: 0,
+ defaultType: 'NULL',
+ ...npuaFalse(),
+ null: true,
+ },
+ radio: {
+ type: 'enum',
+ length: 0,
+ precision: 0,
+ defaultType: 'NULL',
+ ...npuaFalse(),
+ null: true,
+ },
+ checkbox: {
+ type: 'set',
+ length: 0,
+ precision: 0,
+ defaultType: 'NULL',
+ ...npuaFalse(),
+ null: true,
+ },
+ switch: {
+ type: 'tinyint',
+ length: 1,
+ precision: 0,
+ default: '0',
+ defaultType: 'INPUT',
+ ...npuaFalse(),
+ unsigned: true,
+ },
+ textarea: {
+ type: 'varchar',
+ length: 255,
+ precision: 0,
+ defaultType: 'EMPTY STRING',
+ ...npuaFalse(),
+ },
+ array: {
+ type: 'varchar',
+ length: 255,
+ precision: 0,
+ defaultType: 'EMPTY STRING',
+ ...npuaFalse(),
+ },
+ datetime: {
+ type: 'bigint',
+ length: 16,
+ precision: 0,
+ defaultType: 'NULL',
+ ...npuaFalse(),
+ null: true,
+ unsigned: true,
+ },
+ year: {
+ type: 'year',
+ length: 4,
+ precision: 0,
+ defaultType: 'NULL',
+ ...npuaFalse(),
+ null: true,
+ },
+ date: {
+ type: 'date',
+ length: 0,
+ precision: 0,
+ defaultType: 'NULL',
+ ...npuaFalse(),
+ null: true,
+ },
+ time: {
+ type: 'time',
+ length: 0,
+ precision: 0,
+ defaultType: 'NULL',
+ ...npuaFalse(),
+ null: true,
+ },
+ select: {
+ type: 'enum',
+ length: 0,
+ precision: 0,
+ defaultType: 'NULL',
+ ...npuaFalse(),
+ null: true,
+ },
+ selects: {
+ type: 'varchar',
+ length: 255,
+ precision: 0,
+ defaultType: 'EMPTY STRING',
+ ...npuaFalse(),
+ },
+ remoteSelect: {
+ type: 'int',
+ length: 10,
+ precision: 0,
+ defaultType: 'NULL',
+ ...npuaFalse(),
+ null: true,
+ unsigned: true,
+ },
+ remoteSelects: {
+ type: 'varchar',
+ length: 255,
+ precision: 0,
+ defaultType: 'EMPTY STRING',
+ ...npuaFalse(),
+ },
+ editor: {
+ type: 'text',
+ length: 0,
+ precision: 0,
+ defaultType: 'NULL',
+ ...npuaFalse(),
+ null: true,
+ },
+ city: {
+ type: 'varchar',
+ length: 100,
+ precision: 0,
+ defaultType: 'EMPTY STRING',
+ ...npuaFalse(),
+ },
+ image: {
+ type: 'varchar',
+ length: 255,
+ precision: 0,
+ defaultType: 'EMPTY STRING',
+ ...npuaFalse(),
+ },
+ images: {
+ type: 'varchar',
+ length: 1500,
+ precision: 0,
+ defaultType: 'EMPTY STRING',
+ ...npuaFalse(),
+ },
+ file: {
+ type: 'varchar',
+ length: 255,
+ precision: 0,
+ defaultType: 'EMPTY STRING',
+ ...npuaFalse(),
+ },
+ files: {
+ type: 'varchar',
+ length: 1500,
+ precision: 0,
+ defaultType: 'EMPTY STRING',
+ ...npuaFalse(),
+ },
+ icon: {
+ type: 'varchar',
+ length: 50,
+ precision: 0,
+ defaultType: 'EMPTY STRING',
+ ...npuaFalse(),
+ },
+ color: {
+ type: 'varchar',
+ length: 50,
+ precision: 0,
+ defaultType: 'EMPTY STRING',
+ ...npuaFalse(),
+ },
+}
+
+export const stringToArray = (val: string | string[]) => {
+ if (typeof val === 'string') {
+ return val == '' ? [] : val.split(',')
+ } else {
+ return val as string[]
+ }
+}
diff --git a/web/src/components/baInput/index.ts b/web/src/components/baInput/index.ts
new file mode 100644
index 0000000..a46d899
--- /dev/null
+++ b/web/src/components/baInput/index.ts
@@ -0,0 +1,218 @@
+import type { Component, CSSProperties } from 'vue'
+
+/**
+ * 支持的输入框类型
+ * 若您正在设计数据表,可以找到 ./helper.ts 文件来参考对应类型的:数据字段设计示例
+ */
+export const inputTypes = [
+ 'string',
+ 'password',
+ 'number',
+ 'radio',
+ 'checkbox',
+ 'switch',
+ 'textarea',
+ 'array',
+ 'datetime',
+ 'year',
+ 'date',
+ 'time',
+ 'select',
+ 'selects',
+ 'remoteSelect',
+ 'remoteSelects',
+ 'editor',
+ 'city',
+ 'image',
+ 'images',
+ 'file',
+ 'files',
+ 'icon',
+ 'color',
+]
+export type ModelValueTypes = string | number | boolean | object
+
+export interface InputData {
+ // 内容,比如radio的选项列表数据,格式为对象或者数组:{ a: '选项1', b: '选项2' } or [{value: '1', label: 2, disabled: false}, {...}]
+ content?: any
+ // 需要生成子级元素时,子级元素属性(比如radio)
+ childrenAttr?: anyObj
+ // 城市选择器等级,1=省,2=市,3=区
+ level?: number
+}
+
+/**
+ * input可用属性,用于代码提示,渲染不同输入组件时,需要的属性是不一样的
+ * https://element-plus.org/zh-CN/component/input.html#input-属性
+ */
+export interface InputAttr extends InputData {
+ id?: string
+ name?: string
+ type?: string
+ placeholder?: string
+ maxlength?: string | number
+ minlength?: string | number
+ showWordLimit?: boolean
+ clearable?: boolean
+ showPassword?: boolean
+ disabled?: boolean
+ size?: 'large' | 'default' | 'small'
+ prefixIcon?: string | Component
+ suffixIcon?: string | Component
+ rows?: number
+ border?: boolean
+ autosize?: boolean | anyObj
+ autocomplete?: string
+ readonly?: boolean
+ max?: string | number
+ min?: string | number
+ step?: string | number
+ resize?: 'none' | 'both' | 'horizontal' | 'vertical'
+ autofocus?: boolean
+ form?: string
+ label?: string
+ tabindex?: string | number
+ validateEvent?: boolean
+ inputStyle?: anyObj
+ activeValue?: string | number | boolean
+ inactiveValue?: string | number | boolean
+ emptyValues?: any[]
+ valueOnClear?: string | number | boolean | Function
+ // DateTimePicker属性
+ editable?: boolean
+ startPlaceholder?: string
+ endPlaceholder?: string
+ timeArrowControl?: boolean
+ format?: string
+ popperClass?: string
+ rangeSeparator?: string
+ defaultValue?: Date
+ defaultTime?: Date | Date[]
+ valueFormat?: string
+ unlinkPanels?: boolean
+ clearIcon?: string | Component
+ shortcuts?: { text: string; value: Date | Function }[]
+ disabledDate?: Function
+ cellClassName?: Function
+ teleported?: boolean
+ // select属性
+ multiple?: boolean
+ valueKey?: string
+ collapseTags?: string
+ collapseTagsTooltip?: boolean
+ multipleLimit?: number
+ effect?: 'dark' | 'light'
+ filterable?: boolean
+ allowCreate?: boolean
+ filterMethod?: Function
+ remote?: false // 禁止使用远程搜索,如需使用请使用单独封装好的 remoteSelect 组件
+ remoteMethod?: false
+ labelFormatter?: (optionData: anyObj, optionKey: string) => string
+ noMatchText?: string
+ noDataText?: string
+ reserveKeyword?: boolean
+ defaultFirstOption?: boolean
+ popperAppendToBody?: boolean
+ persistent?: boolean
+ automaticDropdown?: boolean
+ fitInputWidth?: boolean
+ tagType?: 'success' | 'info' | 'warning' | 'danger'
+ params?: anyObj
+ // 远程select属性
+ pk?: string
+ field?: string
+ remoteUrl?: string
+ tooltipParams?: anyObj
+ escBlur?: boolean
+ // 图标选择器属性
+ showIconName?: boolean
+ placement?: string
+ title?: string
+ // 颜色选择器
+ showAlpha?: boolean
+ colorFormat?: string
+ predefine?: string[]
+ // 图片文件上传属性
+ action?: string
+ headers?: anyObj
+ method?: string
+ data?: anyObj
+ withCredentials?: boolean
+ showFileList?: boolean
+ drag?: boolean
+ accept?: string
+ listType?: string
+ autoUpload?: boolean
+ limit?: number
+ hideSelectFile?: boolean
+ returnFullUrl?: boolean
+ forceLocal?: boolean
+ hideImagePlusOnOverLimit?: boolean
+ // editor属性
+ height?: string
+ mode?: string
+ editorStyle?: CSSProperties
+ style?: CSSProperties
+ toolbarConfig?: anyObj
+ editorConfig?: anyObj
+ editorType?: string
+ preview?: boolean
+ language?: string
+ theme?: 'light' | 'dark'
+ toolbarsExclude?: string[]
+ fileForceLocal?: boolean
+ // array组件属性
+ keyTitle?: string
+ valueTitle?: string
+ // 返回数据类型
+ dataType?: string
+ // 是否渲染为 button(radio 和 checkbox)
+ button?: boolean
+ // 事件
+ onPreview?: Function
+ onRemove?: Function
+ onSuccess?: Function
+ onError?: Function
+ onProgress?: Function
+ onExceed?: Function
+ onBeforeUpload?: Function
+ onBeforeRemove?: Function
+ onChange?: Function
+ onInput?: Function
+ onVisibleChange?: Function
+ onRemoveTag?: Function
+ onClear?: Function
+ onBlur?: Function
+ onFocus?: Function
+ onCalendarChange?: Function
+ onPanelChange?: Function
+ onActiveChange?: Function
+ onRow?: Function
+ [key: string]: any
+}
+
+/**
+ * Input 支持的类型对应的数据字段设计数据
+ */
+export interface FieldData {
+ [key: string]: {
+ // 数据类型
+ type: string
+ // 长度
+ length: number
+ // 小数点
+ precision: number
+ // 默认值
+ default?: string
+ // 默认值类型:INPUT=输入,EMPTY STRING=空字符串,NULL=NULL,NONE=无
+ defaultType: 'INPUT' | 'EMPTY STRING' | 'NULL' | 'NONE'
+ // 允许 null
+ null: boolean
+ // 主键
+ primaryKey: boolean
+ // 无符号
+ unsigned: boolean
+ // 自动递增
+ autoIncrement: boolean
+ }
+}
diff --git a/web/src/components/baInput/index.vue b/web/src/components/baInput/index.vue
new file mode 100644
index 0000000..cc905f2
--- /dev/null
+++ b/web/src/components/baInput/index.vue
@@ -0,0 +1,525 @@
+
+
+
diff --git a/web/src/components/clickCaptcha/index.ts b/web/src/components/clickCaptcha/index.ts
new file mode 100644
index 0000000..db89488
--- /dev/null
+++ b/web/src/components/clickCaptcha/index.ts
@@ -0,0 +1,47 @@
+import { createVNode, render } from 'vue'
+import ClickCaptchaConstructor from './index.vue'
+import { shortUuid } from '/@/utils/random'
+
+interface ClickCaptchaOptions {
+ // 验证码弹窗的自定义class
+ class?: string
+ // 前端验证成功时立即清理验证码数据,不可再进行二次验证,不开启则 600s 后自动清理数据
+ unset?: boolean
+ // 验证失败的提示信息
+ error?: string
+ // 验证成功的提示信息
+ success?: string
+ // 验证码 API 的基础 URL,默认为当前服务端 URL(VITE_AXIOS_BASE_URL)
+ apiBaseURL?: string
+}
+
+/**
+ * 弹出点击验证码
+ * @param uuid 开发者自定义的唯一标识
+ * @param callback 验证成功的回调,业务接口可通过 captchaInfo 进行二次验证
+ * @param options
+ */
+const clickCaptcha = (uuid: string, callback?: (captchaInfo: string) => void, options: ClickCaptchaOptions = {}) => {
+ const container = document.createElement('div')
+ const vnode = createVNode(ClickCaptchaConstructor, {
+ uuid,
+ callback,
+ ...options,
+ key: shortUuid(),
+ onDestroy: () => {
+ render(null, container)
+ },
+ })
+ render(vnode, container)
+ document.body.appendChild(container.firstElementChild!)
+}
+
+/**
+ * 组件的 props 类型定义
+ */
+export interface Props extends ClickCaptchaOptions {
+ uuid: string
+ callback?: (captchaInfo: string) => void
+}
+
+export default clickCaptcha
diff --git a/web/src/components/clickCaptcha/index.vue b/web/src/components/clickCaptcha/index.vue
new file mode 100644
index 0000000..1c8106e
--- /dev/null
+++ b/web/src/components/clickCaptcha/index.vue
@@ -0,0 +1,221 @@
+
+
+
+
{{ i18n.global.t('utils.Loading') }}
+
+
+
+ {{ index + 1 }}
+
+
+
+ {{ state.tip }}
+
+
+ {{ i18n.global.t('validate.Please click') }}
+
+ {{ text }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/components/contextmenu/index.vue b/web/src/components/contextmenu/index.vue
new file mode 100644
index 0000000..48bc758
--- /dev/null
+++ b/web/src/components/contextmenu/index.vue
@@ -0,0 +1,154 @@
+
+
+
+
+
+
+
+
+
diff --git a/web/src/components/contextmenu/interface.ts b/web/src/components/contextmenu/interface.ts
new file mode 100644
index 0000000..a17574a
--- /dev/null
+++ b/web/src/components/contextmenu/interface.ts
@@ -0,0 +1,20 @@
+export interface Axis {
+ x: number
+ y: number
+}
+
+export interface ContextMenuItem {
+ name: string
+ label: string
+ icon?: string
+ disabled?: boolean
+}
+
+export interface ContextMenuItemClickEmitArg extends ContextMenuItem {
+ sourceData?: T
+}
+
+export interface Props {
+ width?: number
+ items: ContextMenuItem[]
+}
diff --git a/web/src/components/formItem/createData.vue b/web/src/components/formItem/createData.vue
new file mode 100644
index 0000000..16be18a
--- /dev/null
+++ b/web/src/components/formItem/createData.vue
@@ -0,0 +1,304 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/components/formItem/index.ts b/web/src/components/formItem/index.ts
new file mode 100644
index 0000000..c2fa777
--- /dev/null
+++ b/web/src/components/formItem/index.ts
@@ -0,0 +1,13 @@
+import type { CSSProperties } from 'vue'
+import type { FormItemProps, ElTooltipProps } from 'element-plus'
+
+export interface FormItemAttr extends Partial> {
+ // 通用属性名称的键入提示
+ id?: string
+ class?: string
+ style?: CSSProperties
+ // 块级输入帮助信息
+ blockHelp?: string
+ // 输入提示信息(使用 el-tooltip 渲染)
+ tip?: string | Partial
+}
diff --git a/web/src/components/formItem/index.vue b/web/src/components/formItem/index.vue
new file mode 100644
index 0000000..12a09e9
--- /dev/null
+++ b/web/src/components/formItem/index.vue
@@ -0,0 +1,163 @@
+
+
+
diff --git a/web/src/components/icon/index.vue b/web/src/components/icon/index.vue
new file mode 100644
index 0000000..d4a65dc
--- /dev/null
+++ b/web/src/components/icon/index.vue
@@ -0,0 +1,41 @@
+
diff --git a/web/src/components/icon/svg/index.ts b/web/src/components/icon/svg/index.ts
new file mode 100644
index 0000000..0209946
--- /dev/null
+++ b/web/src/components/icon/svg/index.ts
@@ -0,0 +1,69 @@
+import { readFileSync, readdirSync } from 'fs'
+
+let idPerfix = ''
+const iconNames: string[] = []
+const svgTitle = /+].*?)>/
+const clearHeightWidth = /(width|height)="([^>+].*?)"/g
+const hasViewBox = /(viewBox="[^>+].*?")/g
+const clearReturn = /(\r)|(\n)/g
+// 清理 svg 的 fill
+const clearFill = /(fill="[^>+].*?")/g
+
+function findSvgFile(dir: string): string[] {
+ const svgRes = []
+ const dirents = readdirSync(dir, {
+ withFileTypes: true,
+ })
+ for (const dirent of dirents) {
+ iconNames.push(`${idPerfix}-${dirent.name.replace('.svg', '')}`)
+ if (dirent.isDirectory()) {
+ svgRes.push(...findSvgFile(dir + dirent.name + '/'))
+ } else {
+ const svg = readFileSync(dir + dirent.name)
+ .toString()
+ .replace(clearReturn, '')
+ .replace(clearFill, 'fill=""')
+ .replace(svgTitle, ($1, $2) => {
+ let width = 0
+ let height = 0
+ let content = $2.replace(clearHeightWidth, (s1: string, s2: string, s3: number) => {
+ if (s2 === 'width') {
+ width = s3
+ } else if (s2 === 'height') {
+ height = s3
+ }
+ return ''
+ })
+ if (!hasViewBox.test($2)) {
+ content += `viewBox="0 0 ${width} ${height}"`
+ }
+ return ``
+ })
+ .replace(' ', '')
+ svgRes.push(svg)
+ }
+ }
+ return svgRes
+}
+
+export const svgBuilder = (path: string, perfix = 'local') => {
+ if (path === '') return
+ idPerfix = perfix
+ const res = findSvgFile(path)
+ return {
+ name: 'svg-transform',
+ transformIndexHtml(html: string) {
+ return html.replace(
+ '',
+ `
+
+
+ ${res.join('')}
+
+ `
+ )
+ },
+ }
+}
diff --git a/web/src/components/icon/svg/index.vue b/web/src/components/icon/svg/index.vue
new file mode 100644
index 0000000..10cf4e3
--- /dev/null
+++ b/web/src/components/icon/svg/index.vue
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/components/mixins/baUpload.ts b/web/src/components/mixins/baUpload.ts
new file mode 100644
index 0000000..29b566e
--- /dev/null
+++ b/web/src/components/mixins/baUpload.ts
@@ -0,0 +1,11 @@
+import type { AxiosRequestConfig } from 'axios'
+
+export const state: () => 'disable' | 'enable' = () => 'disable'
+
+export function fileUpload(fd: FormData, params: anyObj = {}, config: AxiosRequestConfig = {}): ApiPromise {
+ // 上传扩展,定义此函数,并将上方的 state 设定为 enable,系统可自动使用此函数进行上传
+ return new Promise((resolve, reject) => {
+ console.log(fd, params, config)
+ reject('未定义')
+ })
+}
diff --git a/web/src/components/mixins/editor/default.vue b/web/src/components/mixins/editor/default.vue
new file mode 100644
index 0000000..a780fce
--- /dev/null
+++ b/web/src/components/mixins/editor/default.vue
@@ -0,0 +1,11 @@
+
+ {{ $t('utils.Please install editor') }}
+
+
+
+
+
diff --git a/web/src/components/mixins/loginFooter.vue b/web/src/components/mixins/loginFooter.vue
new file mode 100644
index 0000000..4dcc65f
--- /dev/null
+++ b/web/src/components/mixins/loginFooter.vue
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/web/src/components/mixins/loginMounted.ts b/web/src/components/mixins/loginMounted.ts
new file mode 100644
index 0000000..3635747
--- /dev/null
+++ b/web/src/components/mixins/loginMounted.ts
@@ -0,0 +1,4 @@
+export default function loginMounted(): Promise {
+ // 通常用于会员登录页初始化时接受各种回调或收参跳转,返回 true 将终止会员登录页初始化
+ return new Promise((resolve) => resolve(false))
+}
diff --git a/web/src/components/mixins/userMounted.ts b/web/src/components/mixins/userMounted.ts
new file mode 100644
index 0000000..33fcf3d
--- /dev/null
+++ b/web/src/components/mixins/userMounted.ts
@@ -0,0 +1,11 @@
+interface UserMountedRet {
+ type: 'jump' | 'break' | 'continue' | 'reload'
+ [key: string]: any
+}
+
+export default function userMounted(): Promise {
+ // 通常用于会员中心初始化时接受各种回调或收参跳转,返回 true 将终止会员中心初始化
+ return new Promise((resolve) => {
+ resolve({ type: 'continue' })
+ })
+}
diff --git a/web/src/components/mixins/userProfile.vue b/web/src/components/mixins/userProfile.vue
new file mode 100644
index 0000000..811a80e
--- /dev/null
+++ b/web/src/components/mixins/userProfile.vue
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/web/src/components/table/comSearch/index.vue b/web/src/components/table/comSearch/index.vue
new file mode 100644
index 0000000..f291a12
--- /dev/null
+++ b/web/src/components/table/comSearch/index.vue
@@ -0,0 +1,300 @@
+
+
+
+
+
+
+
diff --git a/web/src/components/table/fieldRender/buttons.vue b/web/src/components/table/fieldRender/buttons.vue
new file mode 100644
index 0000000..60f5b72
--- /dev/null
+++ b/web/src/components/table/fieldRender/buttons.vue
@@ -0,0 +1,160 @@
+
+
+
+
+
+
+
+ {{ getTranslation(btn.text) }}
+
+
+
+
+
+
+ {{ getTranslation(btn.text) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ getTranslation(btn.text) }}
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/components/table/fieldRender/color.vue b/web/src/components/table/fieldRender/color.vue
new file mode 100644
index 0000000..6ba1f74
--- /dev/null
+++ b/web/src/components/table/fieldRender/color.vue
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
diff --git a/web/src/components/table/fieldRender/customRender.vue b/web/src/components/table/fieldRender/customRender.vue
new file mode 100644
index 0000000..d9bac39
--- /dev/null
+++ b/web/src/components/table/fieldRender/customRender.vue
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
diff --git a/web/src/components/table/fieldRender/customTemplate.vue b/web/src/components/table/fieldRender/customTemplate.vue
new file mode 100644
index 0000000..5d95401
--- /dev/null
+++ b/web/src/components/table/fieldRender/customTemplate.vue
@@ -0,0 +1,21 @@
+
+
+
+
+
diff --git a/web/src/components/table/fieldRender/datetime.vue b/web/src/components/table/fieldRender/datetime.vue
new file mode 100644
index 0000000..09dc1a3
--- /dev/null
+++ b/web/src/components/table/fieldRender/datetime.vue
@@ -0,0 +1,22 @@
+
+
+ {{ !cellValue ? '-' : timeFormat(cellValue, field.timeFormat ?? 'yyyy-mm-dd hh:MM:ss') }}
+
+
+
+
diff --git a/web/src/components/table/fieldRender/default.vue b/web/src/components/table/fieldRender/default.vue
new file mode 100644
index 0000000..27bfa30
--- /dev/null
+++ b/web/src/components/table/fieldRender/default.vue
@@ -0,0 +1,5 @@
+
+
+ Field renderer not found
+
+
diff --git a/web/src/components/table/fieldRender/icon.vue b/web/src/components/table/fieldRender/icon.vue
new file mode 100644
index 0000000..e47cdcc
--- /dev/null
+++ b/web/src/components/table/fieldRender/icon.vue
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
diff --git a/web/src/components/table/fieldRender/image.vue b/web/src/components/table/fieldRender/image.vue
new file mode 100644
index 0000000..1e22e76
--- /dev/null
+++ b/web/src/components/table/fieldRender/image.vue
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
diff --git a/web/src/components/table/fieldRender/images.vue b/web/src/components/table/fieldRender/images.vue
new file mode 100644
index 0000000..f47ae20
--- /dev/null
+++ b/web/src/components/table/fieldRender/images.vue
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/components/table/fieldRender/switch.vue b/web/src/components/table/fieldRender/switch.vue
new file mode 100644
index 0000000..2ca05be
--- /dev/null
+++ b/web/src/components/table/fieldRender/switch.vue
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
diff --git a/web/src/components/table/fieldRender/tag.vue b/web/src/components/table/fieldRender/tag.vue
new file mode 100644
index 0000000..667c5eb
--- /dev/null
+++ b/web/src/components/table/fieldRender/tag.vue
@@ -0,0 +1,34 @@
+
+
+
+ {{ !isEmpty(field.replaceValue) ? (field.replaceValue[cellValue] ?? cellValue) : cellValue }}
+
+
+
+
+
diff --git a/web/src/components/table/fieldRender/tags.vue b/web/src/components/table/fieldRender/tags.vue
new file mode 100644
index 0000000..0113b0f
--- /dev/null
+++ b/web/src/components/table/fieldRender/tags.vue
@@ -0,0 +1,56 @@
+
+
+
+
+
+ {{ !isEmpty(field.replaceValue) ? (field.replaceValue[tag] ?? tag) : tag }}
+
+
+
+
+
+ {{ !isEmpty(field.replaceValue) ? (field.replaceValue[cellValue] ?? cellValue) : cellValue }}
+
+
+
+
+
+
+
+
diff --git a/web/src/components/table/fieldRender/url.vue b/web/src/components/table/fieldRender/url.vue
new file mode 100644
index 0000000..eb2e836
--- /dev/null
+++ b/web/src/components/table/fieldRender/url.vue
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/components/table/header/index.vue b/web/src/components/table/header/index.vue
new file mode 100644
index 0000000..e5ccb4b
--- /dev/null
+++ b/web/src/components/table/header/index.vue
@@ -0,0 +1,243 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/components/table/index.ts b/web/src/components/table/index.ts
new file mode 100644
index 0000000..e1fa2de
--- /dev/null
+++ b/web/src/components/table/index.ts
@@ -0,0 +1,141 @@
+import { TableColumnCtx } from 'element-plus'
+import { isUndefined } from 'lodash-es'
+import { i18n } from '/@/lang/index'
+
+/**
+ * 获取单元格值
+ */
+export const getCellValue = (row: TableRow, field: TableColumn, column: TableColumnCtx, index: number) => {
+ if (!field.prop) return ''
+
+ const prop = field.prop
+ let cellValue: any = row[prop]
+
+ // 字段 prop 带 . 比如 user.nickname
+ if (prop.indexOf('.') > -1) {
+ const fieldNameArr = prop.split('.')
+ cellValue = row[fieldNameArr[0]]
+ for (let index = 1; index < fieldNameArr.length; index++) {
+ cellValue = cellValue ? (cellValue[fieldNameArr[index]] ?? '') : ''
+ }
+ }
+
+ // 若无值,尝试取默认值
+ if ([undefined, null, ''].includes(cellValue) && field.default !== undefined) {
+ cellValue = field.default
+ }
+
+ // 渲染前格式化
+ if (field.renderFormatter && typeof field.renderFormatter == 'function') {
+ cellValue = field.renderFormatter(row, field, cellValue, column, index)
+ console.warn('baTable.table.column.renderFormatter 即将废弃,请直接使用兼容 el-table 的 baTable.table.column.formatter 代替')
+ }
+ if (field.formatter && typeof field.formatter == 'function') {
+ cellValue = field.formatter(row, column, cellValue, index)
+ }
+
+ return cellValue
+}
+
+/*
+ * 默认按钮组
+ */
+export const defaultOptButtons = (optButType: DefaultOptButType[] = ['weigh-sort', 'edit', 'delete']): OptButton[] => {
+ const optButtonsPre: Map = new Map([
+ [
+ 'weigh-sort',
+ {
+ render: 'moveButton',
+ name: 'weigh-sort',
+ title: 'Drag sort',
+ text: '',
+ type: 'info',
+ icon: 'fa fa-arrows',
+ class: 'table-row-weigh-sort',
+ disabledTip: false,
+ },
+ ],
+ [
+ 'edit',
+ {
+ render: 'tipButton',
+ name: 'edit',
+ title: 'Edit',
+ text: '',
+ type: 'primary',
+ icon: 'fa fa-pencil',
+ class: 'table-row-edit',
+ disabledTip: false,
+ },
+ ],
+ [
+ 'delete',
+ {
+ render: 'confirmButton',
+ name: 'delete',
+ title: 'Delete',
+ text: '',
+ type: 'danger',
+ icon: 'fa fa-trash',
+ class: 'table-row-delete',
+ popconfirm: {
+ confirmButtonText: i18n.global.t('Delete'),
+ cancelButtonText: i18n.global.t('Cancel'),
+ confirmButtonType: 'danger',
+ title: i18n.global.t('Are you sure to delete the selected record?'),
+ },
+ disabledTip: false,
+ },
+ ],
+ ])
+
+ const optButtons: OptButton[] = []
+ for (const key in optButType) {
+ if (optButtonsPre.has(optButType[key])) {
+ optButtons.push(optButtonsPre.get(optButType[key])!)
+ }
+ }
+ return optButtons
+}
+
+/**
+ * 将带children的数组降维,然后寻找index所在的行
+ */
+export const findIndexRow = (data: TableRow[], findIdx: number, keyIndex: number | TableRow = -1): number | TableRow => {
+ for (const key in data) {
+ if (typeof keyIndex == 'number') {
+ keyIndex++
+ }
+
+ if (keyIndex == findIdx) {
+ return data[key]
+ }
+
+ if (data[key].children) {
+ keyIndex = findIndexRow(data[key].children!, findIdx, keyIndex)
+ if (typeof keyIndex != 'number') {
+ return keyIndex
+ }
+ }
+ }
+
+ return keyIndex
+}
+
+/**
+ * 调用一个接受表格上下文数据的任意属性计算函数
+ */
+export const invokeTableContextDataFun = (
+ fun: TableContextDataFun | undefined,
+ context: TableContextData,
+ defaultValue: any = {}
+): Partial => {
+ if (isUndefined(fun)) {
+ return defaultValue
+ } else if (typeof fun === 'function') {
+ return fun(context)
+ }
+ return fun
+}
+
+type DefaultOptButType = 'weigh-sort' | 'edit' | 'delete'
diff --git a/web/src/components/table/index.vue b/web/src/components/table/index.vue
new file mode 100644
index 0000000..869c2b1
--- /dev/null
+++ b/web/src/components/table/index.vue
@@ -0,0 +1,246 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/components/terminal/index.vue b/web/src/components/terminal/index.vue
new file mode 100644
index 0000000..6096a3c
--- /dev/null
+++ b/web/src/components/terminal/index.vue
@@ -0,0 +1,439 @@
+
+
+
+
+
+
+
+
+
+
+
{{ getTaskStatus(item.status)['statusText'] }}
+
+ {{ t('terminal.Failure to execute this command will block the execution of the queue') }}
+
+
+ {{ t('terminal.Do not refresh the browser') }}
+
+
{{ item.command }}
+
+
+
+
+
+
+
+ {{ t('terminal.Command run log') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('terminal.Back to terminal') }}
+
+
+
+
+
+
+
+
diff --git a/web/src/lang/autoload.ts b/web/src/lang/autoload.ts
new file mode 100644
index 0000000..af5d708
--- /dev/null
+++ b/web/src/lang/autoload.ts
@@ -0,0 +1,14 @@
+import { adminBaseRoutePath } from '/@/router/static/adminBase'
+
+/*
+ * 语言包按需加载映射表
+ * 使用固定字符串 ${lang} 指代当前语言
+ * key 为页面 path,value 为语言包文件相对路径,访问时,按需自动加载映射表的语言包,同时加载 path 对应的语言包(若存在)
+ */
+export default {
+ '/': ['./frontend/${lang}/index.ts'],
+ [adminBaseRoutePath + '/moduleStore']: ['./backend/${lang}/module.ts'],
+ [adminBaseRoutePath + '/user/rule']: ['./backend/${lang}/auth/rule.ts'],
+ [adminBaseRoutePath + '/user/scoreLog']: ['./backend/${lang}/user/moneyLog.ts'],
+ [adminBaseRoutePath + '/crud/crud']: ['./backend/${lang}/crud/log.ts', './backend/${lang}/crud/state.ts'],
+}
diff --git a/web/src/lang/backend/en.ts b/web/src/lang/backend/en.ts
new file mode 100644
index 0000000..293704a
--- /dev/null
+++ b/web/src/lang/backend/en.ts
@@ -0,0 +1,102 @@
+/**
+ * backend common language package
+ */
+export default {
+ Balance: 'Balance',
+ Integral: 'Integral',
+ Connection: 'connection',
+ 'Database connection': 'Database connection',
+ 'Database connection help': 'You can configure multiple database connections in config/database.php and select it here',
+ layouts: {
+ 'Layout configuration': 'Layout configuration',
+ 'Layout mode': 'Layout mode',
+ default: 'Default',
+ classic: 'Classic',
+ 'Single column': 'Single Column',
+ 'overall situation': 'Global',
+ 'Background page switching animation': 'Background page switching animation',
+ 'Please select an animation name': 'Please select an animation name',
+ sidebar: 'Sidebar',
+ 'Side menu bar background color': 'Background color of the side menu bar',
+ 'Side menu text color': 'Side menu text color',
+ 'Side menu active item background color': 'Background color of the side menu activation item',
+ 'Side menu active item text color': 'Side menu activation item text color',
+ 'Show side menu top bar (logo bar)': 'Display the top bar of the side menu (Logo Bar)',
+ 'Side menu top bar background color': 'Background color of the top bar of the side menu ',
+ 'Side menu width (when expanded)': 'Width of the side menu (Unfolding)',
+ 'Side menu default icon': 'Side menu default icon',
+ 'Side menu horizontal collapse': 'Side menu collapsed horizontally',
+ 'Side menu accordion': 'Side menu accordion',
+ 'Top bar': 'Top bar',
+ 'Top bar background color': 'Top bar background color',
+ 'Top bar text color': 'Top bar text color',
+ 'Background color when hovering over the top bar': 'Top bar hover background color',
+ 'Top bar menu active item background color': 'Background color of the top bar activation item',
+ 'Top bar menu active item text color': 'Top bar menu activation item text color',
+ 'Are you sure you want to restore all configurations to the default values?':
+ 'Are you sure to restore all configurations to the default values?',
+ 'Restore default': 'Restore default',
+ Profile: 'Profile',
+ Logout: 'Logout',
+ 'Dark mode': 'Dark mode',
+ 'Exit full screen': 'Exit Full Screen',
+ 'Full screen is not supported': 'Your browser does not support full screen, please change another browser and try again~',
+ 'Member center': 'Member center',
+ 'Member information': 'Member information',
+ 'Login to the buildadmin': 'Login to the buildadmin',
+ 'Please enter buildadmin account name or email': 'Please enter buildadmin account name or email',
+ 'Please enter the buildadmin account password': 'Please enter the buildadmin account password',
+ Login: 'Login',
+ Password: 'Password',
+ Username: 'Username',
+ Register: 'Register',
+ },
+ terminal: {
+ Source: 'source',
+ Terminal: 'Terminal',
+ 'Command run log': 'Command Run Log',
+ 'No mission yet': 'There is no task yet',
+ 'Test command': 'Test command',
+ 'Install dependent packages': 'Install dependent packages',
+ Republish: 'Republish',
+ 'Clean up task list': 'Clean up the task list',
+ unknown: 'Unknown',
+ 'Waiting for execution': 'Waiting for execution',
+ Connecting: 'Connecting',
+ Executing: 'Executing',
+ 'Successful execution': 'Executed successfully',
+ 'Execution failed': 'Failed to execute',
+ 'Unknown execution result': 'Execution result is unknown',
+ 'Are you sure you want to republish?': 'Are you sure to republish?',
+ 'Failure to execute this command will block the execution of the queue': 'Failed to execute this command will block queue execution.',
+ 'NPM package manager': 'NPM package manager',
+ 'NPM package manager tip': 'Select an available package manager for the execution of commands such as npm install in the WEB terminal',
+ 'Clear successful task': 'Clear successful task',
+ 'Clear successful task tip': 'When you start a new task, automatically clear the list of already successful tasks',
+ 'Manual execution': 'Manual execution',
+ 'Do not refresh the browser': 'Do not refresh your browser.',
+ 'Terminal settings': 'Terminal setup',
+ 'Back to terminal': 'Back to terminal',
+ or: 'or',
+ 'Site domain name': 'Site domain name',
+ 'The current terminal is not running under the installation service, and some commands may not be executed':
+ 'The current terminal is not running under the installation service, and some commands may not be executed.',
+ 'Newly added tasks will never start because they are blocked by failed tasks':
+ 'Newly added tasks will never start because they are blocked by failed tasks!(Web terminal)',
+ 'Failed to modify the source command, Please try again manually': 'Failed to modify the source command. Please try again manually.',
+ },
+ vite: {
+ Later: '稍后',
+ 'Restart hot update': '重启热更新',
+ 'Close type terminal': 'WEB Terminal server',
+ 'Close type crud': 'CRUD server',
+ 'Close type modules': 'module install server',
+ 'Close type config': 'system configuration server',
+ 'Reload hot server title': 'Need to restart Vite hot update service',
+ 'Reload hot server tips 1': 'To ensure that ',
+ 'Reload hot server tips 2':
+ " is not interrupted, the system has temporarily suspended Vite's hot update function. During this period, changes to front-end files will not be updated in real-time and web pages will not be automatically reloaded. It has been detected that there are file updates during the service suspension period, and the hot update service needs to be restarted.",
+ 'Reload hot server tips 3':
+ 'The pause of hot updates does not affect the already loaded functions. You can continue to operate and click to restart the hot update service after everything is ready.',
+ },
+}
diff --git a/web/src/lang/backend/en/auth/admin.ts b/web/src/lang/backend/en/auth/admin.ts
new file mode 100644
index 0000000..285962a
--- /dev/null
+++ b/web/src/lang/backend/en/auth/admin.ts
@@ -0,0 +1,13 @@
+export default {
+ username: 'Username',
+ nickname: 'Nickname',
+ group: 'Group',
+ avatar: 'Avatar',
+ email: 'Email',
+ mobile: 'Mobile Number',
+ 'Last login': 'Last login',
+ Password: 'Password',
+ 'Please leave blank if not modified': 'Please leave blank if you do not modify.',
+ 'Personal signature': 'Personal Signature',
+ 'Administrator login': 'Administrator Login Name',
+}
diff --git a/web/src/lang/backend/en/auth/adminLog.ts b/web/src/lang/backend/en/auth/adminLog.ts
new file mode 100644
index 0000000..a0af627
--- /dev/null
+++ b/web/src/lang/backend/en/auth/adminLog.ts
@@ -0,0 +1,12 @@
+export default {
+ admin_id: 'Manage ID',
+ username: 'Manage Username',
+ title: 'Title',
+ data: 'Request Data',
+ url: 'URL',
+ ip: 'IP',
+ useragent: 'UserAgent',
+ 'Operation administrator': 'Operation administrator',
+ 'Operator IP': 'Operator IP',
+ 'Request data': 'Request Data',
+}
diff --git a/web/src/lang/backend/en/auth/group.ts b/web/src/lang/backend/en/auth/group.ts
new file mode 100644
index 0000000..57f8a04
--- /dev/null
+++ b/web/src/lang/backend/en/auth/group.ts
@@ -0,0 +1,9 @@
+export default {
+ GroupName: 'Group Name',
+ 'Group name': 'Group Name',
+ jurisdiction: 'Permissions',
+ 'Parent group': 'Superior group',
+ 'The parent group cannot be the group itself': 'The parent group cannot be the group itself',
+ 'Manage subordinate role groups here':
+ 'In managing a subordinate role group (excluding a peer role group), you have all the rights of a subordinate role group and additional rights',
+}
diff --git a/web/src/lang/backend/en/auth/rule.ts b/web/src/lang/backend/en/auth/rule.ts
new file mode 100644
index 0000000..1f9bbd3
--- /dev/null
+++ b/web/src/lang/backend/en/auth/rule.ts
@@ -0,0 +1,51 @@
+export default {
+ title: 'Title',
+ Icon: 'Icon',
+ name: 'Name',
+ type: 'Type',
+ cache: 'Cache',
+ 'Superior menu rule': 'Superior menu rules',
+ 'Rule type': 'Rule type',
+ 'type menu_dir': 'Menu directory',
+ 'type menu': 'Menu item',
+ 'type button': 'Page button',
+ 'Rule title': 'Rule title',
+ 'Rule name': 'Rule name',
+ 'Routing path': 'Routing path',
+ 'Rule Icon': 'Rule Icon',
+ 'Menu type': 'Menu type',
+ 'Menu type tab': 'Tab',
+ 'Menu type link (offsite)': 'Link (off-site)',
+ 'Link address': 'Link address',
+ 'Component path': 'Component path',
+ 'Extended properties': 'Extended properties',
+ 'Add as route only': 'Add as route only',
+ 'Add as menu only': 'Add as menu only',
+ 'Rule comments': 'Rule comments',
+ 'Rule weight': 'Rule weights',
+ 'Create Page Button': 'Create Page Button',
+ 'Create Page Button index': 'index',
+ 'Create Page Button add': 'add',
+ 'Create Page Button edit': 'edit',
+ 'Create Page Button del': 'del',
+ 'Create Page Button sortable': 'sortable',
+ 'Create Page Button tips': 'When creating the menu, automatically create the menu page buttons (permission nodes)',
+ 'Please select the button for automatically creating the desired page': 'Please select the button for automatically creating the desired page',
+ 'Please enter the weight of menu rule (sort by)': 'Please enter the menu rule weights (sort by)',
+ 'Please enter the correct URL': 'Please enter the correct URL',
+ 'The superior menu rule cannot be the rule itself': 'The superior menu rules cannot be rules itself.',
+ 'It will be registered as the web side routing name and used as the server side API authentication':
+ 'It will be registered as the routing name of the webside and used as a server-side API authentication at the same time.',
+ 'Please enter the URL address of the link or iframe': 'Please enter the link or the URL address of iframe.',
+ 'English name, which does not need to start with `/admin`, such as auth/menu':
+ 'The English name does not need to start with `/admin`, such as: auth/menu.',
+ 'Web side component path, please start with /src, such as: /src/views/backend/dashboard':
+ 'Please start with /src for web side component paths, such as: /src/views/backend/dashboard.vue',
+ 'The web side routing path (path) does not need to start with `/admin`, such as auth/menu':
+ 'The web side routing path (Path) does not need to start with `/admin`, such as: auth/menu.',
+ 'Use in controller `get_ route_ Remark()` function, which can obtain the value of this field for your own use, such as the banner file of the console':
+ 'Use the `get_route_remark()` function in the controller can get the value of this field for your own use, such as the banner file for the console.',
+ 'extend Title':
+ "For example, if 'auth/menu' is only added as a route, then `auth/menu`, `auth/menu/:a` and `auth/menu/:b/:c` can be added only as menus.",
+ none: 'None',
+}
diff --git a/web/src/lang/backend/en/crud/crud.ts b/web/src/lang/backend/en/crud/crud.ts
new file mode 100644
index 0000000..333ca94
--- /dev/null
+++ b/web/src/lang/backend/en/crud/crud.ts
@@ -0,0 +1,173 @@
+export default {
+ show: 'Show in Table Columns',
+ width: 'Width',
+ sortable: 'sortable',
+ operator: 'Search operator',
+ comSearchRender: 'Common search render',
+ comSearchInputAttr: 'Common search render input extend properties',
+ comSearchInputAttrTip: 'Remote pull-down field, no need to fill in the mandatory attributes of the component.',
+ render: 'table column render',
+ timeFormat: 'Format',
+ step: 'Step',
+ rows: 'Rows',
+ 'api url': 'api url',
+ 'api url example': 'For example: /admin/user.User/index',
+ 'remote-pk': 'value field',
+ 'remote-field': 'label field',
+ 'remote-url': 'remote URL',
+ 'remote-table': 'remote table',
+ 'remote-controller': 'remote controller',
+ 'remote-model': 'remote model',
+ 'remote-primary-table-alias': 'primary table alias',
+ 'relation-fields': 'relation fields',
+ 'image-multi': 'Multiple upload',
+ 'file-multi': 'Multiple upload',
+ 'select-multi': 'Multiple',
+ validator: 'validator',
+ validatorMsg: 'validator error message',
+ copy: 'Copy',
+ 'CRUD record': 'CRUD record',
+ 'Delete Code': 'Delete Code',
+ 'Start CRUD design with this record?': 'Start CRUD design with this record?',
+ 'Are you sure to delete the generated CRUD code?': 'Are you sure to delete the generated CRUD code?',
+ start: 'Start',
+ create: 'Create',
+ or: ' or ',
+ 'New background CRUD from zero': 'New background CRUD from zero',
+ 'Select Data Table': 'Select data table',
+ 'Select a designed data table from the database': 'Select a designed data table from the database',
+ 'Start with previously generated CRUD code': 'Start with previously generated CRUD code',
+ 'Fast experience': 'Fast experience',
+ 'Please enter SQL': 'Please enter SQL',
+ 'Please select a data table': 'Please select a data table',
+ 'data sheet help': 'The table prefix must be the same as the table prefix configured for the project',
+ 'data sheet': 'data sheet',
+ 'table create SQL': 'table creation SQL',
+ 'Please enter the table creation SQL': 'Please enter the table creation SQL',
+ 'experience 1 1': 'Prepare the ',
+ 'experience 1 2': 'development environment',
+ 'experience 1 3': '(The site port is 1818)',
+ 'experience 2 1': 'On this page, click to',
+ 'experience 2 2': 'Select data table',
+ 'experience 2 3': '(You can select the test_build data table)',
+ 'experience 3 1': 'Click',
+ 'experience 3 2': 'Generate CRUD Code',
+ 'experience 3 3': ', and click ',
+ 'experience 3 4': 'Continue to Generate',
+ 'experience 4 1': 'You are not currently in the development environment, ',
+ 'experience 4 2': 'please set up the development environment',
+ 'experience 4 3': ', or after generating the code, click on the upper right corner of the terminal to',
+ 'experience 4 4': 'Republish',
+ // design
+ 'Name of the data table': 'Name of the data table',
+ 'Data Table Notes': 'Data Table Notes',
+ 'Generate CRUD code': 'Generate CRUD code',
+ 'give up': 'give up',
+ 'Table Quick Search Fields': 'Table Quick Search Fields',
+ 'Table Default Sort Fields': 'Table Default Sort Fields',
+ 'sort order': 'sort order',
+ 'sort order asc': 'asc',
+ 'sort order desc': 'desc',
+ 'Fields as Table Columns': 'Fields as Table Columns',
+ 'Fields as form items': 'Fields as form items',
+ 'The relative path to the generated code': 'The relative path to the generated code',
+ 'For quick combination code generation location, please fill in the relative path':
+ 'For quick combination code generation location, please fill in the relative path',
+ 'Generated Controller Location': 'Generated Controller Location',
+ 'Generated Data Model Location': 'Generated Data Model Location',
+ 'Generated Validator Location': 'Generated Validator Location',
+ 'Common model': 'Common model',
+ 'WEB end view directory': 'WEB end view directory',
+ 'Check model class': "Check whether protected $connection = '{connection}'; is configured in the above data model class",
+ 'There is no connection attribute in model class': 'If no configuration is available, you can configure it manually',
+ 'Advanced Configuration': 'Advanced Configuration',
+ 'Common Fields': 'Common Fields',
+ 'Base Fields': 'Base Fields',
+ 'Advanced Fields': 'Advanced Fields',
+ 'Field Name': 'Field Name',
+ 'field comment': 'field comment',
+ 'Please select a field from the left first': 'Please select a field from the left first',
+ Common: 'Common',
+ 'Generate type': 'generate type',
+ 'Field comments (CRUD dictionary)': 'Field comments (CRUD dictionary)',
+ 'Field Properties': 'Field Properties',
+ 'Field Type': 'Field Type',
+ length: 'length',
+ 'decimal point': 'decimal point',
+ 'Field Defaults': 'Field Defaults',
+ 'Please input the default value': 'Please input the default value',
+ 'Auto increment': 'Auto increment',
+ Unsigned: 'Unsigned',
+ 'Allow NULL': 'Allow NULL',
+ 'Field Form Properties': 'Field Form Properties',
+ 'Field Table Properties': 'Field Table Properties',
+ 'Remote drop-down association information': 'Remote drop-down association information',
+ 'Associated Data Table': 'Associated Data Table',
+ 'Drop down value field': 'Drop down value field',
+ 'Drop down label field': 'Drop down label field',
+ 'Please select the value field of the select component': 'Please select the value field of the select component',
+ 'Please select the label field of the select component': 'Please select the label field of the select component',
+ 'Fields displayed in the table': 'Fields displayed in the table',
+ 'Please select the fields displayed in the table': 'Please select the fields displayed in the table',
+ 'Controller position': 'Controller position',
+ 'Please select the controller of the data table': 'Please select the controller of the data table',
+ 'Data Model Location': 'Data Model Location',
+ 'Data source configuration type': 'Data source configuration type',
+ 'Fast configuration with generated controllers and models': 'Fast configuration with generated controllers and models',
+ 'Custom configuration': 'Custom configuration',
+ 'If the remote interface query involves associated query of multiple tables, enter the alias of the primary data table here':
+ 'If the remote interface query involves associated query of multiple tables, enter the alias of the primary data table here',
+ 'Please select the data model location of the data table': 'Please select the data model location of the data table',
+ 'Confirm CRUD code generation': 'Confirm CRUD code generation',
+ 'Continue building': 'Continue building',
+ 'Please enter the data table name!': 'Please enter the data table name!',
+ 'Please enter the correct table name!': 'Please enter the correct table name!',
+ 'Use lower case underlined for table names': 'Use lower case underlined for table names',
+ 'Please design the primary key field!': 'Please design the primary key field!',
+ 'It is irreversible to give up the design Are you sure you want to give up?':
+ 'It is irreversible to give up the design. Are you sure you want to give up?',
+ 'There can only be one primary key field': 'There can only be one primary key field.',
+ 'Drag the left element here to start designing CRUD': 'Drag the left element here to start designing CRUD',
+ 'The data table already exists Continuing to generate will automatically delete the original table and create a new one!':
+ 'The data table already exists Continuing to generate will automatically delete the original table and create a new one!',
+ 'The controller already exists Continuing to generate will automatically overwrite the existing code!':
+ 'The controller already exists Continuing to generate will automatically overwrite the existing code!',
+ 'The menu rule with the same name already exists The menu and permission node will not be created in this generation':
+ 'The menu rule with the same name already exists The menu and permission node will not be created in this generation',
+ 'For example: `user table` will be generated into `user management`': 'For example: `user table` will be generated into `user management`',
+ 'The remote pull-down will request the corresponding controller to obtain data, so it is recommended that you create the CRUD of the associated table':
+ 'The remote pull-down will request the corresponding controller to obtain data, so it is recommended that you create the CRUD of the associated table',
+ 'If it is left blank, the model of the associated table will be generated automatically If the table already has a model, it is recommended to select it to avoid repeated generation':
+ 'If it is left blank, the model of the associated table will be generated automatically If the table already has a model, it is recommended to select it to avoid repeated generation',
+ 'The field comment will be used as the CRUD dictionary, and will be identified as the field title before the colon, and as the data dictionary after the colon':
+ 'The field comment will be used as the CRUD dictionary, and will be identified as the field title before the colon, and as the data dictionary after the colon',
+ 'Field name is invalid It starts with a letter or underscore and cannot contain any character other than letters, digits, or underscores':
+ 'Field name {field} is invalid. It starts with a letter or underscore and cannot contain any character other than letters, digits, or underscores',
+ 'The selected table has already generated records You are advised to start with historical records':
+ 'The selected table has already generated records. You are advised to start with historical records',
+ 'Start with the historical record': 'Start with the historical record',
+ 'Add field': 'Add field',
+ 'Modify field properties': 'Modify field properties',
+ 'Modify field name': 'Modify field name',
+ 'Delete field': 'Delete field',
+ 'Modify field order': 'Modify field order',
+ 'First field': 'First field',
+ After: 'after',
+ 'Table design change': 'Table design change',
+ 'Data table design changes preview': 'Data table design changes preview',
+ designChangeTips: 'When unchecked, the change will not be synchronized to the data table (the table structure has been manually modified, etc)',
+ tableReBuild: 'Delete and rebuild',
+ tableReBuildBlockHelp:
+ 'Deleting existing data tables and rebuilding them without adjusting the table structure ensures that CRUD code/records are consistent with the table structure',
+ Yes: 'Yes',
+ No: 'No',
+ 'If the data is abnormal, repeat the previous step': 'If the data is abnormal, repeat the previous step',
+ 'Field name duplication': 'field name {field} is duplicate',
+ 'Design remote select tips':
+ 'The name of the cost field is automatically generated from the table name; Confirm that when the field name user_id is generated, the association method generated by the field name user_id is named user, and the association method generated by the field name developer_done_id is named developerDone. Note that the name prefix of the remote drop-down field is not the same',
+ 'Vite hot warning':
+ 'Vite Hot Update service not found, please generate code in the development environment, or click the WEB terminal in the upper right corner to republish',
+ 'Reset generate type attr':
+ 'The field generation type has been changed. Do you want to reset the field design to the preset scheme for the new type?',
+ 'Design efficiency': 'Determine design validity by yourself',
+}
diff --git a/web/src/lang/backend/en/crud/log.ts b/web/src/lang/backend/en/crud/log.ts
new file mode 100644
index 0000000..d2470f3
--- /dev/null
+++ b/web/src/lang/backend/en/crud/log.ts
@@ -0,0 +1,53 @@
+export default {
+ id: 'id',
+ table_name: 'name',
+ comment: 'comment',
+ table: 'table',
+ fields: 'fields',
+ sync: 'sync',
+ 'sync no': 'no',
+ 'sync yes': 'yes',
+ status: 'status',
+ delete: 'delete code',
+ 'status delete': 'status delete',
+ 'status success': 'status success',
+ 'status error': 'status error',
+ 'status start': 'status start',
+ create_time: 'create_time',
+ 'quick Search Fields': 'id,table_name,comment',
+ 'Upload the selected design records to the cloud for cross-device use': 'Upload the selected design records to the cloud for cross-device use',
+ 'Design records that have been synchronized to the cloud': 'Design records that have been synchronized to the cloud',
+ 'Cloud record': 'Cloud record',
+ Settings: 'Settings',
+ 'Login for backup design': 'Login for backup design',
+ 'CRUD design record synchronization scheme': 'CRUD design record synchronization scheme',
+ Manual: 'Manual',
+ automatic: 'automatic',
+ 'When automatically synchronizing records, share them to the open source community':
+ 'When automatically synchronizing records, share them to the open source community',
+ 'Not to share': 'Not to share',
+ Share: 'Share',
+ 'Enabling sharing can automatically earn community points during development':
+ 'Enabling sharing can automatically earn community points during development',
+ 'The synchronized CRUD records are automatically resynchronized when they are updated':
+ 'The synchronized CRUD records are automatically resynchronized when they are updated',
+ 'Do not resynchronize': 'Do not resynchronize',
+ 'Automatic resynchronization': 'Automatic resynchronization',
+ 'No effective design': 'No effective design',
+ 'Number of fields': 'Number of fields',
+ 'Upload type': 'Upload type',
+ Update: 'Update',
+ 'New added': 'New added',
+ 'Share to earn points': 'Share to earn points',
+ 'Share to the open source community': 'Share to the open source community',
+ 'No design record': 'No design record',
+ Field: 'Field',
+ 'Field information': 'Field information',
+ 'No field': 'No field',
+ 'Field name': 'Field name',
+ Note: 'Note',
+ Type: 'Type',
+ Load: 'Load',
+ 'Delete cloud records?': 'Delete cloud records?',
+ 'You can use the synchronized design records across devices': 'You can use the synchronized design records across devices',
+}
diff --git a/web/src/lang/backend/en/crud/state.ts b/web/src/lang/backend/en/crud/state.ts
new file mode 100644
index 0000000..255f89b
--- /dev/null
+++ b/web/src/lang/backend/en/crud/state.ts
@@ -0,0 +1,21 @@
+export default {
+ remarks: 'remarks',
+ 'Primary key': 'Primary key',
+ 'Primary key (Snowflake ID)': 'Primary key (Snowflake ID)',
+ 'Disable Search': 'Disable Search',
+ 'Weight (drag and drop sorting)': 'Weight (drag and drop sorting)',
+ 'Status:0=Disabled,1=Enabled': 'Status:0=Disabled,1=Enabled',
+ 'Remote Select (association table)': 'Remote Select (association table)',
+ 'Remote Select (Multi)': 'Remote Select (Multi)',
+ 'Radio:opt0=Option1,opt1=Option2': 'Radio:opt0=Option1,opt1=Option2',
+ 'Checkbox:opt0=Option1,opt1=Option2': 'Checkbox:opt0=Option1,opt1=Option2',
+ Multi: '(Multi)',
+ 'Select:opt0=Option1,opt1=Option2': 'Select:opt0=Option1,opt1=Option2',
+ 'Switch:0=off,1=on': 'Switch:0=off,1=on',
+ 'Time date (timestamp storage)': 'Time date (timestamp storage)',
+ 'If left blank, the verifier title attribute will be filled in automatically':
+ 'If left blank, the verifier title attribute will be filled in automatically',
+ 'Weight (automatically generate drag sort button)': 'Weight (automatically generate drag sort button)',
+ 'If it is not input, it will be automatically analyzed by the controller':
+ 'If it is not input, it will be automatically analyzed by the controller',
+}
diff --git a/web/src/lang/backend/en/dashboard.ts b/web/src/lang/backend/en/dashboard.ts
new file mode 100644
index 0000000..3927977
--- /dev/null
+++ b/web/src/lang/backend/en/dashboard.ts
@@ -0,0 +1,39 @@
+export default {
+ 'You have worked today': 'You have worked today: ',
+ 'Continue to work': 'Keep working',
+ 'have a bit of rest': 'Take a break',
+ 'Member registration': 'Member registration',
+ 'Total number of members': 'Total number of members',
+ 'Number of installed plug-ins': 'Number of installed plug-ins',
+ 'Membership growth': 'Membership growth',
+ 'Annex growth': 'Annex Growth',
+ 'New member': 'New Member',
+ 'Joined us': 'Joined us',
+ 'Member source': 'Member source',
+ 'Member last name': 'Member last name',
+ Loading: 'Loading',
+ Monday: 'Monday',
+ Tuesday: 'Tuesday',
+ Wednesday: 'Wednesday',
+ Thursday: 'Thursday',
+ Friday: 'Friday',
+ Saturday: 'Saturday',
+ Sunday: 'Sunday',
+ Visits: 'Visits',
+ 'Registration volume': 'The number of registered users',
+ picture: 'picture',
+ file: 'file',
+ table: 'table',
+ other: 'other',
+ 'Compressed package': 'Compressed package',
+ Baidu: 'Baidu',
+ 'Direct access': 'Direct access',
+ 'take a plane': 'Take a plane',
+ 'Take the high-speed railway': 'Take the high-speed rail',
+ 'full name': 'Full name',
+ hour: 'Hour',
+ minute: 'Minute',
+ second: 'Second',
+ day: 'Day',
+ 'Number of attachments Uploaded': 'Number of attachments upload',
+}
diff --git a/web/src/lang/backend/en/login.ts b/web/src/lang/backend/en/login.ts
new file mode 100644
index 0000000..9d29c63
--- /dev/null
+++ b/web/src/lang/backend/en/login.ts
@@ -0,0 +1,6 @@
+export default {
+ 'Please enter an account': 'Please enter your account',
+ 'Please input a password': 'Please enter your password',
+ 'Hold session': 'Keep the session',
+ 'Sign in': 'Sign in',
+}
diff --git a/web/src/lang/backend/en/module.ts b/web/src/lang/backend/en/module.ts
new file mode 100644
index 0000000..00ac529
--- /dev/null
+++ b/web/src/lang/backend/en/module.ts
@@ -0,0 +1,163 @@
+export default {
+ 'stateTitle init': 'Module installer initialization...',
+ 'stateTitle download': 'Downloading module...',
+ 'stateTitle install': 'Installing module...',
+ 'stateTitle getInstallableVersion': 'Get installable version...',
+ 'env require': 'Composer',
+ 'env require-dev': 'Composer-dev',
+ 'env dependencies': 'NPM',
+ 'env devDependencies': 'NPM-dev',
+ 'env nuxtDependencies': 'Nuxt NPM',
+ 'env nuxtDevDependencies': 'Nuxt NPM Dev',
+ // buy
+ 'Module installation warning':
+ 'Free download and update within one year after purchase. Virtual products do not support 7-day refund without reason',
+ 'Order title': 'Order title',
+ 'Order No': 'Order No.:',
+ 'Purchase user': 'Purchase user',
+ 'Order price': 'Order price',
+ 'Purchased, can be installed directly': 'Purchased, can be installed directly',
+ 'Understand and agree': 'Understand and agree',
+ 'Module purchase and use agreement': 'Module purchase and use agreement',
+ 'Point payment': 'Point payment',
+ 'Balance payment': 'Balance payment',
+ 'Wechat payment': 'Wechat payment',
+ 'Alipay payment': 'Alipay payment',
+ 'Install now': 'Install now',
+ payment: 'payment',
+ 'Confirm order info': 'Confirm order info',
+ // commonDone
+ 'Congratulations, module installation is complete': 'Congratulations, module installation is complete.',
+ 'Module is disabled': 'Module is disabled.',
+ 'Congratulations, the code of the module is ready': 'Congratulations, the code of the module is ready.',
+ 'Unknown state': 'Unknown state.',
+ 'Do not refresh the page!': 'Do not refresh the page!',
+ 'New adjustment of dependency detected': 'New adjustment of dependency detected',
+ 'This module adds new dependencies': 'This module adds new dependencies',
+ 'The built-in terminal of the system is automatically installing these dependencies, please wait~':
+ 'The built-in terminal of the system is automatically installing these dependencies, please wait~',
+ 'View progress': 'View progress',
+ 'Dependency installation completed~': 'Dependency installation completed~',
+ 'This module does not add new dependencies': 'This module does not add new dependencies.',
+ 'There is no adjustment for system dependency': 'There is no adjustment for system dependency.',
+ please: 'please',
+ 'After installation 1': 'After installation',
+ 'Manually clean up the system and browser cache': 'Manually clean up the system and browser cache.',
+ 'After installation 2': 'After installation',
+ 'Automatically execute reissue command?': 'Automatically execute reissue command?',
+ 'End of installation': 'End of installation',
+ 'Dependency installation fail 1': 'The dependency installation failed. Please click the retry button in the ',
+ 'Dependency installation fail 2': 'terminal',
+ 'Dependency installation fail 3': 'You can also view the ',
+ 'Dependency installation fail 4': 'unfinished matters manually',
+ 'Dependency installation fail 5': 'Until you are',
+ 'Dependency installation fail 6': 'sure that the dependency is ready',
+ 'Dependency installation fail 7': ', the module will not work!',
+ 'Is the command that failed on the WEB terminal executed manually or in other ways successfully?':
+ 'Is the command that failed on the WEB terminal executed manually or in other ways successfully?',
+ yes: 'yes',
+ no: 'no',
+ // confirmFileConflict
+ 'Update warning':
+ 'The following module files have been detected to be updated. When disabled, they will be automatically overwritten. Please pay attention to backup.',
+ 'File conflict': 'File conflict',
+ 'Conflict file': 'Conflict file',
+ 'Dependency conflict': 'Dependency conflict',
+ 'Confirm to disable the module': 'Confirm to disable the module',
+ 'The module declares the added dependencies': 'The module declares the added dependencies',
+ Dependencies: 'Dependencies',
+ retain: 'Retain',
+ // goodsInfo
+ 'detailed information': 'detailed information',
+ Price: 'Price',
+ 'Last updated': 'Last updated',
+ 'Published on': 'Published on:',
+ 'amount of downloads': 'amount of downloads',
+ 'Module classification': 'Module classification',
+ 'Module documentation': 'Module documentation',
+ 'Developer Homepage': 'Developer Homepage',
+ 'Click to access': 'Click to access',
+ 'Module status': 'Module status',
+ 'View demo': 'View demo',
+ 'Code scanning Preview': 'Code scanning Preview',
+ 'Buy now': 'Buy now',
+ 'continue installation': 'continue installation',
+ installed: 'installed',
+ 'to update': 'to update',
+ uninstall: 'uninstall',
+ 'Contact developer': 'Contact developer',
+ 'Other works of developers': 'Other works of developers',
+ 'There are no more works': 'There are no more works',
+ 'You need to disable this module before updating Do you want to disable it now?':
+ 'You need to disable this module before updating. Do you want to disable it now?',
+ 'Disable and update': 'Disable and update',
+ 'No module purchase order was found': 'No module purchase order was found. Do you want to purchase the current module now?',
+ // installConflict
+ 'new file': 'new file',
+ 'Existing files': 'Existing files',
+ 'Treatment scheme': 'Treatment scheme',
+ 'Backup and overwrite existing files': 'Backup and overwrite existing files',
+ 'Discard new file': 'Discard new file',
+ environment: 'environment',
+ 'New dependency': 'New dependency',
+ 'Existing dependencies': 'Existing dependencies',
+ 'Overwrite existing dependencies': 'Overwrite existing dependencies',
+ 'Do not use new dependencies': 'Do not use new dependencies',
+ // tableHeader
+ 'Upload zip package for installation': 'Upload zip package for installation',
+ 'Upload installation': 'Upload installation',
+ 'Uploaded / installed modules': 'Uploaded / installed modules',
+ 'Local module': 'Local module',
+ 'Publishing module': 'Publishing module',
+ 'Get points': 'Get points',
+ 'Search is actually very simple': 'Search is actually very simple',
+ // tabs
+ Loading: 'Loading...',
+ 'No more': 'No more.',
+ // uploadInstall
+ 'Local upload warning':
+ 'Please make sure that the module package file comes from the official channel or the officially certified module author, otherwise the system may be damaged because:',
+ 'The module can modify and add system files': 'The module can modify and add system files',
+ 'The module can execute sql commands and codes': 'The module can execute sql commands and codes',
+ 'The module can install new front and rear dependencies': 'The module can install new front and rear dependencies',
+ 'Drag the module package file here': 'Drag the module package file here, Or',
+ 'Click me to upload': 'Click me to upload',
+ 'Uploaded, installation is about to start, please wait': 'Uploaded, installation is about to start, please wait',
+ 'Update Log': 'Update Log',
+ 'No detailed update log': 'No detailed update log',
+ 'Use WeChat to scan QR code for payment': 'Use WeChat to scan QR code for payment',
+ 'Use Alipay to scan QR code for payment': 'Use Alipay to scan QR code for payment',
+ 'dependency-installation-fail-tips':
+ 'If the command is successfully executed manually, click `Make sure dependency is ready` above to change the module to the installed state',
+ 'New version': 'New version',
+ Install: 'Install',
+ 'Installation cancelled because module already exists!': 'Installation cancelled because module already exists!',
+ 'Installation cancelled because the directory required by the module is occupied!':
+ 'Installation cancelled because the directory required by the module is occupied!',
+ 'Installation complete': 'Installation complete',
+ 'A conflict is found Please handle it manually': 'A conflict is found. Please handle it manually',
+ 'Select Version': 'Select install version',
+ 'Wait for dependent installation': 'Wait for dependent installation',
+ 'The operation succeeds Please clear the system cache and refresh the browser ~':
+ 'The operation succeeds. Please clear the system cache and refresh the browser ~',
+ 'Deal with conflict': 'Deal with conflict',
+ 'Wait for installation': 'Wait for installation',
+ 'Conflict pending': 'Conflict pending',
+ 'Dependency to be installed': 'Dependency to be installed',
+ 'Restart Vite hot server': 'Restart Vite hot server',
+ 'Restart Vite hot server tips':
+ 'Before successfully restarting the service, you can find the button to manually restart the service from the button group on the right side of the top bar.',
+ 'Manual restart': 'Manual restart',
+ 'Restart Now': 'Restart Now',
+ // 选择安装版本
+ 'Available system version': 'Available system version',
+ Description: 'Description',
+ Version: 'Version',
+ 'Current installed version': 'Current installed version',
+ 'Insufficient system version': 'Insufficient system version',
+ 'Click to install': 'Click to install',
+ 'Versions released beyond the authorization period': 'Versions released beyond the authorization period',
+ Renewal: 'Renewal',
+ 'Order expiration time':
+ 'The expiration time of the current order authorization is {expiration_time}, and the release time of this version is {create_time}',
+}
diff --git a/web/src/lang/backend/en/routine/adminInfo.ts b/web/src/lang/backend/en/routine/adminInfo.ts
new file mode 100644
index 0000000..0ae2dd1
--- /dev/null
+++ b/web/src/lang/backend/en/routine/adminInfo.ts
@@ -0,0 +1,14 @@
+export default {
+ 'Last logged in on': 'Last logged on',
+ 'user name': 'Username',
+ 'User nickname': 'User nickname',
+ 'Please enter a nickname': 'Please enter a nickname',
+ 'e-mail address': 'E-mail address',
+ 'phone number': 'Mobile number',
+ autograph: 'Signature',
+ 'This guy is lazy and doesn write anything': "This guy is lazy and didn't write anything.",
+ 'New password': 'New password',
+ 'Please leave blank if not modified': 'Please leave blank if you do not modify',
+ 'Save changes': 'Save changes',
+ 'Operation log': 'Operation log',
+}
diff --git a/web/src/lang/backend/en/routine/attachment.ts b/web/src/lang/backend/en/routine/attachment.ts
new file mode 100644
index 0000000..23606ff
--- /dev/null
+++ b/web/src/lang/backend/en/routine/attachment.ts
@@ -0,0 +1,25 @@
+export default {
+ 'Upload administrator': 'Upload administrator',
+ 'Upload user': 'Upload member',
+ 'Storage mode': 'Storage mode',
+ 'Physical path': 'Physical path',
+ 'image width': 'Picture width',
+ 'Picture height': 'Picture height',
+ 'file size': 'file size',
+ 'mime type': 'mime type ',
+ 'SHA1 code': 'SHA1',
+ 'The file is saved in the directory, and the file will not be automatically transferred if the record is modified':
+ 'The file had saved in the directory, and the modification record will not automatically tansfer the file.',
+ 'File saving path Modifying records will not automatically transfer files':
+ 'The file had saved in the path, and the modification record will not automatically tansfer the file.',
+ 'Width of picture file': 'The width of the image file.',
+ 'Height of picture file': 'The height of the image file.',
+ 'Original file name': 'Original name of the file',
+ 'File size (bytes)': 'File size (Bytes)',
+ 'File MIME type': 'File MIME type',
+ 'Upload (Reference) times of this file': 'Upload (Reference) times of this file',
+ 'When the same file is uploaded multiple times, only one attachment record will be saved and added':
+ 'When the same file is uploaded many times, only one attachment record will be saved and added.',
+ 'SHA1 encoding of file': 'The SHA1 encoding of file',
+ 'Files and records will be deleted at the same time Are you sure?': 'Files and records will be deleted at the same time Are you sure?',
+}
diff --git a/web/src/lang/backend/en/routine/config.ts b/web/src/lang/backend/en/routine/config.ts
new file mode 100644
index 0000000..1db001c
--- /dev/null
+++ b/web/src/lang/backend/en/routine/config.ts
@@ -0,0 +1,16 @@
+export default {
+ 'Are you sure to delete the configuration item?': 'Are you sure to delete the configuration item?',
+ 'Add configuration item': 'Add configuration item',
+ 'Quick configuration entry': 'Quick configuration entry',
+ 'Variable name': 'Variable name',
+ 'Variable group': 'Variable group',
+ 'Variable title': 'Variable title',
+ 'Variable type': 'Variable type',
+ number: 'Number',
+ 'Please enter the recipient email address': 'Please enter the recipient email address',
+ 'Test mail sending': 'Test mail sending',
+ 'send out': 'send',
+ 'Please enter the correct email address': 'Please enter the correct email address',
+ Sending: 'Sending',
+ 'Please enter the correct mail configuration': 'Please enter the correct mail configuration',
+}
diff --git a/web/src/lang/backend/en/security/dataRecycle.ts b/web/src/lang/backend/en/security/dataRecycle.ts
new file mode 100644
index 0000000..febee4e
--- /dev/null
+++ b/web/src/lang/backend/en/security/dataRecycle.ts
@@ -0,0 +1,11 @@
+export default {
+ 'Rule name': 'Rule name',
+ controller: 'Controller',
+ 'data sheet': 'Data table',
+ 'Data table primary key': 'Data table primary key',
+ 'Deleting monitoring': 'Delete monitoring',
+ 'The rule name helps to identify deleted data later': 'Rule names help to identify deleted data subsequently later.',
+ 'The data collection mechanism will monitor delete operations under this controller':
+ 'The data recycle mechanism will monitor the delete operations under this controller.',
+ 'Corresponding data sheet': 'Corresponding data sheet',
+}
diff --git a/web/src/lang/backend/en/security/dataRecycleLog.ts b/web/src/lang/backend/en/security/dataRecycleLog.ts
new file mode 100644
index 0000000..0de787a
--- /dev/null
+++ b/web/src/lang/backend/en/security/dataRecycleLog.ts
@@ -0,0 +1,17 @@
+export default {
+ restore: 'Restore',
+ 'Are you sure to restore the selected records?': 'Are you sure to restore the selected records?',
+ 'Restore the selected record to the original data table': 'Restore the selected record to the original data table.',
+ 'Operation administrator': 'Operation administrator',
+ 'Recycling rule name': 'Recycling rule name',
+ 'Rule name': 'Rule name',
+ controller: 'Controller',
+ 'data sheet': 'Data table',
+ DeletedData: 'Deleted data',
+ 'Arbitrary fragment fuzzy query': 'Arbitrary fragment fuzzy query',
+ 'Click to expand': 'Click to expand',
+ 'Data table primary key': 'Data table primary key',
+ 'Operator IP': 'Operator IP',
+ 'Deleted data': 'Deleted data',
+ 'Delete time': 'Delete time',
+}
diff --git a/web/src/lang/backend/en/security/sensitiveData.ts b/web/src/lang/backend/en/security/sensitiveData.ts
new file mode 100644
index 0000000..e2c6081
--- /dev/null
+++ b/web/src/lang/backend/en/security/sensitiveData.ts
@@ -0,0 +1,13 @@
+export default {
+ 'Rule name': 'Rule name',
+ controller: 'Controller',
+ 'data sheet': 'Data table',
+ 'Data table primary key': 'Data table primary key',
+ 'Sensitive fields': 'Sensitive fields',
+ 'Modifying monitoring': 'Modify monitoring',
+ 'The rule name helps to identify the modified data later': 'Rule names help to identify modified data subsequently later.',
+ 'The data listening mechanism will monitor the modification operations under this controller':
+ 'The data monitor mechanism will monitor the modified operation under this controller.',
+ 'Corresponding data sheet': 'Corresponding data table',
+ 'Filling in field notes helps you quickly identify fields later': 'Fill in field comments help to identify fields quickly later.',
+}
diff --git a/web/src/lang/backend/en/security/sensitiveDataLog.ts b/web/src/lang/backend/en/security/sensitiveDataLog.ts
new file mode 100644
index 0000000..f5c7167
--- /dev/null
+++ b/web/src/lang/backend/en/security/sensitiveDataLog.ts
@@ -0,0 +1,18 @@
+export default {
+ 'Operation administrator': 'Operation administrator',
+ 'Rule name': 'Rule name',
+ controller: 'Controller',
+ 'data sheet': 'Data table',
+ 'Modify line': 'Modify row',
+ Modification: 'Modify item',
+ 'Before modification': 'Before modification',
+ 'After modification': 'After modification',
+ 'Modification time': 'Modify time',
+ 'Are you sure you want to rollback the record?': 'Are you sure to rollback the record?',
+ 'Rollback the selected record to the original data table': 'Rollback the selected record to the original data table.',
+ 'Operator IP': 'Operator IP',
+ 'Data table primary key': 'Data table primary key',
+ 'Modified item': 'Modified item',
+ 'Modification comparison': 'Modify the comparison',
+ RollBACK: 'Rollback',
+}
diff --git a/web/src/lang/backend/en/user/group.ts b/web/src/lang/backend/en/user/group.ts
new file mode 100644
index 0000000..8f6b6cc
--- /dev/null
+++ b/web/src/lang/backend/en/user/group.ts
@@ -0,0 +1,5 @@
+export default {
+ GroupName: 'Group name',
+ 'Group name': 'Group name',
+ jurisdiction: 'Permissions',
+}
diff --git a/web/src/lang/backend/en/user/moneyLog.ts b/web/src/lang/backend/en/user/moneyLog.ts
new file mode 100644
index 0000000..0714c79
--- /dev/null
+++ b/web/src/lang/backend/en/user/moneyLog.ts
@@ -0,0 +1,16 @@
+export default {
+ 'User name': 'Username',
+ 'User nickname': 'User nickname',
+ balance: 'Balance',
+ 'User ID': 'User ID',
+ 'Change balance': 'Change balance',
+ 'Before change': 'Before the change',
+ 'After change': 'After the change',
+ remarks: 'Remark',
+ 'Current balance': 'Current balance',
+ 'Change amount': 'Change amount',
+ 'Please enter the balance change amount': 'Please enter the balance change amount.',
+ 'Balance after change': 'Balance after change',
+ 'Please enter change remarks / description': 'Please enter change remarks/description',
+ User: 'User',
+}
diff --git a/web/src/lang/backend/en/user/rule.ts b/web/src/lang/backend/en/user/rule.ts
new file mode 100644
index 0000000..9d2b92f
--- /dev/null
+++ b/web/src/lang/backend/en/user/rule.ts
@@ -0,0 +1,26 @@
+export default {
+ 'Normal routing': 'Normal routing',
+ 'Member center menu contents': 'Member center menu directory ',
+ 'Member center menu items': 'Member Center menu items',
+ 'Top bar menu items': 'Top bar menu items',
+ 'Page button': 'Page button',
+ 'Top bar user dropdown': 'Top bar user dropdown',
+ 'Type route tips': 'Automatically register as a front-end route',
+ 'Type menu_dir tips': 'Automatically register routes and serve as menu directory of member center This item cannot jump',
+ 'Type menu tips': 'Automatically register routes and serve as menu items in member centers',
+ 'Type nav tips': 'Routes are automatically registered as menu items in the top bar of the site',
+ 'Type button tips': 'Automatic registration as a permission node, can be quickly verified by v-auth',
+ 'Type nav_user_menu tips': 'Automatically register routes and serve as a dropdown menu for top bar members',
+ 'English name': 'English name',
+ 'Web side routing path': 'Web side routing path',
+ no_login_valid: 'no login valid',
+ 'no_login_valid 0': 'no',
+ 'no_login_valid 1': 'yes',
+ 'no_login_valid tips': 'Tourists do not have membership groups Use this option to set whether the current rules are valid for tourists (visible)',
+ 'For example, if you add account/overview as a route only':
+ 'Please start with /src for web side component paths, such as: /src/views/frontend/index.vue',
+ 'Web side component path, please start with /src, such as: /src/views/frontend/index':
+ "For example, if you add 'account/overview' as a route only, then you can additionally add 'account/overview', 'account/overview/:a' and 'account/overview/:b/:C' as menus only.",
+ 'Component path tips':
+ 'This item is mandatory within a WEB project; otherwise, it cannot be accessed. However, when it is used as a menu within a Nuxt project, there is no need to fill in this item',
+}
diff --git a/web/src/lang/backend/en/user/scoreLog.ts b/web/src/lang/backend/en/user/scoreLog.ts
new file mode 100644
index 0000000..54a5ab6
--- /dev/null
+++ b/web/src/lang/backend/en/user/scoreLog.ts
@@ -0,0 +1,8 @@
+export default {
+ integral: 'Integral',
+ 'Change points': 'Change points',
+ 'Current points': 'Current points',
+ 'Please enter the change amount of points': 'Please enter the change amount of points',
+ 'Points after change': 'Points after change',
+ 'Please enter change remarks / description': 'Please enter change remarks/description',
+}
diff --git a/web/src/lang/backend/en/user/user.ts b/web/src/lang/backend/en/user/user.ts
new file mode 100644
index 0000000..4a51286
--- /dev/null
+++ b/web/src/lang/backend/en/user/user.ts
@@ -0,0 +1,22 @@
+export default {
+ 'User name': 'Username',
+ nickname: 'Nickname',
+ group: 'Group',
+ avatar: 'Avatar',
+ Gender: 'Gender',
+ male: 'Male',
+ female: 'Female',
+ mobile: 'Mobile Number',
+ 'Last login IP': 'Last login IP',
+ 'Last login': 'Last login',
+ email: 'Email',
+ birthday: 'Birthday',
+ balance: 'Balance',
+ 'Adjustment balance': 'Adjust balance',
+ integral: 'Integral',
+ 'Adjust integral': 'Adjust integral',
+ password: 'Password',
+ 'Please leave blank if not modified': 'Please leave blank if you do not modify',
+ 'Personal signature': 'Personal signature',
+ 'Login account': 'Login account name',
+}
diff --git a/web/src/lang/backend/zh-cn.ts b/web/src/lang/backend/zh-cn.ts
new file mode 100644
index 0000000..031c5f3
--- /dev/null
+++ b/web/src/lang/backend/zh-cn.ts
@@ -0,0 +1,101 @@
+/**
+ * 后台公共语言包
+ * 覆盖风险:请避免使用页面语言包的目录名、文件名作为翻译 key
+ */
+export default {
+ Balance: '余额',
+ Integral: '积分',
+ Connection: '连接标识',
+ 'Database connection': '数据库连接配置标识',
+ 'Database connection help': '您可以在 config/database.php 内配置多个数据库连接,然后在此处选择它,留空将使用默认连接配置',
+ layouts: {
+ 'Layout configuration': '布局配置',
+ 'Layout mode': '布局方式',
+ default: '默认',
+ classic: '经典',
+ 'Single column': '单栏',
+ 'Double column': '双栏',
+ 'overall situation': '全局',
+ 'Background page switching animation': '后台页面切换动画',
+ 'Please select an animation name': '请选择动画名称',
+ sidebar: '侧边栏',
+ 'Side menu bar background color': '侧边菜单栏背景色',
+ 'Side menu text color': '侧边菜单文字颜色',
+ 'Side menu active item background color': '侧边菜单激活项背景色',
+ 'Side menu active item text color': '侧边菜单激活项文字色',
+ 'Show side menu top bar (logo bar)': '显示侧边菜单顶栏(LOGO栏)',
+ 'Side menu top bar background color': '侧边菜单顶栏背景色',
+ 'Side menu width (when expanded)': '侧边菜单宽度(展开时)',
+ 'Side menu default icon': '侧边菜单默认图标',
+ 'Side menu horizontal collapse': '侧边菜单水平折叠',
+ 'Side menu accordion': '侧边菜单手风琴',
+ 'Top bar': '顶栏',
+ 'Top bar background color': '顶栏背景色',
+ 'Top bar text color': '顶栏文字色',
+ 'Background color when hovering over the top bar': '顶栏悬停时背景色',
+ 'Top bar menu active item background color': '顶栏菜单激活项背景色',
+ 'Top bar menu active item text color': '顶栏菜单激活项文字色',
+ 'Are you sure you want to restore all configurations to the default values?': '确定要恢复全部配置到默认值吗?',
+ 'Restore default': '恢复默认',
+ Profile: '个人资料',
+ Logout: '注销',
+ 'Dark mode': '暗黑模式',
+ 'Exit full screen': '退出全屏',
+ 'Full screen is not supported': '您的浏览器不支持全屏,请更换浏览器再试~',
+ 'Member center': '会员中心',
+ 'Member information': '会员信息',
+ 'Login to the buildadmin': '登录到 BuildAdmin 开源社区',
+ 'Please enter buildadmin account name or email': '请输入 BuildAdmin 账户名/邮箱/手机号',
+ 'Please enter the buildadmin account password': '请输入 BuildAdmin 账户密码',
+ Login: '登录',
+ Password: '密码',
+ Username: '用户名',
+ Register: '没有账户?去注册',
+ },
+ terminal: {
+ Source: '源',
+ Terminal: '终端',
+ 'Command run log': '命令运行日志',
+ 'No mission yet': '还没有任务...',
+ 'Test command': '测试命令',
+ 'Install dependent packages': '安装依赖包',
+ Republish: '重新发布',
+ 'Clean up task list': '清理任务列表',
+ unknown: '未知',
+ 'Waiting for execution': '等待执行',
+ Connecting: '连接中...',
+ Executing: '执行中...',
+ 'Successful execution': '执行成功',
+ 'Execution failed': '执行失败',
+ 'Unknown execution result': '执行结果未知',
+ 'Are you sure you want to republish?': '确认要重新发布吗?',
+ 'Failure to execute this command will block the execution of the queue': '本命令执行失败会阻断队列执行',
+ 'NPM package manager': 'NPM 包管理器',
+ 'NPM package manager tip': '选择一个可用的包管理器,用于 WEB 终端中 npm install 等命令的执行',
+ 'Clear successful task': '清理成功任务',
+ 'Clear successful task tip': '开始一个新任务时,自动清理列表中已经成功的任务',
+ 'Manual execution': '手动执行',
+ 'Do not refresh the browser': '请勿刷新浏览器',
+ 'Terminal settings': '终端设置',
+ 'Back to terminal': '回到终端',
+ or: '或',
+ 'Site domain name': '站点域名',
+ 'The current terminal is not running under the installation service, and some commands may not be executed':
+ '当前终端未运行于安装服务下,部分命令可能无法执行。',
+ 'Newly added tasks will never start because they are blocked by failed tasks': '新添加的任务永远不会开始,因为被失败的任务阻塞!(WEB终端)',
+ 'Failed to modify the source command, Please try again manually': '修改源的命令执行失败,请手动重试。',
+ },
+ vite: {
+ Later: '稍后',
+ 'Restart hot update': '重启热更新',
+ 'Close type terminal': 'WEB终端执行命令',
+ 'Close type crud': 'CRUD代码生成服务',
+ 'Close type modules': '模块安装服务',
+ 'Close type config': '修改系统配置',
+ 'Reload hot server title': '需要重启 Vite 热更新服务',
+ 'Reload hot server tips 1': '为确保',
+ 'Reload hot server tips 2':
+ '不被打断,系统暂停了 Vite 的热更新功能,期间前端文件变动将不会实时更新和自动重载网页,现检测到服务暂停期间存在文件更新,需要重启热更新服务。',
+ 'Reload hot server tips 3': '热更新暂停不影响已经加载好的功能,您可以继续操作,并在一切就绪后再点击重新启动热更新服务。',
+ },
+}
diff --git a/web/src/lang/backend/zh-cn/auth/admin.ts b/web/src/lang/backend/zh-cn/auth/admin.ts
new file mode 100644
index 0000000..e5c671f
--- /dev/null
+++ b/web/src/lang/backend/zh-cn/auth/admin.ts
@@ -0,0 +1,13 @@
+export default {
+ username: '用户名',
+ nickname: '昵称',
+ group: '角色组',
+ avatar: '头像',
+ email: '电子邮箱',
+ mobile: '手机号',
+ 'Last login': '最后登录',
+ Password: '密码',
+ 'Please leave blank if not modified': '不修改请留空',
+ 'Personal signature': '个性签名',
+ 'Administrator login': '管理员登录名',
+}
diff --git a/web/src/lang/backend/zh-cn/auth/adminLog.ts b/web/src/lang/backend/zh-cn/auth/adminLog.ts
new file mode 100644
index 0000000..ec22a0a
--- /dev/null
+++ b/web/src/lang/backend/zh-cn/auth/adminLog.ts
@@ -0,0 +1,12 @@
+export default {
+ admin_id: '管理ID',
+ username: '管理用户名',
+ title: '标题',
+ data: '请求数据',
+ url: 'URL',
+ ip: 'IP',
+ useragent: 'UserAgent',
+ 'Operation administrator': '操作管理员',
+ 'Operator IP': '操作人IP',
+ 'Request data': '请求数据',
+}
diff --git a/web/src/lang/backend/zh-cn/auth/group.ts b/web/src/lang/backend/zh-cn/auth/group.ts
new file mode 100644
index 0000000..18f6a82
--- /dev/null
+++ b/web/src/lang/backend/zh-cn/auth/group.ts
@@ -0,0 +1,8 @@
+export default {
+ GroupName: '组名',
+ 'Group name': '组别名称',
+ jurisdiction: '权限',
+ 'Parent group': '上级分组',
+ 'The parent group cannot be the group itself': '上级分组不能是分组本身',
+ 'Manage subordinate role groups here': '在此管理下级角色组(您拥有下级角色组的所有权限并且拥有额外的权限,不含同级)',
+}
diff --git a/web/src/lang/backend/zh-cn/auth/rule.ts b/web/src/lang/backend/zh-cn/auth/rule.ts
new file mode 100644
index 0000000..dc2fb88
--- /dev/null
+++ b/web/src/lang/backend/zh-cn/auth/rule.ts
@@ -0,0 +1,49 @@
+export default {
+ title: '标题',
+ Icon: '图标',
+ name: '名称',
+ type: '类型',
+ cache: '缓存',
+ 'Superior menu rule': '上级菜单规则',
+ 'Rule type': '规则类型',
+ 'type menu_dir': '菜单目录',
+ 'type menu': '菜单项',
+ 'type button': '页面按钮',
+ 'Rule title': '规则标题',
+ 'Rule name': '规则名称',
+ 'Routing path': '路由路径',
+ 'Rule Icon': '规则图标',
+ 'Menu type': '菜单类型',
+ 'Menu type tab': '选项卡',
+ 'Menu type link (offsite)': '链接(站外)',
+ 'Link address': '链接地址',
+ 'Component path': '组件路径',
+ 'Extended properties': '扩展属性',
+ 'Add as route only': '只添加为路由',
+ 'Add as menu only': '只添加为菜单',
+ 'Rule comments': '规则备注',
+ 'Rule weight': '规则权重',
+ 'Create Page Button': '创建页面按钮',
+ 'Create Page Button index': '查看',
+ 'Create Page Button add': '添加',
+ 'Create Page Button edit': '编辑',
+ 'Create Page Button del': '删除',
+ 'Create Page Button sortable': '快速排序',
+ 'Create Page Button tips': '创建菜单的同时,自动创建菜单的页面按钮(权限节点),若需自定义按钮请后续手动添加',
+ 'Please select the button for automatically creating the desired page': '请选择需要自动创建的页面按钮',
+ 'Please enter the weight of menu rule (sort by)': '请输入菜单规则权重(排序依据)',
+ 'Please enter the correct URL': '请输入正确的 URL',
+ 'The superior menu rule cannot be the rule itself': '上级菜单规则不能是规则本身',
+ 'It will be registered as the web side routing name and used as the server side API authentication':
+ '将注册为 WEB 端路由名称,同时作为服务端方法名验权(有此节点权限才能请求对应控制器或方法)',
+ 'Please enter the URL address of the link or iframe': '请输入链接或 Iframe 的 URL 地址',
+ 'English name, which does not need to start with `/admin`, such as auth/menu': '英文名称,无需以 `/admin` 开头,如:auth/menu',
+ 'Web side component path, please start with /src, such as: /src/views/backend/dashboard':
+ 'WEB 端组件路径,请以 /src 开头,如:/src/views/backend/dashboard.vue',
+ 'The web side routing path (path) does not need to start with `/admin`, such as auth/menu':
+ 'vue-router 的 path,无需以 `/admin` 开头,如:auth/menu',
+ 'Use in controller `get_ route_ Remark()` function, which can obtain the value of this field for your own use, such as the banner file of the console':
+ '在控制器中使用 `get_route_remark()` 函数,可以获得此字段值自用,比如控制台的 Banner 文案',
+ 'extend Title': '比如将 `auth/menu` 只添加为路由,那么可以另外将 `auth/menu`、`auth/menu/:a`、`auth/menu/:b/:c` 只添加为菜单',
+ none: '无',
+}
diff --git a/web/src/lang/backend/zh-cn/crud/crud.ts b/web/src/lang/backend/zh-cn/crud/crud.ts
new file mode 100644
index 0000000..104dbed
--- /dev/null
+++ b/web/src/lang/backend/zh-cn/crud/crud.ts
@@ -0,0 +1,168 @@
+export default {
+ show: '在表格列中显示',
+ width: '表格列宽度',
+ sortable: '字段排序',
+ operator: '公共搜索操作符',
+ comSearchRender: '公共搜索输入框渲染方案',
+ comSearchInputAttr: '公共搜索输入框扩展属性',
+ comSearchInputAttrTip: '格式如:size=large,一行一个属性,远程下拉的公共搜索也渲染为远程下拉时,此处免填远程下拉组件的必填属性',
+ render: '渲染方案',
+ timeFormat: '格式化方式',
+ step: '步进值',
+ rows: '行数',
+ 'api url': '数据接口URL',
+ 'api url example': '比如: /admin/user.User/index',
+ 'remote-pk': '远程下拉 value 字段',
+ 'remote-field': '远程下拉 label 字段',
+ 'remote-url': '远程下拉数据接口 URL',
+ 'remote-controller': '关联表的控制器',
+ 'remote-table': '关联数据表',
+ 'remote-model': '关联表的模型',
+ 'remote-primary-table-alias': '主表别名',
+ 'relation-fields': '关联表显示字段',
+ 'image-multi': '图片多选上传',
+ 'file-multi': '文件多选上传',
+ 'select-multi': '下拉框多选',
+ validator: '验证规则',
+ validatorMsg: '验证错误提示',
+ copy: '复制设计',
+ 'CRUD record': 'CRUD 记录',
+ 'Delete Code': '删除代码',
+ 'Start CRUD design with this record?': '以此记录开始 CRUD 设计?',
+ 'Are you sure to delete the generated CRUD code?': '确认删除生成的 CRUD 代码?',
+ start: '开始',
+ create: '新建',
+ or: '或',
+ 'New background CRUD from zero': '从零新建后台 CRUD',
+ 'Select Data Table': '选择数据表',
+ 'Select a designed data table from the database': '从数据库中选择一个设计好的数据表',
+ 'Start with previously generated CRUD code': '从以往生成的 CRUD 代码开始',
+ 'Fast experience': '快速体验',
+ 'Please enter SQL': '请输入 SQL',
+ 'Please select a data table': '请选择数据表',
+ 'data sheet help': '数据表前缀需要同项目配置的数据表前缀一致',
+ 'data sheet': '数据表',
+ 'table create SQL': '建表 SQL',
+ 'Please enter the table creation SQL': '请输入建表 SQL',
+ 'experience 1 1': '准备好',
+ 'experience 1 2': '开发环境',
+ 'experience 1 3': '(站点端口为:1818)',
+ 'experience 2 1': '在本页点击',
+ 'experience 2 2': '选择数据表',
+ 'experience 2 3': '(可选择 test_build 数据表)',
+ 'experience 3 1': '点击',
+ 'experience 3 2': '生成 CRUD 代码',
+ 'experience 3 3': ',点击',
+ 'experience 3 4': '继续生成',
+ 'experience 4 1': '您当前未在开发环境,请 ',
+ 'experience 4 2': '搭建开发环境',
+ 'experience 4 3': ',或生成好代码之后点击右上角终端内的',
+ 'experience 4 4': '重新发布',
+ // design
+ 'Name of the data table': '数据表的名称',
+ 'Data Table Notes': '数据表注释',
+ 'Generate CRUD code': '生成 CRUD 代码',
+ 'give up': '放弃',
+ 'Table Quick Search Fields': '表格快速搜索字段',
+ 'Table Default Sort Fields': '表格默认排序字段',
+ 'sort order': '排序方式',
+ 'sort order asc': 'asc-顺序',
+ 'sort order desc': 'desc-倒序',
+ 'Fields as Table Columns': '作为表格列的字段',
+ 'Fields as form items': '作为表单项的字段',
+ 'The relative path to the generated code': '生成代码的相对位置',
+ 'For quick combination code generation location, please fill in the relative path': '快速的组合代码生成位置,请填写相对路径',
+ 'Generated Controller Location': '生成的控制器位置',
+ 'Generated Data Model Location': '生成的数据模型位置',
+ 'Generated Validator Location': '生成的验证器位置',
+ 'WEB end view directory': 'WEB端视图目录',
+ 'Check model class': "请检查以上数据模型类中是否已经配置 protected $connection = '{connection}';",
+ 'There is no connection attribute in model class': '未配置请手动配置。',
+ 'Common model': '公共模型',
+ 'Advanced Configuration': '高级配置',
+ 'Common Fields': '常用字段',
+ 'Base Fields': '基础字段',
+ 'Advanced Fields': '高级字段',
+ 'Field Name': '字段名',
+ 'field comment': '字段注释',
+ 'Please select a field from the left first': '请先从左侧选择一个字段',
+ Common: '常用',
+ 'Generate type': '生成类型',
+ 'Field comments (CRUD dictionary)': '字段注释(CRUD 字典)',
+ 'Field Properties': '字段属性',
+ 'Field Type': '字段类型',
+ length: '长度',
+ 'decimal point': '小数点',
+ 'Field Defaults': '字段默认值',
+ 'Please input the default value': '请输入默认值',
+ 'Auto increment': '自动递增',
+ Unsigned: '无符号',
+ 'Allow NULL': '允许 NULL',
+ 'Field Form Properties': '字段表单属性',
+ 'Field Table Properties': '字段表格属性',
+ 'Remote drop-down association information': '远程下拉关联信息',
+ 'Associated Data Table': '关联数据表',
+ 'Drop down value field': '下拉 value 字段',
+ 'Drop down label field': '下拉 label 字段',
+ 'Please select the value field of the select component': '请选择 select 组件的 value 字段',
+ 'Please select the label field of the select component': '请选择 select 组件的 label 字段',
+ 'Fields displayed in the table': '在表格中显示的字段',
+ 'Please select the fields displayed in the table': '请选择在表格中显示的字段',
+ 'Controller position': '控制器位置',
+ 'Please select the controller of the data table': '请选择数据表的控制器',
+ 'Data Model Location': '数据模型位置',
+ 'Please select the data model location of the data table': '请选择数据表的数据模型位置',
+ 'Data source configuration type': '数据源配置类型',
+ 'Fast configuration with generated controllers and models': '通过已生成好的控制器和模型快速配置',
+ 'Custom configuration': '自定义配置',
+ 'If the remote interface query involves associated query of multiple tables, enter the alias of the primary data table here':
+ '如果远程接口查询数据时涉及多表关联查询,请在此填写主数据表的别名',
+ 'Confirm CRUD code generation': '确认生成 CRUD 代码',
+ 'Continue building': '继续生成',
+ 'Please enter the data table name!': '请输入数据表名!',
+ 'Please enter the correct table name!': '请输入正确的数据表名!',
+ 'Use lower case underlined for table names': '请使用小写加下划线作为表名,小写字母开头,可以带有数字',
+ 'Please design the primary key field!': '请设计主键字段!',
+ 'It is irreversible to give up the design Are you sure you want to give up?': '放弃设计不可逆,确定要放弃吗?',
+ 'There can only be one primary key field': '只可以有一个主键字段。',
+ 'Drag the left element here to start designing CRUD': '拖动左侧元素至此处以开始设计CRUD',
+ 'The data table already exists Continuing to generate will automatically delete the original table and create a new one!':
+ '数据表已经存在,继续生成将自动删除原表并建立新的数据表(数据表是空的或您已勾选删表重建)!',
+ 'The controller already exists Continuing to generate will automatically overwrite the existing code!':
+ '控制器已经存在,继续生成将自动覆盖已有代码!',
+ 'The menu rule with the same name already exists The menu and permission node will not be created in this generation':
+ '同名菜单规则已经存在,本次生成将不会创建菜单和权限节点!',
+ 'For example: `user table` will be generated into `user management`': '如:会员表(将生成为会员管理)',
+ 'The remote pull-down will request the corresponding controller to obtain data, so it is recommended that you create the CRUD of the associated table':
+ '远程下拉将请求该控制器的 index 方法来获取 value 和 label 字段数据,所以请先生成好被关联表的CRUD',
+ 'If it is left blank, the model of the associated table will be generated automatically If the table already has a model, it is recommended to select it to avoid repeated generation':
+ '留空则自动生成关联表的模型,若该表已有模型,请选择好以免重复生成',
+ 'The field comment will be used as the CRUD dictionary, and will be identified as the field title before the colon, and as the data dictionary after the colon':
+ '字段注释将作为 CRUD 字典,冒号前将识别为字段标题,冒号后识别为数据字典',
+ 'Field name is invalid It starts with a letter or underscore and cannot contain any character other than letters, digits, or underscores':
+ '字段名 {field} 不符合规范,请以 字母、_ 开头,不能出现 字母、数字、下划线 以外的字符',
+ 'The selected table has already generated records You are advised to start with historical records':
+ '选择的表已有成功生成的记录,建议从历史记录开始~',
+ 'Start with the historical record': '从历史记录开始',
+ 'Add field': '添加字段',
+ 'Modify field properties': '修改字段属性',
+ 'Modify field name': '修改字段名称',
+ 'Delete field': '删除字段',
+ 'Modify field order': '修改字段顺序',
+ 'First field': '第一个字段',
+ After: '之后',
+ 'Table design change': '表设计变更',
+ 'Data table design changes preview': '数据表设计变更预览',
+ designChangeTips: '取消勾选后,则该项变动不会尝试同步至数据表(通常用于已经手动修改过表结构等情况)',
+ tableReBuild: '删表重建',
+ tableReBuildBlockHelp: '不调整表结构,直接删除已有数据表并重建,此举可以确保CRUD代码/记录与数据表结构一致',
+ Yes: '是',
+ No: '否',
+ 'If the data is abnormal, repeat the previous step': '数据异常,请重做上步操作',
+ 'Field name duplication': '字段名称 {field} 重复!',
+ 'Design remote select tips':
+ '将自动根据表名生成本字段的名称;确认生成时,字段名 user_id 生成的关联方法名为 user,字段名 developer_done_id 生成的关联方法名为 developerDone,请注意远程下拉字段的名称前缀不要重复',
+ 'Vite hot warning': '未找到 Vite 热更新服务,请在开发环境生成代码,或点击右上角的WEB终端重新发布',
+ 'Reset generate type attr': '字段生成类型已修改,是否将字段设计重置为新类型预设的方案?',
+ 'Design efficiency': '自行确定设计有效性',
+}
diff --git a/web/src/lang/backend/zh-cn/crud/log.ts b/web/src/lang/backend/zh-cn/crud/log.ts
new file mode 100644
index 0000000..165af1c
--- /dev/null
+++ b/web/src/lang/backend/zh-cn/crud/log.ts
@@ -0,0 +1,51 @@
+export default {
+ id: 'ID',
+ table_name: '数据表名',
+ comment: '表注释',
+ table: '数据表数据',
+ fields: '字段数据',
+ sync: '是否上传',
+ 'sync no': '否',
+ 'sync yes': '是',
+ status: '状态',
+ delete: '删除代码',
+ 'status delete': '代码已删除',
+ 'status success': '成功',
+ 'status error': '失败',
+ 'status start': '生成中',
+ create_time: '创建时间',
+ 'quick Search Fields': 'ID、表名、注释',
+ 'Upload the selected design records to the cloud for cross-device use': '上传选中的设计记录至云端以跨设备使用',
+ 'Design records that have been synchronized to the cloud': '已同步至云端的设计记录',
+ 'Cloud record': '云记录',
+ Settings: '设置',
+ 'Login for backup design': '登录以备份设计',
+ 'CRUD design record synchronization scheme': 'CRUD 设计记录同步方案',
+ Manual: '手动',
+ automatic: '自动',
+ 'When automatically synchronizing records, share them to the open source community': '自动同步记录时分享至开源社区',
+ 'Not to share': '不分享',
+ Share: '分享',
+ 'Enabling sharing can automatically earn community points during development': '开启分享可于开发同时自动获取社区积分',
+ 'The synchronized CRUD records are automatically resynchronized when they are updated': '已同步的 CRUD 记录被更新时自动重新同步',
+ 'Do not resynchronize': '不重新同步',
+ 'Automatic resynchronization': '自动重新同步',
+ 'No effective design': '无有效设计',
+ 'Number of fields': '字段数',
+ 'Upload type': '上传类型',
+ Update: '更新',
+ 'New added': '新增',
+ 'Share to earn points': '分享获得积分',
+ 'Share to the open source community': '分享至开源社区',
+ 'No design record': '无设计记录',
+ Field: '字段',
+ 'Field information': '字段信息',
+ 'No field': '无字段',
+ 'Field name': '字段名',
+ Note: '注释',
+ Type: '类型',
+ Load: '载入',
+ 'Delete cloud records?': '删除云端记录?',
+ 'You can use the synchronized design records across devices':
+ '您可以跨设备使用已同步的设计记录;选择手动同步时,系统不会主动收集任何数据,同时系统永远不会同步表内数据',
+}
diff --git a/web/src/lang/backend/zh-cn/crud/state.ts b/web/src/lang/backend/zh-cn/crud/state.ts
new file mode 100644
index 0000000..73351f9
--- /dev/null
+++ b/web/src/lang/backend/zh-cn/crud/state.ts
@@ -0,0 +1,19 @@
+export default {
+ remarks: '备注',
+ 'Primary key': '主键',
+ 'Primary key (Snowflake ID)': '主键(雪花ID)',
+ 'Disable Search': '禁用搜索',
+ 'Weight (drag and drop sorting)': '权重(拖拽排序)',
+ 'Status:0=Disabled,1=Enabled': '状态:0=禁用,1=启用',
+ 'Remote Select (association table)': '远程下拉(关联表)',
+ 'Remote Select (Multi)': '远程下拉(关联多选)',
+ 'Radio:opt0=Option1,opt1=Option2': '单选框:opt0=选项一,opt1=选项二',
+ 'Checkbox:opt0=Option1,opt1=Option2': '复选框:opt0=选项一,opt1=选项二',
+ Multi: '(多选)',
+ 'Select:opt0=Option1,opt1=Option2': '下拉框:opt0=选项一,opt1=选项二',
+ 'Switch:0=off,1=on': '开关:0=关,1=开',
+ 'Time date (timestamp storage)': '时间日期(时间戳存储)',
+ 'If left blank, the verifier title attribute will be filled in automatically': '留空则自动填写验证器title属性(看不懂请直接填写完整错误消息)',
+ 'Weight (automatically generate drag sort button)': '权重(自动生成拖拽排序按钮)',
+ 'If it is not input, it will be automatically analyzed by the controller': '不输入则以控制器自动解析',
+}
diff --git a/web/src/lang/backend/zh-cn/dashboard.ts b/web/src/lang/backend/zh-cn/dashboard.ts
new file mode 100644
index 0000000..009eaa8
--- /dev/null
+++ b/web/src/lang/backend/zh-cn/dashboard.ts
@@ -0,0 +1,39 @@
+export default {
+ 'You have worked today': '您今天已工作了',
+ 'Continue to work': '继续工作',
+ 'have a bit of rest': '休息片刻',
+ 'Member registration': '会员注册量',
+ 'Total number of members': '会员总数',
+ 'Number of installed plug-ins': '已装插件数',
+ 'Membership growth': '会员增长情况',
+ 'Annex growth': '附件增长情况',
+ 'New member': '刚刚加入的会员',
+ 'Joined us': '加入了我们',
+ 'Member source': '会员来源',
+ 'Member last name': '会员姓氏',
+ Loading: '加载中...',
+ Monday: '周一',
+ Tuesday: '周二',
+ Wednesday: '周三',
+ Thursday: '周四',
+ Friday: '周五',
+ Saturday: '周六',
+ Sunday: '周日',
+ Visits: '访问量',
+ 'Registration volume': '注册量',
+ picture: '图片',
+ file: '文档',
+ table: '表格',
+ other: '其它',
+ 'Compressed package': '压缩包',
+ Baidu: '百度',
+ 'Direct access': '直接访问',
+ 'take a plane': '坐飞机',
+ 'Take the high-speed railway': '坐高铁',
+ 'full name': '姓名',
+ hour: '小时',
+ minute: '分',
+ second: '秒',
+ day: '天',
+ 'Number of attachments Uploaded': '附件上传量',
+}
diff --git a/web/src/lang/backend/zh-cn/login.ts b/web/src/lang/backend/zh-cn/login.ts
new file mode 100644
index 0000000..bba33b5
--- /dev/null
+++ b/web/src/lang/backend/zh-cn/login.ts
@@ -0,0 +1,6 @@
+export default {
+ 'Please enter an account': '请输入账号',
+ 'Please input a password': '请输入密码',
+ 'Hold session': '保持会话',
+ 'Sign in': '登录',
+}
diff --git a/web/src/lang/backend/zh-cn/module.ts b/web/src/lang/backend/zh-cn/module.ts
new file mode 100644
index 0000000..3d45453
--- /dev/null
+++ b/web/src/lang/backend/zh-cn/module.ts
@@ -0,0 +1,153 @@
+export default {
+ 'stateTitle init': '模块安装器初始化...',
+ 'stateTitle download': '正在下载模块...',
+ 'stateTitle install': '正在安装模块...',
+ 'stateTitle getInstallableVersion': '正在获取模块版本列表...',
+ 'env require': '后端依赖(composer)',
+ 'env require-dev': '后端开发环境依赖(composer)',
+ 'env dependencies': '前端依赖(NPM)',
+ 'env devDependencies': '前端开发环境依赖(NPM)',
+ 'env nuxtDependencies': '前端依赖(Nuxt-NPM)',
+ 'env nuxtDevDependencies': '前端开发环境依赖(Nuxt-NPM)',
+ // buy
+ 'Module installation warning': '购买后一年内可免费下载和更新,虚拟产品不支持7天无理由退款',
+ 'Order title': '订单标题',
+ 'Order No': '订单编号',
+ 'Purchase user': '购买用户',
+ 'Order price': '订单价格',
+ 'Purchased, can be installed directly': '已购买,可直接安装',
+ 'Understand and agree': '理解并同意',
+ 'Module purchase and use agreement': '模块购买和使用协议',
+ 'Point payment': '积分支付',
+ 'Balance payment': '余额支付',
+ 'Wechat payment': '微信支付',
+ 'Alipay payment': '支付宝支付',
+ 'Install now': '立即安装',
+ payment: '支付',
+ 'Confirm order info': '确认订单信息',
+ // commonDone
+ 'Congratulations, module installation is complete': '恭喜,模块安装已完成。',
+ 'Module is disabled': '模块已禁用。',
+ 'Congratulations, the code of the module is ready': '恭喜,模块的代码已经准备好了。',
+ 'Unknown state': '未知状态。',
+ 'Do not refresh the page!': '请勿刷新页面!',
+ 'New adjustment of dependency detected': '检测到依赖项有新的调整',
+ 'This module adds new dependencies': '本模块添加了新的依赖项',
+ 'The built-in terminal of the system is automatically installing these dependencies, please wait~': '系统内置终端正在自动安装这些依赖,请稍等~',
+ 'View progress': '查看进度',
+ 'Dependency installation completed~': '依赖已安装完成~',
+ 'This module does not add new dependencies': '本模块没有添加新的依赖项。',
+ 'There is no adjustment for system dependency': '系统依赖无调整。',
+ please: '请',
+ 'After installation 1': '在安装结束后',
+ 'Manually clean up the system and browser cache': '手动的清理系统和浏览器缓存。',
+ 'After installation 2': '安装结束后',
+ 'Automatically execute reissue command?': '自动执行重新发布命令?',
+ 'End of installation': '安装结束',
+ 'Dependency installation fail 1': '依赖安装失败,请点击',
+ 'Dependency installation fail 2': '终端',
+ 'Dependency installation fail 3': '中的重试按钮,您也可以查看',
+ 'Dependency installation fail 4': '手动完成未尽事宜',
+ 'Dependency installation fail 5': '在您',
+ 'Dependency installation fail 6': '确定依赖已准备好',
+ 'Dependency installation fail 7': '之前,模块还不能正常使用!',
+ 'Is the command that failed on the WEB terminal executed manually or in other ways successfully?':
+ 'WEB终端失败的命令已经手动或以其他方式执行成功?',
+ yes: '是',
+ no: '否',
+ // confirmFileConflict
+ 'Update warning': '检测到以下的模块文件有更新,禁用时将自动覆盖,请注意备份。',
+ 'File conflict': '文件冲突',
+ 'Conflict file': '冲突文件',
+ 'Dependency conflict': '依赖冲突',
+ 'Confirm to disable the module': '确认禁用模块',
+ 'The module declares the added dependencies': '模块声明添加的依赖',
+ Dependencies: '依赖项',
+ retain: '保留',
+ // goodsInfo
+ 'detailed information': '详细信息',
+ Price: '价格',
+ 'Last updated': '最后更新',
+ 'Published on': '发布时间',
+ 'amount of downloads': '下载次数',
+ 'Module classification': '模块分类',
+ 'Module documentation': '模块文档',
+ 'Developer Homepage': '开发者主页',
+ 'Click to access': '点击访问',
+ 'Module status': '模块状态',
+ 'View demo': '查看演示',
+ 'Code scanning Preview': '扫码预览',
+ 'Buy now': '立即购买',
+ 'continue installation': '继续安装',
+ installed: '已安装',
+ 'to update': '更新',
+ uninstall: '卸载',
+ 'Contact developer': '联系开发者',
+ 'Other works of developers': 'TA的其他作品',
+ 'There are no more works': '没有更多作品了',
+ 'You need to disable this module before updating Do you want to disable it now?': '更新前需要先禁用该模块,立即禁用?',
+ 'Disable and update': '禁用并更新',
+ 'No module purchase order was found': '没有找到有效的模块购买订单,是否立即购买当前模块?',
+ // installConflict
+ 'new file': '新文件',
+ 'Existing files': '已有文件',
+ 'Treatment scheme': '处理方案',
+ 'Backup and overwrite existing files': '备份并覆盖已有文件',
+ 'Discard new file': '丢弃新文件',
+ environment: '环境',
+ 'New dependency': '新依赖',
+ 'Existing dependencies': '已有依赖',
+ 'Overwrite existing dependencies': '覆盖已有依赖',
+ 'Do not use new dependencies': '不使用新依赖',
+ // tableHeader
+ 'Upload zip package for installation': '上传ZIP包安装',
+ 'Upload installation': '上传安装',
+ 'Uploaded / installed modules': '已上传/安装的模块',
+ 'Local module': '本地模块',
+ 'Publishing module': '发布模块',
+ 'Get points': '获得积分',
+ 'Search is actually very simple': '搜索其实很简单',
+ // tabs
+ Loading: '加载中...',
+ 'No more': '没有更多了...',
+ // uploadInstall
+ 'Local upload warning': '请您务必确认模块包文件来自官方渠道或经由官方认证的模块作者,否则系统可能被破坏,因为:',
+ 'The module can modify and add system files': '模块可以修改和新增系统文件',
+ 'The module can execute sql commands and codes': '模块可以执行sql命令和代码',
+ 'The module can install new front and rear dependencies': '模块可以安装新的前后端依赖',
+ 'Drag the module package file here': '拖拽模块包文件到此处或',
+ 'Click me to upload': '点击我上传',
+ 'Uploaded, installation is about to start, please wait': '已上传,即将开始安装,请稍等',
+ 'Update Log': '更新日志',
+ 'No detailed update log': '无详细更新日志',
+ 'Use WeChat to scan QR code for payment': '使用微信扫描二维码支付',
+ 'Use Alipay to scan QR code for payment': '使用支付宝扫描二维码支付',
+ 'dependency-installation-fail-tips': '若手动执行命令成功,可点击以上的 `确定依赖已准备好` 将模块修改为已安装状态。',
+ 'New version': '有新版本',
+ Install: '安装',
+ 'Installation cancelled because module already exists!': '安装取消,因为模块已经存在!',
+ 'Installation cancelled because the directory required by the module is occupied!': '安装取消,因为模块所需目录被占用!',
+ 'Installation complete': '安装完成',
+ 'A conflict is found Please handle it manually': '发现冲突,请手动处理',
+ 'Select Version': '选择安装版本',
+ 'Wait for dependent installation': '等待依赖安装',
+ 'The operation succeeds Please clear the system cache and refresh the browser ~': '操作成功,请清理系统缓存并刷新浏览器~',
+ 'Deal with conflict': '处理冲突',
+ 'Wait for installation': '等待安装',
+ 'Conflict pending': '冲突待处理',
+ 'Dependency to be installed': '依赖待安装',
+ 'Restart Vite hot server': '重启热更新服务',
+ 'Restart Vite hot server tips': '在完成服务重启之前,您还可以随时从顶栏右侧的按钮组中找到手动重启服务的按钮。',
+ 'Manual restart': '手动重启',
+ 'Restart Now': '立即重启',
+ // 选择安装版本
+ 'Available system version': '可用系统版本',
+ Description: '描述',
+ Version: '版本',
+ 'Current installed version': '当前安装版本',
+ 'Insufficient system version': '系统版本不足',
+ 'Click to install': '点击安装',
+ 'Versions released beyond the authorization period': '授权期限以外发布的版本',
+ Renewal: '续费',
+ 'Order expiration time': '当前订单授权过期时间为 {expiration_time},此版本发布时间为 {create_time}',
+}
diff --git a/web/src/lang/backend/zh-cn/routine/adminInfo.ts b/web/src/lang/backend/zh-cn/routine/adminInfo.ts
new file mode 100644
index 0000000..fec914f
--- /dev/null
+++ b/web/src/lang/backend/zh-cn/routine/adminInfo.ts
@@ -0,0 +1,14 @@
+export default {
+ 'Last logged in on': '上次登录于',
+ 'user name': '用户名',
+ 'User nickname': '用户昵称',
+ 'Please enter a nickname': '请输入昵称',
+ 'e-mail address': '邮箱地址',
+ 'phone number': '手机号码',
+ autograph: '签名',
+ 'This guy is lazy and doesn write anything': '这家伙很懒,什么也没写',
+ 'New password': '新密码',
+ 'Please leave blank if not modified': '不修改请留空',
+ 'Save changes': '保存修改',
+ 'Operation log': '操作日志',
+}
diff --git a/web/src/lang/backend/zh-cn/routine/attachment.ts b/web/src/lang/backend/zh-cn/routine/attachment.ts
new file mode 100644
index 0000000..a33d93a
--- /dev/null
+++ b/web/src/lang/backend/zh-cn/routine/attachment.ts
@@ -0,0 +1,24 @@
+export default {
+ 'Upload administrator': '上传管理员',
+ 'Upload user': '上传会员',
+ 'Storage mode': '存储方式',
+ 'Physical path': '物理路径',
+ 'image width': '图片宽度',
+ 'Picture height': '图片高度',
+ 'file size': '文件大小',
+ 'mime type': 'mime类型',
+ 'SHA1 code': 'sha1',
+ 'The file is saved in the directory, and the file will not be automatically transferred if the record is modified':
+ '文件保存目录,修改记录不会自动转移文件',
+ 'File saving path Modifying records will not automatically transfer files': '文件保存路径,修改记录不会自动转移文件',
+ 'Width of picture file': '图片文件的宽度',
+ 'Height of picture file': '图片文件的高度',
+ 'Original file name': '文件原始名称',
+ 'File size (bytes)': '文件大小(bytes)',
+ 'File MIME type': '文件mime类型',
+ 'Upload (Reference) times of this file': '此文件的上传(引用)次数',
+ 'When the same file is uploaded multiple times, only one attachment record will be saved and added':
+ '同一文件被多次上传时,只会保存一份和增加一条附件记录',
+ 'SHA1 encoding of file': '文件的sha1编码',
+ 'Files and records will be deleted at the same time Are you sure?': '将同时删除文件和记录,确认吗?',
+}
diff --git a/web/src/lang/backend/zh-cn/routine/config.ts b/web/src/lang/backend/zh-cn/routine/config.ts
new file mode 100644
index 0000000..401a8dd
--- /dev/null
+++ b/web/src/lang/backend/zh-cn/routine/config.ts
@@ -0,0 +1,16 @@
+export default {
+ 'Are you sure to delete the configuration item?': '确定删除配置项吗?',
+ 'Add configuration item': '添加配置项',
+ 'Quick configuration entry': '快捷配置入口',
+ 'Variable name': '变量名',
+ 'Variable group': '变量分组',
+ 'Variable title': '变量标题',
+ 'Variable type': '变量类型',
+ number: '数字',
+ 'Please enter the recipient email address': '请输入接收者邮箱地址',
+ 'Test mail sending': '测试邮件发送',
+ 'send out': '发送',
+ 'Please enter the correct email address': '请输入正确的电子邮箱地址',
+ Sending: '发送中...',
+ 'Please enter the correct mail configuration': '请输入正确的邮件配置',
+}
diff --git a/web/src/lang/backend/zh-cn/security/dataRecycle.ts b/web/src/lang/backend/zh-cn/security/dataRecycle.ts
new file mode 100644
index 0000000..736e713
--- /dev/null
+++ b/web/src/lang/backend/zh-cn/security/dataRecycle.ts
@@ -0,0 +1,10 @@
+export default {
+ 'Rule name': '规则名称',
+ controller: '控制器',
+ 'data sheet': '数据表',
+ 'Data table primary key': '数据表主键',
+ 'Deleting monitoring': '删除监控中',
+ 'The rule name helps to identify deleted data later': '规则名称有助于后续识别被删数据',
+ 'The data collection mechanism will monitor delete operations under this controller': '数据回收机制将监控此控制器下的删除操作',
+ 'Corresponding data sheet': '对应数据表',
+}
diff --git a/web/src/lang/backend/zh-cn/security/dataRecycleLog.ts b/web/src/lang/backend/zh-cn/security/dataRecycleLog.ts
new file mode 100644
index 0000000..e6743a5
--- /dev/null
+++ b/web/src/lang/backend/zh-cn/security/dataRecycleLog.ts
@@ -0,0 +1,17 @@
+export default {
+ restore: '还原',
+ 'Are you sure to restore the selected records?': '确定还原选中记录?',
+ 'Restore the selected record to the original data table': '还原选中记录到原数据表',
+ 'Operation administrator': '操作管理员',
+ 'Recycling rule name': '回收规则名称',
+ 'Rule name': '规则名称',
+ controller: '控制器',
+ 'data sheet': '数据表',
+ DeletedData: '被删数据',
+ 'Arbitrary fragment fuzzy query': '任意片段模糊查询',
+ 'Click to expand': '点击展开',
+ 'Data table primary key': '数据表主键',
+ 'Operator IP': '操作者IP',
+ 'Deleted data': '被删除的数据',
+ 'Delete time': '删除时间',
+}
diff --git a/web/src/lang/backend/zh-cn/security/sensitiveData.ts b/web/src/lang/backend/zh-cn/security/sensitiveData.ts
new file mode 100644
index 0000000..7431a3a
--- /dev/null
+++ b/web/src/lang/backend/zh-cn/security/sensitiveData.ts
@@ -0,0 +1,12 @@
+export default {
+ 'Rule name': '规则名称',
+ controller: '控制器',
+ 'data sheet': '数据表',
+ 'Data table primary key': '数据表主键',
+ 'Sensitive fields': '敏感字段',
+ 'Modifying monitoring': '修改监控中',
+ 'The rule name helps to identify the modified data later': '规则名称有助于后续识别被修改数据',
+ 'The data listening mechanism will monitor the modification operations under this controller': '数据监听机制将监控此控制器下的修改操作',
+ 'Corresponding data sheet': '对应数据表',
+ 'Filling in field notes helps you quickly identify fields later': '填写字段注释有助于后续快速识别字段',
+}
diff --git a/web/src/lang/backend/zh-cn/security/sensitiveDataLog.ts b/web/src/lang/backend/zh-cn/security/sensitiveDataLog.ts
new file mode 100644
index 0000000..db6913c
--- /dev/null
+++ b/web/src/lang/backend/zh-cn/security/sensitiveDataLog.ts
@@ -0,0 +1,18 @@
+export default {
+ 'Operation administrator': '操作管理员',
+ 'Rule name': '规则名称',
+ controller: '控制器',
+ 'data sheet': '数据表',
+ 'Modify line': '修改行',
+ Modification: '修改项',
+ 'Before modification': '修改前',
+ 'After modification': '修改后',
+ 'Modification time': '修改时间',
+ 'Are you sure you want to rollback the record?': '确认要回滚记录吗?',
+ 'Rollback the selected record to the original data table': '回滚选中记录到原数据表',
+ 'Operator IP': '操作者IP',
+ 'Data table primary key': '数据表主键',
+ 'Modified item': '被修改项',
+ 'Modification comparison': '修改对比',
+ RollBACK: '回滚',
+}
diff --git a/web/src/lang/backend/zh-cn/user/group.ts b/web/src/lang/backend/zh-cn/user/group.ts
new file mode 100644
index 0000000..170fc8c
--- /dev/null
+++ b/web/src/lang/backend/zh-cn/user/group.ts
@@ -0,0 +1,5 @@
+export default {
+ GroupName: '组名',
+ 'Group name': '组别名称',
+ jurisdiction: '权限',
+}
diff --git a/web/src/lang/backend/zh-cn/user/moneyLog.ts b/web/src/lang/backend/zh-cn/user/moneyLog.ts
new file mode 100644
index 0000000..7f40880
--- /dev/null
+++ b/web/src/lang/backend/zh-cn/user/moneyLog.ts
@@ -0,0 +1,16 @@
+export default {
+ 'User name': '用户名',
+ 'User nickname': '用户昵称',
+ balance: '余额',
+ 'User ID': '用户ID',
+ 'Change balance': '变更余额',
+ 'Before change': '变更前',
+ 'After change': '变更后',
+ remarks: '备注',
+ 'Current balance': '当前余额',
+ 'Change amount': '变动数额',
+ 'Please enter the balance change amount': '请输入余额变更数额',
+ 'Balance after change': '变更后余额',
+ 'Please enter change remarks / description': '请输入变更备注/说明',
+ User: '用户',
+}
diff --git a/web/src/lang/backend/zh-cn/user/rule.ts b/web/src/lang/backend/zh-cn/user/rule.ts
new file mode 100644
index 0000000..d2851ad
--- /dev/null
+++ b/web/src/lang/backend/zh-cn/user/rule.ts
@@ -0,0 +1,24 @@
+export default {
+ 'Normal routing': '普通路由',
+ 'Member center menu contents': '会员中心菜单目录',
+ 'Member center menu items': '会员中心菜单项',
+ 'Top bar menu items': '顶栏菜单项',
+ 'Page button': '页面按钮',
+ 'Top bar user dropdown': '顶栏会员菜单下拉项',
+ 'Type route tips': '自动注册为前端路由',
+ 'Type menu_dir tips': '自动注册路由,并作为会员中心的菜单目录,此项本身不可跳转',
+ 'Type menu tips': '自动注册路由,并作为会员中心的菜单项目',
+ 'Type nav tips': '自动注册路由,并作为站点顶栏的菜单项目',
+ 'Type button tips': '自动注册为权限节点,可通过 v-auth 快速验权',
+ 'Type nav_user_menu tips': '自动注册路由,并作为顶栏会员菜单下拉项',
+ 'English name': '英文名称',
+ 'Web side routing path': 'WEB 端路由路径(vue-router 的 path)',
+ no_login_valid: '未登录有效',
+ 'no_login_valid 0': '游客无效',
+ 'no_login_valid 1': '游客有效',
+ 'no_login_valid tips': '游客没有会员分组,通过本选项设置当前规则是否对游客有效(可见)',
+ 'For example, if you add account/overview as a route only': 'WEB 端组件路径,请以 /src 开头,如:/src/views/frontend/index.vue',
+ 'Web side component path, please start with /src, such as: /src/views/frontend/index':
+ '比如将 `account/overview` 只添加为路由,那么可以另外将 `account/overview`、`account/overview/:a`、`account/overview/:b/:c` 只添加为菜单',
+ 'Component path tips': '组件路径在 WEB 工程内是必填的,否则无法访问,但作为 Nuxt 工程内的菜单时,无需填写此项,请根据菜单使用场景填写',
+}
diff --git a/web/src/lang/backend/zh-cn/user/scoreLog.ts b/web/src/lang/backend/zh-cn/user/scoreLog.ts
new file mode 100644
index 0000000..d6cbb21
--- /dev/null
+++ b/web/src/lang/backend/zh-cn/user/scoreLog.ts
@@ -0,0 +1,8 @@
+export default {
+ integral: '积分',
+ 'Change points': '变更积分',
+ 'Current points': '当前积分',
+ 'Please enter the change amount of points': '请输入积分变更数额',
+ 'Points after change': '变更后积分',
+ 'Please enter change remarks / description': '请输入变更备注/说明',
+}
diff --git a/web/src/lang/backend/zh-cn/user/user.ts b/web/src/lang/backend/zh-cn/user/user.ts
new file mode 100644
index 0000000..fc9df85
--- /dev/null
+++ b/web/src/lang/backend/zh-cn/user/user.ts
@@ -0,0 +1,22 @@
+export default {
+ 'User name': '用户名',
+ nickname: '昵称',
+ group: '分组',
+ avatar: '头像',
+ Gender: '性别',
+ male: '男',
+ female: '女',
+ mobile: '手机号',
+ 'Last login IP': '最后登录IP',
+ 'Last login': '最后登录',
+ email: '电子邮箱',
+ birthday: '生日',
+ balance: '余额',
+ 'Adjustment balance': '调整余额',
+ integral: '积分',
+ 'Adjust integral': '调整积分',
+ password: '密码',
+ 'Please leave blank if not modified': '不修改请留空',
+ 'Personal signature': '个性签名',
+ 'Login account': '登录账户名',
+}
diff --git a/web/src/lang/common/en/401.ts b/web/src/lang/common/en/401.ts
new file mode 100644
index 0000000..749859a
--- /dev/null
+++ b/web/src/lang/common/en/401.ts
@@ -0,0 +1,4 @@
+export default {
+ noPowerTip:
+ "It's not what you want, but we're serious. I want to tell you in a special way that you don't have permission to access this page or the file is invalid. You can contact the website administrator to solve the problem faster or go back home page to view another page.",
+}
diff --git a/web/src/lang/common/en/404.ts b/web/src/lang/common/en/404.ts
new file mode 100644
index 0000000..1f52a21
--- /dev/null
+++ b/web/src/lang/common/en/404.ts
@@ -0,0 +1,7 @@
+export default {
+ 'problems tip':
+ 'Your website has encountered some problems. The system is optimizing and reporting fault information. We will improve and reduce this situation in the future.',
+ 'We will automatically return to the previous page when we are finished': 'Auto return to previous page when finished.',
+ 'Return to home page': 'Back to Home',
+ 'Back to previous page': 'Back to previous page',
+}
diff --git a/web/src/lang/common/en/axios.ts b/web/src/lang/common/en/axios.ts
new file mode 100644
index 0000000..041b202
--- /dev/null
+++ b/web/src/lang/common/en/axios.ts
@@ -0,0 +1,20 @@
+export default {
+ 'Operation successful': 'Operate successful',
+ 'Automatic cancellation due to duplicate request:': 'Automatic cancellation due to duplicate requests:',
+ 'Interface redirected!': 'Interface redirected!',
+ 'Incorrect parameter!': 'Incorrect parameter!',
+ 'You do not have permission to operate!': 'You have no permission to operate!',
+ 'Error requesting address:': 'Error requesting address:',
+ 'Request timed out!': 'Request timeout!',
+ 'The same data already exists in the system!': 'The same data already exists on the system!',
+ 'Server internal error!': 'Internal server error!',
+ 'Service not implemented!': 'Service unrealized!',
+ 'Gateway error!': 'Gateway error!',
+ 'Service unavailable!': 'Service unavailable!',
+ 'The service is temporarily unavailable Please try again later!': 'The service is temporarily unavailable, please try again later!',
+ 'HTTP version is not supported!': 'HTTP version is not Unsupported!',
+ 'Abnormal problem, please contact the website administrator!': 'Abnormal problems, please contact the website administrator!',
+ 'Network request timeout!': 'Network request timeout!',
+ 'Server exception!': 'Server-side exceptions!',
+ 'You are disconnected!': 'You are disconnected!',
+}
diff --git a/web/src/lang/common/en/pagesTitle.ts b/web/src/lang/common/en/pagesTitle.ts
new file mode 100644
index 0000000..bc97161
--- /dev/null
+++ b/web/src/lang/common/en/pagesTitle.ts
@@ -0,0 +1,11 @@
+export default {
+ home: 'Home',
+ admin: 'Admin',
+ adminLogin: 'Login',
+ notFound: 'Page not found',
+ noPower: 'No access permission',
+ noTitle: 'No title',
+ loading: 'Loading...',
+ user: 'Member Center',
+ userLogin: 'Menber Login',
+}
diff --git a/web/src/lang/common/en/utils.ts b/web/src/lang/common/en/utils.ts
new file mode 100644
index 0000000..50c3e19
--- /dev/null
+++ b/web/src/lang/common/en/utils.ts
@@ -0,0 +1,88 @@
+export default {
+ 'The moving position is beyond the movable range!': 'The movement position is beyond the removable range!',
+ 'Navigation failed, the menu type is unrecognized!': 'Navigation failed, menu type not recognized!',
+ 'Navigation failed, navigation guard intercepted!': 'Navigation failed, Navigation Guard interception!',
+ 'Navigation failed, it is at the navigation target position!': 'Navigation failed, it is already at the navigation the position!',
+ 'Navigation failed, invalid route!': 'Navigation failed, invalid route!',
+ 'No child menu to jump to!': 'No child menu to jump to!',
+ Loading: 'Loading...',
+ Reload: 'Reload',
+ comma: ',',
+ 'welcome back': 'Welcome back!',
+ 'Late at night, pay attention to your body!': 'It is late at night. Please tack care of your body!',
+ 'good morning!': 'Good morning!',
+ 'Good morning!': 'Good morning!',
+ 'Good noon!': 'Good noon!',
+ 'good afternoon': 'Good afternoon.',
+ 'Good evening': 'Good evening',
+ 'Hello!': 'Hello!',
+ open: 'Open',
+ close: 'Close',
+ 'Clean up system cache': 'Clean up the system cache',
+ 'Clean up browser cache': 'Clean up browser cache',
+ 'Clean up all cache': 'Clean up all cache',
+ 'The data of the uploaded file is incomplete!': 'The data of the uploaded file is incomplete!',
+ 'The type of uploaded file is not allowed!': 'The type of uploaded file is not allowed!',
+ 'The size of the uploaded file exceeds the allowed range!': 'The size of the uploaded file exceeds the allowed range!',
+ 'Please install editor': 'Please install editor',
+ // 输入框类型
+ mobile: 'Mobile Number',
+ 'Id number': 'Id Number',
+ account: 'Account name',
+ password: 'password',
+ 'variable name': 'Variable Name',
+ email: 'Email address',
+ date: 'Date',
+ number: 'Number',
+ float: 'Float',
+ integer: 'Integer',
+ time: 'Time',
+ file: 'File',
+ array: 'Array',
+ switch: 'Switch',
+ year: 'Year',
+ image: 'Image',
+ select: 'Select',
+ string: 'String',
+ radio: 'Radio',
+ checkbox: 'checkbox',
+ 'rich Text': 'Rich Text',
+ 'multi image': 'Multi image',
+ textarea: 'Textarea',
+ 'time date': 'Time Date',
+ 'remote select': 'Remote Select',
+ 'city select': 'City select',
+ 'icon select': 'Icon select',
+ 'color picker': 'color picker',
+ color: 'color',
+ choice: ' Choice',
+ Icon: 'Icon',
+ 'Local icon title': 'Local icon:/src/assets/icons Inside.svg',
+ 'Please select an icon': 'Please select an icon',
+ 'Ali iconcont Icon': 'Ali Iconfont Icon',
+ 'Select File': 'Select File',
+ 'Original name': 'Original name',
+ 'You can also select': 'You can also select',
+ items: 'items',
+ Breakdown: 'Detailed catalogue',
+ size: 'Size',
+ type: 'Type',
+ preview: 'Preview',
+ 'Upload (Reference) times': 'Upload (Reference) times',
+ 'Last upload time': 'Last upload time',
+ 'One attribute per line without quotation marks(formitem)':
+ 'Extensions to FormItem, One attribute per line, no quotation marks required, such as: class=config-item',
+ 'Extended properties of Input, one line without quotation marks, such as: size=large':
+ 'Extended properties of Input, one line without quotation marks, such as: size=large',
+ 'One line at a time, without quotation marks, for example: key1=value1': 'One per line, no quotation marks required, such as: key1=value1',
+ Var: 'Var ',
+ Name: 'Name',
+ Title: 'Title',
+ Tip: 'Tip',
+ Rule: 'Rule',
+ Extend: 'Extend',
+ Dict: 'Dict',
+ ArrayKey: 'Key',
+ ArrayValue: 'Value',
+ 'No data': 'No data',
+}
diff --git a/web/src/lang/common/en/validate.ts b/web/src/lang/common/en/validate.ts
new file mode 100644
index 0000000..8a821d7
--- /dev/null
+++ b/web/src/lang/common/en/validate.ts
@@ -0,0 +1,18 @@
+export default {
+ 'Captcha loading failed, please click refresh button': 'Captcha loading failed, please click refresh button',
+ 'The correct area is not clicked, please try again!': 'The correct area is not clicked, please try again!',
+ 'Verification is successful!': 'Verification is successful!',
+ 'Please click': 'Please click',
+ 'Please enter the correct mobile number': 'Please enter the correct mobile number',
+ 'Please enter the correct account': 'The account requires 3 to 15 characters and contains a-z A-Z 0-9 _',
+ 'Please enter the correct password': 'The password requires 6 to 32 characters and cannot contains & < > " \'',
+ 'Please enter the correct name': 'Please enter the correct name',
+ 'Content cannot be empty': 'The content cannot be blank',
+ 'Floating point number': ' Floating number',
+ required: 'Required',
+ 'editor required': 'editor Required',
+ 'Please enter the correct ID number': 'Please enter the correct ID number',
+ number: 'Number (including float and integer)',
+ integer: 'Integer (excluding float)',
+ float: 'Float (excluding integer)',
+}
diff --git a/web/src/lang/common/zh-cn/401.ts b/web/src/lang/common/zh-cn/401.ts
new file mode 100644
index 0000000..14c30fb
--- /dev/null
+++ b/web/src/lang/common/zh-cn/401.ts
@@ -0,0 +1,4 @@
+export default {
+ noPowerTip:
+ '这不是你想要的,但我们是认真的。我只是想用一种特殊的方式告诉你,你无权访问此页面,或者该文件无效。您可以联系网站管理员以更快地解决问题,或返回网站首页浏览其他页面。',
+}
diff --git a/web/src/lang/common/zh-cn/404.ts b/web/src/lang/common/zh-cn/404.ts
new file mode 100644
index 0000000..2ffe98b
--- /dev/null
+++ b/web/src/lang/common/zh-cn/404.ts
@@ -0,0 +1,6 @@
+export default {
+ 'problems tip': '你的网页遇到了一些问题,系统正在优化和上报故障信息,我们在未来将改善和减少这种情况的发生.',
+ 'We will automatically return to the previous page when we are finished': '我们将在完成后自动返回到上一页。',
+ 'Return to home page': '返回首页',
+ 'Back to previous page': '返回上一页',
+}
diff --git a/web/src/lang/common/zh-cn/axios.ts b/web/src/lang/common/zh-cn/axios.ts
new file mode 100644
index 0000000..c802afe
--- /dev/null
+++ b/web/src/lang/common/zh-cn/axios.ts
@@ -0,0 +1,20 @@
+export default {
+ 'Operation successful': '操作成功',
+ 'Automatic cancellation due to duplicate request:': '因为请求重复被自动取消:',
+ 'Interface redirected!': '接口重定向了!',
+ 'Incorrect parameter!': '参数不正确!',
+ 'You do not have permission to operate!': '您没有权限操作!',
+ 'Error requesting address:': '请求地址出错:',
+ 'Request timed out!': '请求超时!',
+ 'The same data already exists in the system!': '系统已存在相同数据!',
+ 'Server internal error!': '服务器内部错误!',
+ 'Service not implemented!': '服务未实现!',
+ 'Gateway error!': '网关错误!',
+ 'Service unavailable!': '服务不可用!',
+ 'The service is temporarily unavailable Please try again later!': '服务暂时无法访问,请稍后再试!',
+ 'HTTP version is not supported!': 'HTTP版本不受支持!',
+ 'Abnormal problem, please contact the website administrator!': '异常问题,请联系网站管理员!',
+ 'Network request timeout!': '网络请求超时!',
+ 'Server exception!': '服务端异常!',
+ 'You are disconnected!': '您断网了!',
+}
diff --git a/web/src/lang/common/zh-cn/pagesTitle.ts b/web/src/lang/common/zh-cn/pagesTitle.ts
new file mode 100644
index 0000000..39c5a69
--- /dev/null
+++ b/web/src/lang/common/zh-cn/pagesTitle.ts
@@ -0,0 +1,11 @@
+export default {
+ home: '首页',
+ admin: '后台',
+ adminLogin: '登录',
+ notFound: '页面找不到了',
+ noPower: '无访问权限',
+ noTitle: '无标题',
+ loading: 'Loading...',
+ user: '会员中心',
+ userLogin: '会员登录',
+}
diff --git a/web/src/lang/common/zh-cn/utils.ts b/web/src/lang/common/zh-cn/utils.ts
new file mode 100644
index 0000000..a3c3f2a
--- /dev/null
+++ b/web/src/lang/common/zh-cn/utils.ts
@@ -0,0 +1,86 @@
+export default {
+ 'The moving position is beyond the movable range!': '移动位置超出了可移动范围!',
+ 'Navigation failed, the menu type is unrecognized!': '导航失败,菜单类型无法识别!',
+ 'Navigation failed, navigation guard intercepted!': '导航失败,导航守卫拦截!',
+ 'Navigation failed, it is at the navigation target position!': '导航失败,已在导航目标位置!',
+ 'Navigation failed, invalid route!': '导航失败,路由无效!',
+ 'No child menu to jump to!': '没有找到可以跳转的子级菜单!',
+ Loading: '加载中...',
+ Reload: '重新加载',
+ comma: ',',
+ 'welcome back': '欢迎回来!',
+ 'Late at night, pay attention to your body!': '夜深了,注意身体哦!',
+ 'good morning!': '早上好!',
+ 'Good morning!': '上午好!',
+ 'Good noon!': '中午好!',
+ 'good afternoon': '下午好!',
+ 'Good evening': '晚上好!',
+ 'Hello!': '您好!',
+ open: '开启',
+ close: '关闭',
+ 'Clean up system cache': '清理系统缓存',
+ 'Clean up browser cache': '清理浏览器缓存',
+ 'Clean up all cache': '一键清理所有',
+ 'The data of the uploaded file is incomplete!': '上传文件的资料不完整!',
+ 'The type of uploaded file is not allowed!': '上传文件的类型不被允许!',
+ 'The size of the uploaded file exceeds the allowed range!': '上传文件的大小超出允许范围!',
+ 'Please install editor': '请先于模块市场安装富文本编辑器。',
+ // 输入框类型
+ mobile: '手机号',
+ 'Id number': '身份证号',
+ account: '账户名',
+ password: '密码',
+ 'variable name': '变量名',
+ email: '邮箱地址',
+ date: '日期',
+ number: '数字',
+ float: '浮点数',
+ integer: '整数',
+ time: '时间',
+ file: '文件',
+ array: '数组',
+ switch: '开关',
+ year: '年份',
+ image: '图片',
+ select: '下拉框',
+ string: '字符串',
+ radio: '单选框',
+ checkbox: '复选框',
+ 'rich Text': '富文本',
+ 'multi image': '多图',
+ textarea: '多行文本框',
+ 'time date': '时间日期',
+ 'remote select': '远程下拉',
+ 'city select': '城市选择',
+ 'icon select': '图标选择',
+ 'color picker': '颜色选择器',
+ color: '颜色',
+ choice: '选择',
+ Icon: '图标',
+ 'Local icon title': '本地图标:/src/assets/icons中的.svg',
+ 'Please select an icon': '请选择图标',
+ 'Ali iconcont Icon': '阿里 Iconfont 图标',
+ 'Select File': '选择文件',
+ 'Original name': '原始名称',
+ 'You can also select': '还可以选择',
+ items: '项',
+ Breakdown: '细目',
+ size: '大小',
+ type: '类型',
+ preview: '预览',
+ 'Upload (Reference) times': '上传(引用)次数',
+ 'Last upload time': '最后上传时间',
+ 'One attribute per line without quotation marks(formitem)': 'FormItem 的扩展属性,一行一个,无需引号,比如:class=config-item',
+ 'Extended properties of Input, one line without quotation marks, such as: size=large': 'Input 的扩展属性,一行一个,无需引号,比如:size=large',
+ 'One line at a time, without quotation marks, for example: key1=value1': '一行一个,无需引号,比如:key1=value1',
+ Var: '变量',
+ Name: '名',
+ Title: '标题',
+ Tip: '提示信息',
+ Rule: '验证规则',
+ Extend: '扩展属性',
+ Dict: '字典数据',
+ ArrayKey: '键名',
+ ArrayValue: '键值',
+ 'No data': '无数据',
+}
diff --git a/web/src/lang/common/zh-cn/validate.ts b/web/src/lang/common/zh-cn/validate.ts
new file mode 100644
index 0000000..6b8ec16
--- /dev/null
+++ b/web/src/lang/common/zh-cn/validate.ts
@@ -0,0 +1,18 @@
+export default {
+ 'Captcha loading failed, please click refresh button': '验证码加载失败,请点击刷新按钮',
+ 'The correct area is not clicked, please try again!': '未点中正确区域,请重试!',
+ 'Verification is successful!': '验证成功!',
+ 'Please click': '请依次点击',
+ 'Please enter the correct mobile number': '请输入正确的手机号',
+ 'Please enter the correct account': '要求3到15位,字母开头且只含字母、数字、下划线',
+ 'Please enter the correct password': '密码要求6到32位,不能包含 & < > " \'',
+ 'Please enter the correct name': '请输入正确的名称',
+ 'Content cannot be empty': '内容不能为空',
+ 'Floating point number': '浮点数',
+ required: '必填',
+ 'editor required': '富文本必填',
+ 'Please enter the correct ID number': '请输入正确的身份证号码',
+ number: '数字(包括浮点数和整数)',
+ integer: '整数(不包括浮点数)',
+ float: '浮点数(不包括整数)',
+}
diff --git a/web/src/lang/frontend/en.ts b/web/src/lang/frontend/en.ts
new file mode 100644
index 0000000..6c24b0b
--- /dev/null
+++ b/web/src/lang/frontend/en.ts
@@ -0,0 +1,12 @@
+/**
+ * frontend common language package
+ */
+export default {
+ Integral: 'Integral',
+ Balance: 'Balance',
+ Language: 'Language',
+ Copyright: 'Copyright',
+ 'Member Center': 'Member Center',
+ 'Logout login': 'Logout',
+ 'Member center disabled': 'The member center has been disabled. Please contact the webmaster to turn it on.',
+}
diff --git a/web/src/lang/frontend/en/index.ts b/web/src/lang/frontend/en/index.ts
new file mode 100644
index 0000000..40c86d3
--- /dev/null
+++ b/web/src/lang/frontend/en/index.ts
@@ -0,0 +1,3 @@
+export default {
+ 'Steve Jobs': "Great art don't have to follow the trend, it alone can lead.-- Steve Jobs",
+}
diff --git a/web/src/lang/frontend/en/user/account/balance.ts b/web/src/lang/frontend/en/user/account/balance.ts
new file mode 100644
index 0000000..efe4efd
--- /dev/null
+++ b/web/src/lang/frontend/en/user/account/balance.ts
@@ -0,0 +1,6 @@
+export default {
+ 'Change time': 'Change time',
+ 'Current balance': 'Current balance',
+ 'Balance after change': 'Balance after change',
+ 'Balance change record': 'Balance change record',
+}
diff --git a/web/src/lang/frontend/en/user/account/changePassword.ts b/web/src/lang/frontend/en/user/account/changePassword.ts
new file mode 100644
index 0000000..8f603f7
--- /dev/null
+++ b/web/src/lang/frontend/en/user/account/changePassword.ts
@@ -0,0 +1,8 @@
+export default {
+ 'Change Password': 'Change Password',
+ 'Old password': 'Old password',
+ 'New password': 'New password',
+ 'Confirm new password': 'Confirm new password',
+ 'Please enter your current password': 'Please enter your current password',
+ 'The duplicate password does not match the new password': 'The duplicate password does not match the new password',
+}
diff --git a/web/src/lang/frontend/en/user/account/integral.ts b/web/src/lang/frontend/en/user/account/integral.ts
new file mode 100644
index 0000000..4114a61
--- /dev/null
+++ b/web/src/lang/frontend/en/user/account/integral.ts
@@ -0,0 +1,6 @@
+export default {
+ 'Change time': 'Change time',
+ 'Current points': 'Current points',
+ 'Points after change': 'Points after change',
+ 'Score change record': 'Score change record',
+}
diff --git a/web/src/lang/frontend/en/user/account/overview.ts b/web/src/lang/frontend/en/user/account/overview.ts
new file mode 100644
index 0000000..373343b
--- /dev/null
+++ b/web/src/lang/frontend/en/user/account/overview.ts
@@ -0,0 +1,11 @@
+export default {
+ 'Account information': 'Account information',
+ profile: 'Profile',
+ 'Filled in': 'Filled in',
+ 'Not filled in': 'Not filled in',
+ mobile: 'mobile',
+ email: 'email',
+ 'Last login IP': 'Last login IP',
+ 'Last login': 'Last login',
+ 'Growth statistics': 'Growth statistics',
+}
diff --git a/web/src/lang/frontend/en/user/account/profile.ts b/web/src/lang/frontend/en/user/account/profile.ts
new file mode 100644
index 0000000..8eea8a8
--- /dev/null
+++ b/web/src/lang/frontend/en/user/account/profile.ts
@@ -0,0 +1,32 @@
+export default {
+ profile: 'Profile',
+ 'Change Password': 'Change Password',
+ avatar: 'Avatar',
+ 'User name': 'User name',
+ 'User nickname': 'User nickname',
+ mail: 'mail',
+ email: 'email',
+ 'Operation via right button': 'Operation via right button',
+ 'Click Modify': 'Click Modify',
+ bind: 'bind',
+ mobile: 'mobile',
+ Gender: 'Gender',
+ secrecy: 'secrecy',
+ male: 'male',
+ female: 'female',
+ birthday: 'birthday',
+ 'Personal signature': 'Personal signature',
+ 'Account verification': 'Account verification',
+ 'Account password verification': 'Account password verification',
+ 'Mail verification': 'Mail verification',
+ 'SMS verification': 'SMS verification',
+ password: 'password',
+ accept: 'accept',
+ 'next step': 'next step',
+ 'New email': 'New email',
+ 'New mobile': 'New mobile',
+ 'Verification Code': 'Captcha',
+ send: 'send',
+ seconds: 'seconds',
+ nickname: 'nickname',
+}
diff --git a/web/src/lang/frontend/en/user/login.ts b/web/src/lang/frontend/en/user/login.ts
new file mode 100644
index 0000000..98856c1
--- /dev/null
+++ b/web/src/lang/frontend/en/user/login.ts
@@ -0,0 +1,24 @@
+export default {
+ reach: ' Reach ',
+ login: 'Login',
+ register: 'Register',
+ 'Via email': 'By email',
+ 'Via mobile number': 'By mobile number',
+ 'User name': 'User name',
+ account: 'Username/Email/Mobile',
+ password: 'Password',
+ 'Verification Code': 'Captcha',
+ mobile: 'mobile',
+ email: 'email',
+ send: 'send',
+ seconds: 'seconds',
+ 'Remember me': 'Remember me',
+ 'Forgot your password?': 'Forgot your password?',
+ 'Back to login': 'Back to login',
+ 'No account yet? Click Register': 'No account yet? Click Register',
+ 'Retrieve password': 'Retrieve password',
+ 'Retrieval method': 'Retrieval method',
+ 'New password': 'New password',
+ second: 'second',
+ 'Account name': 'Account name',
+}
diff --git a/web/src/lang/frontend/zh-cn.ts b/web/src/lang/frontend/zh-cn.ts
new file mode 100644
index 0000000..59db0fd
--- /dev/null
+++ b/web/src/lang/frontend/zh-cn.ts
@@ -0,0 +1,13 @@
+/**
+ * 前台公共语言包
+ * 覆盖风险:请避免使用页面语言包的目录名、文件名作为翻译 key
+ */
+export default {
+ Integral: '积分',
+ Balance: '余额',
+ Language: '语言',
+ Copyright: '版权所有',
+ 'Member Center': '会员中心',
+ 'Logout login': '注销登录',
+ 'Member center disabled': '会员中心已禁用,请联系网站管理员开启。',
+}
diff --git a/web/src/lang/frontend/zh-cn/index.ts b/web/src/lang/frontend/zh-cn/index.ts
new file mode 100644
index 0000000..7ba6da1
--- /dev/null
+++ b/web/src/lang/frontend/zh-cn/index.ts
@@ -0,0 +1,3 @@
+export default {
+ 'Steve Jobs': '伟大的艺术品不必追随潮流,他本身就能引领潮流。 -- 乔布斯',
+}
diff --git a/web/src/lang/frontend/zh-cn/user/account/balance.ts b/web/src/lang/frontend/zh-cn/user/account/balance.ts
new file mode 100644
index 0000000..bd9dcae
--- /dev/null
+++ b/web/src/lang/frontend/zh-cn/user/account/balance.ts
@@ -0,0 +1,6 @@
+export default {
+ 'Change time': '变更时间',
+ 'Current balance': '当前余额',
+ 'Balance after change': '变更后余额',
+ 'Balance change record': '余额变更记录',
+}
diff --git a/web/src/lang/frontend/zh-cn/user/account/changePassword.ts b/web/src/lang/frontend/zh-cn/user/account/changePassword.ts
new file mode 100644
index 0000000..862427f
--- /dev/null
+++ b/web/src/lang/frontend/zh-cn/user/account/changePassword.ts
@@ -0,0 +1,8 @@
+export default {
+ 'Change Password': '修改密码',
+ 'Old password': '旧密码',
+ 'New password': '新密码',
+ 'Confirm new password': '确认新密码',
+ 'Please enter your current password': '请输入现在的密码',
+ 'The duplicate password does not match the new password': '重复密码与新密码不相符',
+}
diff --git a/web/src/lang/frontend/zh-cn/user/account/integral.ts b/web/src/lang/frontend/zh-cn/user/account/integral.ts
new file mode 100644
index 0000000..937ae84
--- /dev/null
+++ b/web/src/lang/frontend/zh-cn/user/account/integral.ts
@@ -0,0 +1,6 @@
+export default {
+ 'Change time': '变更时间',
+ 'Current points': '当前积分',
+ 'Points after change': '变更后积分',
+ 'Score change record': '积分变更记录',
+}
diff --git a/web/src/lang/frontend/zh-cn/user/account/overview.ts b/web/src/lang/frontend/zh-cn/user/account/overview.ts
new file mode 100644
index 0000000..4802acf
--- /dev/null
+++ b/web/src/lang/frontend/zh-cn/user/account/overview.ts
@@ -0,0 +1,11 @@
+export default {
+ 'Account information': '账户信息',
+ profile: '个人资料',
+ 'Filled in': '已填写',
+ 'Not filled in': '未填写',
+ mobile: '手机号',
+ email: '电子邮箱',
+ 'Last login IP': '最后登录IP',
+ 'Last login': '最后登录',
+ 'Growth statistics': '增长统计',
+}
diff --git a/web/src/lang/frontend/zh-cn/user/account/profile.ts b/web/src/lang/frontend/zh-cn/user/account/profile.ts
new file mode 100644
index 0000000..a8aa187
--- /dev/null
+++ b/web/src/lang/frontend/zh-cn/user/account/profile.ts
@@ -0,0 +1,32 @@
+export default {
+ profile: '个人资料',
+ 'Change Password': '修改密码',
+ avatar: '头像',
+ 'User name': '用户名',
+ 'User nickname': '用户昵称',
+ mail: '邮箱',
+ email: '电子邮箱',
+ 'Operation via right button': '通过右侧按钮操作',
+ 'Click Modify': '点击修改',
+ bind: '绑定',
+ mobile: '手机号',
+ Gender: '性别',
+ secrecy: '保密',
+ male: '男',
+ female: '女',
+ birthday: '生日',
+ 'Personal signature': '个性签名',
+ 'Account verification': '账户验证',
+ 'Account password verification': '账户密码验证',
+ 'Mail verification': '邮件验证',
+ 'SMS verification': '短信验证',
+ password: '密码',
+ accept: '接受',
+ 'next step': '下一步',
+ 'New email': '新邮箱',
+ 'New mobile': '新手机号',
+ 'Verification Code': '验证码',
+ send: '发送',
+ seconds: '秒',
+ nickname: '昵称',
+}
diff --git a/web/src/lang/frontend/zh-cn/user/login.ts b/web/src/lang/frontend/zh-cn/user/login.ts
new file mode 100644
index 0000000..0ab2e0d
--- /dev/null
+++ b/web/src/lang/frontend/zh-cn/user/login.ts
@@ -0,0 +1,24 @@
+export default {
+ reach: '到',
+ login: '登录',
+ register: '注册',
+ 'Via email': '通过邮箱',
+ 'Via mobile number': '通过手机号',
+ 'User name': '用户名',
+ account: '用户名/邮箱/手机号',
+ password: '密码',
+ 'Verification Code': '验证码',
+ mobile: '手机号',
+ email: '电子邮箱',
+ send: '发送',
+ seconds: '秒',
+ 'Remember me': '记住我',
+ 'Forgot your password?': '忘记密码?',
+ 'Back to login': '回到登录',
+ 'No account yet? Click Register': '还没有账户?点击注册',
+ 'Retrieve password': '找回密码',
+ 'Retrieval method': '找回方式',
+ 'New password': '新密码',
+ second: '确定',
+ 'Account name': '账户',
+}
diff --git a/web/src/lang/globs-en.ts b/web/src/lang/globs-en.ts
new file mode 100644
index 0000000..ef294d7
--- /dev/null
+++ b/web/src/lang/globs-en.ts
@@ -0,0 +1,50 @@
+/**
+ * Global common language package
+ */
+export default {
+ Id: 'ID',
+ State: 'State',
+ Home: 'Home',
+ Complete: 'Completed',
+ Edit: 'Edit',
+ Add: 'Add',
+ Info: 'Details',
+ Delete: 'Delete',
+ Refresh: 'Refresh',
+ Operate: 'Operate',
+ Confirm: 'Confirm',
+ Cancel: 'Cancel',
+ Save: 'Save',
+ Upload: 'Upload',
+ Retry: 'Retry',
+ Reminder: 'Reminder',
+ Disable: 'Disable',
+ Enable: 'Enable',
+ Shrink: 'Shrink',
+ Open: 'Open',
+ Search: 'Search',
+ Reset: 'Reset',
+ To: 'To',
+ None: 'None',
+ Unknown: 'Unknown',
+ Weigh: 'weigh',
+ 'Drag sort': 'Drag sort',
+ 'Save and edit next item': 'save and edit next item',
+ 'Quick search placeholder': 'Fuzzy search by {fields}',
+ 'Please select field': 'Please select {field}',
+ 'Please input field': 'Please input {field}',
+ 'Please enter the correct field': 'Please enter the correct {field}',
+ 'Update time': 'Update time',
+ 'Create time': 'Create time',
+ 'Fuzzy query': 'Fuzzy query',
+ 'Click select': 'Click select',
+ 'Edit selected row': 'Edit selected row',
+ 'Delete selected row': 'Delete selected row',
+ 'Are you sure to delete the selected record?': 'Are you sure to delete the selected record?',
+ 'All submenus': 'All submenus',
+ 'Shrink all': 'Shrinkage all',
+ 'Expand all': 'Expand all',
+ 'Expand generic search': 'Expand Universal Search',
+ 'Link address': 'Link address',
+ 'No route found to jump~': 'Failed to find a jump route.',
+}
diff --git a/web/src/lang/globs-zh-cn.ts b/web/src/lang/globs-zh-cn.ts
new file mode 100644
index 0000000..e2508dc
--- /dev/null
+++ b/web/src/lang/globs-zh-cn.ts
@@ -0,0 +1,51 @@
+/**
+ * 全局公共语言包
+ * 覆盖风险:请避免使用页面语言包的目录名、文件名作为翻译 key、请使用大写开头避免覆盖
+ */
+export default {
+ Id: 'ID',
+ State: '状态',
+ Home: '首页',
+ Complete: '完成',
+ Edit: '编辑',
+ Add: '添加',
+ Info: '查看详情',
+ Delete: '删除',
+ Refresh: '刷新',
+ Operate: '操作',
+ Confirm: '确认',
+ Cancel: '取消',
+ Save: '保存',
+ Upload: '上传',
+ Retry: '重试',
+ Reminder: '温馨提示',
+ Disable: '禁用',
+ Enable: '启用',
+ Shrink: '收缩',
+ Open: '展开',
+ Search: '搜索',
+ Reset: '重置',
+ To: '至',
+ None: '无',
+ Unknown: '未知',
+ Weigh: '权重',
+ 'Drag sort': '拖动以排序',
+ 'Save and edit next item': '保存并编辑下一项',
+ 'Quick search placeholder': '通过{fields}模糊搜索',
+ 'Please select field': '请选择{field}',
+ 'Please input field': '请输入{field}',
+ 'Please enter the correct field': '请输入正确的{field}',
+ 'Update time': '修改时间',
+ 'Create time': '创建时间',
+ 'Fuzzy query': '模糊查询',
+ 'Click select': '点击选择',
+ 'Edit selected row': '编辑选中行',
+ 'Delete selected row': '删除选中行',
+ 'Are you sure to delete the selected record?': '确定删除选中记录?',
+ 'All submenus': '所有子菜单',
+ 'Shrink all': '收缩所有',
+ 'Expand all': '展开所有',
+ 'Expand generic search': '展开公共搜索',
+ 'Link address': '链接地址',
+ 'No route found to jump~': '没有找到可以跳转的路由~',
+}
diff --git a/web/src/lang/index.ts b/web/src/lang/index.ts
new file mode 100644
index 0000000..114bb44
--- /dev/null
+++ b/web/src/lang/index.ts
@@ -0,0 +1,148 @@
+import type { App } from 'vue'
+import { createI18n } from 'vue-i18n'
+import type { I18n, Composer } from 'vue-i18n'
+import { useConfig } from '/@/stores/config'
+import { isEmpty } from 'lodash-es'
+
+/*
+ * 默认只引入 element-plus 的中英文语言包
+ * 其他语言包请自行在此 import,并添加到 assignLocale 内
+ * 动态 import 只支持相对路径,所以无法按需 import element-plus 的语言包
+ * 但i18n的 messages 内是按需载入的
+ */
+import elementZhcnLocale from 'element-plus/es/locale/lang/zh-cn'
+import elementEnLocale from 'element-plus/es/locale/lang/en'
+
+export let i18n: {
+ global: Composer
+}
+
+// 准备要合并的语言包
+const assignLocale: anyObj = {
+ 'zh-cn': [elementZhcnLocale],
+ en: [elementEnLocale],
+}
+
+export async function loadLang(app: App) {
+ const config = useConfig()
+ const locale = config.lang.defaultLang
+
+ // 加载框架全局语言包
+ const lang = await import(`./globs-${locale}.ts`)
+ const message = lang.default ?? {}
+
+ // 按需加载语言包文件的句柄
+ if (locale == 'zh-cn') {
+ window.loadLangHandle = {
+ ...import.meta.glob('./backend/zh-cn/**/*.ts'),
+ ...import.meta.glob('./frontend/zh-cn/**/*.ts'),
+ ...import.meta.glob('./backend/zh-cn.ts'),
+ ...import.meta.glob('./frontend/zh-cn.ts'),
+ }
+ } else {
+ window.loadLangHandle = {
+ ...import.meta.glob('./backend/en/**/*.ts'),
+ ...import.meta.glob('./frontend/en/**/*.ts'),
+ ...import.meta.glob('./backend/en.ts'),
+ ...import.meta.glob('./frontend/en.ts'),
+ }
+ }
+
+ /*
+ * 加载页面语言包 import.meta.glob 的路径不能使用变量 import() 在 Vite 中目录名不能使用变量(编译后,文件名可以)
+ */
+ if (locale == 'zh-cn') {
+ assignLocale[locale].push(getLangFileMessage(import.meta.glob('./common/zh-cn/**/*.ts', { eager: true }), locale))
+ } else if (locale == 'en') {
+ assignLocale[locale].push(getLangFileMessage(import.meta.glob('./common/en/**/*.ts', { eager: true }), locale))
+ }
+
+ const messages = {
+ [locale]: {
+ ...message,
+ },
+ }
+
+ // 合并语言包(含element-puls、页面语言包)
+ Object.assign(messages[locale], ...assignLocale[locale])
+
+ i18n = createI18n({
+ locale: locale,
+ legacy: false, // 组合式api
+ globalInjection: true, // 挂载$t,$d等到全局
+ fallbackLocale: config.lang.fallbackLang,
+ messages,
+ })
+
+ app.use(i18n as I18n)
+ return i18n
+}
+
+function getLangFileMessage(mList: any, locale: string) {
+ let msg: anyObj = {}
+ locale = '/' + locale
+ for (const path in mList) {
+ if (mList[path].default) {
+ // 获取文件名
+ const pathName = path.slice(path.lastIndexOf(locale) + (locale.length + 1), path.lastIndexOf('.'))
+ if (pathName.indexOf('/') > 0) {
+ msg = handleMsglist(msg, mList[path].default, pathName)
+ } else {
+ msg[pathName] = mList[path].default
+ }
+ }
+ }
+ return msg
+}
+
+export function mergeMessage(message: anyObj, pathName = '') {
+ if (isEmpty(message)) return
+ if (!pathName) {
+ return i18n.global.mergeLocaleMessage(i18n.global.locale.value, message)
+ }
+ let msg: anyObj = {}
+ if (pathName.indexOf('/') > 0) {
+ msg = handleMsglist(msg, message, pathName)
+ } else {
+ msg[pathName] = message
+ }
+ i18n.global.mergeLocaleMessage(i18n.global.locale.value, msg)
+}
+
+export function handleMsglist(msg: anyObj, mList: anyObj, pathName: string) {
+ const pathNameTmp = pathName.split('/')
+ let obj: anyObj = {}
+ for (let i = pathNameTmp.length - 1; i >= 0; i--) {
+ if (i == pathNameTmp.length - 1) {
+ obj = {
+ [pathNameTmp[i]]: mList,
+ }
+ } else {
+ obj = {
+ [pathNameTmp[i]]: obj,
+ }
+ }
+ }
+ return mergeMsg(msg, obj)
+}
+
+export function mergeMsg(msg: anyObj, obj: anyObj) {
+ for (const key in obj) {
+ if (typeof msg[key] == 'undefined') {
+ msg[key] = obj[key]
+ } else if (typeof msg[key] == 'object') {
+ msg[key] = mergeMsg(msg[key], obj[key])
+ }
+ }
+ return msg
+}
+
+export function editDefaultLang(lang: string): void {
+ const config = useConfig()
+ config.setLang(lang)
+
+ /*
+ * 语言包是按需加载的,比如默认语言为中文,则只在app实例内加载了中文语言包,所以切换语言需要进行 reload
+ */
+ location.reload()
+}
diff --git a/web/src/layouts/backend/components/aside.vue b/web/src/layouts/backend/components/aside.vue
new file mode 100644
index 0000000..b4d43cc
--- /dev/null
+++ b/web/src/layouts/backend/components/aside.vue
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/layouts/backend/components/baAccount.vue b/web/src/layouts/backend/components/baAccount.vue
new file mode 100644
index 0000000..e48cab8
--- /dev/null
+++ b/web/src/layouts/backend/components/baAccount.vue
@@ -0,0 +1,259 @@
+
+
+
+
+
+
+
+
+
+
+
{{ baAccount.nickname }}
+
+ {{ $t('Integral') + ' ' + baAccount.score }}
+ {{ $t('Balance') + ' ' + baAccount.money }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/layouts/backend/components/closeFullScreen.vue b/web/src/layouts/backend/components/closeFullScreen.vue
new file mode 100644
index 0000000..5b90738
--- /dev/null
+++ b/web/src/layouts/backend/components/closeFullScreen.vue
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
diff --git a/web/src/layouts/backend/components/config.vue b/web/src/layouts/backend/components/config.vue
new file mode 100644
index 0000000..36a4515
--- /dev/null
+++ b/web/src/layouts/backend/components/config.vue
@@ -0,0 +1,420 @@
+
+
+
+
+
+
+
{{ t('layouts.Layout mode') }}
+
+
+
+
+
+
{{ t('layouts.default') }}
+
+
+
+
+
+
{{ t('layouts.classic') }}
+
+
+
+
+
+
+
+
{{ t('layouts.Single column') }}
+
+
+
+
+
+
{{ t('layouts.Double column') }}
+
+
+
+
+
{{ t('layouts.overall situation') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ t('layouts.sidebar') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ px
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ t('layouts.Top bar') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('layouts.Restore default') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/layouts/backend/components/header.vue b/web/src/layouts/backend/components/header.vue
new file mode 100644
index 0000000..94575d0
--- /dev/null
+++ b/web/src/layouts/backend/components/header.vue
@@ -0,0 +1,28 @@
+
+
+
+
+
+
diff --git a/web/src/layouts/backend/components/logo.vue b/web/src/layouts/backend/components/logo.vue
new file mode 100644
index 0000000..c03e6b2
--- /dev/null
+++ b/web/src/layouts/backend/components/logo.vue
@@ -0,0 +1,79 @@
+
+
+
+
+ {{ siteConfig.siteName }}
+
+
+
+
+
+
+
+
diff --git a/web/src/layouts/backend/components/menus/menuHorizontal.vue b/web/src/layouts/backend/components/menus/menuHorizontal.vue
new file mode 100644
index 0000000..8665820
--- /dev/null
+++ b/web/src/layouts/backend/components/menus/menuHorizontal.vue
@@ -0,0 +1,110 @@
+
+
+
+
+
+
+
diff --git a/web/src/layouts/backend/components/menus/menuTree.vue b/web/src/layouts/backend/components/menus/menuTree.vue
new file mode 100644
index 0000000..8d3c136
--- /dev/null
+++ b/web/src/layouts/backend/components/menus/menuTree.vue
@@ -0,0 +1,84 @@
+
+
+
+
+
+
+ {{ menu.meta?.title ? menu.meta?.title : $t('noTitle') }}
+
+
+
+
+
+
+
+ {{ menu.meta?.title ? menu.meta?.title : $t('noTitle') }}
+
+
+
+
+
+
+
+
diff --git a/web/src/layouts/backend/components/menus/menuVertical.vue b/web/src/layouts/backend/components/menus/menuVertical.vue
new file mode 100644
index 0000000..98c9d02
--- /dev/null
+++ b/web/src/layouts/backend/components/menus/menuVertical.vue
@@ -0,0 +1,81 @@
+
+
+
+
+
+
diff --git a/web/src/layouts/backend/components/menus/menuVerticalChildren.vue b/web/src/layouts/backend/components/menus/menuVerticalChildren.vue
new file mode 100644
index 0000000..c93dc92
--- /dev/null
+++ b/web/src/layouts/backend/components/menus/menuVerticalChildren.vue
@@ -0,0 +1,99 @@
+
+
+
+
+
+
diff --git a/web/src/layouts/backend/components/navBar/classic.vue b/web/src/layouts/backend/components/navBar/classic.vue
new file mode 100644
index 0000000..e8d8705
--- /dev/null
+++ b/web/src/layouts/backend/components/navBar/classic.vue
@@ -0,0 +1,79 @@
+
+
+
+
+
+
+
diff --git a/web/src/layouts/backend/components/navBar/default.vue b/web/src/layouts/backend/components/navBar/default.vue
new file mode 100644
index 0000000..4a8a2c0
--- /dev/null
+++ b/web/src/layouts/backend/components/navBar/default.vue
@@ -0,0 +1,84 @@
+
+
+
+
+
+
+
diff --git a/web/src/layouts/backend/components/navBar/double.vue b/web/src/layouts/backend/components/navBar/double.vue
new file mode 100644
index 0000000..cc25b5c
--- /dev/null
+++ b/web/src/layouts/backend/components/navBar/double.vue
@@ -0,0 +1,97 @@
+
+
+
+
+
+
+
diff --git a/web/src/layouts/backend/components/navBar/tabs.vue b/web/src/layouts/backend/components/navBar/tabs.vue
new file mode 100644
index 0000000..1a7344f
--- /dev/null
+++ b/web/src/layouts/backend/components/navBar/tabs.vue
@@ -0,0 +1,263 @@
+
+
+
+ {{ item.meta.title }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/layouts/backend/components/navMenus.vue b/web/src/layouts/backend/components/navMenus.vue
new file mode 100644
index 0000000..78d5cda
--- /dev/null
+++ b/web/src/layouts/backend/components/navMenus.vue
@@ -0,0 +1,329 @@
+
+
+
+
+
+
+
diff --git a/web/src/layouts/backend/container/classic.vue b/web/src/layouts/backend/container/classic.vue
new file mode 100644
index 0000000..e9d2c1a
--- /dev/null
+++ b/web/src/layouts/backend/container/classic.vue
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/layouts/backend/container/default.vue b/web/src/layouts/backend/container/default.vue
new file mode 100644
index 0000000..e9d2c1a
--- /dev/null
+++ b/web/src/layouts/backend/container/default.vue
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/layouts/backend/container/double.vue b/web/src/layouts/backend/container/double.vue
new file mode 100644
index 0000000..e9d2c1a
--- /dev/null
+++ b/web/src/layouts/backend/container/double.vue
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/layouts/backend/container/streamline.vue b/web/src/layouts/backend/container/streamline.vue
new file mode 100644
index 0000000..543db54
--- /dev/null
+++ b/web/src/layouts/backend/container/streamline.vue
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/layouts/backend/index.vue b/web/src/layouts/backend/index.vue
new file mode 100644
index 0000000..ac8556d
--- /dev/null
+++ b/web/src/layouts/backend/index.vue
@@ -0,0 +1,116 @@
+
+
+
+
+
diff --git a/web/src/layouts/backend/router-view/main.vue b/web/src/layouts/backend/router-view/main.vue
new file mode 100644
index 0000000..cc87819
--- /dev/null
+++ b/web/src/layouts/backend/router-view/main.vue
@@ -0,0 +1,105 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/layouts/common/components/darkSwitch.vue b/web/src/layouts/common/components/darkSwitch.vue
new file mode 100644
index 0000000..5a11dd7
--- /dev/null
+++ b/web/src/layouts/common/components/darkSwitch.vue
@@ -0,0 +1,77 @@
+
+
+
+
+
diff --git a/web/src/layouts/common/components/loading.vue b/web/src/layouts/common/components/loading.vue
new file mode 100644
index 0000000..bd9d2c3
--- /dev/null
+++ b/web/src/layouts/common/components/loading.vue
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
diff --git a/web/src/layouts/common/router-view/iframe.vue b/web/src/layouts/common/router-view/iframe.vue
new file mode 100644
index 0000000..dc26907
--- /dev/null
+++ b/web/src/layouts/common/router-view/iframe.vue
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
diff --git a/web/src/layouts/frontend/components/aside.vue b/web/src/layouts/frontend/components/aside.vue
new file mode 100644
index 0000000..101156e
--- /dev/null
+++ b/web/src/layouts/frontend/components/aside.vue
@@ -0,0 +1,160 @@
+
+
+
+
+
+
+
+
{{ userInfo.nickname }}
+
+
+ {{ $t('Integral') + ' ' + userInfo.score }}
+
+
+ {{ $t('Balance') + ' ' + userInfo.money }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/layouts/frontend/components/footer.vue b/web/src/layouts/frontend/components/footer.vue
new file mode 100644
index 0000000..38230a5
--- /dev/null
+++ b/web/src/layouts/frontend/components/footer.vue
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
diff --git a/web/src/layouts/frontend/components/header.vue b/web/src/layouts/frontend/components/header.vue
new file mode 100644
index 0000000..d9e1b66
--- /dev/null
+++ b/web/src/layouts/frontend/components/header.vue
@@ -0,0 +1,127 @@
+
+
+
+
+
+
+
diff --git a/web/src/layouts/frontend/components/main.vue b/web/src/layouts/frontend/components/main.vue
new file mode 100644
index 0000000..5990ab8
--- /dev/null
+++ b/web/src/layouts/frontend/components/main.vue
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/layouts/frontend/components/menu.vue b/web/src/layouts/frontend/components/menu.vue
new file mode 100644
index 0000000..919a56c
--- /dev/null
+++ b/web/src/layouts/frontend/components/menu.vue
@@ -0,0 +1,256 @@
+
+
+
+
+ {{ $t('Home') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('Member Center') }}
+
+
+
+
+
+
+
+
+
+
+ {{ $t('Logout login') }}
+
+
+
+
+ {{ $t('Member Center') }}
+
+
+
+
+
+ {{ $t('Language') }}
+
+
+
+ {{ item.value }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/layouts/frontend/components/menuSub.vue b/web/src/layouts/frontend/components/menuSub.vue
new file mode 100644
index 0000000..c741700
--- /dev/null
+++ b/web/src/layouts/frontend/components/menuSub.vue
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+ {{ item.meta?.title }}
+
+
+
+
+
+
+
+ {{ item.meta?.title }}
+
+
+
+
+
+
+
+
diff --git a/web/src/layouts/frontend/container/default.vue b/web/src/layouts/frontend/container/default.vue
new file mode 100644
index 0000000..3b49934
--- /dev/null
+++ b/web/src/layouts/frontend/container/default.vue
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/layouts/frontend/container/disable.vue b/web/src/layouts/frontend/container/disable.vue
new file mode 100644
index 0000000..be3122e
--- /dev/null
+++ b/web/src/layouts/frontend/container/disable.vue
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/layouts/frontend/user.vue b/web/src/layouts/frontend/user.vue
new file mode 100644
index 0000000..076ffc1
--- /dev/null
+++ b/web/src/layouts/frontend/user.vue
@@ -0,0 +1,84 @@
+
+
+
+
+
+
+
diff --git a/web/src/main.ts b/web/src/main.ts
new file mode 100644
index 0000000..57dcc16
--- /dev/null
+++ b/web/src/main.ts
@@ -0,0 +1,36 @@
+import { createApp } from 'vue'
+import App from './App.vue'
+import router from './router'
+import { loadLang } from '/@/lang/index'
+import { registerIcons } from '/@/utils/common'
+import ElementPlus from 'element-plus'
+import mitt from 'mitt'
+import pinia from '/@/stores/index'
+import { directives } from '/@/utils/directives'
+import 'element-plus/dist/index.css'
+import 'element-plus/theme-chalk/display.css'
+import 'font-awesome/css/font-awesome.min.css'
+import '/@/styles/index.scss'
+// modules import mark, Please do not remove.
+
+async function start() {
+ const app = createApp(App)
+ app.use(pinia)
+
+ // 全局语言包加载
+ await loadLang(app)
+
+ app.use(router)
+ app.use(ElementPlus)
+
+ // 全局注册
+ directives(app) // 指令
+ registerIcons(app) // icons
+
+ app.mount('#app')
+
+ // modules start mark, Please do not remove.
+
+ app.config.globalProperties.eventBus = mitt()
+}
+start()
diff --git a/web/src/router/index.ts b/web/src/router/index.ts
new file mode 100644
index 0000000..adbfbc0
--- /dev/null
+++ b/web/src/router/index.ts
@@ -0,0 +1,81 @@
+import { createRouter, createWebHashHistory } from 'vue-router'
+import NProgress from 'nprogress'
+import 'nprogress/nprogress.css'
+import staticRoutes from '/@/router/static'
+import { adminBaseRoutePath } from '/@/router/static/adminBase'
+import { loading } from '/@/utils/loading'
+import langAutoLoadMap from '/@/lang/autoload'
+import { mergeMessage } from '/@/lang/index'
+import { useConfig } from '/@/stores/config'
+import { isAdminApp } from '/@/utils/common'
+import { uniq } from 'lodash-es'
+
+const router = createRouter({
+ history: createWebHashHistory(),
+ routes: staticRoutes,
+})
+
+router.beforeEach((to, from, next) => {
+ NProgress.configure({ showSpinner: false })
+ NProgress.start()
+ if (!window.existLoading) {
+ loading.show()
+ window.existLoading = true
+ }
+
+ // 按需动态加载页面的语言包-start
+ let loadPath: string[] = []
+ const config = useConfig()
+ if (to.path in langAutoLoadMap) {
+ loadPath.push(...langAutoLoadMap[to.path as keyof typeof langAutoLoadMap])
+ }
+ let prefix = ''
+ if (isAdminApp(to.fullPath)) {
+ prefix = './backend/' + config.lang.defaultLang
+
+ // 去除 path 中的 /admin
+ const adminPath = to.path.slice(to.path.indexOf(adminBaseRoutePath) + adminBaseRoutePath.length)
+ if (adminPath) loadPath.push(prefix + adminPath + '.ts')
+ } else {
+ prefix = './frontend/' + config.lang.defaultLang
+ loadPath.push(prefix + to.path + '.ts')
+ }
+
+ // 根据路由 name 加载的语言包
+ if (to.name) {
+ loadPath.push(prefix + '/' + to.name.toString() + '.ts')
+ }
+
+ if (!window.loadLangHandle.publicMessageLoaded) window.loadLangHandle.publicMessageLoaded = []
+ const publicMessagePath = prefix + '.ts'
+ if (!window.loadLangHandle.publicMessageLoaded.includes(publicMessagePath)) {
+ loadPath.push(publicMessagePath)
+ window.loadLangHandle.publicMessageLoaded.push(publicMessagePath)
+ }
+
+ // 去重
+ loadPath = uniq(loadPath)
+
+ for (const key in loadPath) {
+ loadPath[key] = loadPath[key].replaceAll('${lang}', config.lang.defaultLang)
+ if (loadPath[key] in window.loadLangHandle) {
+ window.loadLangHandle[loadPath[key]]().then((res: { default: anyObj }) => {
+ const pathName = loadPath[key].slice(loadPath[key].lastIndexOf(prefix) + (prefix.length + 1), loadPath[key].lastIndexOf('.'))
+ mergeMessage(res.default, pathName)
+ })
+ }
+ }
+ // 动态加载语言包-end
+
+ next()
+})
+
+// 路由加载后
+router.afterEach(() => {
+ if (window.existLoading) {
+ loading.hide()
+ }
+ NProgress.done()
+})
+
+export default router
diff --git a/web/src/router/static.ts b/web/src/router/static.ts
new file mode 100644
index 0000000..e126270
--- /dev/null
+++ b/web/src/router/static.ts
@@ -0,0 +1,100 @@
+import type { RouteRecordRaw } from 'vue-router'
+import { adminBaseRoutePath } from '/@/router/static/adminBase'
+import { memberCenterBaseRoutePath } from '/@/router/static/memberCenterBase'
+
+const pageTitle = (name: string): string => {
+ return `pagesTitle.${name}`
+}
+
+/*
+ * 静态路由
+ * 自动加载 ./static 目录的所有文件,并 push 到以下数组
+ */
+const staticRoutes: Array = [
+ {
+ // 首页
+ path: '/',
+ name: '/',
+ component: () => import('/@/views/frontend/index.vue'),
+ meta: {
+ title: pageTitle('home'),
+ },
+ },
+ {
+ // 管理员登录页 - 不放在 adminBaseRoute.children 因为登录页不需要使用后台的布局
+ path: adminBaseRoutePath + '/login',
+ name: 'adminLogin',
+ component: () => import('/@/views/backend/login.vue'),
+ meta: {
+ title: pageTitle('adminLogin'),
+ },
+ },
+ {
+ // 会员登录页
+ path: memberCenterBaseRoutePath + '/login',
+ name: 'userLogin',
+ component: () => import('/@/views/frontend/user/login.vue'),
+ meta: {
+ title: pageTitle('userLogin'),
+ },
+ },
+ {
+ path: '/:path(.*)*',
+ redirect: '/404',
+ },
+ {
+ // 404
+ path: '/404',
+ name: 'notFound',
+ component: () => import('/@/views/common/error/404.vue'),
+ meta: {
+ title: pageTitle('notFound'), // 页面不存在
+ },
+ },
+ {
+ // 后台找不到页面了-可能是路由未加载上
+ path: adminBaseRoutePath + ':path(.*)*',
+ redirect: (to) => {
+ return {
+ name: 'adminMainLoading',
+ params: {
+ to: JSON.stringify({
+ path: to.path,
+ query: to.query,
+ }),
+ },
+ }
+ },
+ },
+ {
+ // 会员中心找不到页面了
+ path: memberCenterBaseRoutePath + ':path(.*)*',
+ redirect: (to) => {
+ return {
+ name: 'userMainLoading',
+ params: {
+ to: JSON.stringify({
+ path: to.path,
+ query: to.query,
+ }),
+ },
+ }
+ },
+ },
+ {
+ // 无权限访问
+ path: '/401',
+ name: 'noPower',
+ component: () => import('/@/views/common/error/401.vue'),
+ meta: {
+ title: pageTitle('noPower'),
+ },
+ },
+]
+
+const staticFiles: Record> = import.meta.glob('./static/*.ts', { eager: true })
+for (const key in staticFiles) {
+ if (staticFiles[key].default) staticRoutes.push(staticFiles[key].default)
+}
+
+export default staticRoutes
diff --git a/web/src/router/static/adminBase.ts b/web/src/router/static/adminBase.ts
new file mode 100644
index 0000000..c98e818
--- /dev/null
+++ b/web/src/router/static/adminBase.ts
@@ -0,0 +1,33 @@
+import type { RouteRecordRaw } from 'vue-router'
+
+/**
+ * 后台基础路由路径
+ * 您可以随时于后台->系统配置中修改此值,程序可自动完成代码修改,同时建立对应的API入口和禁止admin应用访问
+ */
+export const adminBaseRoutePath = '/admin'
+
+/*
+ * 后台基础静态路由
+ */
+const adminBaseRoute: RouteRecordRaw = {
+ path: adminBaseRoutePath,
+ name: 'admin',
+ component: () => import('/@/layouts/backend/index.vue'),
+ // 直接重定向到 loading 路由
+ redirect: adminBaseRoutePath + '/loading',
+ meta: {
+ title: `pagesTitle.admin`,
+ },
+ children: [
+ {
+ path: 'loading/:to?',
+ name: 'adminMainLoading',
+ component: () => import('/@/layouts/common/components/loading.vue'),
+ meta: {
+ title: `pagesTitle.loading`,
+ },
+ },
+ ],
+}
+
+export default adminBaseRoute
diff --git a/web/src/router/static/memberCenterBase.ts b/web/src/router/static/memberCenterBase.ts
new file mode 100644
index 0000000..38a7265
--- /dev/null
+++ b/web/src/router/static/memberCenterBase.ts
@@ -0,0 +1,32 @@
+import type { RouteRecordRaw } from 'vue-router'
+
+/**
+ * 会员中心基础路由路径
+ */
+export const memberCenterBaseRoutePath = '/user'
+
+/*
+ * 会员中心基础静态路由
+ */
+const memberCenterBaseRoute: RouteRecordRaw = {
+ path: memberCenterBaseRoutePath,
+ name: 'user',
+ component: () => import('/@/layouts/frontend/user.vue'),
+ // 重定向到 loading 路由
+ redirect: memberCenterBaseRoutePath + '/loading',
+ meta: {
+ title: `pagesTitle.user`,
+ },
+ children: [
+ {
+ path: 'loading/:to?',
+ name: 'userMainLoading',
+ component: () => import('/@/layouts/common/components/loading.vue'),
+ meta: {
+ title: `pagesTitle.loading`,
+ },
+ },
+ ],
+}
+
+export default memberCenterBaseRoute
diff --git a/web/src/stores/adminInfo.ts b/web/src/stores/adminInfo.ts
new file mode 100644
index 0000000..65ffd10
--- /dev/null
+++ b/web/src/stores/adminInfo.ts
@@ -0,0 +1,57 @@
+import { defineStore } from 'pinia'
+import { ADMIN_INFO } from '/@/stores/constant/cacheKey'
+import type { AdminInfo } from '/@/stores/interface'
+
+export const useAdminInfo = defineStore('adminInfo', {
+ state: (): AdminInfo => {
+ return {
+ id: 0,
+ username: '',
+ nickname: '',
+ avatar: '',
+ last_login_time: '',
+ token: '',
+ refresh_token: '',
+ super: false,
+ }
+ },
+ actions: {
+ /**
+ * 状态批量填充
+ * @param state 新状态数据
+ * @param [exclude=true] 是否排除某些字段(忽略填充),默认值 true 排除 token 和 refresh_token,传递 false 则不排除,还可传递 string[] 指定排除字段列表
+ */
+ dataFill(state: Partial, exclude: boolean | string[] = true) {
+ if (exclude === true) {
+ exclude = ['token', 'refresh_token']
+ } else if (exclude === false) {
+ exclude = []
+ }
+
+ if (Array.isArray(exclude)) {
+ exclude.forEach((item) => {
+ delete state[item as keyof AdminInfo]
+ })
+ }
+
+ this.$patch(state)
+ },
+ removeToken() {
+ this.token = ''
+ this.refresh_token = ''
+ },
+ setToken(token: string, type: 'auth' | 'refresh') {
+ const field = type == 'auth' ? 'token' : 'refresh_token'
+ this[field] = token
+ },
+ getToken(type: 'auth' | 'refresh' = 'auth') {
+ return type === 'auth' ? this.token : this.refresh_token
+ },
+ setSuper(val: boolean) {
+ this.super = val
+ },
+ },
+ persist: {
+ key: ADMIN_INFO,
+ },
+})
diff --git a/web/src/stores/baAccount.ts b/web/src/stores/baAccount.ts
new file mode 100644
index 0000000..458ff7f
--- /dev/null
+++ b/web/src/stores/baAccount.ts
@@ -0,0 +1,82 @@
+import { defineStore } from 'pinia'
+import router from '../router'
+import { baAccountLogout } from '/@/api/backend/index'
+import { BA_ACCOUNT } from '/@/stores/constant/cacheKey'
+import type { UserInfo } from '/@/stores/interface'
+import { Local } from '/@/utils/storage'
+
+export const useBaAccount = defineStore('baAccount', {
+ state: (): Partial => {
+ return {
+ id: 0,
+ username: '',
+ nickname: '',
+ email: '',
+ mobile: '',
+ avatar: '',
+ gender: 0,
+ birthday: '',
+ money: 0,
+ score: 0,
+ motto: '',
+ token: '',
+ refresh_token: '',
+ }
+ },
+ actions: {
+ /**
+ * 状态批量填充
+ * @param state 新状态数据
+ * @param [exclude=true] 是否排除某些字段(忽略填充),默认值 true 排除 token 和 refresh_token,传递 false 则不排除,还可传递 string[] 指定排除字段列表
+ */
+ dataFill(state: Partial, exclude: boolean | string[] = true) {
+ if (exclude === true) {
+ exclude = ['token', 'refresh_token']
+ } else if (exclude === false) {
+ exclude = []
+ }
+
+ if (Array.isArray(exclude)) {
+ exclude.forEach((item) => {
+ delete state[item as keyof UserInfo]
+ })
+ }
+
+ this.$patch(state)
+ },
+ removeToken() {
+ this.token = ''
+ this.refresh_token = ''
+ },
+ getGenderIcon() {
+ let icon = { name: 'fa fa-transgender-alt', color: 'var(--el-text-color-secondary)' }
+ switch (this.gender) {
+ case 1:
+ icon = { name: 'fa fa-mars-stroke-v', color: 'var(--el-color-primary)' }
+ break
+ case 2:
+ icon = { name: 'fa fa-mars-stroke', color: 'var(--el-color-danger)' }
+ break
+ }
+ return icon
+ },
+ setToken(token: string, type: 'auth' | 'refresh') {
+ const field = type == 'auth' ? 'token' : 'refresh_token'
+ this[field] = token
+ },
+ getToken(type: 'auth' | 'refresh' = 'auth') {
+ return type === 'auth' ? this.token : this.refresh_token
+ },
+ logout() {
+ baAccountLogout().then((res) => {
+ if (res.code == 1) {
+ Local.remove(BA_ACCOUNT)
+ router.go(0)
+ }
+ })
+ },
+ },
+ persist: {
+ key: BA_ACCOUNT,
+ },
+})
diff --git a/web/src/stores/config.ts b/web/src/stores/config.ts
new file mode 100644
index 0000000..1894c5f
--- /dev/null
+++ b/web/src/stores/config.ts
@@ -0,0 +1,111 @@
+import { defineStore } from 'pinia'
+import { reactive } from 'vue'
+import { STORE_CONFIG } from '/@/stores/constant/cacheKey'
+import type { Crud, Lang, Layout } from '/@/stores/interface'
+
+export const useConfig = defineStore(
+ 'config',
+ () => {
+ const layout: Layout = reactive({
+ // 全局
+ showDrawer: false,
+ shrink: false,
+ layoutMode: 'Default',
+ mainAnimation: 'slide-right',
+ isDark: false,
+
+ // 侧边栏
+ menuBackground: ['#ffffff', '#1d1e1f'],
+ menuColor: ['#303133', '#CFD3DC'],
+ menuActiveBackground: ['#ffffff', '#1d1e1f'],
+ menuActiveColor: ['#409eff', '#3375b9'],
+ menuTopBarBackground: ['#fcfcfc', '#1d1e1f'],
+ menuWidth: 260,
+ menuDefaultIcon: 'fa fa-circle-o',
+ menuCollapse: false,
+ menuUniqueOpened: false,
+ menuShowTopBar: true,
+
+ // 顶栏
+ headerBarTabColor: ['#000000', '#CFD3DC'],
+ headerBarTabActiveBackground: ['#ffffff', '#1d1e1f'],
+ headerBarTabActiveColor: ['#000000', '#409EFF'],
+ headerBarBackground: ['#ffffff', '#1d1e1f'],
+ headerBarHoverBackground: ['#f5f5f5', '#18222c'],
+ })
+
+ const lang: Lang = reactive({
+ defaultLang: 'zh-cn',
+ fallbackLang: 'zh-cn',
+ langArray: [
+ { name: 'zh-cn', value: '中文简体' },
+ { name: 'en', value: 'English' },
+ ],
+ })
+
+ const crud: Crud = reactive({
+ syncType: 'manual',
+ syncedUpdate: 'yes',
+ syncAutoPublic: 'no',
+ })
+
+ function menuWidth() {
+ if (layout.shrink) {
+ return layout.menuCollapse ? '0px' : layout.menuWidth + 'px'
+ }
+ // 菜单是否折叠
+ return layout.menuCollapse ? '64px' : layout.menuWidth + 'px'
+ }
+
+ function setLang(val: string) {
+ lang.defaultLang = val
+ }
+
+ function onSetLayoutColor(data = layout.layoutMode) {
+ // 切换布局时,如果是为默认配色方案,对菜单激活背景色重新赋值
+ const tempValue = layout.isDark ? { idx: 1, color: '#1d1e1f', newColor: '#141414' } : { idx: 0, color: '#ffffff', newColor: '#f5f5f5' }
+ if (
+ data == 'Classic' &&
+ layout.headerBarBackground[tempValue.idx] == tempValue.color &&
+ layout.headerBarTabActiveBackground[tempValue.idx] == tempValue.color
+ ) {
+ layout.headerBarTabActiveBackground[tempValue.idx] = tempValue.newColor
+ } else if (
+ data == 'Default' &&
+ layout.headerBarBackground[tempValue.idx] == tempValue.color &&
+ layout.headerBarTabActiveBackground[tempValue.idx] == tempValue.newColor
+ ) {
+ layout.headerBarTabActiveBackground[tempValue.idx] = tempValue.color
+ }
+ }
+
+ function setLayoutMode(data: string) {
+ layout.layoutMode = data
+ onSetLayoutColor(data)
+ }
+
+ const setLayout = (name: keyof Layout, value: any) => {
+ ;(layout[name] as any) = value
+ }
+
+ const getColorVal = function (name: keyof Layout): string {
+ const colors = layout[name] as string[]
+ if (layout.isDark) {
+ return colors[1]
+ } else {
+ return colors[0]
+ }
+ }
+
+ const setCrud = (name: keyof Crud, value: any) => {
+ ;(crud[name] as any) = value
+ }
+
+ return { layout, lang, crud, menuWidth, setLang, setLayoutMode, setLayout, getColorVal, onSetLayoutColor, setCrud }
+ },
+ {
+ persist: {
+ key: STORE_CONFIG,
+ },
+ }
+)
diff --git a/web/src/stores/constant/cacheKey.ts b/web/src/stores/constant/cacheKey.ts
new file mode 100644
index 0000000..64f2276
--- /dev/null
+++ b/web/src/stores/constant/cacheKey.ts
@@ -0,0 +1,25 @@
+/**
+ * 本地缓存Key
+ */
+
+// 管理员资料
+export const ADMIN_INFO = 'adminInfo'
+
+// WEB端布局配置
+export const STORE_CONFIG = 'storeConfig_v2'
+// 后台标签页
+export const STORE_TAB_VIEW_CONFIG = 'storeTabViewConfig'
+// 终端
+export const STORE_TERMINAL = 'storeTerminal'
+
+// 工作时间
+export const WORKING_TIME = 'workingTime'
+
+// 切换到手机端前的上次布局方式
+export const BEFORE_RESIZE_LAYOUT = 'beforeResizeLayout'
+
+// 会员资料
+export const USER_INFO = 'userInfo'
+
+// ba官网用户信息
+export const BA_ACCOUNT = 'ba_account'
diff --git a/web/src/stores/constant/common.ts b/web/src/stores/constant/common.ts
new file mode 100644
index 0000000..87eccc3
--- /dev/null
+++ b/web/src/stores/constant/common.ts
@@ -0,0 +1,8 @@
+/**
+ * 公共常量定义
+ */
+
+/**
+ * 系统级 z-index 配置,比如全局通知消息的 z-index(浏览器支持的最大值通常为 2147483647)
+ */
+export const SYSTEM_ZINDEX = 2147483600
diff --git a/web/src/stores/constant/terminalTaskStatus.ts b/web/src/stores/constant/terminalTaskStatus.ts
new file mode 100644
index 0000000..d564597
--- /dev/null
+++ b/web/src/stores/constant/terminalTaskStatus.ts
@@ -0,0 +1,8 @@
+export const enum taskStatus {
+ Waiting,
+ Connecting,
+ Executing,
+ Success,
+ Failed,
+ Unknown,
+}
diff --git a/web/src/stores/index.ts b/web/src/stores/index.ts
new file mode 100644
index 0000000..e952ed8
--- /dev/null
+++ b/web/src/stores/index.ts
@@ -0,0 +1,7 @@
+import { createPinia } from 'pinia'
+import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
+
+const pinia = createPinia()
+pinia.use(piniaPluginPersistedstate)
+
+export default pinia
diff --git a/web/src/stores/interface/index.ts b/web/src/stores/interface/index.ts
new file mode 100644
index 0000000..5307183
--- /dev/null
+++ b/web/src/stores/interface/index.ts
@@ -0,0 +1,203 @@
+import type { RouteLocationNormalized, RouteRecordRaw } from 'vue-router'
+
+export interface Layout {
+ /* 全局 - s */
+ // 是否显示布局配置抽屉
+ showDrawer: boolean
+ // 是否收缩布局(小屏设备)
+ shrink: boolean
+ // 后台布局方式,可选值
+ layoutMode: string
+ // 后台主页面切换动画,可选值
+ mainAnimation: string
+ // 是否暗黑模式
+ isDark: boolean
+ /* 全局 - e */
+
+ /* 侧边栏 - s */
+ // 侧边菜单宽度(展开时),单位px
+ menuWidth: number
+ // 侧边菜单项默认图标
+ menuDefaultIcon: string
+ // 是否水平折叠收起菜单
+ menuCollapse: boolean
+ // 是否只保持一个子菜单的展开(手风琴)
+ menuUniqueOpened: boolean
+ // 显示菜单栏顶栏(LOGO)
+ menuShowTopBar: boolean
+ // 侧边菜单背景色
+ menuBackground: string[]
+ // 侧边菜单文字颜色
+ menuColor: string[]
+ // 侧边菜单激活项背景色
+ menuActiveBackground: string[]
+ // 侧边菜单激活项文字色
+ menuActiveColor: string[]
+ // 侧边菜单顶栏背景色
+ menuTopBarBackground: string[]
+ /* 侧边栏 - e */
+
+ /* 顶栏 - s */
+ // 顶栏文字色
+ headerBarTabColor: string[]
+ // 顶栏背景色
+ headerBarBackground: string[]
+ // 顶栏悬停时背景色
+ headerBarHoverBackground: string[]
+ // 顶栏激活项背景色
+ headerBarTabActiveBackground: string[]
+ // 顶栏激活项文字色
+ headerBarTabActiveColor: string[]
+ /* 顶栏 - e */
+}
+
+export interface Lang {
+ // 默认语言,可选值
+ defaultLang: string
+ // 当在默认语言包找不到翻译时,继续在 fallbackLang 语言包内查找翻译
+ fallbackLang: string
+ // 支持的语言列表
+ langArray: { name: string; value: string }[]
+}
+
+export interface Crud {
+ // 日志同步方式
+ syncType: 'manual' | 'automatic'
+ // 已同步记录被更新时,是否自动重新同步
+ syncedUpdate: 'no' | 'yes'
+ // 自动同步时是否分享至开源社区
+ syncAutoPublic: 'no' | 'yes'
+}
+
+export interface NavTabs {
+ // 激活 tab 的 index
+ activeIndex: number
+ // 激活的 tab
+ activeRoute: RouteLocationNormalized | null
+ // tab 列表
+ tabsView: RouteLocationNormalized[]
+ // 当前 tab 是否全屏
+ tabFullScreen: boolean
+ // 从后台加载到的菜单路由列表
+ tabsViewRoutes: RouteRecordRaw[]
+ // 权限节点
+ authNode: Map
+}
+
+export interface MemberCenter {
+ // 是否开启会员中心
+ open: boolean
+ // 布局模式
+ layoutMode: string
+ // 从后台加载到的菜单
+ viewRoutes: RouteRecordRaw[]
+ // 是否显示一级菜单标题(当有多个一级菜单分组时显示)
+ showHeadline: boolean
+ // 权限节点
+ authNode: Map
+ // 收缩布局(小屏设备)
+ shrink: boolean
+ // 菜单展开状态(小屏设备)
+ menuExpand: boolean
+ // 顶栏会员菜单下拉项
+ navUserMenus: RouteRecordRaw[]
+}
+
+export interface AdminInfo {
+ id: number
+ username: string
+ nickname: string
+ avatar: string
+ last_login_time: string
+ token: string
+ refresh_token: string
+ // 是否是 superAdmin,用于判定是否显示终端按钮等,不做任何权限判断
+ super: boolean
+}
+
+export interface UserInfo {
+ id: number
+ username: string
+ nickname: string
+ email: string
+ mobile: string
+ gender: number
+ birthday: string
+ money: number
+ score: number
+ avatar: string
+ last_login_time: string
+ last_login_ip: string
+ join_time: string
+ motto: string
+ token: string
+ refresh_token: string
+}
+
+export interface TaskItem {
+ // 任务唯一标识
+ uuid: string
+ // 创建时间
+ createTime: string
+ // 状态
+ status: number
+ // 命令
+ command: string
+ // 命令执行日志
+ message: string[]
+ // 显示命令执行日志
+ showMessage: boolean
+ // 失败阻断后续命令执行
+ blockOnFailure: boolean
+ // 扩展信息,自动发送到后台
+ extend: string
+ // 执行结果回调
+ callback: Function
+}
+
+export interface Terminal {
+ // 显示终端窗口
+ show: boolean
+ // 在后台终端按钮上显示一个红点
+ showDot: boolean
+ // 任务列表
+ taskList: TaskItem[]
+ // 包管理器
+ packageManager: string
+ // 显示终端设置窗口
+ showConfig: boolean
+ // 开始任务时自动清理已完成任务
+ automaticCleanupTask: string
+ // PHP 开发服务环境
+ phpDevelopmentServer: boolean
+ // NPM 源
+ npmRegistry: string
+ // composer 源
+ composerRegistry: string
+}
+
+export interface SiteConfig {
+ // 站点名称
+ siteName: string
+ // 系统版本号
+ version: string
+ // 内容分发网络URL
+ cdnUrl: string
+ // 中心接口地址(用于请求模块市场的数据等用途)
+ apiUrl: string
+ // 上传配置
+ upload: {
+ mode: string
+ [key: string]: any
+ }
+ // 顶部导航菜单数据
+ headNav: RouteRecordRaw[]
+ // 备案号
+ recordNumber?: string
+ // 内容分发网络URL的参数,格式如 imageMogr2/format/heif
+ cdnUrlParams: string
+
+ // 初始化状态
+ initialize: boolean
+ userInitialize: boolean
+}
diff --git a/web/src/stores/memberCenter.ts b/web/src/stores/memberCenter.ts
new file mode 100644
index 0000000..237a66b
--- /dev/null
+++ b/web/src/stores/memberCenter.ts
@@ -0,0 +1,84 @@
+import { defineStore } from 'pinia'
+import { reactive } from 'vue'
+import type { RouteRecordRaw } from 'vue-router'
+import type { MemberCenter } from '/@/stores/interface/index'
+
+export const useMemberCenter = defineStore('memberCenter', () => {
+ const state: MemberCenter = reactive({
+ open: true,
+ layoutMode: 'Default',
+ viewRoutes: [],
+ showHeadline: false,
+ authNode: new Map(),
+ shrink: false,
+ menuExpand: false,
+ navUserMenus: [],
+ })
+
+ const setNavUserMenus = (menus: RouteRecordRaw[]) => {
+ state.navUserMenus = menus
+ }
+
+ const mergeNavUserMenus = (menus: RouteRecordRaw[]) => {
+ state.navUserMenus = [...state.navUserMenus, ...menus]
+ }
+
+ const setAuthNode = (key: string, data: string[]) => {
+ state.authNode.set(key, data)
+ }
+
+ const mergeAuthNode = (authNode: Map) => {
+ state.authNode = new Map([...state.authNode, ...authNode])
+ }
+
+ const setViewRoutes = (data: RouteRecordRaw[]): void => {
+ state.viewRoutes = encodeRoutesURI(data)
+ }
+
+ const setShowHeadline = (show: boolean): void => {
+ state.showHeadline = show
+ }
+
+ const setShrink = (shrink: boolean) => {
+ state.shrink = shrink
+ }
+
+ const setStatus = (status: boolean) => {
+ state.open = status
+ }
+
+ const setLayoutMode = (mode: string) => {
+ state.layoutMode = mode
+ }
+
+ const toggleMenuExpand = (expand = !state.menuExpand) => {
+ state.menuExpand = expand
+ }
+
+ return {
+ state,
+ setNavUserMenus,
+ mergeNavUserMenus,
+ setAuthNode,
+ mergeAuthNode,
+ setViewRoutes,
+ setShowHeadline,
+ setShrink,
+ setStatus,
+ setLayoutMode,
+ toggleMenuExpand,
+ }
+})
+
+function encodeRoutesURI(data: RouteRecordRaw[]) {
+ data.forEach((item) => {
+ if (item.meta?.menu_type == 'iframe') {
+ item.path = '/user/iframe/' + encodeURIComponent(item.path)
+ }
+
+ if (item.children && item.children.length) {
+ item.children = encodeRoutesURI(item.children)
+ }
+ })
+ return data
+}
diff --git a/web/src/stores/navTabs.ts b/web/src/stores/navTabs.ts
new file mode 100644
index 0000000..fbcb483
--- /dev/null
+++ b/web/src/stores/navTabs.ts
@@ -0,0 +1,245 @@
+import { isEmpty } from 'lodash-es'
+import { defineStore } from 'pinia'
+import { reactive } from 'vue'
+import type { RouteLocationNormalized, RouteRecordRaw } from 'vue-router'
+import { i18n } from '../lang'
+import { adminBaseRoutePath } from '/@/router/static/adminBase'
+import { STORE_TAB_VIEW_CONFIG } from '/@/stores/constant/cacheKey'
+import type { NavTabs } from '/@/stores/interface/index'
+import { layoutNavTabsRef } from '/@/stores/refs'
+
+export const useNavTabs = defineStore(
+ 'navTabs',
+ () => {
+ const state: NavTabs = reactive({
+ activeIndex: 0,
+ activeRoute: null,
+ tabsView: [],
+ tabFullScreen: false,
+ tabsViewRoutes: [],
+ authNode: new Map(),
+ })
+
+ /**
+ * 通过路由路径关闭tab
+ * @param fullPath 需要关闭的 tab 的路径
+ */
+ const closeTabByPath = (fullPath: string) => {
+ layoutNavTabsRef.value?.closeTabByPath(fullPath)
+ }
+
+ /**
+ * 关闭所有tab
+ * @param menu 需要保留的标签,否则关闭全部标签并打开第一个路由
+ */
+ const closeAllTab = (menu?: RouteLocationNormalized) => {
+ layoutNavTabsRef.value?.closeAllTab(menu)
+ }
+
+ /**
+ * 修改 tab 标题
+ * @param fullPath 需要修改标题的 tab 的路径
+ * @param title 新的标题
+ */
+ const updateTabTitle = (fullPath: string, title: string) => {
+ layoutNavTabsRef.value?.updateTabTitle(fullPath, title)
+ }
+
+ /**
+ * 添加 tab(内部)
+ * ps: router.push 时可自动完成 tab 添加,无需调用此方法
+ */
+ function _addTab(route: RouteLocationNormalized) {
+ const tabView = { ...route, matched: [], meta: { ...route.meta } }
+ if (!tabView.meta.addtab) return
+
+ // 通过路由寻找菜单的原始数据
+ const tabViewRoute = getTabsViewDataByRoute(tabView)
+ if (tabViewRoute && tabViewRoute.meta) {
+ tabView.name = tabViewRoute.name
+ tabView.meta.id = tabViewRoute.meta.id
+ tabView.meta.title = tabViewRoute.meta.title
+ }
+
+ for (const key in state.tabsView) {
+ // 菜单已在 tabs 存在,更新 params 和 query
+ if (state.tabsView[key].meta.id === tabView.meta.id || state.tabsView[key].fullPath == tabView.fullPath) {
+ state.tabsView[key].fullPath = tabView.fullPath
+ state.tabsView[key].params = !isEmpty(tabView.params) ? tabView.params : state.tabsView[key].params
+ state.tabsView[key].query = !isEmpty(tabView.query) ? tabView.query : state.tabsView[key].query
+ return
+ }
+ }
+ if (typeof tabView.meta.title == 'string') {
+ tabView.meta.title = i18n.global.te(tabView.meta.title) ? i18n.global.t(tabView.meta.title) : tabView.meta.title
+ }
+ state.tabsView.push(tabView)
+ }
+
+ /**
+ * 设置激活 tab(内部)
+ * ps: router.push 时可自动完成 tab 激活,无需调用此方法
+ */
+ const _setActiveRoute = (route: RouteLocationNormalized): void => {
+ const currentRouteIndex: number = state.tabsView.findIndex((item: RouteLocationNormalized) => {
+ return item.fullPath === route.fullPath
+ })
+ if (currentRouteIndex === -1) return
+ state.activeRoute = route
+ state.activeIndex = currentRouteIndex
+ }
+
+ /**
+ * 关闭 tab(内部)
+ * ps: 使用 closeTabByPath 代替
+ */
+ function _closeTab(route: RouteLocationNormalized) {
+ state.tabsView.map((v, k) => {
+ if (v.fullPath == route.fullPath) {
+ state.tabsView.splice(k, 1)
+ return
+ }
+ })
+ }
+
+ /**
+ * 关闭多个标签(内部)
+ * ps:使用 closeAllTab 代替
+ */
+ const _closeTabs = (retainMenu: RouteLocationNormalized | false = false) => {
+ if (retainMenu) {
+ state.tabsView = [retainMenu]
+ } else {
+ state.tabsView = []
+ }
+ }
+
+ /**
+ * 更新标签标题(内部)
+ * ps: 使用 updateTabTitle 代替
+ */
+ const _updateTabTitle = (fullPath: string, title: string) => {
+ for (const key in state.tabsView) {
+ if (state.tabsView[key].fullPath == fullPath) {
+ state.tabsView[key].meta.title = title
+ break
+ }
+ }
+ }
+
+ /**
+ * 设置从后台加载到的菜单路由列表
+ */
+ const setTabsViewRoutes = (data: RouteRecordRaw[]): void => {
+ state.tabsViewRoutes = encodeRoutesURI(data)
+ }
+
+ /**
+ * 以key设置权限节点
+ */
+ const setAuthNode = (key: string, data: string[]) => {
+ state.authNode.set(key, data)
+ }
+
+ /**
+ * 覆盖设置权限节点
+ */
+ const fillAuthNode = (data: Map) => {
+ state.authNode = data
+ }
+
+ /**
+ * 设置当前 tab 是否全屏
+ * @param status 全屏状态
+ */
+ const setFullScreen = (status: boolean): void => {
+ state.tabFullScreen = status
+ }
+
+ /**
+ * 寻找路由在菜单中的数据
+ * @param route 路由
+ * @param returnType 返回值要求:normal=返回被搜索的路径对应的菜单数据,above=返回被搜索的路径对应的上一级菜单数组
+ */
+ const getTabsViewDataByRoute = (route: RouteLocationNormalized, returnType: 'normal' | 'above' = 'normal'): RouteRecordRaw | false => {
+ // 以完整路径寻找
+ let found = getTabsViewDataByPath(route.fullPath, state.tabsViewRoutes, returnType)
+ if (found) {
+ found.meta!.matched = route.fullPath
+ return found
+ }
+
+ // 以路径寻找
+ found = getTabsViewDataByPath(route.path, state.tabsViewRoutes, returnType)
+ if (found) {
+ found.meta!.matched = route.path
+ return found
+ }
+
+ return false
+ }
+
+ /**
+ * 递归的寻找路由路径在菜单中的数据
+ * @param path 路由路径
+ * @param menus 菜单数据(只有 path 代表完整 url,没有 fullPath)
+ * @param returnType 返回值要求:normal=返回被搜索的路径对应的菜单数据,above=返回被搜索的路径对应的上一级菜单数组
+ */
+ const getTabsViewDataByPath = (path: string, menus: RouteRecordRaw[], returnType: 'normal' | 'above'): RouteRecordRaw | false => {
+ for (const key in menus) {
+ // 找到目标
+ if (menus[key].path === path) {
+ return menus[key]
+ }
+ // 从子级继续寻找
+ if (menus[key].children && menus[key].children.length) {
+ const find = getTabsViewDataByPath(path, menus[key].children, returnType)
+ if (find) {
+ return returnType == 'above' ? menus[key] : find
+ }
+ }
+ }
+ return false
+ }
+
+ return {
+ state,
+ closeAllTab,
+ closeTabByPath,
+ updateTabTitle,
+ setTabsViewRoutes,
+ setAuthNode,
+ fillAuthNode,
+ setFullScreen,
+ getTabsViewDataByPath,
+ getTabsViewDataByRoute,
+ _addTab,
+ _closeTab,
+ _closeTabs,
+ _setActiveRoute,
+ _updateTabTitle,
+ }
+ },
+ {
+ persist: {
+ key: STORE_TAB_VIEW_CONFIG,
+ pick: ['state.tabFullScreen'],
+ },
+ }
+)
+
+/**
+ * 对iframe的url进行编码
+ */
+function encodeRoutesURI(data: RouteRecordRaw[]) {
+ data.forEach((item) => {
+ if (item.meta?.menu_type == 'iframe') {
+ item.path = adminBaseRoutePath + '/iframe/' + encodeURIComponent(item.path)
+ }
+
+ if (item.children && item.children.length) {
+ item.children = encodeRoutesURI(item.children)
+ }
+ })
+ return data
+}
diff --git a/web/src/stores/refs.ts b/web/src/stores/refs.ts
new file mode 100644
index 0000000..80a169a
--- /dev/null
+++ b/web/src/stores/refs.ts
@@ -0,0 +1,34 @@
+/**
+ * references
+ * 全局提供:引用(指向)一些对象(组件)的句柄
+ */
+import type { ScrollbarInstance } from 'element-plus'
+import type { CSSProperties } from 'vue'
+import { computed, ref } from 'vue'
+import NavTabs from '/@/layouts/backend/components/navBar/tabs.vue'
+import { mainHeight } from '/@/utils/layout'
+
+/**
+ * 后台顶栏(tabs)组件ref(仅默认和经典布局)
+ */
+export const layoutNavTabsRef = ref>()
+
+/**
+ * 前后台布局的主体的滚动条组件ref
+ */
+export const layoutMainScrollbarRef = ref()
+
+/**
+ * 前后台布局的主体滚动条的额外样式,包括高度
+ */
+export const layoutMainScrollbarStyle = computed(() => mainHeight())
+
+/**
+ * 前后台布局的菜单组件ref
+ */
+export const layoutMenuRef = ref()
+
+/**
+ * 前后台布局的菜单栏滚动条组件ref
+ */
+export const layoutMenuScrollbarRef = ref()
diff --git a/web/src/stores/siteConfig.ts b/web/src/stores/siteConfig.ts
new file mode 100644
index 0000000..6829675
--- /dev/null
+++ b/web/src/stores/siteConfig.ts
@@ -0,0 +1,37 @@
+import { defineStore } from 'pinia'
+import type { RouteRecordRaw } from 'vue-router'
+import type { SiteConfig } from '/@/stores/interface'
+
+export const useSiteConfig = defineStore('siteConfig', {
+ state: (): SiteConfig => {
+ return {
+ siteName: '',
+ version: '',
+ cdnUrl: '',
+ apiUrl: '',
+ upload: {
+ mode: 'local',
+ },
+ headNav: [],
+ recordNumber: '',
+ cdnUrlParams: '',
+ initialize: false,
+ userInitialize: false,
+ }
+ },
+ actions: {
+ dataFill(state: SiteConfig) {
+ // 使用 this.$patch(state) 时 headNav 的类型异常,直接赋值
+ this.$state = state
+ },
+ setHeadNav(headNav: RouteRecordRaw[]) {
+ this.headNav = headNav
+ },
+ setInitialize(initialize: boolean) {
+ this.initialize = initialize
+ },
+ setUserInitialize(userInitialize: boolean) {
+ this.userInitialize = userInitialize
+ },
+ },
+})
diff --git a/web/src/stores/terminal.ts b/web/src/stores/terminal.ts
new file mode 100644
index 0000000..034e5ad
--- /dev/null
+++ b/web/src/stores/terminal.ts
@@ -0,0 +1,292 @@
+import { ElNotification } from 'element-plus'
+import { defineStore } from 'pinia'
+import { nextTick, reactive } from 'vue'
+import { buildTerminalUrl } from '/@/api/common'
+import { i18n } from '/@/lang/index'
+import { STORE_TERMINAL } from '/@/stores/constant/cacheKey'
+import { SYSTEM_ZINDEX } from '/@/stores/constant/common'
+import { taskStatus } from '/@/stores/constant/terminalTaskStatus'
+import type { Terminal } from '/@/stores/interface/index'
+import { timeFormat } from '/@/utils/common'
+import { uuid } from '/@/utils/random'
+import { closeHotUpdate, openHotUpdate } from '/@/utils/vite'
+
+export const useTerminal = defineStore(
+ 'terminal',
+ () => {
+ const state: Terminal = reactive({
+ show: false,
+ showDot: false,
+ taskList: [],
+ packageManager: 'pnpm',
+ showConfig: false,
+ automaticCleanupTask: '1',
+ phpDevelopmentServer: false,
+ npmRegistry: 'unknown',
+ composerRegistry: 'unknown',
+ })
+
+ function init() {
+ for (const key in state.taskList) {
+ if (state.taskList[key].status == taskStatus.Connecting || state.taskList[key].status == taskStatus.Executing) {
+ state.taskList[key].status = taskStatus.Unknown
+ }
+ }
+ }
+
+ function toggle(val = !state.show) {
+ state.show = val
+ if (val) {
+ toggleDot(false)
+ }
+ }
+
+ function toggleDot(val = !state.showDot) {
+ state.showDot = val
+ }
+
+ function toggleConfigDialog(val = !state.showConfig) {
+ toggle(!val)
+ state.showConfig = val
+ }
+
+ function changeRegistry(val: string, type: 'npm' | 'composer') {
+ state[type == 'npm' ? 'npmRegistry' : 'composerRegistry'] = val
+ }
+
+ function changePackageManager(val: string) {
+ state.packageManager = val
+ }
+
+ function changePHPDevelopmentServer(val: boolean) {
+ state.phpDevelopmentServer = val
+ }
+
+ function changeAutomaticCleanupTask(val: '0' | '1') {
+ state.automaticCleanupTask = val
+ }
+
+ function setTaskStatus(idx: number, status: number) {
+ state.taskList[idx].status = status
+ if ((status == taskStatus.Failed || status == taskStatus.Unknown) && state.taskList[idx].blockOnFailure) {
+ setTaskShowMessage(idx, true)
+ }
+ }
+
+ function taskCompleted(idx: number) {
+ // 命令执行完毕,重新打开热更新
+ openHotUpdate('terminal')
+
+ if (typeof state.taskList[idx].callback != 'function') return
+
+ const status = state.taskList[idx].status
+ if (status == taskStatus.Failed || status == taskStatus.Unknown) {
+ state.taskList[idx].callback(taskStatus.Failed)
+ } else if (status == taskStatus.Success) {
+ state.taskList[idx].callback(taskStatus.Success)
+ }
+ }
+
+ function setTaskShowMessage(idx: number, val = !state.taskList[idx].showMessage) {
+ state.taskList[idx].showMessage = val
+ }
+
+ function addTaskMessage(idx: number, message: string) {
+ if (!state.show) toggleDot(true)
+ state.taskList[idx].message = state.taskList[idx].message.concat(message)
+ nextTick(() => {
+ execMessageScrollbarKeep(state.taskList[idx].uuid)
+ })
+ }
+
+ function addTask(command: string, blockOnFailure = true, extend = '', callback: Function = () => {}) {
+ if (!state.show) toggleDot(true)
+ state.taskList = state.taskList.concat({
+ uuid: uuid(),
+ createTime: timeFormat(),
+ status: taskStatus.Waiting,
+ command: command,
+ message: [],
+ showMessage: false,
+ blockOnFailure: blockOnFailure,
+ extend: extend,
+ callback: callback,
+ })
+
+ // 清理任务列表
+ if (parseInt(state.automaticCleanupTask) === 1) {
+ clearSuccessTask()
+ }
+
+ // 检查是否有已经失败的任务
+ if (state.show === false) {
+ for (const key in state.taskList) {
+ if (state.taskList[key].status == taskStatus.Failed || state.taskList[key].status == taskStatus.Unknown) {
+ ElNotification({
+ type: 'error',
+ message: i18n.global.t('terminal.Newly added tasks will never start because they are blocked by failed tasks'),
+ zIndex: SYSTEM_ZINDEX,
+ })
+ break
+ }
+ }
+ }
+
+ startTask()
+ }
+
+ function addTaskPM(command: string, blockOnFailure = true, extend = '', callback: Function = () => {}) {
+ addTask(command + '.' + state.packageManager, blockOnFailure, extend, callback)
+ }
+
+ function delTask(idx: number) {
+ if (state.taskList[idx].status != taskStatus.Connecting && state.taskList[idx].status != taskStatus.Executing) {
+ state.taskList.splice(idx, 1)
+ }
+ startTask()
+ }
+
+ function startTask() {
+ let taskKey = null
+
+ // 寻找可以开始执行的命令
+ for (const key in state.taskList) {
+ if (state.taskList[key].status == taskStatus.Waiting) {
+ taskKey = parseInt(key)
+ break
+ }
+ if (state.taskList[key].status == taskStatus.Connecting || state.taskList[key].status == taskStatus.Executing) {
+ break
+ }
+ if (state.taskList[key].status == taskStatus.Success) {
+ continue
+ }
+ if (state.taskList[key].status == taskStatus.Failed || state.taskList[key].status == taskStatus.Unknown) {
+ if (state.taskList[key].blockOnFailure) {
+ break
+ } else {
+ continue
+ }
+ }
+ }
+ if (taskKey !== null) {
+ setTaskStatus(taskKey, taskStatus.Connecting)
+ startEventSource(taskKey)
+ }
+ }
+
+ function startEventSource(taskKey: number) {
+ // 命令执行期间禁用热更新
+ closeHotUpdate('terminal')
+
+ window.eventSource = new EventSource(
+ buildTerminalUrl(state.taskList[taskKey].command, state.taskList[taskKey].uuid, state.taskList[taskKey].extend)
+ )
+ window.eventSource.onmessage = function (e) {
+ const data = JSON.parse(e.data)
+ if (!data || !data.data) {
+ return
+ }
+
+ const taskIdx = findTaskIdxFromUuid(data.uuid)
+ if (taskIdx === false) {
+ return
+ }
+
+ if (data.data == 'command-exec-error') {
+ setTaskStatus(taskIdx, taskStatus.Failed)
+ window.eventSource.close()
+ taskCompleted(taskIdx)
+ startTask()
+ } else if (data.data == 'command-exec-completed') {
+ window.eventSource.close()
+ if (state.taskList[taskIdx].status != taskStatus.Success) {
+ setTaskStatus(taskIdx, taskStatus.Failed)
+ }
+ taskCompleted(taskIdx)
+ startTask()
+ } else if (data.data == 'command-link-success') {
+ setTaskStatus(taskIdx, taskStatus.Executing)
+ } else if (data.data == 'command-exec-success') {
+ setTaskStatus(taskIdx, taskStatus.Success)
+ } else {
+ addTaskMessage(taskIdx, data.data)
+ }
+ }
+ window.eventSource.onerror = function () {
+ window.eventSource.close()
+ const taskIdx = findTaskIdxFromGuess(taskKey)
+ if (taskIdx === false) return
+ setTaskStatus(taskIdx, taskStatus.Failed)
+ taskCompleted(taskIdx)
+ }
+ }
+
+ function retryTask(idx: number) {
+ state.taskList[idx].message = []
+ setTaskStatus(idx, taskStatus.Waiting)
+ startTask()
+ }
+
+ function clearSuccessTask() {
+ state.taskList = state.taskList.filter((item) => item.status != taskStatus.Success)
+ }
+
+ function findTaskIdxFromUuid(uuid: string) {
+ for (const key in state.taskList) {
+ if (state.taskList[key].uuid == uuid) {
+ return parseInt(key)
+ }
+ }
+ return false
+ }
+
+ function findTaskIdxFromGuess(idx: number) {
+ if (!state.taskList[idx]) {
+ let taskKey = -1
+ for (const key in state.taskList) {
+ if (state.taskList[key].status == taskStatus.Connecting || state.taskList[key].status == taskStatus.Executing) {
+ taskKey = parseInt(key)
+ }
+ }
+ return taskKey === -1 ? false : taskKey
+ } else {
+ return idx
+ }
+ }
+
+ function execMessageScrollbarKeep(uuid: string) {
+ const execMessageEl = document.querySelector('.exec-message-' + uuid) as Element
+ if (execMessageEl && execMessageEl.scrollHeight) {
+ execMessageEl.scrollTop = execMessageEl.scrollHeight
+ }
+ }
+
+ return {
+ state,
+ init,
+ toggle,
+ toggleDot,
+ setTaskStatus,
+ setTaskShowMessage,
+ addTaskMessage,
+ addTask,
+ addTaskPM,
+ delTask,
+ startTask,
+ retryTask,
+ clearSuccessTask,
+ toggleConfigDialog,
+ changeRegistry,
+ changePackageManager,
+ changePHPDevelopmentServer,
+ changeAutomaticCleanupTask,
+ }
+ },
+ {
+ persist: {
+ key: STORE_TERMINAL,
+ pick: ['state.showDot', 'state.taskList', 'state.automaticCleanupTask', 'state.npmRegistry', 'state.composerRegistry'],
+ },
+ }
+)
diff --git a/web/src/stores/userInfo.ts b/web/src/stores/userInfo.ts
new file mode 100644
index 0000000..23077ad
--- /dev/null
+++ b/web/src/stores/userInfo.ts
@@ -0,0 +1,88 @@
+import { defineStore } from 'pinia'
+import router from '../router'
+import { postLogout } from '/@/api/frontend/user/index'
+import { USER_INFO } from '/@/stores/constant/cacheKey'
+import type { UserInfo } from '/@/stores/interface'
+import { Local } from '/@/utils/storage'
+
+export const useUserInfo = defineStore('userInfo', {
+ state: (): UserInfo => {
+ return {
+ id: 0,
+ username: '',
+ nickname: '',
+ email: '',
+ mobile: '',
+ avatar: '',
+ gender: 0,
+ birthday: '',
+ money: 0,
+ score: 0,
+ last_login_time: '',
+ last_login_ip: '',
+ join_time: '',
+ motto: '',
+ token: '',
+ refresh_token: '',
+ }
+ },
+ actions: {
+ /**
+ * 状态批量填充
+ * @param state 新状态数据
+ * @param [exclude=true] 是否排除某些字段(忽略填充),默认值 true 排除 token 和 refresh_token,传递 false 则不排除,还可传递 string[] 指定排除字段列表
+ */
+ dataFill(state: Partial, exclude: boolean | string[] = true) {
+ if (exclude === true) {
+ exclude = ['token', 'refresh_token']
+ } else if (exclude === false) {
+ exclude = []
+ }
+
+ if (Array.isArray(exclude)) {
+ exclude.forEach((item) => {
+ delete state[item as keyof UserInfo]
+ })
+ }
+
+ this.$patch(state)
+ },
+ removeToken() {
+ this.token = ''
+ this.refresh_token = ''
+ },
+ setToken(token: string, type: 'auth' | 'refresh') {
+ const field = type == 'auth' ? 'token' : 'refresh_token'
+ this[field] = token
+ },
+ getToken(type: 'auth' | 'refresh' = 'auth') {
+ return type === 'auth' ? this.token : this.refresh_token
+ },
+ getGenderIcon() {
+ let icon = { name: 'fa fa-transgender-alt', color: 'var(--el-text-color-secondary)' }
+ switch (this.gender) {
+ case 1:
+ icon = { name: 'fa fa-mars-stroke-v', color: 'var(--el-color-primary)' }
+ break
+ case 2:
+ icon = { name: 'fa fa-mars-stroke', color: 'var(--el-color-danger)' }
+ break
+ }
+ return icon
+ },
+ logout() {
+ postLogout().then((res) => {
+ if (res.code == 1) {
+ Local.remove(USER_INFO)
+ router.go(0)
+ }
+ })
+ },
+ isLogin() {
+ return this.id && this.token
+ },
+ },
+ persist: {
+ key: USER_INFO,
+ },
+})
diff --git a/web/src/styles/app.scss b/web/src/styles/app.scss
new file mode 100644
index 0000000..8d7c7ba
--- /dev/null
+++ b/web/src/styles/app.scss
@@ -0,0 +1,240 @@
+/* 基本样式 */
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+ outline: none !important;
+}
+
+html,
+body,
+#app {
+ margin: 0;
+ padding: 0;
+ width: 100%;
+ height: 100%;
+ font-family:
+ Helvetica Neue,
+ Helvetica,
+ PingFang SC,
+ Hiragino Sans GB,
+ Microsoft YaHei,
+ SimSun,
+ sans-serif;
+ font-weight: 400;
+ -webkit-font-smoothing: antialiased;
+ -webkit-tap-highlight-color: transparent;
+ background-color: var(--ba-bg-color);
+ color: var(--el-text-color-primary);
+ font-size: var(--el-font-size-base);
+}
+
+// 阿里 iconfont Symbol引用css
+.iconfont-icon {
+ width: 1em;
+ height: 1em;
+ vertical-align: -0.15em;
+ fill: currentColor;
+ overflow: hidden;
+}
+
+.w100 {
+ width: 100% !important;
+}
+.h100 {
+ height: 100% !important;
+}
+.ba-center {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.default-main {
+ margin: var(--ba-main-space) var(--ba-main-space) 60px var(--ba-main-space);
+}
+.zoom-handle {
+ position: absolute;
+ width: 20px;
+ height: 20px;
+ bottom: -10px;
+ right: -10px;
+ cursor: se-resize;
+}
+.block-help {
+ display: block;
+ width: 100%;
+ color: #909399;
+ font-size: 13px;
+ line-height: 16px;
+ padding-top: 5px;
+}
+
+/* 表格顶部菜单-s */
+.table-header {
+ .table-header-operate .icon {
+ font-size: 14px !important;
+ color: var(--el-color-white) !important;
+ }
+ .el-button.is-disabled .icon {
+ color: var(--el-button-disabled-text-color) !important;
+ }
+}
+/* 表格顶部菜单-e */
+
+/* 鼠标置入浮动效果-s */
+.suspension {
+ transition: all 0.3s ease;
+}
+.suspension:hover {
+ -webkit-transform: translateY(-4px) scale(1.02);
+ -moz-transform: translateY(-4px) scale(1.02);
+ -ms-transform: translateY(-4px) scale(1.02);
+ -o-transform: translateY(-4px) scale(1.02);
+ transform: translateY(-4px) scale(1.02);
+ -webkit-box-shadow: 0 14px 24px rgba(0, 0, 0, 0.2);
+ box-shadow: 0 14px 24px rgba(0, 0, 0, 0.2);
+ z-index: 2147483600;
+ border-radius: 6px;
+}
+/* 鼠标置入浮动效果-e */
+
+/* 表格-s */
+.ba-table-box {
+ border-radius: var(--el-border-radius-round);
+}
+.ba-table-alert {
+ background-color: var(--el-fill-color-darker) !important;
+ border: 1px solid var(--ba-boder-color);
+ border-bottom: 0;
+ border-bottom-left-radius: 0;
+ border-bottom-right-radius: 0;
+}
+/* 表格-e */
+
+/* 新增/编辑表单-s */
+.ba-operate-dialog {
+ overflow: hidden;
+ border-radius: var(--el-border-radius-base);
+ padding-bottom: 52px;
+}
+.ba-operate-dialog .el-dialog__header {
+ border-bottom: 1px solid var(--ba-bg-color);
+ .el-dialog__headerbtn {
+ top: 4px;
+ }
+}
+.ba-operate-dialog .el-dialog__body {
+ height: 58vh;
+}
+.ba-operate-dialog .el-dialog__footer {
+ padding: 10px var(--el-dialog-padding-primary);
+ box-shadow: var(--el-box-shadow);
+ position: absolute;
+ width: 100%;
+ bottom: 0;
+ left: 0;
+}
+.ba-operate-form {
+ padding-top: 20px;
+}
+/* 新增/编辑表单-e */
+
+/* 全局遮罩-s */
+.ba-layout-shade {
+ position: fixed;
+ top: 0;
+ left: 0;
+ height: 100vh;
+ width: 100vw;
+ background-color: rgba(0, 0, 0, 0.5);
+ z-index: 2147483599;
+}
+/* 全局遮罩-e */
+
+/* 图片上传预览-s */
+.img-preview-dialog .el-dialog__body {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ img {
+ max-width: 100%;
+ }
+}
+/* 图片上传预览-e */
+
+/* 页面切换动画-s */
+.slide-right-enter-active,
+.slide-right-leave-active,
+.slide-left-enter-active,
+.slide-left-leave-active {
+ will-change: transform;
+ transition: all 0.3s ease;
+}
+// slide-right
+.slide-right-enter-from {
+ opacity: 0;
+ transform: translateX(-20px);
+}
+.slide-right-leave-to {
+ opacity: 0;
+ transform: translateX(20px);
+}
+// slide-left
+.slide-left-enter-from {
+ @extend .slide-right-leave-to;
+}
+.slide-left-leave-to {
+ @extend .slide-right-enter-from;
+}
+/* 页面切换动画-e */
+
+/* 布局相关-s */
+.frontend-footer-brother {
+ min-height: calc(100vh - 120px);
+}
+.user-views {
+ padding-left: 15px;
+ .user-views-card {
+ margin-bottom: 15px;
+ }
+}
+.ba-aside-drawer {
+ .el-drawer__body {
+ padding: 0;
+ }
+}
+/* 布局相关-e */
+
+/* 暗黑模式公共样式-s */
+.ba-icon-dark {
+ color: var(--el-text-color-primary) !important;
+}
+/* 暗黑模式公共样式-e */
+
+/* NProgress-s */
+#nprogress {
+ .bar,
+ .spinner {
+ z-index: 2147483600;
+ }
+}
+/* NProgress-e */
+
+/* 自适应-s */
+@media screen and (max-width: 768px) {
+ .xs-hidden {
+ display: none;
+ }
+}
+@media screen and (max-width: 1024px) {
+ .ba-operate-dialog {
+ width: 96%;
+ }
+}
+@media screen and (max-width: 991px) {
+ .user-views {
+ padding: 0;
+ }
+}
+/* 自适应-e */
diff --git a/web/src/styles/dark.scss b/web/src/styles/dark.scss
new file mode 100644
index 0000000..13814eb
--- /dev/null
+++ b/web/src/styles/dark.scss
@@ -0,0 +1,27 @@
+@use 'sass:map';
+@use 'mixins.scss' as *;
+@use 'element-plus/theme-chalk/src/dark/css-vars.scss';
+
+// Background
+$bg-color: () !default;
+$bg-color: map.merge(
+ (
+ '': #141414,
+ 'overlay': #1d1e1f,
+ ),
+ $bg-color
+);
+
+// Border
+$border-color: () !default;
+$border-color: map.merge(
+ (
+ '': #4c4d4f,
+ ),
+ $border-color
+);
+
+html.dark {
+ @include set-component-css-var('bg-color', $bg-color);
+ @include set-component-css-var('border-color', $border-color);
+}
diff --git a/web/src/styles/element.scss b/web/src/styles/element.scss
new file mode 100644
index 0000000..f002e97
--- /dev/null
+++ b/web/src/styles/element.scss
@@ -0,0 +1,87 @@
+.el-menu {
+ user-select: none;
+ .el-sub-menu__title:hover {
+ background-color: var(--el-color-primary-light-9) !important;
+ }
+}
+
+.el-table {
+ --el-table-border-color: var(--ba-border-color);
+}
+
+.el-card {
+ border: none;
+ .el-card__header {
+ border-bottom: 1px solid var(--el-border-color-extra-light);
+ }
+}
+
+.el-divider__text.is-center {
+ transform: translateX(-50%) translateY(-62%);
+}
+
+/* 修复 Chrome 浏览器输入框内选中字符行高异常的问题开始 <<< */
+.el-input {
+ .el-input__inner {
+ line-height: calc(var(--el-input-height, 40px) - 4px);
+ }
+}
+/* 修复 Chrome 浏览器输入框内选中字符行高异常的问题结束 >>> */
+
+/* 输入框样式统一开始 <<< */
+.el-input-number.is-controls-right {
+ .el-input__wrapper {
+ padding-left: 11px;
+ }
+ .el-input__inner {
+ text-align: left;
+ }
+}
+.el-textarea__inner {
+ padding: 5px 11px;
+}
+.datetime-picker {
+ height: 32px;
+ padding-top: 0;
+ padding-bottom: 0;
+}
+/* 输入框样式统一结束 >>> */
+
+/* dialog 滚动条样式优化开始 <<< */
+.el-overlay-dialog,
+.ba-scroll-style {
+ &::-webkit-scrollbar {
+ width: 5px;
+ height: 5px;
+ }
+ &::-webkit-scrollbar-thumb {
+ background: #eaeaea;
+ border-radius: var(--el-border-radius-base);
+ box-shadow: none;
+ -webkit-box-shadow: none;
+ }
+ &:hover {
+ &::-webkit-scrollbar-thumb:hover {
+ background: #c8c9cc;
+ }
+ }
+}
+@supports not (selector(::-webkit-scrollbar)) {
+ .el-overlay-dialog,
+ .ba-scroll-style {
+ scrollbar-width: thin;
+ scrollbar-color: #c8c9cc #eaeaea;
+ }
+}
+/* dialog 滚动条样式优化结束 >>> */
+
+/* 小屏设备 el-radio-group 样式优化开始 <<< */
+.ba-input-item-radio {
+ margin-bottom: 10px;
+ .el-radio-group {
+ .el-radio {
+ margin-bottom: 8px;
+ }
+ }
+}
+/* 小屏设备 el-radio-group 样式优化结束 >>> */
diff --git a/web/src/styles/index.scss b/web/src/styles/index.scss
new file mode 100644
index 0000000..3a300cc
--- /dev/null
+++ b/web/src/styles/index.scss
@@ -0,0 +1,5 @@
+@use '/@/styles/app';
+@use '/@/styles/element';
+@use '/@/styles/var';
+@use '/@/styles/dark';
+@use '/@/styles/markdown';
diff --git a/web/src/styles/loading.scss b/web/src/styles/loading.scss
new file mode 100644
index 0000000..5576868
--- /dev/null
+++ b/web/src/styles/loading.scss
@@ -0,0 +1,54 @@
+.block-loading {
+ width: 100%;
+ height: 100%;
+ position: fixed;
+ z-index: 2147483600;
+ background-color: var(--ba-bg-color);
+}
+.block-loading .block-loading-box {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+}
+.block-loading .block-loading-box-warp {
+ width: 80px;
+ height: 80px;
+}
+.block-loading .block-loading-box-warp .block-loading-box-item {
+ width: 33.333333%;
+ height: 33.333333%;
+ background: #409eff;
+ float: left;
+ animation: block-loading-animation 1.2s infinite ease;
+ border-radius: 1px;
+}
+.block-loading .block-loading-box-warp .block-loading-box-item:nth-child(7) {
+ animation-delay: 0s;
+}
+.block-loading .block-loading-box-warp .block-loading-box-item:nth-child(4),
+.block-loading .block-loading-box-warp .block-loading-box-item:nth-child(8) {
+ animation-delay: 0.1s;
+}
+.block-loading .block-loading-box-warp .block-loading-box-item:nth-child(1),
+.block-loading .block-loading-box-warp .block-loading-box-item:nth-child(5),
+.block-loading .block-loading-box-warp .block-loading-box-item:nth-child(9) {
+ animation-delay: 0.2s;
+}
+.block-loading .block-loading-box-warp .block-loading-box-item:nth-child(2),
+.block-loading .block-loading-box-warp .block-loading-box-item:nth-child(6) {
+ animation-delay: 0.3s;
+}
+.block-loading .block-loading-box-warp .block-loading-box-item:nth-child(3) {
+ animation-delay: 0.4s;
+}
+@keyframes block-loading-animation {
+ 0%,
+ 70%,
+ 100% {
+ transform: scale3D(1, 1, 1);
+ }
+ 35% {
+ transform: scale3D(0, 0, 1);
+ }
+}
diff --git a/web/src/styles/markdown.scss b/web/src/styles/markdown.scss
new file mode 100644
index 0000000..68e48fa
--- /dev/null
+++ b/web/src/styles/markdown.scss
@@ -0,0 +1,242 @@
+.ba-markdown {
+ ::-webkit-scrollbar {
+ width: 6px;
+ height: 6px;
+ }
+ ::-webkit-scrollbar-corner,
+ ::-webkit-scrollbar-track {
+ background-color: var(--el-bg-color-page);
+ border-radius: 2px;
+ }
+ ::-webkit-scrollbar-thumb {
+ border-radius: 2px;
+ background-color: var(--el-color-black);
+ }
+ ::-webkit-scrollbar-button:vertical {
+ display: none;
+ }
+ ::-webkit-scrollbar-thumb:vertical:hover {
+ background-color: var(--el-color-black);
+ }
+ ::-webkit-scrollbar-thumb:vertical:active {
+ background-color: var(--el-color-black);
+ }
+ h1 {
+ font-size: var(--el-font-size-large);
+ text-transform: uppercase;
+ color: var(--el-color-primary);
+ }
+ h1,
+ h2,
+ h3,
+ h4,
+ h5,
+ h6 {
+ position: relative;
+ word-break: break-all;
+ }
+ h1 a,
+ h2 a,
+ h3 a,
+ h4 a,
+ h5 a,
+ h6 a,
+ h1 a:hover,
+ h2 a:hover,
+ h3 a:hover,
+ h4 a:hover,
+ h5 a:hover,
+ h6 a:hover {
+ color: inherit;
+ }
+ ol > li {
+ list-style: decimal;
+ }
+ ul > li {
+ list-style: disc;
+ }
+ ol .li-task,
+ ul .li-task {
+ list-style-type: none;
+ }
+ ol .li-task input,
+ ul .li-task input {
+ margin-left: -1.5em;
+ margin-right: 0.1em;
+ }
+ a {
+ text-decoration: none;
+ }
+ pre,
+ code {
+ font-family:
+ source-code-pro,
+ Menlo,
+ Monaco,
+ Consolas,
+ Courier New,
+ monospace;
+ font-size: 14px;
+ color: #24292f;
+ }
+ pre {
+ margin: 20px 0;
+ }
+ pre code {
+ display: block;
+ line-height: 1.6;
+ overflow: auto;
+ }
+ pre code .code-block {
+ display: inline-block;
+ width: 100%;
+ overflow: auto;
+ vertical-align: bottom;
+ }
+ hr {
+ height: 1px;
+ margin: 10px 0;
+ border: none;
+ border-top: 1px solid #eaecef;
+ }
+ div[inline] > .figure {
+ padding-right: 0.5em;
+ }
+ div[inline] > .figure img {
+ padding: 0;
+ border: none;
+ }
+ .figure {
+ margin: 0 0 1em;
+ display: inline-flex;
+ flex-direction: column;
+ text-align: center;
+ }
+ .figure .figcaption {
+ color: #888;
+ font-size: 0.875em;
+ margin-top: 5px;
+ }
+ h1,
+ h2,
+ h3,
+ h4,
+ h5,
+ h6 {
+ margin: 1.4em 0 0.8em;
+ font-weight: 700;
+ }
+ a {
+ color: #2d8cf0;
+ transition: color 0.3s;
+ }
+ a:hover {
+ color: #73d13d;
+ }
+ img {
+ margin: 0 auto;
+ max-width: 100%;
+ box-sizing: border-box;
+ padding: 5px;
+ border: 1px solid #e6e6e6;
+ border-radius: 3px;
+ }
+ p {
+ line-height: 1.6;
+ margin: 0;
+ padding: 0.5rem 0;
+ }
+ p:empty {
+ display: none;
+ }
+ code {
+ color: #3594f7;
+ background-color: #3baafa1a;
+ display: inline-block;
+ padding: 0 4px;
+ border-radius: 2px;
+ line-height: 22px;
+ }
+ blockquote {
+ margin: 20px 0;
+ padding: 0.5em 1.2em;
+ line-height: 2em;
+ background-color: #ececec;
+ border-left: 5px solid #35b378;
+ display: block;
+ }
+ blockquote p {
+ padding: 0;
+ }
+ pre {
+ position: relative;
+ border-radius: 5px;
+ box-shadow: #0005 0 2px 2px;
+ }
+ pre code {
+ position: relative;
+ padding: 1em;
+ background-color: #282c34;
+ color: #a9b7c6;
+ border-radius: 0 0 5px 5px;
+ }
+ pre code > * {
+ line-height: 1.6;
+ }
+ pre .copy-button {
+ color: #999;
+ position: absolute;
+ font-size: 12px;
+ top: 9px;
+ right: 10px;
+ cursor: pointer;
+ }
+ pre:before {
+ content: '';
+ display: block;
+ background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAcIAAACCCAYAAADVN8idAAAgAElEQVR4nO2de5QU5Zn/v1VdVX2/zQwMzDCDgCBKOIx4myXLRlnYGDlhzWWDSTxkhXBQo2iS34kmavb3C5qo5+yqqBs5xNG4ZpVskjXk6BrhqAkbdoyXgSUoiqgMzDjAzPS1+lLX3x/TYNU7F6C7untm+vn8Ne/bVdVvP+8777fe2/MABEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQBEEQExKu2BtN03SyHGVhxdS61jk+77xWr3dWk9c7Y4okTakThbqAIIa8POcTeF4EAM0w1KxhZtKamhxUtcETinKiN5s92p3Nfngok31vx/HB7mr/FmLisaItMGv2NPfclqnCrKYGoXVqWJxWF+TrAj4u5JE4n+jiRZMzoWmmmlPMTDpjJgdTxuDxhNrX2691HzmuffhBX/7gjj3pD6v9W4iJx9TFwXqxWWrlG6UmforYiIhQb4ZcEcPPBzjJ5eZd4AHA0GGYip7nZSPNJfU44tqAcUI9ZhxTetUepfv4W6mBav+W08FxRUvZ0P3F3jjehHBByM+3RyNLLw6H29vCwQubPJ6ZhY/aS3x0JwD05nKH9yRSXW8kEp2dsfiu/UnZKPG5xCRiQYuHb5/vvfyieZ4lbXO8FzU1uE62vwtLfHQXAPT064f3Hsq++cZ7ud2vHci+uv9IjtofYWP6VfWfEud7F2Gu9wJMEacVsteW+NgOAMAJtQ8Hs2+rB7J7P35h4C8lPtNxaloI2+tDkRUNDSuvqG9YPsfvnY/SRe9M6TwkywdeGRjcuaO///nOgWS8Qt9LjCP+ap6v/m8X+1de3ua78twmaT5KF70zpev9XuXAq3syL+54S97+2nsZan81SMN8v9tzaXApvziwBDOkky9epQrf6RgSxqPKYeOt9O7cn1O7+g/I+TJ/52mpSSG8aXbL51ZNa/zCeX7/QlRO/Eaj811Z3re979h/PvLBkf+qclmICvCtlfUrP78k8JX5LdJCVE78RqPrwBFl3+92p3/56PMDz1e5LEQFaPrClEvFvw4uN2d65qD8wnc6OozDuUPmrtTve5478Wa1ClEzQtgW8bu/1ty8dnXT9DWFrDEFkOcAURIhCiJEQYDL5YLocoEXePDgwPHcKeOZpgnTMGHAhKEZUHUduq5D1TSomgo1r+IM5qE6TcD4Ze/HT//70Z6OPYnqvyURzrHoHK/7a8tCG1ZfHvrHQtaYAsgBkCQXRJGDKLggCIDg4uHiOfCFtsdxQ/9DpsnBNE0YhgndMKHpBjQNUDUdqmpCUXScwX9bl2lyxrZXk08+80pi696PstT+JhnN1zdf7Voe/nwhOaYAGpwJSRTBSQK4ocYH3sXD5eIB3gWe42AU2h9vcjBMEzB06LoBQzcATYepaTAVDYqqgjdPKxUdnAld3xl/4eiW3udK/7Vnx6QXwraI372uteWmVY2NX8ZpxM/r8cDjluCRJEhu0dFyKHkVOUVBLq8gm8ud7vLO7ceO/erxw0ceIUGc2Cw6x+ted2Xk1lVLAqtxGvHzelzwuAV43DzcEu9oOfKKgVzeQC6vIZvTT3d51/bd6W2Pvxh/kARx4tNyy4zV3NLQ3xWSowqg4HGD94jg3SJcDvd/el6FkVdh5FRouTGbVAcAmLuSLx156Og2RwsxBpNaCH98/rnrvz6jeS3GEECf1wuf1wOf112yMc4U0zSRyeaRyeaQyWbHurTzF0d7On7wzvtbK1IwwlHuWdN4w9eXh9ZjDAH0eUX4vTx8XlfF2p9hmshmdchZDZnsmKLY9fTO5JY7nzq2pSIFIxyl5ZvTV3JXRr9YSI4ogJzPA7fXDc4nga9g+zMzCvLZPMzMqIOCIUF8MfabIz/7uOxT9pNSCNfNbP7MrbNn3RYSXFGMIIKiICLg98Lv98LFO/vmfbbohgFZziItZ6Fq6kiXdCY1LfbgBx/d9/jhnj9UunzE2bNuRXTZLV+quyPk46MYQQRFgUfALyLgF+CqbvODrgPpjIa0rELVRpzA70pmjNiDvx68p2NH7OVKl484e6ZfVf8p1zUN63mfK4CRBFBwQQp44fJ7wFe5ARq6AV3OQUlnAW3El7IOyEZa3XZiazl3m04qIVwQ8vPfnzt709K6umUYQQDdbhFBfwB+n8fx73YCOZNDSk4jnx9ZEHcNDr78k4Mf3EVHL8YnC1o8/G3X1N/7Nwt9yzGCALolF0IBAX6fUIXSnR45oyGZ1pBXRuyQuv74v9md923rv52OXoxfWu+cuQFt/ksxggAKkgQh6IXL765CyU6PLuehpWRoijbSxx3YI/+5++7DZZmdmDRC+I3WpiU/nDvnXoHnl7KfSYKAUCg4bgWQRc7kkEymoGjDG4RqGH/YdPDQD37e3bu7CkUjRmHNsujSO6+tv18SuOEzECKHSFAatwLIImc0xFMKVHX4/6iimZ13Pz3wvadeju2qQtGIUZj+2boLhOumbeQEiGBEkBdFSCHfuBVAFl3OQ0lmYKjDBgQdpmaoesfxB3tfGjzg5HdOCiG8f8G8Gwq7QW2dEM8B4VAIoaDfse+qJMmUjFgiOdJHndt6P37qe/vf+2mly0QM5761jRsLu0Fto0COMxEJeRAOTgwBZEmkNMQT+ZF2nHY9+0qq4/Yn+h6pfKkIFstuUJsAGpwJTzgIMeSrUslKQ01mkEukRtpx2mHsiP/Oyd2lE14If31J2wMXR8LtYETQ5/UiGglCcLkc+Z5qoak6YsnUSJtqOt+IJzq/9Pqeb1ejXMQQv7qj5eGLz/N8GowI+rwi6sIiBKEyGxDKhaaZGEzkR9pU0/X6u/k//cM93TdXo1zEEK2bZm3E+d5FYESQ83ngi/gBYWL3f9B0ZOLySJtqOvBOdm/3XR9uduJrJqwQtteHIg9ccP5jBVdoNhGsi4QRDEzMt6DRSKUzGIwn2OzO3lzu8Lfffud68k5TWS6b54s8cP20Jwqu0GwiWB+REAw4u/282qTSKgbiCpvd1dOvH/7OY33XkXeaytIw3+/23dL0w4IrNJsIeqJBuILeKpWsPOipLHKxFJvdwR1X++TNvT8q1TvNhBTCFVPrWh9euOBxL88vt+a7RQnRaAhuaXJ1QifJKyoG40koir1DyhrGzpv37V9Hzr0rw4q2wKyHvjXt5z43Z1uPlkQXGqISJIfPAI4XFMVAf0yBotpHh5m8uWvjo33X7tyTpvZXAaYuDtZ7vtu8CW7+BtsHkgBfXQicNDGn4k+HqWjIDCYBdjNN3vhp7p977irFufeEE8KrpjXM+deFC57igCXWfL/Pg/popGJnsaqFaZoYiMUhM1MFJrD7xn3717zQ13+oSkWrCT53UWjuo7c0PsMDF1nz/T4BDVHPKW8vkxXT5NAfy0HOaGz+mzdu7v3qf72ZPlilotUEjZeEp7hva74XzChQ9HngqQ+f8vYyWeFNE7mBFFSm/+NMbM3e33PH8dcTJ4p57oQSwhVT61q3Llr4DCuCoWAA0XCw2KJMSGKJFJKptC3PBHav37vvqzQyLA8r2gKztnxn+n+wIhgKCqgLT4wdeU4xmMgjmRouhusf6P0ijQzLw9TFwXrPD1ruByuCIS+kSG31f1osjXwqY8vjTGzN/uTI94sZGZYqhBWbA2qvD0UeXrjgcVYEI6FgzYkgAETDQURC9t/NAUseXrjg8fb6UKRKxZq0XDbPF3noW9N+zopgNCTVnAgCQF3YjUhIsuVxnHnR5m9Ne/qyeT5qfw7TMN/v9ny3eRMYEXSHAzUnggAgRANwhwO2PJPDes93mzc1zK/8OZGKCeEDF5z/GLsmGA2HEA4FRrtl0hMOBRANh2x5Xp5f/sAF5z9WpSJNWh64ftoT7JpgNOxGODQ516PPhEhIRJR5CfC5uaX/cv20J6pUpEmL75amH7JrglIkACE8uTYFng1C2AcpwvT/bv4G/8amH1a6LBURwl9f0vaAJVAugKGR4EQ9H+gkoaB/2MiwyeOZ+etL2h6oUpEmHb+6o+VhS6BcAEMjwYl6PtBJwkFh2MiwucE18z/uaH24SkWadLRumrXREigXwJAITtTzgU4ihnzDR4ZTxWmtm2ZtrGQ5yi6E9y+YdwN7TjAUDNT0SJAlHAogFLTZo/3iSLj9/gXzbhjtHuLMuG9t40b2nGA4KNT0SJAlEhIRsr8UXHjJee5P33vdtJuqVabJQvP1zVez5wTFkJ9E0IIQ9sEdtNljLc73LpqxoenqSpWhrEL4jdamJazHGL/PU5NrgqcjGh7mQq59ddP0Nd9obVoy2j3E2KxZFl3Keozx+4Rh04HE0Joh40LuwmuuCK5dsyw6zOUhcWZM/2zdBazHGNHngRShmTAWIRqAaO//1vIrIp+f/tm6Cyrx/WXbNbog5Oe3X7L4VavvULcooXFq3aQ/IlEspmmi78Sg7Zyhahh/+PvX31pGjrrPjgUtHv4//9+MP1l9h0qiC9Oneif9EYliMU0OHx/P2s4ZKprZ+YV/OvppctR99rQ8e8FjnIANpzIkAcHG+kl/RKJYeNNE6ljMds7Q1IwtR645cP3p7h23u0a/P3f2JtaBdjQaIhEcA47jUBexb54Ref4z3587e1OVijRhue2a+ntZB9oNUYlEcAw4zkRD1L5eKAlc+23X1N9bpSJNWFrvnLmh4ED7FL66EIngGBgcB1+dvf/jBF5svXPmhlFucYyyCOG6mc2fKYRSOkVdJDxpPcY4iVsSURcJ2/KW1tUtWzez+TNVKtKEY92K6LJCKKVT1Ecmr8cYJ5EkHvURuxj+zULf8rUrostGuYVgmH5V/afYUEqeaHDSeoxxEk4S4Inals7Wos1/6fSr6j9Vzu8tS89w6+xZt8GyLujzeied79ByEgz44PPafA223zr7nNuqVZ6Jxi1fqrsDlnVBn1ecdL5Dy0kwIMLntTl7vvDWIZsSZ4Drmob1sIgg5/NMOt+h5cQV9IJj1gvF1VPWl/M7HRfCH59/7vpCZPmhL+CAaA0eGC2VKHOkIiQI0R+ff25ZG8Nk4J41jTcUIssDGJruqwuTCJ4tdWG3bQNByMdH717TWPYpqolOyzenryxElgcwFErJR5tjzhpfxG+fRvbzgZZvTl9Zru9zVAjbIn7312c0r4VlNBgOhSZ8KKVqIIgu9rB9+9dnNK9tC0+Q6JxVYNE5XvfXl4fWwzIajIQ8Ez6UUjUQBA4R++7aC69dHtqw6Bwvtb8x4K6MfhHWKdFwcOKHUqoGgmvIdp+wtmDbsuCoEK5rbbkJFhGUBIEOzZdAKOiHJNjWFdrXzWyhs12jsO7KyK2wiKAocnRovgTCQQGiaHuJuLBgY2IEWm6Zsdqa5kWRzguWgBjygRftszmsjZ3CMSFsi/jdqxobv2zNC4VoSrRUQozjgVWNjV+mUeFwFp3jda9aErD9k0SC0miXE2cIa8NVSwKraVQ4MtzS0N/BMhqUSARLhrHh2oKNHccxIfxas31K1O0W2QPiRBH4fV64RVtn1P61oelnwsLXloU2wDIadEsu9oA4UQR+nwC3ZN8489UrwrRWzdB8fbPNC4ogSXDR+2rJuPxuCMxu23J4nHFMCAseZE4R9JMLNacI2t0P4StN06+tUlHGLQUPMqcIBUgEnYK1JWtrAmA9yAi0S9QxBPvy2lrX8shVTn+HI0J40+yWz1nTokCjQSfx+7wQhU/myjmAZ21ey3xrZb1tN5ko8DQadBC/T4AofNJVcJzJ38jYvJZp+sKUS20ZgotGgw7i8rshWDYcmRxczVdPuWiMW84aR4Rw1bTGL8AyLRrw09uQ0zA2bS/YnADw+SWBr8AyLRrw03EJp2FseuGqIZsTAMS/Di6HdW0wQP2f0/B2m67llgY/6+jzS31Ae30ocp7fv9Ca5ychdBzWpuf5/QspgC/wV/N89fNbJFv7C/hpNOg0AWaEPb9FWkgBfIcC7pozPXOseS4/zYY5DWtTfqZnjpMBfEsWwhUNDSvBeJFx8eTKymlcPD/M20zB9jXN3y72rwTjRcZFzc9xXC4M8zazYrF/VbXKM17wXBpcCsaLDE8N0HF4Fz/M20zB9s48v9QHXFHfYPPp6PPS21C58DG71q+or1s+yqU1w+Vtviutab+XOqFy4ffaR4Ws7WsRfnHAFibNTSdLygZrW9b2pVBSr7Eg5Ofn+L3zrXlsZ004B/uSMcfvn78g5K/Znn9Bi4c/t0li2h958SgXXsa25zZJ8xe0eGq2/QEAZkgzrUnOR2dXy8Uw2zK2L4WSGnF7NLIUlmlRr8dDYZbKCMdx8HrswXsLdVCTtM/3Xg7LtKjX46L2V0Z4joPXY58evWyoDmoSNiKC4HGDp/ZXNniOg+CxD7ScikpRkhBeHA7b4r153PQ2VG5YG7N1UEtcNM9jmxrxuGmTTLlhbXwxUwe1hDjfuwiW9UHeQ7uVyw1j47WFOij9uaXc3BYOXmhNeyQSwnLD2pitg1qibY7XdpbI467tWbpKwNp4EVMHNcVc7wXWJO8mISw3w2zM1EHRzy3l5iaP59QcLc8BEjWEssPa2FoHtUZTg+vUb+c4E24KvFt23BJvC8/UbKmDmmOKOO3knwZnwkX9X9lxuUV7eCZLHZRC0T3Hiql1rda0SNHnK4ab+Ydj66IWWNEWmGVNSyJNi1YKye57dFhd1AJTFwfrrWlJpP6vUrC2ZuuiGIoWwjk+7zxYNspYXYAR5YWxdXuhLmqK2dPcc8GEXCIqAxuaqVAXNYXYLLXCen5QohexSuESmXXCobooiaKFsNXrtb0FigI1hErB2pqti1qgZarAtD86NlEpWFuzdVEL8I1SkzXNUf9XMUzR3v7YuiiGooWwyeudYU27KAp9xWBtzdZFLdDUINjeAqkfqhysrZvqxZqbmueniI22DHoRqxyMrYfVRREULYRTJGmKNS2SEFYM1tZsXdQCU8P2RXKB3FpVDNbWUyO8IxsWJhQRwbYuRW7VKscwWzN1UdQzi72xThTqbA8SqCFUCp7x5crWRS1QF+Rtv9nF0xphpWBtHQ3WXvszQy6bw3EXCWHFYG3N1kUxFF17AUEM2R9EHVGl4JmOiK2LWiDg4+ztj4SwYrC2DjJ1UQsYft4eeZynGbGKwdh6WF0U88hib/TynC1sOkcdUcVgbc3WRS3gkZj2R66tKgZra7YuagFOctl8fZFrtcrBW88RYnhdFPXMYm8UeN62h5U6osrB2pqti1pAdLHtzxztUsJhWFsLAldz7Y932ftOg9pfxTCY/o+ti2KgiW1iQmJSx0MQhEMULYSaYajWtGlSx1QpWFuzdVELaJrJtD+akagUrK3ZuqgFDB2GNc1T+6sYPNP/sXVR1DOLvTFrmBlr2jRICCsFa+usYWRGuXTSklOY9kcvYhWDtTVbF7WAqeh5a9qg9lcxDOalg8sb+VEuPWOKFsK0piataYOEsGKwtk5rWnKUSyct6YxJ7a9KsLZOMXVRC/CykbZlGHqVSlKDsLbO6OmRLzxzihbCQVUbtKYNo+TRKXGGsLZm66IWGEwZtt+skxBWDNbWsVTttT8uqcetaV2n/q9SsLZm66IYihbCE4pywppWdXojqhSsrU/k7XVRCxyPa33WtEYdUcVgbX08bvSNcunkJa4NWJMGtb+KMczWTF0UQ9FC2JvNHrWmdRLCisHaujdnr4taoHdA7bamNa1aJak9WFuzdVELGCfUY7YMjfq/isHYelhdFEHRQtidzX5oTavUE1UM1tZsXdQCR45rTPujjqhSsLbuZuqiFjCOKb3WtEn9X8Vgbc3WRTEULYSHMtn3AHSeTKtaze2grhqqYmsInYW6qCk+6MsfBNB1Mq2qtEZYKRhbd304VBc1hdqjdAPoOJk2FRLCSsHYuqNQFyVRtBDuOD5o+3JVISGsFHlVsaXZuqgFduxJ20YhikIjwkrB2pqti1rg+Fsp27qUolL/VylYW7N1UQwleZbpzeUOn/zbMAElT42h3OQYG1vroNbo6ddP/XYTQF6hDQvlJq8YsI4HrXVQc5xQT20S4k0OOvV/ZUfPqzbnBdxx1ZGNWiUJ4Z5EqsuazinKaJcSDqEwNmbroJbYeyj7pjWdy5MQlptc3j4FyNZBTXEw+7Y1aZAQlh3Wxub79joolpKE8I1EotOazuVJCMsNa2O2DmqJN97L7bam2U6acB72ZYOtg1pCPZDdC8s6oZEjISw3jI07CnVQMiUJYWcsvguWDTPZXI5cXZUR0zSRzeWsWZ2FOqhJXjuQfRWWDTPZnE6ursqIYZrI5mzrg12FOqhJPn5h4C/WtJbLU/srJ8aQja2wdVAsJQnh/qRsHJLlA9a8TLZkt2/EKGSyNhHEIVk+sD8p1+x84P4jOeP9XsXW/rJZ2jRTLljbvt+rHNh/JFez7Q8AcFSxrZGaGZoVKxc6qy2M7Uuh5DBMrwwM7rSm2c6acA72JYO1fS3y6p7Mi9a0nKXp0XLB2pa1fS1ivJW2TQ3naSBQNljbmm+mHZuWL1kId/T3Pw/L9Ggmm4VOfkcdRzcMZLJZa1bnjhP9z1erPOOFHW/J22GZHs1kdZCTI+fR9SHbWugq2L6myf05tQvW84SZHLlbKwOGbsDM2AZZHdnXk44tC5UshJ0Dyfi7srzPmifL2dEuJ4okzdj0XVne1zmYLNnZ7ETntfcy8QNHFFv7S2doVOg0aWbK70C3uu+19zI13/76D8h543DukDVPl2lWzGmG2fSj3KH+AxnHht+ORKjf3nfsP2EZFbKdNlE6sixbk52/7Tv2q2qVZbzxu93pX8IyKkzLtHvPadKyfTS4/X9Sv6xWWcYb5q7U72EZFSpp6v+chrFph/7fyd87+XxHhPCRD478l4lPogSrmgo5Q29FTiFnsjb/jiZgPPrBkZeqWKRxxaPPDzxvmpyl/RmQaVToGHJGg6p9Mt1nmpzxr88P1Py0/El6njvxJmfik39QTYcu01qhU+hy3uZomzOh9zzX7+j5VUeEEAB+2fvx09Z0Si45ViJRIJWyBwDf1tv7VJWKMm7Z9mrySWs6mSYhdArWlqytCUDfGX8BllGhlpLHuJo4GxhbdhRs7SiOCeG/H+3pgGV6NJ+nUaETyJks61u085mjvU9WqTjjlmdeSWyFZXo0r+g0KnQAOaMhb/ct2vXMK/Gt1SrPeOXolt7nrGlN0WhU6AC6nIfGODRnbe0EjgnhnoSc337Mvm6VTKacenzNkkzaR9bbjx371Z4E/Yex7P0om9++O73NmhdP0ZmuUmFt+Nvd6Wf2fpSj9jcC5q7kS7CuFSYzY1xNnAmMDTsKNnYcx4QQAB4/fOQRWEaFiqYhSVMERZNIyVDssbc6CzYmRuDxF+MPggnNlEjRqLBYEillWMiljhdjm6tVnvHOkYeO2l7EDFWFSmJYNGoyA4OJNMHa2CkcFcI9CTn/C2aKNJFIQlPpYNfZoqk6komkNavzF0d7Omg0ODp7P8rmn96Z3AKLGMaTOWgaub06WzTNRDxh64S6nt6Z3EKjwbExX4z9BpZRYS6Rouj1xaDpQ7b7hI6CbcuCo0IIAD945/2tSU2LnUwbAGI0RXrWxJIpWI/lJjUt9oN33qe1mdNw51PHtiQzxqn2Z5ocBhN0nOJsGUzkbeGWErIZu/OpY1uqVqAJwpGfffw8ZOPUegZvcsjEaVbsbMnEZVu4JchG+sjPPi7bTmXHhRAAHvzgo/vAeJtJpWmK4ExJpTPDvMgUbEqcAQ/+evAe2LzNqEilSQzPlGRaG+ZF5qHfDNxTrfJMNNRtJ7aC8Tajp+hs4Zmip7LDvMgUbFo2yiKEjx/u+cOuwcGXrXmD8QTyFMX+tOQVFYPxhC1v1+Dgy48f7vlDlYo04ejYEXv5j/syNj+sA3EFCgXuPS15xcBg3D77+cf/ze7s2BF7eZRbCIaPXxj4C/bIf4Z1ijSWgqnQevXpMBUNuZh9ShR75D87FWViNMoihADwk4Mf3KUahq3zjsWSFKZpDEzTRCxmWxeEahh/+MnBD+6qUpEmLPc9O3C7opm2WI39MQWmdbqFsGGaHAZi9l2iimZ23vds/+1VKtKEpfvuw1tMzbC9+WcGk+Cp/xsV3jSRGbT3f6ZmqN13Hy77lHzZhHB/UjY2HTz0A1jPFqoKBmI1755wVAZi8WFnBn908P3baznUUrHsP5Iz7n564HuwTJEqqo7+GJ1tHY3+WA6KfWNb193/NvC9/UdrPNRSkWhPHN8My6gQiobcAO2XGI3cQAqwj5o79I7jD1biu8smhADw8+7e3dt6P34KFjGUMznEEtQYWGKJFOuAoPPZ3t4nn+r+uGYj0JfKUy/Hdj37SqoDFjGUMxoGE7TxkWUwkWcdEHQ9+0qq46lXYjUb+LlUPv794NvGjvjvYBFDNZODFiOvWyxaLA2VWRc0dsR/1/vS4IHR7nGSsgohAHxv/3s/fSOe6IRFDJOpNBJJagwnSSTTSKZs9uh8PZ740237D9IuvRK5/Ym+R15/N/8nWMQwmdIQT9J69UniSRVJ+3nLrtffzf/p9if66MxqiRzd0vsc3snuhUUM86kMtARtHjyJlsggb3cj2cG9ndlbDg8yo1F2IQSAL72+59u9uZwtmnA8maLD9gCSKRlx5nhJby53+Muv7/k/VSrSpOMf7um+uadfZ9qfQoftMXRoPp60rwv29OuH/+Ge7purVKRJR/ddH27mjqt91rx8Ik2H7TF0aD6fsA+KuONq3+EfflRRxw0VEUIA+Pbb71yfNQzbTr5YIlnTI8NEMo2Y/dA8srqx89v737m+SkWatHznsb7rMnnTNs0XS+RremQYT6qIMWcsM3lz13ce67uuSkWatMibe3+EvPFTa54ST9f0yFBLZKDEmf4/b/xU3tzzo0qXpWJC2DmQjN+8b/86E9htzY8nUzW5ZhhLpCGUJnwAAAm3SURBVIaNBE1g981/2b+OAu46z2vvZeIbH+271jQ5W/iWeFKpyTXDwUR+2EjQNLk3Nz7Sdy0F3HWe/gNyPvfPPXdxJmzn4fKJdE2uGWqx9PCRoImtuX/uucvJgLtnSsWEEAB2HB/svnHf/jWsGCZTafQPxmriaIVpmugfjLFrgjCB3Tfu27dmx/HB7ioVbdKzc0+6+8bNvV9lxTCZ0nBiMF8TRytMk8OJAYVdE4Rpcm/esLl39c69aWp/ZeL4W6mB7P09dwwTw1QGSn9tHK3gTRNKf5JdEwRnYmv2/iN3HH8rNVCNchX9n1+KaK2YWtf68MIFj3t5frk13y1KiEZDcEti0c8ezwwdlk9CUexv4lnd2HnzX/avIxGsDCvaArMe+ta0n/vc3FJrviS60BCVIEkVfT+sGHnFwEBMYY9IIJM3d218pO9aEsHKMHVxsN7z3eZNcPM32D6QBPjqQuAkoUolKy+mog2dE2QcC/A545HMv/T831JEkONKe4mtihACQHt9KPLABec/1uTxzATQbv2sLhJGMOAr6fnjjVQ6M8xjDIDO3lzu8Lf3v3M9TYdWlsvm+SL/cv20J5obXDMBXGj9rD4iIRiYXC9jqbSKgfiwsFRdPf364e881ncdTYdWlob5frfvlqYfYoo4DcBa62eeaBCuoLdKJSsPeirLeowBgA7uuNonb+75UanToRNWCE/y60vaHrg4Em4HI4Y+rxfRUBCC6HLke6qFpuqIJVOs71CgcESCdodWl/+4o/XhS85zfxqMGPq8LtSF3RCEiT1dqmkmBhN51ncoUDgiQbtDq0vrplkbcb53ERgx5Hwe+CJ+QJjY/R80HZm4zPoOBQpHJJzaHTrhhRAA7l8w74bVTdPXgBFDAIiGQwgF/Y59VyVJpuRhu0ILdD7b2/sknRMcH9x73bSbrrkiuBaMGHIAImE3wsGJOVWVSCmIJ1SM8J/a9ewrqQ46Jzg+mLGh6Wp+ReTzYMTQ4Ex4wkGIoYk5O6YmR9gVOkSHsSP+OyfPCU4KIQSAb7Q2Lblr7pwfizz/GfYzSRAQCgXg902M6QI5k0UymWaD6gIY8h36o4Pv304eY8YXa5ZFl955bf39ksANexkTRQ6RoAS/b2IIopzREB8eVBfAkO/Qu/9t4HvkMWZ8Mf2zdRcI103byAkQwQgiL4qQQj64/O4qle7s0OU8lBGC6gLoMDVD1TuOP+i0x5hJI4QAsCDk578/d/ampXV1yzDC6NDtFhH0+8etIMqZLFKpDOsv9CSdfxyM7bz34KF/It+h45MFLR7+tmvq7/2bhb7lYEaHAOCWXAgFhHEriHJGQzKtIa+MGAi264//m91537P9t5Pv0PFL650zN6DNfykYMQQAQRIgBP3jVhB1OQ8tJUMbOcpGB/bIfy6XA+1JJYQnWTez+TO3zp51W0hwRTGCIIqCiIDfC7/fCxdf3R1+umEgLWchyzLUkSNRdyY1LfbAhx/9pOOjHnoLnwCsXRFdduuX6u4I+fgoRhBEUeAR8IsI+AS4qryEo+tAOqMgLetQtRH1rSshm7GHfjNwD4VSmhhMv6r+U65rGtbzPlcAIwgiBBekgBcuvwe8q7r9n6Eb0OUclHQWGLn/64BspNVtJ7aWM5TSpBTCk/z4/HPXf31G81qMIIYn8Xm98Hnd8Hk9JRvjTDFNE5lsDplsfqRNMFY6n+7p3XrH2wc7xrqIGJ/cvaZxw7XLQxswghiexOd1we8V4PW6wFeo/RmmiWxWh5wdFkCXpevpncktFFl+YtLyzekruSujXywkhwsihjbVuL1ucD6pYu0PBqBn88hn8yNtgjlJBwCYL8Z+U87I8ieZ1EIIAG0Rv3tda8tNqxobv4wxBBEAvB4PPG4JHkmC5HZ2+3sur0JRFOTyCrK504by6fztsWPbOg4f+emehFx7bksmEYvO8brXXRm5ddWSwGqMIYgA4PW44HEL8Lh5uB0+i5hXDOTyBnJ5DdncmOIHAF2/3Z1+puPF2Oa9H+Wo/U1wWm6ZsZpbGvq7QnJEQQQAweMG7xHBu0W4HO7/9LwKI6/CyKnQxm5SQwK4K/nSkYeObnO0EGMw6YXwJG1hv/trM5rXfqVp+rXckEecMUURGDqgL0oCREGAy+WC6HKB53nwPAeO504ZzzRNmIYJwzBhGAZUXYeu61A1DaqiQVUVnMGiSqcJGNt6e5965mjvkySAk4tF53jdX70ivH715aF/5DiTx2lEERhaUxRFDqLggiAAgouHi+eG2h/HgeOG/odMk4NpDrU/3TCh6QY0DVA1HapqQlH0kXZ+snSZJmdsezX55DOvxLeSAE4+Zmxoutq1PHKVycGFMQQRGNpxKokiOEkAN9T4wLt4uFw8wLvAcyaMQv/HmyYMkwMMHbpuwNANQNNhahpMRYOiquBP73WpgzOh6zvjL1QyasRJakYIrdw0u+Vzq6Y1fuE8v38hzkAQy0znu7K877d9x3716AdHXqpyWYgKcOPK+pWrlgS+Mr9FWogzEMQy03WgW923/X9Sv/zX5wfKPgVFVJ/mq6dcxC0Nfpaf6ZmD0whiBejAR7lD+n8nf9/zXP+bp7+8PNSkEJ6kvT4UWdHQsPKK+rrlc/z++aicKHYekuUDrwwM7txxov958gpTm1w2zxdZsdi/6vI235XnNknzUTlR7Hq/Vznw6p7MizvekreTV5japGG+3+25NLiUXxxYghnSzEJ2uYVxaL/DUeWw+WZ6d/b15K5qOMlmqWkhtLIg5Ofbo5GlF4fD7W3h4IUF121A6eLYCQzFCNyTSHW9kUh0dsbiu+gIBGFlQYuHv2y+9/KL53mWLJrjvajgug0oXRy7gKEYgXsPZd98473c7tcOZF/df4SOQBB2pl9V/ylxvncR5novKLhuA0oXxg5gKEag+X72bfVAdm85d38WCwnhGKyYWtc6x+ed1+r1zmryemdMkaQpdaJQFxDEkJfnfALPiwCgGYaaNYxMWtOSg6o2eCKvnOjNZY92Z7MfHspk3yNn2EQxrGgLzJo9zT23Zaowq6lebJ0a4adFg0Jd0MeFPBLnEwRuqP1ppppTzEwqYyZjKW3weNzo6x1Qu7uPax9+2Jc/uGNP+sNq/xZi4jF1cbBebJZa+UapiZ8iNiIi1JshV8Tw8wFOcrl511D0IUOHweWNPDJ6mkvqccS1AeOEesw4pvSqPUp3tSJCnA2VOjFAEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEARBEAQxzvj/snGtbrdYI/0AAAAASUVORK5CYII=);
+ height: 32px;
+ width: 100%;
+ background-size: 40px;
+ background-repeat: no-repeat;
+ background-color: #282c34;
+ margin-bottom: 0;
+ border-top-left-radius: 5px;
+ border-top-right-radius: 5px;
+ background-position: 10px 10px;
+ }
+
+ table {
+ overflow: auto;
+ border-spacing: 0;
+ border-collapse: collapse;
+ margin-bottom: 1em;
+ }
+
+ table tr th,
+ table tr td {
+ word-wrap: break-word;
+ padding: 8px 14px;
+ border: 1px solid #e6e6e6;
+ }
+
+ table tr:nth-child(2n) {
+ background-color: #fafafa;
+ }
+
+ table tr:hover {
+ background-color: #eee;
+ }
+
+ ol,
+ ul {
+ margin: 0.6em 0;
+ padding-left: 1.6em;
+ }
+
+ ol li,
+ ul li {
+ line-height: 1.6;
+ margin: 0.5em 0;
+ }
+}
diff --git a/web/src/styles/mixins.scss b/web/src/styles/mixins.scss
new file mode 100644
index 0000000..ae0d086
--- /dev/null
+++ b/web/src/styles/mixins.scss
@@ -0,0 +1,30 @@
+@mixin set-css-var-value($name, $value) {
+ #{joinVarName($name)}: #{$value};
+}
+
+@function joinVarName($list) {
+ $name: '--ba';
+ @each $item in $list {
+ @if $item != '' {
+ $name: $name + '-' + $item;
+ }
+ }
+ @return $name;
+}
+
+@function getCssVarName($args...) {
+ @return joinVarName($args);
+}
+
+/*
+ * 通过映射设置所有的CSS变量
+ */
+@mixin set-component-css-var($name, $variables) {
+ @each $attribute, $value in $variables {
+ @if $attribute == 'default' {
+ #{getCssVarName($name)}: #{$value};
+ } @else {
+ #{getCssVarName($name, $attribute)}: #{$value};
+ }
+ }
+}
diff --git a/web/src/styles/var.scss b/web/src/styles/var.scss
new file mode 100644
index 0000000..8559562
--- /dev/null
+++ b/web/src/styles/var.scss
@@ -0,0 +1,32 @@
+@use 'sass:map';
+@use 'mixins' as *;
+
+// 后台主体窗口左右间距
+$main-space: 16px;
+$primary-light: #3f6ad8;
+
+// --ba-background
+$bg-color: () !default;
+$bg-color: map.merge(
+ (
+ '': #f5f5f5,
+ 'overlay': #ffffff,
+ ),
+ $bg-color
+);
+
+// --ba-border-color
+$border-color: () !default;
+$border-color: map.merge(
+ (
+ '': #f6f6f6,
+ ),
+ $border-color
+);
+
+:root {
+ @include set-css-var-value('main-space', $main-space);
+ @include set-css-var-value('color-primary-light', $primary-light);
+ @include set-component-css-var('bg-color', $bg-color);
+ @include set-component-css-var('border-color', $border-color);
+}
diff --git a/web/src/utils/axios.ts b/web/src/utils/axios.ts
new file mode 100644
index 0000000..6b65c07
--- /dev/null
+++ b/web/src/utils/axios.ts
@@ -0,0 +1,382 @@
+import type { AxiosRequestConfig, Method } from 'axios'
+import axios from 'axios'
+import { ElLoading, ElNotification, type LoadingOptions } from 'element-plus'
+import { refreshToken } from '/@/api/common'
+import { i18n } from '/@/lang/index'
+import router from '/@/router/index'
+import adminBaseRoute from '/@/router/static/adminBase'
+import { memberCenterBaseRoutePath } from '/@/router/static/memberCenterBase'
+import { useAdminInfo } from '/@/stores/adminInfo'
+import { useConfig } from '/@/stores/config'
+import { SYSTEM_ZINDEX } from '/@/stores/constant/common'
+import { useUserInfo } from '/@/stores/userInfo'
+import { isAdminApp } from '/@/utils/common'
+
+window.requests = []
+window.tokenRefreshing = false
+const pendingMap = new Map()
+const loadingInstance: LoadingInstance = {
+ target: null,
+ count: 0,
+}
+
+/**
+ * 根据运行环境获取基础请求URL
+ */
+export const getUrl = (): string => {
+ const value: string = import.meta.env.VITE_AXIOS_BASE_URL as string
+ return value == 'getCurrentDomain' ? window.location.protocol + '//' + window.location.host : value
+}
+
+/**
+ * 根据运行环境获取基础请求URL的端口
+ */
+export const getUrlPort = (): string => {
+ const url = getUrl()
+ return new URL(url).port
+}
+
+/**
+ * 创建`Axios`
+ * 默认开启`reductDataFormat(简洁响应)`,返回类型为`ApiPromise`
+ * 关闭`reductDataFormat`,返回类型则为`AxiosPromise`
+ */
+function createAxios>(axiosConfig: AxiosRequestConfig, options: Options = {}, loading: LoadingOptions = {}): T {
+ const config = useConfig()
+ const adminInfo = useAdminInfo()
+ const userInfo = useUserInfo()
+
+ const Axios = axios.create({
+ baseURL: getUrl(),
+ timeout: 1000 * 10,
+ headers: {
+ 'think-lang': config.lang.defaultLang,
+ server: true,
+ },
+ responseType: 'json',
+ })
+
+ // 自定义后台入口
+ if (adminBaseRoute.path != '/admin' && isAdminApp() && /^\/admin\//.test(axiosConfig.url!)) {
+ axiosConfig.url = axiosConfig.url!.replace(/^\/admin\//, adminBaseRoute.path + '.php/')
+ }
+
+ // 合并默认请求选项
+ options = Object.assign(
+ {
+ cancelDuplicateRequest: true, // 是否开启取消重复请求, 默认为 true
+ loading: false, // 是否开启loading层效果, 默认为false
+ reductDataFormat: true, // 是否开启简洁的数据结构响应, 默认为true
+ showErrorMessage: true, // 是否开启接口错误信息展示,默认为true
+ showCodeMessage: true, // 是否开启code不为1时的信息提示, 默认为true
+ showSuccessMessage: false, // 是否开启code为1时的信息提示, 默认为false
+ anotherToken: '', // 当前请求使用另外的用户token
+ },
+ options
+ )
+
+ // 请求拦截
+ Axios.interceptors.request.use(
+ (config) => {
+ removePending(config)
+ options.cancelDuplicateRequest && addPending(config)
+ // 创建loading实例
+ if (options.loading) {
+ loadingInstance.count++
+ if (loadingInstance.count === 1) {
+ loadingInstance.target = ElLoading.service(loading)
+ }
+ }
+
+ // 自动携带token
+ if (config.headers) {
+ const token = adminInfo.getToken()
+ if (token) (config.headers as anyObj).batoken = token
+ const userToken = options.anotherToken || userInfo.getToken()
+ if (userToken) (config.headers as anyObj)['ba-user-token'] = userToken
+ }
+
+ return config
+ },
+ (error) => {
+ return Promise.reject(error)
+ }
+ )
+
+ // 响应拦截
+ Axios.interceptors.response.use(
+ (response) => {
+ removePending(response.config)
+ options.loading && closeLoading(options) // 关闭loading
+
+ if (response.config.responseType == 'json') {
+ if (response.data && response.data.code !== 1) {
+ if (response.data.code == 409) {
+ if (!window.tokenRefreshing) {
+ window.tokenRefreshing = true
+ return refreshToken()
+ .then((res) => {
+ if (res.data.type == 'admin-refresh') {
+ adminInfo.setToken(res.data.token, 'auth')
+ response.headers.batoken = `${res.data.token}`
+ window.requests.forEach((cb) => cb(res.data.token, 'admin-refresh'))
+ } else if (res.data.type == 'user-refresh') {
+ userInfo.setToken(res.data.token, 'auth')
+ response.headers['ba-user-token'] = `${res.data.token}`
+ window.requests.forEach((cb) => cb(res.data.token, 'user-refresh'))
+ }
+ window.requests = []
+ return Axios(response.config)
+ })
+ .catch((err) => {
+ if (isAdminApp()) {
+ adminInfo.removeToken()
+ if (router.currentRoute.value.name != 'adminLogin') {
+ router.push({ name: 'adminLogin' })
+ return Promise.reject(err)
+ } else {
+ response.headers.batoken = ''
+ window.requests.forEach((cb) => cb('', 'admin-refresh'))
+ window.requests = []
+ return Axios(response.config)
+ }
+ } else {
+ userInfo.removeToken()
+ if (router.currentRoute.value.name != 'userLogin') {
+ router.push({ name: 'userLogin' })
+ return Promise.reject(err)
+ } else {
+ response.headers['ba-user-token'] = ''
+ window.requests.forEach((cb) => cb('', 'user-refresh'))
+ window.requests = []
+ return Axios(response.config)
+ }
+ }
+ })
+ .finally(() => {
+ window.tokenRefreshing = false
+ })
+ } else {
+ return new Promise((resolve) => {
+ // 用函数形式将 resolve 存入,等待刷新后再执行
+ window.requests.push((token: string, type: string) => {
+ if (type == 'admin-refresh') {
+ response.headers.batoken = `${token}`
+ } else {
+ response.headers['ba-user-token'] = `${token}`
+ }
+ resolve(Axios(response.config))
+ })
+ })
+ }
+ }
+ if (options.showCodeMessage) {
+ ElNotification({
+ type: 'error',
+ message: response.data.msg,
+ zIndex: SYSTEM_ZINDEX,
+ })
+ }
+ // 自动跳转到路由name或path
+ if (response.data.code == 302) {
+ router.push({ path: response.data.data.routePath ?? '', name: response.data.data.routeName ?? '' })
+ }
+ if (response.data.code == 303) {
+ const isAdminAppFlag = isAdminApp()
+ let routerPath = isAdminAppFlag ? adminBaseRoute.path : memberCenterBaseRoutePath
+
+ // 需要登录,清理 token,转到登录页
+ if (response.data.data.type == 'need login') {
+ if (isAdminAppFlag) {
+ adminInfo.removeToken()
+ } else {
+ userInfo.removeToken()
+ }
+ routerPath += '/login'
+ }
+ router.push({ path: routerPath })
+ }
+ // code不等于1, 页面then内的具体逻辑就不执行了
+ return Promise.reject(response.data)
+ } else if (options.showSuccessMessage && response.data && response.data.code == 1) {
+ ElNotification({
+ message: response.data.msg ? response.data.msg : i18n.global.t('axios.Operation successful'),
+ type: 'success',
+ zIndex: SYSTEM_ZINDEX,
+ })
+ }
+ }
+
+ return options.reductDataFormat ? response.data : response
+ },
+ (error) => {
+ error.config && removePending(error.config)
+ options.loading && closeLoading(options) // 关闭loading
+ options.showErrorMessage && httpErrorStatusHandle(error) // 处理错误状态码
+ return Promise.reject(error) // 错误继续返回给到具体页面
+ }
+ )
+ return Axios(axiosConfig) as T
+}
+
+export default createAxios
+
+/**
+ * 处理异常
+ * @param {*} error
+ */
+function httpErrorStatusHandle(error: any) {
+ // 处理被取消的请求
+ if (axios.isCancel(error)) return console.error(i18n.global.t('axios.Automatic cancellation due to duplicate request:') + error.message)
+ let message = ''
+ if (error && error.response) {
+ switch (error.response.status) {
+ case 302:
+ message = i18n.global.t('axios.Interface redirected!')
+ break
+ case 400:
+ message = i18n.global.t('axios.Incorrect parameter!')
+ break
+ case 401:
+ message = i18n.global.t('axios.You do not have permission to operate!')
+ break
+ case 403:
+ message = i18n.global.t('axios.You do not have permission to operate!')
+ break
+ case 404:
+ message = i18n.global.t('axios.Error requesting address:') + error.response.config.url
+ break
+ case 408:
+ message = i18n.global.t('axios.Request timed out!')
+ break
+ case 409:
+ message = i18n.global.t('axios.The same data already exists in the system!')
+ break
+ case 500:
+ message = i18n.global.t('axios.Server internal error!')
+ break
+ case 501:
+ message = i18n.global.t('axios.Service not implemented!')
+ break
+ case 502:
+ message = i18n.global.t('axios.Gateway error!')
+ break
+ case 503:
+ message = i18n.global.t('axios.Service unavailable!')
+ break
+ case 504:
+ message = i18n.global.t('axios.The service is temporarily unavailable Please try again later!')
+ break
+ case 505:
+ message = i18n.global.t('axios.HTTP version is not supported!')
+ break
+ default:
+ message = i18n.global.t('axios.Abnormal problem, please contact the website administrator!')
+ break
+ }
+ }
+ if (error.message.includes('timeout')) message = i18n.global.t('axios.Network request timeout!')
+ if (error.message.includes('Network'))
+ message = window.navigator.onLine ? i18n.global.t('axios.Server exception!') : i18n.global.t('axios.You are disconnected!')
+
+ ElNotification({
+ type: 'error',
+ message,
+ zIndex: SYSTEM_ZINDEX,
+ })
+}
+
+/**
+ * 关闭Loading层实例
+ */
+function closeLoading(options: Options) {
+ if (options.loading && loadingInstance.count > 0) loadingInstance.count--
+ if (loadingInstance.count === 0) {
+ loadingInstance.target.close()
+ loadingInstance.target = null
+ }
+}
+
+/**
+ * 储存每个请求的唯一cancel回调, 以此为标识
+ */
+function addPending(config: AxiosRequestConfig) {
+ const pendingKey = getPendingKey(config)
+ config.cancelToken =
+ config.cancelToken ||
+ new axios.CancelToken((cancel) => {
+ if (!pendingMap.has(pendingKey)) {
+ pendingMap.set(pendingKey, cancel)
+ }
+ })
+}
+
+/**
+ * 删除重复的请求
+ */
+function removePending(config: AxiosRequestConfig) {
+ const pendingKey = getPendingKey(config)
+ if (pendingMap.has(pendingKey)) {
+ const cancelToken = pendingMap.get(pendingKey)
+ cancelToken(pendingKey)
+ pendingMap.delete(pendingKey)
+ }
+}
+
+/**
+ * 生成每个请求的唯一key
+ */
+function getPendingKey(config: AxiosRequestConfig) {
+ let { data } = config
+ const { url, method, params, headers } = config
+ if (typeof data === 'string') data = JSON.parse(data) // response里面返回的config.data是个字符串对象
+ return [
+ url,
+ method,
+ headers && (headers as anyObj).batoken ? (headers as anyObj).batoken : '',
+ headers && (headers as anyObj)['ba-user-token'] ? (headers as anyObj)['ba-user-token'] : '',
+ JSON.stringify(params),
+ JSON.stringify(data),
+ ].join('&')
+}
+
+/**
+ * 根据请求方法组装请求数据/参数
+ */
+export function requestPayload(method: Method, data: anyObj) {
+ if (method == 'GET') {
+ return {
+ params: data,
+ }
+ } else if (method == 'POST') {
+ return {
+ data: data,
+ }
+ }
+}
+
+interface LoadingInstance {
+ target: any
+ count: number
+}
+interface Options {
+ // 是否开启取消重复请求, 默认为 true
+ cancelDuplicateRequest?: boolean
+ // 是否开启loading层效果, 默认为false
+ loading?: boolean
+ // 是否开启简洁的数据结构响应, 默认为true
+ reductDataFormat?: boolean
+ // 是否开启接口错误信息展示,默认为true
+ showErrorMessage?: boolean
+ // 是否开启code不为1时的信息提示, 默认为true
+ showCodeMessage?: boolean
+ // 是否开启code为1时的信息提示, 默认为false
+ showSuccessMessage?: boolean
+ // 当前请求使用另外的用户token
+ anotherToken?: string
+}
+
+/*
+ * 感谢掘金@橙某人提供的思路和分享
+ * 本axios封装详细解释请参考:https://juejin.cn/post/6968630178163458084?share_token=7831c9e0-bea0-469e-8028-b587e13681a8#heading-27
+ */
diff --git a/web/src/utils/baTable.ts b/web/src/utils/baTable.ts
new file mode 100644
index 0000000..bc76ebc
--- /dev/null
+++ b/web/src/utils/baTable.ts
@@ -0,0 +1,684 @@
+import type { FormInstance, TableColumnCtx } from 'element-plus'
+import { ElNotification, dayjs } from 'element-plus'
+import { cloneDeep, isArray, isEmpty } from 'lodash-es'
+import Sortable from 'sortablejs'
+import { reactive } from 'vue'
+import { useRoute } from 'vue-router'
+import type { baTableApi } from '/@/api/common'
+import { findIndexRow } from '/@/components/table'
+import { i18n } from '/@/lang/index'
+import { auth, getArrayKey } from '/@/utils/common'
+
+/**
+ * 表格管家类
+ */
+export default class baTable {
+ /** baTableApi 类的实例,开发者可重写该类 */
+ public api: baTableApi
+
+ /** 表格状态,属性对应含义请查阅 BaTable 的类型定义 */
+ public table: BaTable = reactive({
+ ref: undefined,
+ pk: 'id',
+ data: [],
+ remark: null,
+ loading: false,
+ selection: [],
+ column: [],
+ total: 0,
+ filter: {},
+ dragSortLimitField: 'pid',
+ acceptQuery: true,
+ showComSearch: false,
+ dblClickNotEditColumn: [undefined],
+ expandAll: false,
+ extend: {},
+ })
+
+ /** 表单状态,属性对应含义请查阅 BaTableForm 的类型定义 */
+ public form: BaTableForm = reactive({
+ ref: undefined,
+ labelWidth: 160,
+ operate: '',
+ operateIds: [],
+ items: {},
+ submitLoading: false,
+ defaultItems: {},
+ loading: false,
+ extend: {},
+ })
+
+ /** BaTable 前置处理函数列表(前置埋点) */
+ public before: BaTableBefore
+
+ /** BaTable 后置处理函数列表(后置埋点) */
+ public after: BaTableAfter
+
+ /** 公共搜索数据 */
+ public comSearch: ComSearch = reactive({
+ form: {},
+ fieldData: new Map(),
+ })
+
+ constructor(api: baTableApi, table: BaTable, form: BaTableForm = {}, before: BaTableBefore = {}, after: BaTableAfter = {}) {
+ this.api = api
+ this.form = Object.assign(this.form, form)
+ this.table = Object.assign(this.table, table)
+ this.before = before
+ this.after = after
+ }
+
+ /**
+ * 表格内部鉴权方法
+ * 此方法在表头或表行组件内部自动调用,传递权限节点名,如:add、edit
+ * 若需自定义表格内部鉴权,重写此方法即可
+ */
+ auth(node: string) {
+ return auth(node)
+ }
+
+ /**
+ * 运行前置函数
+ * @param funName 函数名
+ * @param args 参数
+ */
+ runBefore(funName: string, args: any = {}) {
+ if (this.before && this.before[funName] && typeof this.before[funName] == 'function') {
+ return this.before[funName]!({ ...args }) === false ? false : true
+ }
+ return true
+ }
+
+ /**
+ * 运行后置函数
+ * @param funName 函数名
+ * @param args 参数
+ */
+ runAfter(funName: string, args: any = {}) {
+ if (this.after && this.after[funName] && typeof this.after[funName] == 'function') {
+ return this.after[funName]!({ ...args }) === false ? false : true
+ }
+ return true
+ }
+
+ /**
+ * 表格数据获取(请求表格对应控制器的查看方法)
+ * @alias getIndex
+ */
+ getData = () => {
+ if (this.runBefore('getData') === false) return
+ if (this.runBefore('getIndex') === false) return
+ this.table.loading = true
+ return this.api
+ .index(this.table.filter)
+ .then((res) => {
+ this.table.data = res.data.list
+ this.table.total = res.data.total
+ this.table.remark = res.data.remark
+ this.runAfter('getData', { res })
+ this.runAfter('getIndex', { res })
+ })
+ .catch((err) => {
+ this.runAfter('getData', { err })
+ this.runAfter('getIndex', { err })
+ })
+ .finally(() => {
+ this.table.loading = false
+ })
+ }
+
+ /**
+ * 删除数据
+ */
+ postDel = (ids: string[]) => {
+ if (this.runBefore('postDel', { ids }) === false) return
+ this.api.del(ids).then((res) => {
+ this.onTableHeaderAction('refresh', { event: 'delete', ids })
+ this.runAfter('postDel', { res })
+ })
+ }
+
+ /**
+ * 获取被编辑行数据
+ * @alias requestEdit
+ */
+ getEditData = (id: string) => {
+ if (this.runBefore('getEditData', { id }) === false) return
+ if (this.runBefore('requestEdit', { id }) === false) return
+ this.form.loading = true
+ this.form.items = {}
+ return this.api
+ .edit({
+ [this.table.pk!]: id,
+ })
+ .then((res) => {
+ this.form.items = res.data.row
+ this.runAfter('getEditData', { res })
+ this.runAfter('requestEdit', { res })
+ })
+ .catch((err) => {
+ this.toggleForm()
+ this.runAfter('getEditData', { err })
+ this.runAfter('requestEdit', { err })
+ })
+ .finally(() => {
+ this.form.loading = false
+ })
+ }
+
+ /**
+ * 双击表格
+ * @param row 行数据
+ * @param column 列上下文数据
+ */
+ onTableDblclick = (row: TableRow, column: TableColumnCtx) => {
+ if (!this.table.dblClickNotEditColumn!.includes('all') && !this.table.dblClickNotEditColumn!.includes(column.property)) {
+ if (this.runBefore('onTableDblclick', { row, column }) === false) return
+ this.toggleForm('Edit', [row[this.table.pk!]])
+ this.runAfter('onTableDblclick', { row, column })
+ }
+ }
+
+ /**
+ * 打开表单
+ * @param operate 操作:Add=添加,Edit=编辑
+ * @param operateIds 被操作项的数组:Add=[],Edit=[1,2,...]
+ */
+ toggleForm = (operate = '', operateIds: string[] = []) => {
+ if (this.runBefore('toggleForm', { operate, operateIds }) === false) return
+ if (operate == 'Edit') {
+ if (!operateIds.length) {
+ return false
+ }
+ this.getEditData(operateIds[0])
+ } else if (operate == 'Add') {
+ this.form.items = cloneDeep(this.form.defaultItems)
+ }
+ this.form.operate = operate
+ this.form.operateIds = operateIds
+ this.runAfter('toggleForm', { operate, operateIds })
+ }
+
+ /**
+ * 提交表单
+ * @param formEl 表单组件ref
+ */
+ onSubmit = (formEl?: FormInstance | null) => {
+ // 当前操作的首字母小写
+ const operate = this.form.operate!.replace(this.form.operate![0], this.form.operate![0].toLowerCase())
+
+ if (this.runBefore('onSubmit', { formEl: formEl, operate: operate, items: this.form.items! }) === false) return
+
+ // 表单验证通过后执行的 api 请求操作
+ const submitCallback = () => {
+ this.form.submitLoading = true
+ this.api
+ .postData(operate, this.form.items!)
+ .then((res) => {
+ this.onTableHeaderAction('refresh', { event: 'submit', operate, items: this.form.items })
+ this.form.operateIds?.shift()
+ if (this.form.operateIds!.length > 0) {
+ this.toggleForm('Edit', this.form.operateIds)
+ } else {
+ this.toggleForm()
+ }
+ this.runAfter('onSubmit', { res })
+ })
+ .finally(() => {
+ this.form.submitLoading = false
+ })
+ }
+
+ if (formEl) {
+ this.form.ref = formEl
+ formEl.validate((valid: boolean) => {
+ if (valid) {
+ submitCallback()
+ }
+ })
+ } else {
+ submitCallback()
+ }
+ }
+
+ /**
+ * 获取表格选择项的主键数组
+ */
+ getSelectionIds() {
+ const ids: string[] = []
+ this.table.selection?.forEach((item) => {
+ ids.push(item[this.table.pk!])
+ })
+ return ids
+ }
+
+ /**
+ * 表格内的事件统一响应
+ * @param event 事件名称,含义请参考其类型定义
+ * @param data 携带数据
+ */
+ onTableAction = (event: BaTableActionEventName, data: anyObj) => {
+ if (this.runBefore('onTableAction', { event, data }) === false) return
+ const actionFun = new Map([
+ [
+ 'selection-change',
+ () => {
+ this.table.selection = data as TableRow[]
+ },
+ ],
+ [
+ 'page-size-change',
+ () => {
+ this.table.filter!.limit = data.size
+ this.onTableHeaderAction('refresh', { event: 'page-size-change', ...data })
+ },
+ ],
+ [
+ 'current-page-change',
+ () => {
+ this.table.filter!.page = data.page
+ this.onTableHeaderAction('refresh', { event: 'current-page-change', ...data })
+ },
+ ],
+ [
+ 'sort-change',
+ () => {
+ let newOrder: string | undefined
+ if (data.prop && data.order) {
+ newOrder = data.prop + ',' + data.order
+ }
+ if (newOrder != this.table.filter!.order) {
+ this.table.filter!.order = newOrder
+ this.onTableHeaderAction('refresh', { event: 'sort-change', ...data })
+ }
+ },
+ ],
+ [
+ 'edit',
+ () => {
+ this.toggleForm('Edit', [data.row[this.table.pk!]])
+ },
+ ],
+ [
+ 'delete',
+ () => {
+ this.postDel([data.row[this.table.pk!]])
+ },
+ ],
+ [
+ 'field-change',
+ () => {
+ if (data.field && data.field.prop && this.table.data![data.index]) {
+ this.table.data![data.index][data.field.prop!] = data.value
+ }
+ },
+ ],
+ [
+ 'com-search',
+ () => {
+ // 主动触发公共搜索,采用覆盖模式设定请求筛选数据
+ this.setFilterSearchData(this.getComSearchData(), 'cover')
+
+ // 刷新表格
+ this.onTableHeaderAction('refresh', { event: 'com-search', data: this.table.filter!.search })
+ },
+ ],
+ [
+ 'default',
+ () => {
+ console.warn('No action defined')
+ },
+ ],
+ ])
+
+ const action = actionFun.get(event) || actionFun.get('default')
+ action!.call(this)
+ return this.runAfter('onTableAction', { event, data })
+ }
+
+ /**
+ * 表格顶栏按钮事件统一响应
+ * @param event 事件名称,含义参考其类型定义
+ * @param data 携带数据
+ */
+ onTableHeaderAction = (event: BaTableHeaderActionEventName, data: anyObj) => {
+ if (this.runBefore('onTableHeaderAction', { event, data }) === false) return
+ const actionFun = new Map([
+ [
+ 'refresh',
+ () => {
+ // 刷新表格在大多数情况下无需置空 data,但任需防范表格列组件的 :key 不会被更新的问题,比如关联表的数据列
+ this.table.data = []
+ this.getData()
+ },
+ ],
+ [
+ 'add',
+ () => {
+ this.toggleForm('Add')
+ },
+ ],
+ [
+ 'edit',
+ () => {
+ this.toggleForm('Edit', this.getSelectionIds())
+ },
+ ],
+ [
+ 'delete',
+ () => {
+ this.postDel(this.getSelectionIds())
+ },
+ ],
+ [
+ 'unfold',
+ () => {
+ if (!this.table.ref) {
+ console.warn('Collapse/expand failed because table ref is not defined. Please assign table ref when onMounted')
+ return
+ }
+ this.table.expandAll = data.unfold
+ this.table.ref.unFoldAll(data.unfold)
+ },
+ ],
+ [
+ 'quick-search',
+ () => {
+ this.onTableHeaderAction('refresh', { event: 'quick-search', ...data })
+ },
+ ],
+ [
+ 'change-show-column',
+ () => {
+ const columnKey = getArrayKey(this.table.column, 'prop', data.field)
+ this.table.column[columnKey].show = data.value
+ },
+ ],
+ [
+ 'default',
+ () => {
+ console.warn('No action defined')
+ },
+ ],
+ ])
+
+ const action = actionFun.get(event) || actionFun.get('default')
+ action!.call(this)
+ return this.runAfter('onTableHeaderAction', { event, data })
+ }
+
+ /**
+ * 初始化默认排序
+ * el-table 的 `default-sort` 在自定义排序时无效
+ * 此方法只有在表格数据请求结束后执行有效
+ */
+ initSort = () => {
+ if (this.table.defaultOrder && this.table.defaultOrder.prop) {
+ if (!this.table.ref) {
+ console.warn('Failed to initialize default sorting because table ref is not defined. Please assign table ref when onMounted')
+ return
+ }
+
+ const defaultOrder = this.table.defaultOrder.prop + ',' + this.table.defaultOrder.order
+ if (this.table.filter && this.table.filter.order != defaultOrder) {
+ this.table.filter.order = defaultOrder
+ this.table.ref.getRef()?.sort(this.table.defaultOrder.prop, this.table.defaultOrder.order == 'desc' ? 'descending' : 'ascending')
+ }
+ }
+ }
+
+ /**
+ * 初始化表格拖动排序
+ */
+ dragSort = () => {
+ const buttonsKey = getArrayKey(this.table.column, 'render', 'buttons')
+ if (buttonsKey === false) return
+ const moveButton = getArrayKey(this.table.column[buttonsKey]?.buttons, 'render', 'moveButton')
+ if (moveButton === false) return
+ if (!this.table.ref) {
+ console.warn('Failed to initialize drag sort because table ref is not defined. Please assign table ref when onMounted')
+ return
+ }
+
+ const el = this.table.ref.getRef()?.$el.querySelector('.el-table__body-wrapper .el-table__body tbody')
+ const disabledTip = this.table.column[buttonsKey].buttons![moveButton].disabledTip
+ Sortable.create(el, {
+ animation: 200,
+ handle: '.table-row-weigh-sort',
+ ghostClass: 'ba-table-row',
+ onStart: () => {
+ this.table.column[buttonsKey].buttons![moveButton].disabledTip = true
+ },
+ onEnd: (evt: Sortable.SortableEvent) => {
+ this.table.column[buttonsKey].buttons![moveButton].disabledTip = disabledTip
+
+ // 目标位置不变
+ if (evt.oldIndex == evt.newIndex || typeof evt.newIndex == 'undefined' || typeof evt.oldIndex == 'undefined') return
+
+ // 找到对应行id
+ const moveRow = findIndexRow(this.table.data!, evt.oldIndex) as TableRow
+ const targetRow = findIndexRow(this.table.data!, evt.newIndex) as TableRow
+
+ const eventData = {
+ move: moveRow[this.table.pk!],
+ target: targetRow[this.table.pk!],
+ order: this.table.filter?.order,
+ direction: evt.newIndex > evt.oldIndex ? 'down' : 'up',
+ }
+
+ if (this.table.dragSortLimitField && moveRow[this.table.dragSortLimitField] != targetRow[this.table.dragSortLimitField]) {
+ this.onTableHeaderAction('refresh', { event: 'sort', ...eventData })
+ ElNotification({
+ type: 'error',
+ message: i18n.global.t('utils.The moving position is beyond the movable range!'),
+ })
+ return
+ }
+
+ this.api.sortable(eventData).finally(() => {
+ this.onTableHeaderAction('refresh', { event: 'sort', ...eventData })
+ })
+ },
+ })
+ }
+
+ /**
+ * 表格初始化
+ */
+ mount = () => {
+ if (this.runBefore('mount') === false) return
+
+ // 记录表格的路由路径
+ const route = useRoute()
+ this.table.routePath = route.fullPath
+
+ // 按需初始化公共搜索表单数据和字段Map
+ if (this.comSearch.fieldData.size === 0) {
+ this.initComSearch()
+ }
+
+ if (this.table.acceptQuery && !isEmpty(route.query)) {
+ // 根据当前 URL 的 query 初始化公共搜索默认值
+ this.setComSearchData(route.query)
+
+ // 获取公共搜索数据合并至表格筛选条件
+ this.setFilterSearchData(this.getComSearchData(), 'merge')
+ }
+ }
+
+ /**
+ * 公共搜索初始化
+ */
+ initComSearch = () => {
+ const form: anyObj = {}
+ const field = this.table.column
+
+ if (field.length <= 0) return
+
+ for (const key in field) {
+ // 关闭搜索的字段
+ if (field[key].operator === false) continue
+
+ // 取默认操作符号
+ if (typeof field[key].operator == 'undefined') {
+ field[key].operator = 'eq'
+ }
+
+ // 公共搜索表单字段初始化
+ const prop = field[key].prop
+ if (prop) {
+ if (field[key].operator == 'RANGE' || field[key].operator == 'NOT RANGE') {
+ // 范围查询
+ form[prop] = ''
+ form[prop + '-start'] = ''
+ form[prop + '-end'] = ''
+ } else if (field[key].operator == 'NULL' || field[key].operator == 'NOT NULL') {
+ // 复选框
+ form[prop] = false
+ } else {
+ // 普通文本框
+ form[prop] = ''
+ }
+
+ // 初始化字段的公共搜索数据
+ this.comSearch.fieldData.set(prop, {
+ operator: field[key].operator,
+ render: field[key].render,
+ comSearchRender: field[key].comSearchRender,
+ })
+ }
+ }
+
+ this.comSearch.form = Object.assign(this.comSearch.form, form)
+ }
+
+ /**
+ * 设置公共搜索表单数据
+ */
+ setComSearchData = (query: anyObj) => {
+ // 必需已经完成公共搜索数据的初始化
+ if (this.comSearch.fieldData.size === 0) {
+ this.initComSearch()
+ }
+
+ for (const key in this.table.column) {
+ const prop = this.table.column[key].prop
+ if (prop && typeof query[prop] !== 'undefined') {
+ const queryProp = query[prop] ?? ''
+ if (this.table.column[key].operator == 'RANGE' || this.table.column[key].operator == 'NOT RANGE') {
+ const range = queryProp.split(',')
+ if (this.table.column[key].render == 'datetime' || this.table.column[key].comSearchRender == 'date') {
+ if (range && range.length >= 2) {
+ const rangeDayJs = [dayjs(range[0]), dayjs(range[1])]
+ if (rangeDayJs[0].isValid() && rangeDayJs[1].isValid()) {
+ if (this.table.column[key].comSearchRender == 'date') {
+ this.comSearch.form[prop] = [rangeDayJs[0].format('YYYY-MM-DD'), rangeDayJs[1].format('YYYY-MM-DD')]
+ } else {
+ this.comSearch.form[prop] = [
+ rangeDayJs[0].format('YYYY-MM-DD HH:mm:ss'),
+ rangeDayJs[1].format('YYYY-MM-DD HH:mm:ss'),
+ ]
+ }
+ }
+ }
+ } else if (this.table.column[key].comSearchRender == 'time') {
+ if (range && range.length >= 2) {
+ this.comSearch.form[prop] = [range[0], range[1]]
+ }
+ } else {
+ this.comSearch.form[prop + '-start'] = range[0] ?? ''
+ this.comSearch.form[prop + '-end'] = range[1] ?? ''
+ }
+ } else if (this.table.column[key].operator == 'NULL' || this.table.column[key].operator == 'NOT NULL') {
+ this.comSearch.form[prop] = queryProp ? true : false
+ } else if (this.table.column[key].render == 'datetime' || this.table.column[key].comSearchRender == 'date') {
+ const propDayJs = dayjs(queryProp)
+ if (propDayJs.isValid()) {
+ this.comSearch.form[prop] = propDayJs.format(
+ this.table.column[key].comSearchRender == 'date' ? 'YYYY-MM-DD' : 'YYYY-MM-DD HH:mm:ss'
+ )
+ }
+ } else {
+ this.comSearch.form[prop] = queryProp
+ }
+ }
+ }
+ }
+
+ /**
+ * 获取公共搜索表单数据
+ */
+ getComSearchData = () => {
+ // 必需已经完成公共搜索数据的初始化
+ if (this.comSearch.fieldData.size === 0) {
+ this.initComSearch()
+ }
+
+ const comSearchData: ComSearchData[] = []
+
+ for (const key in this.comSearch.form) {
+ if (!this.comSearch.fieldData.has(key)) continue
+
+ let val = null
+ const fieldDataTemp = this.comSearch.fieldData.get(key)
+ if (
+ (fieldDataTemp.render == 'datetime' || ['datetime', 'date', 'time'].includes(fieldDataTemp.comSearchRender)) &&
+ (fieldDataTemp.operator == 'RANGE' || fieldDataTemp.operator == 'NOT RANGE')
+ ) {
+ if (this.comSearch.form[key] && this.comSearch.form[key].length >= 2) {
+ // 日期范围
+ if (fieldDataTemp.comSearchRender == 'date') {
+ val = this.comSearch.form[key][0] + ' 00:00:00' + ',' + this.comSearch.form[key][1] + ' 23:59:59'
+ } else {
+ // 时间范围、时间日期范围
+ val = this.comSearch.form[key][0] + ',' + this.comSearch.form[key][1]
+ }
+ }
+ } else if (fieldDataTemp.operator == 'RANGE' || fieldDataTemp.operator == 'NOT RANGE') {
+ // 普通的范围筛选,公共搜索初始化时已准备好 start 和 end 字段
+ if (!this.comSearch.form[key + '-start'] && !this.comSearch.form[key + '-end']) {
+ continue
+ }
+ val = this.comSearch.form[key + '-start'] + ',' + this.comSearch.form[key + '-end']
+ } else if (this.comSearch.form[key]) {
+ val = this.comSearch.form[key]
+ }
+
+ if (val === null) continue
+ if (isArray(val) && !val.length) continue
+
+ comSearchData.push({
+ field: key,
+ val: val,
+ operator: fieldDataTemp.operator,
+ render: fieldDataTemp.render,
+ })
+ }
+
+ return comSearchData
+ }
+
+ /**
+ * 设置 getData 请求时的过滤条件(搜索数据)
+ * @param search 新的搜索数据
+ * @param mode 模式:cover=覆盖到已有搜索数据,merge=合并到已有搜索数据
+ */
+ setFilterSearchData = (search: ComSearchData[], mode: 'cover' | 'merge' = 'merge') => {
+ if (mode == 'cover' || !this.table.filter?.search) {
+ this.table.filter!.search = search
+ } else {
+ const merged = this.table.filter!.search.concat(search)
+ const fieldMap = new Map()
+
+ merged.forEach((item) => {
+ fieldMap.set(item.field, item)
+ })
+
+ this.table.filter!.search = Array.from(fieldMap.values())
+ }
+ }
+
+ // 方法别名
+ getIndex = this.getData
+ requestEdit = this.getEditData
+}
diff --git a/web/src/utils/build.ts b/web/src/utils/build.ts
new file mode 100644
index 0000000..8370d31
--- /dev/null
+++ b/web/src/utils/build.ts
@@ -0,0 +1,37 @@
+import { readdirSync, writeFile } from 'fs'
+import { trimEnd } from 'lodash-es'
+
+function getFileNames(dir: string) {
+ const dirents = readdirSync(dir, {
+ withFileTypes: true,
+ })
+ const fileNames: string[] = []
+ for (const dirent of dirents) {
+ if (!dirent.isDirectory()) fileNames.push(dirent.name.replace('.vue', ''))
+ }
+ return fileNames
+}
+
+/**
+ * 生成 ./types/tableRenderer.d.ts 文件
+ */
+const buildTableRendererType = () => {
+ let tableRenderer = getFileNames('./src/components/table/fieldRender/')
+
+ // 增加 slot,去除 default
+ tableRenderer.push('slot')
+ tableRenderer = tableRenderer.filter((item) => item !== 'default')
+
+ let tableRendererContent =
+ '/** 可用的表格单元格渲染器,以 ./src/components/table/fieldRender/ 目录中的文件名自动生成 */\ntype TableRenderer =\n | '
+ for (const key in tableRenderer) {
+ tableRendererContent += `'${tableRenderer[key]}'\n | `
+ }
+ tableRendererContent = trimEnd(tableRendererContent, ' | ')
+
+ writeFile('./types/tableRenderer.d.ts', tableRendererContent, 'utf-8', (err) => {
+ if (err) throw err
+ })
+}
+
+buildTableRendererType()
diff --git a/web/src/utils/common.ts b/web/src/utils/common.ts
new file mode 100644
index 0000000..ad3aaef
--- /dev/null
+++ b/web/src/utils/common.ts
@@ -0,0 +1,404 @@
+import * as elIcons from '@element-plus/icons-vue'
+import { useTitle } from '@vueuse/core'
+import type { FormInstance } from 'element-plus'
+import { isArray, isNull, trim, trimStart } from 'lodash-es'
+import type { App } from 'vue'
+import { nextTick } from 'vue'
+import type { TranslateOptions } from 'vue-i18n'
+import { i18n } from '../lang'
+import { useSiteConfig } from '../stores/siteConfig'
+import { getUrl } from './axios'
+import Icon from '/@/components/icon/index.vue'
+import router from '/@/router/index'
+import { adminBaseRoutePath } from '/@/router/static/adminBase'
+import { useMemberCenter } from '/@/stores/memberCenter'
+import { useNavTabs } from '/@/stores/navTabs'
+
+export function registerIcons(app: App) {
+ /*
+ * 全局注册 Icon
+ * 使用方式:
+ * 详见<待完善>
+ */
+ app.component('Icon', Icon)
+
+ /*
+ * 全局注册element Plus的icon
+ */
+ const icons = elIcons as any
+ for (const i in icons) {
+ app.component(`el-icon-${icons[i].name}`, icons[i])
+ }
+}
+
+/**
+ * 加载网络css文件
+ * @param url css资源url
+ */
+export function loadCss(url: string): void {
+ const link = document.createElement('link')
+ link.rel = 'stylesheet'
+ link.href = url
+ link.crossOrigin = 'anonymous'
+ document.getElementsByTagName('head')[0].appendChild(link)
+}
+
+/**
+ * 加载网络js文件
+ * @param url js资源url
+ */
+export function loadJs(url: string): void {
+ const link = document.createElement('script')
+ link.src = url
+ document.body.appendChild(link)
+}
+
+/**
+ * 根据路由 meta.title 设置浏览器标题
+ */
+export function setTitleFromRoute() {
+ nextTick(() => {
+ if (typeof router.currentRoute.value.meta.title != 'string') {
+ return
+ }
+ const webTitle = i18n.global.te(router.currentRoute.value.meta.title)
+ ? i18n.global.t(router.currentRoute.value.meta.title)
+ : router.currentRoute.value.meta.title
+ const title = useTitle()
+ const siteConfig = useSiteConfig()
+ title.value = `${webTitle}${siteConfig.siteName ? ' - ' + siteConfig.siteName : ''}`
+ })
+}
+
+/**
+ * 设置浏览器标题
+ * @param webTitle 新的标题
+ */
+export function setTitle(webTitle: string) {
+ if (router.currentRoute.value) {
+ router.currentRoute.value.meta.title = webTitle
+ }
+ nextTick(() => {
+ const title = useTitle()
+ const siteConfig = useSiteConfig()
+ title.value = `${webTitle}${siteConfig.siteName ? ' - ' + siteConfig.siteName : ''}`
+ })
+}
+
+/**
+ * 是否是外部链接
+ * @param path
+ */
+export function isExternal(path: string): boolean {
+ return /^(https?|ftp|mailto|tel):/.test(path)
+}
+
+/**
+ * 全局防抖
+ * 与 _.debounce 不同的是,间隔期间如果再次传递不同的函数,两个函数也只会执行一次
+ * @param fn 执行函数
+ * @param ms 间隔毫秒数
+ */
+export const debounce = (fn: Function, ms: number) => {
+ return (...args: any[]) => {
+ if (window.lazy) {
+ clearTimeout(window.lazy)
+ }
+ window.lazy = window.setTimeout(() => {
+ fn(...args)
+ }, ms)
+ }
+}
+
+/**
+ * 根据pk字段的值从数组中获取key
+ * @param arr
+ * @param pk
+ * @param value
+ */
+export const getArrayKey = (arr: any, pk: string, value: any): any => {
+ for (const key in arr) {
+ if (arr[key][pk] == value) {
+ return key
+ }
+ }
+ return false
+}
+
+/**
+ * 表单重置
+ * @param formEl
+ */
+export const onResetForm = (formEl?: FormInstance | null) => {
+ typeof formEl?.resetFields == 'function' && formEl.resetFields()
+}
+
+/**
+ * 将数据构建为ElTree的data {label:'', children: []}
+ * @param data
+ */
+export const buildJsonToElTreeData = (data: any): ElTreeData[] => {
+ if (typeof data == 'object') {
+ const childrens = []
+ for (const key in data) {
+ childrens.push({
+ label: key + ': ' + data[key],
+ children: buildJsonToElTreeData(data[key]),
+ })
+ }
+ return childrens
+ } else {
+ return []
+ }
+}
+
+/**
+ * 是否在后台应用内
+ * @param path 不传递则通过当前路由 path 检查
+ */
+export const isAdminApp = (path = '') => {
+ const regex = new RegExp(`^${adminBaseRoutePath}`)
+ if (path) {
+ return regex.test(path)
+ }
+ if (regex.test(getCurrentRoutePath())) {
+ return true
+ }
+ return false
+}
+
+/**
+ * 是否为手机设备
+ */
+export const isMobile = () => {
+ return !!navigator.userAgent.match(
+ /android|webos|ip(hone|ad|od)|opera (mini|mobi|tablet)|iemobile|windows.+(phone|touch)|mobile|fennec|kindle (Fire)|Silk|maemo|blackberry|playbook|bb10\; (touch|kbd)|Symbian(OS)|Ubuntu Touch/i
+ )
+}
+
+/**
+ * 从一个文件路径中获取文件名
+ * @param path 文件路径
+ */
+export const getFileNameFromPath = (path: string) => {
+ const paths = path.split('/')
+ return paths[paths.length - 1]
+}
+
+export function auth(node: string): boolean
+export function auth(node: { name: string; subNodeName?: string }): boolean
+
+/**
+ * 鉴权
+ * 提供 string 将根据当前路由 path 自动拼接和鉴权,还可以提供路由的 name 对象进行鉴权
+ * @param node
+ */
+export function auth(node: string | { name: string; subNodeName?: string }) {
+ const store = isAdminApp() ? useNavTabs() : useMemberCenter()
+ if (typeof node === 'string') {
+ const path = getCurrentRoutePath()
+ if (store.state.authNode.has(path)) {
+ const subNodeName = path + (path == '/' ? '' : '/') + node
+ if (store.state.authNode.get(path)!.some((v: string) => v == subNodeName)) {
+ return true
+ }
+ }
+ } else {
+ // 节点列表中没有找到 name
+ if (!node.name || !store.state.authNode.has(node.name)) return false
+
+ // 无需继续检查子节点或未找到子节点
+ if (!node.subNodeName || store.state.authNode.get(node.name)?.includes(node.subNodeName)) return true
+ }
+ return false
+}
+
+/**
+ * 获取资源完整地址
+ * @param relativeUrl 资源相对地址
+ * @param domain 指定域名
+ */
+export const fullUrl = (relativeUrl: string, domain = '') => {
+ const siteConfig = useSiteConfig()
+ if (!domain) {
+ domain = siteConfig.cdnUrl ? siteConfig.cdnUrl : getUrl()
+ }
+ if (!relativeUrl) return domain
+
+ const regUrl = new RegExp(/^http(s)?:\/\//)
+ const regexImg = new RegExp(/^((?:[a-z]+:)?\/\/|data:image\/)(.*)/i)
+ if (!domain || regUrl.test(relativeUrl) || regexImg.test(relativeUrl)) {
+ return relativeUrl
+ }
+
+ let url = domain + relativeUrl
+ if (domain === siteConfig.cdnUrl && siteConfig.cdnUrlParams) {
+ const separator = url.includes('?') ? '&' : '?'
+ url += separator + siteConfig.cdnUrlParams
+ }
+ return url
+}
+
+/**
+ * 获取路由 path
+ */
+export const getCurrentRoutePath = () => {
+ let path = router.currentRoute.value.path
+ if (path == '/') path = trimStart(window.location.hash, '#')
+ if (path.indexOf('?') !== -1) path = path.replace(/\?.*/, '')
+ return path
+}
+
+/**
+ * 获取根据当前路由路径动态加载的语言翻译
+ * @param key 无需语言路径的翻译key,亦可使用完整路径
+ * @param named — 命名插值的值
+ * @param options — 其他翻译选项
+ * @returns — Translated message
+ */
+export const __ = (key: string, named?: Record, options?: TranslateOptions) => {
+ let langPath = ''
+ const path = getCurrentRoutePath()
+ if (isAdminApp()) {
+ langPath = path.slice(path.indexOf(adminBaseRoutePath) + adminBaseRoutePath.length)
+ langPath = trim(langPath, '/').replaceAll('/', '.')
+ } else {
+ langPath = trim(path, '/').replaceAll('/', '.')
+ }
+ langPath = langPath ? langPath + '.' + key : key
+ return i18n.global.te(langPath)
+ ? i18n.global.t(langPath, named ?? {}, options ? options : {})
+ : i18n.global.t(key, named ?? {}, options ? options : {})
+}
+
+/**
+ * 文件类型效验,前端根据服务端配置进行初步检查
+ * @param fileName 文件名
+ * @param fileType 文件 mimeType,不一定存在
+ */
+export const checkFileMimetype = (fileName: string, fileType: string) => {
+ if (!fileName) return false
+ const siteConfig = useSiteConfig()
+ const allowedSuffixes = isArray(siteConfig.upload.allowedSuffixes)
+ ? siteConfig.upload.allowedSuffixes
+ : siteConfig.upload.allowedSuffixes.toLowerCase().split(',')
+
+ const allowedMimeTypes = isArray(siteConfig.upload.allowedMimeTypes)
+ ? siteConfig.upload.allowedMimeTypes
+ : siteConfig.upload.allowedMimeTypes.toLowerCase().split(',')
+
+ const fileSuffix = fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase()
+ if (allowedSuffixes.includes(fileSuffix) || allowedSuffixes.includes('.' + fileSuffix)) {
+ return true
+ }
+ if (fileType && allowedMimeTypes.includes(fileType)) {
+ return true
+ }
+ return false
+}
+
+/**
+ * 获取一组资源的完整地址
+ * @param relativeUrls 资源相对地址
+ * @param domain 指定域名
+ */
+export const arrayFullUrl = (relativeUrls: string | string[], domain = '') => {
+ if (typeof relativeUrls === 'string') {
+ relativeUrls = relativeUrls == '' ? [] : relativeUrls.split(',')
+ }
+ for (const key in relativeUrls) {
+ relativeUrls[key] = fullUrl(relativeUrls[key], domain)
+ }
+ return relativeUrls
+}
+
+/**
+ * 格式化时间戳
+ * @param dateTime 时间戳,默认使用当前时间戳
+ * @param fmt 格式化方式,默认:yyyy-mm-dd hh:MM:ss
+ */
+export const timeFormat = (dateTime: string | number | null = null, fmt = 'yyyy-mm-dd hh:MM:ss') => {
+ if (dateTime == 'none') {
+ return i18n.global.t('None')
+ }
+
+ if (isNull(dateTime)) {
+ dateTime = Number(new Date())
+ }
+
+ /**
+ * 1. 秒级时间戳(10位)需要转换为毫秒级,才能供 Date 对象直接使用
+ * 2. yyyy-mm-dd 也是10位,使用 isFinite 进行排除
+ */
+ if (String(dateTime).length === 10 && isFinite(Number(dateTime))) {
+ dateTime = +dateTime * 1000
+ }
+
+ let date = new Date(dateTime)
+ if (isNaN(date.getTime())) {
+ date = new Date(Number(dateTime))
+ if (isNaN(date.getTime())) {
+ return 'Invalid Date'
+ }
+ }
+
+ let ret
+ const opt: anyObj = {
+ 'y+': date.getFullYear().toString(), // 年
+ 'm+': (date.getMonth() + 1).toString(), // 月
+ 'd+': date.getDate().toString(), // 日
+ 'h+': date.getHours().toString(), // 时
+ 'M+': date.getMinutes().toString(), // 分
+ 's+': date.getSeconds().toString(), // 秒
+ }
+ for (const k in opt) {
+ ret = new RegExp('(' + k + ')').exec(fmt)
+ if (ret) {
+ fmt = fmt.replace(ret[1], ret[1].length == 1 ? opt[k] : padStart(opt[k], ret[1].length, '0'))
+ }
+ }
+ return fmt
+}
+
+/**
+ * 字符串补位
+ */
+const padStart = (str: string, maxLength: number, fillString = ' ') => {
+ if (str.length >= maxLength) return str
+
+ const fillLength = maxLength - str.length
+ let times = Math.ceil(fillLength / fillString.length)
+ while ((times >>= 1)) {
+ fillString += fillString
+ if (times === 1) {
+ fillString += fillString
+ }
+ }
+ return fillString.slice(0, fillLength) + str
+}
+
+/**
+ * 根据当前时间生成问候语
+ */
+export const getGreet = () => {
+ const now = new Date()
+ const hour = now.getHours()
+ let greet = ''
+
+ if (hour < 5) {
+ greet = i18n.global.t('utils.Late at night, pay attention to your body!')
+ } else if (hour < 9) {
+ greet = i18n.global.t('utils.good morning!') + i18n.global.t('utils.welcome back')
+ } else if (hour < 12) {
+ greet = i18n.global.t('utils.Good morning!') + i18n.global.t('utils.welcome back')
+ } else if (hour < 14) {
+ greet = i18n.global.t('utils.Good noon!') + i18n.global.t('utils.welcome back')
+ } else if (hour < 18) {
+ greet = i18n.global.t('utils.good afternoon') + i18n.global.t('utils.welcome back')
+ } else if (hour < 24) {
+ greet = i18n.global.t('utils.Good evening') + i18n.global.t('utils.welcome back')
+ } else {
+ greet = i18n.global.t('utils.Hello!') + i18n.global.t('utils.welcome back')
+ }
+ return greet
+}
diff --git a/web/src/utils/directives.ts b/web/src/utils/directives.ts
new file mode 100644
index 0000000..814ae07
--- /dev/null
+++ b/web/src/utils/directives.ts
@@ -0,0 +1,224 @@
+import type { App } from 'vue'
+import { nextTick } from 'vue'
+import horizontalScroll from '/@/utils/horizontalScroll'
+import { useEventListener } from '@vueuse/core'
+import { isString } from 'lodash-es'
+import { auth } from '/@/utils/common'
+
+export function directives(app: App) {
+ // 鉴权指令
+ authDirective(app)
+ // 拖动指令
+ dragDirective(app)
+ // 缩放指令
+ zoomDirective(app)
+ // 点击后自动失焦指令
+ blurDirective(app)
+ // 表格横向拖动指令
+ tableLateralDragDirective(app)
+}
+
+/**
+ * 页面按钮鉴权指令
+ * @description v-auth="'name'",name可以为:index,add,edit,del,...
+ */
+function authDirective(app: App) {
+ app.directive('auth', {
+ mounted(el, binding) {
+ if (!binding.value) return false
+ if (!auth(binding.value)) el.parentNode.removeChild(el)
+ },
+ })
+}
+
+/**
+ * 表格横向滚动指令
+ * @description v-table-lateral-drag
+ */
+function tableLateralDragDirective(app: App) {
+ app.directive('tableLateralDrag', {
+ created(el) {
+ new horizontalScroll(el.querySelector('.el-table__body-wrapper .el-scrollbar .el-scrollbar__wrap'))
+ },
+ })
+}
+
+/**
+ * 点击后自动失焦指令
+ * @description v-blur
+ */
+function blurDirective(app: App) {
+ app.directive('blur', {
+ mounted(el) {
+ useEventListener(el, 'focus', () => el.blur())
+ },
+ })
+}
+
+/**
+ * el-dialog 的缩放指令
+ * 可以传递字符串和数组
+ * 当为字符串时,传递dialog的class即可,实际被缩放的元素为'.el-dialog__body'
+ * 当为数组时,参数一为句柄,参数二为实际被缩放的元素
+ * @description v-zoom="'.handle-class-name'"
+ * @description v-zoom="['.handle-class-name', '.zoom-dom-class-name', 句柄元素高度是否跟随缩放:默认false,句柄元素宽度是否跟随缩放:默认true]"
+ */
+function zoomDirective(app: App) {
+ app.directive('zoom', {
+ mounted(el, binding) {
+ if (!binding.value) return false
+ const zoomDomBindData = isString(binding.value) ? [binding.value, '.el-dialog__body', false, true] : binding.value
+ zoomDomBindData[1] = zoomDomBindData[1] ? zoomDomBindData[1] : '.el-dialog__body'
+ zoomDomBindData[2] = typeof zoomDomBindData[2] == 'undefined' ? false : zoomDomBindData[2]
+ zoomDomBindData[3] = typeof zoomDomBindData[3] == 'undefined' ? true : zoomDomBindData[3]
+
+ nextTick(() => {
+ const zoomDom = document.querySelector(zoomDomBindData[1]) as HTMLElement // 实际被缩放的元素
+ const zoomDomBox = document.querySelector(zoomDomBindData[0]) as HTMLElement // 动态添加缩放句柄的元素
+ const zoomHandleEl = document.createElement('div') // 缩放句柄
+ zoomHandleEl.className = 'zoom-handle'
+ zoomHandleEl.onmouseenter = () => {
+ zoomHandleEl.onmousedown = (e: MouseEvent) => {
+ const x = e.clientX
+ const y = e.clientY
+ const zoomDomWidth = zoomDom.offsetWidth
+ const zoomDomHeight = zoomDom.offsetHeight
+ const zoomDomBoxWidth = zoomDomBox.offsetWidth
+ const zoomDomBoxHeight = zoomDomBox.offsetHeight
+ document.onmousemove = (e: MouseEvent) => {
+ e.preventDefault() // 移动时禁用默认事件
+ const w = zoomDomWidth + (e.clientX - x) * 2
+ const h = zoomDomHeight + (e.clientY - y)
+
+ zoomDom.style.width = `${w}px`
+ zoomDom.style.height = `${h}px`
+
+ if (zoomDomBindData[2]) {
+ const boxH = zoomDomBoxHeight + (e.clientY - y)
+ zoomDomBox.style.height = `${boxH}px`
+ }
+ if (zoomDomBindData[3]) {
+ const boxW = zoomDomBoxWidth + (e.clientX - x) * 2
+ zoomDomBox.style.width = `${boxW}px`
+ }
+ }
+
+ document.onmouseup = function () {
+ document.onmousemove = null
+ document.onmouseup = null
+ }
+ }
+ }
+
+ zoomDomBox.appendChild(zoomHandleEl)
+ })
+ },
+ })
+}
+
+/**
+ * 拖动指令
+ * @description v-drag="[domEl,handleEl]"
+ * @description domEl=被拖动的元素,handleEl=在此元素内可以拖动`dom`
+ */
+interface downReturn {
+ [key: string]: number
+}
+function dragDirective(app: App) {
+ app.directive('drag', {
+ mounted(el, binding) {
+ if (!binding.value) return false
+
+ const dragDom = document.querySelector(binding.value[0]) as HTMLElement
+ const dragHandle = document.querySelector(binding.value[1]) as HTMLElement
+
+ if (!dragHandle || !dragDom) {
+ return false
+ }
+
+ function down(e: MouseEvent | TouchEvent, type: string): downReturn {
+ // 鼠标按下,记录鼠标位置
+ const disX = type === 'pc' ? (e as MouseEvent).clientX : (e as TouchEvent).touches[0].clientX
+ const disY = type === 'pc' ? (e as MouseEvent).clientY : (e as TouchEvent).touches[0].clientY
+
+ // body宽度
+ const screenWidth = document.body.clientWidth
+ const screenHeight = document.body.clientHeight || document.documentElement.clientHeight
+
+ // 被拖动元素宽度
+ const dragDomWidth = dragDom.offsetWidth
+ // 被拖动元素高度
+ const dragDomheight = dragDom.offsetHeight
+
+ // 拖动限位
+ const minDragDomLeft = dragDom.offsetLeft
+ const maxDragDomLeft = screenWidth - dragDom.offsetLeft - dragDomWidth
+ const minDragDomTop = dragDom.offsetTop
+ const maxDragDomTop = screenHeight - dragDom.offsetTop - dragDomheight
+
+ // 获取到的值带px 正则匹配替换
+ let styL: string | number = getComputedStyle(dragDom).left
+ let styT: string | number = getComputedStyle(dragDom).top
+ styL = +styL.replace(/\px/g, '')
+ styT = +styT.replace(/\px/g, '')
+
+ return {
+ disX,
+ disY,
+ minDragDomLeft,
+ maxDragDomLeft,
+ minDragDomTop,
+ maxDragDomTop,
+ styL,
+ styT,
+ }
+ }
+
+ function move(e: MouseEvent | TouchEvent, type: string, obj: downReturn) {
+ const { disX, disY, minDragDomLeft, maxDragDomLeft, minDragDomTop, maxDragDomTop, styL, styT } = obj
+
+ // 通过事件委托,计算移动的距离
+ let left = type === 'pc' ? (e as MouseEvent).clientX - disX : (e as TouchEvent).touches[0].clientX - disX
+ let top = type === 'pc' ? (e as MouseEvent).clientY - disY : (e as TouchEvent).touches[0].clientY - disY
+
+ // 边界处理
+ if (-left > minDragDomLeft) {
+ left = -minDragDomLeft
+ } else if (left > maxDragDomLeft) {
+ left = maxDragDomLeft
+ }
+
+ if (-top > minDragDomTop) {
+ top = -minDragDomTop
+ } else if (top > maxDragDomTop) {
+ top = maxDragDomTop
+ }
+
+ // 移动当前元素
+ dragDom.style.cssText += `;left:${left + styL}px;top:${top + styT}px;`
+ }
+
+ dragHandle.onmouseover = () => (dragHandle.style.cursor = `move`)
+ dragHandle.onmousedown = (e) => {
+ const obj = down(e, 'pc')
+ document.onmousemove = (e) => {
+ move(e, 'pc', obj)
+ }
+ document.onmouseup = () => {
+ document.onmousemove = null
+ document.onmouseup = null
+ }
+ }
+ dragHandle.ontouchstart = (e) => {
+ const obj = down(e, 'app')
+ document.ontouchmove = (e) => {
+ move(e, 'app', obj)
+ }
+ document.ontouchend = () => {
+ document.ontouchmove = null
+ document.ontouchend = null
+ }
+ }
+ },
+ })
+}
diff --git a/web/src/utils/horizontalScroll.ts b/web/src/utils/horizontalScroll.ts
new file mode 100644
index 0000000..47d6817
--- /dev/null
+++ b/web/src/utils/horizontalScroll.ts
@@ -0,0 +1,33 @@
+/**
+ * 横向滚动条
+ */
+export default class horizontalScroll {
+ private el: HTMLElement
+
+ constructor(nativeElement: HTMLElement) {
+ this.el = nativeElement
+ this.handleWheelEvent()
+ }
+
+ handleWheelEvent() {
+ let wheel = ''
+
+ if ('onmousewheel' in this.el) {
+ wheel = 'mousewheel'
+ } else if ('onwheel' in this.el) {
+ wheel = 'wheel'
+ } else if ('attachEvent' in window) {
+ wheel = 'onmousewheel'
+ } else {
+ wheel = 'DOMMouseScroll'
+ }
+ this.el['addEventListener'](wheel, this.scroll, { passive: true })
+ }
+
+ scroll = (event: any) => {
+ if (this.el.clientWidth >= this.el.scrollWidth) {
+ return
+ }
+ this.el.scrollLeft += event.deltaY ? event.deltaY : event.detail && event.detail !== 0 ? event.detail : -event.wheelDelta
+ }
+}
diff --git a/web/src/utils/iconfont.ts b/web/src/utils/iconfont.ts
new file mode 100644
index 0000000..ca6a0c5
--- /dev/null
+++ b/web/src/utils/iconfont.ts
@@ -0,0 +1,170 @@
+import { nextTick } from 'vue'
+import { loadCss, loadJs } from './common'
+import * as elIcons from '@element-plus/icons-vue'
+import { getUrl } from '/@/utils/axios'
+
+/**
+ * 动态加载的 css 和 js
+ */
+const cssUrls: Array = ['//at.alicdn.com/t/font_3135462_5axiswmtpj.css']
+const jsUrls: Array = []
+
+/*
+ * 加载预设的字体图标资源
+ */
+export default function init() {
+ if (cssUrls.length > 0) {
+ cssUrls.map((v) => {
+ loadCss(v)
+ })
+ }
+
+ if (jsUrls.length > 0) {
+ jsUrls.map((v) => {
+ loadJs(v)
+ })
+ }
+}
+
+/*
+ * 获取当前页面中从指定域名加载到的样式表内容
+ * 样式表未载入前无法获取
+ */
+function getStylesFromDomain(domain: string) {
+ const sheets = []
+ const styles: StyleSheetList = document.styleSheets
+ for (const key in styles) {
+ if (styles[key].href && (styles[key].href as string).indexOf(domain) > -1) {
+ sheets.push(styles[key])
+ }
+ }
+ return sheets
+}
+
+/**
+ * 获取Vite开发服务/编译后的样式表内容
+ * @param devID style 标签的 viteDevId,只开发服务有
+ */
+function getStylesFromVite(devId: string) {
+ const sheets = []
+ const styles: StyleSheetList = document.styleSheets
+ if (import.meta.env.MODE == 'production') {
+ const url = getUrl()
+ for (const key in styles) {
+ if (styles[key].href && styles[key].href?.indexOf(url) === 0) {
+ sheets.push(styles[key])
+ }
+ }
+ return sheets
+ }
+ for (const key in styles) {
+ const ownerNode = styles[key].ownerNode as HTMLMapElement
+ if (ownerNode && ownerNode.dataset?.viteDevId && ownerNode.dataset.viteDevId!.indexOf(devId) > -1) {
+ sheets.push(styles[key])
+ }
+ }
+ return sheets
+}
+
+/*
+ * 获取本地自带的图标
+ * /src/assets/icons文件夹内的svg文件
+ */
+export function getLocalIconfontNames() {
+ return new Promise((resolve, reject) => {
+ nextTick(() => {
+ let iconfonts: string[] = []
+
+ const svgEl = document.getElementById('local-icon')
+ if (svgEl?.dataset.iconName) {
+ iconfonts = (svgEl?.dataset.iconName as string).split(',')
+ }
+
+ if (iconfonts.length > 0) {
+ resolve(iconfonts)
+ } else {
+ reject('No Local Icons')
+ }
+ })
+ })
+}
+
+/*
+ * 获取 Awesome-Iconfont 的 name 列表
+ */
+export function getAwesomeIconfontNames() {
+ return new Promise((resolve, reject) => {
+ nextTick(() => {
+ const iconfonts = []
+ const sheets = getStylesFromVite('font-awesome.min.css')
+ for (const key in sheets) {
+ const rules: any = sheets[key].cssRules
+ for (const k in rules) {
+ if (!rules[k].selectorText || rules[k].selectorText.indexOf('.fa-') !== 0) {
+ continue
+ }
+ if (/^\.fa-(.*)::before$/g.test(rules[k].selectorText)) {
+ if (rules[k].selectorText.indexOf(', ') > -1) {
+ const iconNames = rules[k].selectorText.split(', ')
+ iconfonts.push(`${iconNames[0].substring(1, iconNames[0].length).replace(/\:\:before/gi, '')}`)
+ } else {
+ iconfonts.push(`${rules[k].selectorText.substring(1, rules[k].selectorText.length).replace(/\:\:before/gi, '')}`)
+ }
+ }
+ }
+ }
+
+ if (iconfonts.length > 0) {
+ resolve(iconfonts)
+ } else {
+ reject('No AwesomeIcon style sheet')
+ }
+ })
+ })
+}
+
+/*
+ * 获取 Iconfont 的 name 列表
+ */
+export function getIconfontNames() {
+ return new Promise((resolve, reject) => {
+ nextTick(() => {
+ const iconfonts = []
+ const sheets = getStylesFromDomain('at.alicdn.com')
+ for (const key in sheets) {
+ const rules: any = sheets[key].cssRules
+ for (const k in rules) {
+ if (rules[k].selectorText && /^\.icon-(.*)::before$/g.test(rules[k].selectorText)) {
+ iconfonts.push(`${rules[k].selectorText.substring(1, rules[k].selectorText.length).replace(/\:\:before/gi, '')}`)
+ }
+ }
+ }
+
+ if (iconfonts.length > 0) {
+ resolve(iconfonts)
+ } else {
+ reject('No Iconfont style sheet')
+ }
+ })
+ })
+}
+
+/*
+ * 获取element plus 自带的图标
+ */
+export function getElementPlusIconfontNames() {
+ return new Promise((resolve, reject) => {
+ nextTick(() => {
+ const iconfonts = []
+ const icons = elIcons as any
+ for (const i in icons) {
+ iconfonts.push(`el-icon-${icons[i].name}`)
+ }
+ if (iconfonts.length > 0) {
+ resolve(iconfonts)
+ } else {
+ reject('No ElementPlus Icons')
+ }
+ })
+ })
+}
diff --git a/web/src/utils/layout.ts b/web/src/utils/layout.ts
new file mode 100644
index 0000000..26078a9
--- /dev/null
+++ b/web/src/utils/layout.ts
@@ -0,0 +1,60 @@
+import type { CSSProperties } from 'vue'
+import { useConfig } from '/@/stores/config'
+import { useMemberCenter } from '/@/stores/memberCenter'
+import { useNavTabs } from '/@/stores/navTabs'
+import { isAdminApp } from '/@/utils/common'
+
+/**
+ * 管理员后台各个布局顶栏高度
+ */
+export const adminLayoutHeaderBarHeight = {
+ Default: 70,
+ Classic: 50,
+ Streamline: 60,
+ Double: 60,
+}
+
+/**
+ * 前台会员中心各个布局顶栏高度
+ */
+export const userLayoutHeaderBarHeight = {
+ Default: 60,
+ Disable: 60,
+}
+
+/**
+ * main高度
+ * @param extra main高度额外减去的px数,可以实现隐藏原有的滚动条
+ * @returns CSSProperties
+ */
+export function mainHeight(extra = 0): CSSProperties {
+ let height = extra
+ if (isAdminApp()) {
+ const config = useConfig()
+ const navTabs = useNavTabs()
+ if (!navTabs.state.tabFullScreen) {
+ height += adminLayoutHeaderBarHeight[config.layout.layoutMode as keyof typeof adminLayoutHeaderBarHeight]
+ }
+ } else {
+ const memberCenter = useMemberCenter()
+ height += userLayoutHeaderBarHeight[memberCenter.state.layoutMode as keyof typeof userLayoutHeaderBarHeight]
+ }
+ return {
+ height: 'calc(100vh - ' + height.toString() + 'px)',
+ }
+}
+
+/**
+ * 设置导航栏宽度
+ * @returns
+ */
+export function setNavTabsWidth() {
+ const navTabs = document.querySelector('.nav-tabs') as HTMLElement
+ if (!navTabs) {
+ return
+ }
+ const navBar = document.querySelector('.nav-bar') as HTMLElement
+ const navMenus = document.querySelector('.nav-menus') as HTMLElement
+ const minWidth = navBar.offsetWidth - (navMenus.offsetWidth + 20)
+ navTabs.style.width = minWidth.toString() + 'px'
+}
diff --git a/web/src/utils/loading.ts b/web/src/utils/loading.ts
new file mode 100644
index 0000000..937cfa5
--- /dev/null
+++ b/web/src/utils/loading.ts
@@ -0,0 +1,34 @@
+import { nextTick } from 'vue'
+import '/@/styles/loading.scss'
+
+export const loading = {
+ show: () => {
+ const bodys: Element = document.body
+ const div = document.createElement('div')
+ div.className = 'block-loading'
+ div.innerHTML = `
+
+ `
+ bodys.insertBefore(div, bodys.childNodes[0])
+ },
+ hide: () => {
+ nextTick(() => {
+ setTimeout(() => {
+ const el = document.querySelector('.block-loading')
+ el && el.parentNode?.removeChild(el)
+ }, 1000)
+ })
+ },
+}
diff --git a/web/src/utils/pageBubble.ts b/web/src/utils/pageBubble.ts
new file mode 100644
index 0000000..f024753
--- /dev/null
+++ b/web/src/utils/pageBubble.ts
@@ -0,0 +1,104 @@
+// 页面气泡效果
+
+const bubble: {
+ width: number
+ height: number
+ bubbleEl: any
+ canvas: any
+ ctx: any
+ circles: any[]
+ animate: boolean
+ requestId: any
+} = {
+ width: 0,
+ height: 0,
+ bubbleEl: null,
+ canvas: null,
+ ctx: {},
+ circles: [],
+ animate: true,
+ requestId: null,
+}
+
+export const init = function (): void {
+ bubble.width = window.innerWidth
+ bubble.height = window.innerHeight
+
+ bubble.bubbleEl = document.getElementById('bubble')
+ bubble.bubbleEl.style.height = bubble.height + 'px'
+
+ bubble.canvas = document.getElementById('bubble-canvas')
+ bubble.canvas.width = bubble.width
+ bubble.canvas.height = bubble.height
+ bubble.ctx = bubble.canvas.getContext('2d')
+
+ // create particles
+ bubble.circles = []
+ for (let x = 0; x < bubble.width * 0.5; x++) {
+ const c = new Circle()
+ bubble.circles.push(c)
+ }
+ animate()
+ addListeners()
+}
+
+function scrollCheck() {
+ bubble.animate = document.body.scrollTop > bubble.height ? false : true
+}
+
+function resize() {
+ bubble.width = window.innerWidth
+ bubble.height = window.innerHeight
+ bubble.bubbleEl.style.height = bubble.height + 'px'
+ bubble.canvas.width = bubble.width
+ bubble.canvas.height = bubble.height
+}
+
+function animate() {
+ if (bubble.animate) {
+ bubble.ctx.clearRect(0, 0, bubble.width, bubble.height)
+ for (const i in bubble.circles) {
+ bubble.circles[i].draw()
+ }
+ }
+ bubble.requestId = requestAnimationFrame(animate)
+}
+
+class Circle {
+ pos: {
+ x: number
+ y: number
+ }
+ alpha: number
+ scale: number
+ velocity: number
+ draw: () => void
+ constructor() {
+ this.pos = {
+ x: Math.random() * bubble.width,
+ y: bubble.height + Math.random() * 100,
+ }
+ this.alpha = 0.1 + Math.random() * 0.3
+ this.scale = 0.1 + Math.random() * 0.3
+ this.velocity = Math.random()
+ this.draw = function () {
+ this.pos.y -= this.velocity
+ this.alpha -= 0.0005
+ bubble.ctx.beginPath()
+ bubble.ctx.arc(this.pos.x, this.pos.y, this.scale * 10, 0, 2 * Math.PI, false)
+ bubble.ctx.fillStyle = 'rgba(255,255,255,' + this.alpha + ')'
+ bubble.ctx.fill()
+ }
+ }
+}
+
+function addListeners() {
+ window.addEventListener('scroll', scrollCheck)
+ window.addEventListener('resize', resize)
+}
+
+export function removeListeners() {
+ window.removeEventListener('scroll', scrollCheck)
+ window.removeEventListener('resize', resize)
+ cancelAnimationFrame(bubble.requestId)
+}
diff --git a/web/src/utils/pageShade.ts b/web/src/utils/pageShade.ts
new file mode 100644
index 0000000..98cd5a9
--- /dev/null
+++ b/web/src/utils/pageShade.ts
@@ -0,0 +1,22 @@
+import { useEventListener } from '@vueuse/core'
+
+/*
+ * 显示页面遮罩
+ */
+export const showShade = function (className = 'shade', closeCallBack: Function): void {
+ const containerEl = document.querySelector('.layout-container') as HTMLElement
+ const shadeDiv = document.createElement('div')
+ shadeDiv.setAttribute('class', 'ba-layout-shade ' + className)
+ containerEl.appendChild(shadeDiv)
+ useEventListener(shadeDiv, 'click', () => closeShade(closeCallBack))
+}
+
+/*
+ * 隐藏页面遮罩
+ */
+export const closeShade = function (closeCallBack: Function = () => {}): void {
+ const shadeEl = document.querySelector('.ba-layout-shade') as HTMLElement
+ shadeEl && shadeEl.remove()
+
+ closeCallBack()
+}
diff --git a/web/src/utils/random.ts b/web/src/utils/random.ts
new file mode 100644
index 0000000..6b2a11d
--- /dev/null
+++ b/web/src/utils/random.ts
@@ -0,0 +1,57 @@
+const hexList: string[] = []
+for (let i = 0; i <= 15; i++) {
+ hexList[i] = i.toString(16)
+}
+
+/**
+ * 生成随机数
+ * @param min 最小值
+ * @param max 最大值
+ * @returns 生成的随机数
+ */
+export function randomNum(min: number, max: number) {
+ switch (arguments.length) {
+ case 1:
+ return parseInt((Math.random() * min + 1).toString(), 10)
+ break
+ case 2:
+ return parseInt((Math.random() * (max - min + 1) + min).toString(), 10)
+ break
+ default:
+ return 0
+ break
+ }
+}
+
+/**
+ * 生成全球唯一标识
+ * @returns uuid
+ */
+export function uuid(): string {
+ let uuid = ''
+ for (let i = 1; i <= 36; i++) {
+ if (i === 9 || i === 14 || i === 19 || i === 24) {
+ uuid += '-'
+ } else if (i === 15) {
+ uuid += 4
+ } else if (i === 20) {
+ uuid += hexList[(Math.random() * 4) | 8]
+ } else {
+ uuid += hexList[(Math.random() * 16) | 0]
+ }
+ }
+ return uuid
+}
+
+/**
+ * 生成唯一标识
+ * @param prefix 前缀
+ * @returns 唯一标识
+ */
+export function shortUuid(prefix = ''): string {
+ const time = Date.now()
+ const random = Math.floor(Math.random() * 1000000000)
+ if (!window.unique) window.unique = 0
+ window.unique++
+ return prefix + '_' + random + window.unique + String(time)
+}
diff --git a/web/src/utils/router.ts b/web/src/utils/router.ts
new file mode 100644
index 0000000..dd6c331
--- /dev/null
+++ b/web/src/utils/router.ts
@@ -0,0 +1,318 @@
+import { ElNotification } from 'element-plus'
+import { compact, isEmpty, reverse } from 'lodash-es'
+import type { RouteLocationRaw, RouteRecordRaw } from 'vue-router'
+import { isNavigationFailure, NavigationFailureType } from 'vue-router'
+import { i18n } from '/@/lang/index'
+import router from '/@/router/index'
+import adminBaseRoute from '/@/router/static/adminBase'
+import memberCenterBaseRoute from '/@/router/static/memberCenterBase'
+import { useConfig } from '/@/stores/config'
+import { useMemberCenter } from '/@/stores/memberCenter'
+import { useNavTabs } from '/@/stores/navTabs'
+import { useSiteConfig } from '/@/stores/siteConfig'
+import { isAdminApp } from '/@/utils/common'
+import { closeShade } from '/@/utils/pageShade'
+
+/**
+ * 导航失败有错误消息的路由push
+ * @param to — 导航位置,同 router.push
+ */
+export const routePush = async (to: RouteLocationRaw) => {
+ try {
+ const failure = await router.push(to)
+ if (isNavigationFailure(failure, NavigationFailureType.aborted)) {
+ ElNotification({
+ message: i18n.global.t('utils.Navigation failed, navigation guard intercepted!'),
+ type: 'error',
+ })
+ } else if (isNavigationFailure(failure, NavigationFailureType.duplicated)) {
+ ElNotification({
+ message: i18n.global.t('utils.Navigation failed, it is at the navigation target position!'),
+ type: 'warning',
+ })
+ }
+ } catch (error) {
+ ElNotification({
+ message: i18n.global.t('utils.Navigation failed, invalid route!'),
+ type: 'error',
+ })
+ console.error(error)
+ }
+}
+
+/**
+ * 获取第一个菜单
+ */
+export const getFirstRoute = (routes: RouteRecordRaw[]): false | RouteRecordRaw => {
+ const routerPaths: string[] = []
+ const routers = router.getRoutes()
+ routers.forEach((item) => {
+ if (item.path) routerPaths.push(item.path)
+ })
+ let find: boolean | RouteRecordRaw = false
+ for (const key in routes) {
+ if (routes[key].meta?.type == 'menu' && routerPaths.indexOf(routes[key].path) !== -1) {
+ return routes[key]
+ } else if (routes[key].children && routes[key].children?.length) {
+ find = getFirstRoute(routes[key].children!)
+ if (find) return find
+ }
+ }
+ return find
+}
+
+/**
+ * 打开侧边菜单
+ * @param menu 菜单数据
+ */
+export const onClickMenu = (menu: RouteRecordRaw) => {
+ switch (menu.meta?.menu_type) {
+ case 'iframe':
+ case 'tab':
+ routePush(menu.path)
+ break
+ case 'link':
+ window.open(menu.path, '_blank')
+ break
+
+ default:
+ ElNotification({
+ message: i18n.global.t('utils.Navigation failed, the menu type is unrecognized!'),
+ type: 'error',
+ })
+ break
+ }
+
+ const config = useConfig()
+ if (config.layout.shrink) {
+ closeShade(() => {
+ config.setLayout('menuCollapse', true)
+ })
+ }
+}
+
+/**
+ * 处理前台的路由
+ * @param routes 路由规则
+ * @param menus 会员中心菜单路由规则
+ */
+export const handleFrontendRoute = (routes: any, menus: any) => {
+ const siteConfig = useSiteConfig()
+ const memberCenter = useMemberCenter()
+ const viewsComponent = import.meta.glob('/src/views/frontend/**/*.vue')
+
+ if (routes.length) {
+ addRouteAll(viewsComponent, routes, '', true)
+ memberCenter.mergeAuthNode(handleAuthNode(routes, '/'))
+ siteConfig.setHeadNav(handleMenuRule(routes, '/', ['nav']))
+ memberCenter.mergeNavUserMenus(handleMenuRule(routes, '/', ['nav_user_menu']))
+ }
+ if (menus.length && isEmpty(memberCenter.state.viewRoutes)) {
+ addRouteAll(viewsComponent, menus, memberCenterBaseRoute.name as string)
+ const menuMemberCenterBaseRoute = (memberCenterBaseRoute.path as string) + '/'
+ memberCenter.mergeAuthNode(handleAuthNode(menus, menuMemberCenterBaseRoute))
+
+ memberCenter.mergeNavUserMenus(handleMenuRule(menus, '/', ['nav_user_menu']))
+ memberCenter.setShowHeadline(menus.length > 1)
+ memberCenter.setViewRoutes(handleMenuRule(menus, menuMemberCenterBaseRoute))
+ }
+}
+
+/**
+ * 处理后台的路由
+ */
+export const handleAdminRoute = (routes: any) => {
+ const viewsComponent = import.meta.glob('/src/views/backend/**/*.vue')
+ addRouteAll(viewsComponent, routes, adminBaseRoute.name as string)
+ const menuAdminBaseRoute = (adminBaseRoute.path as string) + '/'
+
+ // 更新stores中的路由菜单数据
+ const navTabs = useNavTabs()
+ navTabs.setTabsViewRoutes(handleMenuRule(routes, menuAdminBaseRoute))
+ navTabs.fillAuthNode(handleAuthNode(routes, menuAdminBaseRoute))
+}
+
+/**
+ * 获取菜单的paths
+ */
+export const getMenuPaths = (menus: RouteRecordRaw[]): string[] => {
+ let menuPaths: string[] = []
+ menus.forEach((item) => {
+ menuPaths.push(item.path)
+ if (item.children && item.children.length > 0) {
+ menuPaths = menuPaths.concat(getMenuPaths(item.children))
+ }
+ })
+ return menuPaths
+}
+
+/**
+ * 获取菜单唯一标识
+ * @param menu 菜单数据
+ * @param prefix 前缀
+ */
+export const getMenuKey = (menu: RouteRecordRaw, prefix = '') => {
+ if (prefix === '') {
+ prefix = menu.path
+ }
+ return `${prefix}-${menu.name as string}-${menu.meta && menu.meta.id ? menu.meta.id : ''}`
+}
+
+/**
+ * 会员中心和后台的菜单处理
+ */
+const handleMenuRule = (routes: any, pathPrefix = '/', type = ['menu', 'menu_dir']) => {
+ const menuRule: RouteRecordRaw[] = []
+ for (const key in routes) {
+ if (routes[key].extend == 'add_rules_only') {
+ continue
+ }
+ if (!type.includes(routes[key].type)) {
+ continue
+ }
+ if (routes[key].type == 'menu_dir' && routes[key].children && !routes[key].children.length) {
+ continue
+ }
+ if (
+ ['route', 'menu', 'nav_user_menu', 'nav'].includes(routes[key].type) &&
+ ((routes[key].menu_type == 'tab' && !routes[key].component) || (['link', 'iframe'].includes(routes[key].menu_type) && !routes[key].url))
+ ) {
+ continue
+ }
+ const currentPath = ['link', 'iframe'].includes(routes[key].menu_type) ? routes[key].url : pathPrefix + routes[key].path
+ let children: RouteRecordRaw[] = []
+ if (routes[key].children && routes[key].children.length > 0) {
+ children = handleMenuRule(routes[key].children, pathPrefix, type)
+ }
+ menuRule.push({
+ path: currentPath,
+ name: routes[key].name,
+ component: routes[key].component,
+ meta: {
+ id: routes[key].id,
+ title: routes[key].title,
+ icon: routes[key].icon,
+ keepalive: routes[key].keepalive,
+ menu_type: routes[key].menu_type,
+ type: routes[key].type,
+ },
+ children: children,
+ })
+ }
+ return menuRule
+}
+
+/**
+ * 处理权限节点
+ * @param routes 路由数据
+ * @param prefix 节点前缀
+ * @returns 组装好的权限节点
+ */
+const handleAuthNode = (routes: any, prefix = '/') => {
+ const authNode: Map = new Map([])
+ assembleAuthNode(routes, authNode, prefix, prefix)
+ return authNode
+}
+const assembleAuthNode = (routes: any, authNode: Map, prefix = '/', parent = '/') => {
+ const authNodeTemp = []
+ for (const key in routes) {
+ if (routes[key].type == 'button') authNodeTemp.push(prefix + routes[key].name)
+ if (routes[key].children && routes[key].children.length > 0) {
+ assembleAuthNode(routes[key].children, authNode, prefix, prefix + routes[key].name)
+ }
+ }
+ if (authNodeTemp && authNodeTemp.length > 0) {
+ authNode.set(parent, authNodeTemp)
+ }
+}
+
+/**
+ * 动态添加路由-带子路由
+ * @param viewsComponent
+ * @param routes
+ * @param parentName
+ * @param analyticRelation 根据 name 从已注册路由分析父级路由
+ */
+export const addRouteAll = (viewsComponent: Record, routes: any, parentName: string, analyticRelation = false) => {
+ for (const idx in routes) {
+ if (routes[idx].extend == 'add_menu_only') {
+ continue
+ }
+ if ((routes[idx].menu_type == 'tab' && viewsComponent[routes[idx].component]) || routes[idx].menu_type == 'iframe') {
+ addRouteItem(viewsComponent, routes[idx], parentName, analyticRelation)
+ }
+
+ if (routes[idx].children && routes[idx].children.length > 0) {
+ addRouteAll(viewsComponent, routes[idx].children, parentName, analyticRelation)
+ }
+ }
+}
+
+/**
+ * 动态添加路由
+ * @param viewsComponent
+ * @param route
+ * @param parentName
+ * @param analyticRelation 根据 name 从已注册路由分析父级路由
+ */
+export const addRouteItem = (viewsComponent: Record, route: any, parentName: string, analyticRelation: boolean) => {
+ let path = '',
+ component
+ if (route.menu_type == 'iframe') {
+ path = (isAdminApp() ? adminBaseRoute.path : memberCenterBaseRoute.path) + '/iframe/' + encodeURIComponent(route.url)
+ component = () => import('/@/layouts/common/router-view/iframe.vue')
+ } else {
+ path = parentName ? route.path : '/' + route.path
+ component = viewsComponent[route.component]
+ }
+
+ if (route.menu_type == 'tab' && analyticRelation) {
+ const parentNames = getParentNames(route.name)
+ if (parentNames.length) {
+ for (const key in parentNames) {
+ if (router.hasRoute(parentNames[key])) {
+ parentName = parentNames[key]
+ break
+ }
+ }
+ }
+ }
+
+ const routeBaseInfo: RouteRecordRaw = {
+ path: path,
+ name: route.name,
+ component: component,
+ meta: {
+ title: route.title,
+ extend: route.extend,
+ icon: route.icon,
+ keepalive: route.keepalive,
+ menu_type: route.menu_type,
+ type: route.type,
+ url: route.url,
+ addtab: true,
+ },
+ }
+ if (parentName) {
+ router.addRoute(parentName, routeBaseInfo)
+ } else {
+ router.addRoute(routeBaseInfo)
+ }
+}
+
+/**
+ * 根据name字符串,获取父级name组合的数组
+ * @param name
+ */
+const getParentNames = (name: string) => {
+ const names = compact(name.split('/'))
+ const tempNames = []
+ const parentNames = []
+ for (const key in names) {
+ tempNames.push(names[key])
+ if (parseInt(key) != names.length - 1) {
+ parentNames.push(tempNames.join('/'))
+ }
+ }
+ return reverse(parentNames)
+}
diff --git a/web/src/utils/storage.ts b/web/src/utils/storage.ts
new file mode 100644
index 0000000..1b4d3e5
--- /dev/null
+++ b/web/src/utils/storage.ts
@@ -0,0 +1,45 @@
+/**
+ * window.localStorage
+ * @method set 设置
+ * @method get 获取
+ * @method remove 移除
+ * @method clear 移除全部
+ */
+export const Local = {
+ set(key: string, val: any) {
+ window.localStorage.setItem(key, JSON.stringify(val))
+ },
+ get(key: string) {
+ const json: any = window.localStorage.getItem(key)
+ return JSON.parse(json)
+ },
+ remove(key: string) {
+ window.localStorage.removeItem(key)
+ },
+ clear() {
+ window.localStorage.clear()
+ },
+}
+
+/**
+ * window.sessionStorage
+ * @method set 设置会话缓存
+ * @method get 获取会话缓存
+ * @method remove 移除会话缓存
+ * @method clear 移除全部会话缓存
+ */
+export const Session = {
+ set(key: string, val: any) {
+ window.sessionStorage.setItem(key, JSON.stringify(val))
+ },
+ get(key: string) {
+ const json: any = window.sessionStorage.getItem(key)
+ return JSON.parse(json)
+ },
+ remove(key: string) {
+ window.sessionStorage.removeItem(key)
+ },
+ clear() {
+ window.sessionStorage.clear()
+ },
+}
diff --git a/web/src/utils/useCurrentInstance.ts b/web/src/utils/useCurrentInstance.ts
new file mode 100644
index 0000000..c0fc26e
--- /dev/null
+++ b/web/src/utils/useCurrentInstance.ts
@@ -0,0 +1,13 @@
+import { getCurrentInstance } from 'vue'
+import type { ComponentInternalInstance } from 'vue'
+
+export default function useCurrentInstance() {
+ if (!getCurrentInstance()) {
+ throw new Error('useCurrentInstance() can only be used inside setup() or functional components!')
+ }
+ const { appContext } = getCurrentInstance() as ComponentInternalInstance
+ const proxy = appContext.config.globalProperties
+ return {
+ proxy,
+ }
+}
diff --git a/web/src/utils/useDark.ts b/web/src/utils/useDark.ts
new file mode 100644
index 0000000..197b720
--- /dev/null
+++ b/web/src/utils/useDark.ts
@@ -0,0 +1,49 @@
+import { useDark, useToggle } from '@vueuse/core'
+import { useConfig } from '/@/stores/config'
+import { onMounted, onUnmounted, ref, watch } from 'vue'
+
+const isDark = useDark({
+ onChanged(dark: boolean) {
+ const config = useConfig()
+ updateHtmlDarkClass(dark)
+ config.setLayout('isDark', dark)
+ config.onSetLayoutColor()
+ },
+})
+
+/**
+ * 切换暗黑模式
+ */
+const toggleDark = useToggle(isDark)
+
+/**
+ * 切换当前页面的暗黑模式
+ */
+export function togglePageDark(val: boolean) {
+ const config = useConfig()
+ const isDark = ref(config.layout.isDark)
+ onMounted(() => {
+ if (isDark.value !== val) updateHtmlDarkClass(val)
+ })
+ onUnmounted(() => {
+ updateHtmlDarkClass(isDark.value)
+ })
+ watch(
+ () => config.layout.isDark,
+ (newVal) => {
+ isDark.value = newVal
+ if (isDark.value !== val) updateHtmlDarkClass(val)
+ }
+ )
+}
+
+export function updateHtmlDarkClass(val: boolean) {
+ const htmlEl = document.getElementsByTagName('html')[0]
+ if (val) {
+ htmlEl.setAttribute('class', 'dark')
+ } else {
+ htmlEl.setAttribute('class', '')
+ }
+}
+
+export default toggleDark
diff --git a/web/src/utils/validate.ts b/web/src/utils/validate.ts
new file mode 100644
index 0000000..7a8fa48
--- /dev/null
+++ b/web/src/utils/validate.ts
@@ -0,0 +1,169 @@
+import type { RuleType } from 'async-validator'
+import type { FormItemRule } from 'element-plus'
+import { i18n } from '../lang'
+
+/**
+ * 手机号码验证
+ */
+export function validatorMobile(rule: any, mobile: string | number, callback: Function) {
+ // 允许空值,若需必填请添加多验证规则
+ if (!mobile) {
+ return callback()
+ }
+ if (!/^(1[3-9])\d{9}$/.test(mobile.toString())) {
+ return callback(new Error(i18n.global.t('validate.Please enter the correct mobile number')))
+ }
+ return callback()
+}
+
+/**
+ * 身份证号验证
+ */
+export function validatorIdNumber(rule: any, idNumber: string | number, callback: Function) {
+ if (!idNumber) {
+ return callback()
+ }
+ if (!/(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/.test(idNumber.toString())) {
+ return callback(new Error(i18n.global.t('validate.Please enter the correct ID number')))
+ }
+ return callback()
+}
+
+/**
+ * 账户名验证
+ */
+export function validatorAccount(rule: any, val: string, callback: Function) {
+ if (!val) {
+ return callback()
+ }
+ if (!/^[a-zA-Z][a-zA-Z0-9_]{2,15}$/.test(val)) {
+ return callback(new Error(i18n.global.t('validate.Please enter the correct account')))
+ }
+ return callback()
+}
+
+/**
+ * 密码验证
+ */
+export function regularPassword(val: string) {
+ return /^(?!.*[&<>"'\n\r]).{6,32}$/.test(val)
+}
+export function validatorPassword(rule: any, val: string, callback: Function) {
+ if (!val) {
+ return callback()
+ }
+ if (!regularPassword(val)) {
+ return callback(new Error(i18n.global.t('validate.Please enter the correct password')))
+ }
+ return callback()
+}
+
+/**
+ * 变量名验证
+ */
+export function regularVarName(val: string) {
+ return /^([^\x00-\xff]|[a-zA-Z_$])([^\x00-\xff]|[a-zA-Z0-9_$])*$/.test(val)
+}
+export function validatorVarName(rule: any, val: string, callback: Function) {
+ if (!val) {
+ return callback()
+ }
+ if (!regularVarName(val)) {
+ return callback(new Error(i18n.global.t('validate.Please enter the correct name')))
+ }
+ return callback()
+}
+
+export function validatorEditorRequired(rule: any, val: string, callback: Function) {
+ if (!val || val == '
') {
+ return callback(new Error(i18n.global.t('validate.Content cannot be empty')))
+ }
+ return callback()
+}
+
+/**
+ * 支持的表单验证规则
+ */
+export const validatorType = {
+ required: i18n.global.t('validate.required'),
+ mobile: i18n.global.t('utils.mobile'),
+ idNumber: i18n.global.t('utils.Id number'),
+ account: i18n.global.t('utils.account'),
+ password: i18n.global.t('utils.password'),
+ varName: i18n.global.t('utils.variable name'),
+ editorRequired: i18n.global.t('validate.editor required'),
+ url: 'URL',
+ email: i18n.global.t('utils.email'),
+ date: i18n.global.t('utils.date'),
+ number: i18n.global.t('validate.number'), // 数字(包括浮点和整数)
+ integer: i18n.global.t('validate.integer'), // 整数(不包括浮点数)
+ float: i18n.global.t('validate.float'), // 浮点数(不包括整数)
+}
+
+export interface buildValidatorParams {
+ // 规则名:required=必填,mobile=手机号,idNumber=身份证号,account=账户,password=密码,varName=变量名,editorRequired=富文本必填,number、integer、float、date、url、email
+ name:
+ | 'required'
+ | 'mobile'
+ | 'idNumber'
+ | 'account'
+ | 'password'
+ | 'varName'
+ | 'editorRequired'
+ | 'number'
+ | 'integer'
+ | 'float'
+ | 'date'
+ | 'url'
+ | 'email'
+ // 自定义验证错误消息
+ message?: string
+ // 验证项的标题,这些验证方式不支持:mobile、account、password、varName、editorRequired
+ title?: string
+ // 验证触发方式
+ trigger?: 'change' | 'blur'
+}
+
+/**
+ * 构建表单验证规则
+ * @param {buildValidatorParams} paramsObj 参数对象
+ */
+export function buildValidatorData({ name, message, title, trigger = 'blur' }: buildValidatorParams): FormItemRule {
+ // 必填
+ if (name == 'required') {
+ return {
+ required: true,
+ message: message ? message : i18n.global.t('Please input field', { field: title }),
+ trigger: trigger,
+ }
+ }
+
+ // 常见类型
+ const validatorType = ['number', 'integer', 'float', 'date', 'url', 'email']
+ if (validatorType.includes(name)) {
+ return {
+ type: name as RuleType,
+ message: message ? message : i18n.global.t('Please enter the correct field', { field: title }),
+ trigger: trigger,
+ }
+ }
+
+ // 自定义验证方法
+ const validatorCustomFun: anyObj = {
+ mobile: validatorMobile,
+ idNumber: validatorIdNumber,
+ account: validatorAccount,
+ password: validatorPassword,
+ varName: validatorVarName,
+ editorRequired: validatorEditorRequired,
+ }
+ if (validatorCustomFun[name]) {
+ return {
+ required: name == 'editorRequired' ? true : false,
+ validator: validatorCustomFun[name],
+ trigger: trigger,
+ message: message,
+ }
+ }
+ return {}
+}
diff --git a/web/src/utils/vite.ts b/web/src/utils/vite.ts
new file mode 100644
index 0000000..6500262
--- /dev/null
+++ b/web/src/utils/vite.ts
@@ -0,0 +1,184 @@
+import type { Plugin, ViteDevServer } from 'vite'
+import { reactive } from 'vue'
+
+interface HotUpdateState {
+ // 热更新状态
+ switch: boolean
+ // 热更新关闭类型:terminal=WEB终端执行命令,crud=CRUD,modules=模块安装服务,config=修改系统配置
+ closeType: string
+ // 是否有脏文件(热更新 switch 为 false,又触发了热更新就会产生脏文件)
+ dirtyFile: boolean
+ // 监听是否有脏文件
+ listenDirtyFileSwitch: boolean
+}
+
+/**
+ * 调试模式下的 Vite 热更新相关状态(这些状态均由 Vite 服务器记录并随时同步至客户端)
+ */
+export const hotUpdateState = reactive({
+ switch: true,
+ closeType: '',
+ dirtyFile: false,
+ listenDirtyFileSwitch: true,
+})
+
+/**
+ * Vite 相关初始化
+ */
+export function init() {
+ if (import.meta.hot) {
+ // 监听 Vite 服务器通知热更新相关状态更新
+ import.meta.hot.on('custom:change-hot-update-state', (state: Partial) => {
+ hotUpdateState.switch = state.switch ?? hotUpdateState.switch
+ hotUpdateState.closeType = state.closeType ?? hotUpdateState.closeType
+ hotUpdateState.dirtyFile = state.dirtyFile ?? hotUpdateState.dirtyFile
+ hotUpdateState.listenDirtyFileSwitch = state.listenDirtyFileSwitch ?? hotUpdateState.listenDirtyFileSwitch
+ })
+
+ // 保持脏文件监听功能开启(同时可以从服务端同步一次热更新服务的状态数据)
+ changeListenDirtyFileSwitch(true)
+ }
+}
+
+/**
+ * 是否在开发环境
+ */
+export function isDev(mode: string): boolean {
+ return mode === 'development'
+}
+
+/**
+ * 是否在生产环境
+ */
+export function isProd(mode: string | undefined): boolean {
+ return mode === 'production'
+}
+
+/**
+ * 调试模式下关闭热更新
+ */
+export const closeHotUpdate = (type: string) => {
+ if (import.meta.hot) {
+ import.meta.hot.send('custom:close-hot', { type })
+ }
+}
+
+/**
+ * 调试模式下开启热更新
+ */
+export const openHotUpdate = (type: string) => {
+ if (import.meta.hot) {
+ import.meta.hot.send('custom:open-hot', { type })
+ }
+}
+
+/**
+ * 调试模式下重启服务并刷新网页
+ */
+export const reloadServer = (type: string) => {
+ if (import.meta.hot) {
+ import.meta.hot.send('custom:reload-server', { type })
+ }
+}
+
+/**
+ * 改变脏文件监听功能的开关
+ */
+export const changeListenDirtyFileSwitch = (status: boolean) => {
+ if (import.meta.hot) {
+ import.meta.hot.send('custom:change-listen-dirty-file-switch', status)
+ }
+}
+
+/**
+ * 自定义热更新/热替换处理的 Vite 插件
+ */
+export const customHotUpdate = (): Plugin => {
+ type Listeners = ((...args: any[]) => void)[]
+
+ let addFunctionBack: Listeners = []
+ let unlinkFunctionBack: Listeners = []
+
+ // 本服务端的热更新状态数据
+ const hotUpdateState: HotUpdateState = {
+ switch: true,
+ closeType: '',
+ dirtyFile: false,
+ listenDirtyFileSwitch: true,
+ }
+
+ /**
+ * 同步所有热更新状态数据至客户端
+ */
+ const syncHotUpdateState = (server: ViteDevServer) => {
+ server.ws.send('custom:change-hot-update-state', hotUpdateState)
+ }
+
+ return {
+ name: 'vite-plugin-custom-hot-update',
+ apply: 'serve',
+ configureServer(server) {
+ // 关闭热更新
+ server.ws.on('custom:close-hot', ({ type }) => {
+ hotUpdateState.switch = false
+ hotUpdateState.closeType = type
+
+ // 备份文件添加和删除时的函数列表
+ addFunctionBack = server.watcher.listeners('add') as Listeners
+ unlinkFunctionBack = server.watcher.listeners('unlink') as Listeners
+
+ // 关闭文件添加和删除的监听
+ server.watcher.removeAllListeners('add')
+ server.watcher.removeAllListeners('unlink')
+
+ syncHotUpdateState(server)
+
+ // 文件添加时通知客户端新增了脏文件(文件删除无需记录为脏文件)
+ server.watcher.on('add', () => {
+ if (hotUpdateState.listenDirtyFileSwitch) {
+ hotUpdateState.dirtyFile = true
+ syncHotUpdateState(server)
+ }
+ })
+ })
+
+ // 开启热更新
+ server.ws.on('custom:open-hot', () => {
+ hotUpdateState.switch = true
+ server.watcher.removeAllListeners('add')
+ server.watcher.removeAllListeners('unlink')
+
+ // 恢复备份的函数列表
+ for (const key in addFunctionBack) {
+ server.watcher.on('add', addFunctionBack[key])
+ }
+ for (const key in unlinkFunctionBack) {
+ server.watcher.on('unlink', unlinkFunctionBack[key])
+ }
+
+ syncHotUpdateState(server)
+ })
+
+ // 重启热更新
+ server.ws.on('custom:reload-server', () => {
+ server.restart()
+ })
+
+ // 客户端可从本服务端获取热更新服务状态数据
+ server.ws.on('custom:get-hot-update-state', () => {
+ syncHotUpdateState(server)
+ })
+
+ // 修改监听脏文件的开关
+ server.ws.on('custom:change-listen-dirty-file-switch', (status: boolean) => {
+ hotUpdateState.listenDirtyFileSwitch = status
+ syncHotUpdateState(server)
+ })
+ },
+ handleHotUpdate() {
+ if (!hotUpdateState.switch) {
+ return []
+ }
+ },
+ }
+}
diff --git a/web/src/views/backend/auth/admin/index.vue b/web/src/views/backend/auth/admin/index.vue
new file mode 100644
index 0000000..295f068
--- /dev/null
+++ b/web/src/views/backend/auth/admin/index.vue
@@ -0,0 +1,97 @@
+
+
+
+
+
+
+
diff --git a/web/src/views/backend/auth/admin/popupForm.vue b/web/src/views/backend/auth/admin/popupForm.vue
new file mode 100644
index 0000000..cf3e43c
--- /dev/null
+++ b/web/src/views/backend/auth/admin/popupForm.vue
@@ -0,0 +1,198 @@
+
+
+
+
+
+ {{ baTable.form.operate ? t(baTable.form.operate) : '' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('Cancel') }}
+
+ {{ baTable.form.operateIds && baTable.form.operateIds.length > 1 ? t('Save and edit next item') : t('Save') }}
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/backend/auth/adminLog/index.vue b/web/src/views/backend/auth/adminLog/index.vue
new file mode 100644
index 0000000..ee8e697
--- /dev/null
+++ b/web/src/views/backend/auth/adminLog/index.vue
@@ -0,0 +1,145 @@
+
+
+
+
+
+
+
diff --git a/web/src/views/backend/auth/adminLog/info.vue b/web/src/views/backend/auth/adminLog/info.vue
new file mode 100644
index 0000000..cd4dc6f
--- /dev/null
+++ b/web/src/views/backend/auth/adminLog/info.vue
@@ -0,0 +1,62 @@
+
+
+
+
+ {{ t('Info') }}
+
+
+
+
+
+ {{ baTable.form.extend!.info.id }}
+
+
+ {{ baTable.form.extend!.info.username }}
+
+
+ {{ baTable.form.extend!.info.title }}
+
+
+ {{ baTable.form.extend!.info.ip }}
+
+
+ {{ baTable.form.extend!.info.url }}
+
+
+ {{ baTable.form.extend!.info.useragent }}
+
+
+ {{ timeFormat(baTable.form.extend!.info.create_time) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/backend/auth/group/index.vue b/web/src/views/backend/auth/group/index.vue
new file mode 100644
index 0000000..42d574b
--- /dev/null
+++ b/web/src/views/backend/auth/group/index.vue
@@ -0,0 +1,179 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/backend/auth/group/popupForm.vue b/web/src/views/backend/auth/group/popupForm.vue
new file mode 100644
index 0000000..95ebb66
--- /dev/null
+++ b/web/src/views/backend/auth/group/popupForm.vue
@@ -0,0 +1,173 @@
+
+
+
+
+
+ {{ baTable.form.operate ? t(baTable.form.operate) : '' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('Cancel') }}
+
+ {{ baTable.form.operateIds && baTable.form.operateIds.length > 1 ? t('Save and edit next item') : t('Save') }}
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/backend/auth/rule/index.vue b/web/src/views/backend/auth/rule/index.vue
new file mode 100644
index 0000000..f147310
--- /dev/null
+++ b/web/src/views/backend/auth/rule/index.vue
@@ -0,0 +1,196 @@
+
+
+
+
+
+
+
diff --git a/web/src/views/backend/auth/rule/popupForm.vue b/web/src/views/backend/auth/rule/popupForm.vue
new file mode 100644
index 0000000..4845f82
--- /dev/null
+++ b/web/src/views/backend/auth/rule/popupForm.vue
@@ -0,0 +1,244 @@
+
+
+
+
+
+ {{ baTable.form.operate ? t(baTable.form.operate) : '' }}
+
+
+
+
+
+
+
+ {{ t('Cancel') }}
+
+ {{ baTable.form.operateIds && baTable.form.operateIds.length > 1 ? t('Save and edit next item') : t('Save') }}
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/backend/crud/design.vue b/web/src/views/backend/crud/design.vue
new file mode 100644
index 0000000..ee588b0
--- /dev/null
+++ b/web/src/views/backend/crud/design.vue
@@ -0,0 +1,2075 @@
+
+
+
+
+
+
+
+
+
+ {{ field.title }}
+
+
+
+
+
+
+ {{ field.title }}
+
+
+
+
+
+
+ {{ field.title }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('Cancel') }}
+
+ {{ $t('Save') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('crud.crud.Data table design changes preview') }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('crud.crud.designChangeTips') }}
+
+ 暂无表设计变更
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/backend/crud/index.ts b/web/src/views/backend/crud/index.ts
new file mode 100644
index 0000000..85559e1
--- /dev/null
+++ b/web/src/views/backend/crud/index.ts
@@ -0,0 +1,970 @@
+import { reactive } from 'vue'
+import { fieldData, npuaFalse } from '/@/components/baInput/helper'
+import { i18n } from '/@/lang/index'
+import { validatorType } from '/@/utils/validate'
+
+/**
+ * 字段修改类型标识
+ * 改排序需要在表结构变更完成之后再单独处理所以标识独立
+ */
+export type TableDesignChangeType = 'change-field-name' | 'del-field' | 'add-field' | 'change-field-attr' | 'change-field-order'
+
+export interface TableDesignChange {
+ type: TableDesignChangeType
+ // 字段在设计器中的数组 index
+ index?: number
+ // 字段旧名称(重命名、修改属性、删除)
+ oldName: string
+ // 字段新名称(重命名、添加)
+ newName: string
+ // 是否同步到数据表
+ sync?: boolean
+ // 此字段在 after 字段之后,值为`FIRST FIELD`表示它是第一个字段
+ after?: string
+}
+
+export const state: {
+ step: 'Start' | 'Design'
+ type: string
+ startData: {
+ sql: string
+ table: string
+ logId: string
+ logType: string
+ databaseConnection: string
+ }
+} = reactive({
+ step: 'Start',
+ type: '',
+ startData: {
+ sql: '',
+ table: '',
+ logId: '',
+ logType: '',
+ databaseConnection: '',
+ },
+})
+
+export const changeStep = (type: string) => {
+ state.type = type
+ if (type == 'start') {
+ state.step = 'Start'
+ for (const key in state.startData) {
+ state.startData[key as keyof typeof state.startData] = ''
+ }
+ } else {
+ state.step = 'Design'
+ }
+}
+
+export interface FieldItem {
+ index?: number
+ title: string
+ name: string
+ type: string
+ dataType?: string
+ length: number
+ precision: number
+ default?: string
+ defaultType: 'INPUT' | 'EMPTY STRING' | 'NULL' | 'NONE'
+ null: boolean
+ primaryKey: boolean
+ unsigned: boolean
+ autoIncrement: boolean
+ comment: string
+ designType: string
+ formBuildExclude?: boolean
+ tableBuildExclude?: boolean
+ table: anyObj
+ form: anyObj
+ uuid?: string
+}
+
+export const fieldItem: {
+ common: FieldItem[]
+ base: FieldItem[]
+ senior: FieldItem[]
+} = {
+ common: [
+ {
+ title: i18n.global.t('crud.state.Primary key'),
+ name: 'id',
+ comment: 'ID',
+ designType: 'pk',
+ formBuildExclude: true,
+ table: {},
+ form: {},
+ ...fieldData.number,
+ defaultType: 'NONE',
+ null: false,
+ primaryKey: true,
+ unsigned: true,
+ autoIncrement: true,
+ },
+ {
+ title: i18n.global.t('crud.state.Primary key (Snowflake ID)'),
+ name: 'id',
+ comment: 'ID',
+ designType: 'spk',
+ formBuildExclude: true,
+ table: {},
+ form: {},
+ ...fieldData.number,
+ type: 'bigint',
+ length: 20,
+ defaultType: 'NONE',
+ null: false,
+ primaryKey: true,
+ unsigned: true,
+ },
+ {
+ title: i18n.global.t('State'),
+ name: 'status',
+ comment: i18n.global.t('crud.state.Status:0=Disabled,1=Enabled'),
+ designType: 'switch',
+ table: {},
+ form: {},
+ ...fieldData.switch,
+ default: '1',
+ defaultType: 'INPUT',
+ },
+ {
+ title: i18n.global.t('crud.state.remarks'),
+ name: 'remark',
+ comment: i18n.global.t('crud.state.remarks'),
+ designType: 'textarea',
+ tableBuildExclude: true,
+ table: {},
+ form: {},
+ ...fieldData.textarea,
+ },
+ {
+ title: i18n.global.t('crud.state.Weight (drag and drop sorting)'),
+ name: 'weigh',
+ comment: i18n.global.t('Weigh'),
+ designType: 'weigh',
+ table: {},
+ form: {},
+ ...fieldData.number,
+ },
+ {
+ title: i18n.global.t('Update time'),
+ name: 'update_time',
+ comment: i18n.global.t('Update time'),
+ designType: 'timestamp',
+ formBuildExclude: true,
+ table: {},
+ form: {},
+ ...fieldData.datetime,
+ },
+ {
+ title: i18n.global.t('Create time'),
+ name: 'create_time',
+ comment: i18n.global.t('Create time'),
+ designType: 'timestamp',
+ formBuildExclude: true,
+ table: {},
+ form: {},
+ ...fieldData.datetime,
+ },
+ {
+ title: i18n.global.t('crud.state.Remote Select (association table)'),
+ name: 'remote_select',
+ comment: i18n.global.t('utils.remote select'),
+ designType: 'remoteSelect',
+ tableBuildExclude: true,
+ table: {},
+ form: {},
+ ...fieldData.remoteSelect,
+ },
+ ],
+ base: [
+ {
+ title: i18n.global.t('utils.string'),
+ name: 'string',
+ comment: i18n.global.t('utils.string'),
+ designType: 'string',
+ table: {},
+ form: {},
+ ...fieldData.string,
+ },
+ {
+ title: i18n.global.t('utils.image'),
+ name: 'image',
+ comment: i18n.global.t('utils.image'),
+ designType: 'image',
+ table: {},
+ form: {},
+ ...fieldData.image,
+ },
+ {
+ title: i18n.global.t('utils.file'),
+ name: 'file',
+ comment: i18n.global.t('utils.file'),
+ designType: 'file',
+ tableBuildExclude: true,
+ table: {},
+ form: {},
+ ...fieldData.file,
+ },
+ {
+ title: i18n.global.t('utils.radio'),
+ name: 'radio',
+ dataType: "enum('opt0','opt1')",
+ comment: i18n.global.t('crud.state.Radio:opt0=Option1,opt1=Option2'),
+ designType: 'radio',
+ table: {},
+ form: {},
+ ...fieldData.radio,
+ default: 'opt0',
+ defaultType: 'INPUT',
+ },
+ {
+ title: i18n.global.t('utils.checkbox'),
+ name: 'checkbox',
+ dataType: "set('opt0','opt1')",
+ comment: i18n.global.t('crud.state.Checkbox:opt0=Option1,opt1=Option2'),
+ designType: 'checkbox',
+ table: {},
+ form: {},
+ ...fieldData.checkbox,
+ default: 'opt0,opt1',
+ defaultType: 'INPUT',
+ },
+ {
+ title: i18n.global.t('utils.select'),
+ name: 'select',
+ dataType: "enum('opt0','opt1')",
+ comment: i18n.global.t('crud.state.Select:opt0=Option1,opt1=Option2'),
+ designType: 'select',
+ table: {},
+ form: {},
+ ...fieldData.select,
+ default: 'opt0',
+ defaultType: 'INPUT',
+ },
+ {
+ title: i18n.global.t('utils.switch'),
+ name: 'switch',
+ comment: i18n.global.t('crud.state.Switch:0=off,1=on'),
+ designType: 'switch',
+ table: {},
+ form: {},
+ ...fieldData.switch,
+ default: '1',
+ defaultType: 'INPUT',
+ },
+ {
+ title: i18n.global.t('utils.rich Text'),
+ name: 'editor',
+ comment: i18n.global.t('utils.rich Text'),
+ designType: 'editor',
+ tableBuildExclude: true,
+ table: {},
+ form: {},
+ ...fieldData.editor,
+ },
+ {
+ title: i18n.global.t('utils.textarea'),
+ name: 'textarea',
+ comment: i18n.global.t('utils.textarea'),
+ designType: 'textarea',
+ tableBuildExclude: true,
+ table: {},
+ form: {},
+ ...fieldData.textarea,
+ },
+ {
+ title: i18n.global.t('utils.number'),
+ name: 'number',
+ comment: i18n.global.t('utils.number'),
+ designType: 'number',
+ table: {},
+ form: {},
+ ...fieldData.number,
+ },
+ {
+ title: i18n.global.t('utils.float'),
+ name: 'float',
+ type: 'decimal',
+ length: 5,
+ precision: 2,
+ defaultType: 'NULL',
+ ...npuaFalse(),
+ null: true,
+ comment: i18n.global.t('utils.float'),
+ designType: 'float',
+ table: {},
+ form: {},
+ },
+ {
+ title: i18n.global.t('utils.password'),
+ name: 'password',
+ comment: i18n.global.t('utils.password'),
+ designType: 'password',
+ tableBuildExclude: true,
+ table: {},
+ form: {},
+ ...fieldData.password,
+ },
+ {
+ title: i18n.global.t('utils.date'),
+ name: 'date',
+ comment: i18n.global.t('utils.date'),
+ designType: 'date',
+ table: {},
+ form: {},
+ ...fieldData.date,
+ },
+ {
+ title: i18n.global.t('utils.time'),
+ name: 'time',
+ comment: i18n.global.t('utils.time'),
+ designType: 'time',
+ table: {},
+ form: {},
+ ...fieldData.time,
+ },
+ {
+ title: i18n.global.t('utils.time date'),
+ name: 'datetime',
+ type: 'datetime',
+ length: 0,
+ precision: 0,
+ defaultType: 'NULL',
+ ...npuaFalse(),
+ null: true,
+ comment: i18n.global.t('utils.time date'),
+ designType: 'datetime',
+ table: {},
+ form: {},
+ },
+ {
+ title: i18n.global.t('utils.year'),
+ name: 'year',
+ comment: i18n.global.t('utils.year'),
+ designType: 'year',
+ table: {},
+ form: {},
+ ...fieldData.year,
+ },
+ {
+ title: i18n.global.t('crud.state.Time date (timestamp storage)'),
+ name: 'timestamp',
+ comment: i18n.global.t('utils.time date'),
+ designType: 'timestamp',
+ table: {},
+ form: {},
+ ...fieldData.datetime,
+ },
+ ],
+ senior: [
+ {
+ title: i18n.global.t('utils.array'),
+ name: 'array',
+ comment: i18n.global.t('utils.array'),
+ designType: 'array',
+ tableBuildExclude: true,
+ table: {},
+ form: {},
+ ...fieldData.array,
+ },
+ {
+ title: i18n.global.t('utils.city select'),
+ name: 'city',
+ comment: i18n.global.t('utils.city select'),
+ designType: 'city',
+ table: {},
+ form: {},
+ ...fieldData.city,
+ },
+ {
+ title: i18n.global.t('utils.icon select'),
+ name: 'icon',
+ comment: i18n.global.t('utils.icon select'),
+ designType: 'icon',
+ table: {},
+ form: {},
+ ...fieldData.icon,
+ },
+ {
+ title: i18n.global.t('utils.color picker'),
+ name: 'color',
+ comment: i18n.global.t('utils.color picker'),
+ designType: 'color',
+ table: {},
+ form: {},
+ ...fieldData.color,
+ },
+ {
+ title: i18n.global.t('utils.image') + i18n.global.t('crud.state.Multi'),
+ name: 'images',
+ comment: i18n.global.t('utils.image'),
+ designType: 'images',
+ table: {},
+ form: {},
+ ...fieldData.images,
+ },
+ {
+ title: i18n.global.t('utils.file') + i18n.global.t('crud.state.Multi'),
+ name: 'files',
+ comment: i18n.global.t('utils.file'),
+ designType: 'files',
+ tableBuildExclude: true,
+ table: {},
+ form: {},
+ ...fieldData.files,
+ },
+ {
+ title: i18n.global.t('utils.select') + i18n.global.t('crud.state.Multi'),
+ name: 'selects',
+ comment: i18n.global.t('crud.state.Select:opt0=Option1,opt1=Option2'),
+ designType: 'selects',
+ table: {},
+ form: {},
+ ...fieldData.selects,
+ },
+ {
+ title: i18n.global.t('crud.state.Remote Select (Multi)'),
+ name: 'remote_select',
+ comment: i18n.global.t('utils.remote select'),
+ designType: 'remoteSelects',
+ tableBuildExclude: true,
+ table: {},
+ form: {},
+ ...fieldData.remoteSelects,
+ },
+ ],
+}
+
+const tableBaseAttr = {
+ render: {
+ type: 'select',
+ value: 'none',
+ options: {
+ none: i18n.global.t('None'),
+ icon: 'Icon',
+ switch: i18n.global.t('utils.switch'),
+ image: i18n.global.t('utils.image'),
+ images: i18n.global.t('utils.multi image'),
+ tag: 'Tag',
+ tags: 'Tags',
+ url: 'URL',
+ datetime: i18n.global.t('utils.time date'),
+ color: i18n.global.t('utils.color'),
+ },
+ },
+ operator: {
+ type: 'select',
+ value: 'eq',
+ options: {
+ false: i18n.global.t('crud.state.Disable Search'),
+ eq: 'eq =',
+ ne: 'ne !=',
+ gt: 'gt >',
+ egt: 'egt >=',
+ lt: 'lt <',
+ elt: 'elt <=',
+ LIKE: 'LIKE',
+ 'NOT LIKE': 'NOT LIKE',
+ IN: 'IN',
+ 'NOT IN': 'NOT IN',
+ RANGE: 'RANGE',
+ 'NOT RANGE': 'NOT RANGE',
+ NULL: 'NULL',
+ 'NOT NULL': 'NOT NULL',
+ FIND_IN_SET: 'FIND_IN_SET',
+ },
+ },
+ comSearchRender: {
+ type: 'select',
+ value: 'string',
+ options: {
+ string: i18n.global.t('utils.string'),
+ select: i18n.global.t('utils.select'),
+ remoteSelect: i18n.global.t('utils.remote select'),
+ time: i18n.global.t('utils.time') + i18n.global.t('utils.choice'),
+ date: i18n.global.t('utils.date') + i18n.global.t('utils.choice'),
+ datetime: i18n.global.t('utils.time date') + i18n.global.t('utils.choice'),
+ },
+ },
+ comSearchInputAttr: {
+ type: 'textarea',
+ value: '',
+ placeholder: i18n.global.t('crud.crud.comSearchInputAttrTip'),
+ attr: {
+ rows: 3,
+ },
+ },
+ sortable: {
+ type: 'select',
+ value: 'false',
+ options: {
+ false: i18n.global.t('Disable'),
+ custom: i18n.global.t('Enable'),
+ },
+ },
+}
+
+const formBaseAttr = {
+ validator: {
+ type: 'selects',
+ value: [],
+ options: validatorType,
+ },
+ validatorMsg: {
+ type: 'textarea',
+ value: '',
+ placeholder: i18n.global.t('crud.state.If left blank, the verifier title attribute will be filled in automatically'),
+ attr: {
+ rows: 3,
+ },
+ },
+}
+
+export const getTableAttr = (type: keyof typeof tableBaseAttr, val: string) => {
+ return {
+ ...tableBaseAttr[type],
+ value: val,
+ }
+}
+
+const getFormAttr = (type: keyof typeof formBaseAttr, val: string[]) => {
+ return {
+ ...formBaseAttr[type],
+ value: val,
+ }
+}
+
+export const designTypes: anyObj = {
+ pk: {
+ name: i18n.global.t('crud.state.Primary key'),
+ table: {
+ width: {
+ type: 'number',
+ value: 70,
+ },
+ operator: getTableAttr('operator', 'RANGE'),
+ sortable: getTableAttr('sortable', 'custom'),
+ },
+ form: {},
+ },
+ spk: {
+ name: i18n.global.t('crud.state.Primary key (Snowflake ID)'),
+ table: {
+ width: {
+ type: 'number',
+ value: 180,
+ },
+ operator: getTableAttr('operator', 'RANGE'),
+ sortable: getTableAttr('sortable', 'custom'),
+ },
+ form: {},
+ },
+ weigh: {
+ name: i18n.global.t('crud.state.Weight (automatically generate drag sort button)'),
+ table: {
+ operator: getTableAttr('operator', 'RANGE'),
+ sortable: getTableAttr('sortable', 'custom'),
+ },
+ form: formBaseAttr,
+ },
+ timestamp: {
+ name: i18n.global.t('crud.state.Time date (timestamp storage)'),
+ table: {
+ render: getTableAttr('render', 'datetime'),
+ operator: getTableAttr('operator', 'RANGE'),
+ comSearchRender: getTableAttr('comSearchRender', 'datetime'),
+ comSearchInputAttr: getTableAttr('comSearchInputAttr', ''),
+ sortable: getTableAttr('sortable', 'custom'),
+ width: {
+ type: 'number',
+ value: 160,
+ },
+ timeFormat: {
+ type: 'string',
+ value: 'yyyy-mm-dd hh:MM:ss',
+ },
+ },
+ form: {
+ ...formBaseAttr,
+ validator: getFormAttr('validator', ['date']),
+ },
+ },
+ string: {
+ name: i18n.global.t('utils.string'),
+ table: {
+ render: getTableAttr('render', 'none'),
+ sortable: getTableAttr('sortable', 'false'),
+ operator: getTableAttr('operator', 'LIKE'),
+ },
+ form: formBaseAttr,
+ },
+ password: {
+ name: i18n.global.t('utils.password'),
+ table: {
+ operator: getTableAttr('operator', 'false'),
+ },
+ form: {
+ ...formBaseAttr,
+ validator: getFormAttr('validator', ['password']),
+ },
+ },
+ number: {
+ name: i18n.global.t('utils.number'),
+ table: {
+ render: getTableAttr('render', 'none'),
+ sortable: getTableAttr('sortable', 'false'),
+ operator: getTableAttr('operator', 'RANGE'),
+ },
+ form: {
+ ...formBaseAttr,
+ validator: getFormAttr('validator', ['number']),
+ step: {
+ type: 'number',
+ value: 1,
+ },
+ },
+ },
+ float: {
+ name: i18n.global.t('utils.float'),
+ table: {
+ render: getTableAttr('render', 'none'),
+ sortable: getTableAttr('sortable', 'false'),
+ operator: getTableAttr('operator', 'RANGE'),
+ },
+ form: {
+ ...formBaseAttr,
+ validator: getFormAttr('validator', ['float']),
+ step: {
+ type: 'number',
+ value: 1,
+ },
+ },
+ },
+ radio: {
+ name: i18n.global.t('utils.radio'),
+ table: {
+ operator: getTableAttr('operator', 'eq'),
+ sortable: getTableAttr('sortable', 'false'),
+ render: getTableAttr('render', 'tag'),
+ },
+ form: formBaseAttr,
+ },
+ checkbox: {
+ name: i18n.global.t('utils.checkbox'),
+ table: {
+ sortable: getTableAttr('sortable', 'false'),
+ render: getTableAttr('render', 'tags'),
+ operator: getTableAttr('operator', 'FIND_IN_SET'),
+ },
+ form: formBaseAttr,
+ },
+ switch: {
+ name: i18n.global.t('utils.switch'),
+ table: {
+ operator: getTableAttr('operator', 'eq'),
+ sortable: getTableAttr('sortable', 'false'),
+ render: getTableAttr('render', 'switch'),
+ },
+ form: formBaseAttr,
+ },
+ textarea: {
+ name: i18n.global.t('utils.textarea'),
+ table: {
+ operator: getTableAttr('operator', 'false'),
+ },
+ form: {
+ ...formBaseAttr,
+ rows: {
+ type: 'number',
+ value: 3,
+ },
+ },
+ },
+ array: {
+ name: i18n.global.t('utils.array'),
+ table: {
+ operator: getTableAttr('operator', 'false'),
+ },
+ form: formBaseAttr,
+ },
+ datetime: {
+ name: i18n.global.t('utils.time date') + i18n.global.t('utils.choice'),
+ table: {
+ operator: getTableAttr('operator', 'RANGE'),
+ comSearchRender: getTableAttr('comSearchRender', 'datetime'),
+ comSearchInputAttr: getTableAttr('comSearchInputAttr', ''),
+ sortable: getTableAttr('sortable', 'custom'),
+ width: {
+ type: 'number',
+ value: 160,
+ },
+ },
+ form: {
+ ...formBaseAttr,
+ validator: getFormAttr('validator', ['date']),
+ },
+ },
+ year: {
+ name: i18n.global.t('utils.year') + i18n.global.t('utils.choice'),
+ table: {
+ operator: getTableAttr('operator', 'RANGE'),
+ sortable: getTableAttr('sortable', 'custom'),
+ },
+ form: {
+ ...formBaseAttr,
+ validator: getFormAttr('validator', ['date']),
+ },
+ },
+ date: {
+ name: i18n.global.t('utils.date') + i18n.global.t('utils.choice'),
+ table: {
+ operator: getTableAttr('operator', 'RANGE'),
+ comSearchRender: getTableAttr('comSearchRender', 'date'),
+ comSearchInputAttr: getTableAttr('comSearchInputAttr', ''),
+ sortable: getTableAttr('sortable', 'custom'),
+ },
+ form: {
+ ...formBaseAttr,
+ validator: getFormAttr('validator', ['date']),
+ },
+ },
+ time: {
+ name: i18n.global.t('utils.time') + i18n.global.t('utils.choice'),
+ table: {
+ operator: getTableAttr('operator', 'RANGE'),
+ comSearchRender: getTableAttr('comSearchRender', 'time'),
+ comSearchInputAttr: getTableAttr('comSearchInputAttr', ''),
+ sortable: getTableAttr('sortable', 'custom'),
+ },
+ form: formBaseAttr,
+ },
+ select: {
+ name: i18n.global.t('utils.select'),
+ table: {
+ operator: getTableAttr('operator', 'eq'),
+ sortable: getTableAttr('sortable', 'false'),
+ render: getTableAttr('render', 'tag'),
+ },
+ form: {
+ ...formBaseAttr,
+ 'select-multi': {
+ type: 'switch',
+ value: false,
+ },
+ },
+ },
+ selects: {
+ name: i18n.global.t('utils.select') + i18n.global.t('crud.state.Multi'),
+ table: {
+ sortable: getTableAttr('sortable', 'false'),
+ render: getTableAttr('render', 'tags'),
+ operator: getTableAttr('operator', 'FIND_IN_SET'),
+ },
+ form: {
+ ...formBaseAttr,
+ 'select-multi': {
+ type: 'switch',
+ value: true,
+ },
+ },
+ },
+ remoteSelect: {
+ name: i18n.global.t('utils.remote select') + i18n.global.t('utils.choice'),
+ table: {
+ render: getTableAttr('render', 'tags'),
+ operator: getTableAttr('operator', 'LIKE'),
+ comSearchRender: getTableAttr('comSearchRender', 'string'),
+ comSearchInputAttr: getTableAttr('comSearchInputAttr', ''),
+ },
+ form: {
+ ...formBaseAttr,
+ 'select-multi': {
+ type: 'switch',
+ value: false,
+ },
+ 'remote-pk': {
+ type: 'string',
+ value: 'id',
+ },
+ 'remote-field': {
+ type: 'string',
+ value: 'name',
+ },
+ 'remote-table': {
+ type: 'string',
+ value: '',
+ },
+ 'remote-controller': {
+ type: 'string',
+ value: '',
+ },
+ 'remote-model': {
+ type: 'string',
+ value: '',
+ },
+ 'relation-fields': {
+ type: 'string',
+ value: '',
+ },
+ 'remote-url': {
+ type: 'string',
+ value: '',
+ placeholder: i18n.global.t('crud.state.If it is not input, it will be automatically analyzed by the controller'),
+ },
+ 'remote-primary-table-alias': {
+ type: 'string',
+ value: '',
+ },
+ 'remote-source-config-type': {
+ type: 'hidden',
+ value: '',
+ },
+ },
+ },
+ remoteSelects: {
+ name: i18n.global.t('utils.remote select') + i18n.global.t('utils.choice') + i18n.global.t('crud.state.Multi'),
+ table: {
+ render: getTableAttr('render', 'tags'),
+ operator: getTableAttr('operator', 'FIND_IN_SET'),
+ comSearchRender: getTableAttr('comSearchRender', 'remoteSelect'),
+ comSearchInputAttr: getTableAttr('comSearchInputAttr', ''),
+ },
+ form: {
+ ...formBaseAttr,
+ 'select-multi': {
+ type: 'switch',
+ value: true,
+ },
+ 'remote-pk': {
+ type: 'string',
+ value: 'id',
+ },
+ 'remote-field': {
+ type: 'string',
+ value: 'name',
+ },
+ 'remote-table': {
+ type: 'string',
+ value: '',
+ },
+ 'remote-controller': {
+ type: 'string',
+ value: '',
+ },
+ 'remote-model': {
+ type: 'string',
+ value: '',
+ },
+ 'relation-fields': {
+ type: 'string',
+ value: '',
+ },
+ 'remote-url': {
+ type: 'string',
+ value: '',
+ placeholder: i18n.global.t('crud.state.If it is not input, it will be automatically analyzed by the controller'),
+ },
+ 'remote-primary-table-alias': {
+ type: 'string',
+ value: '',
+ },
+ 'remote-source-config-type': {
+ type: 'hidden',
+ value: '',
+ },
+ },
+ },
+ editor: {
+ name: i18n.global.t('utils.rich Text'),
+ table: {
+ operator: getTableAttr('operator', 'false'),
+ },
+ form: {
+ ...formBaseAttr,
+ validator: getFormAttr('validator', ['editorRequired']),
+ },
+ },
+ city: {
+ name: i18n.global.t('utils.city select'),
+ table: {
+ operator: getTableAttr('operator', 'false'),
+ },
+ form: formBaseAttr,
+ },
+ image: {
+ name: i18n.global.t('utils.image') + i18n.global.t('Upload'),
+ table: {
+ render: getTableAttr('render', 'image'),
+ operator: getTableAttr('operator', 'false'),
+ },
+ form: {
+ ...formBaseAttr,
+ 'image-multi': {
+ type: 'switch',
+ value: false,
+ },
+ },
+ },
+ images: {
+ name: i18n.global.t('utils.image') + i18n.global.t('Upload') + i18n.global.t('crud.state.Multi'),
+ table: {
+ render: getTableAttr('render', 'images'),
+ operator: getTableAttr('operator', 'false'),
+ },
+ form: {
+ ...formBaseAttr,
+ 'image-multi': {
+ type: 'switch',
+ value: true,
+ },
+ },
+ },
+ file: {
+ name: i18n.global.t('utils.file') + i18n.global.t('Upload'),
+ table: {
+ render: getTableAttr('render', 'none'),
+ operator: getTableAttr('operator', 'false'),
+ },
+ form: {
+ ...formBaseAttr,
+ 'file-multi': {
+ type: 'switch',
+ value: false,
+ },
+ },
+ },
+ files: {
+ name: i18n.global.t('utils.file') + i18n.global.t('Upload') + i18n.global.t('crud.state.Multi'),
+ table: {
+ render: getTableAttr('render', 'none'),
+ operator: getTableAttr('operator', 'false'),
+ },
+ form: {
+ ...formBaseAttr,
+ 'file-multi': {
+ type: 'switch',
+ value: true,
+ },
+ },
+ },
+ icon: {
+ name: i18n.global.t('utils.icon select'),
+ table: {
+ render: getTableAttr('render', 'icon'),
+ operator: getTableAttr('operator', 'false'),
+ },
+ form: formBaseAttr,
+ },
+ color: {
+ name: i18n.global.t('utils.color picker'),
+ table: {
+ render: getTableAttr('render', 'color'),
+ operator: getTableAttr('operator', 'false'),
+ },
+ form: formBaseAttr,
+ },
+}
+
+export const tableFieldsKey = ['quickSearchField', 'formFields', 'columnFields']
diff --git a/web/src/views/backend/crud/index.vue b/web/src/views/backend/crud/index.vue
new file mode 100644
index 0000000..1c00aae
--- /dev/null
+++ b/web/src/views/backend/crud/index.vue
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/backend/crud/log.vue b/web/src/views/backend/crud/log.vue
new file mode 100644
index 0000000..9d931e6
--- /dev/null
+++ b/web/src/views/backend/crud/log.vue
@@ -0,0 +1,578 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ (scope.row.table.databaseConnection ? scope.row.table.databaseConnection + '.' : '') + scope.row.table.name }}
+
+
+
+
+
+
+
+ {{ scope.row.sync > 0 ? t('crud.log.sync yes') : t('crud.log.sync no') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('Cancel') }}
+ {{ t('Save') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ scope.row.id > 0 ? t('crud.log.Update') : t('crud.log.New added') }}
+
+
+
+
+
+ {{ scope.row.score }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('Cancel') }}
+ {{ t('Upload') }}
+
+
+
+
+
+
+
+
+
+
+ {{ (scope.row.table.databaseConnection ? scope.row.table.databaseConnection + '.' : '') + scope.row.table.name }}
+
+
+
+
+
+
+
+ {{ scope.row.fieldCount }}
+
+
+
+
+
+
+ {{ typeScope.row.dataType ?? typeScope.row.type }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('crud.log.Load') }}
+
+
+
+
+
+
+ {{ t('Delete') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/backend/crud/start.vue b/web/src/views/backend/crud/start.vue
new file mode 100644
index 0000000..c1aa244
--- /dev/null
+++ b/web/src/views/backend/crud/start.vue
@@ -0,0 +1,319 @@
+
+
+
{{ t('crud.crud.start') }}
+
+
+
+
+
{{ t('crud.crud.create') }}
+
+
+
+
+
+
{{ t('crud.crud.Select Data Table') }}
+
+
+
+
+
+
{{ t('crud.crud.CRUD record') }}
+
+
+
+
+
+
+ {{ t('crud.crud.Fast experience') }}
+
+
+ {{ t('crud.crud.experience 1 1') }}
+
+ {{ t('crud.crud.experience 1 2') }}
+
+ {{ t('crud.crud.experience 1 3') }}
+
+
+ {{ t('crud.crud.experience 2 1') }}
+ {{ t('crud.crud.create') }}
+ {{ t('crud.crud.or') }}
+ {{ t('crud.crud.experience 2 2') }}{{ t('crud.crud.experience 2 3') }}
+
+
+ {{ t('crud.crud.experience 3 1') }} {{ t('crud.crud.experience 3 2') }}
+ {{ t('crud.crud.experience 3 3') }}
+ {{ t('crud.crud.experience 3 4') }}
+
+
+
+
+ {{ t('crud.crud.experience 4 1') }}
+
+ {{ t('crud.crud.experience 4 2') }}
+
+
+ {{ t('crud.crud.experience 4 3') }}{{ t('crud.crud.experience 4 4') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('Cancel') }}
+ {{ t('Confirm') }}
+
+ {{ t('crud.crud.Start with the historical record') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/backend/dashboard.vue b/web/src/views/backend/dashboard.vue
new file mode 100644
index 0000000..0921c94
--- /dev/null
+++ b/web/src/views/backend/dashboard.vue
@@ -0,0 +1,826 @@
+
+
+
+
+
+
+
+
+
{{ adminInfo.nickname + t('utils.comma') + getGreet() }}
+
{{ state.remark }}
+
+
+
+
+
+
+
+ {{ t('dashboard.You have worked today') }}{{ state.workingTimeFormat }}
+
+
+ {{ state.pauseWork ? t('dashboard.Continue to work') : t('dashboard.have a bit of rest') }}
+
+
+
+
+
+
+
+
+
+
{{ t('dashboard.Member registration') }}
+
+
+
+
+
+
{{ t('dashboard.Number of attachments Uploaded') }}
+
+
+
+
+
+
{{ t('dashboard.Total number of members') }}
+
+
+
+
+
+
{{ t('dashboard.Number of installed plug-ins') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
妙码生花
+
12分钟前{{ t('dashboard.Joined us') }}
+
+
+
+
+
+
+
码上生花
+
12分钟前{{ t('dashboard.Joined us') }}
+
+
+
+
+
+
+
Admin
+
12分钟前{{ t('dashboard.Joined us') }}
+
+
+
+
+
+
+
纯属虚构
+
12分钟前{{ t('dashboard.Joined us') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/backend/login.vue b/web/src/views/backend/login.vue
new file mode 100644
index 0000000..cf07b38
--- /dev/null
+++ b/web/src/views/backend/login.vue
@@ -0,0 +1,284 @@
+
+
+
+
+
+
+
+
+ {{ item.value }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/backend/module/components/buy.vue b/web/src/views/backend/module/components/buy.vue
new file mode 100644
index 0000000..7123aeb
--- /dev/null
+++ b/web/src/views/backend/module/components/buy.vue
@@ -0,0 +1,126 @@
+
+
+
+
+
+
+
{{ t('module.Order title') }}:{{ state.buy.info.title }}
+
{{ t('module.Order No') }}:{{ state.buy.info.sn }}
+
{{ t('module.Purchase user') }}:{{ specificUserName(baAccount) }}
+
+ {{ t('module.Order price') }}:
+
+ {{ currency(state.buy.info.amount, state.buy.info.pay.money ? 1 : 0) }}
+
+ {{ t('module.Purchased, can be installed directly') }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/backend/module/components/commonDialog.vue b/web/src/views/backend/module/components/commonDialog.vue
new file mode 100644
index 0000000..f626756
--- /dev/null
+++ b/web/src/views/backend/module/components/commonDialog.vue
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/backend/module/components/commonDone.vue b/web/src/views/backend/module/components/commonDone.vue
new file mode 100644
index 0000000..a38afaa
--- /dev/null
+++ b/web/src/views/backend/module/components/commonDone.vue
@@ -0,0 +1,288 @@
+
+
+
+
+ {{ t('module.Congratulations, module installation is complete') }}
+
+ {{ t('module.Module is disabled') }}
+
+ {{ t('module.Congratulations, the code of the module is ready') }}
+
+ {{ t('module.Unknown state') }}
+
+
+
+
+
+
+ {{ t('module.Do not refresh the page!') }}
+
+ {{ t('module.New adjustment of dependency detected') }}
+
+
+ {{ t('module.This module adds new dependencies') }}
+
+ ,
+
+ {{ t('module.The built-in terminal of the system is automatically installing these dependencies, please wait~') }}
+
+ {{ t('module.View progress') }}
+
+
+ {{ t('module.Dependency installation completed~') }}
+
+
+ {{ t('module.Dependency installation fail 1') }}
+ {{ t('module.Dependency installation fail 2') }}
+ {{ t('module.Dependency installation fail 3') }}
+
+ {{ t('module.Dependency installation fail 4') }}
+
+
+
+
+
+ {{ t('module.This module does not add new dependencies') }}
+
+
{{ t('module.There is no adjustment for system dependency') }}
+
+
+
+ {{ t('module.Dependency installation fail 5') }}
+
+ {{ t('module.Dependency installation fail 6') }}
+
+ {{ t('module.Dependency installation fail 7') }}
+
+ {{ t('module.dependency-installation-fail-tips') }}
+
+
+
+
+
+ {{ t('module.please') }}
+ {{ state.common.moduleState == moduleInstallState.DISABLE ? '' : t('module.After installation 1') }}
+ {{ t('module.Manually clean up the system and browser cache') }}
+
+
+
+
+
+
+ {{ state.common.moduleState == moduleInstallState.DISABLE ? t('Complete') : t('module.End of installation') }}
+
+
+
+
+
+
+
+
diff --git a/web/src/views/backend/module/components/commonSelectVersion.vue b/web/src/views/backend/module/components/commonSelectVersion.vue
new file mode 100644
index 0000000..54fd514
--- /dev/null
+++ b/web/src/views/backend/module/components/commonSelectVersion.vue
@@ -0,0 +1,134 @@
+
+
+
+
+
+
+
+
+
+
+
+
{{ scope.row.available_system_version_text }}
+
+
+ -
+
+
+
+
+
+
+
{{ t('module.Current installed version') }}
+
{{ t('module.Insufficient system version') }}
+
+
+ {{ t('module.Click to install') }}
+
+
+
+
+
+
{{ t('module.Versions released beyond the authorization period') }}
+
{{ t('module.Renewal') }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/backend/module/components/confirmFileConflict.vue b/web/src/views/backend/module/components/confirmFileConflict.vue
new file mode 100644
index 0000000..8f7f26e
--- /dev/null
+++ b/web/src/views/backend/module/components/confirmFileConflict.vue
@@ -0,0 +1,90 @@
+
+
+
+
+ {{ $t('module.File conflict') }}
+
+
+
+
+
+
+
+ {{ $t('module.The module declares the added dependencies') }}
+
+
+
+ {{ $t('module.env ' + scope.row.env) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('module.Confirm to disable the module') }}
+
+ {{ $t('Cancel') }}
+
+
+
+
+
+
+
diff --git a/web/src/views/backend/module/components/goodsInfo.vue b/web/src/views/backend/module/components/goodsInfo.vue
new file mode 100644
index 0000000..d483d27
--- /dev/null
+++ b/web/src/views/backend/module/components/goodsInfo.vue
@@ -0,0 +1,610 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ state.goodsInfo.title }}
+
+
+ {{ tag.name }}
+
+
+
+
{{ t('module.Price') }}
+
+ {{
+ typeof state.goodsInfo.currency_select != 'undefined'
+ ? currency(state.goodsInfo.present_price, state.goodsInfo.currency_select)
+ : '-'
+ }}
+
+
+
+
{{ t('module.Last updated') }}
+
{{ state.goodsInfo.updatetime ? timeFormat(state.goodsInfo.updatetime) : '-' }}
+
+
+
{{ t('module.Published on') }}
+
{{ state.goodsInfo.createtime ? timeFormat(state.goodsInfo.createtime) : '-' }}
+
+
+
{{ t('module.amount of downloads') }}
+
{{ state.goodsInfo.downloads ? state.goodsInfo.downloads : '-' }}
+
+
+
{{ t('module.Module classification') }}
+
{{ state.goodsInfo.category ? state.goodsInfo.category.name : '-' }}
+
+
+
{{ t('module.Module documentation') }}
+
+
+ {{ t('module.Click to access') }}
+
+ -
+
+
+
+
{{ t('module.Developer Homepage') }}
+
+
+ {{ t('module.Click to access') }}
+
+ -
+
+
+
+
{{ t('module.Module status') }}
+
+
+
+
+
+
+
+
+
+
+
{{ t('module.Other works of developers') }}
+
+
+
+
{{ goods_item.title }}
+
+
+
{{ t('module.There are no more works') }}
+
+
+
+
+
+
{{ t('module.Update Log') }}
+
+
{{ $t('module.No detailed update log') }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/backend/module/components/installConflict.vue b/web/src/views/backend/module/components/installConflict.vue
new file mode 100644
index 0000000..72f8628
--- /dev/null
+++ b/web/src/views/backend/module/components/installConflict.vue
@@ -0,0 +1,93 @@
+
+
+
+
+ {{ $t('module.File conflict') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('module.Dependency conflict') }}
+
+
+
+ {{ $t('module.env ' + scope.row.env) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('Confirm') }}
+
+
+
+
+
+
+
diff --git a/web/src/views/backend/module/components/pay.vue b/web/src/views/backend/module/components/pay.vue
new file mode 100644
index 0000000..9dc386a
--- /dev/null
+++ b/web/src/views/backend/module/components/pay.vue
@@ -0,0 +1,150 @@
+
+
+
+
+
+
+
+
+
{{ t('module.Order title') }}:{{ state.payInfo.info.title }}
+
{{ t('module.Order No') }}:{{ state.payInfo.info.sn }}
+
{{ t('module.Purchase user') }}:{{ specificUserName(baAccount) }}
+
+ {{ t('module.Order price') }}:
+
+ ¥{{ state.payInfo.info.amount }}
+
+
+
+
+
+
+
+ {{ t('module.Use WeChat to scan QR code for payment') }}
+ {{ t('module.Use Alipay to scan QR code for payment') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/backend/module/components/tableHeader.vue b/web/src/views/backend/module/components/tableHeader.vue
new file mode 100644
index 0000000..8fdc46a
--- /dev/null
+++ b/web/src/views/backend/module/components/tableHeader.vue
@@ -0,0 +1,125 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/backend/module/components/tabs.vue b/web/src/views/backend/module/components/tabs.vue
new file mode 100644
index 0000000..bc4694d
--- /dev/null
+++ b/web/src/views/backend/module/components/tabs.vue
@@ -0,0 +1,168 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/backend/module/components/uploadInstall.vue b/web/src/views/backend/module/components/uploadInstall.vue
new file mode 100644
index 0000000..67a4513
--- /dev/null
+++ b/web/src/views/backend/module/components/uploadInstall.vue
@@ -0,0 +1,71 @@
+
+
+
+
{{ $t('module.Local upload warning') }}
+
1. {{ $t('module.The module can modify and add system files') }}
+
2. {{ $t('module.The module can execute sql commands and codes') }}
+
3. {{ $t('module.The module can install new front and rear dependencies') }}
+
+
+
+
+
+ {{ $t('module.Drag the module package file here') }} {{ $t('module.Click me to upload') }}
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/backend/module/index.ts b/web/src/views/backend/module/index.ts
new file mode 100644
index 0000000..9ff0d07
--- /dev/null
+++ b/web/src/views/backend/module/index.ts
@@ -0,0 +1,604 @@
+import { ElNotification } from 'element-plus'
+import { isArray } from 'lodash-es'
+import { state } from './store'
+import { moduleInstallState, type moduleState } from './types'
+import {
+ changeState,
+ createOrder,
+ getInstallState,
+ index,
+ info,
+ modules,
+ payCheck,
+ payOrder,
+ postInstallModule,
+ preDownload,
+} from '/@/api/backend/module'
+import { i18n } from '/@/lang/index'
+import router from '/@/router/index'
+import { useBaAccount } from '/@/stores/baAccount'
+import { SYSTEM_ZINDEX } from '/@/stores/constant/common'
+import { taskStatus } from '/@/stores/constant/terminalTaskStatus'
+import type { UserInfo } from '/@/stores/interface'
+import { useTerminal } from '/@/stores/terminal'
+import { fullUrl } from '/@/utils/common'
+import { uuid } from '/@/utils/random'
+import { changeListenDirtyFileSwitch, closeHotUpdate } from '/@/utils/vite'
+
+export const loadData = () => {
+ state.loading.table = true
+ if (!state.table.indexLoaded) {
+ loadIndex().then(() => {
+ getModules()
+ })
+ } else {
+ getModules()
+ }
+}
+
+export const onRefreshTableData = () => {
+ state.table.indexLoaded = false
+ for (const key in state.table.modulesEbak) {
+ state.table.modulesEbak[key] = undefined
+ }
+ loadData()
+}
+
+const loadIndex = () => {
+ return index().then((res) => {
+ state.table.indexLoaded = true
+ state.sysVersion = res.data.sysVersion
+ state.nuxtVersion = res.data.nuxtVersion
+ state.installedModule = res.data.installed
+
+ const installedModuleUids: string[] = []
+ const installedModuleVersions: { uid: string; version: string }[] = []
+ if (res.data.installed) {
+ state.installedModule.forEach((item) => {
+ installedModuleUids.push(item.uid)
+ installedModuleVersions.push({
+ uid: item.uid,
+ version: item.version,
+ })
+ })
+ state.installedModuleUids = installedModuleUids
+ state.installedModuleVersions = installedModuleVersions
+ }
+ })
+}
+
+const getModules = () => {
+ if (typeof state.table.modulesEbak[state.table.params.activeTab] != 'undefined') {
+ state.table.modules[state.table.params.activeTab] = modulesOnlyLocalHandle(state.table.modulesEbak[state.table.params.activeTab])
+ state.loading.table = false
+ return
+ }
+ const params: anyObj = {}
+ for (const key in state.table.params) {
+ if (state.table.params[key] != '') {
+ params[key] = state.table.params[key]
+ }
+ }
+ const moduleUids: string[] = []
+ params['installed'] = state.installedModuleVersions
+ params['sysVersion'] = state.sysVersion
+ modules(params)
+ .then((res) => {
+ if (params.activeTab == 'all') {
+ res.data.rows.forEach((item: anyObj) => {
+ moduleUids.push(item.uid)
+ })
+
+ state.installedModule.forEach((item) => {
+ if (moduleUids.indexOf(item.uid) === -1) {
+ if (state.table.params.quickSearch) {
+ if (item.title.includes(state.table.params.quickSearch)) res.data.rows.push(item)
+ } else {
+ res.data.rows.push(item)
+ }
+ }
+ })
+ }
+
+ state.table.remark = res.data.remark
+ state.table.modulesEbak[params.activeTab] = res.data.rows.map((item: anyObj) => {
+ const idx = state.installedModuleUids.indexOf(item.uid)
+ if (idx !== -1) {
+ item.state = state.installedModule[idx].state
+ item.title = state.installedModule[idx].title
+ item.version = state.installedModule[idx].version
+ item.website = state.installedModule[idx].website
+ item.stateTag = moduleStatus(item.state)
+
+ if (!isArray(item.tags)) item.tags = []
+ item.tags.push({
+ name: `${i18n.global.t('module.installed')} v${state.installedModule[idx].version}`,
+ type: 'primary',
+ })
+ } else {
+ item.state = 0
+ }
+
+ if (item.new_version && item.tags) {
+ item.tags.push({
+ name: i18n.global.t('module.New version'),
+ type: 'danger',
+ })
+ }
+
+ return item
+ })
+ state.table.modules[params.activeTab] = modulesOnlyLocalHandle(state.table.modulesEbak[params.activeTab])
+ state.table.category = res.data.category
+ })
+ .finally(() => {
+ state.loading.table = false
+ })
+}
+
+export const showInfo = (uid: string) => {
+ state.dialog.goodsInfo = true
+ state.loading.goodsInfo = true
+
+ const localItem = state.installedModule.find((item) => {
+ return item.uid == uid
+ })
+
+ info({
+ uid: uid,
+ localVersion: localItem?.version,
+ sysVersion: state.sysVersion,
+ })
+ .then((res) => {
+ if (localItem) {
+ if (res.data.info.type == 'local') {
+ res.data.info = localItem
+ res.data.info.images = [fullUrl('/static/images/local-module-logo.png')]
+ res.data.info.type = 'local' // 纯本地模块
+ } else {
+ res.data.info.type = 'online'
+ res.data.info.state = localItem.state
+ res.data.info.version = localItem.version
+ }
+ res.data.info.enable = localItem.state === moduleInstallState.DISABLE ? false : true
+ } else {
+ res.data.info.state = 0
+ res.data.info.type = 'online'
+ }
+ state.goodsInfo = res.data.info
+ })
+ .catch((err) => {
+ if (loginExpired(err)) {
+ state.dialog.goodsInfo = false
+ }
+ })
+ .finally(() => {
+ state.loading.goodsInfo = false
+ })
+}
+
+/**
+ * 支付订单
+ * @param renew 是否是续费订单
+ */
+export const onBuy = (renew = false) => {
+ state.dialog.buy = true
+ state.loading.buy = true
+ createOrder({
+ goods_id: state.goodsInfo.id,
+ })
+ .then((res) => {
+ state.loading.buy = false
+ state.buy.renew = renew
+ state.buy.info = res.data.info
+ })
+ .catch((err) => {
+ state.dialog.buy = false
+ state.loading.buy = false
+ loginExpired(err)
+ })
+}
+
+export const onPay = (payType: 'score' | 'wx' | 'balance' | 'zfb') => {
+ state.common.payType = payType
+ state.loading.common = true
+ payOrder(state.buy.info.id, payType)
+ .then((res) => {
+ // 关闭其他弹窗
+ state.dialog.buy = false
+ state.dialog.goodsInfo = false
+
+ if (payType == 'wx' || payType == 'zfb') {
+ // 显示支付二维码
+ state.dialog.pay = true
+ state.payInfo = res.data
+
+ // 轮询获取支付状态
+ const timer = setInterval(() => {
+ payCheck(state.payInfo.info.sn)
+ .then(() => {
+ state.payInfo.pay.status = 'success'
+ clearInterval(timer)
+ if (state.buy.renew) {
+ showInfo(res.data.info.uid)
+ } else {
+ onPreInstallModule(res.data.info.uid, res.data.info.id, true)
+ }
+ state.dialog.pay = false
+ })
+ .catch(() => {})
+ }, 3000)
+ } else {
+ if (state.buy.renew) {
+ showInfo(res.data.info.uid)
+ } else {
+ onPreInstallModule(res.data.info.uid, res.data.info.id, true)
+ }
+ }
+ })
+ .catch((err) => {
+ loginExpired(err)
+ })
+ .finally(() => {
+ state.loading.common = false
+ })
+}
+
+export const showCommonLoading = (loadingTitle: moduleState['common']['loadingTitle']) => {
+ state.common.type = 'loading'
+ state.common.loadingTitle = loadingTitle
+ state.common.loadingComponentKey = uuid()
+}
+
+/**
+ * 模块预安装
+ */
+export const onPreInstallModule = (uid: string, id: number, needGetInstallableVersion: boolean, update: boolean = false) => {
+ state.dialog.common = true
+ showCommonLoading('init')
+ state.common.dialogTitle = i18n.global.t('module.Install')
+
+ const nextStep = (moduleState: number) => {
+ if (needGetInstallableVersion) {
+ // 获取模块版本列表
+ showCommonLoading('getInstallableVersion')
+ preDownload({
+ uid,
+ orderId: id,
+ sysVersion: state.sysVersion,
+ nuxtVersion: state.nuxtVersion,
+ installed: state.installedModuleUids,
+ })
+ .then((res) => {
+ state.common.uid = uid
+ state.common.update = update
+ state.common.type = 'selectVersion'
+ state.common.dialogTitle = i18n.global.t('module.Select Version')
+ state.common.versions = res.data.versions
+
+ // 关闭其他弹窗
+ state.dialog.baAccount = false
+ state.dialog.buy = false
+ state.dialog.goodsInfo = false
+ })
+ .catch((res) => {
+ if (loginExpired(res)) return
+ state.dialog.common = false
+ })
+ } else {
+ // 立即安装(上传安装、继续安装)
+ showCommonLoading(moduleState === moduleInstallState.UNINSTALLED ? 'download' : 'install')
+ execInstall(uid, id, '', update)
+
+ // 关闭其他弹窗
+ state.dialog.baAccount = false
+ state.dialog.buy = false
+ state.dialog.goodsInfo = false
+ }
+ }
+
+ if (update) {
+ nextStep(moduleInstallState.DISABLE)
+ } else {
+ // 获取安装状态
+ getInstallState(uid).then((res) => {
+ if (
+ res.data.state === moduleInstallState.INSTALLED ||
+ res.data.state === moduleInstallState.DISABLE ||
+ res.data.state === moduleInstallState.DIRECTORY_OCCUPIED
+ ) {
+ ElNotification({
+ type: 'error',
+ message:
+ res.data.state === moduleInstallState.INSTALLED || res.data.state === moduleInstallState.DISABLE
+ ? i18n.global.t('module.Installation cancelled because module already exists!')
+ : i18n.global.t('module.Installation cancelled because the directory required by the module is occupied!'),
+ })
+ state.dialog.common = false
+ return
+ }
+
+ nextStep(res.data.state)
+ })
+ }
+}
+
+/**
+ * 执行安装请求,还包含启用、安装时的冲突处理
+ */
+export const execInstall = (uid: string, id: number, version: string = '', update: boolean = false, extend: anyObj = {}) => {
+ postInstallModule(uid, id, version, update, extend)
+ .then(() => {
+ state.common.dialogTitle = i18n.global.t('module.Installation complete')
+ state.common.moduleState = moduleInstallState.INSTALLED
+ state.common.type = 'done'
+ onRefreshTableData()
+ })
+ .catch((res) => {
+ if (loginExpired(res)) return
+ if (res.code == -1) {
+ state.common.uid = res.data.uid
+ state.common.type = 'installConflict'
+ state.common.dialogTitle = i18n.global.t('module.A conflict is found Please handle it manually')
+ state.common.fileConflict = res.data.fileConflict
+ state.common.dependConflict = res.data.dependConflict
+ } else if (res.code == -2) {
+ state.common.type = 'done'
+ state.common.uid = res.data.uid
+ state.common.dialogTitle = i18n.global.t('module.Wait for dependent installation')
+ state.common.moduleState = moduleInstallState.DEPENDENT_WAIT_INSTALL
+ state.common.waitInstallDepend = res.data.wait_install
+ state.common.dependInstallState = 'executing'
+ const terminal = useTerminal()
+ if (res.data.wait_install.includes('npm_dependent_wait_install')) {
+ terminal.addTaskPM('web-install', true, 'module-install:' + res.data.uid, (res: number) => {
+ terminalTaskExecComplete(res, 'npm_dependent_wait_install')
+ })
+ }
+ if (res.data.wait_install.includes('nuxt_npm_dependent_wait_install')) {
+ terminal.addTaskPM('nuxt-install', true, 'module-install:' + res.data.uid, (res: number) => {
+ terminalTaskExecComplete(res, 'nuxt_npm_dependent_wait_install')
+ })
+ }
+ if (res.data.wait_install.includes('composer_dependent_wait_install')) {
+ terminal.addTask('composer.update', true, 'module-install:' + res.data.uid, (res: number) => {
+ terminalTaskExecComplete(res, 'composer_dependent_wait_install')
+ })
+ }
+ } else if (res.code == 0) {
+ ElNotification({
+ type: 'error',
+ message: res.msg,
+ zIndex: SYSTEM_ZINDEX,
+ })
+ state.dialog.common = false
+ onRefreshTableData()
+ }
+ })
+ .finally(() => {
+ state.loading.common = false
+ })
+}
+
+const terminalTaskExecComplete = (res: number, type: string) => {
+ if (res == taskStatus.Success) {
+ state.common.waitInstallDepend = state.common.waitInstallDepend.filter((depend: string) => {
+ return depend != type
+ })
+ if (state.common.waitInstallDepend.length == 0) {
+ state.common.dependInstallState = 'success'
+
+ // 仅在命令全部执行完毕才刷新数据
+ if (router.currentRoute.value.name === 'moduleStore/moduleStore') {
+ onRefreshTableData()
+ }
+ }
+ } else {
+ const terminal = useTerminal()
+ terminal.toggle(true)
+ state.common.dependInstallState = 'fail'
+
+ // 有命令执行失败了,刷新一次数据
+ if (router.currentRoute.value.name === 'moduleStore/moduleStore') {
+ onRefreshTableData()
+ }
+ }
+
+ // 连续安装模块的情况中,首个模块的命令执行完毕时,自动启动了热更新
+ if (router.currentRoute.value.name === 'moduleStore/moduleStore') {
+ closeHotUpdate('modules')
+ }
+}
+
+export const onDisable = (confirmConflict = false) => {
+ state.loading.common = true
+
+ // 拼装依赖处理方案
+ if (confirmConflict) {
+ const dependConflict: anyObj = {}
+ for (const key in state.common.disableDependConflict) {
+ if (state.common.disableDependConflict[key]['solution'] != 'delete') {
+ continue
+ }
+ if (typeof dependConflict[state.common.disableDependConflict[key].env] == 'undefined') {
+ dependConflict[state.common.disableDependConflict[key].env] = []
+ }
+ dependConflict[state.common.disableDependConflict[key].env].push(state.common.disableDependConflict[key].depend)
+ }
+ state.common.disableParams['confirmConflict'] = 1
+ state.common.disableParams['dependConflictSolution'] = dependConflict
+ }
+
+ changeState(state.common.disableParams)
+ .then(() => {
+ ElNotification({
+ type: 'success',
+ message: i18n.global.t('module.The operation succeeds Please clear the system cache and refresh the browser ~'),
+ zIndex: SYSTEM_ZINDEX,
+ })
+ state.dialog.common = false
+ onRefreshTableData()
+ })
+ .catch((res) => {
+ if (res.code == -1) {
+ state.dialog.common = true
+ state.common.dialogTitle = i18n.global.t('module.Deal with conflict')
+ state.common.type = 'disableConfirmConflict'
+ state.common.disableDependConflict = res.data.dependConflict
+ if (res.data.conflictFile && res.data.conflictFile.length) {
+ const conflictFile = []
+ for (const key in res.data.conflictFile) {
+ conflictFile.push({
+ file: res.data.conflictFile[key],
+ })
+ }
+ state.common.disableConflictFile = conflictFile
+ }
+ } else if (res.code == -2) {
+ state.dialog.common = true
+ const commandsData = {
+ type: 'disable',
+ commands: res.data.wait_install,
+ }
+ state.common.uid = state.goodsInfo.uid
+ execCommand(commandsData)
+ } else if (res.code == -3) {
+ // 更新
+ onPreInstallModule(state.goodsInfo.uid, state.goodsInfo.purchased, true, true)
+ } else {
+ ElNotification({
+ type: 'error',
+ message: res.msg,
+ zIndex: SYSTEM_ZINDEX,
+ })
+ if (state.common.disableParams && state.common.disableParams.uid) {
+ showInfo(state.common.disableParams.uid)
+ } else {
+ onRefreshTableData()
+ }
+ }
+ })
+ .finally(() => {
+ state.loading.common = false
+ })
+}
+
+export const onEnable = (uid: string) => {
+ state.loading.common = true
+ changeState({
+ uid: uid,
+ state: 1,
+ })
+ .then(() => {
+ state.dialog.common = true
+ showCommonLoading('init')
+ state.common.dialogTitle = i18n.global.t('Enable')
+
+ execInstall(uid, 0)
+ state.dialog.goodsInfo = false
+ })
+ .catch((res) => {
+ ElNotification({
+ type: 'error',
+ message: res.msg,
+ zIndex: SYSTEM_ZINDEX,
+ })
+ state.loading.common = false
+ })
+}
+
+export const loginExpired = (res: ApiResponse) => {
+ const baAccount = useBaAccount()
+ if (res.code == 301 || res.code == 408) {
+ baAccount.removeToken()
+ state.dialog.baAccount = true
+ return true
+ }
+ return false
+}
+
+const modulesOnlyLocalHandle = (modules: anyObj) => {
+ if (!state.table.onlyLocal) return modules
+ return modules.filter((item: anyObj) => {
+ return item.installed
+ })
+}
+
+export const execCommand = (data: anyObj) => {
+ if (data.type == 'disable') {
+ state.dialog.common = true
+ state.common.type = 'done'
+ state.common.dialogTitle = i18n.global.t('module.Wait for dependent installation')
+ state.common.moduleState = moduleInstallState.DISABLE
+ state.common.dependInstallState = 'executing'
+ const terminal = useTerminal()
+ data.commands.forEach((item: anyObj) => {
+ state.common.waitInstallDepend.push(item.type)
+ if (item.pm) {
+ if (item.command == 'web-install') {
+ changeListenDirtyFileSwitch(false)
+ }
+ terminal.addTaskPM(item.command, true, '', (res: number) => {
+ terminalTaskExecComplete(res, item.type)
+ if (item.command == 'web-install') {
+ changeListenDirtyFileSwitch(true)
+ }
+ })
+ } else {
+ terminal.addTask(item.command, true, '', (res: number) => {
+ terminalTaskExecComplete(res, item.type)
+ })
+ }
+ })
+ }
+}
+
+export const specificUserName = (userInfo: Partial) => {
+ return userInfo.nickname + '(' + (userInfo.email || userInfo.mobile || 'ID:' + userInfo.id) + ')'
+}
+
+export const currency = (price: number, val: number) => {
+ if (typeof price == 'undefined' || typeof val == 'undefined') {
+ return '-'
+ }
+ if (val == 0) {
+ return parseInt(price.toString()) + i18n.global.t('Integral')
+ } else {
+ return '¥' + price
+ }
+}
+
+export const moduleStatus = (state: number) => {
+ switch (state) {
+ case moduleInstallState.INSTALLED:
+ return {
+ type: '',
+ text: i18n.global.t('module.installed'),
+ }
+ case moduleInstallState.WAIT_INSTALL:
+ return {
+ type: 'success',
+ text: i18n.global.t('module.Wait for installation'),
+ }
+ case moduleInstallState.CONFLICT_PENDING:
+ return {
+ type: 'danger',
+ text: i18n.global.t('module.Conflict pending'),
+ }
+ case moduleInstallState.DEPENDENT_WAIT_INSTALL:
+ return {
+ type: 'warning',
+ text: i18n.global.t('module.Dependency to be installed'),
+ }
+ case moduleInstallState.DISABLE:
+ return {
+ type: 'warning',
+ text: i18n.global.t('Disable'),
+ }
+ default:
+ return {
+ type: 'info',
+ text: i18n.global.t('Unknown'),
+ }
+ }
+}
diff --git a/web/src/views/backend/module/index.vue b/web/src/views/backend/module/index.vue
new file mode 100644
index 0000000..fde6ad7
--- /dev/null
+++ b/web/src/views/backend/module/index.vue
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
diff --git a/web/src/views/backend/module/store.ts b/web/src/views/backend/module/store.ts
new file mode 100644
index 0000000..4215a92
--- /dev/null
+++ b/web/src/views/backend/module/store.ts
@@ -0,0 +1,63 @@
+import { reactive } from 'vue'
+import { uuid } from '/@/utils/random'
+import type { moduleState } from './types'
+
+export const state = reactive({
+ loading: {
+ buy: false,
+ table: true,
+ common: false,
+ install: false,
+ goodsInfo: false,
+ },
+ dialog: {
+ buy: false,
+ pay: false,
+ common: false,
+ goodsInfo: false,
+ baAccount: false,
+ },
+ table: {
+ remark: '',
+ modules: [],
+ modulesEbak: [],
+ category: [],
+ onlyLocal: false,
+ indexLoaded: false,
+ params: {
+ quickSearch: '',
+ activeTab: 'all',
+ },
+ },
+ payInfo: {},
+ goodsInfo: {},
+ buy: {
+ info: {},
+ renew: false,
+ agreement: true,
+ },
+ common: {
+ uid: '',
+ moduleState: 0,
+ quickClose: false,
+ type: 'loading',
+ dialogTitle: '',
+ fileConflict: [],
+ dependConflict: [],
+ loadingTitle: 'init',
+ loadingComponentKey: uuid(),
+ waitInstallDepend: [],
+ dependInstallState: 'none',
+ disableConflictFile: [],
+ disableDependConflict: [],
+ disableParams: {},
+ payType: 'wx',
+ update: false,
+ versions: [],
+ },
+ sysVersion: '',
+ nuxtVersion: '',
+ installedModule: [],
+ installedModuleUids: [],
+ installedModuleVersions: [],
+})
diff --git a/web/src/views/backend/module/types.ts b/web/src/views/backend/module/types.ts
new file mode 100644
index 0000000..d32a4c1
--- /dev/null
+++ b/web/src/views/backend/module/types.ts
@@ -0,0 +1,78 @@
+export enum moduleInstallState {
+ UNINSTALLED,
+ INSTALLED,
+ WAIT_INSTALL,
+ CONFLICT_PENDING,
+ DEPENDENT_WAIT_INSTALL,
+ DIRECTORY_OCCUPIED,
+ DISABLE,
+}
+
+export interface moduleInfo {
+ uid: string
+ title: string
+ version: string
+ state: number
+ website: string
+ stateTag: {
+ type: string
+ text: string
+ }
+}
+
+export interface moduleState {
+ loading: {
+ buy: boolean
+ table: boolean
+ common: boolean
+ install: boolean
+ goodsInfo: boolean
+ }
+ dialog: {
+ buy: boolean
+ pay: boolean
+ common: boolean
+ goodsInfo: boolean
+ baAccount: boolean
+ }
+ table: {
+ remark: string
+ modules: anyObj
+ modulesEbak: anyObj
+ category: anyObj
+ onlyLocal: boolean
+ indexLoaded: boolean
+ params: anyObj
+ }
+ payInfo: anyObj
+ goodsInfo: anyObj
+ buy: {
+ info: anyObj
+ renew: boolean
+ agreement: boolean
+ }
+ common: {
+ uid: string
+ moduleState: number
+ quickClose: boolean
+ type: 'loading' | 'installConflict' | 'done' | 'disableConfirmConflict' | 'uploadInstall' | 'selectVersion'
+ dialogTitle: string
+ fileConflict: anyObj[]
+ dependConflict: anyObj[]
+ loadingTitle: 'init' | 'download' | 'install' | 'getInstallableVersion'
+ loadingComponentKey: string
+ waitInstallDepend: string[]
+ dependInstallState: 'none' | 'executing' | 'success' | 'fail'
+ disableConflictFile: { file: string }[]
+ disableDependConflict: anyObj[]
+ disableParams: anyObj
+ payType: 'score' | 'wx' | 'balance' | 'zfb'
+ update: boolean
+ versions: anyObj[]
+ }
+ sysVersion: string
+ nuxtVersion: string
+ installedModule: moduleInfo[]
+ installedModuleUids: string[]
+ installedModuleVersions: { uid: string; version: string }[]
+}
diff --git a/web/src/views/backend/routine/adminInfo.vue b/web/src/views/backend/routine/adminInfo.vue
new file mode 100644
index 0000000..3d97ece
--- /dev/null
+++ b/web/src/views/backend/routine/adminInfo.vue
@@ -0,0 +1,297 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ state.adminInfo.nickname }}
+
+
{{ t('routine.adminInfo.Last logged in on') }} {{ timeFormat(state.adminInfo.last_login_time) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('routine.adminInfo.Save changes') }}
+
+ {{ t('Reset') }}
+
+
+
+
+
+
+
+
+
+ {{ item.title }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/backend/routine/attachment/index.ts b/web/src/views/backend/routine/attachment/index.ts
new file mode 100644
index 0000000..92e18be
--- /dev/null
+++ b/web/src/views/backend/routine/attachment/index.ts
@@ -0,0 +1,12 @@
+import { buildSuffixSvgUrl } from '/@/api/common'
+
+/**
+ * 表格和表单中文件预览图的生成
+ */
+export const previewRenderFormatter = (row: TableRow, column: TableColumn, cellValue: string) => {
+ const imgSuffix = ['gif', 'jpg', 'jpeg', 'bmp', 'png', 'webp']
+ if (imgSuffix.includes(cellValue)) {
+ return row.full_url
+ }
+ return buildSuffixSvgUrl(cellValue)
+}
diff --git a/web/src/views/backend/routine/attachment/index.vue b/web/src/views/backend/routine/attachment/index.vue
new file mode 100644
index 0000000..e279069
--- /dev/null
+++ b/web/src/views/backend/routine/attachment/index.vue
@@ -0,0 +1,184 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/backend/routine/attachment/popupForm.vue b/web/src/views/backend/routine/attachment/popupForm.vue
new file mode 100644
index 0000000..6fe3ba6
--- /dev/null
+++ b/web/src/views/backend/routine/attachment/popupForm.vue
@@ -0,0 +1,150 @@
+
+
+
+
+
+ {{ baTable.form.operate ? t(baTable.form.operate) : '' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{
+ t(
+ 'routine.attachment.When the same file is uploaded multiple times, only one attachment record will be saved and added'
+ )
+ }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('Cancel') }}
+
+ {{ baTable.form.operateIds!.length > 1 ? t('Save and edit next item') : t('Save') }}
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/backend/routine/config/add.vue b/web/src/views/backend/routine/config/add.vue
new file mode 100644
index 0000000..bef6f21
--- /dev/null
+++ b/web/src/views/backend/routine/config/add.vue
@@ -0,0 +1,136 @@
+
+
+
+
+ {{ t('routine.config.Add configuration item') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('Cancel') }}
+ {{ t('Add') }}
+
+
+
+
+
+
+
+
diff --git a/web/src/views/backend/routine/config/index.vue b/web/src/views/backend/routine/config/index.vue
new file mode 100644
index 0000000..f3452f3
--- /dev/null
+++ b/web/src/views/backend/routine/config/index.vue
@@ -0,0 +1,369 @@
+
+
+
+
+
+
+
+
+
+ {{ t('routine.config.Test mail sending') }}
+
+ {{ t('Save') }}
+
+
+
+
+
+
+
+
+ {{ item['key'] }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/backend/security/dataRecycle/index.vue b/web/src/views/backend/security/dataRecycle/index.vue
new file mode 100644
index 0000000..6ff57e4
--- /dev/null
+++ b/web/src/views/backend/security/dataRecycle/index.vue
@@ -0,0 +1,122 @@
+
+
+
+
+
+
+
diff --git a/web/src/views/backend/security/dataRecycle/popupForm.vue b/web/src/views/backend/security/dataRecycle/popupForm.vue
new file mode 100644
index 0000000..2f32c24
--- /dev/null
+++ b/web/src/views/backend/security/dataRecycle/popupForm.vue
@@ -0,0 +1,152 @@
+
+
+
+
+
+ {{ baTable.form.operate ? t(baTable.form.operate) : '' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('Cancel') }}
+
+ {{ baTable.form.operateIds && baTable.form.operateIds.length > 1 ? t('Save and edit next item') : t('Save') }}
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/backend/security/dataRecycleLog/index.vue b/web/src/views/backend/security/dataRecycleLog/index.vue
new file mode 100644
index 0000000..99dd328
--- /dev/null
+++ b/web/src/views/backend/security/dataRecycleLog/index.vue
@@ -0,0 +1,216 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/backend/security/dataRecycleLog/info.vue b/web/src/views/backend/security/dataRecycleLog/info.vue
new file mode 100644
index 0000000..2bfab75
--- /dev/null
+++ b/web/src/views/backend/security/dataRecycleLog/info.vue
@@ -0,0 +1,93 @@
+
+
+
+ {{ t('Info') }}
+
+
+
+
+
+ {{ baTable.form.extend!.info.id }}
+
+
+ {{ baTable.form.extend!.info.admin?.nickname + '(' + baTable.form.extend!.info.admin?.username + ')' }}
+
+
+ {{ baTable.form.extend!.info.recycle?.name }}
+
+
+ {{ baTable.form.extend!.info.connection }}
+
+
+ {{ baTable.form.extend!.info.data_table }}
+
+
+ {{ baTable.form.extend!.info.primary_key }}
+
+
+ {{ baTable.form.extend!.info.ip }}
+
+
+ {{ timeFormat(baTable.form.extend!.info.create_time) }}
+
+
+ {{ baTable.form.extend!.info.useragent }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/backend/security/sensitiveData/index.ts b/web/src/views/backend/security/sensitiveData/index.ts
new file mode 100644
index 0000000..9cab58a
--- /dev/null
+++ b/web/src/views/backend/security/sensitiveData/index.ts
@@ -0,0 +1,113 @@
+import baTableClass from '/@/utils/baTable'
+import type { baTableApi } from '/@/api/common'
+import { getTableFieldList } from '/@/api/common'
+import { add } from '/@/api/backend/security/sensitiveData'
+import { uuid } from '/@/utils/random'
+
+export interface DataFields {
+ name: string
+ value: string
+}
+
+export class sensitiveDataClass extends baTableClass {
+ constructor(api: baTableApi, table: BaTable, form: BaTableForm = {}, before: BaTableBefore = {}, after: BaTableAfter = {}) {
+ super(api, table, form, before, after)
+ }
+
+ // 重写编辑
+ getEditData = (id: string) => {
+ this.form.loading = true
+ this.form.items = {}
+ return this.api.edit({ id: id }).then((res) => {
+ const fields: string[] = []
+ const dataFields: DataFields[] = []
+ for (const key in res.data.row.data_fields) {
+ fields.push(key)
+ dataFields.push({
+ name: key,
+ value: res.data.row.data_fields[key] ?? '',
+ })
+ }
+
+ this.form.items!.connection = res.data.row.connection ? res.data.row.connection : ''
+ this.form.extend!.controllerList = res.data.controllers
+
+ if (res.data.row.data_table) {
+ this.onTableChange(res.data.row.data_table)
+ if (this.form.extend!.parentRef) this.form.extend!.parentRef.setDataFields(dataFields)
+ }
+
+ res.data.row.data_fields = fields
+ this.form.loading = false
+ this.form.items = res.data.row
+ })
+ }
+
+ onConnectionChange = () => {
+ this.form.extend!.fieldList = {}
+ this.form.extend!.fieldSelect = {}
+ this.form.extend!.fieldSelectKey = uuid()
+
+ this.form.items!.data_table = ''
+ this.form.items!.data_fields = []
+ if (this.form.extend!.parentRef) this.form.extend!.parentRef.setDataFields([])
+ }
+
+ // 数据表改变事件
+ onTableChange = (table: string) => {
+ this.form.extend = Object.assign(this.form.extend!, {
+ fieldLoading: true,
+ fieldList: {},
+ fieldSelect: {},
+ fieldSelectKey: uuid(),
+ })
+
+ this.form.items!.data_fields = []
+ if (this.form.extend!.parentRef) this.form.extend!.parentRef.setDataFields([])
+
+ getTableFieldList(table, true, this.form.items!.connection).then((res) => {
+ this.form.items!.primary_key = res.data.pk
+ this.form.defaultItems!.primary_key = res.data.pk
+
+ const fieldSelect: anyObj = {}
+ for (const key in res.data.fieldList) {
+ fieldSelect[key] = (key ? key + ' - ' : '') + res.data.fieldList[key]
+ }
+
+ this.form.extend = Object.assign(this.form.extend!, {
+ fieldLoading: false,
+ fieldList: res.data.fieldList,
+ fieldSelect: fieldSelect,
+ fieldSelectKey: uuid(),
+ })
+ })
+ }
+
+ /**
+ * 重写打开表单方法
+ */
+ toggleForm = (operate = '', operateIds: string[] = []) => {
+ if (this.form.ref) {
+ this.form.ref.resetFields()
+ }
+
+ if (this.form.extend!.parentRef) this.form.extend!.parentRef.setDataFields([])
+
+ if (operate == 'Edit') {
+ if (!operateIds.length) {
+ return false
+ }
+ this.getEditData(operateIds[0])
+ } else if (operate == 'Add') {
+ this.form.loading = true
+ add().then((res) => {
+ this.form.extend!.controllerList = res.data.controllers
+ this.form.items = Object.assign({}, this.form.defaultItems)
+ this.form.loading = false
+ })
+ }
+
+ this.form.operate = operate
+ this.form.operateIds = operateIds
+ }
+}
diff --git a/web/src/views/backend/security/sensitiveData/index.vue b/web/src/views/backend/security/sensitiveData/index.vue
new file mode 100644
index 0000000..ed102ca
--- /dev/null
+++ b/web/src/views/backend/security/sensitiveData/index.vue
@@ -0,0 +1,125 @@
+
+
+
+
+
+
+
diff --git a/web/src/views/backend/security/sensitiveData/popupForm.vue b/web/src/views/backend/security/sensitiveData/popupForm.vue
new file mode 100644
index 0000000..82fbd22
--- /dev/null
+++ b/web/src/views/backend/security/sensitiveData/popupForm.vue
@@ -0,0 +1,229 @@
+
+
+
+
+
+ {{ baTable.form.operate ? t(baTable.form.operate) : '' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('Cancel') }}
+
+ {{ baTable.form.operateIds && baTable.form.operateIds.length > 1 ? t('Save and edit next item') : t('Save') }}
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/backend/security/sensitiveDataLog/index.vue b/web/src/views/backend/security/sensitiveDataLog/index.vue
new file mode 100644
index 0000000..83d3d64
--- /dev/null
+++ b/web/src/views/backend/security/sensitiveDataLog/index.vue
@@ -0,0 +1,229 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/backend/security/sensitiveDataLog/info.vue b/web/src/views/backend/security/sensitiveDataLog/info.vue
new file mode 100644
index 0000000..03e0408
--- /dev/null
+++ b/web/src/views/backend/security/sensitiveDataLog/info.vue
@@ -0,0 +1,128 @@
+
+
+
+ {{ t('Info') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/backend/user/group/index.vue b/web/src/views/backend/user/group/index.vue
new file mode 100644
index 0000000..27110de
--- /dev/null
+++ b/web/src/views/backend/user/group/index.vue
@@ -0,0 +1,155 @@
+
+
+
+
+
+
+
diff --git a/web/src/views/backend/user/group/popupForm.vue b/web/src/views/backend/user/group/popupForm.vue
new file mode 100644
index 0000000..f60742e
--- /dev/null
+++ b/web/src/views/backend/user/group/popupForm.vue
@@ -0,0 +1,145 @@
+
+
+
+
+
+ {{ baTable.form.operate ? t(baTable.form.operate) : '' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('Cancel') }}
+
+ {{ baTable.form.operateIds && baTable.form.operateIds.length > 1 ? t('Save and edit next item') : t('Save') }}
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/backend/user/moneyLog/index.vue b/web/src/views/backend/user/moneyLog/index.vue
new file mode 100644
index 0000000..b620f7f
--- /dev/null
+++ b/web/src/views/backend/user/moneyLog/index.vue
@@ -0,0 +1,147 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/backend/user/moneyLog/popupForm.vue b/web/src/views/backend/user/moneyLog/popupForm.vue
new file mode 100644
index 0000000..50570b4
--- /dev/null
+++ b/web/src/views/backend/user/moneyLog/popupForm.vue
@@ -0,0 +1,160 @@
+
+
+
+
+
+ {{ baTable.form.operate ? t(baTable.form.operate) : '' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('Cancel') }}
+
+ {{ baTable.form.operateIds!.length > 1 ? t('Save and edit next item') : t('Save') }}
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/backend/user/rule/index.vue b/web/src/views/backend/user/rule/index.vue
new file mode 100644
index 0000000..f631ff8
--- /dev/null
+++ b/web/src/views/backend/user/rule/index.vue
@@ -0,0 +1,108 @@
+
+
+
+
+
+
+
diff --git a/web/src/views/backend/user/rule/popupForm.vue b/web/src/views/backend/user/rule/popupForm.vue
new file mode 100644
index 0000000..9343f12
--- /dev/null
+++ b/web/src/views/backend/user/rule/popupForm.vue
@@ -0,0 +1,237 @@
+
+
+
+
+
+ {{ baTable.form.operate ? t(baTable.form.operate) : '' }}
+
+
+
+
+
+
+
+ {{ t('Cancel') }}
+
+ {{ baTable.form.operateIds && baTable.form.operateIds.length > 1 ? t('Save and edit next item') : t('Save') }}
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/backend/user/scoreLog/index.vue b/web/src/views/backend/user/scoreLog/index.vue
new file mode 100644
index 0000000..676eaa8
--- /dev/null
+++ b/web/src/views/backend/user/scoreLog/index.vue
@@ -0,0 +1,126 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/backend/user/scoreLog/popupForm.vue b/web/src/views/backend/user/scoreLog/popupForm.vue
new file mode 100644
index 0000000..0f54c04
--- /dev/null
+++ b/web/src/views/backend/user/scoreLog/popupForm.vue
@@ -0,0 +1,160 @@
+
+
+
+
+
+ {{ baTable.form.operate ? t(baTable.form.operate) : '' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('Cancel') }}
+
+ {{ baTable.form.operateIds!.length > 1 ? t('Save and edit next item') : t('Save') }}
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/backend/user/user/index.vue b/web/src/views/backend/user/user/index.vue
new file mode 100644
index 0000000..a55b23b
--- /dev/null
+++ b/web/src/views/backend/user/user/index.vue
@@ -0,0 +1,114 @@
+
+
+
+
+
+
+
diff --git a/web/src/views/backend/user/user/popupForm.vue b/web/src/views/backend/user/user/popupForm.vue
new file mode 100644
index 0000000..48850fd
--- /dev/null
+++ b/web/src/views/backend/user/user/popupForm.vue
@@ -0,0 +1,238 @@
+
+
+
+
+
+ {{ baTable.form.operate ? t(baTable.form.operate) : '' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('user.user.Adjustment balance') }}
+
+
+
+
+
+
+ {{ t('user.user.Adjust integral') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('Cancel') }}
+
+ {{ baTable.form.operateIds && baTable.form.operateIds.length > 1 ? t('Save and edit next item') : t('Save') }}
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/common/error/401.vue b/web/src/views/common/error/401.vue
new file mode 100644
index 0000000..625b22e
--- /dev/null
+++ b/web/src/views/common/error/401.vue
@@ -0,0 +1,79 @@
+
+
+
+
401 WARNING
+
+ {{ $t('401.noPowerTip') }}
+
+
+
+
+
+
+
diff --git a/web/src/views/common/error/404.vue b/web/src/views/common/error/404.vue
new file mode 100644
index 0000000..7045129
--- /dev/null
+++ b/web/src/views/common/error/404.vue
@@ -0,0 +1,120 @@
+
+
+
+
:(
+
{{ $t('404.problems tip') }}
+
+ {{ $t('Complete') }} {{ complete }} %
+
+
+
+
+
+
+
{{ $t('404.We will automatically return to the previous page when we are finished') }}
+
+
+ {{ $t('404.Back to previous page') }}
+
+ {{ $t('404.Return to home page') }}
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/frontend/index.vue b/web/src/views/frontend/index.vue
new file mode 100644
index 0000000..f32cfe4
--- /dev/null
+++ b/web/src/views/frontend/index.vue
@@ -0,0 +1,146 @@
+
+
+
+
+
+
+
+
{{ siteConfig.siteName }}
+
+ {{ $t('index.Steve Jobs') }}
+
+
+ {{ $t('Member Center') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/frontend/user/account/balance.vue b/web/src/views/frontend/user/account/balance.vue
new file mode 100644
index 0000000..f3fb1b0
--- /dev/null
+++ b/web/src/views/frontend/user/account/balance.vue
@@ -0,0 +1,122 @@
+
+
+
+
+
+
+
+
+
{{ item.memo }}
+
{{ $t('Balance') + ':+' + item.money }}
+
{{ $t('Balance') + ':' + item.money }}
+
{{ $t('user.account.balance.Balance after change') + ':' + item.after }}
+
{{ $t('user.account.balance.Change time') + ':' + timeFormat(item.create_time) }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/frontend/user/account/changePassword.vue b/web/src/views/frontend/user/account/changePassword.vue
new file mode 100644
index 0000000..4086002
--- /dev/null
+++ b/web/src/views/frontend/user/account/changePassword.vue
@@ -0,0 +1,116 @@
+
+
+
+
+
+
+
+
+
+ {{ $t('Reset') }}
+ {{ $t('Save') }}
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/frontend/user/account/integral.vue b/web/src/views/frontend/user/account/integral.vue
new file mode 100644
index 0000000..00fcdd4
--- /dev/null
+++ b/web/src/views/frontend/user/account/integral.vue
@@ -0,0 +1,124 @@
+
+
+
+
+
+
+
+
+
{{ item.memo }}
+
+ {{ $t('Integral') + ':+' + item.score }}
+
+
{{ $t('Integral') + ':' + item.score }}
+
{{ $t('user.account.integral.Points after change') + ':' + item.after }}
+
{{ $t('user.account.integral.Change time') + ':' + timeFormat(item.create_time) }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/frontend/user/account/overview.vue b/web/src/views/frontend/user/account/overview.vue
new file mode 100644
index 0000000..5dcf41d
--- /dev/null
+++ b/web/src/views/frontend/user/account/overview.vue
@@ -0,0 +1,299 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ userInfo.nickname + $t('utils.comma') + getGreet() }}
+
+ {{ $t('Integral') }}
+
+ {{ userInfo.score }}
+
+ {{ $t('Balance') }}
+
+ {{ userInfo.money }}
+
+
+
+ {{ $t('user.account.overview.Last login') }}
+ {{ timeFormat(userInfo.last_login_time) }}
+ {{ $t('user.account.overview.Last login IP') }}
+ {{ userInfo.last_login_ip }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/frontend/user/account/profile.vue b/web/src/views/frontend/user/account/profile.vue
new file mode 100644
index 0000000..b2e1fa4
--- /dev/null
+++ b/web/src/views/frontend/user/account/profile.vue
@@ -0,0 +1,533 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ state.form.email ? t('user.account.profile.Click Modify') : t('user.account.profile.bind') }}
+
+
+
+
+
+
+
+
+ {{ state.form.mobile ? t('user.account.profile.Click Modify') : t('user.account.profile.bind') }}
+
+
+
+
+
+
+
+
+
+ {{ $t('Reset') }}
+ {{ $t('Save') }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('user.account.profile.Mail verification') }}
+ ({{ t('user.account.profile.accept') + t('user.account.profile.mail') + ':' + userInfo.email }})
+
+
+ {{ t('user.account.profile.SMS verification') }}
+ ({{ t('user.account.profile.accept') + t('user.account.profile.mobile') + ':' + userInfo.mobile }})
+
+
+
+
+
+
+
+
+ {{
+ state.dialog.codeSendCountdown <= 0
+ ? t('user.account.profile.send')
+ : state.dialog.codeSendCountdown + t('user.account.profile.seconds')
+ }}
+
+
+
+
+
+
+
+ {{ t('Cancel') }}
+
+ {{ t('user.account.profile.next step') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{
+ state.dialog.codeSendCountdown <= 0
+ ? t('user.account.profile.send')
+ : state.dialog.codeSendCountdown + t('user.account.profile.seconds')
+ }}
+
+
+
+
+
+
+
+ {{ t('Cancel') }}
+
+ {{ t('user.account.profile.bind') }}
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/frontend/user/login.vue b/web/src/views/frontend/user/login.vue
new file mode 100644
index 0000000..29f891c
--- /dev/null
+++ b/web/src/views/frontend/user/login.vue
@@ -0,0 +1,590 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('user.login.Via email') }}
+
+
+ {{ t('user.login.Via mobile number') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ state.codeSendCountdown <= 0 ? t('user.login.send') : state.codeSendCountdown + t('user.login.seconds') }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('Cancel') }}
+
+ {{ t('user.login.second') }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/tsconfig.json b/web/tsconfig.json
new file mode 100644
index 0000000..7b52b4a
--- /dev/null
+++ b/web/tsconfig.json
@@ -0,0 +1,23 @@
+{
+ "compilerOptions": {
+ "target": "ESNext",
+ "module": "ESNext",
+ "lib": ["ESNext", "DOM"],
+ "useDefineForClassFields": true,
+ "moduleResolution": "Bundler",
+ "strict": true,
+ "jsx": "preserve",
+ "sourceMap": false,
+ "resolveJsonModule": true,
+ "esModuleInterop": true,
+ "isolatedModules": true,
+ "baseUrl": "./",
+ "allowJs": true,
+ "skipLibCheck": true,
+ "paths": {
+ "/@/*": ["src/*"]
+ },
+ "types": ["vite/client", "element-plus/global"]
+ },
+ "include": ["src/**/*.ts", "src/**/*.vue", "types/**/*.d.ts", "vite.config.ts"]
+}
diff --git a/web/types/global.d.ts b/web/types/global.d.ts
new file mode 100644
index 0000000..85c9e97
--- /dev/null
+++ b/web/types/global.d.ts
@@ -0,0 +1,30 @@
+interface Window {
+ existLoading: boolean
+ lazy: number
+ unique: number
+ tokenRefreshing: boolean
+ requests: Function[]
+ eventSource: EventSource
+ loadLangHandle: Record
+}
+
+interface anyObj {
+ [key: string]: any
+}
+
+interface TableDefaultData {
+ list: T
+ remark: string
+ total: number
+}
+
+interface ApiResponse {
+ code: number
+ data: T
+ msg: string
+ time: number
+}
+
+type ApiPromise = Promise>
+
+type Writeable = { -readonly [P in keyof T]: T[P] }
diff --git a/web/types/module.d.ts b/web/types/module.d.ts
new file mode 100644
index 0000000..88a1545
--- /dev/null
+++ b/web/types/module.d.ts
@@ -0,0 +1,8 @@
+///
+
+declare module '*.vue' {
+ import { DefineComponent } from 'vue'
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
+ const component: DefineComponent<{}, {}, any>
+ export default component
+}
diff --git a/web/types/table.d.ts b/web/types/table.d.ts
new file mode 100644
index 0000000..703aeee
--- /dev/null
+++ b/web/types/table.d.ts
@@ -0,0 +1,519 @@
+import type {
+ ButtonProps,
+ ButtonType,
+ ColProps,
+ ElTooltipProps,
+ FormInstance,
+ ImageProps,
+ PopconfirmProps,
+ SwitchProps,
+ TableColumnCtx,
+ TagProps,
+} from 'element-plus'
+import type { Component, ComponentPublicInstance } from 'vue'
+import Icon from '/@/components/icon/index.vue'
+import Table from '/@/components/table/index.vue'
+
+declare global {
+ interface BaTable {
+ /**
+ * 表格数据,通过 baTable.getData 获取
+ * 刷新数据可使用 baTable.onTableHeaderAction('refresh', { event: 'custom' })
+ */
+ data?: TableRow[]
+
+ /**
+ * 表格列定义
+ */
+ column: TableColumn[]
+
+ /**
+ * 获取表格数据时的过滤条件(含公共搜索、快速搜索、分页、排序等数据)
+ * 公共搜索数据可使用 baTable.setComSearchData 和 baTable.getComSearchData 进行管理
+ */
+ filter?: {
+ page?: number
+ limit?: number
+ order?: string
+ quickSearch?: string
+ search?: ComSearchData[]
+ [key: string]: any
+ }
+
+ /**
+ * 不需要双击编辑的字段,type=selection 的列为 undefined
+ * 禁用全部列的双击编辑,可使用 ['all']
+ */
+ dblClickNotEditColumn?: (string | undefined)[]
+
+ /**
+ * 表格扩展数据,随意定义,以便一些自定义数据可以随 baTable 实例传递
+ */
+ extend?: anyObj
+
+ // 表格 ref,通常在 页面 onMounted 时赋值,可选的
+ ref?: InstanceType | null
+ // 表格对应数据表的主键字段
+ pk?: string
+ // 路由 remark,后台菜单规则备注信息
+ remark?: string | null
+ // 表格加载状态
+ loading?: boolean
+ // 当前选中行
+ selection?: TableRow[]
+ // 数据总量
+ total?: number
+ // 默认排序字段和排序方式
+ defaultOrder?: { prop: string; order: string }
+ // 拖动排序限位字段,例如拖动行 pid=1,那么拖动目的行 pid 也需要为 1
+ dragSortLimitField?: string
+ // 接受 url 的 query 参数并自动触发公共搜索
+ acceptQuery?: boolean
+ // 显示公共搜索
+ showComSearch?: boolean
+ // 是否展开所有子项,树状表格专用属性
+ expandAll?: boolean
+ // 当前表格所在页面的路由 path
+ routePath?: string
+ }
+
+ interface BaTableForm {
+ /**
+ * 当前表单项数据
+ */
+ items?: anyObj
+
+ /**
+ * 当前操作标识:Add=添加,Edit=编辑
+ */
+ operate?: string
+
+ /**
+ * 添加表单字段默认值,打开表单时会使用 cloneDeep 赋值给 this.form.items 对象
+ */
+ defaultItems?: anyObj
+
+ /**
+ * 表单扩展数据,可随意定义,以便一些自定义数据可以随 baTable 实例传递
+ */
+ extend?: anyObj
+
+ // 表单 ref,实例化表格时通常无需传递
+ ref?: FormInstance | undefined
+ // 表单项 label 的宽度
+ labelWidth?: number
+ // 被操作数据ID,支持批量编辑:add=[0],edit=[1,2,n]
+ operateIds?: string[]
+ // 提交按钮状态
+ submitLoading?: boolean
+ // 表单加载状态
+ loading?: boolean
+ }
+
+ /**
+ * BaTable 前置处理函数(前置埋点)
+ */
+ interface BaTableBefore {
+ /**
+ * 获取表格数据前的钩子(返回 false 可取消原操作)
+ */
+ getData?: () => boolean | void
+
+ /**
+ * 删除前的钩子(返回 false 可取消原操作)
+ * @param object.ids 被删除数据的主键集合
+ */
+ postDel?: ({ ids }: { ids: string[] }) => boolean | void
+
+ /**
+ * 获取被编辑行数据前的钩子(返回 false 可取消原操作)
+ * @param object.id 被编辑行主键
+ */
+ getEditData?: ({ id }: { id: string }) => boolean | void
+
+ /**
+ * 双击表格具体操作执行前钩子(返回 false 可取消原操作)
+ * @param object.row 被双击行数据
+ * @param object.column 被双击列数据
+ */
+ onTableDblclick?: ({ row, column }: { row: TableRow; column: TableColumn }) => boolean | void
+
+ /**
+ * 表单切换前钩子(返回 false 可取消默认行为)
+ * @param object.operate 当前操作标识:Add=添加,Edit=编辑
+ * @param object.operateIds 被操作的行 ID 集合
+ */
+ toggleForm?: ({ operate, operateIds }: { operate: string; operateIds: string[] }) => boolean | void
+
+ /**
+ * 表单提交前钩子(返回 false 可取消原操作)
+ * @param object.formEl 表单组件ref
+ * @param object.operate 当前操作标识:Add=添加,Edit=编辑
+ * @param object.items 表单数据
+ */
+ onSubmit?: ({ formEl, operate, items }: { formEl?: FormInstance | null; operate: string; items: anyObj }) => boolean | void
+
+ /**
+ * 表格内事件响应前钩子(返回 false 可取消原操作)
+ * @param object.event 事件名称
+ * @param object.data 事件携带的数据
+ */
+ onTableAction?: ({ event, data }: { event: BaTableActionEventName; data: anyObj }) => boolean | void
+
+ /**
+ * 表格顶部菜单事件响应前钩子(返回 false 可取消原操作)
+ * @param object.event 事件名称
+ * @param object.data 事件携带的数据
+ */
+ onTableHeaderAction?: ({ event, data }: { event: BaTableHeaderActionEventName; data: anyObj }) => boolean | void
+
+ /**
+ * 表格初始化前钩子
+ */
+ mount?: () => boolean | void
+
+ /** getData 的别名 */
+ getIndex?: () => boolean | void
+ /** getEditData 的别名 */
+ requestEdit?: ({ id }: { id: string }) => boolean | void
+
+ // 可自定义其他钩子
+ [key: string]: Function | undefined
+ }
+
+ /**
+ * BaTable 后置处理函数(后置埋点)
+ */
+ interface BaTableAfter {
+ /**
+ * 请求到表格数据后钩子
+ * 此时 baTable.table.data 已赋值
+ * @param object.res 请求完整响应
+ */
+ getData?: ({ res }: { res: ApiResponse }) => void
+
+ /**
+ * 删除请求后钩子
+ * @param object.res 请求完整响应
+ */
+ postDel?: ({ res }: { res: ApiResponse }) => void
+
+ /**
+ * 获取到编辑行数据后钩子
+ * 此时 baTable.form.items 已赋值
+ * @param object.res 请求完整响应
+ */
+ getEditData?: ({ res }: { res: ApiResponse }) => void
+
+ /**
+ * 双击单元格操作执行后钩子
+ * @param object.row 当前行数据
+ * @param object.column 当前列数据
+ */
+ onTableDblclick?: ({ row, column }: { row: TableRow; column: TableColumn }) => void
+
+ /**
+ * 表单切换后钩子
+ * @param object.operate 当前操作标识:Add=添加,Edit=编辑
+ * @param object.operateIds 被操作的 ID 集合
+ */
+ toggleForm?: ({ operate, operateIds }: { operate: string; operateIds: string[] }) => void
+
+ /**
+ * 表单提交后钩子
+ * @param object.res 请求完整响应
+ */
+ onSubmit?: ({ res }: { res: ApiResponse }) => void
+
+ /**
+ * 表格内事件响应后钩子
+ * @param object.event 事件名称
+ * @param object.data 事件携带的数据
+ */
+ onTableAction?: ({ event, data }: { event: BaTableActionEventName; data: anyObj }) => void
+
+ /**
+ * 表格顶部菜单事件响应后钩子
+ * @param object.event 事件名称
+ * @param object.data 事件携带的数据
+ */
+ onTableHeaderAction?: ({ event, data }: { event: BaTableHeaderActionEventName; data: anyObj }) => void
+
+ /** getData 的别名 */
+ getIndex?: ({ res }: { res: ApiResponse }) => void
+ /** getEditData 的别名 */
+ requestEdit?: ({ res }: { res: ApiResponse }) => void
+
+ // 可自定义其他钩子
+ [key: string]: Function | undefined
+ }
+
+ /**
+ * baTable 表格内事件名称
+ * selection-change=选中项改变,page-size-change=每页数量改变,current-page-change=翻页,sort-change=排序,edit=编辑,delete=删除,field-change=单元格值改变,com-search=公共搜索
+ */
+ type BaTableActionEventName =
+ | 'selection-change'
+ | 'page-size-change'
+ | 'current-page-change'
+ | 'sort-change'
+ | 'edit'
+ | 'delete'
+ | 'field-change'
+ | 'com-search'
+
+ /**
+ * baTable 表格头部事件名称
+ * refresh=刷新,add=添加,edit=编辑,delete=删除,quick-search=快速查询,unfold=折叠/展开,change-show-column=调整列显示状态
+ */
+ type BaTableHeaderActionEventName = 'refresh' | 'add' | 'edit' | 'delete' | 'quick-search' | 'unfold' | 'change-show-column'
+
+ /**
+ * 表格公共搜索数据
+ */
+ interface ComSearch {
+ /** 表单项数据 */
+ form: anyObj
+ /** 字段搜索配置,搜索操作符(operator)、字段渲染方式(render)等 */
+ fieldData: Map
+ }
+
+ /**
+ * 表格列
+ */
+ interface TableColumn extends Partial> {
+ // 是否于表格显示此列
+ show?: boolean
+ // 渲染器组件名,即 \src\components\table\fieldRender\ 中的组件之一,也可以查看 TableRenderer 类型定义
+ render?: TableRenderer
+ // 值替换数据(字典数据),同时用于单元格渲染时和作为公共搜索下拉框数据,格式如:{ open: '开', close: '关', disable: '已禁用' }
+ replaceValue?: Record
+
+ // render=slot 时,slot 的名称
+ slotName?: string
+ // render=customRender 时,要渲染的组件或已注册组件名称的字符串
+ customRender?: string | Component
+ // render=customTemplate 时,自定义渲染 html,应谨慎使用:请返回 html 内容,务必确保返回内容是 xss 安全的
+ customTemplate?: (row: TableRow, field: TableColumn, value: any, column: TableColumnCtx, index: number) => string
+ // 渲染前对字段值的预处理函数(对 el-table 的 formatter 扩展)
+ formatter?: (row: TableRow, column: TableColumnCtx, cellValue: any, index: number) => any
+
+ /**
+ * 自定义单元格渲染属性(比如单元格渲染器内部的 tag、button 组件的属性,设计上不仅是组件属性,也可以自定义其他渲染相关属性)
+ * 直接定义对应组件的属性 object,或使用一个函数返回组件属性 object
+ */
+ customRenderAttr?: {
+ tag?: TableContextDataFun
+ icon?: TableContextDataFun['$props']>
+ image?: TableContextDataFun
+ switch?: TableContextDataFun
+ tooltip?: TableContextDataFun