ui: black market buy page with seller list, payment panel, per-merchant accepted coins and auto-convert; wallet row keys beside the data cells

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ZHU MINCHAOandClaude Sonnet 5.5 committed 2026-11-04 11:06:38 +08:00
1 parent 12a63c562f
commit 9d24840561
7 files changed
+262 -73

No files matched your search

+197 -69
View File
@@ -990,7 +990,9 @@ namespace Decentralized
var w = d.Wallets[i];
int idx = i;
bool isCurrent = idx == d.Current;
b.BeginPanel(740f, 32f, TextAnchor.MiddleLeft, 4f, 0f);
// 一行三块:数据格(整行反色的是这一块)、操作键(和顶栏页签一样的黑色触控键,放在纸面上)、连接状态靠右
b.BeginRow(4f, TextAnchor.MiddleLeft);
b.BeginPanel(cols[0] + cols[1] + cols[2] + cols[3] + 8f, 32f, TextAnchor.MiddleLeft, 4f, 0f);
_rows.Add(new PledgerSkin.RowDef
{
Root = b.Last.root,
@@ -1003,8 +1005,9 @@ namespace Decentralized
Cell(b, cols[1], w.Address, false, TextAnchor.MiddleLeft);
Cell(b, cols[2], Fmt.Credits(w.Value(Session.Price)), false, TextAnchor.MiddleRight);
Cell(b, cols[3], Fmt.Credits(w.Taint), false, TextAnchor.MiddleRight);
// 操作键一组(垂直居中、固定高度),连接状态单独靠右
b.BeginPanel(cols[4] - 140f, 32f, TextAnchor.MiddleLeft, 0f, 0f);
b.End();
b.End();
b.BeginPanel(cols[4] - 150f, 32f, TextAnchor.MiddleLeft, 0f, 0f);
_clear.Add(b.Last.image);
b.BeginRow(4f, TextAnchor.MiddleLeft);
RowKey(b, L.T("wallets.open"), () => WalletWindow.Open(w));
@@ -1013,7 +1016,7 @@ namespace Decentralized
if (idx > 0) RowKey(b, _destroyPick == w.Address ? L.T("wallets.destroy.sure") : L.T("wallets.destroy"), () => DestroyWallet(w));
b.End();
b.End();
b.BeginPanel(100f, 32f, TextAnchor.MiddleRight, 0f, 0f);
b.BeginPanel(84f, 32f, TextAnchor.MiddleRight, 0f, 0f);
_clear.Add(b.Last.image);
b.BeginRow(0f, TextAnchor.MiddleRight);
if (idx != d.Current) RowKey(b, L.T("wallets.use"), () => { d.Current = idx; Session.Save(); Open(); });
@@ -1021,7 +1024,6 @@ namespace Decentralized
b.End();
b.End();
b.End();
b.End();
}
if (_renamePick != null) RenameRow(b);
}
@@ -1033,7 +1035,7 @@ namespace Decentralized
var go = b.Last.root;
if (go == null) return;
var le = go.GetComponent<LayoutElement>() ?? go.AddComponent<LayoutElement>();
le.minHeight = 24f; le.preferredHeight = 24f; le.flexibleHeight = 0f;
le.minHeight = 26f; le.preferredHeight = 26f; le.flexibleHeight = 0f;
}
private static string _destroyPick, _renamePick, _renameText = "";
@@ -1146,7 +1148,6 @@ namespace Decentralized
private static PayMode _mode = PayMode.Escrow;
private static string _deskAsset = Assets.PSC;
private static bool _deskIn = true;
private static string _payAsset = Assets.CRT;
private static readonly System.Collections.Generic.Dictionary<string, Sprite> IconCache = new System.Collections.Generic.Dictionary<string, Sprite>();
/// <summary>货架上的商品图:游戏物品自己的贴图(本 mod 的显卡用本 mod 的贴图)。</summary>
@@ -1178,60 +1179,46 @@ namespace Decentralized
{
var tab = t;
string name = L.T("black.sub." + t.ToString().ToLowerInvariant());
PledgerSkin.Pick(b, name, tab == _blackTab, () => { _blackTab = tab; _message = null; _edit = null; Open(); }, _picked);
PledgerSkin.Pick(b, name, tab == _blackTab, () => { _blackTab = tab; _message = null; _edit = null; _pay = null; Open(); }, _picked);
}
b.End();
if (d.Black.Closed(day)) { b.AddLabel(L.T("black.rugged", d.Black.ClosedUntil - day + 1), "black_rugged_note"); return; }
if (_pay != null && _blackTab != BlackTab.Buy) { PayPanel(b); return; }
if (_edit != null) { EditPage(b); return; }
if (_blackTab == BlackTab.Orders) { OrdersPage(b, day); return; }
if (_blackTab == BlackTab.Sell) { SellPage(b, day); return; }
BuyPage(b, d, rep);
}
/// <summary>买货:选一家卖家(第一个是平台现货,立即到货但贵,其余是商家,便宜但要等),选件数,下单。论坛情报在最下面。</summary>
/// <summary>
/// 买货:左栏是卖家列表(第一个是平台现货,立即到货但贵,其余是商家,便宜但要等),右栏是这家的货架。
/// 点"购买"后右栏换成付款面板:选数量(和付款方式)、付款币种;商家不收手里的币时可以用 Web3 内置的自动换币(多付滑点)。论坛情报在最下面。
/// </summary>
private static void BuyPage(CustomUIBuilder b, SaveData d, int rep)
{
var w = d.Hot;
var sellers = d.Black.Sellers;
if (_merchant >= sellers.Count) _merchant = -1;
bool platform = _merchant < 0;
b.AddLabel(L.T("black.rep", rep, BlackMarket.MaxLevel(rep)) + " " + L.T("mixer.balance", _payAsset, Fmt.Num(w.Avail(_payAsset))) + (w.Taint > 0.5 ? " " + L.T("wallet.taint", Fmt.Credits(w.Taint)) : ""));
b.BeginRow(4f);
foreach (var a in new[] { Assets.PSC, Assets.CRT, Assets.CRA, Assets.Ponero })
b.AddLabel(L.T("black.rep", rep, BlackMarket.MaxLevel(rep)) + (w.Taint > 0.5 ? " " + L.T("wallet.taint", Fmt.Credits(w.Taint)) : ""), "black_rep_dim");
b.BeginRow(8f, TextAnchor.UpperLeft);
b.BeginPanel(SellerW + 8f, -1f, TextAnchor.UpperLeft, 0f, 2f);
_clear.Add(b.Last.image);
var names = new System.Collections.Generic.List<string> { L.T("merchant.Platform") };
for (int k = 0; k < sellers.Count; k++) names.Add(MerchantLabel(sellers[k], true));
for (int i = 0; i < names.Count; i++)
{
var asset = a;
PledgerSkin.Pick(b, asset, asset == _payAsset, () => { _payAsset = asset; Open(); }, _picked);
}
if (!platform)
foreach (PayMode m in Enum.GetValues(typeof(PayMode)))
{
var mode = m;
string n = L.T("black.mode." + m);
PledgerSkin.Pick(b, n, mode == _mode, () => { _mode = mode; Open(); }, _picked);
}
b.End();
b.BeginRow(4f);
foreach (var m in Qtys)
{
int q = m;
PledgerSkin.Pick(b, "×" + q, q == _qty, () => { _qty = q; Open(); }, _picked);
int idx = i - 1;
TableRow(b, SellerW, new[] { SellerW - 8f }, new[] { TextAnchor.MiddleLeft }, new[] { names[i] }, idx == _merchant, () => { _merchant = idx; _pay = null; _message = null; Open(); });
}
b.End();
var all = new System.Collections.Generic.List<System.Collections.Generic.KeyValuePair<int, string>> { new System.Collections.Generic.KeyValuePair<int, string>(-1, L.T("merchant.Platform") + " " + d.Address("platform")) };
for (int k = 0; k < sellers.Count; k++) all.Add(new System.Collections.Generic.KeyValuePair<int, string>(k, MerchantLabel(sellers[k])));
for (int i = 0; i < all.Count; i += 3)
{
b.BeginRow(4f);
for (int k = i; k < Math.Min(all.Count, i + 3); k++)
{
int idx = all[k].Key; string n = all[k].Value;
PledgerSkin.Pick(b, n, idx == _merchant, () => { _merchant = idx; Open(); }, _picked);
}
b.End();
}
b.AddSeparator();
if (platform) PlatformShelf(b, d);
b.BeginPanel(ShelfW, -1f, TextAnchor.UpperLeft, 0f, 4f);
_clear.Add(b.Last.image);
if (_pay != null) PayPanel(b);
else if (platform) PlatformShelf(b, d);
else MerchantShelf(b, d);
b.End();
b.End();
b.AddSeparator();
b.AddHeader(L.T("black.forum.h"));
if (d.Black.Forum.Count == 0) b.AddLabel(L.T("black.forum.quiet"), "forum_quiet_dim");
@@ -1239,18 +1226,153 @@ namespace Decentralized
b.AddLabel(f.StartsWith("phish:") ? L.T("black.forum.phish", f.Substring(6)) : f.StartsWith("scam:") ? L.T("black.forum.scam", BlackDelivery.DisplayName(f.Substring(5))) : f.StartsWith("busted:") ? L.T("black.forum.busted", f.Substring(7)) : L.T("black.forum." + f), "forum_dim");
}
private const float SellerW = 176f, ShelfW = 560f;
private static void PlatformShelf(CustomUIBuilder b, SaveData d)
{
b.AddLabel(L.T("black.platform.line", d.Address("platform")), "platform_line_dim");
b.AddLabel(L.T("black.platform.line"), "platform_line_dim");
if (d.Black.Offers.Count == 0) { b.AddLabel(L.T("black.empty"), "platform_empty_dim"); return; }
Shelf(b, d.Black.Offers.Cast<BmGood>().ToList(), (c, g) =>
{
var offer = (BmOffer)g;
int count = Math.Min(_qty, offer.Qty);
GoodHead(c, g, L.T("black.stock", offer.Qty));
c.AddLabel(L.T("black.price", offer.Price * count) + (count > 1 ? " ×" + count : ""));
c.AddLabel(L.T("black.price", offer.Price));
c.AddFlexibleSpace();
c.AddButton(L.T("black.buy"), () => { string asset = _payAsset; BeginPay(d.Address("platform"), ItemName(offer.Item) + (count > 1 ? " ×" + count : ""), asset, offer.Price * count, pw => BuyOffer(offer, count, asset, pw)); });
c.AddButton(L.T("black.buy"), () => OpenPay(new PayDraft
{
Title = ItemName(offer.Item), Address = d.Address("platform"), Accepted = Merchants.PlatformAccepts, MaxQty = offer.Qty,
Credits = (q, m) => offer.Price * q,
Execute = (asset, q, m, pw) => BuyOffer(offer, q, asset, pw),
}));
}, 3);
}
// ——— 付款面板 ———
/// <summary>一次付款的草稿:买什么、收款地址、商家收哪些币、可选数量与付款方式、合计,以及付款成功后做的事。</summary>
private sealed class PayDraft
{
public string Title, Address, Asset, SwapFrom;
public string[] Accepted;
public int MaxQty = 1, Qty = 1;
public bool Modes;
public PayMode Mode = PayMode.Escrow;
public Func<int, PayMode, double> Credits;
public Action<string, int, PayMode, Wallet> Execute;
}
private static PayDraft _pay;
private static void OpenPay(PayDraft p)
{
var w = Session.Data.Hot;
p.Asset = p.Accepted.FirstOrDefault(a => w.Avail(a) * Session.Price(a) >= p.Credits(1, p.Mode)) ?? p.Accepted[0];
p.SwapFrom = null;
_pay = p;
_message = null;
Open();
}
private static string Summary(PayDraft p) => p.Title + (p.Qty > 1 ? " ×" + p.Qty : "");
/// <summary>付款面板:数量、付款方式、合计、付款币种(商家收的币)、生成账单;下面是自动换币。</summary>
private static void PayPanel(CustomUIBuilder b)
{
var d = Session.Data;
var w = d.Hot;
var p = _pay;
if (p.Qty > p.MaxQty) p.Qty = p.MaxQty;
b.AddHeader(L.T("bpay.h"));
b.AddLabel(p.Title, "bpay_title");
if (p.MaxQty > 1)
{
b.BeginRow(4f);
b.AddLabel(L.T("bpay.qty"));
foreach (var q in Qtys)
{
if (q > p.MaxQty && q != Qtys[0]) continue;
int qq = q;
PledgerSkin.Pick(b, "×" + qq, qq == p.Qty, () => { p.Qty = qq; Open(); }, _picked);
}
b.End();
}
if (p.Modes)
{
b.BeginRow(4f);
foreach (PayMode m in Enum.GetValues(typeof(PayMode)))
{
var mode = m;
PledgerSkin.Pick(b, L.T("black.mode." + m), mode == p.Mode, () => { p.Mode = mode; Open(); }, _picked);
}
b.End();
}
double credits = p.Credits(p.Qty, p.Mode);
b.AddLabel(L.T("bpay.total", Fmt.Credits(credits)), "bpay_total");
b.AddSeparator();
b.AddLabel(L.T("bpay.coin"), "bpay_coin_dim");
b.BeginRow(4f);
foreach (var a in p.Accepted)
{
string asset = a;
PledgerSkin.Pick(b, asset, asset == p.Asset, () => { p.Asset = asset; Open(); }, _picked);
}
b.End();
double need = WalletWindow.RoundUp(credits / Session.Price(p.Asset));
b.AddLabel(L.T("bpay.need", WalletWindow.Amt(need), p.Asset, Fmt.Num(w.Avail(p.Asset))), "bpay_need_dim");
b.AddButton(L.T("bpay.invoice"), () =>
{
string asset = p.Asset; int q = p.Qty; var mode = p.Mode;
string summary = Summary(p);
_pay = null;
BeginPay(p.Address, summary, asset, credits, pw => p.Execute(asset, q, mode, pw));
});
b.AddSeparator();
// 自动换币:从钱包里别的币里换出商家收的币,多付滑点;是合约调用,签名后一步完成
b.AddLabel(L.T("bpay.swap.h", p.Asset), "bpay_swap_dim");
double slipped = credits * (1 + Tuning.BmAutoSwapSlip);
var sources = w.Balances.Keys.Where(a => !p.Accepted.Contains(a) && a != Assets.Ponero && Session.Price(a) > 0 && w.Avail(a) * Session.Price(a) >= slipped).ToList();
if (sources.Count == 0) b.AddLabel(L.T("bpay.swap.none"), "bpay_swap_none_dim");
else
{
if (p.SwapFrom == null || !sources.Contains(p.SwapFrom)) p.SwapFrom = sources[0];
b.BeginRow(4f);
foreach (var s in sources)
{
string src = s;
PledgerSkin.Pick(b, src, src == p.SwapFrom, () => { p.SwapFrom = src; Open(); }, _picked);
}
b.End();
double needSrc = WalletWindow.RoundUp(slipped / Session.Price(p.SwapFrom));
b.AddLabel(L.T("bpay.swap.need", WalletWindow.Amt(needSrc), p.SwapFrom, (Tuning.BmAutoSwapSlip * 100).ToString("0", CultureInfo.InvariantCulture)), "bpay_swap_need_dim");
b.AddButton(L.T("bpay.swap.btn"), () => SwapAndPay(p, credits, needSrc));
}
b.AddSpace(4f);
b.AddButton(L.T("bpay.back"), () => { _pay = null; _message = null; Open(); });
}
/// <summary>自动换币并付款:一次合约调用,先把别的币按滑点换成商家收的币,再下单。</summary>
private static void SwapAndPay(PayDraft p, double credits, double needSrc)
{
var d = Session.Data;
string src = p.SwapFrom, asset = p.Asset; int q = p.Qty; var mode = p.Mode;
_pay = null;
_message = L.T("hw.wait");
Open();
WalletWindow.OpenForBill(new WalletWindow.Bill
{
Contract = true, Asset = src, Amount = needSrc, Spend = needSrc,
Summary = L.T("bpay.swap.sum", Summary(p), src, WalletWindow.Amt(needSrc), asset),
Execute = pw =>
{
if (pw.Avail(src) + 1e-9 < needSrc) { _message = L.T("msg.balance"); Open(); return; }
double got = credits / Session.Price(asset) * 1.002;
pw.Add(src, -needSrc);
pw.Add(asset, got);
pw.RecordBuy(asset, got, credits);
CandleChart.Trade(src, credits); CandleChart.Trade(asset, credits);
Log.Info($"自动换币:{needSrc:0.####} {src} → {got:0.####} {asset}({credits:0} 信用点等值)");
p.Execute(asset, q, mode, pw);
},
});
}
@@ -1269,11 +1391,16 @@ namespace Decentralized
PledgerSkin.Pick(b, "×" + n, Math.Abs(mult - _sellMult) < 1e-9, () => { _sellMult = mult; Open(); }, _picked);
}
b.End();
b.AddLabel(L.T("mixer.balance", _payAsset, Fmt.Num(d.Hot.Avail(_payAsset))) + " " + L.T("black.sell.rating", d.Black.Rating) + " " + L.T("black.sell.slots", d.Black.ActiveListings, Tuning.BmMaxListings));
b.AddLabel(L.T("black.sell.rating", d.Black.Rating) + " " + L.T("black.sell.slots", d.Black.ActiveListings, Tuning.BmMaxListings), "sell_status_dim");
b.AddButton(L.T("black.sell.all"), () =>
{
string asset = _payAsset; double mult = _sellMult;
BeginPay(d.Address("platform"), L.T("black.sell.all") + L.T("pay.listfee", mult.ToString("0.0", CultureInfo.InvariantCulture)), asset, BlackDelivery.Sellable().Sum(it => BlackMarket.ListFee(BlackMarket.ListPriceOf(BlackDelivery.RefValue(it), mult))), pw => ListAll(asset, mult, pw));
double mult = _sellMult;
double fee = BlackDelivery.Sellable().Sum(it => BlackMarket.ListFee(BlackMarket.ListPriceOf(BlackDelivery.RefValue(it), mult)));
OpenPay(new PayDraft
{
Title = L.T("black.sell.all") + L.T("pay.listfee", mult.ToString("0.0", CultureInfo.InvariantCulture)), Address = d.Address("platform"), Accepted = Merchants.PlatformAccepts,
Credits = (q, m) => fee, Execute = (asset, q, m, pw) => ListAll(asset, mult, pw),
});
});
b.AddSeparator();
var items = BlackDelivery.Sellable();
@@ -1331,47 +1458,45 @@ namespace Decentralized
Open();
}
private static string MerchantLabel(BmSeller s) => L.T("merchant." + s.Type) + " " + s.Name;
private static string MerchantLabel(BmSeller s, bool shortName = false) => L.T("merchant." + s.Type) + " " + (shortName ? ChainAddress.Short(s.Name) : s.Name);
private static int _merchant = -1;
private static int _qty = 1;
private static readonly int[] Qtys = { 1, 5, 10 };
/// <summary>商家货架:选一家商家,看它这周的货,选件数下单。</summary>
/// <summary>商家货架:这家的成交数、好评和到货天数,下面是这周的货;点"购买"进付款面板。</summary>
private static void MerchantShelf(CustomUIBuilder b, SaveData d)
{
var sellers = d.Black.Sellers;
var seller = sellers[_merchant];
b.AddLabel(L.T("black.seller", MerchantLabel(seller), seller.Trades, seller.Rating) + " " + L.T("black.days", seller.Days));
var goods = d.Black.Catalog.Where(l => l.Seller == _merchant).Cast<BmGood>().ToList();
int merchant = _merchant;
b.AddLabel(L.T("black.seller", MerchantLabel(seller), seller.Trades, seller.Rating) + " " + L.T("black.days", seller.Days), "black_seller_dim");
var goods = d.Black.Catalog.Where(l => l.Seller == merchant).Cast<BmGood>().ToList();
if (goods.Count == 0) { b.AddLabel(L.T("black.empty"), "shelf_empty_dim"); return; }
Shelf(b, goods, (c, g) =>
{
var listing = (BmListing)g;
int count = Math.Min(_qty, listing.Qty);
GoodHead(c, g, L.T("black.stock", listing.Qty));
c.AddLabel(L.T("black.price", BlackMarket.Pay(BlackMarket.OrderTotal(listing.Price, count), _mode)) + (count > 1 ? " ×" + count : ""));
c.AddLabel(L.T("black.price", BlackMarket.Pay(BlackMarket.OrderTotal(listing.Price, 1), PayMode.Escrow)));
c.AddFlexibleSpace();
bool ok = _mode == PayMode.Escrow || seller.Advance;
c.AddButton(ok ? L.T("black.order") : L.T("black.noadvance"), () =>
c.AddButton(L.T("black.buy"), () => OpenPay(new PayDraft
{
if (!ok) return;
string asset = _payAsset; var mode = _mode;
BeginPay(seller.Name, ItemName(listing.Item) + (count > 1 ? " ×" + count : ""), asset, BlackMarket.Pay(BlackMarket.OrderTotal(listing.Price, count), mode), pw => PlaceOrder(listing, count, asset, mode, pw));
});
});
Title = ItemName(listing.Item), Address = seller.Name, Accepted = Merchants.Accepts(seller.Type), MaxQty = listing.Qty, Modes = seller.Advance,
Credits = (q, m) => BlackMarket.Pay(BlackMarket.OrderTotal(listing.Price, q), m),
Execute = (asset, q, m, pw) => PlaceOrder(listing, q, asset, m, pw),
}));
}, 3);
}
private const int ShelfCols = 4;
private const float CardW = 172f, CardH = 268f;
/// <summary>货架:一行 4 张商品卡。</summary>
private static void Shelf(CustomUIBuilder b, System.Collections.Generic.List<BmGood> goods, Action<CustomUIBuilder, BmGood> card)
private static void Shelf(CustomUIBuilder b, System.Collections.Generic.List<BmGood> goods, Action<CustomUIBuilder, BmGood> card, int cols = ShelfCols)
{
for (int i = 0; i < goods.Count; i += ShelfCols)
for (int i = 0; i < goods.Count; i += cols)
{
b.BeginRow(6f);
for (int k = i; k < Math.Min(goods.Count, i + ShelfCols); k++)
for (int k = i; k < Math.Min(goods.Count, i + cols); k++)
{
var g = goods[k];
b.BeginPanel(CardW, CardH, TextAnchor.UpperCenter, 6f, 3f);
@@ -1497,9 +1622,12 @@ namespace Decentralized
var d = Session.Data;
int fee = e.Sale != null ? BlackMarket.RepriceFee(price) : BlackMarket.ListFee(price);
_edit = null;
string asset = _payAsset;
BeginPay(d.Address("platform"), ItemName(e.Id) + L.T(e.Sale != null ? "pay.repricefee" : "pay.listfee", mult.ToString("0.00", CultureInfo.InvariantCulture)), asset, fee,
pw => { if (e.Sale != null) RepriceSale(e.Sale, mult, asset, pw); else ListItem(e.Item, mult, asset, pw); });
OpenPay(new PayDraft
{
Title = ItemName(e.Id) + L.T(e.Sale != null ? "pay.repricefee" : "pay.listfee", mult.ToString("0.00", CultureInfo.InvariantCulture)), Address = d.Address("platform"), Accepted = Merchants.PlatformAccepts,
Credits = (q, m) => fee,
Execute = (asset, q, m, pw) => { if (e.Sale != null) RepriceSale(e.Sale, mult, asset, pw); else ListItem(e.Item, mult, asset, pw); },
});
});
b.AddButton(L.T("black.edit.cancel"), () => { _edit = null; _message = null; Open(); });
b.End();