using System.Globalization;
using System.Text.Json;
using JiDangJia.Contracts;
using JiDangJia.Core;
using JiDangJia.Services;
namespace JiDangJia.Views;
internal static partial class CommunityUi
{
public static string Name(CommunityAccount peer)=>peer.Remark.Length>0?peer.Remark+"("+peer.Name+")":peer.Name;
public static string FeedPath(FeedFilter f)
{
var values=new Dictionary<string,string>{["tab"]=f.Tab,["before"]=f.Before.ToString(),["city"]=f.City,["intent"]=f.Intent,["category"]=f.Category,["subcategory"]=f.Subcategory,["brand"]=f.Brand,["condition"]=f.Condition,["query"]=f.Query,["serviceCategory"]=f.ServiceCategory};
if(f.MinPrice.HasValue)values["minPrice"]=f.MinPrice.Value.ToString(CultureInfo.InvariantCulture);if(f.MaxPrice.HasValue)values["maxPrice"]=f.MaxPrice.Value.ToString(CultureInfo.InvariantCulture);return "/feed?"+string.Join("&",values.Where(p=>p.Value.Length>0).Select(p=>p.Key+"="+Uri.EscapeDataString(p.Value)));
}
public static View Photos(List<CommunityMedia>? media,ContentPage page,bool all=false)
{
var strip=new HorizontalStackLayout{Spacing=8};foreach(var m in (media??[]).Where(m=>m.Kind=="IMAGE").Take(all?int.MaxValue:3))strip.Add(Ui.Tap(new ProtectedMediaImage(m,130){WidthRequest=130},()=>page.Navigation.PushAsync(new CommunityMediaPage(m)),"查看发布图片"));
return new ScrollView{Content=strip,Orientation=ScrollOrientation.Horizontal,HorizontalScrollBarVisibility=ScrollBarVisibility.Never,IsVisible=strip.Count>0};
}
public static View SystemImage(string name,int size=64)
{
var asset=name.Contains("配货")?"service_sourcing.png":name.Contains("车辆")?"service_car.png":name.Contains("奢侈品")?"service_luxury.png":name.Contains("烟酒")?"service_tobacco.png":"";
View graphic=asset.Length>0?new Image{Source=asset,Aspect=Aspect.AspectFit,WidthRequest=size,HeightRequest=size}:Ui.Icon("gem",Ui.Gold,size-16);
return Ui.Card(graphic,10,new Thickness(0),Ui.Cream);
}
public static View SystemMark(string name,string icon="shield")=>Ui.Row(SystemImage(name,80),Ui.Stack(Ui.Text(name,13,Ui.Bronze,true),Ui.Text("系统标识图,无需上传照片",11,Ui.Muted)));
private static Button CardAction(string label,Func<Task> action,bool primary=false){var b=Ui.Button(label,action,primary);b.HeightRequest=28;b.FontSize=11;b.Padding=new Thickness(10,0);b.CornerRadius=8;return b;}
private static Label OneLine(string value,double size=13,Color? color=null,bool bold=false){var label=Ui.Text(value,size,color,bold);label.MaxLines=1;label.LineBreakMode=LineBreakMode.TailTruncation;return label;}
private static View CompactIdentity(CommunityPost p)=>Ui.Pill((p.IdentityKind=="ENTERPRISE"&&p.Verified?"企业":"个人")+" · 信用"+CommunityCatalog.CreditLevels.GetValueOrDefault(p.CreditLevel,"良好"),p.CreditLevel is "RISK" or "HIGH_RISK"?Ui.Red:p.IdentityKind=="ENTERPRISE"&&p.Verified?Ui.Bronze:Color.FromArgb("#2474B5"));
public static View PostMedia(CommunityPost post,ContentPage page,bool detail=false)
{
var body=Ui.Stack(Photos(post.Media,page,detail));
foreach(var media in (post.Media??[]).Where(m=>m.Kind=="VIDEO").Take(detail?int.MaxValue:1))body.Add(new CommunityVideo(media,page,detail,()=>page.Navigation.PushAsync(new PostDetailPage(post.Id))));
if(!detail&&(post.Media?.Count??0)>3)body.Add(Ui.Text("共 "+post.Media!.Count+" 项图片 / 视频,进入详情查看全部",11,Ui.Muted));return body;
}
public static View PostCard(CommunityPost post,ContentPage page)
{
if(post.Supply is not null)return CompactSupplyCard(post,page);
if(post.Service is not null)return CompactServiceCard(post,page);
var name=post.AuthorRemark.Length>0?post.AuthorRemark+"("+post.AuthorName+")":post.AuthorName;var body=Ui.Stack(Ui.Spread(Person(name,Time(post.RefreshedAt)),CommunitySafetyUi.Badge(post.IdentityKind,post.Verified,post.CreditLevel)),Ui.Text(post.Title,18,bold:true));
if(post.ModerationStatus!="APPROVED")body.Add(Ui.Pill(CommunitySafetyUi.Status(post.ModerationStatus),Ui.Red));
if(post.Service is { } service)body.Add(ServiceSummary(service));
var preview=Ui.Text(Short(post.Content),13,Ui.Muted);preview.MaxLines=3;body.Add(preview);if(post.Kind=="RECOMMEND")body.Add(Ui.Pill(post.Pinned?"置顶 · 官方推荐":"官方推荐"));
if(CommunityCatalog.DefaultIcon(null,post.Service))body.Add(SystemMark(ServiceCatalog.Categories.GetValueOrDefault(post.Service!.Category,"鉴定评估服务")));else body.Add(PostMedia(post,page));body.Add(Ui.Text("📍 "+(post.City.Length>0?post.City:"历史发布未填写城市")+" · "+post.Address,12,Ui.Muted));body.Add(Ui.Spread(Ui.Button("查看内容 · "+post.CommentCount+" 评论",()=>page.Navigation.PushAsync(new PostDetailPage(post.Id)),false),Ui.Text(post.LikeCount+" 赞",12,Ui.Bronze)));
if((post.Supply is not null||post.Service is not null)&&post.AuthorId!=AuthClient.Current.AccountId)body.Add(Ui.Button("联系他",()=>CommunitySafetyUi.OpenChat(page,new(post.AuthorId,"",post.AuthorName,post.Following,post.AuthorRemark,post.IdentityKind,post.Verified,post.CreditLevel))));
if((post.Supply is not null||post.Service is not null)&&post.AuthorId==AuthClient.Current.AccountId)body.Add(Ui.Button("一键刷新卡片",async()=>{await AuthClient.Current.CommunityRequest<CommunityPost>($"/posts/{post.Id}/refresh",new{});await page.DisplayAlertAsync("已刷新","原卡片已更新展示位置,无需重复发布。","完成");},false));return Ui.Card(body);
}
private static View CompactSupplyCard(CommunityPost post,ContentPage page)
{
var supply=post.Supply!;var f=supply.Fields;var name=post.AuthorRemark.Length>0?post.AuthorRemark:post.AuthorName;
var body=Ui.Stack(Ui.Spread(OneLine(post.Title,15,bold:true),CompactIdentity(post)));body.Spacing=5;
var labels=supply.Intent=="OTHER"?"其他图文":CommunityCatalog.Name(CommunityCatalog.Intents,supply.Intent)+" · "+CommunityCatalog.Name(CommunityCatalog.Categories,supply.Category)+(supply.Subcategory.Length>0?" · "+CommunityCatalog.Name(CommunityCatalog.Subcategories(supply.Category),supply.Subcategory):"");
body.Add(OneLine(name+" · "+labels,10,Ui.Bronze));
var content=Ui.Stack();content.Spacing=4;
if(post.Content.Trim()!=post.Title.Trim()){var preview=Ui.Text(CommunityText.Preview(post.Content,70),12,Ui.Muted);preview.MaxLines=2;preview.LineBreakMode=LineBreakMode.TailTruncation;content.Add(preview);}
if(supply.Intent is not ("SOURCING" or "OTHER"))content.Add(Ui.Text(CommunityCatalog.PriceLabel(supply),15,Ui.Red,true));
var facts=new List<string>();
if(supply.Intent=="SOURCING"&&supply.Category=="METAL_JEWELRY")facts.Add(f.GetValueOrDefault("supportsExchange")=="YES"?"黄料兑换 "+f.GetValueOrDefault("exchangeRatio",""):"不支持黄料兑换");
else if(supply.Intent!="OTHER"&&supply.Category=="BULLION")
{
var source=f.GetValueOrDefault("source","");facts.Add(source=="CUSTOM"?f.GetValueOrDefault("sourceCustom",""):CommunityCatalog.Name(CommunityCatalog.Fields(supply.Category,supply.Intent).Single(d=>d.Key=="source").Choices!,source));
if(f.GetValueOrDefault("purity","").Length>0)facts.Add(f["purity"]);
if(f.GetValueOrDefault("weight","").Length>0)facts.Add(f["weight"]+" 克");
if(f.GetValueOrDefault("minimumWeight","").Length>0)facts.Add(f["minimumWeight"]+" 克起拿");
}
else if(supply.Intent!="OTHER")foreach(var key in new[]{"brand","model","condition"})if(f.TryGetValue(key,out var value)&&value.Length>0){var def=CommunityCatalog.Fields(supply.Category,supply.Intent).FirstOrDefault(d=>d.Key==key);facts.Add(def?.Choices is null?value:CommunityCatalog.Name(def.Choices,value));}
if(facts.Count>0)content.Add(OneLine(string.Join(" · ",facts),11,Ui.Muted));
View? graphic=CommunityCatalog.DefaultIcon(supply)?SystemImage("配货代购",56):post.Media?.FirstOrDefault(m=>m.Kind=="IMAGE") is { } media?Ui.Tap(new ProtectedMediaImage(media,56){WidthRequest=56},()=>page.Navigation.PushAsync(new CommunityMediaPage(media)),"查看发布图片"):null;
if(graphic is not null){var grid=new Grid{ColumnDefinitions=[new(new GridLength(56)),new(GridLength.Star)],ColumnSpacing=9};grid.Add(graphic);grid.Add(content,1);body.Add(grid);}else body.Add(content);
if(post.ModerationStatus!="APPROVED"||post.ShelfStatus!="ACTIVE")body.Add(Ui.Text(post.ShelfStatus!="ACTIVE"?"已下架":CommunitySafetyUi.Status(post.ModerationStatus),10,Ui.Red));
var actions=Ui.Row(CardAction("详情",()=>page.Navigation.PushAsync(new PostDetailPage(post.Id))));actions.Spacing=6;
if(post.AuthorId!=AuthClient.Current.AccountId)actions.Add(CardAction("私信",()=>CommunitySafetyUi.OpenChat(page,new(post.AuthorId,"",post.AuthorName,post.Following,post.AuthorRemark,post.IdentityKind,post.Verified,post.CreditLevel)),true));
else if(post.ShelfStatus=="ACTIVE"&&post.ModerationStatus=="APPROVED")actions.Add(CardAction("刷新卡片",async()=>{await AuthClient.Current.CommunityRequest<CommunityPost>($"/posts/{post.Id}/refresh",new{});if(page is OnlinePage online)await online.Reload();}));
body.Add(Ui.Spread(OneLine("📍 "+post.City+" · "+Time(post.RefreshedAt),10,Ui.Muted),actions));return Ui.Card(body,14,new Thickness(10,8));
}
private static View CompactServiceCard(CommunityPost post,ContentPage page)
{
var service=post.Service!;var name=ServiceCatalog.Categories.GetValueOrDefault(service.Category,"其他服务");
var body=Ui.Stack(Ui.Spread(OneLine(post.AuthorRemark.Length>0?post.AuthorRemark:post.AuthorName,bold:true),CompactIdentity(post)));body.Spacing=6;
var info=Ui.Stack(OneLine(service.Project,15,bold:true),OneLine((service.FeeMin==service.FeeMax?service.FeeMin.ToString("0.##"):service.FeeMin.ToString("0.##")+"–"+service.FeeMax.ToString("0.##"))+" 元 / "+service.Unit,15,Ui.Bronze,true),Ui.Text("兜底回购:"+(service.Buyback?"是":"否"),10,Ui.Muted));info.Spacing=4;
var description=Ui.Text(CommunityText.Preview(service.Description,60),12,Ui.Muted);description.MaxLines=2;description.LineBreakMode=LineBreakMode.TailTruncation;info.Add(description);
View? image=CommunityCatalog.DefaultIcon(null,service)?SystemImage(name):post.Media?.FirstOrDefault(m=>m.Kind=="IMAGE") is { } m?new ProtectedMediaImage(m,64){WidthRequest=64}:null;
if(image is null)body.Add(info);else{var row=new Grid{ColumnDefinitions=[new(new GridLength(64)),new(GridLength.Star)],ColumnSpacing=9};row.Add(image);row.Add(info,1);body.Add(row);}
if(post.ModerationStatus!="APPROVED")body.Add(Ui.Text(CommunitySafetyUi.Status(post.ModerationStatus),10,Ui.Red));
var actions=Ui.Row(CardAction("详情",()=>page.Navigation.PushAsync(new PostDetailPage(post.Id))));if(post.AuthorId!=AuthClient.Current.AccountId)actions.Add(CardAction("私信",()=>CommunitySafetyUi.OpenChat(page,new(post.AuthorId,"",post.AuthorName,post.Following,post.AuthorRemark,post.IdentityKind,post.Verified,post.CreditLevel)),true));
body.Add(Ui.Spread(OneLine("📍 "+post.City+" · "+name,10,Ui.Muted),actions));return Ui.Card(body,14,new Thickness(12,10));
}
public static View TradeNotice(string context,string category="",bool compact=false)
{
var message=context switch
{
"CERTIFICATE"=>"证照仅用于本次交易核验,请核对水印日期与资料一致性,勿转发或挪作他用。认证标识及证照互换不能代替实物核验。",
"CHAT"=>"请核对交易对象和实物,谨慎处理转账、陌生链接及索取验证码的消息。发现异常可通过‘举报用户’提交图片与说明。",
"PUBLISH"=>"请如实填写商品、价格与交割信息。发布照片中请遮挡身份证、银行卡等私密资料,证照通过双方同意的互换功能核验。",
"DELIVERY"=>"优先面交查验,或选择有保障的交割渠道;发货前确认收件信息、费用和验收方式。平台提供信息交流,具体交易由双方协商。",
_=>category=="BULLION"?"请核验实物克重、纯度、凭证与报价基准,确认加减价及最终金额。行情会变化,发布报价和估算价值需在交易时再次确认。":category=="SERVICE"?"确认鉴定范围、收费和回购条件,留存服务凭证;服务发布及认证标识不代表平台担保。":"请核验商品实物、品相、配件与相关凭证,确认交易对象及最终价格。发布信息和认证标识不代表平台担保。"
};
var text=Ui.Text(message,12,Ui.Bronze);if(compact){text.MaxLines=2;text.LineBreakMode=LineBreakMode.TailTruncation;}
var body=Ui.Stack(Ui.Row(Ui.Icon("shield",Ui.Bronze,16),Ui.Text("温馨提示",13,Ui.Bronze,true)),text);body.Spacing=6;var card=Ui.Card(body,14,new Thickness(12),Ui.Cream);
return compact?Ui.Tap(card,()=>Application.Current!.Windows[0].Page!.DisplayAlertAsync("温馨提示",message,"知道了"),"温馨提示"):card;
}
public static View ServiceSummary(ServiceCard service,bool detail=false)=>Ui.Stack(Ui.Pill(ServiceCatalog.Categories.GetValueOrDefault(service.Category,"其他服务")),Ui.Text(service.Project,17,bold:true),Ui.Text((service.FeeMin==service.FeeMax?service.FeeMin.ToString("0.##"):service.FeeMin.ToString("0.##")+"–"+service.FeeMax.ToString("0.##"))+" 元 / "+service.Unit,20,Ui.Bronze,true),Ui.Text("提供兜底回购:"+(service.Buyback?"是":"否"),12,Ui.Muted),Ui.Text(detail?service.Description:CommunityText.Preview(service.Description),13));
public static View ConversationCard(Conversation row,ContentPage page)
{
var peer=new CommunityAccount(row.PeerId,row.PeerUsername,row.PeerName,row.Following,row.Remark,row.IdentityKind,row.Verified,row.CreditLevel);var body=Ui.Stack(Ui.Spread(Person(Name(peer),"@"+row.PeerUsername),Ui.Text(Time(row.UpdatedAt),10,Ui.Muted)),Ui.Text(Short(row.LastContent,70),12,Ui.Muted),CommunitySafetyUi.Badge(row.IdentityKind,row.Verified,row.CreditLevel));if(row.UnreadCount>0)body.Add(Ui.Pill(row.UnreadCount+" 条未读",Ui.Red));return Ui.Tap(Ui.Card(body),()=>page.Navigation.PushAsync(new ChatPage(peer)),"私聊 "+Name(peer));
}
public static async Task<CommunityAccount> Follow(ContentPage page,CommunityAccount peer,bool remark=false)
{
string? note=null;if(remark){note=await page.DisplayPromptAsync("设置备注","备注仅自己可见,无需添加好友。","保存","取消",initialValue:peer.Remark,maxLength:60);if(note is null)return peer;}return await AuthClient.Current.CommunityRequest<CommunityAccount>("/follows/"+peer.Id,new FollowRequest(remark||!peer.Following,note));
}
}
public sealed class PostsPage : OnlinePage
{
private readonly string tab;
private readonly VerticalStackLayout list=Ui.Stack();
private readonly Button more;
private long? next;
public PostsPage(string tab="SQUARE")
{
this.tab=tab;Title=tab=="MINE"?"我的发布":tab=="RECOMMEND"?"官方推荐":"帖子广场";more=Ui.Button("加载更多帖子",More,false);more.IsVisible=false;var top=Ui.Stack(Ui.Section(Title),tab=="MINE"?Ui.Spread(Ui.Button("刷新全部",RefreshAll,false),Ui.Button("更新列表",Reload,false)):Ui.Button("刷新",Reload,false),State);if(tab!="MINE"&&(tab!="RECOMMEND"||Auth.IsAdmin))top.Add(Ui.Button("+ 发布图文",()=>Navigation.PushAsync(new PublishPostPage(tab))));list.Spacing=8;Content=CommunityUi.Layout(top,new ScrollView{Content=Ui.Stack(list,more)});
}
private async Task RefreshAll(){var result=await Auth.CommunityRequest<RefreshCardsResult>("/posts/refresh-all",new{});await Reload();await DisplayAlertAsync("刷新全部",$"已刷新 {result.Refreshed} 张供需 / 服务卡片。"+(result.CoolingDown>0?$"\n{result.CoolingDown} 张卡片刚刷新过,请一分钟后再试。":"")+(result.Unavailable>0?$"\n{result.Unavailable} 张未展示或已下架卡片保持原状态。":""),"完成");}
protected override async Task LoadData(){var data=await Auth.CommunityRequest<PostPage>(CommunityUi.FeedPath(new(tab)));if(!CanUpdate)return;list.Clear();Append(data);if(data.Items.Count==0)list.Add(Ui.Empty("还没有发布内容","填写城市和地址后发布图文或供需卡片"));}
private void Append(PostPage page){foreach(var post in page.Items)list.Add(CommunityUi.PostCard(post,this));next=page.NextBefore;more.IsVisible=next.HasValue;}
private async Task More(){if(next is not { } cursor)return;var data=await Auth.CommunityRequest<PostPage>(CommunityUi.FeedPath(new(tab,cursor)));if(CanUpdate)Append(data);}
}
public sealed class PublishPostPage : ContentPage
{
private readonly string account=AuthClient.Current.AccountId,kind,draftFile;
private readonly Entry title=new(){Placeholder="输入帖子标题 / 物品简介",MaxLength=120},city=new(){Placeholder="发布城市",MaxLength=60},address=new(){Placeholder="发布地址 / 发货地",MaxLength=200};
private readonly Editor text=new(){Placeholder="分享经营心得、物品信息或交流话题…",MaxLength=5000,HeightRequest=160};
private readonly Picker intent=new(){Title="发布意向"},category=new(){Title="物品大类"},sub=new(){Title="物品细类"};
private readonly VerticalStackLayout details=Ui.Stack(),classificationBody=Ui.Stack();
private Border? deliveryCard;
private readonly CommunityPhotos photos;
private readonly RecommendationAttachments? recommended;
private readonly VerticalStackLayout photoBody=Ui.Stack();
private Dictionary<string,string> availableCategories=new();
private CancellationTokenSource? publishing;
private readonly Dictionary<string,CheckBox> delivery=[];
private readonly Dictionary<string,DbRow> snapshots=[];
private FieldSet? fields;
private string activeCategory="",activeIntent="",nonce=Guid.NewGuid().ToString("N"),attempt="";
private bool published,building,visible;
private readonly CommunityPost? editing;
private readonly PostingLocationField publishingLocation;
private sealed record Draft(string Title,string Content,string City,string Address,string Intent,string Category,string Subcategory,Dictionary<string,DbRow> Fields,List<string> Delivery,List<CommunityPhotos.Photo> Photos,List<RecommendationAttachments.Attachment>? Recommended=null);
public PublishPostPage(string kind="SQUARE",CommunityPost? editing=null)
{
this.kind=kind;this.editing=editing;publishingLocation=new(editing);if(kind=="RECOMMEND"&&!AuthClient.Current.IsAdmin)throw new InvalidOperationException("推荐内容由管理员发布");Title=editing is null?kind=="SUPPLY"?"发布供需卡片":kind=="RECOMMEND"?"发布官方推荐":"发布图文":"编辑发布";
draftFile=Path.Combine(FileSystem.AppDataDirectory,"social-draft-"+account+"-"+kind+".json");Draft? draft=null;try{if(editing is null&&File.Exists(draftFile))draft=JsonSerializer.Deserialize<Draft>(File.ReadAllText(draftFile));}catch(Exception ex)when(ex is IOException or JsonException){}
title.Text=editing?.Title??draft?.Title;text.Text=editing?.Content??draft?.Content;city.Text=editing?.City??draft?.City??CommunityLocation.City;address.Text=editing?.Address??draft?.Address??CommunityLocation.Address;
photos=new(9,editing?.Media?.Where(m=>m.Kind=="IMAGE").ToList(),draft?.Photos);if(kind=="RECOMMEND"){text.MaxLength=int.MaxValue;recommended=new(editing?.Media,draft?.Recommended);}
var count=Ui.Text("简介正文最多 200 个字符",11,Ui.Muted);count.IsVisible=kind=="SUPPLY";if(kind=="SUPPLY"){text.MaxLength=int.MaxValue;text.Placeholder="填写物品简介,最多 200 个字符";text.TextChanged+=(_,_)=>{var clipped=CommunityText.TrimTo(text.Text??"",200);if(clipped!=text.Text)text.Text=clipped;count.Text=$"简介正文 {CommunityText.Length(clipped)} / 200 个字符";};}
var body=Ui.Stack(Ui.Card(Ui.Stack(Ui.Section(kind=="SUPPLY"?"物品简介":kind=="RECOMMEND"?"推荐内容":"图文内容"),title,text,count)),publishingLocation.View);
if(kind=="SUPPLY")
{
intent.ItemsSource=CommunityCatalog.Intents.Values.ToList();if(draft?.Fields is { } saved)foreach(var p in saved)snapshots[p.Key]=new(p.Value);if(editing?.Supply is { } existing)snapshots[existing.Category+"/"+existing.Intent]=new(existing.Fields.ToDictionary(p=>p.Key,p=>(object?)p.Value));
intent.SelectedIndex=Math.Max(0,CommunityCatalog.Intents.Keys.ToList().IndexOf(editing?.Supply?.Intent??draft?.Intent??"SALE"));SetCategories(editing?.Supply?.Category??draft?.Category);classificationBody.Add(Ui.Text("物品大类",12,Ui.Muted));classificationBody.Add(category);classificationBody.Add(sub);classificationBody.Add(details);body.Add(Ui.Card(Ui.Stack(Ui.Section("供需卡片"),Ui.Text("发布意向",12,Ui.Muted),intent,classificationBody)));BuildFields(editing?.Supply?.Subcategory??draft?.Subcategory);category.SelectedIndexChanged+=(_,_)=>BuildFields();intent.SelectedIndexChanged+=(_,_)=>{SetCategories(Category());BuildFields();};
var modes=Ui.Stack(Ui.Text("交割方式",12,Ui.Muted));foreach(var d in CommunityCatalog.Deliveries){var box=new CheckBox{IsChecked=(editing?.Supply?.Delivery??draft?.Delivery??["COD"]).Contains(d.Key),Color=Ui.Bronze};delivery[d.Key]=box;string key=d.Key;box.CheckedChanged+=(_,e)=>{if(e.Value&&key is "COD" or "FREE")delivery[key=="COD"?"FREE":"COD"].IsChecked=false;};modes.Add(Ui.Row(box,Ui.Text(d.Value)));}deliveryCard=Ui.Card(modes);deliveryCard.IsVisible=Intent()!="OTHER";body.Add(deliveryCard);
}
if(kind=="RECOMMEND")photoBody.Add(Ui.Stack(Ui.Section("推荐图片与视频"),Ui.Text("图片、视频数量及视频时长不设发布限制。视频支持 MP4 / WebM;列表静音自动播放,进入详情后有声音。",11,Ui.Muted),recommended!.View));
else if(kind!="SUPPLY")photoBody.Add(Ui.Stack(Ui.Section("实物照片"),Ui.Text("最多 9 张照片",11,Ui.Muted),photos.View));
body.Add(Ui.Card(photoBody));body.Add(CommunityUi.TradeNotice("PUBLISH"));body.Add(Ui.Text("未发布的内容会保存为本账户在本机的草稿。",11,Ui.Muted));body.Add(Ui.Button(editing is null?"确认发布":"保存修改",Publish));body.Padding=18;Content=new ScrollView{Content=body};
}
private void SetCategories(string? previous=null){building=true;try{availableCategories=CommunityCatalog.CategoriesFor(Intent());category.ItemsSource=availableCategories.Values.ToList();category.SelectedIndex=Math.Max(0,availableCategories.Keys.ToList().IndexOf(previous??""));}finally{building=false;}}
private string Category()=>availableCategories.Keys.ElementAt(Math.Max(0,category.SelectedIndex));
private string Intent()=>CommunityCatalog.Intents.Keys.ElementAt(Math.Max(0,intent.SelectedIndex));
private void BuildFields(string? selectedSub=null)
{
if(building)return;building=true;try
{
if(fields is not null)snapshots[activeCategory+"/"+activeIntent]=fields.Read(false);activeCategory=Category();activeIntent=Intent();classificationBody.IsVisible=activeIntent!="OTHER";if(deliveryCard is not null)deliveryCard.IsVisible=activeIntent!="OTHER";var values=CommunityCatalog.Subcategories(activeCategory,activeIntent);sub.ItemsSource=values.Values.ToList();sub.SelectedIndex=values.Count==0?-1:Math.Max(0,values.Keys.ToList().IndexOf(selectedSub??""));sub.IsVisible=values.Count>0;
var row=snapshots.GetValueOrDefault(activeCategory+"/"+activeIntent)??new DbRow{["source"]="NONE",["invoice"]="NO",["priceMode"]="FIXED",["supportsExchange"]="YES",["shippingFrom"]=""};fields=new(row,CommunityCatalog.Fields(activeCategory,activeIntent).Select(d=>new Field(d.Key,d.Label,d.Kind switch{"number"=>FieldKind.Number,"choice"=>FieldKind.Choice,"date"=>FieldKind.Date,_=>FieldKind.Text},d.Required,d.Choices,d.Hint)));details.Clear();details.Add(fields.View);photos.Limit=CommunityCatalog.PhotoLimit(activeCategory,activeIntent);
photoBody.Clear();if(photos.Limit==0)photoBody.Add(CommunityUi.SystemMark("配货代购 · 贵金属首饰","gold"));else{if(photos.View.Parent is Layout old)old.Children.Remove(photos.View);photoBody.Add(Ui.Section("实物照片"));photoBody.Add(Ui.Text($"选填,最多 {photos.Limit} 张照片",11,Ui.Muted));photoBody.Add(photos.View);}
void Visibility(){var current=fields.Read(false);fields.SetVisible("sourceCustom",current.Text("source")=="CUSTOM");fields.SetVisible("exchangeRatio",current.Text("supportsExchange")=="YES");}Visibility();fields.Changed+=Visibility;
}
finally{building=false;}
}
private SupplyCard? Card(bool validate)
{
if(kind!="SUPPLY")return null;var row=fields!.Read(validate);var allowed=CommunityCatalog.Fields(Category(),Intent()).Select(d=>d.Key).ToHashSet();var values=row.Where(p=>allowed.Contains(p.Key)&&p.Value is not null&&!string.IsNullOrEmpty(Convert.ToString(p.Value,CultureInfo.InvariantCulture))).ToDictionary(p=>p.Key,p=>Convert.ToString(p.Value,CultureInfo.InvariantCulture)??"");if(values.GetValueOrDefault("source")=="CUSTOM"&&values.GetValueOrDefault("sourceCustom","").Length==0&&validate)throw new ArgumentException("请填写自定义源头");if(validate&&values.GetValueOrDefault("supportsExchange")=="YES"&&values.GetValueOrDefault("exchangeRatio","").Length==0)throw new ArgumentException("支持黄料兑换时请填写兑换比例,如 1:0.998");return new(Intent(),Category(),sub.SelectedIndex>=0?CommunityCatalog.Subcategories(Category(),Intent()).Keys.ElementAt(sub.SelectedIndex):"",values,Intent()=="OTHER"?[]:delivery.Where(p=>p.Value.IsChecked).Select(p=>p.Key).ToList());
}
private async Task Publish()
{
if(account!=AuthClient.Current.AccountId)throw new InvalidOperationException("账户已切换,请重新进入发布页");if(!await CommunitySafetyUi.Write(this))return;var gps=await publishingLocation.Get();string heading=title.Text?.Trim()??"",content=text.Text?.Trim()??"",place=gps?.City??editing?.City??"",location="";if(heading.Length==0)throw new ArgumentException("请填写标题 / 简介");if(kind=="SUPPLY"&&content.Length==0)content=heading;if(content.Length==0)throw new ArgumentException("请填写正文");var supply=Card(true);IsEnabled=false;
try{if(kind=="SUPPLY"&&CommunityText.Length(content)>200)throw new ArgumentException("供需简介正文最多 200 个字符");if(kind=="SUPPLY"&&editing is null){var quota=await AuthClient.Current.CommunityRequest<SupplyQuota>("/supply/quota");if(quota.Active>=quota.Limit)throw new ArgumentException($"服务与供需合计已达上限({quota.Active}/{quota.Limit}),请先下架一张");}publishing=new();var ids=recommended is not null?await recommended.Upload(publishing.Token):photos.Limit==0?[]:await photos.Upload();if(!visible||account!=AuthClient.Current.AccountId)return;var current=JsonSerializer.Serialize(new{heading,content,place,location,ids,supply});if(attempt!=current){attempt=current;nonce=Guid.NewGuid().ToString("N");}var request=new PublishPostRequest(heading,content,nonce,kind,place,location,ids,supply,Location:gps);if(kind=="RECOMMEND"){using var result=await AuthClient.Current.AdminRequest(editing is null?"/community/recommendations":$"/community/recommendations/{editing.Id}/edit",request);}else await AuthClient.Current.CommunityRequest<CommunityPost>(editing is null?"/posts":$"/posts/{editing.Id}/edit",request);published=true;if(editing is null&&File.Exists(draftFile))File.Delete(draftFile);CommunityLocation.City=place;CommunityLocation.Address=location;if(visible&&Navigation.NavigationStack.LastOrDefault()==this)await Navigation.PopAsync();}finally{publishing?.Dispose();publishing=null;IsEnabled=true;}
}
protected override async void OnAppearing(){base.OnAppearing();visible=true;try{if(await CommunitySafetyUi.Write(this))await publishingLocation.Start();}catch(Exception ex){await DisplayAlertAsync("需要认证",ex.Message,"知道了");}}
protected override void OnDisappearing()
{
visible=false;publishing?.Cancel();if(!published&&editing is null&&!AccountDataClear.InProgress&&account==AuthClient.Current.AccountId){if(fields is not null)snapshots[activeCategory+"/"+activeIntent]=fields.Read(false);var card=Card(false);try{File.WriteAllText(draftFile,JsonSerializer.Serialize(new Draft(title.Text??"",text.Text??"",city.Text??"",address.Text??"",card?.Intent??"",card?.Category??"",card?.Subcategory??"",snapshots,card?.Delivery??[],photos.Draft(),recommended?.Draft())));}catch(IOException){}}base.OnDisappearing();
}
}
public sealed class PostDetailPage : OnlinePage
{
private readonly long postId;
private CommunityPost? post;
private readonly VerticalStackLayout header=Ui.Stack(),comments=Ui.Stack();
private readonly Entry reply=new(){Placeholder="写下评论",MaxLength=1000};
private readonly View commentHeading,replyBar;
private readonly Button more;
private long? next;
private string nonce=Guid.NewGuid().ToString("N"),attempt="";
public PostDetailPage(long id)
{
postId=id;Title="发布详情";more=Ui.Button("更多评论",More,false);more.IsVisible=false;commentHeading=Ui.Section("评论");replyBar=Ui.Spread(Ui.Card(reply,12,new Thickness(12,0)),Ui.Button("评论",Reply));replyBar.IsVisible=commentHeading.IsVisible=false;Content=CommunityUi.Layout(Ui.Stack(State,Ui.Button("刷新内容",Reload,false)),new ScrollView{Content=Ui.Stack(header,commentHeading,comments,more)},replyBar);
}
protected override async Task LoadData(){var latest=await Auth.CommunityRequest<CommunityPost>("/posts/"+postId);var rows=latest.Kind=="SUPPLY"?null:await Auth.CommunityRequest<CommentPage>($"/posts/{postId}/comments");if(!CanUpdate)return;CommunityVideo.StopAll();post=latest;RenderHeader();comments.Clear();replyBar.IsVisible=commentHeading.IsVisible=rows is not null;more.IsVisible=false;next=null;if(rows is not null){Append(rows);if(rows.Items.Count==0)comments.Add(Ui.Text("暂无评论,来交流第一条心得吧。",12,Ui.Muted));}}
private void RenderHeader()
{
if(post is null)return;header.Clear();var body=Ui.Stack(CommunityUi.Person(post.AuthorName,CommunityUi.Time(post.CreatedAt)),Ui.Text(post.Title,23,bold:true),Ui.Text("📍 "+post.City+(post.PublishArea.Length>0?" · "+post.PublishArea:""),12,Ui.Muted),Ui.Divider(),Ui.Text(post.Content,15));if(CommunityCatalog.DefaultIcon(post.Supply,post.Service))body.Add(CommunityUi.SystemMark(post.Supply is not null?"配货代购 · 贵金属首饰":ServiceCatalog.Categories.GetValueOrDefault(post.Service!.Category,"鉴定评估服务"),post.Supply is not null?"gold":"shield"));else body.Add(CommunityUi.PostMedia(post,this,true));
if(post.Supply is { Intent:"OTHER" })body.Add(Ui.Pill("其他图文"));
else if(post.Supply is { } s){body.Add(Ui.Section("供需卡片"));body.Add(Ui.Text(CommunityCatalog.Name(CommunityCatalog.Intents,s.Intent)+" · "+CommunityCatalog.Name(CommunityCatalog.Categories,s.Category)+" · "+CommunityCatalog.Name(CommunityCatalog.Subcategories(s.Category),s.Subcategory),14,Ui.Bronze,true));foreach(var d in CommunityCatalog.Fields(s.Category,s.Intent))if(s.Fields.TryGetValue(d.Key,out var v))body.Add(Ui.Spread(Ui.Text(d.Label,12,Ui.Muted),Ui.Text(d.Choices is null?v:CommunityCatalog.Name(d.Choices,v),13)));body.Add(Ui.Text("交割:"+string.Join(" / ",s.Delivery.Select(d=>CommunityCatalog.Name(CommunityCatalog.Deliveries,d))),13));if(s.Category=="BULLION"&&s.Intent=="BUY"&&(post.Media?.Count??0)==0)body.Add(Ui.Row(Ui.Icon("gold",Ui.Gold,36),Ui.Text("贵金属求购",13,Ui.Bronze)));}
body.Add(CommunitySafetyUi.Badge(post.IdentityKind,post.Verified,post.CreditLevel));if(post.Service is { } service)body.Add(CommunityUi.ServiceSummary(service,true));if(post.ModerationStatus!="APPROVED")body.Add(Ui.Pill(CommunitySafetyUi.Status(post.ModerationStatus),Ui.Red));if(post.ShelfStatus!="ACTIVE")body.Add(Ui.Pill("已下架",Ui.Muted));
if(post.AuthorId!=Auth.AccountId){body.Add(Ui.Button("举报此用户",()=>Navigation.PushAsync(new ReportPage(post.AuthorId,post.Id)),false));var peer=new CommunityAccount(post.AuthorId,"",post.AuthorName,post.Following,post.AuthorRemark,post.IdentityKind,post.Verified,post.CreditLevel);body.Add(Ui.Spread(Ui.Button(peer.Following?"已关注 · 取消":"关注作者",async()=>{await CommunityUi.Follow(this,peer);await Reload();},false),Ui.Button("联系他",()=>CommunitySafetyUi.OpenChat(this,peer))));if(peer.Following)body.Add(Ui.Button("设置作者备注",async()=>{await CommunityUi.Follow(this,peer,true);await Reload();},false));}
if(post.Kind!="SUPPLY")body.Add(Ui.Button((post.Liked?"已赞 · ":"点赞 · ")+post.LikeCount,Like,false));if(post.AuthorId==Auth.AccountId){if(post.Kind is "SUPPLY" or "SERVICE"&&post.ShelfStatus=="ACTIVE")body.Add(Ui.Button("一键刷新卡片",async()=>{await Auth.CommunityRequest<CommunityPost>($"/posts/{postId}/refresh",new{});await Reload();},false));if(post.Kind is "SUPPLY" or "SERVICE")body.Add(Ui.Button(post.ShelfStatus=="ACTIVE"?"下架卡片":"重新上架",async()=>{await Auth.CommunityRequest<CommunityPost>($"/posts/{postId}/shelf",new ShelfRequest(post.ShelfStatus!="ACTIVE"));await Reload();},false));body.Add(Ui.Button("编辑发布",()=>Navigation.PushAsync(post.Kind=="SERVICE"?(Page)new ServicePublishPage(post):new PublishPostPage(post.Kind,post)),false));}if(Auth.IsAdmin&&post.Kind=="RECOMMEND")body.Add(Ui.Button(post.Pinned?"取消置顶":"置顶推荐",async()=>{using var result=await Auth.AdminRequest($"/community/posts/{postId}/pin",new PinRequest(!post.Pinned));await Reload();},false));if(post.CanDelete)body.Add(Ui.Button("删除发布",Delete,false));header.Add(Ui.Card(body));
if(post.Supply is not null||post.Service is not null){header.Add(CommunityUi.TradeNotice("VERIFY",post.Service is not null?"SERVICE":post.Supply!.Category));if(post.Supply?.Intent!="OTHER")header.Add(CommunityUi.TradeNotice("DELIVERY"));}
}
private void Append(CommentPage data){foreach(var row in data.Items){var body=Ui.Stack(CommunityUi.Person(row.AuthorName,CommunityUi.Time(row.CreatedAt)),Ui.Text(row.Content,14),CommunitySafetyUi.Badge(row.IdentityKind,row.Verified,row.CreditLevel));if(row.ModerationStatus!="APPROVED")body.Add(Ui.Pill(CommunitySafetyUi.Status(row.ModerationStatus)));if(row.CanDelete)body.Add(Ui.Button("删除评论",async()=>{if(!await DisplayAlertAsync("删除评论","确认删除这条评论?","删除","取消"))return;await Auth.CommunityRequest<OperationResult>($"/comments/{row.Id}/delete",new{});await Reload();},false));comments.Add(Ui.Card(body,15));}next=data.NextBefore;more.IsVisible=next.HasValue;}
private async Task More(){if(next is not { } cursor)return;var data=await Auth.CommunityRequest<CommentPage>($"/posts/{postId}/comments?before={cursor}");if(CanUpdate)Append(data);}
private async Task Like(){if(post is null||!await CommunitySafetyUi.Write(this))return;var updated=await Auth.CommunityRequest<CommunityPost>($"/posts/{postId}/like",new LikeRequest(!post.Liked));if(CanUpdate){post=updated;RenderHeader();}}
private async Task Delete(){if(!await DisplayAlertAsync("删除发布","确认删除此内容?相关评论将不再展示。","删除","取消"))return;await Auth.CommunityRequest<OperationResult>($"/posts/{postId}/delete",new{});await Navigation.PopAsync();}
private async Task Reply(){if(!await CommunitySafetyUi.Write(this))return;var content=reply.Text?.Trim()??"";if(content.Length==0)throw new ArgumentException("请填写评论");if(attempt!=content){attempt=content;nonce=Guid.NewGuid().ToString("N");}await Auth.CommunityRequest<CommunityComment>($"/posts/{postId}/comments",new PublishCommentRequest(content,nonce));if(reply.Text?.Trim()==content)reply.Text="";attempt="";await Reload();}
protected override void OnDisappearing(){CommunityVideo.StopAll();base.OnDisappearing();}
}
using System.Globalization;
using System.Text.Json;
using JiDangJia.Contracts;
using JiDangJia.Core;
using JiDangJia.Services;
namespace JiDangJia.Views;
internal static partial class CommunityUi
{
public static string Name(CommunityAccount peer)=>peer.Remark.Length>0?peer.Remark+"("+peer.Name+")":peer.Name;
public static string FeedPath(FeedFilter f)
{
var values=new Dictionary<string,string>{["tab"]=f.Tab,["before"]=f.Before.ToString(),["city"]=f.City,["intent"]=f.Intent,["category"]=f.Category,["subcategory"]=f.Subcategory,["brand"]=f.Brand,["condition"]=f.Condition,["query"]=f.Query,["serviceCategory"]=f.ServiceCategory};
if(f.MinPrice.HasValue)values["minPrice"]=f.MinPrice.Value.ToString(CultureInfo.InvariantCulture);if(f.MaxPrice.HasValue)values["maxPrice"]=f.MaxPrice.Value.ToString(CultureInfo.InvariantCulture);return "/feed?"+string.Join("&",values.Where(p=>p.Value.Length>0).Select(p=>p.Key+"="+Uri.EscapeDataString(p.Value)));
}
public static View Photos(List<CommunityMedia>? media,ContentPage page,bool all=false)
{
var strip=new HorizontalStackLayout{Spacing=8};foreach(var m in (media??[]).Where(m=>m.Kind=="IMAGE").Take(all?int.MaxValue:3))strip.Add(Ui.Tap(new ProtectedMediaImage(m,130){WidthRequest=130},()=>page.Navigation.PushAsync(new CommunityMediaPage(m)),"查看发布图片"));
return new ScrollView{Content=strip,Orientation=ScrollOrientation.Horizontal,HorizontalScrollBarVisibility=ScrollBarVisibility.Never,IsVisible=strip.Count>0};
}
public static View SystemImage(string name,int size=64)
{
var asset=name.Contains("配货")?"service_sourcing.png":name.Contains("车辆")?"service_car.png":name.Contains("奢侈品")?"service_luxury.png":name.Contains("烟酒")?"service_tobacco.png":"";
View graphic=asset.Length>0?new Image{Source=asset,Aspect=Aspect.AspectFit,WidthRequest=size,HeightRequest=size}:Ui.Icon("gem",Ui.Gold,size-16);
return Ui.Card(graphic,10,new Thickness(0),Ui.Cream);
}
public static View SystemMark(string name,string icon="shield")=>Ui.Row(SystemImage(name,80),Ui.Stack(Ui.Text(name,13,Ui.Bronze,true),Ui.Text("系统标识图,无需上传照片",11,Ui.Muted)));
private static Button CardAction(string label,Func<Task> action,bool primary=false){var b=Ui.Button(label,action,primary);b.HeightRequest=28;b.FontSize=11;b.Padding=new Thickness(10,0);b.CornerRadius=8;return b;}
private static Label OneLine(string value,double size=13,Color? color=null,bool bold=false){var label=Ui.Text(value,size,color,bold);label.MaxLines=1;label.LineBreakMode=LineBreakMode.TailTruncation;return label;}
private static View CompactIdentity(CommunityPost p)=>Ui.Pill((p.IdentityKind=="ENTERPRISE"&&p.Verified?"企业":"个人")+" · 信用"+CommunityCatalog.CreditLevels.GetValueOrDefault(p.CreditLevel,"良好"),p.CreditLevel is "RISK" or "HIGH_RISK"?Ui.Red:p.IdentityKind=="ENTERPRISE"&&p.Verified?Ui.Bronze:Color.FromArgb("#2474B5"));
public static View PostMedia(CommunityPost post,ContentPage page,bool detail=false)
{
var body=Ui.Stack(Photos(post.Media,page,detail));
foreach(var media in (post.Media??[]).Where(m=>m.Kind=="VIDEO").Take(detail?int.MaxValue:1))body.Add(new CommunityVideo(media,page,detail,()=>page.Navigation.PushAsync(new PostDetailPage(post.Id))));
if(!detail&&(post.Media?.Count??0)>3)body.Add(Ui.Text("共 "+post.Media!.Count+" 项图片 / 视频,进入详情查看全部",11,Ui.Muted));return body;
}
public static View PostCard(CommunityPost post,ContentPage page)
{
if(post.Supply is not null)return CompactSupplyCard(post,page);
if(post.Service is not null)return CompactServiceCard(post,page);
var name=post.AuthorRemark.Length>0?post.AuthorRemark+"("+post.AuthorName+")":post.AuthorName;var body=Ui.Stack(Ui.Spread(Person(name,Time(post.RefreshedAt)),CommunitySafetyUi.Badge(post.IdentityKind,post.Verified,post.CreditLevel)),Ui.Text(post.Title,18,bold:true));
if(post.ModerationStatus!="APPROVED")body.Add(Ui.Pill(CommunitySafetyUi.Status(post.ModerationStatus),Ui.Red));
if(post.Service is { } service)body.Add(ServiceSummary(service));
var preview=Ui.Text(Short(post.Content),13,Ui.Muted);preview.MaxLines=3;body.Add(preview);if(post.Kind=="RECOMMEND")body.Add(Ui.Pill(post.Pinned?"置顶 · 官方推荐":"官方推荐"));
if(CommunityCatalog.DefaultIcon(null,post.Service))body.Add(SystemMark(ServiceCatalog.Categories.GetValueOrDefault(post.Service!.Category,"鉴定评估服务")));else body.Add(PostMedia(post,page));body.Add(Ui.Text("📍 "+(post.City.Length>0?post.City:"历史发布未填写城市")+" · "+post.Address,12,Ui.Muted));body.Add(Ui.Spread(Ui.Button("查看内容 · "+post.CommentCount+" 评论",()=>page.Navigation.PushAsync(new PostDetailPage(post.Id)),false),Ui.Text(post.LikeCount+" 赞",12,Ui.Bronze)));
if((post.Supply is not null||post.Service is not null)&&post.AuthorId!=AuthClient.Current.AccountId)body.Add(Ui.Button("联系他",()=>CommunitySafetyUi.OpenChat(page,new(post.AuthorId,"",post.AuthorName,post.Following,post.AuthorRemark,post.IdentityKind,post.Verified,post.CreditLevel))));
if((post.Supply is not null||post.Service is not null)&&post.AuthorId==AuthClient.Current.AccountId)body.Add(Ui.Button("一键刷新卡片",async()=>{await AuthClient.Current.CommunityRequest<CommunityPost>($"/posts/{post.Id}/refresh",new{});await page.DisplayAlertAsync("已刷新","原卡片已更新展示位置,无需重复发布。","完成");},false));return Ui.Card(body);
}
private static View CompactSupplyCard(CommunityPost post,ContentPage page)
{
var supply=post.Supply!;var f=supply.Fields;var name=post.AuthorRemark.Length>0?post.AuthorRemark:post.AuthorName;
var body=Ui.Stack(Ui.Spread(OneLine(post.Title,15,bold:true),CompactIdentity(post)));body.Spacing=5;
var labels=supply.Intent=="OTHER"?"其他图文":CommunityCatalog.Name(CommunityCatalog.Intents,supply.Intent)+" · "+CommunityCatalog.Name(CommunityCatalog.Categories,supply.Category)+(supply.Subcategory.Length>0?" · "+CommunityCatalog.Name(CommunityCatalog.Subcategories(supply.Category),supply.Subcategory):"");
body.Add(OneLine(name+" · "+labels,10,Ui.Bronze));
var content=Ui.Stack();content.Spacing=4;
if(post.Content.Trim()!=post.Title.Trim()){var preview=Ui.Text(CommunityText.Preview(post.Content,70),12,Ui.Muted);preview.MaxLines=2;preview.LineBreakMode=LineBreakMode.TailTruncation;content.Add(preview);}
if(supply.Intent is not ("SOURCING" or "OTHER"))content.Add(Ui.Text(CommunityCatalog.PriceLabel(supply),15,Ui.Red,true));
var facts=new List<string>();
if(supply.Intent=="SOURCING"&&supply.Category=="METAL_JEWELRY")facts.Add(f.GetValueOrDefault("supportsExchange")=="YES"?"黄料兑换 "+f.GetValueOrDefault("exchangeRatio",""):"不支持黄料兑换");
else if(supply.Intent!="OTHER"&&supply.Category=="BULLION")
{
var source=f.GetValueOrDefault("source","");facts.Add(source=="CUSTOM"?f.GetValueOrDefault("sourceCustom",""):CommunityCatalog.Name(CommunityCatalog.Fields(supply.Category,supply.Intent).Single(d=>d.Key=="source").Choices!,source));
if(f.GetValueOrDefault("purity","").Length>0)facts.Add(f["purity"]);
if(f.GetValueOrDefault("weight","").Length>0)facts.Add(f["weight"]+" 克");
if(f.GetValueOrDefault("minimumWeight","").Length>0)facts.Add(f["minimumWeight"]+" 克起拿");
}
else if(supply.Intent!="OTHER")foreach(var key in new[]{"brand","model","condition"})if(f.TryGetValue(key,out var value)&&value.Length>0){var def=CommunityCatalog.Fields(supply.Category,supply.Intent).FirstOrDefault(d=>d.Key==key);facts.Add(def?.Choices is null?value:CommunityCatalog.Name(def.Choices,value));}
if(facts.Count>0)content.Add(OneLine(string.Join(" · ",facts),11,Ui.Muted));
View? graphic=CommunityCatalog.DefaultIcon(supply)?SystemImage("配货代购",56):post.Media?.FirstOrDefault(m=>m.Kind=="IMAGE") is { } media?Ui.Tap(new ProtectedMediaImage(media,56){WidthRequest=56},()=>page.Navigation.PushAsync(new CommunityMediaPage(media)),"查看发布图片"):null;
if(graphic is not null){var grid=new Grid{ColumnDefinitions=[new(new GridLength(56)),new(GridLength.Star)],ColumnSpacing=9};grid.Add(graphic);grid.Add(content,1);body.Add(grid);}else body.Add(content);
if(post.ModerationStatus!="APPROVED"||post.ShelfStatus!="ACTIVE")body.Add(Ui.Text(post.ShelfStatus!="ACTIVE"?"已下架":CommunitySafetyUi.Status(post.ModerationStatus),10,Ui.Red));
var actions=Ui.Row(CardAction("详情",()=>page.Navigation.PushAsync(new PostDetailPage(post.Id))));actions.Spacing=6;
if(post.AuthorId!=AuthClient.Current.AccountId)actions.Add(CardAction("私信",()=>CommunitySafetyUi.OpenChat(page,new(post.AuthorId,"",post.AuthorName,post.Following,post.AuthorRemark,post.IdentityKind,post.Verified,post.CreditLevel)),true));
else if(post.ShelfStatus=="ACTIVE"&&post.ModerationStatus=="APPROVED")actions.Add(CardAction("刷新卡片",async()=>{await AuthClient.Current.CommunityRequest<CommunityPost>($"/posts/{post.Id}/refresh",new{});if(page is OnlinePage online)await online.Reload();}));
body.Add(Ui.Spread(OneLine("📍 "+post.City+" · "+Time(post.RefreshedAt),10,Ui.Muted),actions));return Ui.Card(body,14,new Thickness(10,8));
}
private static View CompactServiceCard(CommunityPost post,ContentPage page)
{
var service=post.Service!;var name=ServiceCatalog.Categories.GetValueOrDefault(service.Category,"其他服务");
var body=Ui.Stack(Ui.Spread(OneLine(post.AuthorRemark.Length>0?post.AuthorRemark:post.AuthorName,bold:true),CompactIdentity(post)));body.Spacing=6;
var info=Ui.Stack(OneLine(service.Project,15,bold:true),OneLine((service.FeeMin==service.FeeMax?service.FeeMin.ToString("0.##"):service.FeeMin.ToString("0.##")+"–"+service.FeeMax.ToString("0.##"))+" 元 / "+service.Unit,15,Ui.Bronze,true),Ui.Text("兜底回购:"+(service.Buyback?"是":"否"),10,Ui.Muted));info.Spacing=4;
var description=Ui.Text(CommunityText.Preview(service.Description,60),12,Ui.Muted);description.MaxLines=2;description.LineBreakMode=LineBreakMode.TailTruncation;info.Add(description);
View? image=CommunityCatalog.DefaultIcon(null,service)?SystemImage(name):post.Media?.FirstOrDefault(m=>m.Kind=="IMAGE") is { } m?new ProtectedMediaImage(m,64){WidthRequest=64}:null;
if(image is null)body.Add(info);else{var row=new Grid{ColumnDefinitions=[new(new GridLength(64)),new(GridLength.Star)],ColumnSpacing=9};row.Add(image);row.Add(info,1);body.Add(row);}
if(post.ModerationStatus!="APPROVED")body.Add(Ui.Text(CommunitySafetyUi.Status(post.ModerationStatus),10,Ui.Red));
var actions=Ui.Row(CardAction("详情",()=>page.Navigation.PushAsync(new PostDetailPage(post.Id))));if(post.AuthorId!=AuthClient.Current.AccountId)actions.Add(CardAction("私信",()=>CommunitySafetyUi.OpenChat(page,new(post.AuthorId,"",post.AuthorName,post.Following,post.AuthorRemark,post.IdentityKind,post.Verified,post.CreditLevel)),true));
body.Add(Ui.Spread(OneLine("📍 "+post.City+" · "+name,10,Ui.Muted),actions));return Ui.Card(body,14,new Thickness(12,10));
}
public static View TradeNotice(string context,string category="",bool compact=false)
{
var message=context switch
{
"CERTIFICATE"=>"证照仅用于本次交易核验,请核对水印日期与资料一致性,勿转发或挪作他用。认证标识及证照互换不能代替实物核验。",
"CHAT"=>"请核对交易对象和实物,谨慎处理转账、陌生链接及索取验证码的消息。发现异常可通过‘举报用户’提交图片与说明。",
"PUBLISH"=>"请如实填写商品、价格与交割信息。发布照片中请遮挡身份证、银行卡等私密资料,证照通过双方同意的互换功能核验。",
"DELIVERY"=>"优先面交查验,或选择有保障的交割渠道;发货前确认收件信息、费用和验收方式。平台提供信息交流,具体交易由双方协商。",
_=>category=="BULLION"?"请核验实物克重、纯度、凭证与报价基准,确认加减价及最终金额。行情会变化,发布报价和估算价值需在交易时再次确认。":category=="SERVICE"?"确认鉴定范围、收费和回购条件,留存服务凭证;服务发布及认证标识不代表平台担保。":"请核验商品实物、品相、配件与相关凭证,确认交易对象及最终价格。发布信息和认证标识不代表平台担保。"
};
var text=Ui.Text(message,12,Ui.Bronze);if(compact){text.MaxLines=2;text.LineBreakMode=LineBreakMode.TailTruncation;}
var body=Ui.Stack(Ui.Row(Ui.Icon("shield",Ui.Bronze,16),Ui.Text("温馨提示",13,Ui.Bronze,true)),text);body.Spacing=6;var card=Ui.Card(body,14,new Thickness(12),Ui.Cream);
return compact?Ui.Tap(card,()=>Application.Current!.Windows[0].Page!.DisplayAlertAsync("温馨提示",message,"知道了"),"温馨提示"):card;
}
public static View ServiceSummary(ServiceCard service,bool detail=false)=>Ui.Stack(Ui.Pill(ServiceCatalog.Categories.GetValueOrDefault(service.Category,"其他服务")),Ui.Text(service.Project,17,bold:true),Ui.Text((service.FeeMin==service.FeeMax?service.FeeMin.ToString("0.##"):service.FeeMin.ToString("0.##")+"–"+service.FeeMax.ToString("0.##"))+" 元 / "+service.Unit,20,Ui.Bronze,true),Ui.Text("提供兜底回购:"+(service.Buyback?"是":"否"),12,Ui.Muted),Ui.Text(detail?service.Description:CommunityText.Preview(service.Description),13));
public static View ConversationCard(Conversation row,ContentPage page)
{
var peer=new CommunityAccount(row.PeerId,row.PeerUsername,row.PeerName,row.Following,row.Remark,row.IdentityKind,row.Verified,row.CreditLevel);var body=Ui.Stack(Ui.Spread(Person(Name(peer),"@"+row.PeerUsername),Ui.Text(Time(row.UpdatedAt),10,Ui.Muted)),Ui.Text(Short(row.LastContent,70),12,Ui.Muted),CommunitySafetyUi.Badge(row.IdentityKind,row.Verified,row.CreditLevel));if(row.UnreadCount>0)body.Add(Ui.Pill(row.UnreadCount+" 条未读",Ui.Red));return Ui.Tap(Ui.Card(body),()=>page.Navigation.PushAsync(new ChatPage(peer)),"私聊 "+Name(peer));
}
public static async Task<CommunityAccount> Follow(ContentPage page,CommunityAccount peer,bool remark=false)
{
string? note=null;if(remark){note=await page.DisplayPromptAsync("设置备注","备注仅自己可见,无需添加好友。","保存","取消",initialValue:peer.Remark,maxLength:60);if(note is null)return peer;}return await AuthClient.Current.CommunityRequest<CommunityAccount>("/follows/"+peer.Id,new FollowRequest(remark||!peer.Following,note));
}
}
public sealed class PostsPage : OnlinePage
{
private readonly string tab;
private readonly VerticalStackLayout list=Ui.Stack();
private readonly Button more;
private long? next;
public PostsPage(string tab="SQUARE")
{
this.tab=tab;Title=tab=="MINE"?"我的发布":tab=="RECOMMEND"?"官方推荐":"帖子广场";more=Ui.Button("加载更多帖子",More,false);more.IsVisible=false;var top=Ui.Stack(Ui.Section(Title),tab=="MINE"?Ui.Spread(Ui.Button("刷新全部",RefreshAll,false),Ui.Button("更新列表",Reload,false)):Ui.Button("刷新",Reload,false),State);if(tab!="MINE"&&(tab!="RECOMMEND"||Auth.IsAdmin))top.Add(Ui.Button("+ 发布图文",()=>Navigation.PushAsync(new PublishPostPage(tab))));list.Spacing=8;Content=CommunityUi.Layout(top,new ScrollView{Content=Ui.Stack(list,more)});
}
private async Task RefreshAll(){var result=await Auth.CommunityRequest<RefreshCardsResult>("/posts/refresh-all",new{});await Reload();await DisplayAlertAsync("刷新全部",$"已刷新 {result.Refreshed} 张供需 / 服务卡片。"+(result.CoolingDown>0?$"\n{result.CoolingDown} 张卡片刚刷新过,请一分钟后再试。":"")+(result.Unavailable>0?$"\n{result.Unavailable} 张未展示或已下架卡片保持原状态。":""),"完成");}
protected override async Task LoadData(){var data=await Auth.CommunityRequest<PostPage>(CommunityUi.FeedPath(new(tab)));if(!CanUpdate)return;list.Clear();Append(data);if(data.Items.Count==0)list.Add(Ui.Empty("还没有发布内容","填写城市和地址后发布图文或供需卡片"));}
private void Append(PostPage page){foreach(var post in page.Items)list.Add(CommunityUi.PostCard(post,this));next=page.NextBefore;more.IsVisible=next.HasValue;}
private async Task More(){if(next is not { } cursor)return;var data=await Auth.CommunityRequest<PostPage>(CommunityUi.FeedPath(new(tab,cursor)));if(CanUpdate)Append(data);}
}
public sealed class PublishPostPage : ContentPage
{
private readonly string account=AuthClient.Current.AccountId,kind,draftFile;
private readonly Entry title=new(){Placeholder="输入帖子标题 / 物品简介",MaxLength=120},city=new(){Placeholder="发布城市",MaxLength=60},address=new(){Placeholder="发布地址 / 发货地",MaxLength=200};
private readonly Editor text=new(){Placeholder="分享经营心得、物品信息或交流话题…",MaxLength=5000,HeightRequest=160};
private readonly Picker intent=new(){Title="发布意向"},category=new(){Title="物品大类"},sub=new(){Title="物品细类"};
private readonly VerticalStackLayout details=Ui.Stack(),classificationBody=Ui.Stack();
private Border? deliveryCard;
private readonly CommunityPhotos photos;
private readonly RecommendationAttachments? recommended;
private readonly VerticalStackLayout photoBody=Ui.Stack();
private Dictionary<string,string> availableCategories=new();
private CancellationTokenSource? publishing;
private readonly Dictionary<string,CheckBox> delivery=[];
private readonly Dictionary<string,DbRow> snapshots=[];
private FieldSet? fields;
private string activeCategory="",activeIntent="",nonce=Guid.NewGuid().ToString("N"),attempt="";
private bool published,building,visible;
private readonly CommunityPost? editing;
private readonly PostingLocationField publishingLocation;
private sealed record Draft(string Title,string Content,string City,string Address,string Intent,string Category,string Subcategory,Dictionary<string,DbRow> Fields,List<string> Delivery,List<CommunityPhotos.Photo> Photos,List<RecommendationAttachments.Attachment>? Recommended=null);
public PublishPostPage(string kind="SQUARE",CommunityPost? editing=null)
{
this.kind=kind;this.editing=editing;publishingLocation=new(editing);if(kind=="RECOMMEND"&&!AuthClient.Current.IsAdmin)throw new InvalidOperationException("推荐内容由管理员发布");Title=editing is null?kind=="SUPPLY"?"发布供需卡片":kind=="RECOMMEND"?"发布官方推荐":"发布图文":"编辑发布";
draftFile=Path.Combine(FileSystem.AppDataDirectory,"social-draft-"+account+"-"+kind+".json");Draft? draft=null;try{if(editing is null&&File.Exists(draftFile))draft=JsonSerializer.Deserialize<Draft>(File.ReadAllText(draftFile));}catch(Exception ex)when(ex is IOException or JsonException){}
title.Text=editing?.Title??draft?.Title;text.Text=editing?.Content??draft?.Content;city.Text=editing?.City??draft?.City??CommunityLocation.City;address.Text=editing?.Address??draft?.Address??CommunityLocation.Address;
photos=new(9,editing?.Media?.Where(m=>m.Kind=="IMAGE").ToList(),draft?.Photos);if(kind=="RECOMMEND"){text.MaxLength=int.MaxValue;recommended=new(editing?.Media,draft?.Recommended);}
var count=Ui.Text("简介正文最多 200 个字符",11,Ui.Muted);count.IsVisible=kind=="SUPPLY";if(kind=="SUPPLY"){text.MaxLength=int.MaxValue;text.Placeholder="填写物品简介,最多 200 个字符";text.TextChanged+=(_,_)=>{var clipped=CommunityText.TrimTo(text.Text??"",200);if(clipped!=text.Text)text.Text=clipped;count.Text=$"简介正文 {CommunityText.Length(clipped)} / 200 个字符";};}
var body=Ui.Stack(Ui.Card(Ui.Stack(Ui.Section(kind=="SUPPLY"?"物品简介":kind=="RECOMMEND"?"推荐内容":"图文内容"),title,text,count)),publishingLocation.View);
if(kind=="SUPPLY")
{
intent.ItemsSource=CommunityCatalog.Intents.Values.ToList();if(draft?.Fields is { } saved)foreach(var p in saved)snapshots[p.Key]=new(p.Value);if(editing?.Supply is { } existing)snapshots[existing.Category+"/"+existing.Intent]=new(existing.Fields.ToDictionary(p=>p.Key,p=>(object?)p.Value));
intent.SelectedIndex=Math.Max(0,CommunityCatalog.Intents.Keys.ToList().IndexOf(editing?.Supply?.Intent??draft?.Intent??"SALE"));SetCategories(editing?.Supply?.Category??draft?.Category);classificationBody.Add(Ui.Text("物品大类",12,Ui.Muted));classificationBody.Add(category);classificationBody.Add(sub);classificationBody.Add(details);body.Add(Ui.Card(Ui.Stack(Ui.Section("供需卡片"),Ui.Text("发布意向",12,Ui.Muted),intent,classificationBody)));BuildFields(editing?.Supply?.Subcategory??draft?.Subcategory);category.SelectedIndexChanged+=(_,_)=>BuildFields();intent.SelectedIndexChanged+=(_,_)=>{SetCategories(Category());BuildFields();};
var modes=Ui.Stack(Ui.Text("交割方式",12,Ui.Muted));foreach(var d in CommunityCatalog.Deliveries){var box=new CheckBox{IsChecked=(editing?.Supply?.Delivery??draft?.Delivery??["COD"]).Contains(d.Key),Color=Ui.Bronze};delivery[d.Key]=box;string key=d.Key;box.CheckedChanged+=(_,e)=>{if(e.Value&&key is "COD" or "FREE")delivery[key=="COD"?"FREE":"COD"].IsChecked=false;};modes.Add(Ui.Row(box,Ui.Text(d.Value)));}deliveryCard=Ui.Card(modes);deliveryCard.IsVisible=Intent()!="OTHER";body.Add(deliveryCard);
}
if(kind=="RECOMMEND")photoBody.Add(Ui.Stack(Ui.Section("推荐图片与视频"),Ui.Text("图片、视频数量及视频时长不设发布限制。视频支持 MP4 / WebM;列表静音自动播放,进入详情后有声音。",11,Ui.Muted),recommended!.View));
else if(kind!="SUPPLY")photoBody.Add(Ui.Stack(Ui.Section("实物照片"),Ui.Text("最多 9 张照片",11,Ui.Muted),photos.View));
body.Add(Ui.Card(photoBody));body.Add(CommunityUi.TradeNotice("PUBLISH"));body.Add(Ui.Text("未发布的内容会保存为本账户在本机的草稿。",11,Ui.Muted));body.Add(Ui.Button(editing is null?"确认发布":"保存修改",Publish));body.Padding=18;Content=new ScrollView{Content=body};
}
private void SetCategories(string? previous=null){building=true;try{availableCategories=CommunityCatalog.CategoriesFor(Intent());category.ItemsSource=availableCategories.Values.ToList();category.SelectedIndex=Math.Max(0,availableCategories.Keys.ToList().IndexOf(previous??""));}finally{building=false;}}
private string Category()=>availableCategories.Keys.ElementAt(Math.Max(0,category.SelectedIndex));
private string Intent()=>CommunityCatalog.Intents.Keys.ElementAt(Math.Max(0,intent.SelectedIndex));
private void BuildFields(string? selectedSub=null)
{
if(building)return;building=true;try
{
if(fields is not null)snapshots[activeCategory+"/"+activeIntent]=fields.Read(false);activeCategory=Category();activeIntent=Intent();classificationBody.IsVisible=activeIntent!="OTHER";if(deliveryCard is not null)deliveryCard.IsVisible=activeIntent!="OTHER";var values=CommunityCatalog.Subcategories(activeCategory,activeIntent);sub.ItemsSource=values.Values.ToList();sub.SelectedIndex=values.Count==0?-1:Math.Max(0,values.Keys.ToList().IndexOf(selectedSub??""));sub.IsVisible=values.Count>0;
var row=snapshots.GetValueOrDefault(activeCategory+"/"+activeIntent)??new DbRow{["source"]="NONE",["invoice"]="NO",["priceMode"]="FIXED",["supportsExchange"]="YES",["shippingFrom"]=""};fields=new(row,CommunityCatalog.Fields(activeCategory,activeIntent).Select(d=>new Field(d.Key,d.Label,d.Kind switch{"number"=>FieldKind.Number,"choice"=>FieldKind.Choice,"date"=>FieldKind.Date,_=>FieldKind.Text},d.Required,d.Choices,d.Hint)));details.Clear();details.Add(fields.View);photos.Limit=CommunityCatalog.PhotoLimit(activeCategory,activeIntent);
photoBody.Clear();if(photos.Limit==0)photoBody.Add(CommunityUi.SystemMark("配货代购 · 贵金属首饰","gold"));else{if(photos.View.Parent is Layout old)old.Children.Remove(photos.View);photoBody.Add(Ui.Section("实物照片"));photoBody.Add(Ui.Text($"选填,最多 {photos.Limit} 张照片",11,Ui.Muted));photoBody.Add(photos.View);}
void Visibility(){var current=fields.Read(false);fields.SetVisible("sourceCustom",current.Text("source")=="CUSTOM");fields.SetVisible("exchangeRatio",current.Text("supportsExchange")=="YES");}Visibility();fields.Changed+=Visibility;
}
finally{building=false;}
}
private SupplyCard? Card(bool validate)
{
if(kind!="SUPPLY")return null;var row=fields!.Read(validate);var allowed=CommunityCatalog.Fields(Category(),Intent()).Select(d=>d.Key).ToHashSet();var values=row.Where(p=>allowed.Contains(p.Key)&&p.Value is not null&&!string.IsNullOrEmpty(Convert.ToString(p.Value,CultureInfo.InvariantCulture))).ToDictionary(p=>p.Key,p=>Convert.ToString(p.Value,CultureInfo.InvariantCulture)??"");if(values.GetValueOrDefault("source")=="CUSTOM"&&values.GetValueOrDefault("sourceCustom","").Length==0&&validate)throw new ArgumentException("请填写自定义源头");if(validate&&values.GetValueOrDefault("supportsExchange")=="YES"&&values.GetValueOrDefault("exchangeRatio","").Length==0)throw new ArgumentException("支持黄料兑换时请填写兑换比例,如 1:0.998");return new(Intent(),Category(),sub.SelectedIndex>=0?CommunityCatalog.Subcategories(Category(),Intent()).Keys.ElementAt(sub.SelectedIndex):"",values,Intent()=="OTHER"?[]:delivery.Where(p=>p.Value.IsChecked).Select(p=>p.Key).ToList());
}
private async Task Publish()
{
if(account!=AuthClient.Current.AccountId)throw new InvalidOperationException("账户已切换,请重新进入发布页");if(!await CommunitySafetyUi.Write(this))return;var gps=await publishingLocation.Get();string heading=title.Text?.Trim()??"",content=text.Text?.Trim()??"",place=gps?.City??editing?.City??"",location="";if(heading.Length==0)throw new ArgumentException("请填写标题 / 简介");if(kind=="SUPPLY"&&content.Length==0)content=heading;if(content.Length==0)throw new ArgumentException("请填写正文");var supply=Card(true);IsEnabled=false;
try{if(kind=="SUPPLY"&&CommunityText.Length(content)>200)throw new ArgumentException("供需简介正文最多 200 个字符");if(kind=="SUPPLY"&&editing is null){var quota=await AuthClient.Current.CommunityRequest<SupplyQuota>("/supply/quota");if(quota.Active>=quota.Limit)throw new ArgumentException($"服务与供需合计已达上限({quota.Active}/{quota.Limit}),请先下架一张");}publishing=new();var ids=recommended is not null?await recommended.Upload(publishing.Token):photos.Limit==0?[]:await photos.Upload();if(!visible||account!=AuthClient.Current.AccountId)return;var current=JsonSerializer.Serialize(new{heading,content,place,location,ids,supply});if(attempt!=current){attempt=current;nonce=Guid.NewGuid().ToString("N");}var request=new PublishPostRequest(heading,content,nonce,kind,place,location,ids,supply,Location:gps);if(kind=="RECOMMEND"){using var result=await AuthClient.Current.AdminRequest(editing is null?"/community/recommendations":$"/community/recommendations/{editing.Id}/edit",request);}else await AuthClient.Current.CommunityRequest<CommunityPost>(editing is null?"/posts":$"/posts/{editing.Id}/edit",request);published=true;if(editing is null&&File.Exists(draftFile))File.Delete(draftFile);CommunityLocation.City=place;CommunityLocation.Address=location;if(visible&&Navigation.NavigationStack.LastOrDefault()==this)await Navigation.PopAsync();}finally{publishing?.Dispose();publishing=null;IsEnabled=true;}
}
protected override async void OnAppearing(){base.OnAppearing();visible=true;try{if(await CommunitySafetyUi.Write(this))await publishingLocation.Start();}catch(Exception ex){await DisplayAlertAsync("需要认证",ex.Message,"知道了");}}
protected override void OnDisappearing()
{
visible=false;publishing?.Cancel();if(!published&&editing is null&&!AccountDataClear.InProgress&&account==AuthClient.Current.AccountId){if(fields is not null)snapshots[activeCategory+"/"+activeIntent]=fields.Read(false);var card=Card(false);try{File.WriteAllText(draftFile,JsonSerializer.Serialize(new Draft(title.Text??"",text.Text??"",city.Text??"",address.Text??"",card?.Intent??"",card?.Category??"",card?.Subcategory??"",snapshots,card?.Delivery??[],photos.Draft(),recommended?.Draft())));}catch(IOException){}}base.OnDisappearing();
}
}
public sealed class PostDetailPage : OnlinePage
{
private readonly long postId;
private CommunityPost? post;
private readonly VerticalStackLayout header=Ui.Stack(),comments=Ui.Stack();
private readonly Entry reply=new(){Placeholder="写下评论",MaxLength=1000};
private readonly View commentHeading,replyBar;
private readonly Button more;
private long? next;
private string nonce=Guid.NewGuid().ToString("N"),attempt="";
public PostDetailPage(long id)
{
postId=id;Title="发布详情";more=Ui.Button("更多评论",More,false);more.IsVisible=false;commentHeading=Ui.Section("评论");replyBar=Ui.Spread(Ui.Card(reply,12,new Thickness(12,0)),Ui.Button("评论",Reply));replyBar.IsVisible=commentHeading.IsVisible=false;Content=CommunityUi.Layout(Ui.Stack(State,Ui.Button("刷新内容",Reload,false)),new ScrollView{Content=Ui.Stack(header,commentHeading,comments,more)},replyBar);
}
protected override async Task LoadData(){var latest=await Auth.CommunityRequest<CommunityPost>("/posts/"+postId);var rows=latest.Kind=="SUPPLY"?null:await Auth.CommunityRequest<CommentPage>($"/posts/{postId}/comments");if(!CanUpdate)return;CommunityVideo.StopAll();post=latest;RenderHeader();comments.Clear();replyBar.IsVisible=commentHeading.IsVisible=rows is not null;more.IsVisible=false;next=null;if(rows is not null){Append(rows);if(rows.Items.Count==0)comments.Add(Ui.Text("暂无评论,来交流第一条心得吧。",12,Ui.Muted));}}
private void RenderHeader()
{
if(post is null)return;header.Clear();var body=Ui.Stack(CommunityUi.Person(post.AuthorName,CommunityUi.Time(post.CreatedAt)),Ui.Text(post.Title,23,bold:true),Ui.Text("📍 "+post.City+(post.PublishArea.Length>0?" · "+post.PublishArea:""),12,Ui.Muted),Ui.Divider(),Ui.Text(post.Content,15));if(CommunityCatalog.DefaultIcon(post.Supply,post.Service))body.Add(CommunityUi.SystemMark(post.Supply is not null?"配货代购 · 贵金属首饰":ServiceCatalog.Categories.GetValueOrDefault(post.Service!.Category,"鉴定评估服务"),post.Supply is not null?"gold":"shield"));else body.Add(CommunityUi.PostMedia(post,this,true));
if(post.Supply is { Intent:"OTHER" })body.Add(Ui.Pill("其他图文"));
else if(post.Supply is { } s){body.Add(Ui.Section("供需卡片"));body.Add(Ui.Text(CommunityCatalog.Name(CommunityCatalog.Intents,s.Intent)+" · "+CommunityCatalog.Name(CommunityCatalog.Categories,s.Category)+" · "+CommunityCatalog.Name(CommunityCatalog.Subcategories(s.Category),s.Subcategory),14,Ui.Bronze,true));foreach(var d in CommunityCatalog.Fields(s.Category,s.Intent))if(s.Fields.TryGetValue(d.Key,out var v))body.Add(Ui.Spread(Ui.Text(d.Label,12,Ui.Muted),Ui.Text(d.Choices is null?v:CommunityCatalog.Name(d.Choices,v),13)));body.Add(Ui.Text("交割:"+string.Join(" / ",s.Delivery.Select(d=>CommunityCatalog.Name(CommunityCatalog.Deliveries,d))),13));if(s.Category=="BULLION"&&s.Intent=="BUY"&&(post.Media?.Count??0)==0)body.Add(Ui.Row(Ui.Icon("gold",Ui.Gold,36),Ui.Text("贵金属求购",13,Ui.Bronze)));}
body.Add(CommunitySafetyUi.Badge(post.IdentityKind,post.Verified,post.CreditLevel));if(post.Service is { } service)body.Add(CommunityUi.ServiceSummary(service,true));if(post.ModerationStatus!="APPROVED")body.Add(Ui.Pill(CommunitySafetyUi.Status(post.ModerationStatus),Ui.Red));if(post.ShelfStatus!="ACTIVE")body.Add(Ui.Pill("已下架",Ui.Muted));
if(post.AuthorId!=Auth.AccountId){body.Add(Ui.Button("举报此用户",()=>Navigation.PushAsync(new ReportPage(post.AuthorId,post.Id)),false));var peer=new CommunityAccount(post.AuthorId,"",post.AuthorName,post.Following,post.AuthorRemark,post.IdentityKind,post.Verified,post.CreditLevel);body.Add(Ui.Spread(Ui.Button(peer.Following?"已关注 · 取消":"关注作者",async()=>{await CommunityUi.Follow(this,peer);await Reload();},false),Ui.Button("联系他",()=>CommunitySafetyUi.OpenChat(this,peer))));if(peer.Following)body.Add(Ui.Button("设置作者备注",async()=>{await CommunityUi.Follow(this,peer,true);await Reload();},false));}
if(post.Kind!="SUPPLY")body.Add(Ui.Button((post.Liked?"已赞 · ":"点赞 · ")+post.LikeCount,Like,false));if(post.AuthorId==Auth.AccountId){if(post.Kind is "SUPPLY" or "SERVICE"&&post.ShelfStatus=="ACTIVE")body.Add(Ui.Button("一键刷新卡片",async()=>{await Auth.CommunityRequest<CommunityPost>($"/posts/{postId}/refresh",new{});await Reload();},false));if(post.Kind is "SUPPLY" or "SERVICE")body.Add(Ui.Button(post.ShelfStatus=="ACTIVE"?"下架卡片":"重新上架",async()=>{await Auth.CommunityRequest<CommunityPost>($"/posts/{postId}/shelf",new ShelfRequest(post.ShelfStatus!="ACTIVE"));await Reload();},false));body.Add(Ui.Button("编辑发布",()=>Navigation.PushAsync(post.Kind=="SERVICE"?(Page)new ServicePublishPage(post):new PublishPostPage(post.Kind,post)),false));}if(Auth.IsAdmin&&post.Kind=="RECOMMEND")body.Add(Ui.Button(post.Pinned?"取消置顶":"置顶推荐",async()=>{using var result=await Auth.AdminRequest($"/community/posts/{postId}/pin",new PinRequest(!post.Pinned));await Reload();},false));if(post.CanDelete)body.Add(Ui.Button("删除发布",Delete,false));header.Add(Ui.Card(body));
if(post.Supply is not null||post.Service is not null){header.Add(CommunityUi.TradeNotice("VERIFY",post.Service is not null?"SERVICE":post.Supply!.Category));if(post.Supply?.Intent!="OTHER")header.Add(CommunityUi.TradeNotice("DELIVERY"));}
}
private void Append(CommentPage data){foreach(var row in data.Items){var body=Ui.Stack(CommunityUi.Person(row.AuthorName,CommunityUi.Time(row.CreatedAt)),Ui.Text(row.Content,14),CommunitySafetyUi.Badge(row.IdentityKind,row.Verified,row.CreditLevel));if(row.ModerationStatus!="APPROVED")body.Add(Ui.Pill(CommunitySafetyUi.Status(row.ModerationStatus)));if(row.CanDelete)body.Add(Ui.Button("删除评论",async()=>{if(!await DisplayAlertAsync("删除评论","确认删除这条评论?","删除","取消"))return;await Auth.CommunityRequest<OperationResult>($"/comments/{row.Id}/delete",new{});await Reload();},false));comments.Add(Ui.Card(body,15));}next=data.NextBefore;more.IsVisible=next.HasValue;}
private async Task More(){if(next is not { } cursor)return;var data=await Auth.CommunityRequest<CommentPage>($"/posts/{postId}/comments?before={cursor}");if(CanUpdate)Append(data);}
private async Task Like(){if(post is null||!await CommunitySafetyUi.Write(this))return;var updated=await Auth.CommunityRequest<CommunityPost>($"/posts/{postId}/like",new LikeRequest(!post.Liked));if(CanUpdate){post=updated;RenderHeader();}}
private async Task Delete(){if(!await DisplayAlertAsync("删除发布","确认删除此内容?相关评论将不再展示。","删除","取消"))return;await Auth.CommunityRequest<OperationResult>($"/posts/{postId}/delete",new{});await Navigation.PopAsync();}
private async Task Reply(){if(!await CommunitySafetyUi.Write(this))return;var content=reply.Text?.Trim()??"";if(content.Length==0)throw new ArgumentException("请填写评论");if(attempt!=content){attempt=content;nonce=Guid.NewGuid().ToString("N");}await Auth.CommunityRequest<CommunityComment>($"/posts/{postId}/comments",new PublishCommentRequest(content,nonce));if(reply.Text?.Trim()==content)reply.Text="";attempt="";await Reload();}
protected override void OnDisappearing(){CommunityVideo.StopAll();base.OnDisappearing();}
}