Compare commits

...

33 Commits
5.3.38 ... dev

Author SHA1 Message Date
Pan Qiancheng 4929590c81 fix: 修复组件定义中的Aspect泛型使用Cxt 2026-01-12 10:57:38 +08:00
Xu Chang 567e0abc6a 5.3.46-dev 2026-01-09 15:56:14 +08:00
Xu Chang a26f464ba1 5.3.45-pub 2026-01-09 15:55:08 +08:00
Pan Qiancheng fdd24a8cc8 build 2026-01-09 15:27:08 +08:00
Pan Qiancheng fff9750b2a feat: upload相关全部统一化,并且完善了mp端的上传逻辑 2025-12-26 17:03:26 +08:00
Pan Qiancheng f16b3f91a8 fix: 改为抛出AbortError 2025-12-26 14:44:59 +08:00
Pan Qiancheng 6e48eb02b4 fix: 修复中断上传的部分问题 2025-12-26 13:50:07 +08:00
Pan Qiancheng b07f084341 fix: 修复upload中的部分问题,以支持分片上传 2025-12-26 12:19:58 +08:00
Xu Chang 96b8cd9366 5.3.45-dev 2025-12-26 11:10:45 +08:00
Xu Chang 363b329070 5.3.44-pub 2025-12-26 11:09:41 +08:00
Xu Chang 51cc8786a0 Merge branch 'dev' of gitea.51mars.com:Oak-Team/oak-frontend-base into dev 2025-12-26 11:02:29 +08:00
Xu Chang ea01036c5a runningTree在直接execute某个action时,简化了原先的行为 2025-12-26 11:02:21 +08:00
lxy 71f385f0f4 fix: 分页组件total为undefined时不显示 2025-12-17 16:37:28 +08:00
Xu Chang 91bf1da808 5.3.44-dev 2025-11-15 21:50:33 +08:00
Xu Chang 81b12efe6e 5.3.43-pub 2025-11-15 21:48:40 +08:00
Xu Chang 40a98dd51a subscriber在调用回调之前,应该先同步cache中的数据 2025-11-15 21:45:38 +08:00
Xu Chang 94dc93868c 5.3.43-dev 2025-11-12 13:55:00 +08:00
Xu Chang a9a358727b 5.3.42-pub 2025-11-12 13:53:52 +08:00
Xu Chang b8e3dcd24a Merge branch 'dev' of gitea.51mars.com:Oak-Team/oak-frontend-base into dev 2025-11-12 12:56:34 +08:00
Xu Chang 3ca9d51d4d navigator的代码有边界问题 2025-11-12 12:56:28 +08:00
lxy 73de8a74c0 fix: uploadFile方法传入参数保持一致 2025-11-07 10:02:34 +08:00
Xu Chang 5c710ef019 5.3.42-dev 2025-11-06 18:04:56 +08:00
Xu Chang 2e214df7fd 5.3.41-pub 2025-11-06 17:56:38 +08:00
lxy 626b40b4e3 fix: 修正小程序props为null的情况 2025-11-04 18:26:14 +08:00
Xu Chang 39ee45fee2 5.3.41-dev 2025-10-23 12:04:47 +08:00
Xu Chang 306c0a75ef 5.3.40-pub 2025-10-23 12:03:53 +08:00
Xu Chang 5a3d06d037 runningTree的listNode,在处理update行为时原来的代码有问题 2025-10-22 17:51:42 +08:00
Xu Chang e4d8ed5e03 5.3.40-dev 2025-10-21 17:20:17 +08:00
Xu Chang 9541e67508 5.3.39-pub 2025-10-21 17:19:00 +08:00
Xu Chang 285c872822 Merge branch 'dev' of gitea.51mars.com:Oak-Team/oak-frontend-base into dev 2025-10-21 13:37:22 +08:00
xzf f52d28a4e5 upload.ts 定义中断文件相关函数 2025-10-21 11:54:30 +08:00
xzf 9d71fd9c30 extraFile实现中断文件上传 2025-10-21 11:29:06 +08:00
Xu Chang 90a2125ddb 5.3.39-dev 2025-10-16 09:22:29 +08:00
62 changed files with 1198 additions and 404 deletions

View File

@ -19,7 +19,7 @@ MiddleSelect.Option = Select.Option;
export default function Render(props) { export default function Render(props) {
const { style, className, oakPagination, oakFullpath, newTotal, showQuickJumper, showSizeChanger, size, showTotal, showTitle, } = props.data; const { style, className, oakPagination, oakFullpath, newTotal, showQuickJumper, showSizeChanger, size, showTotal, showTitle, } = props.data;
const { t, setPageSize, setCurrentPage } = props.methods; const { t, setPageSize, setCurrentPage } = props.methods;
const { pageSize, total, currentPage, more, count, getTotal } = oakPagination || {}; const { pageSize = 0, total = 0, currentPage = 1, more, count, getTotal } = oakPagination || {};
const paginationRef = useRef(null); const paginationRef = useRef(null);
const [internalInputVal, setInternalInputVal] = useState(currentPage); const [internalInputVal, setInternalInputVal] = useState(currentPage);
useEffect(() => { useEffect(() => {
@ -28,7 +28,7 @@ export default function Render(props) {
if (!oakPagination) { if (!oakPagination) {
return null; return null;
} }
if (total === 0) { if (!total) {
return null; return null;
} }
const prefixCls = 'rc-pagination'; const prefixCls = 'rc-pagination';

View File

@ -75,13 +75,12 @@ export class Navigator extends CommonNavigator {
wx.navigateTo({ wx.navigateTo({
url: url, url: url,
success: () => { success: () => {
this.leave();
this.publish(); this.publish();
resolve(undefined); resolve(undefined);
}, },
fail: (err) => reject(err), fail: (err) => reject(err),
}); });
this.publish();
this.leave();
}); });
} }
// 关闭当前页面跳转到应用内的某个页面但不允许跳转到tabBar页面。 // 关闭当前页面跳转到应用内的某个页面但不允许跳转到tabBar页面。
@ -94,12 +93,12 @@ export class Navigator extends CommonNavigator {
wx.redirectTo({ wx.redirectTo({
url: url, url: url,
success: () => { success: () => {
resolve(undefined); this.leave();
this.publish(); this.publish();
resolve(undefined);
}, },
fail: (err) => reject(err), fail: (err) => reject(err),
}); });
this.leave();
}); });
} }
//跳转到tabBar页面并关闭其他所有非tabBar页面用于跳转到主页。 //跳转到tabBar页面并关闭其他所有非tabBar页面用于跳转到主页。
@ -112,12 +111,12 @@ export class Navigator extends CommonNavigator {
wx.switchTab({ wx.switchTab({
url: url, url: url,
success: () => { success: () => {
resolve(undefined); this.leave();
this.publish(); this.publish();
resolve(undefined);
}, },
fail: (err) => reject(err), fail: (err) => reject(err),
}); });
this.leave();
}); });
} }
navigateBack(delta) { navigateBack(delta) {
@ -128,29 +127,23 @@ export class Navigator extends CommonNavigator {
wx.navigateBack({ wx.navigateBack({
delta: delta || 1, delta: delta || 1,
success: () => { success: () => {
resolve(undefined); this.leave();
this.publish(); this.publish();
resolve(undefined);
}, },
fail: (err) => reject(err), fail: (err) => reject(err),
}); });
this.leave();
}); });
} }
navigateBackOrRedirectTo(options, state, disableNamespace) { navigateBackOrRedirectTo(options, state, disableNamespace) {
if (!this.enter()) {
return;
}
const pages = getCurrentPages(); const pages = getCurrentPages();
if (pages.length > 1) { if (pages.length > 1) {
this.leave();
return this.navigateBack(); return this.navigateBack();
} }
const isTabBar = options?.isTabBar; const isTabBar = options?.isTabBar;
if (isTabBar) { if (isTabBar) {
this.leave();
return this.switchTab(options, state, disableNamespace); return this.switchTab(options, state, disableNamespace);
} }
this.leave();
return this.redirectTo(options, state, disableNamespace); return this.redirectTo(options, state, disableNamespace);
} }
} }

View File

@ -56,8 +56,8 @@ export class Navigator extends CommonNavigator {
const { url, props } = this.getUrlAndProps(options, state, disableNamespace); const { url, props } = this.getUrlAndProps(options, state, disableNamespace);
const replaceAction = StackActions.replace(url, props); const replaceAction = StackActions.replace(url, props);
this.history.dispatch(replaceAction); this.history.dispatch(replaceAction);
this.publish();
this.leave(); this.leave();
this.publish();
} }
async switchTab(options, state, disableNamespace) { async switchTab(options, state, disableNamespace) {
if (!this.enter()) { if (!this.enter()) {
@ -66,8 +66,8 @@ export class Navigator extends CommonNavigator {
const { url, props } = this.getUrlAndProps(options, state, disableNamespace); const { url, props } = this.getUrlAndProps(options, state, disableNamespace);
const jumpToAction = TabActions.jumpTo(url, props); const jumpToAction = TabActions.jumpTo(url, props);
this.history.dispatch(jumpToAction); this.history.dispatch(jumpToAction);
this.publish();
this.leave(); this.leave();
this.publish();
} }
async navigateBack(delta) { async navigateBack(delta) {
if (!this.enter()) { if (!this.enter()) {
@ -77,9 +77,12 @@ export class Navigator extends CommonNavigator {
if (canGoBack) { if (canGoBack) {
const popAction = StackActions.pop(delta || 1); const popAction = StackActions.pop(delta || 1);
this.history.dispatch(popAction); this.history.dispatch(popAction);
this.leave();
this.publish(); this.publish();
} }
this.leave(); else {
this.leave();
}
} }
navigateBackOrRedirectTo(options, state, disableNamespace) { navigateBackOrRedirectTo(options, state, disableNamespace) {
if (!this.enter()) { if (!this.enter()) {
@ -88,13 +91,13 @@ export class Navigator extends CommonNavigator {
const canGoBack = this.history.canGoBack(); const canGoBack = this.history.canGoBack();
if (canGoBack) { if (canGoBack) {
this.navigateBack(); this.navigateBack();
this.publish();
this.leave(); this.leave();
this.publish();
return; return;
} }
// 回最顶层 // 回最顶层
this.history.dispatch(StackActions.popToTop()); this.history.dispatch(StackActions.popToTop());
this.publish();
this.leave(); this.leave();
this.publish();
} }
} }

View File

@ -45,8 +45,8 @@ export class Navigator extends CommonNavigator {
} }
const { url, props } = this.getUrlAndProps(options, state, disableNamespace); const { url, props } = this.getUrlAndProps(options, state, disableNamespace);
this.history.push(url, props); this.history.push(url, props);
this.publish();
this.leave(); this.leave();
this.publish();
} }
async redirectTo(options, state, disableNamespace) { async redirectTo(options, state, disableNamespace) {
if (!this.enter()) { if (!this.enter()) {
@ -54,8 +54,8 @@ export class Navigator extends CommonNavigator {
} }
const { url, props } = this.getUrlAndProps(options, state, disableNamespace); const { url, props } = this.getUrlAndProps(options, state, disableNamespace);
this.history.replace(url, props); this.history.replace(url, props);
this.publish();
this.leave(); this.leave();
this.publish();
} }
async switchTab(options, state, disableNamespace) { async switchTab(options, state, disableNamespace) {
console.error('浏览器无switchTab'); console.error('浏览器无switchTab');
@ -68,8 +68,8 @@ export class Navigator extends CommonNavigator {
return; return;
} }
this.history.go(delta ? 0 - delta : -1); this.history.go(delta ? 0 - delta : -1);
this.publish();
this.leave(); this.leave();
this.publish();
} }
navigateBackOrRedirectTo(options, state, disableNamespace) { navigateBackOrRedirectTo(options, state, disableNamespace) {
console.error('浏览器暂无法获得history堆栈'); console.error('浏览器暂无法获得history堆栈');

View File

@ -233,7 +233,7 @@ declare class SingleNode<ED extends EntityDict & BaseEntityDict, T extends keyof
saveRefreshResult(data: Record<string, any>): void; saveRefreshResult(data: Record<string, any>): void;
refresh(): Promise<void>; refresh(): Promise<void>;
clean(lsn?: number, dontPublish?: true): void; clean(lsn?: number, dontPublish?: true): void;
private getFilter; getFilter(ignoreNew?: true, onlyHot?: true): ED[T]['Filter'] | undefined;
getIntrinsticFilters(): ED[T]["Filter"] | undefined; getIntrinsticFilters(): ED[T]["Filter"] | undefined;
/** /**
* getParentFilter不能假设一定已经有数据filter的条件去构造 * getParentFilter不能假设一定已经有数据filter的条件去构造

View File

@ -5,7 +5,7 @@ import { createOperationsFromModies } from 'oak-domain/lib/store/modi';
import { judgeRelation } from "oak-domain/lib/store/relation"; import { judgeRelation } from "oak-domain/lib/store/relation";
import { CreateAtAttribute, UpdateAtAttribute, DeleteAtAttribute } from "oak-domain/lib/types"; import { CreateAtAttribute, UpdateAtAttribute, DeleteAtAttribute } from "oak-domain/lib/types";
import { Feature } from '../types/Feature'; import { Feature } from '../types/Feature';
import { generateNewId } from 'oak-domain/lib/utils/uuid'; import { generateNewId, generateNewIdAsync } from 'oak-domain/lib/utils/uuid';
export const MODI_NEXT_PATH_SUFFIX = ':next'; export const MODI_NEXT_PATH_SUFFIX = ':next';
const START_LSN = 100; const START_LSN = 100;
function mergeOperation(schema, entity, oper1, oper2) { function mergeOperation(schema, entity, oper1, oper2) {
@ -537,14 +537,14 @@ class ListNode extends EntityNode {
/** /**
* 以及原本不在的现在是不是满足条件了 * 以及原本不在的现在是不是满足条件了
* 后一种情况也可能原本就满足但不在当前的ids中这时是判断不出来的total+1可能不对但是应该是较少的case*/ * 后一种情况也可能原本就满足但不在当前的ids中这时是判断不出来的total+1可能不对但是应该是较少的case*/
const filter = this.constructFilters(true, true, true); const filters = this.constructFilters(true, true, true);
if (intersected.length) { if (intersected.length) {
const rows = this.cache.get(this.entity, { const rows = this.cache.get(this.entity, {
data: { data: {
id: 1, id: 1,
}, },
filter: combineFilters(this.entity, this.schema, [ filter: combineFilters(this.entity, this.schema, [
...(filter || []), ...(filters || []),
{ {
id: { id: {
$in: intersected, $in: intersected,
@ -562,12 +562,8 @@ class ListNode extends EntityNode {
}); });
} }
if (diffed.length) { if (diffed.length) {
diffed.forEach((ele) => { const filter = filters && combineFilters(e, this.schema, filters);
this.sr[ele] = {}; diffed.forEach((ele) => tryAddRowToList(ele, filter));
if (this.pagination.total) {
this.pagination.total++;
}
});
} }
} }
// hasUpdated = true; // hasUpdated = true;
@ -1256,7 +1252,7 @@ class SingleNode extends EntityNode {
setId(id) { setId(id) {
if (id !== this.id) { if (id !== this.id) {
const operations = this.ulManager.makeOperations(); const operations = this.ulManager.makeOperations();
assert(operations.length <= 1); assert(operations.length <= 1, 'singleNode在setId时出现数据不一致');
const [operation] = operations; const [operation] = operations;
if (operation?.action === 'create') { if (operation?.action === 'create') {
if (operation.data.id === id) { if (operation.data.id === id) {
@ -1417,7 +1413,7 @@ class SingleNode extends EntityNode {
} }
setDirty() { setDirty() {
const id = this.getId(); const id = this.getId();
assert(id); assert(id, "不能对没有id的singleNode设置dirty");
this.ulManager.push(this.ulManager.maxLsn, { this.ulManager.push(this.ulManager.maxLsn, {
action: 'update', action: 'update',
data: {}, data: {},
@ -1445,7 +1441,7 @@ class SingleNode extends EntityNode {
const childOperations = child.composeOperations(paths?.length ? paths : undefined); const childOperations = child.composeOperations(paths?.length ? paths : undefined);
if (childOperations) { if (childOperations) {
if (child instanceof SingleNode) { if (child instanceof SingleNode) {
assert(childOperations.length === 1); assert(childOperations.length === 1, 'singleNode在composeOperations时出现数据不一致');
this.ulManager.push(lsnMax + 100, { this.ulManager.push(lsnMax + 100, {
action: 'update', action: 'update',
data: { data: {
@ -1457,7 +1453,7 @@ class SingleNode extends EntityNode {
}); });
} }
else { else {
assert(child instanceof ListNode); assert(child instanceof ListNode, 'child必须是ListNode');
this.ulManager.push(lsnMax + 100, { this.ulManager.push(lsnMax + 100, {
action: 'update', action: 'update',
data: { data: {
@ -1547,8 +1543,8 @@ class SingleNode extends EntityNode {
const rel = this.judgeRelation(k2); const rel = this.judgeRelation(k2);
if (rel === 2) { if (rel === 2) {
if (value?.entityId) { if (value?.entityId) {
assert(child instanceof SingleNode); assert(child instanceof SingleNode, 'child必须是singleNode');
assert(value.entity === child.getEntity()); assert(value.entity === child.getEntity(), 'singleNode的entity必须一致');
child.saveRefreshResult({ child.saveRefreshResult({
[value.entityId]: this.sr[k2] || {}, [value.entityId]: this.sr[k2] || {},
}); });
@ -2011,6 +2007,7 @@ export class RunningTree extends Feature {
assert(!parentNode || parentNode instanceof VirtualNode); assert(!parentNode || parentNode instanceof VirtualNode);
node = new VirtualNode(fullPath, path, parentNode, stale); node = new VirtualNode(fullPath, path, parentNode, stale);
} }
// 任何临时的修改都需要回滚上面缓存了一些update操作
rollback(); rollback();
if (!parentNode) { if (!parentNode) {
assert(!parent && !this.root[path]); assert(!parent && !this.root[path]);
@ -2417,7 +2414,7 @@ export class RunningTree extends Feature {
const node = path && this.findNode(path); const node = path && this.findNode(path);
// assert(node.isDirty()); // assert(node.isDirty());
node && node.setExecuting(true); node && node.setExecuting(true);
let pollute = false; // let pollute = false;
try { try {
let operations = path && this.getOperations(path) || []; let operations = path && this.getOperations(path) || [];
if (opers) { if (opers) {
@ -2436,15 +2433,30 @@ export class RunningTree extends Feature {
else if (node) { else if (node) {
// 老的写法直接对一个非脏的结点execute某个action也可以支持 // 老的写法直接对一个非脏的结点execute某个action也可以支持
assert(node instanceof SingleNode); assert(node instanceof SingleNode);
node.update(this.logSerailNumber, {}, action); // node.update(this.logSerailNumber, {}, action);
pollute = true; // pollute = true;
operations = node.composeOperations() || []; // operations = node.composeOperations() || [];
assert(operations.length === 1); // assert(operations.length === 1);
const [operation1] = operations; // const [operation1] = operations;
if (action !== operation1.operation.action) { // if (action !== operation1.operation.action) {
assert(operation1.operation.action === 'update'); // 如果execute时传action前面update动作应该只可能是update // assert(operation1.operation.action === 'update'); // 如果execute时传action前面update动作应该只可能是update
operation1.operation.action = action; // operation1.operation.action = action;
} // }
/**
* 上述写法会触发publish行为一个close动作会导致页面像下面这样渲染
* opened --> closed(上面的node.update) --> opened(执行成功回来setExecuting时) --> closed执行成功回来再sync cache后
*/
operations.push({
entity: node.getEntity(),
operation: {
id: await generateNewIdAsync(),
action,
data: {
$$updateAt$$: Date.now(),
},
filter: node.getFilter(true),
}
});
} }
} }
if (operations.length > 0) { if (operations.length > 0) {
@ -2474,9 +2486,9 @@ export class RunningTree extends Feature {
} }
catch (err) { catch (err) {
node && node.setExecuting(false); node && node.setExecuting(false);
if (pollute) { /* if (pollute) {
path && this.clean(path); path && this.clean(path);
} } */
throw err; throw err;
} }
} }

View File

@ -94,11 +94,11 @@ export class SubScriber extends Feature {
} }
}); });
socket.on('data', (opRecords, event) => { socket.on('data', (opRecords, event) => {
this.cache.sync(opRecords);
const registered = this.eventMap[event]; const registered = this.eventMap[event];
if (registered) { if (registered) {
registered.callbacks.forEach((ele) => ele(event, opRecords)); registered.callbacks.forEach((ele) => ele(event, opRecords));
} }
this.cache.sync(opRecords);
}); });
socket.on('error', (errString) => { socket.on('error', (errString) => {
console.error(errString); console.error(errString);

2
es/page.mp.d.ts vendored
View File

@ -5,4 +5,4 @@ import { Feature } from './types/Feature';
import { DataOption, OakComponentOption } from './types/Page'; import { DataOption, OakComponentOption } from './types/Page';
import { AsyncContext } from 'oak-domain/lib/store/AsyncRowStore'; import { AsyncContext } from 'oak-domain/lib/store/AsyncRowStore';
import { SyncContext } from 'oak-domain/lib/store/SyncRowStore'; import { SyncContext } from 'oak-domain/lib/store/SyncRowStore';
export declare function createComponent<IsList extends boolean, ED extends EntityDict & BaseEntityDict, T extends keyof ED, Cxt extends AsyncContext<ED>, FrontCxt extends SyncContext<ED>, AD extends Record<string, Aspect<ED, AsyncContext<ED>>>, FD extends Record<string, Feature>, FormedData extends Record<string, any>, TData extends DataOption = {}, TProperty extends DataOption = {}, TMethod extends Record<string, Function> = {}>(option: OakComponentOption<IsList, ED, T, Cxt, FrontCxt, AD, FD, FormedData, TData, TProperty, TMethod>, features: BasicFeatures<ED> & FD): string; export declare function createComponent<IsList extends boolean, ED extends EntityDict & BaseEntityDict, T extends keyof ED, Cxt extends AsyncContext<ED>, FrontCxt extends SyncContext<ED>, AD extends Record<string, Aspect<ED, Cxt>>, FD extends Record<string, Feature>, FormedData extends Record<string, any>, TData extends DataOption = {}, TProperty extends DataOption = {}, TMethod extends Record<string, Function> = {}>(option: OakComponentOption<IsList, ED, T, Cxt, FrontCxt, AD, FD, FormedData, TData, TProperty, TMethod>, features: BasicFeatures<ED> & FD): string;

View File

@ -540,6 +540,13 @@ function translatePropertiesToPropertyDefinitions(properties) {
const definitions = {}; const definitions = {};
if (properties) { if (properties) {
Object.keys(properties).forEach((prop) => { Object.keys(properties).forEach((prop) => {
if (properties[prop] === null) {
definitions[prop] = {
type: null,
value: null,
};
return;
}
switch (typeof properties[prop]) { switch (typeof properties[prop]) {
case 'string': { case 'string': {
if (properties[prop]) { if (properties[prop]) {

2
es/page.native.d.ts vendored
View File

@ -6,4 +6,4 @@ import { Feature } from './types/Feature';
import { DataOption, OakComponentOption } from './types/Page'; import { DataOption, OakComponentOption } from './types/Page';
import { SyncContext } from 'oak-domain/lib/store/SyncRowStore'; import { SyncContext } from 'oak-domain/lib/store/SyncRowStore';
import { AsyncContext } from 'oak-domain/lib/store/AsyncRowStore'; import { AsyncContext } from 'oak-domain/lib/store/AsyncRowStore';
export declare function createComponent<IsList extends boolean, ED extends EntityDict & BaseEntityDict, T extends keyof ED, Cxt extends AsyncContext<ED>, FrontCxt extends SyncContext<ED>, AD extends Record<string, Aspect<ED, AsyncContext<ED>>>, FD extends Record<string, Feature>, FormedData extends Record<string, any>, TData extends Record<string, any> = {}, TProperty extends DataOption = {}, TMethod extends Record<string, Function> = {}>(option: OakComponentOption<IsList, ED, T, Cxt, FrontCxt, AD, FD, FormedData, TData, TProperty, TMethod>, features: BasicFeatures<ED> & FD): React.ForwardRefExoticComponent<React.RefAttributes<unknown>>; export declare function createComponent<IsList extends boolean, ED extends EntityDict & BaseEntityDict, T extends keyof ED, Cxt extends AsyncContext<ED>, FrontCxt extends SyncContext<ED>, AD extends Record<string, Aspect<ED, Cxt>>, FD extends Record<string, Feature>, FormedData extends Record<string, any>, TData extends Record<string, any> = {}, TProperty extends DataOption = {}, TMethod extends Record<string, Function> = {}>(option: OakComponentOption<IsList, ED, T, Cxt, FrontCxt, AD, FD, FormedData, TData, TProperty, TMethod>, features: BasicFeatures<ED> & FD): React.ForwardRefExoticComponent<React.RefAttributes<unknown>>;

2
es/page.react.d.ts vendored
View File

@ -9,7 +9,7 @@ import { MessageProps } from './types/Message';
import { NotificationProps } from './types/Notification'; import { NotificationProps } from './types/Notification';
import { SyncContext } from 'oak-domain/lib/store/SyncRowStore'; import { SyncContext } from 'oak-domain/lib/store/SyncRowStore';
import { AsyncContext } from 'oak-domain/lib/store/AsyncRowStore'; import { AsyncContext } from 'oak-domain/lib/store/AsyncRowStore';
export declare function createComponent<IsList extends boolean, ED extends EntityDict & BaseEntityDict, T extends keyof ED, Cxt extends AsyncContext<ED>, FrontCxt extends SyncContext<ED>, AD extends Record<string, Aspect<ED, AsyncContext<ED>>>, FD extends Record<string, Feature>, FormedData extends Record<string, any>, TData extends Record<string, any> = {}, TProperty extends DataOption = {}, TMethod extends Record<string, Function> = {}>(option: OakComponentOption<IsList, ED, T, Cxt, FrontCxt, AD, FD, FormedData, TData, TProperty, TMethod>, features: BasicFeatures<ED> & FD): { export declare function createComponent<IsList extends boolean, ED extends EntityDict & BaseEntityDict, T extends keyof ED, Cxt extends AsyncContext<ED>, FrontCxt extends SyncContext<ED>, AD extends Record<string, Aspect<ED, Cxt>>, FD extends Record<string, Feature>, FormedData extends Record<string, any>, TData extends Record<string, any> = {}, TProperty extends DataOption = {}, TMethod extends Record<string, Function> = {}>(option: OakComponentOption<IsList, ED, T, Cxt, FrontCxt, AD, FD, FormedData, TData, TProperty, TMethod>, features: BasicFeatures<ED> & FD): {
new (props: ComponentProps<ED, T, TProperty>): { new (props: ComponentProps<ED, T, TProperty>): {
features: BasicFeatures<ED> & FD; features: BasicFeatures<ED> & FD;
oakOption: OakComponentOption<IsList, ED, T, Cxt, FrontCxt, AD, FD, FormedData, TData, TProperty, TMethod>; oakOption: OakComponentOption<IsList, ED, T, Cxt, FrontCxt, AD, FD, FormedData, TData, TProperty, TMethod>;

2
es/page.web.d.ts vendored
View File

@ -6,4 +6,4 @@ import { Feature } from './types/Feature';
import { DataOption, OakComponentOption } from './types/Page'; import { DataOption, OakComponentOption } from './types/Page';
import { SyncContext } from 'oak-domain/lib/store/SyncRowStore'; import { SyncContext } from 'oak-domain/lib/store/SyncRowStore';
import { AsyncContext } from 'oak-domain/lib/store/AsyncRowStore'; import { AsyncContext } from 'oak-domain/lib/store/AsyncRowStore';
export declare function createComponent<IsList extends boolean, ED extends EntityDict & BaseEntityDict, T extends keyof ED, Cxt extends AsyncContext<ED>, FrontCxt extends SyncContext<ED>, AD extends Record<string, Aspect<ED, AsyncContext<ED>>>, FD extends Record<string, Feature>, FormedData extends Record<string, any>, TData extends Record<string, any> = {}, TProperty extends DataOption = {}, TMethod extends Record<string, Function> = {}>(option: OakComponentOption<IsList, ED, T, Cxt, FrontCxt, AD, FD, FormedData, TData, TProperty, TMethod>, features: BasicFeatures<ED> & FD): React.ForwardRefExoticComponent<React.RefAttributes<unknown>>; export declare function createComponent<IsList extends boolean, ED extends EntityDict & BaseEntityDict, T extends keyof ED, Cxt extends AsyncContext<ED>, FrontCxt extends SyncContext<ED>, AD extends Record<string, Aspect<ED, Cxt>>, FD extends Record<string, Feature>, FormedData extends Record<string, any>, TData extends Record<string, any> = {}, TProperty extends DataOption = {}, TMethod extends Record<string, Function> = {}>(option: OakComponentOption<IsList, ED, T, Cxt, FrontCxt, AD, FD, FormedData, TData, TProperty, TMethod>, features: BasicFeatures<ED> & FD): React.ForwardRefExoticComponent<React.RefAttributes<unknown>>;

4
es/types/Page.d.ts vendored
View File

@ -146,7 +146,7 @@ export type ComponentFullThisType<ED extends EntityDict & BaseEntityDict, T exte
triggerEvent: <DetailType = any>(name: string, detail?: DetailType, options?: WechatMiniprogram.Component.TriggerEventOption) => void; triggerEvent: <DetailType = any>(name: string, detail?: DetailType, options?: WechatMiniprogram.Component.TriggerEventOption) => void;
oakLifetime: OakLifetime; oakLifetime: OakLifetime;
} & OakCommonComponentMethods<ED, T> & OakListComponentMethods<ED, T> & OakSingleComponentMethods<ED, T>; } & OakCommonComponentMethods<ED, T> & OakListComponentMethods<ED, T> & OakSingleComponentMethods<ED, T>;
export type OakComponentOption<IsList extends boolean, ED extends EntityDict & BaseEntityDict, T extends keyof ED, Cxt extends AsyncContext<ED>, FrontCxt extends SyncContext<ED>, AD extends Record<string, Aspect<ED, AsyncContext<ED>>>, FD extends Record<string, Feature>, FormedData extends Record<string, any>, TData extends Record<string, any>, TProperty extends DataOption, TMethod extends Record<string, Function>, EMethod extends Record<string, Function> = {}> = ComponentOption<IsList, ED, T, Cxt, FrontCxt, AD, FD, FormedData, TData, TProperty, TMethod, EMethod> & Partial<{ export type OakComponentOption<IsList extends boolean, ED extends EntityDict & BaseEntityDict, T extends keyof ED, Cxt extends AsyncContext<ED>, FrontCxt extends SyncContext<ED>, AD extends Record<string, Aspect<ED, Cxt>>, FD extends Record<string, Feature>, FormedData extends Record<string, any>, TData extends Record<string, any>, TProperty extends DataOption, TMethod extends Record<string, Function>, EMethod extends Record<string, Function> = {}> = ComponentOption<IsList, ED, T, Cxt, FrontCxt, AD, FD, FormedData, TData, TProperty, TMethod, EMethod> & Partial<{
/** /**
* *
*/ */
@ -351,7 +351,7 @@ export type OakComponentData<ED extends EntityDict & BaseEntityDict, T extends k
type OakListComoponetData<ED extends EntityDict & BaseEntityDict, T extends keyof ED> = { type OakListComoponetData<ED extends EntityDict & BaseEntityDict, T extends keyof ED> = {
oakPagination?: Pagination; oakPagination?: Pagination;
}; };
export type MakeOakComponent<ED extends EntityDict & BaseEntityDict, Cxt extends AsyncContext<ED>, FrontCxt extends SyncContext<ED>, AD extends Record<string, Aspect<ED, AsyncContext<ED>>>, FD extends Record<string, Feature>> = <IsList extends boolean, T extends keyof ED, FormedData extends DataOption, TData extends DataOption, TProperty extends DataOption, TMethod extends MethodOption>(options: OakComponentOption<IsList, ED, T, Cxt, FrontCxt, AD, FD, FormedData, TData, TProperty, TMethod>) => (props: ReactComponentProps<ED, T, IsList, TProperty>) => React.ReactElement; export type MakeOakComponent<ED extends EntityDict & BaseEntityDict, Cxt extends AsyncContext<ED>, FrontCxt extends SyncContext<ED>, AD extends Record<string, Aspect<ED, Cxt>>, FD extends Record<string, Feature>> = <IsList extends boolean, T extends keyof ED, FormedData extends DataOption, TData extends DataOption, TProperty extends DataOption, TMethod extends MethodOption>(options: OakComponentOption<IsList, ED, T, Cxt, FrontCxt, AD, FD, FormedData, TData, TProperty, TMethod>) => (props: ReactComponentProps<ED, T, IsList, TProperty>) => React.ReactElement;
export type WebComponentCommonMethodNames = 'setNotification' | 'setMessage' | 'navigateTo' | 'navigateBack' | 'redirectTo' | 'clean' | 't' | 'execute' | 'refresh' | 'aggregate' | 'checkOperation' | 'isDirty'; export type WebComponentCommonMethodNames = 'setNotification' | 'setMessage' | 'navigateTo' | 'navigateBack' | 'redirectTo' | 'clean' | 't' | 'execute' | 'refresh' | 'aggregate' | 'checkOperation' | 'isDirty';
export type WebComponentListMethodNames = 'loadMore' | 'setFilters' | 'addNamedFilter' | 'removeNamedFilter' | 'removeNamedFilterByName' | 'setNamedSorters' | 'addNamedSorter' | 'removeNamedSorter' | 'removeNamedSorterByName' | 'setPageSize' | 'setCurrentPage' | 'addItem' | 'addItems' | 'removeItem' | 'removeItems' | 'updateItem' | 'updateItems' | 'resetItem' | 'recoverItem' | 'recoverItems'; export type WebComponentListMethodNames = 'loadMore' | 'setFilters' | 'addNamedFilter' | 'removeNamedFilter' | 'removeNamedFilterByName' | 'setNamedSorters' | 'addNamedSorter' | 'removeNamedSorter' | 'removeNamedSorterByName' | 'setPageSize' | 'setCurrentPage' | 'addItem' | 'addItems' | 'removeItem' | 'removeItems' | 'updateItem' | 'updateItems' | 'resetItem' | 'recoverItem' | 'recoverItems';
export type WebComponentSingleMethodNames = 'update' | 'remove' | 'create' | 'isCreation' | 'getId' | 'setId'; export type WebComponentSingleMethodNames = 'update' | 'remove' | 'create' | 'isCreation' | 'getId' | 'setId';

30
es/types/Upload.d.ts vendored Normal file
View File

@ -0,0 +1,30 @@
type UploadFileFn = (options: {
file: string | File | Blob;
name: string;
uploadUrl: string;
formData: Record<string, any>;
autoInform?: boolean;
getPercent?: Function;
uploadId?: string;
method?: "POST" | "PUT" | "PATCH";
isFilePath?: boolean;
}) => Promise<{
status: number;
statusText: string;
statusCode?: number;
headers: {
get(name: string): string | null;
};
json(): Promise<any>;
text(): Promise<string>;
errMsg?: string;
data?: any;
}>;
export interface UploadInterface {
uploadFile: UploadFileFn;
abortUpload(uploadId: string): boolean;
abortAllUploads(): void;
getUploadStatus(uploadId: string): 'uploading' | 'completed' | 'aborted' | 'not-found';
getActiveUploads(): string[];
}
export {};

1
es/types/Upload.js Normal file
View File

@ -0,0 +1 @@
export {};

22
es/utils/upload.d.ts vendored
View File

@ -1,3 +1,21 @@
export declare class Upload { import { UploadInterface } from "../types/Upload";
uploadFile(file: string | File, name: string, uploadUrl: string, formData: Record<string, any>, autoInform?: boolean): Promise<any>; export declare class Upload implements UploadInterface {
uploadFile(options: {
file: string | File | Blob;
name: string;
uploadUrl: string;
formData: Record<string, any>;
autoInform?: boolean;
getPercent?: Function;
uploadId?: string;
method?: "POST" | "PUT" | "PATCH";
isFilePath?: boolean;
}): Promise<any>;
private controllers;
constructor();
abortUpload(uploadId: string): boolean;
abortAllUploads(): void;
getUploadStatus(uploadId: string): 'uploading' | 'completed' | 'aborted' | 'not-found';
private generateUploadId;
getActiveUploads(): string[];
} }

View File

@ -1,5 +1,35 @@
export class Upload { export class Upload {
async uploadFile(file, name, uploadUrl, formData, autoInform) { async uploadFile(options) {
console.warn('server不会调用此函数'); console.warn('server不会调用此函数');
} }
controllers = new Map();
constructor() {
this.uploadFile = this.uploadFile.bind(this);
this.abortUpload = this.abortUpload.bind(this);
this.abortAllUploads = this.abortAllUploads.bind(this);
this.getUploadStatus = this.getUploadStatus.bind(this);
this.getActiveUploads = this.getActiveUploads.bind(this);
}
// 中断特定上传
abortUpload(uploadId) {
console.warn('server不会调用此函数');
return false;
}
// 中断所有上传
abortAllUploads() {
console.warn('server不会调用此函数');
}
// 获取上传状态
getUploadStatus(uploadId) {
console.warn('server不会调用此函数');
return 'uploading';
}
// 生成唯一的上传ID
generateUploadId(file, uploadUrl) {
return `server不会调用此函数`;
}
// 获取所有进行中的上传任务
getActiveUploads() {
return [];
}
} }

View File

@ -1,3 +1,18 @@
export declare class Upload { import { UploadInterface } from '../types/Upload';
uploadFile(file: string | File, name: string, uploadUrl: string, formData: Record<string, any>, autoInform?: boolean, getPercent?: Function, method?: "POST" | "PUT" | "PATCH"): Promise<any>; export declare class Upload implements UploadInterface {
abortUpload(uploadId: string): boolean;
abortAllUploads(): void;
getUploadStatus(uploadId: string): 'uploading' | 'completed' | 'aborted' | 'not-found';
getActiveUploads(): string[];
uploadFile(options: {
file: string | File | Blob;
name: string;
uploadUrl: string;
formData: Record<string, any>;
autoInform?: boolean;
getPercent?: Function;
uploadId?: string;
method?: "POST" | "PUT" | "PATCH";
isFilePath?: boolean;
}): Promise<any>;
} }

View File

@ -1,40 +1,47 @@
import { promisify } from './promisify'; import { promisify } from './promisify';
export class Upload { export class Upload {
async uploadFile(file, name, uploadUrl, formData, autoInform, getPercent, method = "POST") { abortUpload(uploadId) {
return false;
}
abortAllUploads() {
}
getUploadStatus(uploadId) {
return 'not-found';
}
getActiveUploads() {
return [];
}
async uploadFile(options) {
const { file, name, uploadUrl, formData, isFilePath, method = "POST" } = options;
const isPut = method === "PUT"; const isPut = method === "PUT";
if (isPut) { if (isPut) {
return new Promise((resolve, reject) => { if (isFilePath) {
const fs = wx.getFileSystemManager(); return new Promise((resolve, reject) => {
fs.readFile({ const fs = wx.getFileSystemManager();
filePath: file, fs.readFile({
success: (fileRes) => { filePath: file,
// 使用 PUT 方法上传 encoding: 'binary',
wx.request({ success: res => {
url: uploadUrl, resolve(global.fetch(uploadUrl, {
method: 'PUT', method: "PUT",
data: fileRes.data, // ArrayBuffer 格式 headers: {
header: { 'Content-Type': 'application/octet-stream',
'Content-Type': 'image/jpeg', // 根据实际文件类型设置 },
}, body: res.data,
success: (uploadRes) => { }));
if (uploadRes.statusCode === 200) { },
resolve(uploadRes); fail: err => {
} reject(err);
else { }
reject(new Error(`HTTP Error: ${uploadRes.statusCode}`)); });
}
},
fail: (err) => {
console.error('上传失败', err);
reject(err);
}
});
},
fail: (err) => {
console.error('读取文件失败', err);
reject(err);
}
}); });
}
return global.fetch(uploadUrl, {
method: "PUT",
headers: {
'Content-Type': 'application/octet-stream',
},
body: file,
}); });
} }
else { else {

View File

@ -1,3 +1,17 @@
export declare class Upload { import { UploadInterface } from "../types/Upload";
uploadFile(file: File | string, name: string, uploadUrl: string, formData: Record<string, any>, autoInform?: boolean, getPercent?: Function, method?: "POST" | "PUT" | "PATCH"): Promise<any>; export declare class Upload implements UploadInterface {
abortUpload(uploadId: string): boolean;
abortAllUploads(): void;
getUploadStatus(uploadId: string): 'uploading' | 'completed' | 'aborted' | 'not-found';
getActiveUploads(): string[];
uploadFile(options: {
file: File | string | Blob;
name: string;
uploadUrl: string;
formData: Record<string, any>;
autoInform?: boolean;
getPercent?: Function;
uploadId?: string;
method?: "POST" | "PUT" | "PATCH";
}): Promise<any>;
} }

View File

@ -1,5 +1,17 @@
export class Upload { export class Upload {
async uploadFile(file, name, uploadUrl, formData, autoInform, getPercent, method = "POST") { abortUpload(uploadId) {
return false;
}
abortAllUploads() {
}
getUploadStatus(uploadId) {
return 'not-found';
}
getActiveUploads() {
return [];
}
async uploadFile(options) {
const { file, name, uploadUrl, formData, method = "POST" } = options;
const isPut = method === "PUT"; const isPut = method === "PUT";
if (isPut) { if (isPut) {
// S3 预签名上传 // S3 预签名上传

View File

@ -1,3 +1,20 @@
export declare class Upload { import { UploadInterface } from "../types/Upload";
uploadFile(file: File | string, name: string, uploadUrl: string, formData: Record<string, any>, autoInform?: boolean, getPercent?: Function, method?: "POST" | "PUT" | "PATCH"): Promise<any>; export declare class Upload implements UploadInterface {
private controllers;
constructor();
uploadFile(options: {
file: File | string | Blob;
name: string;
uploadUrl: string;
formData: Record<string, any>;
autoInform?: boolean;
getPercent?: Function;
uploadId?: string;
method?: "POST" | "PUT" | "PATCH";
}): Promise<any>;
abortUpload(uploadId: string): boolean;
abortAllUploads(): void;
getUploadStatus(uploadId: string): 'uploading' | 'completed' | 'aborted' | 'not-found';
private generateUploadId;
getActiveUploads(): string[];
} }

View File

@ -1,11 +1,33 @@
export class Upload { export class Upload {
async uploadFile(file, name, uploadUrl, formData, autoInform, getPercent, method = "POST") { controllers = new Map();
constructor() {
this.uploadFile = this.uploadFile.bind(this);
this.abortUpload = this.abortUpload.bind(this);
this.abortAllUploads = this.abortAllUploads.bind(this);
this.getUploadStatus = this.getUploadStatus.bind(this);
this.getActiveUploads = this.getActiveUploads.bind(this);
}
async uploadFile(options) {
const { file, name, uploadUrl, formData, getPercent, uploadId, method = "POST" } = options;
const isPut = method === "PUT"; const isPut = method === "PUT";
const id = uploadId || this.generateUploadId(file, uploadUrl);
// 如果已有相同ID的上传在进行先中断它
if (this.controllers.has(id)) {
this.abortUpload(id);
}
// 创建新的 AbortController
const controller = new AbortController();
this.controllers.set(id, controller);
// 进度监听模式 // 进度监听模式
if (getPercent) { if (getPercent) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest(); const xhr = new XMLHttpRequest();
let percent = 0; let percent = 0;
// 监听中止信号
controller.signal.addEventListener('abort', () => {
xhr.abort();
reject(new DOMException('Upload aborted', 'AbortError'));
});
xhr.upload.addEventListener("progress", (event) => { xhr.upload.addEventListener("progress", (event) => {
if (event.lengthComputable) { if (event.lengthComputable) {
percent = Math.round((event.loaded / event.total) * 100); percent = Math.round((event.loaded / event.total) * 100);
@ -13,17 +35,34 @@ export class Upload {
} }
}); });
xhr.onload = () => { xhr.onload = () => {
this.controllers.delete(id); // 清理控制器
// 构造类似 Response 的对象,支持 headers.get()
const headersMap = new Map();
const headersStr = xhr.getAllResponseHeaders();
headersStr.split('\r\n').forEach(line => {
const parts = line.split(': ');
if (parts.length === 2) {
headersMap.set(parts[0].toLowerCase(), parts[1]);
}
});
const headers = {
get: (name) => headersMap.get(name.toLowerCase()) || null,
has: (name) => headersMap.has(name.toLowerCase()),
forEach: (callback) => {
headersMap.forEach(callback);
}
};
if (xhr.status >= 200 && xhr.status < 300) { if (xhr.status >= 200 && xhr.status < 300) {
if (xhr.status === 204) { if (xhr.status === 204) {
resolve({ status: 204 }); resolve({ status: 204, headers });
} }
else { else {
try { try {
const data = JSON.parse(xhr.responseText); const data = JSON.parse(xhr.responseText);
resolve(data); resolve({ ...data, status: xhr.status, headers });
} }
catch { catch {
resolve({ status: xhr.status, raw: xhr.responseText }); resolve({ status: xhr.status, raw: xhr.responseText, headers });
} }
} }
} }
@ -31,13 +70,28 @@ export class Upload {
reject(new Error(`HTTP Error: ${xhr.status}`)); reject(new Error(`HTTP Error: ${xhr.status}`));
} }
}; };
xhr.onerror = () => reject(new Error("Network Error")); xhr.onerror = () => {
this.controllers.delete(id);
// 如果不是因为中止导致的错误
if (!controller.signal.aborted) {
reject(new Error("Network Error"));
}
};
xhr.onabort = () => {
this.controllers.delete(id);
if (controller.signal.aborted) {
reject(new DOMException('Upload aborted', 'AbortError'));
}
};
xhr.open(method, uploadUrl); xhr.open(method, uploadUrl);
if (isPut) { if (isPut) {
// PUT 模式:直接上传文件 // PUT 模式:直接上传文件
if (file instanceof File) { if (file instanceof File) {
xhr.setRequestHeader("Content-Type", file.type || "application/octet-stream"); xhr.setRequestHeader("Content-Type", file.type || "application/octet-stream");
} }
else if (file instanceof Blob) {
xhr.setRequestHeader("Content-Type", "application/octet-stream");
}
xhr.send(file); xhr.send(file);
} }
else { else {
@ -52,31 +106,84 @@ export class Upload {
}); });
} }
// 无进度监听模式(直接 fetch // 无进度监听模式(直接 fetch
if (isPut) { try {
// S3 预签名上传 let result;
const headers = {}; if (isPut) {
if (file instanceof File) { // S3 预签名上传
headers["Content-Type"] = file.type || "application/octet-stream"; const headers = {};
if (file instanceof File) {
headers["Content-Type"] = file.type || "application/octet-stream";
}
else if (file instanceof Blob) {
headers["Content-Type"] = "application/octet-stream";
}
result = await fetch(uploadUrl, {
method: "PUT",
headers,
body: file,
signal: controller.signal, // 添加中止信号
});
} }
const result = await fetch(uploadUrl, { else {
method: "PUT", // 表单上传
headers, const formData2 = new FormData();
body: file, for (const key of Object.keys(formData)) {
}); formData2.append(key, formData[key]);
}
formData2.append(name || "file", file);
result = await fetch(uploadUrl, {
method,
body: formData2,
signal: controller.signal, // 添加中止信号
});
}
this.controllers.delete(id); // 成功后清理控制器
return result; return result;
} }
else { catch (error) {
// 表单上传 this.controllers.delete(id); // 失败后清理控制器
const formData2 = new FormData(); // 人为中断返回204 使general-business处理成功
for (const key of Object.keys(formData)) { if (error instanceof DOMException && error.name === 'AbortError') {
formData2.append(key, formData[key]); throw new DOMException('Upload aborted', 'AbortError');
} }
formData2.append(name || "file", file); throw error;
const result = await fetch(uploadUrl, {
method,
body: formData2,
});
return result;
} }
} }
// 中断特定上传
abortUpload(uploadId) {
const controller = this.controllers.get(uploadId);
if (controller) {
controller.abort();
this.controllers.delete(uploadId);
return true;
}
return false;
}
// 中断所有上传
abortAllUploads() {
this.controllers.forEach((controller, id) => {
controller.abort();
});
this.controllers.clear();
}
// 获取上传状态
getUploadStatus(uploadId) {
const controller = this.controllers.get(uploadId);
if (!controller)
return 'not-found';
if (controller.signal.aborted)
return 'aborted';
return 'uploading';
}
// 生成唯一的上传ID
generateUploadId(file, uploadUrl) {
const timestamp = Date.now();
const random = Math.random().toString(36).substring(2, 9);
const fileInfo = file instanceof File ? `${file.name}-${file.size}` : file instanceof Blob ? `blob-${file.size}` : file;
return `${uploadUrl}-${fileInfo}-${timestamp}-${random}`;
}
// 获取所有进行中的上传任务
getActiveUploads() {
return Array.from(this.controllers.keys());
}
} }

View File

@ -78,13 +78,12 @@ class Navigator extends navigator_common_1.Navigator {
wx.navigateTo({ wx.navigateTo({
url: url, url: url,
success: () => { success: () => {
this.leave();
this.publish(); this.publish();
resolve(undefined); resolve(undefined);
}, },
fail: (err) => reject(err), fail: (err) => reject(err),
}); });
this.publish();
this.leave();
}); });
} }
// 关闭当前页面跳转到应用内的某个页面但不允许跳转到tabBar页面。 // 关闭当前页面跳转到应用内的某个页面但不允许跳转到tabBar页面。
@ -97,12 +96,12 @@ class Navigator extends navigator_common_1.Navigator {
wx.redirectTo({ wx.redirectTo({
url: url, url: url,
success: () => { success: () => {
resolve(undefined); this.leave();
this.publish(); this.publish();
resolve(undefined);
}, },
fail: (err) => reject(err), fail: (err) => reject(err),
}); });
this.leave();
}); });
} }
//跳转到tabBar页面并关闭其他所有非tabBar页面用于跳转到主页。 //跳转到tabBar页面并关闭其他所有非tabBar页面用于跳转到主页。
@ -115,12 +114,12 @@ class Navigator extends navigator_common_1.Navigator {
wx.switchTab({ wx.switchTab({
url: url, url: url,
success: () => { success: () => {
resolve(undefined); this.leave();
this.publish(); this.publish();
resolve(undefined);
}, },
fail: (err) => reject(err), fail: (err) => reject(err),
}); });
this.leave();
}); });
} }
navigateBack(delta) { navigateBack(delta) {
@ -131,29 +130,23 @@ class Navigator extends navigator_common_1.Navigator {
wx.navigateBack({ wx.navigateBack({
delta: delta || 1, delta: delta || 1,
success: () => { success: () => {
resolve(undefined); this.leave();
this.publish(); this.publish();
resolve(undefined);
}, },
fail: (err) => reject(err), fail: (err) => reject(err),
}); });
this.leave();
}); });
} }
navigateBackOrRedirectTo(options, state, disableNamespace) { navigateBackOrRedirectTo(options, state, disableNamespace) {
if (!this.enter()) {
return;
}
const pages = getCurrentPages(); const pages = getCurrentPages();
if (pages.length > 1) { if (pages.length > 1) {
this.leave();
return this.navigateBack(); return this.navigateBack();
} }
const isTabBar = options?.isTabBar; const isTabBar = options?.isTabBar;
if (isTabBar) { if (isTabBar) {
this.leave();
return this.switchTab(options, state, disableNamespace); return this.switchTab(options, state, disableNamespace);
} }
this.leave();
return this.redirectTo(options, state, disableNamespace); return this.redirectTo(options, state, disableNamespace);
} }
} }

View File

@ -59,8 +59,8 @@ class Navigator extends navigator_common_1.Navigator {
const { url, props } = this.getUrlAndProps(options, state, disableNamespace); const { url, props } = this.getUrlAndProps(options, state, disableNamespace);
const replaceAction = native_1.StackActions.replace(url, props); const replaceAction = native_1.StackActions.replace(url, props);
this.history.dispatch(replaceAction); this.history.dispatch(replaceAction);
this.publish();
this.leave(); this.leave();
this.publish();
} }
async switchTab(options, state, disableNamespace) { async switchTab(options, state, disableNamespace) {
if (!this.enter()) { if (!this.enter()) {
@ -69,8 +69,8 @@ class Navigator extends navigator_common_1.Navigator {
const { url, props } = this.getUrlAndProps(options, state, disableNamespace); const { url, props } = this.getUrlAndProps(options, state, disableNamespace);
const jumpToAction = native_1.TabActions.jumpTo(url, props); const jumpToAction = native_1.TabActions.jumpTo(url, props);
this.history.dispatch(jumpToAction); this.history.dispatch(jumpToAction);
this.publish();
this.leave(); this.leave();
this.publish();
} }
async navigateBack(delta) { async navigateBack(delta) {
if (!this.enter()) { if (!this.enter()) {
@ -80,9 +80,12 @@ class Navigator extends navigator_common_1.Navigator {
if (canGoBack) { if (canGoBack) {
const popAction = native_1.StackActions.pop(delta || 1); const popAction = native_1.StackActions.pop(delta || 1);
this.history.dispatch(popAction); this.history.dispatch(popAction);
this.leave();
this.publish(); this.publish();
} }
this.leave(); else {
this.leave();
}
} }
navigateBackOrRedirectTo(options, state, disableNamespace) { navigateBackOrRedirectTo(options, state, disableNamespace) {
if (!this.enter()) { if (!this.enter()) {
@ -91,14 +94,14 @@ class Navigator extends navigator_common_1.Navigator {
const canGoBack = this.history.canGoBack(); const canGoBack = this.history.canGoBack();
if (canGoBack) { if (canGoBack) {
this.navigateBack(); this.navigateBack();
this.publish();
this.leave(); this.leave();
this.publish();
return; return;
} }
// 回最顶层 // 回最顶层
this.history.dispatch(native_1.StackActions.popToTop()); this.history.dispatch(native_1.StackActions.popToTop());
this.publish();
this.leave(); this.leave();
this.publish();
} }
} }
exports.Navigator = Navigator; exports.Navigator = Navigator;

View File

@ -48,8 +48,8 @@ class Navigator extends navigator_common_1.Navigator {
} }
const { url, props } = this.getUrlAndProps(options, state, disableNamespace); const { url, props } = this.getUrlAndProps(options, state, disableNamespace);
this.history.push(url, props); this.history.push(url, props);
this.publish();
this.leave(); this.leave();
this.publish();
} }
async redirectTo(options, state, disableNamespace) { async redirectTo(options, state, disableNamespace) {
if (!this.enter()) { if (!this.enter()) {
@ -57,8 +57,8 @@ class Navigator extends navigator_common_1.Navigator {
} }
const { url, props } = this.getUrlAndProps(options, state, disableNamespace); const { url, props } = this.getUrlAndProps(options, state, disableNamespace);
this.history.replace(url, props); this.history.replace(url, props);
this.publish();
this.leave(); this.leave();
this.publish();
} }
async switchTab(options, state, disableNamespace) { async switchTab(options, state, disableNamespace) {
console.error('浏览器无switchTab'); console.error('浏览器无switchTab');
@ -71,8 +71,8 @@ class Navigator extends navigator_common_1.Navigator {
return; return;
} }
this.history.go(delta ? 0 - delta : -1); this.history.go(delta ? 0 - delta : -1);
this.publish();
this.leave(); this.leave();
this.publish();
} }
navigateBackOrRedirectTo(options, state, disableNamespace) { navigateBackOrRedirectTo(options, state, disableNamespace) {
console.error('浏览器暂无法获得history堆栈'); console.error('浏览器暂无法获得history堆栈');

View File

@ -233,7 +233,7 @@ declare class SingleNode<ED extends EntityDict & BaseEntityDict, T extends keyof
saveRefreshResult(data: Record<string, any>): void; saveRefreshResult(data: Record<string, any>): void;
refresh(): Promise<void>; refresh(): Promise<void>;
clean(lsn?: number, dontPublish?: true): void; clean(lsn?: number, dontPublish?: true): void;
private getFilter; getFilter(ignoreNew?: true, onlyHot?: true): ED[T]['Filter'] | undefined;
getIntrinsticFilters(): ED[T]["Filter"] | undefined; getIntrinsticFilters(): ED[T]["Filter"] | undefined;
/** /**
* getParentFilter不能假设一定已经有数据filter的条件去构造 * getParentFilter不能假设一定已经有数据filter的条件去构造

View File

@ -540,14 +540,14 @@ class ListNode extends EntityNode {
/** /**
* 以及原本不在的现在是不是满足条件了 * 以及原本不在的现在是不是满足条件了
* 后一种情况也可能原本就满足但不在当前的ids中这时是判断不出来的total+1可能不对但是应该是较少的case*/ * 后一种情况也可能原本就满足但不在当前的ids中这时是判断不出来的total+1可能不对但是应该是较少的case*/
const filter = this.constructFilters(true, true, true); const filters = this.constructFilters(true, true, true);
if (intersected.length) { if (intersected.length) {
const rows = this.cache.get(this.entity, { const rows = this.cache.get(this.entity, {
data: { data: {
id: 1, id: 1,
}, },
filter: (0, filter_1.combineFilters)(this.entity, this.schema, [ filter: (0, filter_1.combineFilters)(this.entity, this.schema, [
...(filter || []), ...(filters || []),
{ {
id: { id: {
$in: intersected, $in: intersected,
@ -565,12 +565,8 @@ class ListNode extends EntityNode {
}); });
} }
if (diffed.length) { if (diffed.length) {
diffed.forEach((ele) => { const filter = filters && (0, filter_1.combineFilters)(e, this.schema, filters);
this.sr[ele] = {}; diffed.forEach((ele) => tryAddRowToList(ele, filter));
if (this.pagination.total) {
this.pagination.total++;
}
});
} }
} }
// hasUpdated = true; // hasUpdated = true;
@ -1259,7 +1255,7 @@ class SingleNode extends EntityNode {
setId(id) { setId(id) {
if (id !== this.id) { if (id !== this.id) {
const operations = this.ulManager.makeOperations(); const operations = this.ulManager.makeOperations();
(0, assert_1.assert)(operations.length <= 1); (0, assert_1.assert)(operations.length <= 1, 'singleNode在setId时出现数据不一致');
const [operation] = operations; const [operation] = operations;
if (operation?.action === 'create') { if (operation?.action === 'create') {
if (operation.data.id === id) { if (operation.data.id === id) {
@ -1420,7 +1416,7 @@ class SingleNode extends EntityNode {
} }
setDirty() { setDirty() {
const id = this.getId(); const id = this.getId();
(0, assert_1.assert)(id); (0, assert_1.assert)(id, "不能对没有id的singleNode设置dirty");
this.ulManager.push(this.ulManager.maxLsn, { this.ulManager.push(this.ulManager.maxLsn, {
action: 'update', action: 'update',
data: {}, data: {},
@ -1448,7 +1444,7 @@ class SingleNode extends EntityNode {
const childOperations = child.composeOperations(paths?.length ? paths : undefined); const childOperations = child.composeOperations(paths?.length ? paths : undefined);
if (childOperations) { if (childOperations) {
if (child instanceof SingleNode) { if (child instanceof SingleNode) {
(0, assert_1.assert)(childOperations.length === 1); (0, assert_1.assert)(childOperations.length === 1, 'singleNode在composeOperations时出现数据不一致');
this.ulManager.push(lsnMax + 100, { this.ulManager.push(lsnMax + 100, {
action: 'update', action: 'update',
data: { data: {
@ -1460,7 +1456,7 @@ class SingleNode extends EntityNode {
}); });
} }
else { else {
(0, assert_1.assert)(child instanceof ListNode); (0, assert_1.assert)(child instanceof ListNode, 'child必须是ListNode');
this.ulManager.push(lsnMax + 100, { this.ulManager.push(lsnMax + 100, {
action: 'update', action: 'update',
data: { data: {
@ -1550,8 +1546,8 @@ class SingleNode extends EntityNode {
const rel = this.judgeRelation(k2); const rel = this.judgeRelation(k2);
if (rel === 2) { if (rel === 2) {
if (value?.entityId) { if (value?.entityId) {
(0, assert_1.assert)(child instanceof SingleNode); (0, assert_1.assert)(child instanceof SingleNode, 'child必须是singleNode');
(0, assert_1.assert)(value.entity === child.getEntity()); (0, assert_1.assert)(value.entity === child.getEntity(), 'singleNode的entity必须一致');
child.saveRefreshResult({ child.saveRefreshResult({
[value.entityId]: this.sr[k2] || {}, [value.entityId]: this.sr[k2] || {},
}); });
@ -2014,6 +2010,7 @@ class RunningTree extends Feature_1.Feature {
(0, assert_1.assert)(!parentNode || parentNode instanceof VirtualNode); (0, assert_1.assert)(!parentNode || parentNode instanceof VirtualNode);
node = new VirtualNode(fullPath, path, parentNode, stale); node = new VirtualNode(fullPath, path, parentNode, stale);
} }
// 任何临时的修改都需要回滚上面缓存了一些update操作
rollback(); rollback();
if (!parentNode) { if (!parentNode) {
(0, assert_1.assert)(!parent && !this.root[path]); (0, assert_1.assert)(!parent && !this.root[path]);
@ -2420,7 +2417,7 @@ class RunningTree extends Feature_1.Feature {
const node = path && this.findNode(path); const node = path && this.findNode(path);
// assert(node.isDirty()); // assert(node.isDirty());
node && node.setExecuting(true); node && node.setExecuting(true);
let pollute = false; // let pollute = false;
try { try {
let operations = path && this.getOperations(path) || []; let operations = path && this.getOperations(path) || [];
if (opers) { if (opers) {
@ -2439,15 +2436,30 @@ class RunningTree extends Feature_1.Feature {
else if (node) { else if (node) {
// 老的写法直接对一个非脏的结点execute某个action也可以支持 // 老的写法直接对一个非脏的结点execute某个action也可以支持
(0, assert_1.assert)(node instanceof SingleNode); (0, assert_1.assert)(node instanceof SingleNode);
node.update(this.logSerailNumber, {}, action); // node.update(this.logSerailNumber, {}, action);
pollute = true; // pollute = true;
operations = node.composeOperations() || []; // operations = node.composeOperations() || [];
(0, assert_1.assert)(operations.length === 1); // assert(operations.length === 1);
const [operation1] = operations; // const [operation1] = operations;
if (action !== operation1.operation.action) { // if (action !== operation1.operation.action) {
(0, assert_1.assert)(operation1.operation.action === 'update'); // 如果execute时传action前面update动作应该只可能是update // assert(operation1.operation.action === 'update'); // 如果execute时传action前面update动作应该只可能是update
operation1.operation.action = action; // operation1.operation.action = action;
} // }
/**
* 上述写法会触发publish行为一个close动作会导致页面像下面这样渲染
* opened --> closed(上面的node.update) --> opened(执行成功回来setExecuting时) --> closed执行成功回来再sync cache后
*/
operations.push({
entity: node.getEntity(),
operation: {
id: await (0, uuid_1.generateNewIdAsync)(),
action,
data: {
$$updateAt$$: Date.now(),
},
filter: node.getFilter(true),
}
});
} }
} }
if (operations.length > 0) { if (operations.length > 0) {
@ -2477,9 +2489,9 @@ class RunningTree extends Feature_1.Feature {
} }
catch (err) { catch (err) {
node && node.setExecuting(false); node && node.setExecuting(false);
if (pollute) { /* if (pollute) {
path && this.clean(path); path && this.clean(path);
} } */
throw err; throw err;
} }
} }

View File

@ -98,11 +98,11 @@ class SubScriber extends Feature_1.Feature {
} }
}); });
socket.on('data', (opRecords, event) => { socket.on('data', (opRecords, event) => {
this.cache.sync(opRecords);
const registered = this.eventMap[event]; const registered = this.eventMap[event];
if (registered) { if (registered) {
registered.callbacks.forEach((ele) => ele(event, opRecords)); registered.callbacks.forEach((ele) => ele(event, opRecords));
} }
this.cache.sync(opRecords);
}); });
socket.on('error', (errString) => { socket.on('error', (errString) => {
console.error(errString); console.error(errString);

2
lib/page.mp.d.ts vendored
View File

@ -5,4 +5,4 @@ import { Feature } from './types/Feature';
import { DataOption, OakComponentOption } from './types/Page'; import { DataOption, OakComponentOption } from './types/Page';
import { AsyncContext } from 'oak-domain/lib/store/AsyncRowStore'; import { AsyncContext } from 'oak-domain/lib/store/AsyncRowStore';
import { SyncContext } from 'oak-domain/lib/store/SyncRowStore'; import { SyncContext } from 'oak-domain/lib/store/SyncRowStore';
export declare function createComponent<IsList extends boolean, ED extends EntityDict & BaseEntityDict, T extends keyof ED, Cxt extends AsyncContext<ED>, FrontCxt extends SyncContext<ED>, AD extends Record<string, Aspect<ED, AsyncContext<ED>>>, FD extends Record<string, Feature>, FormedData extends Record<string, any>, TData extends DataOption = {}, TProperty extends DataOption = {}, TMethod extends Record<string, Function> = {}>(option: OakComponentOption<IsList, ED, T, Cxt, FrontCxt, AD, FD, FormedData, TData, TProperty, TMethod>, features: BasicFeatures<ED> & FD): string; export declare function createComponent<IsList extends boolean, ED extends EntityDict & BaseEntityDict, T extends keyof ED, Cxt extends AsyncContext<ED>, FrontCxt extends SyncContext<ED>, AD extends Record<string, Aspect<ED, Cxt>>, FD extends Record<string, Feature>, FormedData extends Record<string, any>, TData extends DataOption = {}, TProperty extends DataOption = {}, TMethod extends Record<string, Function> = {}>(option: OakComponentOption<IsList, ED, T, Cxt, FrontCxt, AD, FD, FormedData, TData, TProperty, TMethod>, features: BasicFeatures<ED> & FD): string;

View File

@ -543,6 +543,13 @@ function translatePropertiesToPropertyDefinitions(properties) {
const definitions = {}; const definitions = {};
if (properties) { if (properties) {
Object.keys(properties).forEach((prop) => { Object.keys(properties).forEach((prop) => {
if (properties[prop] === null) {
definitions[prop] = {
type: null,
value: null,
};
return;
}
switch (typeof properties[prop]) { switch (typeof properties[prop]) {
case 'string': { case 'string': {
if (properties[prop]) { if (properties[prop]) {

View File

@ -6,4 +6,4 @@ import { Feature } from './types/Feature';
import { DataOption, OakComponentOption } from './types/Page'; import { DataOption, OakComponentOption } from './types/Page';
import { SyncContext } from 'oak-domain/lib/store/SyncRowStore'; import { SyncContext } from 'oak-domain/lib/store/SyncRowStore';
import { AsyncContext } from 'oak-domain/lib/store/AsyncRowStore'; import { AsyncContext } from 'oak-domain/lib/store/AsyncRowStore';
export declare function createComponent<IsList extends boolean, ED extends EntityDict & BaseEntityDict, T extends keyof ED, Cxt extends AsyncContext<ED>, FrontCxt extends SyncContext<ED>, AD extends Record<string, Aspect<ED, AsyncContext<ED>>>, FD extends Record<string, Feature>, FormedData extends Record<string, any>, TData extends Record<string, any> = {}, TProperty extends DataOption = {}, TMethod extends Record<string, Function> = {}>(option: OakComponentOption<IsList, ED, T, Cxt, FrontCxt, AD, FD, FormedData, TData, TProperty, TMethod>, features: BasicFeatures<ED> & FD): React.ForwardRefExoticComponent<React.RefAttributes<unknown>>; export declare function createComponent<IsList extends boolean, ED extends EntityDict & BaseEntityDict, T extends keyof ED, Cxt extends AsyncContext<ED>, FrontCxt extends SyncContext<ED>, AD extends Record<string, Aspect<ED, Cxt>>, FD extends Record<string, Feature>, FormedData extends Record<string, any>, TData extends Record<string, any> = {}, TProperty extends DataOption = {}, TMethod extends Record<string, Function> = {}>(option: OakComponentOption<IsList, ED, T, Cxt, FrontCxt, AD, FD, FormedData, TData, TProperty, TMethod>, features: BasicFeatures<ED> & FD): React.ForwardRefExoticComponent<React.RefAttributes<unknown>>;

2
lib/page.react.d.ts vendored
View File

@ -9,7 +9,7 @@ import { MessageProps } from './types/Message';
import { NotificationProps } from './types/Notification'; import { NotificationProps } from './types/Notification';
import { SyncContext } from 'oak-domain/lib/store/SyncRowStore'; import { SyncContext } from 'oak-domain/lib/store/SyncRowStore';
import { AsyncContext } from 'oak-domain/lib/store/AsyncRowStore'; import { AsyncContext } from 'oak-domain/lib/store/AsyncRowStore';
export declare function createComponent<IsList extends boolean, ED extends EntityDict & BaseEntityDict, T extends keyof ED, Cxt extends AsyncContext<ED>, FrontCxt extends SyncContext<ED>, AD extends Record<string, Aspect<ED, AsyncContext<ED>>>, FD extends Record<string, Feature>, FormedData extends Record<string, any>, TData extends Record<string, any> = {}, TProperty extends DataOption = {}, TMethod extends Record<string, Function> = {}>(option: OakComponentOption<IsList, ED, T, Cxt, FrontCxt, AD, FD, FormedData, TData, TProperty, TMethod>, features: BasicFeatures<ED> & FD): { export declare function createComponent<IsList extends boolean, ED extends EntityDict & BaseEntityDict, T extends keyof ED, Cxt extends AsyncContext<ED>, FrontCxt extends SyncContext<ED>, AD extends Record<string, Aspect<ED, Cxt>>, FD extends Record<string, Feature>, FormedData extends Record<string, any>, TData extends Record<string, any> = {}, TProperty extends DataOption = {}, TMethod extends Record<string, Function> = {}>(option: OakComponentOption<IsList, ED, T, Cxt, FrontCxt, AD, FD, FormedData, TData, TProperty, TMethod>, features: BasicFeatures<ED> & FD): {
new (props: ComponentProps<ED, T, TProperty>): { new (props: ComponentProps<ED, T, TProperty>): {
features: BasicFeatures<ED> & FD; features: BasicFeatures<ED> & FD;
oakOption: OakComponentOption<IsList, ED, T, Cxt, FrontCxt, AD, FD, FormedData, TData, TProperty, TMethod>; oakOption: OakComponentOption<IsList, ED, T, Cxt, FrontCxt, AD, FD, FormedData, TData, TProperty, TMethod>;

2
lib/page.web.d.ts vendored
View File

@ -6,4 +6,4 @@ import { Feature } from './types/Feature';
import { DataOption, OakComponentOption } from './types/Page'; import { DataOption, OakComponentOption } from './types/Page';
import { SyncContext } from 'oak-domain/lib/store/SyncRowStore'; import { SyncContext } from 'oak-domain/lib/store/SyncRowStore';
import { AsyncContext } from 'oak-domain/lib/store/AsyncRowStore'; import { AsyncContext } from 'oak-domain/lib/store/AsyncRowStore';
export declare function createComponent<IsList extends boolean, ED extends EntityDict & BaseEntityDict, T extends keyof ED, Cxt extends AsyncContext<ED>, FrontCxt extends SyncContext<ED>, AD extends Record<string, Aspect<ED, AsyncContext<ED>>>, FD extends Record<string, Feature>, FormedData extends Record<string, any>, TData extends Record<string, any> = {}, TProperty extends DataOption = {}, TMethod extends Record<string, Function> = {}>(option: OakComponentOption<IsList, ED, T, Cxt, FrontCxt, AD, FD, FormedData, TData, TProperty, TMethod>, features: BasicFeatures<ED> & FD): React.ForwardRefExoticComponent<React.RefAttributes<unknown>>; export declare function createComponent<IsList extends boolean, ED extends EntityDict & BaseEntityDict, T extends keyof ED, Cxt extends AsyncContext<ED>, FrontCxt extends SyncContext<ED>, AD extends Record<string, Aspect<ED, Cxt>>, FD extends Record<string, Feature>, FormedData extends Record<string, any>, TData extends Record<string, any> = {}, TProperty extends DataOption = {}, TMethod extends Record<string, Function> = {}>(option: OakComponentOption<IsList, ED, T, Cxt, FrontCxt, AD, FD, FormedData, TData, TProperty, TMethod>, features: BasicFeatures<ED> & FD): React.ForwardRefExoticComponent<React.RefAttributes<unknown>>;

4
lib/types/Page.d.ts vendored
View File

@ -146,7 +146,7 @@ export type ComponentFullThisType<ED extends EntityDict & BaseEntityDict, T exte
triggerEvent: <DetailType = any>(name: string, detail?: DetailType, options?: WechatMiniprogram.Component.TriggerEventOption) => void; triggerEvent: <DetailType = any>(name: string, detail?: DetailType, options?: WechatMiniprogram.Component.TriggerEventOption) => void;
oakLifetime: OakLifetime; oakLifetime: OakLifetime;
} & OakCommonComponentMethods<ED, T> & OakListComponentMethods<ED, T> & OakSingleComponentMethods<ED, T>; } & OakCommonComponentMethods<ED, T> & OakListComponentMethods<ED, T> & OakSingleComponentMethods<ED, T>;
export type OakComponentOption<IsList extends boolean, ED extends EntityDict & BaseEntityDict, T extends keyof ED, Cxt extends AsyncContext<ED>, FrontCxt extends SyncContext<ED>, AD extends Record<string, Aspect<ED, AsyncContext<ED>>>, FD extends Record<string, Feature>, FormedData extends Record<string, any>, TData extends Record<string, any>, TProperty extends DataOption, TMethod extends Record<string, Function>, EMethod extends Record<string, Function> = {}> = ComponentOption<IsList, ED, T, Cxt, FrontCxt, AD, FD, FormedData, TData, TProperty, TMethod, EMethod> & Partial<{ export type OakComponentOption<IsList extends boolean, ED extends EntityDict & BaseEntityDict, T extends keyof ED, Cxt extends AsyncContext<ED>, FrontCxt extends SyncContext<ED>, AD extends Record<string, Aspect<ED, Cxt>>, FD extends Record<string, Feature>, FormedData extends Record<string, any>, TData extends Record<string, any>, TProperty extends DataOption, TMethod extends Record<string, Function>, EMethod extends Record<string, Function> = {}> = ComponentOption<IsList, ED, T, Cxt, FrontCxt, AD, FD, FormedData, TData, TProperty, TMethod, EMethod> & Partial<{
/** /**
* *
*/ */
@ -351,7 +351,7 @@ export type OakComponentData<ED extends EntityDict & BaseEntityDict, T extends k
type OakListComoponetData<ED extends EntityDict & BaseEntityDict, T extends keyof ED> = { type OakListComoponetData<ED extends EntityDict & BaseEntityDict, T extends keyof ED> = {
oakPagination?: Pagination; oakPagination?: Pagination;
}; };
export type MakeOakComponent<ED extends EntityDict & BaseEntityDict, Cxt extends AsyncContext<ED>, FrontCxt extends SyncContext<ED>, AD extends Record<string, Aspect<ED, AsyncContext<ED>>>, FD extends Record<string, Feature>> = <IsList extends boolean, T extends keyof ED, FormedData extends DataOption, TData extends DataOption, TProperty extends DataOption, TMethod extends MethodOption>(options: OakComponentOption<IsList, ED, T, Cxt, FrontCxt, AD, FD, FormedData, TData, TProperty, TMethod>) => (props: ReactComponentProps<ED, T, IsList, TProperty>) => React.ReactElement; export type MakeOakComponent<ED extends EntityDict & BaseEntityDict, Cxt extends AsyncContext<ED>, FrontCxt extends SyncContext<ED>, AD extends Record<string, Aspect<ED, Cxt>>, FD extends Record<string, Feature>> = <IsList extends boolean, T extends keyof ED, FormedData extends DataOption, TData extends DataOption, TProperty extends DataOption, TMethod extends MethodOption>(options: OakComponentOption<IsList, ED, T, Cxt, FrontCxt, AD, FD, FormedData, TData, TProperty, TMethod>) => (props: ReactComponentProps<ED, T, IsList, TProperty>) => React.ReactElement;
export type WebComponentCommonMethodNames = 'setNotification' | 'setMessage' | 'navigateTo' | 'navigateBack' | 'redirectTo' | 'clean' | 't' | 'execute' | 'refresh' | 'aggregate' | 'checkOperation' | 'isDirty'; export type WebComponentCommonMethodNames = 'setNotification' | 'setMessage' | 'navigateTo' | 'navigateBack' | 'redirectTo' | 'clean' | 't' | 'execute' | 'refresh' | 'aggregate' | 'checkOperation' | 'isDirty';
export type WebComponentListMethodNames = 'loadMore' | 'setFilters' | 'addNamedFilter' | 'removeNamedFilter' | 'removeNamedFilterByName' | 'setNamedSorters' | 'addNamedSorter' | 'removeNamedSorter' | 'removeNamedSorterByName' | 'setPageSize' | 'setCurrentPage' | 'addItem' | 'addItems' | 'removeItem' | 'removeItems' | 'updateItem' | 'updateItems' | 'resetItem' | 'recoverItem' | 'recoverItems'; export type WebComponentListMethodNames = 'loadMore' | 'setFilters' | 'addNamedFilter' | 'removeNamedFilter' | 'removeNamedFilterByName' | 'setNamedSorters' | 'addNamedSorter' | 'removeNamedSorter' | 'removeNamedSorterByName' | 'setPageSize' | 'setCurrentPage' | 'addItem' | 'addItems' | 'removeItem' | 'removeItems' | 'updateItem' | 'updateItems' | 'resetItem' | 'recoverItem' | 'recoverItems';
export type WebComponentSingleMethodNames = 'update' | 'remove' | 'create' | 'isCreation' | 'getId' | 'setId'; export type WebComponentSingleMethodNames = 'update' | 'remove' | 'create' | 'isCreation' | 'getId' | 'setId';

30
lib/types/Upload.d.ts vendored Normal file
View File

@ -0,0 +1,30 @@
type UploadFileFn = (options: {
file: string | File | Blob;
name: string;
uploadUrl: string;
formData: Record<string, any>;
autoInform?: boolean;
getPercent?: Function;
uploadId?: string;
method?: "POST" | "PUT" | "PATCH";
isFilePath?: boolean;
}) => Promise<{
status: number;
statusText: string;
statusCode?: number;
headers: {
get(name: string): string | null;
};
json(): Promise<any>;
text(): Promise<string>;
errMsg?: string;
data?: any;
}>;
export interface UploadInterface {
uploadFile: UploadFileFn;
abortUpload(uploadId: string): boolean;
abortAllUploads(): void;
getUploadStatus(uploadId: string): 'uploading' | 'completed' | 'aborted' | 'not-found';
getActiveUploads(): string[];
}
export {};

2
lib/types/Upload.js Normal file
View File

@ -0,0 +1,2 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });

22
lib/utils/upload.d.ts vendored
View File

@ -1,3 +1,21 @@
export declare class Upload { import { UploadInterface } from "../types/Upload";
uploadFile(file: string | File, name: string, uploadUrl: string, formData: Record<string, any>, autoInform?: boolean): Promise<any>; export declare class Upload implements UploadInterface {
uploadFile(options: {
file: string | File | Blob;
name: string;
uploadUrl: string;
formData: Record<string, any>;
autoInform?: boolean;
getPercent?: Function;
uploadId?: string;
method?: "POST" | "PUT" | "PATCH";
isFilePath?: boolean;
}): Promise<any>;
private controllers;
constructor();
abortUpload(uploadId: string): boolean;
abortAllUploads(): void;
getUploadStatus(uploadId: string): 'uploading' | 'completed' | 'aborted' | 'not-found';
private generateUploadId;
getActiveUploads(): string[];
} }

View File

@ -2,8 +2,38 @@
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
exports.Upload = void 0; exports.Upload = void 0;
class Upload { class Upload {
async uploadFile(file, name, uploadUrl, formData, autoInform) { async uploadFile(options) {
console.warn('server不会调用此函数'); console.warn('server不会调用此函数');
} }
controllers = new Map();
constructor() {
this.uploadFile = this.uploadFile.bind(this);
this.abortUpload = this.abortUpload.bind(this);
this.abortAllUploads = this.abortAllUploads.bind(this);
this.getUploadStatus = this.getUploadStatus.bind(this);
this.getActiveUploads = this.getActiveUploads.bind(this);
}
// 中断特定上传
abortUpload(uploadId) {
console.warn('server不会调用此函数');
return false;
}
// 中断所有上传
abortAllUploads() {
console.warn('server不会调用此函数');
}
// 获取上传状态
getUploadStatus(uploadId) {
console.warn('server不会调用此函数');
return 'uploading';
}
// 生成唯一的上传ID
generateUploadId(file, uploadUrl) {
return `server不会调用此函数`;
}
// 获取所有进行中的上传任务
getActiveUploads() {
return [];
}
} }
exports.Upload = Upload; exports.Upload = Upload;

View File

@ -1,3 +1,18 @@
export declare class Upload { import { UploadInterface } from '../types/Upload';
uploadFile(file: string | File, name: string, uploadUrl: string, formData: Record<string, any>, autoInform?: boolean, getPercent?: Function, method?: "POST" | "PUT" | "PATCH"): Promise<any>; export declare class Upload implements UploadInterface {
abortUpload(uploadId: string): boolean;
abortAllUploads(): void;
getUploadStatus(uploadId: string): 'uploading' | 'completed' | 'aborted' | 'not-found';
getActiveUploads(): string[];
uploadFile(options: {
file: string | File | Blob;
name: string;
uploadUrl: string;
formData: Record<string, any>;
autoInform?: boolean;
getPercent?: Function;
uploadId?: string;
method?: "POST" | "PUT" | "PATCH";
isFilePath?: boolean;
}): Promise<any>;
} }

View File

@ -3,41 +3,48 @@ Object.defineProperty(exports, "__esModule", { value: true });
exports.Upload = void 0; exports.Upload = void 0;
const promisify_1 = require("./promisify"); const promisify_1 = require("./promisify");
class Upload { class Upload {
async uploadFile(file, name, uploadUrl, formData, autoInform, getPercent, method = "POST") { abortUpload(uploadId) {
return false;
}
abortAllUploads() {
}
getUploadStatus(uploadId) {
return 'not-found';
}
getActiveUploads() {
return [];
}
async uploadFile(options) {
const { file, name, uploadUrl, formData, isFilePath, method = "POST" } = options;
const isPut = method === "PUT"; const isPut = method === "PUT";
if (isPut) { if (isPut) {
return new Promise((resolve, reject) => { if (isFilePath) {
const fs = wx.getFileSystemManager(); return new Promise((resolve, reject) => {
fs.readFile({ const fs = wx.getFileSystemManager();
filePath: file, fs.readFile({
success: (fileRes) => { filePath: file,
// 使用 PUT 方法上传 encoding: 'binary',
wx.request({ success: res => {
url: uploadUrl, resolve(global.fetch(uploadUrl, {
method: 'PUT', method: "PUT",
data: fileRes.data, // ArrayBuffer 格式 headers: {
header: { 'Content-Type': 'application/octet-stream',
'Content-Type': 'image/jpeg', // 根据实际文件类型设置 },
}, body: res.data,
success: (uploadRes) => { }));
if (uploadRes.statusCode === 200) { },
resolve(uploadRes); fail: err => {
} reject(err);
else { }
reject(new Error(`HTTP Error: ${uploadRes.statusCode}`)); });
}
},
fail: (err) => {
console.error('上传失败', err);
reject(err);
}
});
},
fail: (err) => {
console.error('读取文件失败', err);
reject(err);
}
}); });
}
return global.fetch(uploadUrl, {
method: "PUT",
headers: {
'Content-Type': 'application/octet-stream',
},
body: file,
}); });
} }
else { else {

View File

@ -1,3 +1,17 @@
export declare class Upload { import { UploadInterface } from "../types/Upload";
uploadFile(file: File | string, name: string, uploadUrl: string, formData: Record<string, any>, autoInform?: boolean, getPercent?: Function, method?: "POST" | "PUT" | "PATCH"): Promise<any>; export declare class Upload implements UploadInterface {
abortUpload(uploadId: string): boolean;
abortAllUploads(): void;
getUploadStatus(uploadId: string): 'uploading' | 'completed' | 'aborted' | 'not-found';
getActiveUploads(): string[];
uploadFile(options: {
file: File | string | Blob;
name: string;
uploadUrl: string;
formData: Record<string, any>;
autoInform?: boolean;
getPercent?: Function;
uploadId?: string;
method?: "POST" | "PUT" | "PATCH";
}): Promise<any>;
} }

View File

@ -2,7 +2,19 @@
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
exports.Upload = void 0; exports.Upload = void 0;
class Upload { class Upload {
async uploadFile(file, name, uploadUrl, formData, autoInform, getPercent, method = "POST") { abortUpload(uploadId) {
return false;
}
abortAllUploads() {
}
getUploadStatus(uploadId) {
return 'not-found';
}
getActiveUploads() {
return [];
}
async uploadFile(options) {
const { file, name, uploadUrl, formData, method = "POST" } = options;
const isPut = method === "PUT"; const isPut = method === "PUT";
if (isPut) { if (isPut) {
// S3 预签名上传 // S3 预签名上传

View File

@ -1,3 +1,20 @@
export declare class Upload { import { UploadInterface } from "../types/Upload";
uploadFile(file: File | string, name: string, uploadUrl: string, formData: Record<string, any>, autoInform?: boolean, getPercent?: Function, method?: "POST" | "PUT" | "PATCH"): Promise<any>; export declare class Upload implements UploadInterface {
private controllers;
constructor();
uploadFile(options: {
file: File | string | Blob;
name: string;
uploadUrl: string;
formData: Record<string, any>;
autoInform?: boolean;
getPercent?: Function;
uploadId?: string;
method?: "POST" | "PUT" | "PATCH";
}): Promise<any>;
abortUpload(uploadId: string): boolean;
abortAllUploads(): void;
getUploadStatus(uploadId: string): 'uploading' | 'completed' | 'aborted' | 'not-found';
private generateUploadId;
getActiveUploads(): string[];
} }

View File

@ -2,13 +2,35 @@
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
exports.Upload = void 0; exports.Upload = void 0;
class Upload { class Upload {
async uploadFile(file, name, uploadUrl, formData, autoInform, getPercent, method = "POST") { controllers = new Map();
constructor() {
this.uploadFile = this.uploadFile.bind(this);
this.abortUpload = this.abortUpload.bind(this);
this.abortAllUploads = this.abortAllUploads.bind(this);
this.getUploadStatus = this.getUploadStatus.bind(this);
this.getActiveUploads = this.getActiveUploads.bind(this);
}
async uploadFile(options) {
const { file, name, uploadUrl, formData, getPercent, uploadId, method = "POST" } = options;
const isPut = method === "PUT"; const isPut = method === "PUT";
const id = uploadId || this.generateUploadId(file, uploadUrl);
// 如果已有相同ID的上传在进行先中断它
if (this.controllers.has(id)) {
this.abortUpload(id);
}
// 创建新的 AbortController
const controller = new AbortController();
this.controllers.set(id, controller);
// 进度监听模式 // 进度监听模式
if (getPercent) { if (getPercent) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest(); const xhr = new XMLHttpRequest();
let percent = 0; let percent = 0;
// 监听中止信号
controller.signal.addEventListener('abort', () => {
xhr.abort();
reject(new DOMException('Upload aborted', 'AbortError'));
});
xhr.upload.addEventListener("progress", (event) => { xhr.upload.addEventListener("progress", (event) => {
if (event.lengthComputable) { if (event.lengthComputable) {
percent = Math.round((event.loaded / event.total) * 100); percent = Math.round((event.loaded / event.total) * 100);
@ -16,17 +38,34 @@ class Upload {
} }
}); });
xhr.onload = () => { xhr.onload = () => {
this.controllers.delete(id); // 清理控制器
// 构造类似 Response 的对象,支持 headers.get()
const headersMap = new Map();
const headersStr = xhr.getAllResponseHeaders();
headersStr.split('\r\n').forEach(line => {
const parts = line.split(': ');
if (parts.length === 2) {
headersMap.set(parts[0].toLowerCase(), parts[1]);
}
});
const headers = {
get: (name) => headersMap.get(name.toLowerCase()) || null,
has: (name) => headersMap.has(name.toLowerCase()),
forEach: (callback) => {
headersMap.forEach(callback);
}
};
if (xhr.status >= 200 && xhr.status < 300) { if (xhr.status >= 200 && xhr.status < 300) {
if (xhr.status === 204) { if (xhr.status === 204) {
resolve({ status: 204 }); resolve({ status: 204, headers });
} }
else { else {
try { try {
const data = JSON.parse(xhr.responseText); const data = JSON.parse(xhr.responseText);
resolve(data); resolve({ ...data, status: xhr.status, headers });
} }
catch { catch {
resolve({ status: xhr.status, raw: xhr.responseText }); resolve({ status: xhr.status, raw: xhr.responseText, headers });
} }
} }
} }
@ -34,13 +73,28 @@ class Upload {
reject(new Error(`HTTP Error: ${xhr.status}`)); reject(new Error(`HTTP Error: ${xhr.status}`));
} }
}; };
xhr.onerror = () => reject(new Error("Network Error")); xhr.onerror = () => {
this.controllers.delete(id);
// 如果不是因为中止导致的错误
if (!controller.signal.aborted) {
reject(new Error("Network Error"));
}
};
xhr.onabort = () => {
this.controllers.delete(id);
if (controller.signal.aborted) {
reject(new DOMException('Upload aborted', 'AbortError'));
}
};
xhr.open(method, uploadUrl); xhr.open(method, uploadUrl);
if (isPut) { if (isPut) {
// PUT 模式:直接上传文件 // PUT 模式:直接上传文件
if (file instanceof File) { if (file instanceof File) {
xhr.setRequestHeader("Content-Type", file.type || "application/octet-stream"); xhr.setRequestHeader("Content-Type", file.type || "application/octet-stream");
} }
else if (file instanceof Blob) {
xhr.setRequestHeader("Content-Type", "application/octet-stream");
}
xhr.send(file); xhr.send(file);
} }
else { else {
@ -55,32 +109,85 @@ class Upload {
}); });
} }
// 无进度监听模式(直接 fetch // 无进度监听模式(直接 fetch
if (isPut) { try {
// S3 预签名上传 let result;
const headers = {}; if (isPut) {
if (file instanceof File) { // S3 预签名上传
headers["Content-Type"] = file.type || "application/octet-stream"; const headers = {};
if (file instanceof File) {
headers["Content-Type"] = file.type || "application/octet-stream";
}
else if (file instanceof Blob) {
headers["Content-Type"] = "application/octet-stream";
}
result = await fetch(uploadUrl, {
method: "PUT",
headers,
body: file,
signal: controller.signal, // 添加中止信号
});
} }
const result = await fetch(uploadUrl, { else {
method: "PUT", // 表单上传
headers, const formData2 = new FormData();
body: file, for (const key of Object.keys(formData)) {
}); formData2.append(key, formData[key]);
}
formData2.append(name || "file", file);
result = await fetch(uploadUrl, {
method,
body: formData2,
signal: controller.signal, // 添加中止信号
});
}
this.controllers.delete(id); // 成功后清理控制器
return result; return result;
} }
else { catch (error) {
// 表单上传 this.controllers.delete(id); // 失败后清理控制器
const formData2 = new FormData(); // 人为中断返回204 使general-business处理成功
for (const key of Object.keys(formData)) { if (error instanceof DOMException && error.name === 'AbortError') {
formData2.append(key, formData[key]); throw new DOMException('Upload aborted', 'AbortError');
} }
formData2.append(name || "file", file); throw error;
const result = await fetch(uploadUrl, {
method,
body: formData2,
});
return result;
} }
} }
// 中断特定上传
abortUpload(uploadId) {
const controller = this.controllers.get(uploadId);
if (controller) {
controller.abort();
this.controllers.delete(uploadId);
return true;
}
return false;
}
// 中断所有上传
abortAllUploads() {
this.controllers.forEach((controller, id) => {
controller.abort();
});
this.controllers.clear();
}
// 获取上传状态
getUploadStatus(uploadId) {
const controller = this.controllers.get(uploadId);
if (!controller)
return 'not-found';
if (controller.signal.aborted)
return 'aborted';
return 'uploading';
}
// 生成唯一的上传ID
generateUploadId(file, uploadUrl) {
const timestamp = Date.now();
const random = Math.random().toString(36).substring(2, 9);
const fileInfo = file instanceof File ? `${file.name}-${file.size}` : file instanceof Blob ? `blob-${file.size}` : file;
return `${uploadUrl}-${fileInfo}-${timestamp}-${random}`;
}
// 获取所有进行中的上传任务
getActiveUploads() {
return Array.from(this.controllers.keys());
}
} }
exports.Upload = Upload; exports.Upload = Upload;

View File

@ -1,6 +1,6 @@
{ {
"name": "oak-frontend-base", "name": "oak-frontend-base",
"version": "5.3.38", "version": "5.3.46",
"description": "oak框架中前端与业务逻辑无关的平台部分", "description": "oak框架中前端与业务逻辑无关的平台部分",
"author": { "author": {
"name": "XuChang" "name": "XuChang"
@ -22,9 +22,9 @@
"i18n-js": "^4.3.0", "i18n-js": "^4.3.0",
"node-schedule": "^2.1.1", "node-schedule": "^2.1.1",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"oak-common-aspect": "^3.0.5", "oak-common-aspect": "file:../oak-common-aspect",
"oak-domain": "^5.1.28", "oak-domain": "file:../oak-domain",
"oak-memory-tree-store": "^3.3.13", "oak-memory-tree-store": "file:../oak-memory-tree-store",
"ol": "^7.3.0", "ol": "^7.3.0",
"rc-pagination": "^4.3.0", "rc-pagination": "^4.3.0",
"react-activation": "^0.12.4", "react-activation": "^0.12.4",
@ -64,7 +64,7 @@
"@types/node": "^20.7.0", "@types/node": "^20.7.0",
"@types/node-schedule": "^2.1.0", "@types/node-schedule": "^2.1.0",
"@types/nprogress": "^0.2.3", "@types/nprogress": "^0.2.3",
"@types/react": "^18.3.26", "@types/react": "^18.3.27",
"@types/react-dom": "^18.2.14", "@types/react-dom": "^18.2.14",
"@types/react-native": "^0.72.8", "@types/react-native": "^0.72.8",
"@types/uuid": "^9.0.6", "@types/uuid": "^9.0.6",

View File

@ -60,7 +60,7 @@ export default function Render(
showTitle, showTitle,
} = props.data; } = props.data;
const { t, setPageSize, setCurrentPage } = props.methods; const { t, setPageSize, setCurrentPage } = props.methods;
const { pageSize, total, currentPage, more, count, getTotal } = oakPagination || {}; const { pageSize = 0, total = 0, currentPage = 1, more, count, getTotal } = oakPagination || {};
const paginationRef = useRef<HTMLUListElement>(null); const paginationRef = useRef<HTMLUListElement>(null);
const [internalInputVal, setInternalInputVal] = useState(currentPage); const [internalInputVal, setInternalInputVal] = useState(currentPage);
@ -71,7 +71,7 @@ export default function Render(
if (!oakPagination) { if (!oakPagination) {
return null; return null;
} }
if (total === 0) { if (!total) {
return null return null
} }

View File

@ -108,13 +108,12 @@ export class Navigator extends CommonNavigator {
wx.navigateTo({ wx.navigateTo({
url: url, url: url,
success: () => { success: () => {
this.leave();
this.publish(); this.publish();
resolve(undefined); resolve(undefined);
}, },
fail: (err) => reject(err), fail: (err) => reject(err),
}); });
this.publish();
this.leave();
}); });
} }
@ -133,12 +132,12 @@ export class Navigator extends CommonNavigator {
wx.redirectTo({ wx.redirectTo({
url: url, url: url,
success: () => { success: () => {
resolve(undefined); this.leave();
this.publish(); this.publish();
resolve(undefined);
}, },
fail: (err) => reject(err), fail: (err) => reject(err),
}); });
this.leave();
}); });
} }
@ -156,12 +155,12 @@ export class Navigator extends CommonNavigator {
wx.switchTab({ wx.switchTab({
url: url, url: url,
success: () => { success: () => {
resolve(undefined); this.leave();
this.publish(); this.publish();
resolve(undefined);
}, },
fail: (err) => reject(err), fail: (err) => reject(err),
}); });
this.leave();
}); });
} }
@ -173,12 +172,12 @@ export class Navigator extends CommonNavigator {
wx.navigateBack({ wx.navigateBack({
delta: delta || 1, delta: delta || 1,
success: () => { success: () => {
resolve(undefined); this.leave();
this.publish(); this.publish();
resolve(undefined);
}, },
fail: (err) => reject(err), fail: (err) => reject(err),
}); });
this.leave();
}); });
} }
@ -193,20 +192,14 @@ export class Navigator extends CommonNavigator {
state?: Record<string, any>, state?: Record<string, any>,
disableNamespace?: boolean disableNamespace?: boolean
) { ) {
if (!this.enter()) {
return;
}
const pages = getCurrentPages(); const pages = getCurrentPages();
if (pages.length > 1) { if (pages.length > 1) {
this.leave();
return this.navigateBack(); return this.navigateBack();
} }
const isTabBar = options?.isTabBar; const isTabBar = options?.isTabBar;
if (isTabBar) { if (isTabBar) {
this.leave();
return this.switchTab(options, state, disableNamespace); return this.switchTab(options, state, disableNamespace);
} }
this.leave();
return this.redirectTo(options, state, disableNamespace); return this.redirectTo(options, state, disableNamespace);
} }
} }

View File

@ -109,8 +109,8 @@ export class Navigator extends CommonNavigator {
); );
const replaceAction = StackActions.replace(url, props); const replaceAction = StackActions.replace(url, props);
this.history.dispatch(replaceAction); this.history.dispatch(replaceAction);
this.publish();
this.leave(); this.leave();
this.publish();
} }
async switchTab< async switchTab<
@ -131,8 +131,8 @@ export class Navigator extends CommonNavigator {
); );
const jumpToAction = TabActions.jumpTo(url, props); const jumpToAction = TabActions.jumpTo(url, props);
this.history.dispatch(jumpToAction); this.history.dispatch(jumpToAction);
this.publish();
this.leave(); this.leave();
this.publish();
} }
async navigateBack(delta?: number) { async navigateBack(delta?: number) {
@ -143,9 +143,12 @@ export class Navigator extends CommonNavigator {
if (canGoBack) { if (canGoBack) {
const popAction = StackActions.pop(delta || 1); const popAction = StackActions.pop(delta || 1);
this.history.dispatch(popAction); this.history.dispatch(popAction);
this.leave();
this.publish(); this.publish();
} }
this.leave(); else {
this.leave();
}
} }
navigateBackOrRedirectTo< navigateBackOrRedirectTo<
@ -165,13 +168,13 @@ export class Navigator extends CommonNavigator {
const canGoBack = this.history.canGoBack(); const canGoBack = this.history.canGoBack();
if (canGoBack) { if (canGoBack) {
this.navigateBack(); this.navigateBack();
this.publish();
this.leave(); this.leave();
this.publish();
return; return;
} }
// 回最顶层 // 回最顶层
this.history.dispatch(StackActions.popToTop()); this.history.dispatch(StackActions.popToTop());
this.publish();
this.leave(); this.leave();
this.publish();
} }
} }

View File

@ -82,8 +82,8 @@ export class Navigator extends CommonNavigator {
disableNamespace disableNamespace
); );
this.history.push(url, props); this.history.push(url, props);
this.publish();
this.leave(); this.leave();
this.publish();
} }
async redirectTo< async redirectTo<
@ -103,8 +103,8 @@ export class Navigator extends CommonNavigator {
disableNamespace disableNamespace
); );
this.history.replace(url, props); this.history.replace(url, props);
this.publish();
this.leave(); this.leave();
this.publish();
} }
async switchTab< async switchTab<
@ -130,8 +130,8 @@ export class Navigator extends CommonNavigator {
return; return;
} }
this.history.go(delta ? 0 - delta : -1); this.history.go(delta ? 0 - delta : -1);
this.publish();
this.leave(); this.leave();
this.publish();
} }
navigateBackOrRedirectTo< navigateBackOrRedirectTo<

View File

@ -12,7 +12,7 @@ import { Cache } from './cache';
import { Pagination } from '../types/Pagination'; import { Pagination } from '../types/Pagination';
import { Feature } from '../types/Feature'; import { Feature } from '../types/Feature';
import { ActionDef, CreateDataDef } from '../types/Page'; import { ActionDef, CreateDataDef } from '../types/Page';
import { generateNewId } from 'oak-domain/lib/utils/uuid'; import { generateNewId, generateNewIdAsync } from 'oak-domain/lib/utils/uuid';
export const MODI_NEXT_PATH_SUFFIX = ':next'; export const MODI_NEXT_PATH_SUFFIX = ':next';
const START_LSN = 100; const START_LSN = 100;
@ -664,14 +664,14 @@ class ListNode<
/** /**
* *
* ids中total+1case*/ * ids中total+1case*/
const filter = this.constructFilters(true, true, true); const filters = this.constructFilters(true, true, true);
if (intersected.length) { if (intersected.length) {
const rows = this.cache.get(this.entity, { const rows = this.cache.get(this.entity, {
data: { data: {
id: 1, id: 1,
}, },
filter: combineFilters(this.entity, this.schema, [ filter: combineFilters(this.entity, this.schema, [
...(filter || []), ...(filters || []),
{ {
id: { id: {
$in: intersected, $in: intersected,
@ -691,13 +691,9 @@ class ListNode<
); );
} }
if (diffed.length) { if (diffed.length) {
const filter = filters && combineFilters(e, this.schema, filters);
diffed.forEach( diffed.forEach(
(ele) => { (ele) => tryAddRowToList(ele, filter)
this.sr[ele] = {};
if (this.pagination.total) {
this.pagination.total++;
}
}
); );
} }
} }
@ -1552,7 +1548,7 @@ class SingleNode<ED extends EntityDict & BaseEntityDict,
setId(id: string) { setId(id: string) {
if (id !== this.id) { if (id !== this.id) {
const operations = this.ulManager.makeOperations(); const operations = this.ulManager.makeOperations();
assert(operations.length <= 1); assert(operations.length <= 1, 'singleNode在setId时出现数据不一致');
const [operation] = operations; const [operation] = operations;
if (operation?.action === 'create') { if (operation?.action === 'create') {
if (operation.data.id === id) { if (operation.data.id === id) {
@ -1731,7 +1727,7 @@ class SingleNode<ED extends EntityDict & BaseEntityDict,
setDirty(): void { setDirty(): void {
const id = this.getId(); const id = this.getId();
assert(id); assert(id, "不能对没有id的singleNode设置dirty");
this.ulManager.push(this.ulManager.maxLsn, { this.ulManager.push(this.ulManager.maxLsn, {
action: 'update', action: 'update',
data: {}, data: {},
@ -1763,7 +1759,7 @@ class SingleNode<ED extends EntityDict & BaseEntityDict,
const childOperations = child!.composeOperations(paths?.length ? paths : undefined); const childOperations = child!.composeOperations(paths?.length ? paths : undefined);
if (childOperations) { if (childOperations) {
if (child instanceof SingleNode) { if (child instanceof SingleNode) {
assert(childOperations.length === 1); assert(childOperations.length === 1, 'singleNode在composeOperations时出现数据不一致');
this.ulManager.push(lsnMax + 100, { this.ulManager.push(lsnMax + 100, {
action: 'update', action: 'update',
data: { data: {
@ -1775,7 +1771,7 @@ class SingleNode<ED extends EntityDict & BaseEntityDict,
} as any); } as any);
} }
else { else {
assert(child instanceof ListNode); assert(child instanceof ListNode, 'child必须是ListNode');
this.ulManager.push(lsnMax + 100, { this.ulManager.push(lsnMax + 100, {
action: 'update', action: 'update',
data: { data: {
@ -1872,8 +1868,8 @@ class SingleNode<ED extends EntityDict & BaseEntityDict,
const rel = this.judgeRelation(k2); const rel = this.judgeRelation(k2);
if (rel === 2) { if (rel === 2) {
if (value?.entityId) { if (value?.entityId) {
assert(child instanceof SingleNode); assert(child instanceof SingleNode, 'child必须是singleNode');
assert(value.entity === child.getEntity()); assert(value.entity === child.getEntity(), 'singleNode的entity必须一致');
child.saveRefreshResult({ child.saveRefreshResult({
[value.entityId!]: this.sr[k2] || {}, [value.entityId!]: this.sr[k2] || {},
}); });
@ -1969,7 +1965,7 @@ class SingleNode<ED extends EntityDict & BaseEntityDict,
} }
} }
private getFilter(ignoreNew?: true, onlyHot?: true): ED[T]['Filter'] | undefined { getFilter(ignoreNew?: true, onlyHot?: true): ED[T]['Filter'] | undefined {
// 如果是新建等于没有filter // 如果是新建等于没有filter
const [operation] = this.ulManager.makeOperations(); const [operation] = this.ulManager.makeOperations();
if (operation?.action === 'create') { if (operation?.action === 'create') {
@ -2441,6 +2437,7 @@ export class RunningTree<ED extends EntityDict & BaseEntityDict> extends Feature
assert(!parentNode || parentNode instanceof VirtualNode); assert(!parentNode || parentNode instanceof VirtualNode);
node = new VirtualNode(fullPath, path, parentNode, stale); node = new VirtualNode(fullPath, path, parentNode, stale);
} }
// 任何临时的修改都需要回滚上面缓存了一些update操作
rollback(); rollback();
if (!parentNode) { if (!parentNode) {
assert(!parent && !this.root[path]); assert(!parent && !this.root[path]);
@ -2974,7 +2971,7 @@ export class RunningTree<ED extends EntityDict & BaseEntityDict> extends Feature
node && node.setExecuting(true); node && node.setExecuting(true);
let pollute = false; // let pollute = false;
try { try {
let operations = path && this.getOperations(path) || []; let operations = path && this.getOperations(path) || [];
if (opers) { if (opers) {
@ -2994,15 +2991,30 @@ export class RunningTree<ED extends EntityDict & BaseEntityDict> extends Feature
else if (node) { else if (node) {
// 老的写法直接对一个非脏的结点execute某个action也可以支持 // 老的写法直接对一个非脏的结点execute某个action也可以支持
assert(node instanceof SingleNode); assert(node instanceof SingleNode);
node.update(this.logSerailNumber, {}, action); // node.update(this.logSerailNumber, {}, action);
pollute = true; // pollute = true;
operations = node.composeOperations() || []; // operations = node.composeOperations() || [];
assert(operations.length === 1); // assert(operations.length === 1);
const [operation1] = operations; // const [operation1] = operations;
if (action !== operation1.operation.action) { // if (action !== operation1.operation.action) {
assert(operation1.operation.action === 'update'); // 如果execute时传action前面update动作应该只可能是update // assert(operation1.operation.action === 'update'); // 如果execute时传action前面update动作应该只可能是update
operation1.operation.action = action; // operation1.operation.action = action;
} // }
/**
* publish行为close动作会导致页面像下面这样渲染
* opened --> closed(node.update) --> opened(setExecuting时) --> closedsync cache后
*/
operations.push({
entity: node.getEntity(),
operation: {
id: await generateNewIdAsync(),
action,
data: {
$$updateAt$$: Date.now(),
},
filter: node.getFilter(true),
}
});
} }
} }
if (operations.length > 0) { if (operations.length > 0) {
@ -3042,9 +3054,9 @@ export class RunningTree<ED extends EntityDict & BaseEntityDict> extends Feature
return { message: 'No Operation' }; return { message: 'No Operation' };
} catch (err) { } catch (err) {
node && node.setExecuting(false); node && node.setExecuting(false);
if (pollute) { /* if (pollute) {
path && this.clean(path); path && this.clean(path);
} } */
throw err; throw err;
} }
} }

View File

@ -127,13 +127,13 @@ export class SubScriber<ED extends EntityDict & BaseEntityDict> extends Feature
socket.on( socket.on(
'data', 'data',
(opRecords: OpRecord<ED>[], event: string) => { (opRecords: OpRecord<ED>[], event: string) => {
this.cache.sync(opRecords);
const registered = this.eventMap[event]; const registered = this.eventMap[event];
if (registered) { if (registered) {
registered.callbacks.forEach( registered.callbacks.forEach(
(ele) => ele(event, opRecords) (ele) => ele(event, opRecords)
); );
} }
this.cache.sync(opRecords);
} }
); );

View File

@ -773,6 +773,13 @@ function translatePropertiesToPropertyDefinitions(properties?: DataOption) {
if (properties) { if (properties) {
Object.keys(properties).forEach( Object.keys(properties).forEach(
(prop) => { (prop) => {
if (properties[prop] === null) {
definitions[prop] = {
type: null,
value: null,
};
return;
}
switch (typeof properties[prop]) { switch (typeof properties[prop]) {
case 'string': { case 'string': {
if (properties[prop]) { if (properties[prop]) {
@ -851,7 +858,7 @@ export function createComponent<
T extends keyof ED, T extends keyof ED,
Cxt extends AsyncContext<ED>, Cxt extends AsyncContext<ED>,
FrontCxt extends SyncContext<ED>, FrontCxt extends SyncContext<ED>,
AD extends Record<string, Aspect<ED, AsyncContext<ED>>>, AD extends Record<string, Aspect<ED, Cxt>>,
FD extends Record<string, Feature>, FD extends Record<string, Feature>,
FormedData extends Record<string, any>, FormedData extends Record<string, any>,
TData extends DataOption = {}, TData extends DataOption = {},

View File

@ -22,7 +22,7 @@ export function createComponent<
T extends keyof ED, T extends keyof ED,
Cxt extends AsyncContext<ED>, Cxt extends AsyncContext<ED>,
FrontCxt extends SyncContext<ED>, FrontCxt extends SyncContext<ED>,
AD extends Record<string, Aspect<ED, AsyncContext<ED>>>, AD extends Record<string, Aspect<ED, Cxt>>,
FD extends Record<string, Feature>, FD extends Record<string, Feature>,
FormedData extends Record<string, any>, FormedData extends Record<string, any>,
TData extends Record<string, any> = {}, TData extends Record<string, any> = {},

View File

@ -34,7 +34,7 @@ abstract class OakComponentBase<
T extends keyof ED, T extends keyof ED,
Cxt extends AsyncContext<ED>, Cxt extends AsyncContext<ED>,
FrontCxt extends SyncContext<ED>, FrontCxt extends SyncContext<ED>,
AD extends Record<string, Aspect<ED, AsyncContext<ED>>>, AD extends Record<string, Aspect<ED, Cxt>>,
FD extends Record<string, Feature>, FD extends Record<string, Feature>,
FormedData extends Record<string, any>, FormedData extends Record<string, any>,
IsList extends boolean, IsList extends boolean,
@ -650,7 +650,7 @@ export function createComponent<
T extends keyof ED, T extends keyof ED,
Cxt extends AsyncContext<ED>, Cxt extends AsyncContext<ED>,
FrontCxt extends SyncContext<ED>, FrontCxt extends SyncContext<ED>,
AD extends Record<string, Aspect<ED, AsyncContext<ED>>>, AD extends Record<string, Aspect<ED, Cxt>>,
FD extends Record<string, Feature>, FD extends Record<string, Feature>,
FormedData extends Record<string, any>, FormedData extends Record<string, any>,
TData extends Record<string, any> = {}, TData extends Record<string, any> = {},

View File

@ -22,7 +22,7 @@ export function createComponent<
T extends keyof ED, T extends keyof ED,
Cxt extends AsyncContext<ED>, Cxt extends AsyncContext<ED>,
FrontCxt extends SyncContext<ED>, FrontCxt extends SyncContext<ED>,
AD extends Record<string, Aspect<ED, AsyncContext<ED>>>, AD extends Record<string, Aspect<ED, Cxt>>,
FD extends Record<string, Feature>, FD extends Record<string, Feature>,
FormedData extends Record<string, any>, FormedData extends Record<string, any>,
TData extends Record<string, any> = {}, TData extends Record<string, any> = {},

View File

@ -317,7 +317,7 @@ export type OakComponentOption<
T extends keyof ED, T extends keyof ED,
Cxt extends AsyncContext<ED>, Cxt extends AsyncContext<ED>,
FrontCxt extends SyncContext<ED>, FrontCxt extends SyncContext<ED>,
AD extends Record<string, Aspect<ED, AsyncContext<ED>>>, AD extends Record<string, Aspect<ED, Cxt>>,
FD extends Record<string, Feature>, FD extends Record<string, Feature>,
FormedData extends Record<string, any>, FormedData extends Record<string, any>,
TData extends Record<string, any>, TData extends Record<string, any>,
@ -599,7 +599,7 @@ export type MakeOakComponent<
ED extends EntityDict & BaseEntityDict, ED extends EntityDict & BaseEntityDict,
Cxt extends AsyncContext<ED>, Cxt extends AsyncContext<ED>,
FrontCxt extends SyncContext<ED>, FrontCxt extends SyncContext<ED>,
AD extends Record<string, Aspect<ED, AsyncContext<ED>>>, AD extends Record<string, Aspect<ED, Cxt>>,
FD extends Record<string, Feature> FD extends Record<string, Feature>
> = < > = <
IsList extends boolean, IsList extends boolean,

39
src/types/Upload.ts Normal file
View File

@ -0,0 +1,39 @@
type UploadFileFn = (options: {
// 需要上传的内容
file: string | File | Blob;
// 表单字段名
name: string;
// 上传地址
uploadUrl: string;
// 额外的表单数据
formData: Record<string, any>;
// 是否自动通知上传结果
autoInform?: boolean;
// 进度回调函数
getPercent?: Function;
// 上传任务ID用于中断特定上传
uploadId?: string;
// HTTP方法
method?: "POST" | "PUT" | "PATCH";
// 是否为文件路径主要用于小程序在PUT模式下需要
isFilePath?: boolean;
}) => Promise<{
status: number;
statusText: string;
statusCode?: number;
headers: {
get(name: string): string | null;
};
json(): Promise<any>;
text(): Promise<string>;
errMsg?: string;
data?: any;
}>;
export interface UploadInterface {
uploadFile: UploadFileFn;
abortUpload(uploadId: string): boolean;
abortAllUploads(): void;
getUploadStatus(uploadId: string): 'uploading' | 'completed' | 'aborted' | 'not-found';
getActiveUploads(): string[];
}

View File

@ -1,50 +1,67 @@
import { UploadInterface } from '../types/Upload';
import { promisify } from './promisify'; import { promisify } from './promisify';
export class Upload { export class Upload implements UploadInterface {
abortUpload(uploadId: string): boolean {
return false;
}
abortAllUploads(): void {
}
getUploadStatus(uploadId: string): 'uploading' | 'completed' | 'aborted' | 'not-found' {
return 'not-found';
}
getActiveUploads(): string[] {
return [];
}
async uploadFile( async uploadFile(
file: string | File, options: {
name: string, file: string | File | Blob,
uploadUrl: string, name: string,
formData: Record<string, any>, uploadUrl: string,
autoInform?: boolean, formData: Record<string, any>,
getPercent?: Function, autoInform?: boolean,
method: "POST" | "PUT" | "PATCH" = "POST" getPercent?: Function,
uploadId?: string, // 新增上传任务ID用于中断特定上传
method?: "POST" | "PUT" | "PATCH"
isFilePath?: boolean
}
): Promise<any> { ): Promise<any> {
const { file, name, uploadUrl, formData, isFilePath, method = "POST" } = options;
const isPut = method === "PUT"; const isPut = method === "PUT";
if (isPut) { if (isPut) {
return new Promise((resolve, reject) => { if (isFilePath) {
const fs = wx.getFileSystemManager(); return new Promise((resolve, reject) => {
fs.readFile({ const fs = wx.getFileSystemManager();
filePath: file as string, fs.readFile({
success: (fileRes) => { filePath: file as string,
// 使用 PUT 方法上传 encoding: 'binary',
wx.request({ success: res => {
url: uploadUrl, resolve(global.fetch(uploadUrl, {
method: 'PUT', method: "PUT",
data: fileRes.data, // ArrayBuffer 格式 headers: {
header: { 'Content-Type': 'application/octet-stream',
'Content-Type': 'image/jpeg', // 根据实际文件类型设置 },
}, body: res.data,
success: (uploadRes) => { }));
if (uploadRes.statusCode === 200) { }
resolve(uploadRes); ,
} else { fail: err => {
reject(new Error(`HTTP Error: ${uploadRes.statusCode}`)); reject(err);
} }
}, });
fail: (err) => {
console.error('上传失败', err);
reject(err);
}
});
},
fail: (err) => {
console.error('读取文件失败', err);
reject(err);
}
}); });
}); }
return global.fetch(uploadUrl, {
method: "PUT",
headers: {
'Content-Type': 'application/octet-stream',
},
body: file as any,
})
} else { } else {

View File

@ -1,15 +1,34 @@
import { UploadInterface } from "../types/Upload";
export class Upload { export class Upload implements UploadInterface {
abortUpload(uploadId: string): boolean {
return false;
}
abortAllUploads(): void {
}
getUploadStatus(uploadId: string): 'uploading' | 'completed' | 'aborted' | 'not-found' {
return 'not-found';
}
getActiveUploads(): string[] {
return [];
}
async uploadFile( async uploadFile(
file: File | string, options: {
name: string, file: File | string | Blob,
uploadUrl: string, name: string,
formData: Record<string, any>, uploadUrl: string,
autoInform?: boolean, formData: Record<string, any>,
getPercent?: Function, autoInform?: boolean,
method: "POST" | "PUT" | "PATCH" = "POST" getPercent?: Function,
uploadId?: string, // 新增上传任务ID用于中断特定上传
method?: "POST" | "PUT" | "PATCH"
}
): Promise<any> { ): Promise<any> {
const { file, name, uploadUrl, formData, method = "POST" } = options;
const isPut = method === "PUT"; const isPut = method === "PUT";
if (isPut) { if (isPut) {

View File

@ -1,12 +1,57 @@
import { UploadInterface } from "../types/Upload";
export class Upload {
export class Upload implements UploadInterface {
async uploadFile( async uploadFile(
file: string | File, options: {
name: string, file: string | File | Blob,
uploadUrl: string, name: string,
formData: Record<string, any>, uploadUrl: string,
autoInform?: boolean formData: Record<string, any>,
autoInform?: boolean,
getPercent?: Function,
uploadId?: string, // 新增上传任务ID用于中断特定上传
method?: "POST" | "PUT" | "PATCH",
isFilePath?: boolean,
}
): Promise<any> { ): Promise<any> {
console.warn('server不会调用此函数') console.warn('server不会调用此函数')
} }
private controllers: Map<string, AbortController> = new Map();
constructor() {
this.uploadFile = this.uploadFile.bind(this);
this.abortUpload = this.abortUpload.bind(this);
this.abortAllUploads = this.abortAllUploads.bind(this);
this.getUploadStatus = this.getUploadStatus.bind(this);
this.getActiveUploads = this.getActiveUploads.bind(this);
}
// 中断特定上传
abortUpload(uploadId: string): boolean {
console.warn('server不会调用此函数')
return false;
}
// 中断所有上传
abortAllUploads(): void {
console.warn('server不会调用此函数')
}
// 获取上传状态
getUploadStatus(uploadId: string): 'uploading' | 'completed' | 'aborted' | 'not-found' {
console.warn('server不会调用此函数')
return 'uploading';
}
// 生成唯一的上传ID
private generateUploadId(file: File | string, uploadUrl: string): string {
return `server不会调用此函数`;
}
// 获取所有进行中的上传任务
getActiveUploads(): string[] {
return [];
}
} }

View File

@ -1,16 +1,42 @@
import { OakUserException } from "oak-domain/lib/types";
import { UploadInterface } from "../types/Upload";
export class Upload implements UploadInterface {
private controllers: Map<string, AbortController> = new Map();
constructor() {
this.uploadFile = this.uploadFile.bind(this);
this.abortUpload = this.abortUpload.bind(this);
this.abortAllUploads = this.abortAllUploads.bind(this);
this.getUploadStatus = this.getUploadStatus.bind(this);
this.getActiveUploads = this.getActiveUploads.bind(this);
}
export class Upload {
async uploadFile( async uploadFile(
file: File | string, options: {
name: string, file: File | string | Blob,
uploadUrl: string, name: string,
formData: Record<string, any>, uploadUrl: string,
autoInform?: boolean, formData: Record<string, any>,
getPercent?: Function, autoInform?: boolean,
method: "POST" | "PUT" | "PATCH" = "POST" getPercent?: Function,
uploadId?: string, // 新增上传任务ID用于中断特定上传
method?: "POST" | "PUT" | "PATCH"
}
): Promise<any> { ): Promise<any> {
const { file, name, uploadUrl, formData, getPercent, uploadId, method = "POST" } = options;
const isPut = method === "PUT"; const isPut = method === "PUT";
const id = uploadId || this.generateUploadId(file, uploadUrl);
// 如果已有相同ID的上传在进行先中断它
if (this.controllers.has(id)) {
this.abortUpload(id);
}
// 创建新的 AbortController
const controller = new AbortController();
this.controllers.set(id, controller);
// 进度监听模式 // 进度监听模式
if (getPercent) { if (getPercent) {
@ -18,6 +44,12 @@ export class Upload {
const xhr = new XMLHttpRequest(); const xhr = new XMLHttpRequest();
let percent = 0; let percent = 0;
// 监听中止信号
controller.signal.addEventListener('abort', () => {
xhr.abort();
reject(new DOMException('Upload aborted', 'AbortError'));
});
xhr.upload.addEventListener("progress", (event) => { xhr.upload.addEventListener("progress", (event) => {
if (event.lengthComputable) { if (event.lengthComputable) {
percent = Math.round((event.loaded / event.total) * 100); percent = Math.round((event.loaded / event.total) * 100);
@ -26,15 +58,35 @@ export class Upload {
}); });
xhr.onload = () => { xhr.onload = () => {
this.controllers.delete(id); // 清理控制器
// 构造类似 Response 的对象,支持 headers.get()
const headersMap = new Map<string, string>();
const headersStr = xhr.getAllResponseHeaders();
headersStr.split('\r\n').forEach(line => {
const parts = line.split(': ');
if (parts.length === 2) {
headersMap.set(parts[0].toLowerCase(), parts[1]);
}
});
const headers = {
get: (name: string) => headersMap.get(name.toLowerCase()) || null,
has: (name: string) => headersMap.has(name.toLowerCase()),
forEach: (callback: (value: string, key: string) => void) => {
headersMap.forEach(callback);
}
};
if (xhr.status >= 200 && xhr.status < 300) { if (xhr.status >= 200 && xhr.status < 300) {
if (xhr.status === 204) { if (xhr.status === 204) {
resolve({ status: 204 }); resolve({ status: 204, headers });
} else { } else {
try { try {
const data = JSON.parse(xhr.responseText); const data = JSON.parse(xhr.responseText);
resolve(data); resolve({ ...data, status: xhr.status, headers });
} catch { } catch {
resolve({ status: xhr.status, raw: xhr.responseText }); resolve({ status: xhr.status, raw: xhr.responseText, headers });
} }
} }
} else { } else {
@ -42,13 +94,29 @@ export class Upload {
} }
}; };
xhr.onerror = () => reject(new Error("Network Error")); xhr.onerror = () => {
this.controllers.delete(id);
// 如果不是因为中止导致的错误
if (!controller.signal.aborted) {
reject(new Error("Network Error"));
}
};
xhr.onabort = () => {
this.controllers.delete(id);
if (controller.signal.aborted) {
reject(new DOMException('Upload aborted', 'AbortError'));
}
};
xhr.open(method, uploadUrl); xhr.open(method, uploadUrl);
if (isPut) { if (isPut) {
// PUT 模式:直接上传文件 // PUT 模式:直接上传文件
if (file instanceof File) { if (file instanceof File) {
xhr.setRequestHeader("Content-Type", file.type || "application/octet-stream"); xhr.setRequestHeader("Content-Type", file.type || "application/octet-stream");
} else if (file instanceof Blob) {
xhr.setRequestHeader("Content-Type", "application/octet-stream");
} }
xhr.send(file as any); xhr.send(file as any);
} else { } else {
@ -64,32 +132,90 @@ export class Upload {
} }
// 无进度监听模式(直接 fetch // 无进度监听模式(直接 fetch
if (isPut) { try {
// S3 预签名上传 let result: Response;
const headers: Record<string, string> = {};
if (file instanceof File) { if (isPut) {
headers["Content-Type"] = file.type || "application/octet-stream"; // S3 预签名上传
const headers: Record<string, string> = {};
if (file instanceof File) {
headers["Content-Type"] = file.type || "application/octet-stream";
} else if (file instanceof Blob) {
headers["Content-Type"] = "application/octet-stream";
}
result = await fetch(uploadUrl, {
method: "PUT",
headers,
body: file as File | Blob,
signal: controller.signal, // 添加中止信号
});
} else {
// 表单上传
const formData2 = new FormData();
for (const key of Object.keys(formData)) {
formData2.append(key, formData[key]);
}
formData2.append(name || "file", file as File);
result = await fetch(uploadUrl, {
method,
body: formData2,
signal: controller.signal, // 添加中止信号
});
} }
const result = await fetch(uploadUrl, { this.controllers.delete(id); // 成功后清理控制器
method: "PUT",
headers,
body: file as any,
});
return result; return result;
} else {
// 表单上传
const formData2 = new FormData();
for (const key of Object.keys(formData)) {
formData2.append(key, formData[key]);
}
formData2.append(name || "file", file as File);
const result = await fetch(uploadUrl, { } catch (error) {
method, this.controllers.delete(id); // 失败后清理控制器
body: formData2,
}); // 人为中断返回204 使general-business处理成功
return result; if (error instanceof DOMException && error.name === 'AbortError') {
throw new DOMException('Upload aborted', 'AbortError');
}
throw error;
} }
} }
}
// 中断特定上传
abortUpload(uploadId: string): boolean {
const controller = this.controllers.get(uploadId);
if (controller) {
controller.abort();
this.controllers.delete(uploadId);
return true;
}
return false;
}
// 中断所有上传
abortAllUploads(): void {
this.controllers.forEach((controller, id) => {
controller.abort();
});
this.controllers.clear();
}
// 获取上传状态
getUploadStatus(uploadId: string): 'uploading' | 'completed' | 'aborted' | 'not-found' {
const controller = this.controllers.get(uploadId);
if (!controller) return 'not-found';
if (controller.signal.aborted) return 'aborted';
return 'uploading';
}
// 生成唯一的上传ID
private generateUploadId(file: File | string | Blob, uploadUrl: string): string {
const timestamp = Date.now();
const random = Math.random().toString(36).substring(2, 9);
const fileInfo = file instanceof File ? `${file.name}-${file.size}` : file instanceof Blob ? `blob-${file.size}` : file;
return `${uploadUrl}-${fileInfo}-${timestamp}-${random}`;
}
// 获取所有进行中的上传任务
getActiveUploads(): string[] {
return Array.from(this.controllers.keys());
}
}