mirror of
https://gitee.com/dgflash/oops-plugin-framework.git
synced 2026-05-07 19:07:30 +08:00
@@ -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!;
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ export class CCBusiness<T extends CCEntity> {
|
||||
destroy() {
|
||||
// 释放消息对象
|
||||
if (this._event) {
|
||||
this._event.destroy();
|
||||
this._event.clear();
|
||||
this._event = null;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
9
assets/module/decorator.meta
Normal file
9
assets/module/decorator.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "72863800-5c5d-44f0-b86c-a5b0d0643d8f",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
32
assets/module/decorator/GamePrefabDecorator.ts
Normal file
32
assets/module/decorator/GamePrefabDecorator.ts
Normal 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;
|
||||
};
|
||||
}
|
||||
}
|
||||
9
assets/module/decorator/GamePrefabDecorator.ts.meta
Normal file
9
assets/module/decorator/GamePrefabDecorator.ts.meta
Normal 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
9
assets/types.meta
Normal 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
38
assets/types/Types.ts
Normal 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>;
|
||||
9
assets/types/Types.ts.meta
Normal file
9
assets/types/Types.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "4d52b2a4-af0e-4ae4-b575-920ec36b6bcf",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
8
dist/asset-directory.js
vendored
8
dist/asset-directory.js
vendored
@@ -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
23
dist/assets-menu.js
vendored
@@ -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;
|
||||
;
|
||||
|
||||
31
dist/common/version.js
vendored
31
dist/common/version.js
vendored
@@ -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
6
dist/main.js
vendored
@@ -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
159
dist/tinypng.js
vendored
@@ -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) + '%';
|
||||
}
|
||||
@@ -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",
|
||||
};
|
||||
};
|
||||
|
||||
@@ -19,8 +19,6 @@ module.exports = {
|
||||
createView: "创建 ECS 视图层脚本",
|
||||
createViewMvvm: "创建 ECS 视图层脚本 - MVVM",
|
||||
tools: "框架工具",
|
||||
tools_asset_menu: "工具",
|
||||
tools_compress: "图片压缩",
|
||||
tools_animator_editor: "动画状态机编辑器",
|
||||
panel_create_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",
|
||||
|
||||
@@ -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 [];
|
||||
};
|
||||
|
||||
159
src/tinypng.ts
159
src/tinypng.ts
@@ -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) + '%';
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user