You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
AdLike2048/assets/script/gm/ProjectGmSavePanel.ts

514 lines
19 KiB

2 days ago
import {
Button, Color, EditBox, Graphics, Label, Layout, Mask, Node, ScrollView, UITransform, Widget,
HorizontalTextAlignment, VerticalTextAlignment, Layers, Overflow,
} from 'cc';
import type { IGmContentPanel, IGmPanelContext } from '../../mx/module/gm';
import { Doc } from '../game/GameData';
import { ProjectGmActions } from './ProjectGmActions';
import MTools from '../../mx/tools/MTools';
const C = {
text: new Color(255, 255, 255, 255),
dim: new Color(180, 190, 210, 255),
accent: new Color(255, 220, 70, 255),
rowBg: new Color(28, 36, 56, 255),
inputBg: new Color(12, 16, 28, 255),
inputLine: new Color(100, 160, 240, 255),
btn: new Color(36, 130, 95, 255),
btnSet: new Color(210, 120, 35, 255),
btnSave: new Color(45, 105, 190, 255),
btnCancel: new Color(70, 78, 96, 255),
};
/** 固定字号,避免 Overflow.SHRINK 把字压到看不清 */
const FONT = {
section: 26,
label: 24,
input: 24,
tip: 20,
btn: 26,
};
/** Doc 字段中文名 */
const DOC_LABELS: { [key: string]: string } = {
key: '存档key',
uid: '用户uid',
roleId: '角色roleId',
playerTitleLevel: '称号等级',
dataVersion: 'dataVersion',
rankId: '段位id',
rankStars: '总星数',
pendingBattleOppRankId: '未结算对手段位',
matchWinCount: '匹配胜场',
matchLoseCount: '匹配负场',
totalBattleCount: '总挑战次数',
matchWinStatsDate: '胜率统计日',
matchWinStreak: '连胜',
matchLoseStreak: '连败',
dailyWins: '今日胜场',
yesterdayWins: '昨日胜场',
dailyWinsDate: '今日胜场日期',
rankLastSyncDailyWins: '同步每日胜场',
rankLastSyncDailyWinsDate: '同步每日胜场日',
userName: '昵称',
userAvatar: '头像url',
userProvince: '省份',
rankUserAuthPrompted: '已弹授权',
rankSyncedOnce: '已同步排行',
rankLastSyncStars: '同步星数',
rankLastSyncProvince: '同步省份',
rankLastSyncNick: '同步昵称',
rankLastSyncAvatar: '同步头像',
rankLastSyncRegion: '同步地区',
emolumentSnapStars: '俸禄待领星',
emolumentSnapWins: '俸禄待领胜',
emolumentSnapDate: '俸禄快照日',
gmAwaitEmolumentPop: 'GM弹俸禄',
headIconId: '头像id',
battleIllustratedGuideClicked: '已点图鉴',
level: '关卡level',
itemCount: '道具itemCount',
status: '状态status',
statistics: '统计statistics',
guide: '引导guide',
other: '其他other',
onlyLocal: '仅本地onlyLocal',
failCount: '上报失败次数',
lastFailCount: '上次上报失败次数',
};
/**
* 存档页:复制 / 设置(必弹粘贴层)+ Doc 全字段上下布局编辑。
*/
export class GmSaveEditorPanel implements IGmContentPanel {
private _root: Node | null = null;
private _ctx: IGmPanelContext | null = null;
private _scrollContent: Node | null = null;
private _listW = 660;
private _fieldEdits: { key: string; edit: EditBox }[] = [];
private _pasteLayer: Node | null = null;
private _pasteEdit: EditBox | null = null;
open(parent: Node, ctx: IGmPanelContext): void {
this.close();
this._ctx = ctx;
const pUt = parent.getComponent(UITransform);
const W = pUt?.width || 660;
const H = pUt?.height || 900;
this._listW = W - 24;
this._root = new Node('saveEditor');
this._root.layer = Layers.Enum.UI_2D;
this._root.addComponent(UITransform).setContentSize(W, H);
const rw = this._root.addComponent(Widget);
rw.isAlignLeft = rw.isAlignRight = rw.isAlignTop = rw.isAlignBottom = true;
rw.left = rw.right = rw.top = rw.bottom = 0;
parent.addChild(this._root);
const topH = 118;
const top = new Node('top');
top.layer = Layers.Enum.UI_2D;
top.addComponent(UITransform).setContentSize(W, topH);
const tw = top.addComponent(Widget);
tw.isAlignTop = tw.isAlignLeft = tw.isAlignRight = true;
tw.top = 0;
tw.left = tw.right = 0;
this._root.addChild(top);
this._makeBtn(top, '复制存档', 200, 52, C.btn, () => {
ProjectGmActions.copySave();
}).setPosition(-120, 28);
this._makeBtn(top, '设置存档', 200, 52, C.btnSet, () => {
this._openSetSaveFlow();
}).setPosition(120, 28);
const tip = new Node('tip');
tip.layer = Layers.Enum.UI_2D;
tip.addComponent(UITransform).setContentSize(W - 32, 44);
const tipLb = tip.addComponent(Label);
tipLb.string = '设置存档会打开粘贴框;下方每字段:上名称、下输入';
tipLb.fontSize = FONT.tip;
tipLb.lineHeight = 24;
tipLb.color = C.dim;
tipLb.isBold = true;
tipLb.horizontalAlign = HorizontalTextAlignment.CENTER;
tip.setPosition(0, -32);
top.addChild(tip);
const bottomH = 76;
const { host, content } = this._makeScroll(this._listW, Math.max(200, H - topH - bottomH - 8));
this._scrollContent = content;
const sw = host.addComponent(Widget);
sw.isAlignTop = sw.isAlignBottom = sw.isAlignLeft = sw.isAlignRight = true;
sw.top = topH;
sw.bottom = bottomH;
sw.left = sw.right = 12;
this._root.addChild(host);
this._rebuildFieldList();
const bottom = new Node('bottom');
bottom.layer = Layers.Enum.UI_2D;
bottom.addComponent(UITransform).setContentSize(W, bottomH);
const bw = bottom.addComponent(Widget);
bw.isAlignBottom = bw.isAlignLeft = bw.isAlignRight = true;
bw.bottom = 0;
bw.left = bw.right = 0;
this._root.addChild(bottom);
this._makeBtn(bottom, '保存全部字段', 320, 54, C.btnSave, () => {
this._applyFieldEdits();
}).setPosition(0, 4);
}
close(): void {
this._hidePasteLayer();
if (this._root?.isValid) this._root.destroy();
this._root = null;
this._ctx = null;
this._scrollContent = null;
this._fieldEdits = [];
}
/** 每次点击必有界面反馈:打开粘贴层,并尝试自动填入剪切板 */
private _openSetSaveFlow(): void {
const host = this._ctx?.host;
if (this._pasteLayer?.isValid) {
host?.toast('请在弹层中粘贴或确认');
return;
}
this._showPasteLayer('可粘贴存档 JSON;若剪切板有内容将自动填入');
host?.toast('请粘贴或确认存档');
void this._tryFillClipboard();
}
private async _tryFillClipboard(): Promise<void> {
const r = await MTools.readClipboardResult(1500);
if (!this._pasteEdit || !this._pasteLayer?.isValid) return;
if (r.err || !r.text?.trim()) return;
this._pasteEdit.string = r.text.trim();
this._ctx?.host.toast('已从剪切板填入,请点确认设置');
}
private _rebuildFieldList(): void {
if (!this._scrollContent?.isValid) return;
this._scrollContent.removeAllChildren();
this._fieldEdits = [];
const doc = gg.data.doc as unknown as { [key: string]: unknown };
const template = new Doc() as unknown as { [key: string]: unknown };
const keySet: { [k: string]: boolean } = {};
const keys: string[] = [];
const addKey = (k: string): void => {
if (keySet[k]) return;
keySet[k] = true;
keys.push(k);
};
const tKeys = Object.keys(template);
for (let i = 0; i < tKeys.length; i++) addKey(tKeys[i]);
const dKeys = Object.keys(doc);
for (let i = 0; i < dKeys.length; i++) addKey(dKeys[i]);
this._addSection('标量字段');
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
const cur = doc[key] !== undefined ? doc[key] : template[key];
if (Array.isArray(cur)) continue;
const title = DOC_LABELS[key] || key;
const sub = DOC_LABELS[key] ? key : '';
const row = this._addFieldRow(title, sub, false);
row.edit.string = cur === undefined || cur === null ? '' : String(cur);
this._fieldEdits.push({ key, edit: row.edit });
}
this._addSection('数组字段(JSON)');
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
const cur = doc[key] !== undefined ? doc[key] : template[key];
if (!Array.isArray(cur)) continue;
const title = DOC_LABELS[key] || key;
const sub = DOC_LABELS[key] ? key : '';
const row = this._addFieldRow(title, sub, true);
try {
row.edit.string = JSON.stringify(cur);
} catch {
row.edit.string = '[]';
}
this._fieldEdits.push({ key, edit: row.edit });
}
this._scrollContent.getComponent(Layout)?.updateLayout();
}
private _showPasteLayer(reason: string): void {
this._hidePasteLayer();
if (!this._root?.isValid) return;
const W = this._root.getComponent(UITransform)!.width;
const H = this._root.getComponent(UITransform)!.height;
this._pasteLayer = new Node('pasteLayer');
this._pasteLayer.layer = Layers.Enum.UI_2D;
this._pasteLayer.addComponent(UITransform).setContentSize(W, H);
const g = this._pasteLayer.addComponent(Graphics);
g.fillColor = new Color(0, 0, 0, 210);
g.rect(-W / 2, -H / 2, W, H);
g.fill();
this._root.addChild(this._pasteLayer);
const cardW = Math.min(W - 32, 640);
const cardH = 400;
const card = new Node('card');
card.layer = Layers.Enum.UI_2D;
card.addComponent(UITransform).setContentSize(cardW, cardH);
this._drawRound(card, C.rowBg, cardW, cardH, 16);
this._pasteLayer.addChild(card);
const title = new Node('t');
title.layer = Layers.Enum.UI_2D;
title.addComponent(UITransform).setContentSize(cardW - 24, 40);
const tl = title.addComponent(Label);
tl.string = '设置存档';
tl.fontSize = 30;
tl.isBold = true;
tl.color = C.accent;
tl.horizontalAlign = HorizontalTextAlignment.CENTER;
title.setPosition(0, cardH / 2 - 32);
card.addChild(title);
const reasonN = new Node('r');
reasonN.layer = Layers.Enum.UI_2D;
reasonN.addComponent(UITransform).setContentSize(cardW - 32, 48);
const rl = reasonN.addComponent(Label);
rl.string = reason;
rl.fontSize = FONT.tip;
rl.color = C.dim;
rl.isBold = true;
rl.horizontalAlign = HorizontalTextAlignment.CENTER;
rl.overflow = Overflow.RESIZE_HEIGHT;
reasonN.setPosition(0, cardH / 2 - 78);
card.addChild(reasonN);
const editW = cardW - 40;
const editH = 180;
const editN = new Node('edit');
editN.layer = Layers.Enum.UI_2D;
editN.addComponent(UITransform).setContentSize(editW, editH);
this._drawRound(editN, C.inputBg, editW, editH, 10, C.inputLine);
const textLbN = new Node('TEXT_LABEL');
textLbN.layer = Layers.Enum.UI_2D;
textLbN.addComponent(UITransform).setContentSize(editW - 16, editH - 16);
const textLb = textLbN.addComponent(Label);
textLb.fontSize = FONT.input;
textLb.color = C.accent;
textLb.isBold = true;
textLb.horizontalAlign = HorizontalTextAlignment.LEFT;
textLb.verticalAlign = VerticalTextAlignment.TOP;
textLb.overflow = Overflow.CLAMP;
editN.addChild(textLbN);
const phN = new Node('PLACEHOLDER_LABEL');
phN.layer = Layers.Enum.UI_2D;
phN.addComponent(UITransform).setContentSize(editW - 16, editH - 16);
const phLb = phN.addComponent(Label);
phLb.string = '在此粘贴存档 JSON';
phLb.fontSize = FONT.input;
phLb.color = C.dim;
phLb.horizontalAlign = HorizontalTextAlignment.CENTER;
phLb.verticalAlign = VerticalTextAlignment.CENTER;
editN.addChild(phN);
this._pasteEdit = editN.addComponent(EditBox);
this._pasteEdit.textLabel = textLb;
this._pasteEdit.placeholderLabel = phLb;
this._pasteEdit.maxLength = 500000;
this._pasteEdit.inputMode = EditBox.InputMode.ANY;
editN.setPosition(0, 8);
card.addChild(editN);
this._makeBtn(card, '确认设置', 170, 52, C.btnSet, () => {
const raw = this._pasteEdit?.string ?? '';
if (!raw.trim()) {
this._ctx?.host.toast('内容为空');
return;
}
const ok = ProjectGmActions.applySaveJson(raw, false);
if (ok) {
this._hidePasteLayer();
this._rebuildFieldList();
}
}).setPosition(-100, -cardH / 2 + 44);
this._makeBtn(card, '取消', 170, 52, C.btnCancel, () => {
this._hidePasteLayer();
}).setPosition(100, -cardH / 2 + 44);
}
private _hidePasteLayer(): void {
if (this._pasteLayer?.isValid) this._pasteLayer.destroy();
this._pasteLayer = null;
this._pasteEdit = null;
}
private _applyFieldEdits(): void {
const fields: { [key: string]: string } = {};
for (let i = 0; i < this._fieldEdits.length; i++) {
const f = this._fieldEdits[i];
fields[f.key] = f.edit.string ?? '';
}
ProjectGmActions.applySaveFieldEdits(fields);
this._rebuildFieldList();
}
private _makeScroll(w: number, h: number): { host: Node; content: Node } {
const host = new Node('scroll');
host.layer = Layers.Enum.UI_2D;
host.addComponent(UITransform).setContentSize(w, h);
const view = new Node('view');
view.layer = Layers.Enum.UI_2D;
view.addComponent(UITransform).setContentSize(w, h);
view.addComponent(Mask);
const vw = view.addComponent(Widget);
vw.isAlignLeft = vw.isAlignRight = vw.isAlignTop = vw.isAlignBottom = true;
vw.left = vw.right = vw.top = vw.bottom = 0;
host.addChild(view);
const content = new Node('content');
content.layer = Layers.Enum.UI_2D;
const cut = content.addComponent(UITransform);
cut.setContentSize(w, h);
cut.setAnchorPoint(0.5, 1);
content.setPosition(0, h / 2);
const layout = content.addComponent(Layout);
layout.type = Layout.Type.VERTICAL;
layout.resizeMode = Layout.ResizeMode.CONTAINER;
layout.spacingY = 12;
layout.paddingTop = 8;
layout.paddingBottom = 28;
view.addChild(content);
const sv = host.addComponent(ScrollView);
sv.content = content;
sv.horizontal = false;
sv.vertical = true;
return { host, content };
}
private _addSection(title: string): void {
if (!this._scrollContent) return;
const n = new Node('sec');
n.layer = Layers.Enum.UI_2D;
n.addComponent(UITransform).setContentSize(this._listW, 40);
const lb = n.addComponent(Label);
lb.string = title;
lb.fontSize = FONT.section;
lb.isBold = true;
lb.color = C.accent;
lb.horizontalAlign = HorizontalTextAlignment.LEFT;
lb.overflow = Overflow.NONE;
this._scrollContent.addChild(n);
}
/**
* 上下布局:上行标题(固定字号),下行全宽输入框(固定字号)。
*/
private _addFieldRow(title: string, subKey: string, tall: boolean): { edit: EditBox } {
const rowH = tall ? 120 : 100;
const w = this._listW;
const row = new Node(title);
row.layer = Layers.Enum.UI_2D;
row.addComponent(UITransform).setContentSize(w, rowH);
this._drawRound(row, C.rowBg, w, rowH, 12);
const headH = 36;
const lbN = new Node('lb');
lbN.layer = Layers.Enum.UI_2D;
lbN.addComponent(UITransform).setContentSize(w - 24, headH);
const lb = lbN.addComponent(Label);
lb.string = subKey ? `${title} · ${subKey}` : title;
lb.fontSize = FONT.label;
lb.isBold = true;
lb.color = C.text;
lb.horizontalAlign = HorizontalTextAlignment.LEFT;
lb.verticalAlign = VerticalTextAlignment.CENTER;
lb.overflow = Overflow.CLAMP;
lb.enableWrapText = false;
lbN.setPosition(0, rowH / 2 - headH / 2 - 6);
row.addChild(lbN);
const editW = w - 24;
const editH = tall ? 68 : 48;
const editN = new Node('edit');
editN.layer = Layers.Enum.UI_2D;
editN.addComponent(UITransform).setContentSize(editW, editH);
this._drawRound(editN, C.inputBg, editW, editH, 8, C.inputLine);
const textLbN = new Node('TEXT_LABEL');
textLbN.layer = Layers.Enum.UI_2D;
textLbN.addComponent(UITransform).setContentSize(editW - 16, editH - 10);
const textLb = textLbN.addComponent(Label);
textLb.fontSize = FONT.input;
textLb.isBold = true;
textLb.color = C.accent;
textLb.horizontalAlign = HorizontalTextAlignment.LEFT;
textLb.verticalAlign = tall ? VerticalTextAlignment.TOP : VerticalTextAlignment.CENTER;
textLb.overflow = Overflow.CLAMP;
editN.addChild(textLbN);
const phN = new Node('PLACEHOLDER_LABEL');
phN.layer = Layers.Enum.UI_2D;
phN.addComponent(UITransform).setContentSize(editW - 16, editH - 10);
const phLb = phN.addComponent(Label);
phLb.string = tall ? '输入 JSON 数组' : '点击输入';
phLb.fontSize = FONT.input;
phLb.color = C.dim;
phLb.horizontalAlign = HorizontalTextAlignment.LEFT;
phLb.verticalAlign = VerticalTextAlignment.CENTER;
editN.addChild(phN);
const eb = editN.addComponent(EditBox);
eb.textLabel = textLb;
eb.placeholderLabel = phLb;
eb.maxLength = tall ? 200000 : 512;
eb.inputMode = tall ? EditBox.InputMode.ANY : EditBox.InputMode.SINGLE_LINE;
editN.setPosition(0, -rowH / 2 + editH / 2 + 8);
row.addChild(editN);
this._scrollContent!.addChild(row);
return { edit: eb };
}
private _makeBtn(parent: Node, text: string, w: number, h: number, bg: Color, onClick: () => void): Node {
const n = new Node(text);
n.layer = Layers.Enum.UI_2D;
n.addComponent(UITransform).setContentSize(w, h);
this._drawRound(n, bg, w, h, 12);
const lbN = new Node('lb');
lbN.layer = Layers.Enum.UI_2D;
lbN.addComponent(UITransform).setContentSize(w - 8, h - 4);
const lb = lbN.addComponent(Label);
lb.string = text;
lb.fontSize = FONT.btn;
lb.isBold = true;
lb.color = C.text;
lb.horizontalAlign = HorizontalTextAlignment.CENTER;
lb.verticalAlign = VerticalTextAlignment.CENTER;
lb.overflow = Overflow.NONE;
n.addChild(lbN);
n.addComponent(Button);
n.on(Button.EventType.CLICK, onClick, this);
parent.addChild(n);
return n;
}
private _drawRound(node: Node, fill: Color, w: number, h: number, r: number, stroke?: Color): void {
const g = node.getComponent(Graphics) || node.addComponent(Graphics);
g.clear();
g.fillColor = fill;
g.roundRect(-w / 2, -h / 2, w, h, r);
g.fill();
if (stroke) {
g.strokeColor = stroke;
g.lineWidth = 2;
g.roundRect(-w / 2, -h / 2, w, h, r);
g.stroke();
}
}
}