版本 1.0.0-rc8:卖货橱窗重构、收款币种、Gas 档位速度、区块自然增长、钱包持仓

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ZHU MINCHAOandClaude Sonnet 5.5 committed 2026-11-05 21:32:30 +08:00
1 parent 018ee7d0d2
commit 93e26fc4f5
20 files changed
+582 -498

No files matched your search

+275 -300
View File
@@ -32,9 +32,6 @@ namespace Decentralized
private enum Page { Home, Bank, Poinbase, Web3, Mining, Gray, Wallets }
private static Page _page = Page.Home;
private sealed class EditPrice { public GameItem Item; public BmSale Sale; public string Id; public long Ref; }
private static EditPrice _edit;
private static string _editPrice = "";
private static string _amount = "";
private static string _message;
@@ -59,7 +56,7 @@ namespace Decentralized
TabRows(b, Enum.GetValues(typeof(Page)).Cast<Page>().Where(p => p != Page.Gray || GrayAvailable).Select(p =>
{
var page = p;
return new TabDef { Name = L.T("tab." + p.ToString().ToLowerInvariant()), On = page == _page, Click = () => { _page = page; _message = null; _edit = null; Open(); } };
return new TabDef { Name = L.T("tab." + p.ToString().ToLowerInvariant()), On = page == _page, Click = () => { _page = page; _message = null; Open(); } };
}).ToList(), true);
ConnectPicker(b);
b.AddSeparator();
@@ -86,7 +83,7 @@ namespace Decentralized
Log.Guard("购物车按钮", () => CartFab(win));
}
private static void Go(Page p) { _page = p; _message = null; _edit = null; Open(); }
private static void Go(Page p) { _page = p; _message = null; Open(); }
/// <summary>钱包那边交易落地后刷新终端(开着的话),带上结果消息。</summary>
public static void Refresh(string message)
@@ -1169,7 +1166,7 @@ namespace Decentralized
private static void WalletsPage(CustomUIBuilder b)
{
var d = Session.Data;
TabRows(b, Enum.GetValues(typeof(WalletTab)).Cast<WalletTab>().Select(t => { var tab = t; return new TabDef { Name = L.T("wallets.sub." + t.ToString().ToLowerInvariant()), On = tab == _walletTab, Click = () => { _walletTab = tab; _message = null; _edit = null; Open(); } }; }).ToList(), false);
TabRows(b, Enum.GetValues(typeof(WalletTab)).Cast<WalletTab>().Select(t => { var tab = t; return new TabDef { Name = L.T("wallets.sub." + t.ToString().ToLowerInvariant()), On = tab == _walletTab, Click = () => { _walletTab = tab; _message = null; Open(); } }; }).ToList(), false);
if (_walletTab == WalletTab.Explorer) { ExplorerPage(b); return; }
b.AddLabel(L.T("wallets.count", d.Wallets.Count, Tuning.WalletMax), "wallets_count_dim");
if (d.Wallets.Count == 0) { b.AddLabel(L.T("wallets.none"), "wallets_none_dim"); return; }
@@ -1220,6 +1217,13 @@ namespace Decentralized
b.End();
b.End();
}
if (d.Connected)
{
// 选中的钱包:下面列出它持有的币和数量
b.AddSeparator();
b.AddHeader(L.T("web3.wallet.h") + " " + WalletWindow.NameOf(d.Hot));
Holdings(b, d.Hot, a => true);
}
if (_renamePick != null) RenameRow(b);
}
@@ -1329,7 +1333,7 @@ namespace Decentralized
if (_grayTab == GrayTab.Mixer && !Unlock.MixerUnlocked) _grayTab = GrayTab.Black;
if (_grayTab == GrayTab.Black && !Unlock.BlackUnlocked) _grayTab = GrayTab.Mixer;
if (Unlock.MixerUnlocked && Unlock.BlackUnlocked)
TabRows(b, Enum.GetValues(typeof(GrayTab)).Cast<GrayTab>().Select(t => { var tab = t; return new TabDef { Name = L.T("gray.sub." + t.ToString().ToLowerInvariant()), On = tab == _grayTab, Click = () => { _grayTab = tab; _message = null; _edit = null; Open(); } }; }).ToList(), false);
TabRows(b, Enum.GetValues(typeof(GrayTab)).Cast<GrayTab>().Select(t => { var tab = t; return new TabDef { Name = L.T("gray.sub." + t.ToString().ToLowerInvariant()), On = tab == _grayTab, Click = () => { _grayTab = tab; _message = null; Open(); } }; }).ToList(), false);
if (!Session.Data.Connected) b.AddLabel(L.T("web3.err.noconn"), "term_note");
if (_grayTab == GrayTab.Mixer) MixerPage(b); else BlackPage(b);
}
@@ -1341,7 +1345,7 @@ namespace Decentralized
if (_web3Tab == Web3Tab.Predict && !PredictGame.Available(StoreStation.GetDayCounter())) _web3Tab = Web3Tab.Trade;
TabRows(b, Enum.GetValues(typeof(Web3Tab)).Cast<Web3Tab>()
.Where(t => t != Web3Tab.Predict || PredictGame.Available(StoreStation.GetDayCounter()))
.Select(t => { var tab = t; return new TabDef { Name = L.T("web3.sub." + t.ToString().ToLowerInvariant()), On = tab == _web3Tab, Click = () => { _web3Tab = tab; _message = null; _edit = null; Open(); } }; }).ToList(), false);
.Select(t => { var tab = t; return new TabDef { Name = L.T("web3.sub." + t.ToString().ToLowerInvariant()), On = tab == _web3Tab, Click = () => { _web3Tab = tab; _message = null; Open(); } }; }).ToList(), false);
if (!Session.Data.Connected) b.AddLabel(L.T("web3.err.noconn"), "term_note");
if (_web3Tab == Web3Tab.Stake) { StakePage(b); return; }
if (_web3Tab == Web3Tab.Predict) { PredictPage(b); return; }
@@ -1481,13 +1485,11 @@ 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; _pay = null; _checkout = false; Open(); }, _picked);
PledgerSkin.Pick(b, name, tab == _blackTab, () => { _blackTab = tab; _message = null; _sellView = SellView.Showcase; _stage.Clear(); _delist.Clear(); _coinsOpen = false; _checkout = false; 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 (_checkout) { CheckoutPage(b, d); 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);
@@ -1545,142 +1547,142 @@ namespace Decentralized
});
}
// ——— 付款面板 ———
/// <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);
},
});
}
private static double _sellMult = 1.0;
/// <summary>出售:储藏区里能上架的货做成货架,选标价倍率后上架。</summary>
private static void SellPage(CustomUIBuilder b, int day)
private enum SellView { Showcase, Add, Fee }
private static SellView _sellView = SellView.Showcase;
private static readonly Dictionary<string, int> _stage = new Dictionary<string, int>();
private static readonly Dictionary<string, int> _delist = new Dictionary<string, int>();
private static bool _coinsOpen;
private static string _feeCoin;
/// <summary>储藏区里同一种货(标识、型号、真伪、成色都相同)聚成一组。</summary>
private sealed class SellGroup
{
public string Key, Id, Model, FakeOf;
public double Health;
public long Ref;
public GoodKind Kind;
public readonly List<GameItem> Items = new List<GameItem>();
}
private static List<SellGroup> SellGroups()
{
var map = new Dictionary<string, SellGroup>();
var list = new List<SellGroup>();
foreach (var it in BlackDelivery.Sellable())
{
string id = BlackDelivery.SellId(it);
if (id == null) continue;
var kind = BlackDelivery.KindOf(id);
var u = kind == GoodKind.Gpu ? Items.ToUnit(it) : null;
string key = BmSale.KeyOf(id, u?.Model, u?.FakeOf, u?.Health ?? 0);
if (!map.TryGetValue(key, out var g))
{
g = new SellGroup { Key = key, Id = id, Model = u?.Model, FakeOf = u?.FakeOf, Health = u?.Health ?? 0, Ref = BlackDelivery.RefValue(it), Kind = kind };
map[key] = g; list.Add(g);
}
g.Items.Add(it);
}
return list;
}
private static double MultFor(string key) => Session.Data.Black.MultOf(key) ?? _sellMult;
private static int Staged(string key) => _stage.TryGetValue(key, out int n) ? n : 0;
/// <summary>这一组预备数量再加一件会不会超限:返回提示文字,可以加返回 null。</summary>
private static string StageBlock(SellGroup g)
{
var d = Session.Data;
var items = BlackDelivery.Sellable();
int have = d.Black.ActiveOf(g.Key), staged = Staged(g.Key);
if (have + staged + 1 > Tuning.BmMaxStack) return L.T("black.sell.limit.stack", Tuning.BmMaxStack);
if (have == 0 && staged == 0)
{
int kinds = d.Black.ActiveKinds + _stage.Count(kv => kv.Value > 0 && d.Black.ActiveOf(kv.Key) == 0);
if (kinds >= Tuning.BmMaxKinds) return L.T("black.sell.limit.kinds", Tuning.BmMaxKinds);
}
return null;
}
private static BmGood GoodOf(string id, GoodKind kind, string model, double health, int price) =>
new BmGood { Item = id, Kind = kind, Level = kind == GoodKind.Gpu ? 2 : Merchants.Info(id).Level, Model = model, Health = health, Price = price };
/// <summary>卖货:默认是已上架的橱窗;"上架更多"进入备货页,"货架"付平台费后上架。</summary>
private static void SellPage(CustomUIBuilder b, int day)
{
if (_sellView == SellView.Add) { SellAddPage(b); return; }
if (_sellView == SellView.Fee) { SellFeePage(b); return; }
ShowcasePage(b);
}
private static void ShowcasePage(CustomUIBuilder b)
{
var d = Session.Data;
var listed = d.Black.Sales.Where(x => x.State == SaleState.Listed).GroupBy(x => x.Key).ToList();
b.AddLabel(L.T("black.sell.rating", d.Black.Rating) + " " + L.T("black.sell.kinds", d.Black.ActiveKinds, Tuning.BmMaxKinds), "sell_status_dim");
b.BeginRow(6f);
b.AddButton(L.T("black.sell.add"), () => { _sellView = SellView.Add; _delist.Clear(); _message = null; Open(); });
PledgerSkin.Pick(b, L.T("black.sell.coins"), _coinsOpen, () => { _coinsOpen = !_coinsOpen; Open(); }, _picked);
b.End();
if (_coinsOpen)
{
b.BeginRow(4f);
foreach (var c in BlackMarket.SellCoins)
{
string coin = c;
PledgerSkin.Pick(b, coin, d.Black.AcceptedCoins.Contains(coin), () =>
{
if (!d.Black.ToggleCoin(coin)) _message = L.T("black.sell.nocoin");
else { _message = null; Session.Save(); }
Open();
}, _picked);
}
b.End();
}
if (listed.Count == 0) { b.AddLabel(L.T("black.sell.empty"), "sell_none_dim"); return; }
var cards = new Dictionary<BmGood, IGrouping<string, BmSale>>();
Shelf(b, listed.Select(g =>
{
var s = g.First();
var kind = BlackDelivery.KindOf(s.Item);
var good = GoodOf(s.Item, kind, s.Model, s.Health, s.ListPrice);
cards[good] = g;
return good;
}).ToList(), (c, good) =>
{
var g = cards[good];
string key = g.Key; int count = g.Count();
GoodHead(c, good, L.T("black.sell.have", count));
c.AddLabel(L.T("black.price", good.Price));
c.AddFlexibleSpace();
int n = _delist.TryGetValue(key, out int cur) ? cur : 0;
c.AddButton(n > 0 ? L.T("black.sell.off.n", n) : L.T("black.sell.off"), () => { _delist[key] = (n + 1) % (count + 1); _message = null; Open(); });
});
}
private static void ConfirmDelist()
{
var d = Session.Data;
int n = 0;
foreach (var kv in _delist.Where(kv => kv.Value > 0).ToList())
foreach (var s in d.Black.Sales.Where(x => x.State == SaleState.Listed && x.Key == kv.Key).Take(kv.Value).ToList())
{
d.Black.Delist(s);
Items.Give(BlackDelivery.Make(s.Item, s.Model, s.FakeOf, s.Health));
n++;
}
_delist.Clear();
_message = L.T("black.sell.delisted", n);
Session.Save();
Open();
}
private static void SellAddPage(CustomUIBuilder b)
{
var d = Session.Data;
var groups = SellGroups();
_stage.Keys.Where(k => groups.All(g => g.Key != k)).ToList().ForEach(k => _stage.Remove(k));
foreach (var g in groups) if (Staged(g.Key) > g.Items.Count) _stage[g.Key] = g.Items.Count;
b.BeginRow(8f, TextAnchor.UpperLeft);
b.BeginPanel(SellerW + 8f, -1f, TextAnchor.UpperLeft, 0f, 2f);
_clear.Add(b.Last.image);
@@ -1688,79 +1690,111 @@ namespace Decentralized
foreach (var m in Tuning.BmSellMults)
{
double mult = m;
TableRow(b, SellerW, new[] { 70f, SellerW - 78f }, new[] { TextAnchor.MiddleLeft, TextAnchor.MiddleRight },
new[] { "×" + m.ToString("0.0", CultureInfo.InvariantCulture), L.T("black.sell.days", Math.Round(BlackMarket.ExpectedDays(m))) },
TableRow(b, SellerW, new[] { SellerW - 8f }, new[] { TextAnchor.MiddleLeft },
new[] { "×" + m.ToString("0.0", CultureInfo.InvariantCulture) },
Math.Abs(mult - _sellMult) < 1e-9, () => { _sellMult = mult; Open(); });
}
b.AddSpace(4f);
b.AddLabel(L.T("black.sell.rating", d.Black.Rating), "sell_status_dim");
b.AddLabel(L.T("black.sell.slots", d.Black.ActiveListings, Tuning.BmMaxListings), "sell_status_dim");
b.AddLabel(L.T("black.sell.kinds", d.Black.ActiveKinds, Tuning.BmMaxKinds), "sell_status_dim");
b.AddSpace(4f);
b.AddButton(L.T("black.sell.all"), () =>
{
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.AddButton(L.T("black.sell.back"), () => { _sellView = SellView.Showcase; _stage.Clear(); _message = null; Open(); });
b.End();
b.BeginPanel(ShelfW, -1f, TextAnchor.UpperLeft, 0f, 4f);
_clear.Add(b.Last.image);
if (items.Count == 0) b.AddLabel(L.T("black.sell.none"), "sell_none_dim");
else Shelf(b, items.Select(it =>
if (groups.Count == 0) b.AddLabel(L.T("black.sell.none"), "sell_none_dim");
else
{
string id = BlackDelivery.SellId(it);
var kind = BlackDelivery.KindOf(id);
var g = new BmGood { Item = id, Kind = kind, Level = kind == GoodKind.Gpu ? 2 : Merchants.Info(id).Level };
if (kind == GoodKind.Gpu) { var uu = Items.ToUnit(it); g.Health = uu.Health; g.Model = uu.Model; }
g.Price = BlackMarket.ListPriceOf(BlackDelivery.RefValue(it), _sellMult);
return new { g, it };
}).Select(x => { SellCards[x.g] = x.it; return x.g; }).ToList(), (c, g) =>
{
var it = SellCards[g];
GoodHead(c, g, "");
c.AddLabel(L.T("black.price", g.Price));
c.AddFlexibleSpace();
c.AddButton(L.T("black.sell.list"), () => BeginEdit(new EditPrice { Item = it, Id = g.Item, Ref = BlackDelivery.RefValue(it) }, g.Price));
});
b.End();
b.End();
}
private static readonly System.Collections.Generic.Dictionary<BmGood, GameItem> SellCards = new System.Collections.Generic.Dictionary<BmGood, GameItem>();
private static void ListAll(string asset, double mult, Wallet pw)
{
var d = Session.Data;
int n = 0;
foreach (var it in BlackDelivery.Sellable())
{
string id = BlackDelivery.SellId(it);
if (id == null) continue;
var u = Items.IsGpu(it) ? Items.ToUnit(it) : null;
var s = d.Black.List(id, u?.Model, u?.FakeOf, u?.Health ?? 0, BlackDelivery.RefValue(it), mult, pw, asset, StoreStation.GetDayCounter(), Session.Price);
if (s == null) break;
it.Destroy();
n++;
var cards = new Dictionary<BmGood, SellGroup>();
Shelf(b, groups.Select(g =>
{
var good = GoodOf(g.Id, g.Kind, g.Model, g.Health, BlackMarket.ListPriceOf(g.Ref, MultFor(g.Key)));
cards[good] = g;
return good;
}).ToList(), (c, good) =>
{
var g = cards[good];
GoodHead(c, good, "");
c.AddLabel(L.T("black.price", good.Price));
c.AddLabel(L.T("black.stock", g.Items.Count), "good_line2_dim");
c.AddFlexibleSpace();
c.BeginRow(4f, TextAnchor.MiddleCenter);
RowKey(c, "-", () => { if (Staged(g.Key) > 0) _stage[g.Key] = Staged(g.Key) - 1; _message = null; Open(); });
Cell(c, 34f, Staged(g.Key).ToString(), false, TextAnchor.MiddleCenter);
RowKey(c, "+", () =>
{
string block = Staged(g.Key) >= g.Items.Count ? null : StageBlock(g);
if (block != null) { _message = block; Open(); return; }
if (Staged(g.Key) < g.Items.Count) _stage[g.Key] = Staged(g.Key) + 1;
_message = null; Open();
});
c.End();
});
}
_message = L.T("black.sell.listedall", n);
Session.Save();
Open();
b.End();
b.End();
}
private static void ListItem(GameItem it, double mult, string asset, Wallet pw)
private static double StageFee(List<SellGroup> groups) =>
groups.Sum(g => Staged(g.Key) * (double)BlackMarket.ListFee(BlackMarket.ListPriceOf(g.Ref, MultFor(g.Key))));
/// <summary>货架:左栏是准备上架的清单,右栏选收款币付平台费,付完才上架。</summary>
private static void SellFeePage(CustomUIBuilder b)
{
var d = Session.Data;
string id = BlackDelivery.SellId(it);
if (id == null) { Open(); return; }
var u = Items.IsGpu(it) ? Items.ToUnit(it) : null;
var s = d.Black.List(id, u?.Model, u?.FakeOf, u?.Health ?? 0, BlackDelivery.RefValue(it), mult, pw, asset, StoreStation.GetDayCounter(), Session.Price);
if (s == null) { _message = d.Black.ActiveListings >= Tuning.BmMaxListings ? L.T("black.sell.full", Tuning.BmMaxListings) : L.T("msg.balance"); Open(); return; }
it.Destroy();
_message = L.T("black.sell.listed", ItemName(id), s.ListPrice);
var w = d.Hot;
var groups = SellGroups().Where(g => Staged(g.Key) > 0).ToList();
if (groups.Count == 0) { _sellView = SellView.Add; Open(); return; }
double fee = StageFee(groups);
var acc = Merchants.PlatformAccepts;
if (_feeCoin == null || !acc.Contains(_feeCoin)) _feeCoin = acc[0];
Columns(b, l =>
{
l.AddHeader(L.T("black.fee.h"));
foreach (var g in groups)
l.AddLabel(L.T("black.fee.line", ItemName(g.Id), Staged(g.Key), BlackMarket.ListPriceOf(g.Ref, MultFor(g.Key)), "×" + MultFor(g.Key).ToString("0.0", CultureInfo.InvariantCulture)), "cart_shop");
l.AddSeparator();
l.AddLabel(L.T("black.fee.total", Fmt.Credits(fee)), "cart_total");
}, r =>
{
r.AddHeader(L.T("bpay.h"));
r.AddLabel(L.T("bpay.coin"), "bpay_coin_dim");
r.BeginRow(4f);
foreach (var a in acc) { string asset = a; PledgerSkin.Pick(r, asset, asset == _feeCoin, () => { _feeCoin = asset; Open(); }, _picked); }
r.End();
r.AddLabel(L.T("bpay.need", WalletWindow.Amt(WalletWindow.RoundUp(fee / Session.Price(_feeCoin))), _feeCoin, Fmt.Num(w.Avail(_feeCoin))), "bpay_need_dim");
r.AddButton(L.T("bpay.invoice"), () =>
{
string coin = _feeCoin;
string summary = L.T("black.fee.sum", groups.Sum(g => Staged(g.Key)));
BeginPay(d.Address("platform"), summary, coin, fee, pw => ListStaged(coin, pw));
});
r.AddSpace(4f);
r.AddButton(L.T("black.sell.back"), () => { _sellView = SellView.Add; _message = null; Open(); });
});
}
/// <summary>付完平台费后:按预备数量逐件上架,货从储藏区拿走。</summary>
private static void ListStaged(string asset, Wallet pw)
{
var d = Session.Data;
int day = StoreStation.GetDayCounter(), ok = 0, fail = 0;
foreach (var g in SellGroups())
{
int n = Staged(g.Key);
if (n <= 0) continue;
double mult = MultFor(g.Key);
foreach (var it in g.Items.Take(n))
{
var s = d.Black.List(g.Id, g.Model, g.FakeOf, g.Health, g.Ref, mult, pw, asset, day, Session.Price);
if (s == null) { fail++; continue; }
it.Destroy();
ok++;
}
}
_stage.Clear();
_sellView = SellView.Showcase;
_message = fail == 0 ? L.T("black.sell.listedall", ok) : L.T("black.sell.partial", ok, fail);
Session.Save();
Open();
}
@@ -1775,7 +1809,7 @@ namespace Decentralized
private static readonly Dictionary<int, string> _ckGroupCoin = new Dictionary<int, string>();
/// <summary>读档或换局时清空购物车(货架对象已经不是同一批了)。</summary>
public static void ClearCart() { _cart.Clear(); _checkout = false; _ckMode.Clear(); _ckGroupCoin.Clear(); _ckSwap.Clear(); }
public static void ClearCart() { _stage.Clear(); _delist.Clear(); _sellView = SellView.Showcase; _cart.Clear(); _checkout = false; _ckMode.Clear(); _ckGroupCoin.Clear(); _ckSwap.Clear(); }
private static int StockOf(BmGood g) => g is BmOffer o ? o.Qty : ((BmListing)g).Qty;
@@ -1808,12 +1842,33 @@ namespace Decentralized
private static double GroupCredits(SaveData d, int seller) => _cart.Where(l => l.Seller == seller).Sum(l => LineCredits(d, l));
/// <summary>悬浮购物车:商店页右下角一个常驻的小按钮,显示件数,点开进结算页。</summary>
private sealed class Fab { public string Text; public bool On; public Action Click; }
/// <summary>右下角悬浮按钮:买货页是购物车,卖货的备货页是货架,橱窗里有预备下架时是确定下架;其余页面没有。</summary>
private static Fab FabNow()
{
if (_page != Page.Gray || _grayTab != GrayTab.Black || _checkout || WalletWindow.Pending != null || !Unlock.BlackUnlocked) return null;
if (_blackTab == BlackTab.Buy) return new Fab { Text = L.T("cart.fab", CartCount()), On = CartCount() > 0, Click = () => { _checkout = true; _message = null; Open(); } };
if (_blackTab != BlackTab.Sell) return null;
if (_sellView == SellView.Add)
{
int n = _stage.Values.Sum();
return new Fab { Text = L.T("shelf.fab", n), On = n > 0, Click = () => { if (n > 0) { _sellView = SellView.Fee; _message = null; Open(); } } };
}
if (_sellView == SellView.Showcase)
{
int n = _delist.Values.Sum();
return n > 0 ? new Fab { Text = L.T("delist.fab", n), On = true, Click = ConfirmDelist } : null;
}
return null;
}
private static void CartFab(CustomUIWindow win)
{
if (win?.root == null) return;
var old = win.root.transform.Find("pd_cart_fab");
bool show = _page == Page.Gray && _grayTab == GrayTab.Black && !_checkout && WalletWindow.Pending == null && Unlock.BlackUnlocked;
if (!show) { if (old != null) UnityEngine.Object.Destroy(old.gameObject); return; }
var fab = FabNow();
if (fab == null) { if (old != null) UnityEngine.Object.Destroy(old.gameObject); return; }
if (old != null) UnityEngine.Object.DestroyImmediate(old.gameObject);
var go = new GameObject("pd_cart_fab", typeof(RectTransform), typeof(CanvasRenderer), typeof(Image), typeof(Button), typeof(LayoutElement));
go.transform.SetParent(win.root.transform, false);
@@ -1821,20 +1876,20 @@ namespace Decentralized
var rt = (RectTransform)go.transform;
rt.anchorMin = rt.anchorMax = new Vector2(1f, 0f);
rt.pivot = new Vector2(1f, 0f);
rt.sizeDelta = new Vector2(132f, 36f);
rt.sizeDelta = new Vector2(Mathf.Max(132f, fab.Text.Length * 9f + 40f), 36f);
rt.anchoredPosition = new Vector2(-34f, 34f);
var img = go.GetComponent<Image>();
PledgerSkin.KeyShape(img, CartCount() > 0 ? PledgerSkin.Ink : PledgerSkin.Key);
PledgerSkin.KeyShape(img, fab.On ? PledgerSkin.Ink : PledgerSkin.Key);
var btn = go.GetComponent<Button>();
btn.transition = Selectable.Transition.None;
btn.onClick.AddListener(() => { _checkout = true; _message = null; Open(); });
btn.onClick.AddListener(() => fab.Click());
var tgo = new GameObject("label", typeof(RectTransform), typeof(CanvasRenderer), typeof(TextMeshProUGUI));
tgo.transform.SetParent(go.transform, false);
var trt = (RectTransform)tgo.transform;
trt.anchorMin = Vector2.zero; trt.anchorMax = Vector2.one; trt.offsetMin = trt.offsetMax = Vector2.zero;
var tmp = tgo.GetComponent<TextMeshProUGUI>();
if (win.frame?.titleText != null) { tmp.font = win.frame.titleText.font; tmp.fontSharedMaterial = win.frame.titleText.fontSharedMaterial; }
tmp.text = L.T("cart.fab", CartCount());
tmp.text = fab.Text;
tmp.fontSize = 15f;
tmp.fontStyle = FontStyles.Bold;
tmp.alignment = TextAlignmentOptions.Center;
@@ -2128,106 +2183,26 @@ namespace Decentralized
Open();
}
private static void BeginEdit(EditPrice e, int initial)
{
_edit = e;
_editPrice = initial.ToString(CultureInfo.InvariantCulture);
_message = null;
Open();
}
private static int MinPrice(long refValue) => (int)Math.Ceiling(Tuning.BmSellMin * refValue - 1e-9);
private static int MaxPrice(long refValue) => (int)Math.Floor(Tuning.BmSellMax * refValue + 1e-9);
/// <summary>设定标价:显示参考价,档位按钮只是帮忙填价格,价格也可以手输;按 标价 / 参考价 的比例算成交和污点。</summary>
private static void EditPage(CustomUIBuilder b)
{
var e = _edit;
b.AddHeader(ItemName(e.Id));
b.AddLabel(L.T("black.edit.ref", e.Ref), "edit_ref_dim");
b.BeginPanel(RightW, -1f, TextAnchor.MiddleCenter, 0f, 0f);
if (b.Last != null && b.Last.image != null) _clear.Add(b.Last.image);
b.AddInput(L.T("black.edit.input"), _editPrice, s => _editPrice = s, "price", s => _editPrice = s);
b.End();
b.BeginRow(4f);
foreach (var m in Tuning.BmSellMults)
{
double mult = m;
string label = "×" + m.ToString("0.0", CultureInfo.InvariantCulture);
int targetPrice = BlackMarket.ListPriceOf(e.Ref, mult);
bool sel = int.TryParse((_editPrice ?? "").Trim(), NumberStyles.Integer, CultureInfo.InvariantCulture, out int curP) && curP == targetPrice;
PledgerSkin.Pick(b, label, sel, () => { _editPrice = targetPrice.ToString(CultureInfo.InvariantCulture); Open(); }, _picked);
}
b.End();
if (int.TryParse((_editPrice ?? "").Trim(), NumberStyles.Integer, CultureInfo.InvariantCulture, out int p) && p > 0)
b.AddLabel(L.T("black.edit.price", p, (p / (double)e.Ref).ToString("0.00", CultureInfo.InvariantCulture)));
b.BeginRow(6f);
b.AddButton(L.T("black.edit.ok"), () =>
{
if (!int.TryParse((_editPrice ?? "").Trim(), NumberStyles.Integer, CultureInfo.InvariantCulture, out int price) || price < MinPrice(e.Ref) || price > MaxPrice(e.Ref))
{ _message = L.T("black.edit.range", MinPrice(e.Ref), MaxPrice(e.Ref)); Open(); return; }
double mult = price / (double)e.Ref;
var d = Session.Data;
int fee = e.Sale != null ? BlackMarket.RepriceFee(price) : BlackMarket.ListFee(price);
_edit = null;
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();
}
private static void RepriceSale(BmSale s, double mult, string asset, Wallet pw)
{
var d = Session.Data;
if (!d.Black.Reprice(s, mult, pw, asset, StoreStation.GetDayCounter(), Session.Price)) { _message = L.T("msg.balance"); Open(); return; }
_message = L.T("black.sell.repriced", ItemName(s.Item), s.ListPrice);
Session.Save();
Open();
}
private static void Delist(BmSale s)
{
var d = Session.Data;
d.Black.Delist(s);
Items.Give(BlackDelivery.Make(s.Item, s.Model, s.FakeOf, s.Health));
Session.Save();
Open();
}
/// <summary>我的订单与论坛情报。</summary>
/// <summary>在途订单:买入还没取货的订单,和卖出已成交、货款在托管中的订单;每条显示状态与到期日。</summary>
private static void OrdersPage(CustomUIBuilder b, int day)
{
var d = Session.Data;
var buys = d.Black.Orders.Where(o => o.State == OrderState.Pending || o.State == OrderState.Ready).OrderByDescending(x => x.PlacedDay).ToList();
var sells = d.Black.Sales.Where(x => x.State == SaleState.Sold).OrderByDescending(x => x.ListedDay).ToList();
b.BeginRow(8f, TextAnchor.UpperLeft);
b.BeginPanel((SellerW + 8f + ShelfW) * 0.45f, -1f, TextAnchor.UpperLeft, 4f, 4f);
b.BeginPanel((SellerW + 8f + ShelfW) * 0.5f, -1f, TextAnchor.UpperLeft, 4f, 4f);
_clear.Add(b.Last.image);
b.AddHeader(L.T("black.orders.in"));
if (d.Black.Orders.Count == 0) b.AddLabel(L.T("black.mine.none"), "orders_in_none_dim");
foreach (var o in d.Black.Orders.OrderByDescending(x => x.PlacedDay))
b.AddLabel(L.T("black.mine.line", ItemName(o.Item) + (o.Count > 1 ? " ×" + o.Count : ""), BlackDelivery.DisplayName(o.Seller), L.T("black.mode." + o.Mode), L.T("black.state." + o.State + (o.Why != null && o.State != OrderState.Done ? "." + o.Why : "")), o.DueDay));
if (buys.Count == 0) b.AddLabel(L.T("black.mine.none"), "orders_in_none_dim");
foreach (var o in buys)
b.AddLabel(L.T("black.mine.line", ItemName(o.Item) + (o.Count > 1 ? " ×" + o.Count : ""), BlackDelivery.DisplayName(o.Seller), L.T("black.mode." + o.Mode), L.T("black.state." + o.State), o.DueDay));
b.End();
b.BeginPanel((SellerW + 8f + ShelfW) * 0.55f, -1f, TextAnchor.UpperLeft, 4f, 4f);
b.BeginPanel((SellerW + 8f + ShelfW) * 0.5f, -1f, TextAnchor.UpperLeft, 4f, 4f);
_clear.Add(b.Last.image);
b.AddHeader(L.T("black.orders.out"));
if (d.Black.Sales.Count == 0) b.AddLabel(L.T("black.mine.none"), "orders_out_none_dim");
foreach (var sl in d.Black.Sales.Where(x => x.State != SaleState.Delisted || x.Why == "fake").OrderByDescending(x => x.ListedDay))
{
var sale = sl;
string st = sale.State == SaleState.Sold ? L.T("black.sale.Sold", sale.PayDay) : L.T("black.sale." + sale.State + (sale.State == SaleState.Delisted && sale.Why != null ? "." + sale.Why : ""));
b.AddLabel(L.T("black.mine.sale", ItemName(sale.Item), sale.Ref, sale.ListPrice, sale.Mult.ToString("0.00", CultureInfo.InvariantCulture), st));
if (sale.State == SaleState.Listed)
{
b.BeginRow(6f);
b.AddButton(L.T("black.sell.reprice"), () => BeginEdit(new EditPrice { Sale = sale, Id = sale.Item, Ref = sale.Ref }, sale.ListPrice));
b.AddButton(L.T("black.sell.delist"), () => Delist(sale));
b.End();
}
}
if (sells.Count == 0) b.AddLabel(L.T("black.mine.none"), "orders_out_none_dim");
foreach (var sale in sells)
b.AddLabel(L.T("black.mine.sale", ItemName(sale.Item), sale.Ref, sale.ListPrice, sale.Mult.ToString("0.00", CultureInfo.InvariantCulture), L.T("black.sale.Sold", sale.PayDay, sale.PayAsset ?? Assets.CRT)));
b.End();
b.End();
}