Merge pull request !25 from dgflash/develop
This commit is contained in:
dgflash
2026-02-23 12:38:00 +00:00
committed by Gitee
23 changed files with 237 additions and 524 deletions

View File

@@ -9,6 +9,7 @@ import { director, isValid } from 'cc';
import { GameComponent } from '../../module/common/GameComponent';
import { resLoader } from '../common/loader/ResLoader';
import { ViewUtil } from '../utils/ViewUtil';
import { View } from '../../types/Types';
/** 游戏元素打开参数 */
export interface ElementParams {
@@ -37,7 +38,7 @@ export class GameManager {
* @param params 可选参数据
* @returns Promise<Node | null> 成功返回节点,失败返回 null
*/
async open(parent: Node | GameComponent, prefabPath: string, params?: ElementParams): Promise<Node | null> {
async open(parent: View, prefabPath: string, params?: ElementParams): Promise<Node | null> {
try {
// 简化 bundleName 获取逻辑
const bundleName = params?.bundle || resLoader.defaultBundleName;
@@ -61,7 +62,7 @@ export class GameManager {
return null;
}
node.parent = parent;
// 记录手动管理的节点,便于后续释放
this._manualNodes.add(node);
}
@@ -144,7 +145,7 @@ export class GameManager {
destroy(): void {
// 释放所有手动管理的节点
this.releaseAllManualNodes();
// 清理引用
this.root = null!;
}

View File

@@ -1,3 +1,4 @@
import type { GameComponentCtor, UICtor } from '../../types/Types';
import type { UIConfigMap } from './layer/LayerEnum';
import type { UIConfig } from './layer/UIConfig';
@@ -5,10 +6,9 @@ const configs: UIConfigMap = {};
export namespace gui {
/** 注册界面组件 */
export function register(key: string, config: UIConfig) {
return function (ctor: any) {
//@ts-ignore
ctor[gui.internal.GUI_KEY] = key;
export function register(key: string, config: UIConfig): (ctor: GameComponentCtor) => void {
return function (ctor: GameComponentCtor): void {
(ctor as any)[gui.internal.GUI_KEY] = key;
internal.setConfig(key, config);
};
}
@@ -19,28 +19,26 @@ export namespace gui {
export const GUI_KEY = 'OOPS_GUI_KEY';
/** 获取界面唯一关键字 */
export function getKey(ctor: any) {
return ctor[GUI_KEY];
export function getKey(ctor: UICtor): string {
return (ctor as any)[GUI_KEY];
}
/** 获取界面组件配置 */
export function getConfig(key: string) {
export function getConfig(key: string): UIConfig {
return configs[key];
}
/** 获取界面组件配置 */
export function setConfig(key: string, config: UIConfig) {
const c = getConfig(key);
if (c == null) {
configs[key] = config;
}
else {
/** 设置界面组件配置 */
export function setConfig(key: string, config: UIConfig): void {
if (configs[key] != null) {
console.error(`界面${key}重复注册`);
return;
}
configs[key] = config;
}
/** 初始化界面组件配置 */
export function initConfigs(uicm: UIConfigMap) {
export function initConfigs(uicm: UIConfigMap): void {
for (const key in uicm) {
configs[key] = uicm[key];
}

View File

@@ -269,7 +269,7 @@ export class ECSEntity {
remove(ctor: CompType<ecs.IComp>, isRecycle = true): void {
const componentTypeId = typeof ctor === 'number' ? ctor : ctor.tid;
const compName = typeof ctor === 'number' ? '' : ctor.compName;
if (!this.mask.has(componentTypeId)) {
return;
}
@@ -280,7 +280,7 @@ export class ECSEntity {
}
comp.ent = null!;
if (isRecycle) {
comp.reset();
@@ -296,7 +296,8 @@ export class ECSEntity {
// 限制缓存组件数量,防止内存泄漏
if (this.compTid2Obj.size < ECSEntity.MAX_CACHE_COMP) {
this.compTid2Obj.set(componentTypeId, comp); // 用于缓存显示对象组件
} else {
}
else {
// 超过限制,强制回收
console.warn(`实体 ${this.name} 缓存组件数量超过限制,强制回收组件 ${compName}`);
comp.reset();
@@ -338,10 +339,10 @@ export class ECSEntity {
// 移除实体上所有组件
this.compTid2Ctor.forEach(this._remove, this);
destroyEntity(this);
// 清理缓存的组件对象,防止内存泄漏
this.compTid2Obj.clear();
// 回收 mask 到对象池
this.mask.destroy();
}

View File

@@ -21,7 +21,7 @@ export class CCBusiness<T extends CCEntity> {
destroy() {
// 释放消息对象
if (this._event) {
this._event.destroy();
this._event.clear();
this._event = null;
}

View File

@@ -1,27 +1,20 @@
import type { __private, Node } from 'cc';
import { resLoader } from '../../core/common/loader/ResLoader';
import type { Node } from 'cc';
import { oops } from '../../core/Oops';
import { gui } from '../../core/gui/Gui';
import type { UIParam } from '../../core/gui/layer/LayerUIElement';
import { LayerUIElement } from '../../core/gui/layer/LayerUIElement';
import { oops } from '../../core/Oops';
import { ViewUtil } from '../../core/utils/ViewUtil';
import { ecs } from '../../libs/ecs/ECS';
import type { ECSEntity } from '../../libs/ecs/ECSEntity';
import type { CompType } from '../../libs/ecs/ECSModel';
import type { BusinessCtor, EntityCtor, UICtor, View, ViewCtor } from '../../types/Types';
import type { CCBusiness } from './CCBusiness';
import type { CCView } from './CCView';
import { GameComponent } from './GameComponent';
type ECSCtor<T extends ecs.Comp> =
| __private.__types_globals__Constructor<T>
| __private.__types_globals__AbstractedConstructor<T>;
type ECSView = CCView<CCEntity>;
type EntityCtor<T extends CCEntity = CCEntity> = new (...args: any[]) => T;
type BusinessCtor<T extends CCBusiness<CCEntity> = CCBusiness<CCEntity>> = new (...args: any[]) => T;
/** ECS 游戏模块实体 */
export abstract class CCEntity extends ecs.Entity {
//#region 子模块管理
/** 单例子实体集合key: 实体类构造函数value: 实体实例) */
private singletons: Map<EntityCtor, ECSEntity> = null!;
@@ -84,50 +77,66 @@ export abstract class CCEntity extends ecs.Entity {
//#region 游戏视图层管理
/**
* 通过资源内存中获取预制上的组件添加到ECS实体中
* @param ctor 界面逻辑组件
* @param ctor 界面逻辑组件(支持 ECSView 或使用 gui.register 注册的 GameComponent
* @param parent 显示对象父级
* @param path 显示资源地址
* @param bundleName 资源包名称
* @param path 显示资源地址(可选,不传时使用 @game.prefab 装饰器注册的路径)
* @param bundleName 资源包名称(可选,不传时使用 @game.prefab 装饰器注册的包名)
*/
async addPrefab<T extends ECSView>(
ctor: ECSCtor<T>,
parent: Node | GameComponent,
path: string,
bundleName: string = resLoader.defaultBundleName
async addPrefab<T extends GameComponent>(
ctor: ViewCtor<T>,
parent: View,
path?: string,
bundleName?: string
): Promise<Node> {
// 未传入路径时,从装饰器注册的数据中获取
if (path == null) {
path = (ctor as any).GAME_PREFAB_PATH;
bundleName = (ctor as any).GAME_PREFAB_BUNDLE;
if (path == null) {
throw new Error(`组件 ${(ctor as any).name} 未使用 @game.prefab 装饰器注册,请添加 @game.prefab('path/to/prefab') 装饰器或手动传入路径参数`);
}
}
let node: Node;
// 跟随父节点释放自动释放当前资源
if (parent instanceof GameComponent) {
node = await parent.createPrefabNode(path, bundleName);
const comp = node.getComponent(ctor);
if (!comp) {
throw new Error(`组件 ${ctor.name} 不存在于预制 ${path}`);
}
this.add(comp);
if (comp) this.add(comp as unknown as ecs.Comp);
node.parent = parent.node;
}
// 手动内存管理
else {
node = await ViewUtil.createPrefabNodeAsync(path, bundleName);
const comp = node.getComponent(ctor);
if (!comp) {
throw new Error(`组件 ${ctor.name} 不存在于预制 ${path}`);
}
this.add(comp);
if (comp) this.add(comp as unknown as ecs.Comp);
node.parent = parent;
}
return node;
}
/**
* 移除预制体组件及其节点
* @param node 要销毁的节点或组件Node 或 GameComponent
*/
removePrefab(node: View): void {
if (node instanceof GameComponent) {
node.remove();
}
else {
node.destroy();
}
}
/**
* 添加视图层组件
* @param ctor 界面逻辑组件
* @param ctor 界面逻辑组件(支持 CCView 或使用 gui.register 注册的 GameComponent 子类)
* @param params 界面参数
* @returns 界面节点
*/
async addUi<T extends ECSView>(ctor: ECSCtor<T>, params?: UIParam): Promise<Node> {
async addUi<T extends GameComponent>(ctor: UICtor<T>, params?: UIParam): Promise<Node> {
const key = gui.internal.getKey(ctor);
if (!key) {
throw new Error(`${ctor.name} 界面组件未使用 gui.register 注册`);
@@ -146,21 +155,18 @@ export abstract class CCEntity extends ecs.Entity {
}
const node = await oops.gui.open(key, params);
const comp = node.getComponent(ctor) as ecs.Comp;
if (!comp) {
throw new Error(`界面节点上未找到组件 ${ctor.name}`);
}
const comp = node.getComponent(ctor) as unknown as ecs.Comp;
if (comp) this.add(comp);
this.add(comp);
oops.gui.show(key);
return node;
}
/**
* 移除视图层组件
* @param ctor 界面逻辑组件
* @param ctor 界面逻辑组件(支持 CCView 或使用 gui.register 注册的 GameComponent 子类)
*/
removeUi(ctor: CompType<ecs.IComp>) {
removeUi(ctor: UICtor) {
const key = gui.internal.getKey(ctor);
if (key) {
@@ -170,12 +176,13 @@ export abstract class CCEntity extends ecs.Entity {
return;
}
const comp = node.getComponent(LayerUIElement);
if (comp) {
const layer = node.getComponent(LayerUIElement);
if (layer) {
// 处理界面关闭动画播放完成后移除ECS组件避免使用到组件实体数据还在动画播放时在使用导致的空对象问题
comp.onClose = () => {
layer.onClose = () => {
try {
this.remove(ctor);
const view = node.getComponent(ctor) as unknown as ecs.Comp;
if (view) this.remove(ctor as unknown as CompType<ecs.IComp>);
}
catch (error) {
console.error(`移除界面组件失败: ${key}`, error);
@@ -185,11 +192,12 @@ export abstract class CCEntity extends ecs.Entity {
}
else {
// 没有 LayerUIElement直接移除
this.remove(ctor);
this.remove(ctor as unknown as CompType<ecs.IComp>);
}
}
else {
this.remove(ctor);
// 组件未使用 gui.register 注册,尝试直接移除
this.remove(ctor as unknown as CompType<ecs.IComp>);
}
}
//#endregion
@@ -224,6 +232,10 @@ export abstract class CCEntity extends ecs.Entity {
//@ts-ignore
business.init();
this.businesss.set(cls, business);
// 将业务逻辑组件直接附加到实体对象身上,方便直接获取
Reflect.set(this, cls.name, business);
return business as T;
}
@@ -247,6 +259,9 @@ export abstract class CCEntity extends ecs.Entity {
if (business) {
business.destroy();
this.businesss.delete(cls);
// 清理实体上的业务逻辑组件引用
Reflect.set(this, cls.name, null);
}
}
}

View File

@@ -11,6 +11,7 @@ import { ECSModel } from '../../libs/ecs/ECSModel';
import { VM } from '../../libs/model-view/ViewModel';
import { VMBase } from '../../libs/model-view/VMBase';
import type { CCEntity } from './CCEntity';
import type { UICtor } from '../../types/Types';
import { GameComponent } from './GameComponent';
/**
@@ -43,7 +44,7 @@ export class RoleViewComp extends CCView<Role> {
@ecs.register('LoadingView', false)
export class LoadingViewComp extends CCView<Initialize> {
protected mvvm = true; // 启用 MVVM 功能
data: LoadingData = {
finished: 0,
total: 0,
@@ -70,20 +71,19 @@ export abstract class CCView<T extends CCEntity> extends GameComponent implement
//#region MVVM 功能相关(仅在 mvvm = true 时使用)
/** 是否启用 MVVM 功能(子类可覆盖为 true */
protected mvvm: boolean = false;
/**
protected mvvm = false;
/**
* MVVM 绑定的标签,延迟初始化以节省内存
* 仅在启用 MVVM 时创建
*/
protected tag?: string;
/**
/**
* 需要绑定的私有数据
* 注意:子类应该显式初始化此属性
*/
protected data?: any;
//#endregion
/**
* 组件加载时调用
@@ -109,11 +109,11 @@ export abstract class CCView<T extends CCEntity> extends GameComponent implement
// 优化:只在必要时替换点号,使用更快的 replaceAll如果支持
this.tag = `_temp<${uuid.replace('.', '')}>`;
VM.add(this.data!, this.tag);
// 搜寻所有节点:找到 watch path
const comps = this.getVMComponents();
const len = comps.length;
// 优化:避免属性查找,缓存 tag
const tag = this.tag;
for (let i = 0; i < len; i++) {
@@ -140,10 +140,10 @@ export abstract class CCView<T extends CCEntity> extends GameComponent implement
* @private
*/
private replaceVMPath(comp: Component, tag: string) {
// @ts-ignore - 优化:使用 any 类型避免多次类型转换
// 优化:使用 any 类型避免多次类型转换
const vmComp: any = comp;
const path: string = vmComp.watchPath;
// 优化:使用严格相等避免类型转换
if (vmComp.templateMode === true) {
const pathArr: string[] = vmComp.watchPathArr;
@@ -171,20 +171,20 @@ export abstract class CCView<T extends CCEntity> extends GameComponent implement
*/
private getVMComponents(): Component[] {
const comps = this.node.getComponentsInChildren(VMBase);
// 优化:提前返回,避免不必要的计算
if (comps.length === 0) {
return comps;
}
// 优化:只在有嵌套 CCView 时才获取 parents
const parents = this.node.getComponentsInChildren(CCView);
// 优化:使用数组长度判断,避免创建新数组
let hasNested = false;
const len = parents.length;
const myUuid = this.uuid;
for (let i = 0; i < len; i++) {
const p = parents[i];
if (p.uuid !== myUuid && p.mvvm) {
@@ -219,9 +219,10 @@ export abstract class CCView<T extends CCEntity> extends GameComponent implement
result.push(comps[i]);
}
}
return result;
}
//#endregion
/** 从父节点移除自己 */
remove() {
@@ -229,20 +230,20 @@ export abstract class CCView<T extends CCEntity> extends GameComponent implement
console.error(`组件 ${this.name} 移除失败,实体不存在`);
return;
}
if (this.tid < 0) {
console.error(`组件 ${this.name} 移除失败,组件未注册 (tid=${this.tid})`);
return;
}
const cct = ECSModel.compCtors[this.tid];
if (!cct) {
console.error(`组件 ${this.name} 移除失败,组件构造函数不存在 (tid=${this.tid})`);
return;
}
this.ent.removeUi(cct);
this.ent = null!; // 清空引用,避免内存泄漏
this.ent.removeUi(cct as unknown as UICtor);
this.ent = null!; // 清空引用,避免内存泄漏
}
/**
@@ -257,10 +258,9 @@ export abstract class CCView<T extends CCEntity> extends GameComponent implement
// 解除全部引用
if (this.tag) {
VM.remove(this.tag);
// @ts-ignore - 优化:显式清空引用,帮助 GC
this.tag = undefined;
}
// @ts-ignore - 优化:显式清空引用,帮助 GC
this.data = undefined;
}
@@ -269,4 +269,4 @@ export abstract class CCView<T extends CCEntity> extends GameComponent implement
}
abstract reset(): void;
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "72863800-5c5d-44f0-b86c-a5b0d0643d8f",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,32 @@
import { resLoader } from '../../core/common/loader/ResLoader';
import { GameComponentCtor } from '../../types/Types';
/**
* 游戏装饰器命名空间
*/
export namespace prefab {
/**
* Prefab 装饰器 - 用于标记组件的预制体路径
* @param path 预制体路径
* @param bundleName 资源包名称(可选,默认使用 resLoader.defaultBundleName
* @example
* ```typescript
* @ccclass('V_Backpack_Prop')
* @ecs.register('V_Backpack_Prop', false)
* @prefab.register('gui/backpack/prefab/V_Backpack_Prop')
* export class V_Backpack_Prop extends CCView<Backpack> {
* // ...
* }
*
* // 使用时
* await entity.addPrefab(V_Backpack_Prop, parentNode);
* ```
*/
export function register(path: string, bundleName?: string): (ctor: GameComponentCtor) => void {
return function (ctor: GameComponentCtor): void {
const bundle = bundleName || resLoader.defaultBundleName;
(ctor as any).GAME_PREFAB_PATH = path;
(ctor as any).GAME_PREFAB_BUNDLE = bundle;
};
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "7c1ef3cb-50c6-49d4-b17c-11b8d37535bf",
"files": [],
"subMetas": {},
"userData": {}
}

9
assets/types.meta Normal file
View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "172d5432-5689-483f-8ede-94d9fdf5c747",
"files": [],
"subMetas": {},
"userData": {}
}

38
assets/types/Types.ts Normal file
View File

@@ -0,0 +1,38 @@
import type { Node } from 'cc';
import type { GameComponent } from 'db://oops-framework/module/common/GameComponent';
import type { ecs } from 'db://oops-framework/libs/ecs/ECS';
import type { CCView } from 'db://oops-framework/module/common/CCView';
import type { CCEntity } from 'db://oops-framework/module/common/CCEntity';
import type { CCBusiness } from 'db://oops-framework/module/common/CCBusiness';
// ==================== 通用构造函数类型 ====================
/** 通用构造函数类型 */
type Ctor<T = any> = new (...args: any[]) => T;
// ==================== 视图类型 ====================
/** GameComponent 及其子类的构造函数类型,用于类型安全的组件实例化 */
export type GameComponentCtor<T extends GameComponent = GameComponent> = Ctor<T>;
/** UI 组件构造函数类型(用于继承自 GameComponent 并使用 gui.register 注册的组件) */
export type UICtor<T extends GameComponent = GameComponent> = Ctor<T>;
/** 通用的视图组件构造函数类型(支持 ECSView 或 GameComponent */
export type ViewCtor<T extends GameComponent | ecs.Comp = GameComponent | ecs.Comp> = Ctor<T>;
/** ECS 游戏视图组件类型(继承自 CCView用于完整的 ECS 组件) */
export type ECSView = CCView<CCEntity>;
/** 视图节点类型Node 或 GameComponent */
export type View = Node | GameComponent;
// ==================== 实体类型 ====================
/** ECS 实体构造函数类型 */
export type EntityCtor<T extends CCEntity = CCEntity> = Ctor<T>;
// ==================== 业务逻辑类型 ====================
/** ECS 业务逻辑组件构造函数类型 */
export type BusinessCtor<T extends CCBusiness<CCEntity> = CCBusiness<CCEntity>> = Ctor<T>;

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "4d52b2a4-af0e-4ae4-b575-920ec36b6bcf",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -1,9 +1,6 @@
'use strict';
Object.defineProperty(exports, "__esModule", { value: true });
exports.template = exports.$ = void 0;
exports.update = update;
exports.ready = ready;
exports.close = close;
exports.close = exports.ready = exports.update = exports.template = exports.$ = void 0;
const fs_1 = require("fs");
const path_1 = require("path");
exports.$ = {
@@ -41,5 +38,8 @@ function update(assetList, metaList) {
this.$.section.hidden = false;
}
}
exports.update = update;
function ready() { }
exports.ready = ready;
function close() { }
exports.close = close;

23
dist/assets-menu.js vendored
View File

@@ -1,26 +1,9 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.onAssetMenu = onAssetMenu;
const tinypng_1 = require("./tinypng");
exports.onAssetMenu = void 0;
/** 资源栏右键菜单 */
function onAssetMenu(assetInfo) {
return [
{
label: 'i18n:oops-framework.name',
submenu: [
{
label: `i18n:oops-framework.tools_asset_menu`,
submenu: [
{
label: `i18n:oops-framework.tools_compress`,
click() {
(0, tinypng_1.compress)(assetInfo.file);
},
}
]
}
],
},
];
return [];
}
exports.onAssetMenu = onAssetMenu;
;

View File

@@ -15,27 +15,15 @@ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.checkUpdate = checkUpdate;
exports.statistics = statistics;
exports.reload = reload;
exports.reload = exports.statistics = exports.checkUpdate = void 0;
const package_util_1 = require("./package-util");
const https = __importStar(require("https"));
/**
@@ -80,6 +68,7 @@ function checkUpdate() {
console.error("【Oops Framework】请检查你的网络是否正常框架版本验证失败");
});
}
exports.checkUpdate = checkUpdate;
async function statistics() {
// 获取本地 IP 地址
const os = require('os');
@@ -123,12 +112,14 @@ async function statistics() {
req.write(postData);
req.end();
}
exports.statistics = statistics;
async function reload() {
const path = await Editor.Package.getPath(package_util_1.PackageUtil.name);
await Editor.Package.unregister(path);
await Editor.Package.register(path);
await Editor.Package.enable(path);
}
exports.reload = reload;
/**
* 获取本地版本号
* @returns {string}

6
dist/main.js vendored
View File

@@ -1,8 +1,6 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.methods = exports.config = void 0;
exports.load = load;
exports.unload = unload;
exports.methods = exports.config = exports.unload = exports.load = void 0;
const electron_1 = require("electron");
const version_1 = require("./common/version");
/**
@@ -13,11 +11,13 @@ function load() {
(0, version_1.checkUpdate)();
(0, version_1.statistics)();
}
exports.load = load;
/**
* @en Hooks triggered after extension uninstallation is complete
* @zh 扩展卸载完成后触发的钩子
*/
function unload() { }
exports.unload = unload;
/**
* @en
* @zh 为扩展的主进程的注册方法

159
dist/tinypng.js vendored
View File

@@ -1,159 +0,0 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.compress = compress;
const fs_1 = __importDefault(require("fs"));
const https_1 = __importDefault(require("https"));
const path_1 = __importDefault(require("path"));
const url_1 = __importDefault(require("url"));
const exts = ['.png', '.jpg', '.jpeg'];
const max = 5200000;
const options = {
method: 'POST',
hostname: 'tinypng.com',
path: '/backend/opt/shrink',
headers: {
rejectUnauthorized: 'false',
'Postman-Token': Date.now(),
'Cache-Control': 'no-cache',
'Content-Type': 'application/x-www-form-urlencoded',
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36'
}
};
function compress(filePath) {
if (!fs_1.default.existsSync(filePath)) {
console.log(`路径不存在:${filePath}`);
return;
}
const fileName = path_1.default.basename(filePath);
if (!fs_1.default.statSync(filePath).isDirectory()) {
if (exts.includes(path_1.default.extname(filePath))) {
console.log(`[${fileName}] 压缩中...`);
fileTinyUpload(filePath)
.then(data => {
console.log(`[1/1] [${fileName}] 压缩成功,原始: ${toSize(data.input.size)},压缩: ${toSize(data.output.size)},压缩比: ${toPercent(data.output.ratio)}`);
})
.catch(err => {
console.log(`[1/1] [${fileName}] 压缩失败!报错:${err}`);
});
}
else {
console.log(`[${fileName}] 压缩失败!报错:只支持 png、jpg 与 jpeg 格式`);
}
}
else {
let totalCount = 0;
let processedCount = 0;
fileEach(filePath, (filePathInDir) => {
totalCount++;
const relativePath = path_1.default.relative(filePath, filePathInDir);
fileTinyUpload(filePathInDir)
.then(data => {
console.log(`[${++processedCount}/${totalCount}] [${relativePath}] 压缩成功,原始: ${toSize(data.input.size)},压缩: ${toSize(data.output.size)},压缩比: ${toPercent(data.output.ratio)}`);
})
.catch(err => {
console.log(`[${++processedCount}/${totalCount}] [${relativePath}] 压缩失败!报错:${err}`);
});
});
}
}
function getRandomIP() {
return Array.from(Array(4)).map(() => Math.floor(255 * Math.random())).join('.');
}
function fileEach(dir, callback) {
fs_1.default.readdir(dir, (err, files) => {
if (err) {
console.error(err);
return;
}
files.forEach((file) => {
const filePath = path_1.default.join(dir, file);
fs_1.default.stat(filePath, (statErr, stats) => {
if (statErr) {
console.error(statErr);
return;
}
if (stats.isDirectory()) {
fileEach(filePath, callback);
}
else {
if (stats.size <= max && stats.isFile() && exts.includes(path_1.default.extname(file))) {
callback(filePath);
}
}
});
});
});
}
function fileUpload(filePath) {
return new Promise((resolve, reject) => {
options.headers['X-Forwarded-For'] = getRandomIP();
const req = https_1.default.request(options, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
try {
const result = JSON.parse(data);
if (result.error) {
reject(result.message);
}
else {
resolve(result);
}
}
catch (parseErr) {
reject(parseErr);
}
});
});
req.write(fs_1.default.readFileSync(filePath), 'binary');
req.on('error', err => {
reject(err);
});
req.end();
});
}
function fileUpdate(filePath, data) {
return new Promise((resolve, reject) => {
const urlObj = new url_1.default.URL(data.output.url);
const req = https_1.default.request(urlObj, (res) => {
let body = '';
res.setEncoding('binary');
res.on('data', (chunk) => {
body += chunk;
});
res.on('end', () => {
fs_1.default.writeFile(filePath, body, 'binary', (err) => {
if (err) {
reject(err);
}
else {
resolve(data);
}
});
});
});
req.on('error', (err) => {
reject(err);
});
req.end();
});
}
function fileTinyUpload(filePath) {
return fileUpload(filePath).then(data => fileUpdate(filePath, data));
}
function toSize(size) {
if (size < 1024)
return size + 'B';
else if (size < 1048576)
return (size / 1024).toFixed(2) + 'KB';
else
return (size / 1024 / 1024).toFixed(2) + 'MB';
}
function toPercent(ratio) {
return (100 * ratio).toFixed(2) + '%';
}

View File

@@ -19,8 +19,6 @@ module.exports = {
createView: "Create ECS view layer script",
createViewMvvm: "Create ECS view layer script - MVVM",
tools: "Framework Tools",
tools_asset_menu: "Tools",
tools_compress: "Image Compression",
tools_animator_editor: "Animation State Machine Editor",
panel_create_file: "Create Framework Template",
};
};

View File

@@ -19,8 +19,6 @@ module.exports = {
createView: "创建 ECS 视图层脚本",
createViewMvvm: "创建 ECS 视图层脚本 - MVVM",
tools: "框架工具",
tools_asset_menu: "工具",
tools_compress: "图片压缩",
tools_animator_editor: "动画状态机编辑器",
panel_create_file: "创建框架模板"
};
};

View File

@@ -28,12 +28,7 @@
"readonly": false
}
},
"assets": {
"menu": {
"methods": "./dist/assets-menu.js",
"assetMenu": "onAssetMenu"
}
},
"menu": [
{
"path": "i18n:oops-framework.name/i18n:oops-framework.tools",

View File

@@ -1,24 +1,6 @@
import { AssetInfo } from "../@types/packages/asset-db/@types/public";
import { compress } from "./tinypng";
/** 资源栏右键菜单 */
export function onAssetMenu(assetInfo: AssetInfo) {
return [
{
label: 'i18n:oops-framework.name',
submenu: [
{
label: `i18n:oops-framework.tools_asset_menu`,
submenu: [
{
label: `i18n:oops-framework.tools_compress`,
click() {
compress(assetInfo.file);
},
}
]
}
],
},
];
};
return [];
};

View File

@@ -1,159 +0,0 @@
import fs from 'fs';
import https from 'https';
import path from 'path';
import url from 'url';
const exts = ['.png', '.jpg', '.jpeg'];
const max = 5200000;
const options: any = {
method: 'POST',
hostname: 'tinypng.com',
path: '/backend/opt/shrink',
headers: {
rejectUnauthorized: 'false',
'Postman-Token': Date.now(),
'Cache-Control': 'no-cache',
'Content-Type': 'application/x-www-form-urlencoded',
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36'
}
};
export function compress(filePath: string): void {
if (!fs.existsSync(filePath)) {
console.log(`路径不存在:${filePath}`);
return;
}
const fileName = path.basename(filePath);
if (!fs.statSync(filePath).isDirectory()) {
if (exts.includes(path.extname(filePath))) {
console.log(`[${fileName}] 压缩中...`);
fileTinyUpload(filePath)
.then(data => {
console.log(`[1/1] [${fileName}] 压缩成功,原始: ${toSize(data.input.size)},压缩: ${toSize(data.output.size)},压缩比: ${toPercent(data.output.ratio)}`);
})
.catch(err => {
console.log(`[1/1] [${fileName}] 压缩失败!报错:${err}`);
});
}
else {
console.log(`[${fileName}] 压缩失败!报错:只支持 png、jpg 与 jpeg 格式`);
}
}
else {
let totalCount = 0;
let processedCount = 0;
fileEach(filePath, (filePathInDir) => {
totalCount++;
const relativePath = path.relative(filePath, filePathInDir);
fileTinyUpload(filePathInDir)
.then(data => {
console.log(`[${++processedCount}/${totalCount}] [${relativePath}] 压缩成功,原始: ${toSize(data.input.size)},压缩: ${toSize(data.output.size)},压缩比: ${toPercent(data.output.ratio)}`);
})
.catch(err => {
console.log(`[${++processedCount}/${totalCount}] [${relativePath}] 压缩失败!报错:${err}`);
});
});
}
}
function getRandomIP(): string {
return Array.from(Array(4)).map(() => Math.floor(255 * Math.random())).join('.');
}
function fileEach(dir: string, callback: (filePath: string) => void): void {
fs.readdir(dir, (err: any, files: any[]) => {
if (err) {
console.error(err);
return;
}
files.forEach((file: any) => {
const filePath = path.join(dir, file);
fs.stat(filePath, (statErr: any, stats: { isDirectory: () => any; size: number; isFile: () => any; }) => {
if (statErr) {
console.error(statErr);
return;
}
if (stats.isDirectory()) {
fileEach(filePath, callback);
}
else {
if (stats.size <= max && stats.isFile() && exts.includes(path.extname(file))) {
callback(filePath);
}
}
});
});
});
}
function fileUpload(filePath: string): Promise<any> {
return new Promise((resolve, reject) => {
options.headers['X-Forwarded-For'] = getRandomIP();
const req = https.request(options, (res: any) => {
let data = '';
res.on('data', (chunk: string) => {
data += chunk;
});
res.on('end', () => {
try {
const result = JSON.parse(data);
if (result.error) {
reject(result.message);
} else {
resolve(result);
}
} catch (parseErr) {
reject(parseErr);
}
});
});
req.write(fs.readFileSync(filePath), 'binary');
req.on('error', err => {
reject(err);
});
req.end();
});
}
function fileUpdate(filePath: string, data: any): Promise<any> {
return new Promise((resolve, reject) => {
const urlObj = new url.URL(data.output.url);
const req = https.request(urlObj, (res: any) => {
let body = '';
res.setEncoding('binary');
res.on('data', (chunk: string) => {
body += chunk;
});
res.on('end', () => {
fs.writeFile(filePath, body, 'binary', (err: any) => {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
});
req.on('error', (err: any) => {
reject(err);
});
req.end();
});
}
function fileTinyUpload(filePath: string): Promise<any> {
return fileUpload(filePath).then(data => fileUpdate(filePath, data));
}
function toSize(size: number): string {
if (size < 1024)
return size + 'B';
else if (size < 1048576)
return (size / 1024).toFixed(2) + 'KB';
else
return (size / 1024 / 1024).toFixed(2) + 'MB';
}
function toPercent(ratio: number): string {
return (100 * ratio).toFixed(2) + '%';
}

View File

@@ -1,37 +0,0 @@
ui-input {
width: 100%;
min-width: 100px;
min-height: 30px;
line-height: 30px;
}
ui-button {
height: 30px;
}
ui-label {
margin-top: 5px;
margin-bottom: 5px;
}
.f {
flex: 1;
overflow: auto;
padding: 10px;
}
.c {
display: inline-flex;
width: 100%;
flex-wrap: nowrap;
}
.c ui-button {
width: 40%;
}
.b {
text-align: center;
padding: 10px 0;
margin-top: 5px;
}