diff --git a/package.json b/package.json index d7c5833..e450ca3 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "koajax": "^1.1.2", "mobx": "^6.12.4", "mobx-github": "^0.3.2", + "mobx-restful": "^1.0.0-rc.5", "web-cell": "^3.0.0-rc.16", "web-utility": "^4.4.0" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dd1ee95..de79e93 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,6 +41,9 @@ importers: mobx-github: specifier: ^0.3.2 version: 0.3.2(typescript@5.4.5) + mobx-restful: + specifier: ^1.0.0-rc.5 + version: 1.0.0-rc.5(mobx@6.12.4)(typescript@5.4.5) web-cell: specifier: ^3.0.0-rc.16 version: 3.0.0-rc.16(element-internals-polyfill@1.3.11)(typescript@5.4.5) @@ -2250,6 +2253,10 @@ packages: fastq@1.17.1: resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} + fetch-blob@3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + figures@3.2.0: resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} engines: {node: '>=8'} @@ -2490,6 +2497,9 @@ packages: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} + idb-keyval@6.2.1: + resolution: {integrity: sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg==} + idb@7.1.1: resolution: {integrity: sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==} @@ -3012,6 +3022,11 @@ packages: peerDependencies: mobx: '>=6.11' + mobx-restful@1.0.0-rc.5: + resolution: {integrity: sha512-7NpXLHqMXyQpIAsFb0bXM+fURhrxfO+tD5SGFElyCKvF95QoeMmhXI258SvAF9Q/5lwE2+NbI5TGEHDKqo8lAg==} + peerDependencies: + mobx: '>=6.11' + mobx@6.12.4: resolution: {integrity: sha512-uIymg89x+HmItX1p3MG+d09irn2k63J6biftZ5Ok+UpNojS1I3NJPLfcmJT9ANnUltNlHi+HQqrVyxiAN8ISYg==} @@ -3028,6 +3043,10 @@ packages: mute-stream@0.0.8: resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} + native-file-system-adapter@3.0.1: + resolution: {integrity: sha512-ocuhsYk2SY0906LPc3QIMW+rCV3MdhqGiy7wV5Bf0e8/5TsMjDdyIwhNiVPiKxzTJLDrLT6h8BoV9ERfJscKhw==} + engines: {node: '>=14.8.0'} + natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} @@ -3043,6 +3062,10 @@ packages: resolution: {integrity: sha512-mNcltoe1R8o7STTegSOHdnJNN7s5EUvhoS7ShnTHDyOSd+8H+UdWODq6qSv67PjC8Zc5JRT8+oLAMCr0SIXw7g==} engines: {node: ^16 || ^18 || >= 20} + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + node-gyp-build-optional-packages@5.0.7: resolution: {integrity: sha512-YlCCc6Wffkx0kHkmam79GKvDQ6x+QZkMjFGrIMxgFNILFvGSbCp2fCBC55pGTT9gVaz8Na5CLmxt/urtzRv36w==} hasBin: true @@ -3786,6 +3809,10 @@ packages: element-internals-polyfill: ^1 jsdom: '>=21' + web-streams-polyfill@3.3.3: + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} + engines: {node: '>= 8'} + web-streams-polyfill@4.0.0: resolution: {integrity: sha512-0zJXHRAYEjM2tUfZ2DiSOHAa2aw1tisnnhU3ufD57R8iefL+DcdJyRBRyJpG+NUimDgbTI/lH+gAE1PAvV3Cgw==} engines: {node: '>= 8'} @@ -5718,7 +5745,7 @@ snapshots: '@swc/helpers@0.5.6': dependencies: - tslib: 2.6.2 + tslib: 2.6.3 '@swc/types@0.1.5': {} @@ -6565,6 +6592,12 @@ snapshots: dependencies: reusify: 1.0.4 + fetch-blob@3.2.0: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.3.3 + optional: true + figures@3.2.0: dependencies: escape-string-regexp: 1.0.5 @@ -6807,6 +6840,8 @@ snapshots: safer-buffer: 2.1.2 optional: true + idb-keyval@6.2.1: {} + idb@7.1.1: {} ieee754@1.2.1: {} @@ -6986,7 +7021,7 @@ snapshots: iterable-observer@1.0.1: dependencies: - '@swc/helpers': 0.5.6 + '@swc/helpers': 0.5.11 jake@10.8.7: dependencies: @@ -7315,6 +7350,20 @@ snapshots: - jsdom - typescript + mobx-restful@1.0.0-rc.5(mobx@6.12.4)(typescript@5.4.5): + dependencies: + '@swc/helpers': 0.5.11 + idb-keyval: 6.2.1 + koajax: 1.1.2(typescript@5.4.5) + mobx: 6.12.4 + native-file-system-adapter: 3.0.1 + regenerator-runtime: 0.14.1 + web-streams-polyfill: 4.0.0 + web-utility: 4.4.0(typescript@5.4.5) + transitivePeerDependencies: + - jsdom + - typescript + mobx@6.12.4: {} ms@2.1.2: {} @@ -7337,6 +7386,10 @@ snapshots: mute-stream@0.0.8: {} + native-file-system-adapter@3.0.1: + optionalDependencies: + fetch-blob: 3.2.0 + natural-compare@1.4.0: {} needle@3.3.1: @@ -7349,6 +7402,9 @@ snapshots: node-addon-api@7.1.0: {} + node-domexception@1.0.0: + optional: true + node-gyp-build-optional-packages@5.0.7: optional: true @@ -8112,6 +8168,9 @@ snapshots: transitivePeerDependencies: - typescript + web-streams-polyfill@3.3.3: + optional: true + web-streams-polyfill@4.0.0: {} web-utility@4.4.0(typescript@5.4.5): diff --git a/source/component/AuditBar.tsx b/source/component/AuditBar.tsx index ddaac33..c02cb1c 100644 --- a/source/component/AuditBar.tsx +++ b/source/component/AuditBar.tsx @@ -77,7 +77,7 @@ export const AuditBar: FC = observer(props => { diff --git a/source/component/CardsPage.tsx b/source/component/CardsPage.tsx index da4c400..fa10ebd 100644 --- a/source/component/CardsPage.tsx +++ b/source/component/CardsPage.tsx @@ -25,17 +25,21 @@ export abstract class CardsPage }; mountedCallback() { - this.model.getNextPage(this.filter); + this.model.getList(this.filter); + } + + disconnectedCallback() { + this.model.clear(); } loadMore: TouchHandler = detail => { - if (detail === 'bottom') return this.model.getNextPage(this.filter); + if (detail === 'bottom') return this.model.getList(this.filter); }; changeDistrict = ({ detail }: DistrictEvent) => - this.model.getNextPage( + this.model.getList( (this.filter = { ...detail, verified: this.filter.verified }), - true + 1 ); changeVerified = ({ target }: Event) => { @@ -43,7 +47,7 @@ export abstract class CardsPage this.filter.verified = checked; - return this.model.getNextPage(this.filter, true); + return this.model.getList(this.filter, 1); }; async clip2board(raw: string) { @@ -56,7 +60,7 @@ export abstract class CardsPage render() { const { name: title, scope, districtFilter } = this, - { loading, list, noMore } = this.model, + { downloading, allItems, noMore } = this.model, admin = session.hasRole('Admin'); return ( @@ -81,11 +85,11 @@ export abstract class CardsPage 0} className="row row-cols-1 row-cols-sm-2 row-cols-md-4 g-3" > - {list.map(item => ( -
+ {allItems.map(item => ( +
{this.renderItem(item as T)}
))} diff --git a/source/model/Area.ts b/source/model/Area.ts index 2f3df19..397d7d7 100644 --- a/source/model/Area.ts +++ b/source/model/Area.ts @@ -1,8 +1,10 @@ import { observable } from 'mobx'; +import { BaseModel, persist, restore, toggle } from 'mobx-restful'; import { District, getSubDistricts } from '../service'; -export class AreaModel { +export class AreaModel extends BaseModel { + @persist() @observable accessor provinces: District[] = []; @@ -13,9 +15,13 @@ export class AreaModel { accessor districts: District[] = []; constructor() { - getSubDistricts().then(list => (this.provinces = list)); + super(); + restore(this, 'area').then(async () => { + if (!this.provinces[0]) this.provinces = await getSubDistricts(); + }); } + @toggle('downloading') async getSubs(type: 'city' | 'district', parent: string) { const list = await getSubDistricts(parent); diff --git a/source/model/BaseModel.ts b/source/model/BaseModel.ts index d67bedf..b8acad8 100644 --- a/source/model/BaseModel.ts +++ b/source/model/BaseModel.ts @@ -1,99 +1,27 @@ -import { observable } from 'mobx'; +import { Filter, ListModel, toggle } from 'mobx-restful'; +import { buildURLData } from 'web-utility'; import { DataItem, service, PageData, User } from '../service'; import { session } from '.'; -export abstract class BaseModel { - @observable - accessor loading = false; - - @observable - accessor noMore = false; - - pageIndex = 0; - - pageSize = 10; - - totalCount = 0; - - @observable - accessor list: T[] = []; - - abstract baseURI: string; - - reset() { - this.loading = this.noMore = false; - - this.list.length = this.pageIndex = this.totalCount = 0; - } - - async getNextPage(filter: F, reset?: boolean) { - if (reset) this.reset(); - - if (this.loading || this.noMore) return; - - if (this.pageIndex && this.list.length === this.totalCount) { - this.noMore = true; - return; - } - - this.loading = true; +export abstract class BaseModel< + T extends DataItem = DataItem, + F extends Filter = Filter +> extends ListModel { + client = service; + indexKey = 'objectId' as const; + async loadPage(pageIndex: number, pageSize: number, filter: F) { const { body: { count, data } - } = await service.get>( - `${this.baseURI}?${new URLSearchParams({ + } = await this.client.get>( + `${this.baseURI}?${buildURLData({ ...filter, - pageIndex: this.pageIndex + 1 + '', - pageSize: this.pageSize + '' + pageIndex, + pageSize })}` ); - this.pageIndex++, (this.totalCount = count); - - this.list = this.list.concat(data); - - this.loading = false; - - if (data[0]) return data; - - this.noMore = true; - } - - async update(data: T, id?: string) { - this.loading = true; - - if (!id) { - const { body } = await service.post(this.baseURI, data); - - this.list = [body].concat(this.list); - } else { - const { body } = await service.put(this.baseURI + id, data), - index = this.list.findIndex(({ objectId }) => objectId === id); - - this.list[index] = body; - } - - this.loading = false; - } - - async getOne(id: string) { - this.loading = true; - - const { body } = await service.get(this.baseURI + id); - - this.loading = false; - - return body; - } - - async delete(id: string) { - this.loading = true; - - await service.delete(this.baseURI + id); - - this.list = this.list.filter(({ objectId }) => objectId !== id); - - this.loading = false; + return { pageData: data, totalCount: count }; } } @@ -103,18 +31,17 @@ export interface VerifiableData extends DataItem { } export abstract class VerifiableModel< - T extends VerifiableData = {}, - F = {} -> extends BaseModel { + T extends VerifiableData = VerifiableData, + F extends Filter = Filter +> extends BaseModel { + @toggle('uploading') async verify(id: string) { - this.loading = true; + await this.client.patch(this.baseURI + id, { verified: true }); - await service.patch(this.baseURI + id, { verified: true }); - - const item = this.list.find(({ objectId }) => objectId === id); - - (item.verified = true), (item.verifier = session.user); - - this.loading = false; + this.changeOne( + { verified: true, verifier: session.user } as Partial, + id, + true + ); } } diff --git a/source/model/Session.ts b/source/model/Session.ts index 969d6f9..19eebd1 100644 --- a/source/model/Session.ts +++ b/source/model/Session.ts @@ -1,50 +1,47 @@ import { observable } from 'mobx'; +import { BaseModel, persist, restore, toggle } from 'mobx-restful'; import { blobOf } from 'web-utility'; -import { User, service, RoleNames, FileData } from '../service'; +import { FileData, RoleNames, User, service } from '../service'; -export class Session { +export class Session extends BaseModel { + @persist() @observable - accessor user: User; + accessor user: User | undefined; constructor() { - if (sessionStorage.user) this.user = JSON.parse(sessionStorage.user); - else this.getProfile(); - } - - save(user?: User) { - this.user = user; - - if (user != null) sessionStorage.user = JSON.stringify(user); - else delete sessionStorage.user; - - return user; + super(); + restore(this, 'session').then(() => this.user || this.getProfile()); } + @toggle('downloading') async getProfile() { try { const { body } = await service.get('/session'); - return this.save(body); + return (this.user = body); } catch (error) { if (error.status !== 401) throw error; } } + @toggle('uploading') sendSMSCode(phone: string) { return service.post('/session/smsCode', { phone }); } + @toggle('uploading') async signIn(phone: string, code: string) { const { body } = await service.post('/session', { phone, code }); - return this.save(body); + return (this.user = body); } + @toggle('uploading') async signOut() { await service.delete('/session'); - this.save(null); + this.user = undefined; location.href = '.'; } @@ -53,6 +50,7 @@ export class Session { return this.user?.roles.includes(name); } + @toggle('uploading') async upload(file: Blob | string | URL, name?: string) { if (!(file instanceof Blob)) file = await blobOf(file + ''); @@ -62,10 +60,8 @@ export class Session { data.append('file', file); - const { - body: { url } - } = await service.post('/file', data); + const { body } = await service.post('/file', data); - return url; + return body.url; } } diff --git a/source/model/User.ts b/source/model/User.ts index 6eb5a79..533afc3 100644 --- a/source/model/User.ts +++ b/source/model/User.ts @@ -1,37 +1,44 @@ import { observable } from 'mobx'; +import { Filter, toggle } from 'mobx-restful'; -import { DataItem, RoleNames, User, service } from '../service'; +import { DataItem, RoleNames, User } from '../service'; import { BaseModel } from './BaseModel'; export interface Role extends DataItem { name: RoleNames; } -export class UserModel extends BaseModel { +export class UserModel extends BaseModel< + User, + Filter & { phone?: string } +> { baseURI = '/user/'; @observable - accessor roles: Role[]; + accessor roles: Role[] = []; + @toggle('downloading') async getRoles() { - const { body } = await service.get('/role'); + const { body } = await this.client.get('/role'); return (this.roles = body); } + @toggle('uploading') async addRole(uid: string, rid: string) { - await service.post(`${this.baseURI}${uid}/role/${rid}`); + await this.client.post(`${this.baseURI}${uid}/role/${rid}`); - const user = this.list.find(({ objectId }) => objectId === uid), + const user = this.allItems.find(({ objectId }) => objectId === uid), { name } = this.roles.find(({ objectId }) => objectId === rid); user.roles = user.roles.concat(name); } + @toggle('uploading') async removeRole(uid: string, rid: string) { - await service.delete(`${this.baseURI}${uid}/role/${rid}`); + await this.client.delete(`${this.baseURI}${uid}/role/${rid}`); - const user = this.list.find(({ objectId }) => objectId === uid), + const user = this.allItems.find(({ objectId }) => objectId === uid), { name } = this.roles.find(({ objectId }) => objectId === rid); user.roles = user.roles.filter(role => role !== name); diff --git a/source/page/Admin/User.tsx b/source/page/Admin/User.tsx index f4ecdbc..405bbb6 100644 --- a/source/page/Admin/User.tsx +++ b/source/page/Admin/User.tsx @@ -23,7 +23,7 @@ export default class UserAdmin extends HTMLElement implements CustomElement { } loadMore: TouchHandler = detail => { - if (detail === 'bottom') return user.getNextPage(this.filter); + if (detail === 'bottom') return user.getList(this.filter); }; search = (event: Event) => { @@ -32,10 +32,7 @@ export default class UserAdmin extends HTMLElement implements CustomElement { const { elements } = event.target as HTMLFormElement; const { value } = elements.item(0) as HTMLInputElement; - return user.getNextPage( - (this.filter = value ? { phone: value } : {}), - true - ); + return user.getList((this.filter = value ? { phone: value } : {}), 1); }; toggleRole(uid: string, rid: string, { target }: MouseEvent) { @@ -68,7 +65,7 @@ export default class UserAdmin extends HTMLElement implements CustomElement { ); render() { - const { loading, list, noMore } = user; + const { allItems, noMore } = user; return ( @@ -100,7 +97,7 @@ export default class UserAdmin extends HTMLElement implements CustomElement { 角色 - {list.map(this.renderItem)} + {allItems.map(this.renderItem)}

diff --git a/source/page/Clinic/Edit.tsx b/source/page/Clinic/Edit.tsx index 7878260..69373fb 100644 --- a/source/page/Clinic/Edit.tsx +++ b/source/page/Clinic/Edit.tsx @@ -60,9 +60,10 @@ export default class ClinicEdit const { contacts, ...data } = this.state; - await clinic.update( + await clinic.updateOne( { ...data, + // @ts-ignore contacts: contacts.filter( ({ name, phone }) => name?.trim() && phone?.trim() ) @@ -135,7 +136,7 @@ export default class ClinicEdit diff --git a/source/page/Donation/edit.tsx b/source/page/Donation/edit.tsx index 2a45011..05eb5d5 100644 --- a/source/page/Donation/edit.tsx +++ b/source/page/Donation/edit.tsx @@ -93,13 +93,15 @@ export default class DonationEdit const { accounts, contacts, ...data } = this.state; - await donationRecipient.update( + await donationRecipient.updateOne( { ...data, + // @ts-ignore accounts: accounts.filter( ({ name, number, bank }) => name?.trim() && number?.trim() && bank?.trim() ), + // @ts-ignore contacts: contacts.filter( ({ name, phone }) => name?.trim() && phone?.trim() ) @@ -194,7 +196,7 @@ export default class DonationEdit diff --git a/source/page/Factory/edit.tsx b/source/page/Factory/edit.tsx index 810ac51..0670b97 100644 --- a/source/page/Factory/edit.tsx +++ b/source/page/Factory/edit.tsx @@ -100,10 +100,12 @@ export default class FactoryEdit const { supplies, contacts, ...data } = this.state; - await factory.update( + await factory.updateOne( { ...data, + // @ts-ignore supplies: supplies.filter(({ count }) => count), + // @ts-ignore contacts: contacts.filter( ({ name, phone }) => name?.trim() && phone?.trim() ) @@ -184,7 +186,7 @@ export default class FactoryEdit diff --git a/source/page/Hospital/Edit.tsx b/source/page/Hospital/Edit.tsx index acd38fb..cf33aad 100644 --- a/source/page/Hospital/Edit.tsx +++ b/source/page/Hospital/Edit.tsx @@ -101,10 +101,12 @@ export default class HospitalEdit const { supplies, contacts, ...data } = this.state; - await suppliesRequirement.update( + await suppliesRequirement.updateOne( { ...data, + // @ts-ignore supplies: supplies.filter(({ count }) => count), + // @ts-ignore contacts: contacts.filter( ({ name, phone }) => name?.trim() && phone?.trim() ) @@ -178,7 +180,7 @@ export default class HospitalEdit diff --git a/source/page/Hotel/Edit.tsx b/source/page/Hotel/Edit.tsx index cd3b798..4069763 100644 --- a/source/page/Hotel/Edit.tsx +++ b/source/page/Hotel/Edit.tsx @@ -83,10 +83,11 @@ export default class HotelEdit extends HTMLElement implements WebCell name?.trim() && phone?.trim() ) @@ -163,7 +164,7 @@ export default class HotelEdit extends HTMLElement implements WebCell0} > 提交 diff --git a/source/page/Logistics/Edit.tsx b/source/page/Logistics/Edit.tsx index 99a612e..0a465e3 100644 --- a/source/page/Logistics/Edit.tsx +++ b/source/page/Logistics/Edit.tsx @@ -90,10 +90,12 @@ export default class LogisticsEdit const { serviceArea, contacts, ...data } = this.state; - await logistics.update( + await logistics.updateOne( { ...data, + // @ts-ignore serviceArea: serviceArea.filter(({ city }) => city?.trim()), + // @ts-ignore contacts: contacts.filter( ({ name, phone }) => name?.trim() && phone?.trim() ) @@ -208,7 +210,7 @@ export default class LogisticsEdit