diff --git a/oaer/lib/cfg.ts b/oaer/lib/cfg.ts index c1cfc26..4b95d67 100644 --- a/oaer/lib/cfg.ts +++ b/oaer/lib/cfg.ts @@ -20,6 +20,10 @@ let cfg = proxy.Watch({ phone: '', icon: 'https://public.veypi.com/img/avatar/0001.jpg' }, + myapps: [ + { name: 'a' }, + { name: 'b' }, + ], ready: false, local_user: {}, diff --git a/oaer/lib/components/account.ts b/oaer/lib/components/account.ts index 0ae0cc8..8d0a48a 100644 --- a/oaer/lib/components/account.ts +++ b/oaer/lib/components/account.ts @@ -4,18 +4,18 @@ * 2024-10-22 22:07 * Distributed under terms of the GPL license. */ -import b from './build' +import v from './v2dom' import cfg from '../cfg' -export default b('voa-account', [ - b('voa-account-header', [b('voa-ah-1', '我的账户'), b('voa-ah-2')]), - b('voa-account-body', [ - b('voa-ab-ico', [b('img', '', (d) => { d.setAttribute('src', cfg.user.icon) })]), - b('voa-ab-info', [ - b('voa-abi-1', [b('span', '昵称:'), b('span', '', (d) => { d.innerHTML = cfg.user.nickname })]), - b('voa-abi-2', [b('span', '账户:'), b('span', '', (d) => { d.innerHTML = cfg.user.username })]), - b('voa-abi-3', [b('span', '邮箱:'), b('span', '', (d) => { d.innerHTML = cfg.user.email })]), - b('voa-abi-4', [b('span', '手机:'), b('span', '', (d) => { d.innerHTML = cfg.user.phone })]), +export default v('voa-account', [ + v('voa-account-header', [v('voa-ah-1', '我的账户'), v('voa-ah-2')]), + v('voa-account-body', [ + v('voa-ab-ico', [v('img', '', (d) => { d.setAttribute('src', cfg.user.icon) })]), + v('voa-ab-info', [ + v('voa-abi-1', [v('span', '昵称:'), v('span', '', (d) => { d.innerHTML = cfg.user.nickname })]), + v('voa-abi-2', [v('span', '账户:'), v('span', '', (d) => { d.innerHTML = cfg.user.username })]), + v('voa-abi-3', [v('span', '邮箱:'), v('span', '', (d) => { d.innerHTML = cfg.user.email })]), + v('voa-abi-4', [v('span', '手机:'), v('span', '', (d) => { d.innerHTML = cfg.user.phone })]), ]) ]) ]) diff --git a/oaer/lib/components/app.ts b/oaer/lib/components/app.ts index 1926cf4..f254446 100644 --- a/oaer/lib/components/app.ts +++ b/oaer/lib/components/app.ts @@ -5,20 +5,20 @@ * Distributed under terms of the GPL license. */ -import b from "./build"; +import cfg from "../cfg"; +import v from "./v2dom"; -export default class { - main: HTMLElement - body: HTMLElement - constructor() { - this.body = b('voa-apps-body') - this.main = b({ - class: 'voa-apps', - children: [ - b({ class: 'voa-apps-header', children: [b('voa-apps-title')] }), - this.body - ] - }) - } -} +export default v({ + class: 'voa-apps', + children: [ + v({ class: 'voa-apps-header', children: [v('voa-apps-title', '我的应用')] }), + v('voa-apps-body', [ + [cfg.myapps, + (data) => v('voa-app-box', '', (d) => d.innerHTML = data.name)], + v('div', '222'), + [cfg.myapps, + (data) => v('voa-app-box', '', (d) => d.innerHTML = data.name)], + ]) + ] +}) diff --git a/oaer/lib/components/build.ts b/oaer/lib/components/build.ts deleted file mode 100644 index 3649473..0000000 --- a/oaer/lib/components/build.ts +++ /dev/null @@ -1,77 +0,0 @@ -/* - * build.ts - * Copyright (C) 2024 veypi - * 2024-10-23 15:54 - * Distributed under terms of the GPL license. - */ - -import proxy from "./proxy" - - -interface buildOpts { - id?: string - typ?: string - class?: string - style?: string - innerHtml?: string - onclick?: any - children?: HTMLElement[] - updator?: (p: HTMLElement) => void -} - -const typs = ['div', 'img', 'span', 'p', 'a'] - -export default (opts: buildOpts | string, inner?: string | HTMLElement[], updator?: (p: HTMLElement) => void) => { - if (typeof opts === 'string') { - if (typs.indexOf(opts) >= 0) { - opts = { typ: opts } - } else { - opts = { class: opts } - } - } - if (inner) { - if (typeof inner == 'string') { - opts.innerHtml = inner - } else { - opts.children = inner - } - } - if (opts.updator) { - updator = opts.updator - } - let dom = document.createElement(opts.typ || 'div') - if (opts.id) { - dom.id = opts.id - } - if (opts.class) { - dom.classList.add(...opts.class.split(' ')) - } - if (opts.innerHtml) { - dom.innerHTML = opts.innerHtml - } - if (opts.onclick) { - dom.onclick = opts.onclick - } - if (opts.children) { - for (let c in opts.children) { - dom.appendChild(opts.children[c]) - } - } - if (opts.style) { - const regex = /([a-zA-Z-]+)\s*:\s*([^;]+);?/g; - let match: any - while ((match = regex.exec(opts.style)) !== null) { - const key = match[1].trim(); - const value = match[2].trim(); - dom.style.setProperty(key, value) - } - } - dom.setAttribute('voa', '1') - if (updator) { - proxy.Listen(() => { - updator(dom) - }) - } - return dom -} - diff --git a/oaer/lib/components/index.ts b/oaer/lib/components/index.ts index 6266e95..7102a51 100644 --- a/oaer/lib/components/index.ts +++ b/oaer/lib/components/index.ts @@ -6,7 +6,7 @@ */ import slide from './slide' -import b from './build' +import v from './v2dom' import bus from '../bus' export default class { slide: slide @@ -24,7 +24,7 @@ export default class { }) } mount_login() { - this.frame_login = b({ + this.frame_login = v({ class: 'voa-off voa-hover-line-b voa-scale-in', innerHtml: '登录', onclick: () => { @@ -40,7 +40,7 @@ export default class { } mount_user() { let icon = 'https://public.veypi.com/img/avatar/0001.jpg' - this.frame_user = b({ + this.frame_user = v({ class: 'voa-on voa-scale-in', innerHtml: ` diff --git a/oaer/lib/components/proxy.ts b/oaer/lib/components/proxy.ts index ca27807..71a2250 100644 --- a/oaer/lib/components/proxy.ts +++ b/oaer/lib/components/proxy.ts @@ -6,8 +6,22 @@ */ type voidFn = () => void +// TODO: 没有删除机制 const callbackCache: voidFn[] = [] +const cacheUpdateList: number[] = [] +// 界面响应频率40hz +setInterval(() => { + let list = new Set(cacheUpdateList.splice(0)) + for (let l of list) { + callbackCache[l]() + } +}, 25) +function generateUniqueId() { + const timestamp = performance.now().toString(36); + const random = Math.random().toString(36).substring(2, 5); + return `${timestamp}-${random}`; +} function ForceUpdate() { for (let c of callbackCache) { @@ -15,51 +29,77 @@ function ForceUpdate() { } } -var listen_tag = -1 +var listen_tags: number[] = [] function Listen(callback: voidFn) { - if (listen_tag >= 0) { - console.warn('it shuold not happen') - return - } - listen_tag = callbackCache.length + listen_tags.push(callbackCache.length) callbackCache.push(callback) callback() - listen_tag = -1 + listen_tags.pop() } +const isProxy = Symbol("isProxy") +const DataID = Symbol("DataID") -function Watch(target: T) { - const listeners: { [key: string]: number[] } = {} +function Watch(data: T) { + const did = generateUniqueId() + let isArray = false + if (Object.prototype.toString.call(data) === '[object Array]') { + isArray = true + } + // console.log(`watch ${did} ${isArray}`, data) + const listeners: { [key: string | symbol]: number[] } = {} const handler = { - get(target: Object, key: string, receiver: any) { - console.log(`get ${key} ${listen_tag}`) - const value = Reflect.get(target, key, receiver); + get(target: Object, key: string | symbol, receiver: any) { + if (key === isProxy) { + return true + } + if (key === DataID) { + return did + } + const value = Reflect.get(target, key, receiver) if (typeof value === 'object' && value !== null) { - return new Proxy(value, handler); + if (value[isProxy]) { + return value + } else { + let newValue = Watch(value) + Reflect.set(target, key, newValue, receiver) + return newValue + } } - if (listen_tag >= 0) { - if (!listeners[key]) { - listeners[key] = [] + let idx = -1 + if (listen_tags.length > 0) { + let lkey = key + idx = listen_tags[listen_tags.length - 1] + if (isArray) { + lkey = '' + } + if (!listeners[lkey]) { + listeners[lkey] = [idx] + } else if (listeners[lkey].indexOf(idx) == -1) { + listeners[lkey].push(idx) } - listeners[key].push(listen_tag) } + // console.log(`${did} get ${key.toString()}:${value} ${idx}`) return value; }, - set(target: Object, key: string, newValue: any, receiver: any) { - console.log(`set ${key} ${newValue}`) + set(target: Object, key: string | symbol, newValue: any, receiver: any) { + // console.log(`${did} set ${key.toString()} ${newValue}`) const result = Reflect.set(target, key, newValue, receiver); if (result) { - if (listeners[key]) { - console.log(listeners[key]) - for (let cb of listeners[key]) { - callbackCache[cb]() + let lkey = key + if (isArray) { + lkey = '' + } + if (listeners[lkey]) { + for (let cb of listeners[lkey]) { + cacheUpdateList.push(cb) } } } return result; }, deleteProperty(target: Object, key: string) { - console.log(`del ${key}`) + // console.log(`del ${key}`) const result = Reflect.deleteProperty(target, key); if (result) { } @@ -67,7 +107,10 @@ function Watch(target: T) { } }; - return new Proxy(target, handler); + let res = new Proxy(data, handler); + // Symbol(Symbol.toStringTag) + // res[Symbol.toStringTag] = 'Proxy' + return res } -export default { Watch, Listen, ForceUpdate } +export default { Watch, Listen, ForceUpdate, DataID, generateUniqueId } diff --git a/oaer/lib/components/slide.ts b/oaer/lib/components/slide.ts index 96809d2..acbbe14 100644 --- a/oaer/lib/components/slide.ts +++ b/oaer/lib/components/slide.ts @@ -4,10 +4,10 @@ * 2024-10-22 17:57 * Distributed under terms of the GPL license. */ -import b from './build' +import v from './v2dom' import bus from '../bus' import account from './account' -import cfg from '../cfg' +import app from './app' /* mask @@ -26,38 +26,36 @@ export default class { main: HTMLElement footer: HTMLElement constructor() { - this.header = b({ + this.header = v({ class: 'voa-slide-header voa-animate-slow', }) - this.footer = b({ + this.footer = v({ class: 'voa-slide-footer', innerHtml: 'logout', onclick: () => { bus.emit('logout') } }) - this.main = b({ + this.main = v({ class: 'voa-slide-main', children: [ account, - b({ class: 'voa-sm-separate' }) - ], - onclick: () => { - console.log(cfg) - cfg.user.phone = new Date().toLocaleString() - } + v({ class: 'voa-sm-separate' }), + app, + v({ class: 'voa-sm-separate' }) + ] }) - this.body = b({ + this.body = v({ class: 'voa-slide-body voa-animate-slow', style: 'animation-delay: 300ms', children: [this.main, this.footer] }) - this.slide = b({ + this.slide = v({ id: 'voa-slide', class: 'voa-slide', children: [this.header, this.body] }) - this.mask = b({ + this.mask = v({ class: 'voa-slide-mask', style: 'visibility: hidden', children: [this.slide], diff --git a/oaer/lib/components/v2dom.ts b/oaer/lib/components/v2dom.ts new file mode 100644 index 0000000..8a40589 --- /dev/null +++ b/oaer/lib/components/v2dom.ts @@ -0,0 +1,154 @@ +/* + * v2dom.ts + * Copyright (C) 2024 veypi + * 2024-10-23 15:54 + * Distributed under terms of the GPL license. + */ + +import proxy from "./proxy" + + +interface buildOpts { + id?: string + typ?: string + class?: string + style?: string + innerHtml?: string + onclick?: any + children?: childTyp[] + updator?: (p: HTMLElement) => void +} + +const typs = ['div', 'img', 'span', 'p', 'a'] + +type iterChild = [T[], (d: T) => HTMLElement] +type childTyp = HTMLElement | iterChild +export default (opts: buildOpts | string, inner?: string | childTyp[], updator?: (p: HTMLElement) => void) => { + if (typeof opts === 'string') { + if (typs.indexOf(opts) >= 0) { + opts = { typ: opts } + } else { + opts = { class: opts } + } + } + if (inner) { + if (typeof inner == 'string') { + opts.innerHtml = inner + } else { + opts.children = inner + } + } + if (opts.updator) { + updator = opts.updator + } + let dom = document.createElement(opts.typ || 'div') + if (opts.id) { + dom.id = opts.id + } + if (opts.class) { + dom.classList.add(...opts.class.split(' ')) + } + if (opts.innerHtml) { + dom.innerHTML = opts.innerHtml + } + if (opts.onclick) { + dom.onclick = opts.onclick + } + if (opts.children) { + let tmpID = '' + for (let c of opts.children) { + if (Object.prototype.toString.call(c) === '[object Array]') { + const iterID = proxy.generateUniqueId() + const iterLast = tmpID + proxy.Listen(() => { + c = c as iterChild + let itemIDs: string[] = [] + for (let i = 0; i < c[0].length; i++) { + let line = c[0][i] + //@ts-ignore + let itemID = line[proxy.DataID] || '' + let nextID = '' + if (i < c[0].length - 1) { + //@ts-ignore + nextID = c[0][i + 1][proxy.DataID] || '' + } + itemIDs.push(itemID) + let inserted = -2 + let subdom: HTMLElement | null = null + let beforeChild: HTMLElement | null = null + for (let childID = 0; childID < dom.children.length; childID++) { + let child = dom.children[childID] + // console.log(`${inserted} ${childID} ${itemID} ${nextID}`) + if (child.getAttribute('vbind-iter') === iterID) { + if (child.getAttribute('vbind-proxy') === itemID) { + inserted = childID + subdom = child as HTMLElement + } else if (child.getAttribute('vbind-proxy') === nextID && nextID != '') { + // 用于交换位置 + if (inserted != childID - 1) { + beforeChild = child as HTMLElement + } + } + } else if (child.getAttribute('vbind-iterlast') === iterID) { + if (inserted == -2) { + beforeChild = child as HTMLElement + } + // 找到第一个不是这个循环块的dom元素时退出 + break + } + } + if (inserted == -2) { + subdom = c[1](line) + subdom.setAttribute('vbind-proxy', itemID) + subdom.setAttribute('vbind-iter', iterID) + if (iterLast != '') { + subdom.setAttribute('vbind-iterlast', iterLast) + } + if (beforeChild) { + dom.insertBefore(subdom, beforeChild) + // console.log(`${iterID} insert new`, subdom) + } else { + // console.log(`${iterID} append new`, subdom) + dom.appendChild(subdom) + } + } else if (beforeChild) { + // console.log(`${iterID} insert old`, subdom) + dom.insertBefore(subdom!, beforeChild) + } else { + // console.log(`${iterID} none`, subdom) + } + } + for (let child of dom.children) { + if (child.getAttribute('vbind-iter') === iterID && itemIDs.indexOf(child.getAttribute('vbind-proxy') || '') == -1) { + dom.removeChild(child) + } + } + }) + tmpID = iterID + } else { + c = c as HTMLElement + if (tmpID != '') { + c.setAttribute('vbind-iterlast', tmpID) + } + dom.appendChild(c) + } + } + } + if (opts.style) { + const regex = /([a-zA-Z-]+)\s*:\s*([^;]+);?/g; + let match: any + while ((match = regex.exec(opts.style)) !== null) { + const key = match[1].trim(); + const value = match[2].trim(); + dom.style.setProperty(key, value) + } + } + dom.setAttribute('voa', '1') + if (updator) { + proxy.Listen(() => { + updator(dom) + }) + } + return dom +} + diff --git a/oaer/lib/main.ts b/oaer/lib/main.ts index f6f1cdf..0484ea2 100644 --- a/oaer/lib/main.ts +++ b/oaer/lib/main.ts @@ -7,7 +7,6 @@ import './assets/css/oaer.scss' import bus from './bus' - import ui from './components' export class OAer { diff --git a/oaer/package.json b/oaer/package.json index c62a177..109a383 100644 --- a/oaer/package.json +++ b/oaer/package.json @@ -22,7 +22,8 @@ "devDependencies": { "sass": "^1.77.6", "typescript": "^5.4.5", - "vite": "^5.2.10" + "vite": "^5.2.10", + "vite-plugin-css-injected-by-js": "^3.5.2" }, "repository": { "type": "git", diff --git a/oaer/vite.config.ts b/oaer/vite.config.ts index 53f21ce..bab15ba 100644 --- a/oaer/vite.config.ts +++ b/oaer/vite.config.ts @@ -1,6 +1,8 @@ import { defineConfig } from 'vite' +import cssInjectedByJsPlugin from "vite-plugin-css-injected-by-js"; export default defineConfig({ + plugins: [cssInjectedByJsPlugin()], build: { lib: { entry: './lib/main.ts', diff --git a/oaer/yarn.lock b/oaer/yarn.lock index d5c35c9..2d48f03 100644 --- a/oaer/yarn.lock +++ b/oaer/yarn.lock @@ -229,7 +229,7 @@ binary-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== -braces@~3.0.2: +braces@^3.0.3, braces@~3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== @@ -354,6 +354,14 @@ is-number@^7.0.0: resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== +micromatch@^4.0.7: + version "4.0.8" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" + integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== + dependencies: + braces "^3.0.3" + picomatch "^2.3.1" + mime-db@1.52.0: version "1.52.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" @@ -386,7 +394,7 @@ picocolors@^1.0.1: resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1" integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew== -picomatch@^2.0.4, picomatch@^2.2.1: +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== @@ -463,6 +471,18 @@ typescript@^5.4.5: resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.3.tgz#e1b0a3c394190838a0b168e771b0ad56a0af0faa" integrity sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ== +vite-plugin-css-injected-by-js@^3.5.2: + version "3.5.2" + resolved "https://registry.yarnpkg.com/vite-plugin-css-injected-by-js/-/vite-plugin-css-injected-by-js-3.5.2.tgz#1f75d16ad5c05b6b49bf18018099a189ec2e46ad" + integrity sha512-2MpU/Y+SCZyWUB6ua3HbJCrgnF0KACAsmzOQt1UvRVJCGF6S8xdA3ZUhWcWdM9ivG4I5az8PnQmwwrkC2CAQrQ== + +vite-plugin-singlefile@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/vite-plugin-singlefile/-/vite-plugin-singlefile-2.0.2.tgz#af2c95e0182bd363dbe29b80bea5c5a78209f649" + integrity sha512-Z2ou6HcvED5CF0hM+vcFSaFa+klyS8RyyLxW0PbMRLnMbvzTI6ueWyxdYNFhpuXZgz/aj6+E/dHFTdEcw6gb9w== + dependencies: + micromatch "^4.0.7" + vite@^5.2.10: version "5.3.3" resolved "https://registry.yarnpkg.com/vite/-/vite-5.3.3.tgz#5265b1f0a825b3b6564c2d07524777c83e3c04c2"