using System.Text.Json;
using JiDangJia.Contracts;
using JiDangJia.Services;
namespace JiDangJia.Views;
internal static class CommunitySafetyUi
{
private static readonly Dictionary<string,Task<bool>> agreements=[];
public static string Status(string s)=>s switch{"NONE"=>"尚未认证","PENDING"=>"待人工审核","APPROVED"=>"已通过人工审核","REJECTED"=>"未通过审核",_=>s};
public static View Badge(string kind,bool verified,string credit="GOOD")=>Ui.Row(Ui.Pill(kind=="ENTERPRISE"&&verified?"企业":"个人",kind=="ENTERPRISE"&&verified?Ui.Bronze:Color.FromArgb("#2474B5"),kind=="ENTERPRISE"&&verified?Ui.Cream:Color.FromArgb("#E7F2FF")),Ui.Pill("信用 · "+CommunityCatalog.CreditLevels.GetValueOrDefault(credit,"良好"),credit is "RISK" or "HIGH_RISK"?Ui.Red:Ui.Green,Ui.Paper));
public static View Trial()=>Ui.Text(ServerSettings.AllowPrivateHttpTrial&&ServerSettings.BaseUrl.StartsWith("http:")?"HTTP 试用:传输未加密,请仅使用虚构测试资料。":"资料仅用于平台审核和双方同意的交易核验。",11,Ui.Muted);
public static async Task<bool> Agreement(ContentPage page)
{
var account=AuthClient.Current.AccountId;
if(agreements.TryGetValue(account,out var existing))return await existing;
var pending=Check();agreements[account]=pending;
try{return await pending;}finally{if(agreements.GetValueOrDefault(account)==pending)agreements.Remove(account);}
async Task<bool> Check()
{
var access=await AuthClient.Current.CommunityRequest<CommunityAccess>("/access");if(access.AgreementAccepted)return true;
var policy=await AuthClient.Current.CommunityRequest<CommunityAgreement>("/agreement");if(account!=AuthClient.Current.AccountId)return false;return await AgreementPage.Show(page,policy);
}
}
public static async Task<bool> Write(ContentPage page)
{
if(!await Agreement(page))return false;var access=await AuthClient.Current.CommunityRequest<CommunityAccess>("/access");if(access.Banned)throw new InvalidOperationException("社群已禁言"+(DateTimeOffset.TryParse(access.BanUntil,out var end)?",截止 "+end.ToLocalTime().ToString("yyyy-MM-dd HH:mm"):"(永久)")+":"+access.BanReason);if(access.IdentityStatus=="APPROVED"&&access.PhoneBound)return true;
await page.Navigation.PushAsync(new IdentityPage());return false;
}
public static async Task OpenChat(ContentPage page,CommunityAccount peer)
{if(!await Agreement(page))return;var access=await AuthClient.Current.CommunityRequest<CommunityAccess>("/access");if(access.IdentityStatus!="APPROVED"||!access.PhoneBound){await page.Navigation.PushAsync(new IdentityPage());return;}await page.Navigation.PushAsync(new ChatPage(peer));}
}
public sealed class AgreementPage : ContentPage
{
private readonly TaskCompletionSource<bool> answer=new(TaskCreationOptions.RunContinuationsAsynchronously);
private INavigation? owner;private NavigationPage? host;private bool closing;
private AgreementPage(CommunityAgreement policy)
{
Title="社群用户协议";var box=new CheckBox{Color=Ui.Bronze};var button=Ui.Button("同意并进入社群",async()=>{if(!box.IsChecked)throw new ArgumentException("请主动勾选同意协议");await AuthClient.Current.CommunityRequest<CommunityAccess>("/agreement/accept",new AcceptAgreementRequest(policy.Version,true));await Close(true);});
var body=Ui.Stack(Ui.Text("版本 "+policy.Version,11,Ui.Muted),Ui.Text(policy.Text,14),Ui.Row(box,Ui.Text("我已阅读并同意社群用户协议",13)),button,Ui.Button("返回社群",()=>Close(false),false));body.Padding=20;Content=new ScrollView{Content=body};
}
private async Task Close(bool accepted){if(closing)return;closing=true;try{await Navigation.PopModalAsync();answer.TrySetResult(accepted);}catch{closing=false;throw;}}
protected override bool OnBackButtonPressed(){_ = Close(false);return true;}
protected override void OnDisappearing(){base.OnDisappearing();Dispatcher.Dispatch(()=>{if(!closing&&owner is not null&&host is not null&&!owner.ModalStack.Contains(host))answer.TrySetResult(false);});}
public static async Task<bool> Show(ContentPage page,CommunityAgreement policy){var dialog=new AgreementPage(policy){owner=page.Navigation};dialog.host=new NavigationPage(dialog);await page.Navigation.PushModalAsync(dialog.host);return await dialog.answer.Task;}
}
internal sealed class PrivateImage : ContentView
{
public PrivateImage(string id,string? apiPath=null)
{
var image=new Image{HeightRequest=240,Aspect=Aspect.AspectFit};Content=Ui.Card(image,12,new Thickness(5));CancellationTokenSource? cancel=null;
Loaded+=async(_,_)=>{cancel?.Cancel();cancel=new();var ct=cancel.Token;try{var bytes=await AuthClient.Current.DownloadPrivate(apiPath??"/safety/documents/"+id,ct);if(!ct.IsCancellationRequested)image.Source=ImageSource.FromStream(()=>new MemoryStream(bytes));}catch(Exception ex){if(!ct.IsCancellationRequested)Content=Ui.Text(ex.Message,12,Ui.Red);}};
Unloaded+=(_,_)=>{cancel?.Cancel();cancel?.Dispose();cancel=null;image.Source=null;};
}
}
internal sealed class PrivatePhotos
{
private readonly string purpose;private readonly int max;private readonly List<(string Local,string Nonce,string? Id)> photos=[];private readonly VerticalStackLayout list=Ui.Stack();
public View View=>list;
public PrivatePhotos(string purpose,int max,IEnumerable<string>? initial=null){this.purpose=purpose;this.max=max;foreach(var id in initial??[])photos.Add(("",Guid.NewGuid().ToString("N"),id));Render();}
private void Render()
{
list.Clear();foreach(var p in photos.ToList())list.Add(Ui.Stack(p.Id is { } id?new PrivateImage(id):new Image{Source=MediaFiles.Source(p.Local),HeightRequest=150,Aspect=Aspect.AspectFit},Ui.Button("移除此照片",()=>{photos.Remove(p);Render();return Task.CompletedTask;},false)));
list.Add(Ui.Button("+ 添加资料照片",async()=>{if(photos.Count>=max)throw new ArgumentException($"最多 {max} 张照片");var page=Application.Current!.Windows[0].Page!;var mode=await page.DisplayActionSheetAsync("添加清晰照片","取消",null,"拍照","相册");if(mode is not ("拍照" or "相册"))return;var local=await MediaFiles.Pick(mode=="拍照");if(local is not null){photos.Add((local,Guid.NewGuid().ToString("N"),null));Render();}},false));
}
public async Task<List<string>> Upload(bool required=true)
{
if(required&&photos.Count==0)throw new ArgumentException("请上传必填资料照片");for(int i=0;i<photos.Count;i++){var p=photos[i];if(p.Id is null){var bytes=Convert.FromBase64String(p.Local[(p.Local.IndexOf(',')+1)..]);var result=await AuthClient.Current.UploadDocument(bytes,purpose,p.Nonce);photos[i]=(p.Local,p.Nonce,result.Id);}}return photos.Select(p=>p.Id!).ToList();
}
}
public sealed class IdentityPage : PrivatePage
{
private readonly VerticalStackLayout body=Ui.Stack();private readonly string account=AuthClient.Current.AccountId;
public IdentityPage(){Title="个人 / 企业认证";body.Padding=18;Content=new ScrollView{Content=body};}
protected override async void OnAppearing(){base.OnAppearing();try{var app=await AuthClient.Current.SafetyRequest<IdentityApplication>("/identity");if(account==AuthClient.Current.AccountId)Build(app);}catch(Exception ex){body.Clear();body.Add(Ui.Text(ex.Message,13,Ui.Red));}}
private void Build(IdentityApplication app)
{
body.Clear();body.Add(Ui.Card(Ui.Stack(Ui.Section("认证状态"),Ui.Text(CommunitySafetyUi.Status(app.State.Status),16,Ui.Bronze,true),Ui.Text(app.State.Reason,12,Ui.Red),Ui.Text("仅浏览无需认证;发布、聊天需通过人工审核。",12,Ui.Muted),CommunitySafetyUi.Trial())));
var f=app.Form;var kind=new Picker{Title="认证身份",ItemsSource=new[]{"个人","企业"},SelectedIndex=f?.Kind=="ENTERPRISE"?1:0};
Entry Input(string label,string? value,int max=120){var e=new Entry{Placeholder=label,Text=value,MaxLength=max};body.Add(Ui.Stack(Ui.Text(label,12,Ui.Muted),Ui.Card(e,12,new Thickness(10,0),Ui.Paper)));return e;}
body.Add(kind);var company=Input("企业名(企业必填)",f?.CompanyName);var name=Input("姓名 / 法人姓名 *",f?.Name,64);var number=Input("身份证号 *",f?.IdNumber,18);var phone=Input("电话 / 手机号 *",f?.Phone,11);phone.Keyboard=Keyboard.Telephone;
body.Add(Ui.Button("手机号短信验证",()=>Navigation.PushAsync(new PhonePage()),false));var address=Input("地址(选填)",f?.Address,300);var holder=Ui.Text(name.Text??"",14);body.Add(Ui.Stack(Ui.Text("银行卡户名(与姓名保持一致,不可修改)",12,Ui.Muted),holder));name.TextChanged+=(_,_)=>holder.Text=name.Text??"";
var bank=Input("银行卡号(企业必填)",f?.BankNumber,32);bank.Keyboard=Keyboard.Numeric;var bankName=Input("开户行(企业必填)",f?.BankName);
var photos=new PrivatePhotos("IDENTITY",2,f?.IdPhotos);body.Add(Ui.Card(Ui.Stack(Ui.Section("身份证照片 *"),Ui.Text("人像面必填,可附国徽面;资料不会公开展示。",11,Ui.Muted),photos.View)));
var business=new PrivatePhotos("IDENTITY",1,f?.BusinessPhoto is { } photo?new[]{photo}:null);var businessBox=Ui.Card(Ui.Stack(Ui.Section("营业执照照片 *"),business.View));businessBox.IsVisible=kind.SelectedIndex==1;company.IsEnabled=kind.SelectedIndex==1;kind.SelectedIndexChanged+=(_,_)=>{company.IsEnabled=businessBox.IsVisible=kind.SelectedIndex==1;};body.Add(businessBox);
var consent=new CheckBox{Color=Ui.Bronze};body.Add(Ui.Row(consent,Ui.Text("同意提交资料供平台人工审核",13)));
body.Add(Ui.Button("提交人工审核",async()=>{if(account!=AuthClient.Current.AccountId)throw new InvalidOperationException("账户已切换");if(!consent.IsChecked)throw new ArgumentException("请勾选资料审核授权");var ids=await photos.Upload();var license=kind.SelectedIndex==1?(await business.Upload()).Single():null;var result=await AuthClient.Current.SafetyRequest<IdentityStatus>("/identity",new IdentityForm(kind.SelectedIndex==1?"ENTERPRISE":"PERSONAL",name.Text??"",number.Text??"",phone.Text??"",address.Text??"",company.Text??"",bank.Text??"",bankName.Text??"",ids,license,true,name.Text??""));await DisplayAlertAsync("已提交",CommunitySafetyUi.Status(result.Status),"完成");await Navigation.PopAsync();}));
}
}
public sealed class ReportPage : PrivatePage
{
public ReportPage(string peerId,long? postId=null)
{
Title="举报用户";var text=new Editor{Placeholder="描述违规行为、发生时间和证据内容(必填)",HeightRequest=160,MaxLength=2000};var photos=new PrivatePhotos("REPORT",5);var nonce=Guid.NewGuid().ToString("N");
var body=Ui.Stack(Ui.Section("举报说明与图片均必填"),CommunitySafetyUi.Trial(),text,photos.View,Ui.Button("提交举报",async()=>{if(string.IsNullOrWhiteSpace(text.Text))throw new ArgumentException("请填写举报文字说明");var ids=await photos.Upload();await AuthClient.Current.SafetyRequest<CommunityReport>("/reports",new ReportRequest(peerId,text.Text,ids,nonce,postId));await DisplayAlertAsync("已提交","管理员将审核图片与说明,可在我的举报中查看结果。","完成");await Navigation.PopAsync();}));body.Padding=18;Content=new ScrollView{Content=body};
}
}
public sealed class MyReportsPage : PrivatePage
{
private readonly VerticalStackLayout body=Ui.Stack();
public MyReportsPage(){Title="我的举报";body.Padding=18;Content=new ScrollView{Content=body};}
protected override async void OnAppearing(){base.OnAppearing();try{var reports=await AuthClient.Current.SafetyRequest<List<CommunityReport>>("/reports");body.Clear();foreach(var r in reports)body.Add(Ui.Card(Ui.Stack(Ui.Text(r.Description,14),Ui.Text(r.Status=="OPEN"?"待处理":r.Status=="RESOLVED"?"已处理":"未成立",12,Ui.Bronze),Ui.Text(r.Result,12,Ui.Muted))));if(reports.Count==0)body.Add(Ui.Empty("暂无举报","举报材料仅自己与管理员可见。"));}catch(Exception ex){body.Add(Ui.Text(ex.Message,12,Ui.Red));}}
}
public sealed class ExchangePage : PrivatePage
{
private readonly string id;private readonly VerticalStackLayout body=Ui.Stack();
public ExchangePage(string id){this.id=id;Title="双方证照互换";body.Padding=18;Content=new ScrollView{Content=body};}
public static async Task Request(ContentPage page,string peer)
{
if(!await CommunitySafetyUi.Write(page))return;
if(!await page.DisplayAlertAsync("申请互换证照","对方确认后,双方姓名、电话、地址、证件及营业执照照片、银行卡号和开户行会互相发送。照片自动带本次核验水印。是否同意并申请?","同意并申请","取消"))return;
var e=await AuthClient.Current.SafetyRequest<CertificateExchange>("/exchanges",new ExchangeRequest(peer,Guid.NewGuid().ToString("N"),true));await page.Navigation.PushAsync(new ExchangePage(e.Id));
}
protected override async void OnAppearing(){base.OnAppearing();try{await Load();}catch(Exception ex){body.Clear();body.Add(Ui.Text(ex.Message,13,Ui.Red));}}
private async Task Load()
{
var e=await AuthClient.Current.SafetyRequest<CertificateExchange>("/exchanges/"+id);body.Clear();body.Add(CommunitySafetyUi.Trial());body.Add(Ui.Text(e.Status=="PENDING"?"等待对方确认":e.Status=="CONFIRMED"?"双方已确认,仅用于本次交易核验":e.Status=="CANCELLED"?"已取消":"已拒绝",16,Ui.Bronze,true));
if(e.Status=="PENDING")
{
bool own=e.RequesterId==AuthClient.Current.AccountId;body.Add(Ui.Text("确认后将互发姓名、电话、地址、证照照片、卡号、开户行。请只向交易对方分享。",13,Ui.Muted));
if(!own)body.Add(Ui.Button("同意并互换双方证照",async()=>{if(await DisplayAlertAsync("确认分享资料","同意将本人认证信息和加水印证件照片发送给对方,并接收对方信息?","同意互换","取消")){await AuthClient.Current.SafetyRequest<CertificateExchange>("/exchanges/"+id,new ExchangeDecision("CONFIRM",true));await Load();}}));
body.Add(Ui.Button(own?"取消申请":"拒绝申请",async()=>{await AuthClient.Current.SafetyRequest<CertificateExchange>("/exchanges/"+id,new ExchangeDecision(own?"CANCEL":"REJECT",false));await Load();},false));body.Add(Ui.Button("刷新互换状态",Load,false));
}
body.Add(CommunityUi.TradeNotice("CERTIFICATE"));if(e.PeerCard is { } other)body.Add(Card("对方信息卡",other));if(e.MyCard is { } mine)body.Add(Card("我的已发送信息卡",mine));
}
private View Card(string title,CertificateCard card)
{
var info=Ui.Stack(Ui.Section(title),Ui.Text(card.CompanyName,16,bold:true),Ui.Text(card.Name+"\n"+card.Phone+"\n"+card.Address,14),Ui.Button("复制姓名 / 电话 / 地址",()=>Clipboard.Default.SetTextAsync(card.Name+" "+card.Phone+" "+card.Address),false),Ui.Button("单独复制姓名",()=>Clipboard.Default.SetTextAsync(card.Name),false),Ui.Text("卡号:"+card.BankNumber+"\n开户行:"+card.BankName,13),Ui.Button("单独复制卡号",()=>Clipboard.Default.SetTextAsync(card.BankNumber),false),Ui.Button("单独复制开户行",()=>Clipboard.Default.SetTextAsync(card.BankName),false),Ui.Text(card.Watermark,11,Ui.Red));
foreach(var image in card.IdPhotos)info.Add(new PrivateImage(image));if(card.BusinessPhoto is { } business)info.Add(new PrivateImage(business));return Ui.Card(info);
}
}
using System.Text.Json;
using JiDangJia.Contracts;
using JiDangJia.Services;
namespace JiDangJia.Views;
internal static class CommunitySafetyUi
{
private static readonly Dictionary<string,Task<bool>> agreements=[];
public static string Status(string s)=>s switch{"NONE"=>"尚未认证","PENDING"=>"待人工审核","APPROVED"=>"已通过人工审核","REJECTED"=>"未通过审核",_=>s};
public static View Badge(string kind,bool verified,string credit="GOOD")=>Ui.Row(Ui.Pill(kind=="ENTERPRISE"&&verified?"企业":"个人",kind=="ENTERPRISE"&&verified?Ui.Bronze:Color.FromArgb("#2474B5"),kind=="ENTERPRISE"&&verified?Ui.Cream:Color.FromArgb("#E7F2FF")),Ui.Pill("信用 · "+CommunityCatalog.CreditLevels.GetValueOrDefault(credit,"良好"),credit is "RISK" or "HIGH_RISK"?Ui.Red:Ui.Green,Ui.Paper));
public static View Trial()=>Ui.Text(ServerSettings.AllowPrivateHttpTrial&&ServerSettings.BaseUrl.StartsWith("http:")?"HTTP 试用:传输未加密,请仅使用虚构测试资料。":"资料仅用于平台审核和双方同意的交易核验。",11,Ui.Muted);
public static async Task<bool> Agreement(ContentPage page)
{
var account=AuthClient.Current.AccountId;
if(agreements.TryGetValue(account,out var existing))return await existing;
var pending=Check();agreements[account]=pending;
try{return await pending;}finally{if(agreements.GetValueOrDefault(account)==pending)agreements.Remove(account);}
async Task<bool> Check()
{
var access=await AuthClient.Current.CommunityRequest<CommunityAccess>("/access");if(access.AgreementAccepted)return true;
var policy=await AuthClient.Current.CommunityRequest<CommunityAgreement>("/agreement");if(account!=AuthClient.Current.AccountId)return false;return await AgreementPage.Show(page,policy);
}
}
public static async Task<bool> Write(ContentPage page)
{
if(!await Agreement(page))return false;var access=await AuthClient.Current.CommunityRequest<CommunityAccess>("/access");if(access.Banned)throw new InvalidOperationException("社群已禁言"+(DateTimeOffset.TryParse(access.BanUntil,out var end)?",截止 "+end.ToLocalTime().ToString("yyyy-MM-dd HH:mm"):"(永久)")+":"+access.BanReason);if(access.IdentityStatus=="APPROVED"&&access.PhoneBound)return true;
await page.Navigation.PushAsync(new IdentityPage());return false;
}
public static async Task OpenChat(ContentPage page,CommunityAccount peer)
{if(!await Agreement(page))return;var access=await AuthClient.Current.CommunityRequest<CommunityAccess>("/access");if(access.IdentityStatus!="APPROVED"||!access.PhoneBound){await page.Navigation.PushAsync(new IdentityPage());return;}await page.Navigation.PushAsync(new ChatPage(peer));}
}
public sealed class AgreementPage : ContentPage
{
private readonly TaskCompletionSource<bool> answer=new(TaskCreationOptions.RunContinuationsAsynchronously);
private INavigation? owner;private NavigationPage? host;private bool closing;
private AgreementPage(CommunityAgreement policy)
{
Title="社群用户协议";var box=new CheckBox{Color=Ui.Bronze};var button=Ui.Button("同意并进入社群",async()=>{if(!box.IsChecked)throw new ArgumentException("请主动勾选同意协议");await AuthClient.Current.CommunityRequest<CommunityAccess>("/agreement/accept",new AcceptAgreementRequest(policy.Version,true));await Close(true);});
var body=Ui.Stack(Ui.Text("版本 "+policy.Version,11,Ui.Muted),Ui.Text(policy.Text,14),Ui.Row(box,Ui.Text("我已阅读并同意社群用户协议",13)),button,Ui.Button("返回社群",()=>Close(false),false));body.Padding=20;Content=new ScrollView{Content=body};
}
private async Task Close(bool accepted){if(closing)return;closing=true;try{await Navigation.PopModalAsync();answer.TrySetResult(accepted);}catch{closing=false;throw;}}
protected override bool OnBackButtonPressed(){_ = Close(false);return true;}
protected override void OnDisappearing(){base.OnDisappearing();Dispatcher.Dispatch(()=>{if(!closing&&owner is not null&&host is not null&&!owner.ModalStack.Contains(host))answer.TrySetResult(false);});}
public static async Task<bool> Show(ContentPage page,CommunityAgreement policy){var dialog=new AgreementPage(policy){owner=page.Navigation};dialog.host=new NavigationPage(dialog);await page.Navigation.PushModalAsync(dialog.host);return await dialog.answer.Task;}
}
internal sealed class PrivateImage : ContentView
{
public PrivateImage(string id,string? apiPath=null)
{
var image=new Image{HeightRequest=240,Aspect=Aspect.AspectFit};Content=Ui.Card(image,12,new Thickness(5));CancellationTokenSource? cancel=null;
Loaded+=async(_,_)=>{cancel?.Cancel();cancel=new();var ct=cancel.Token;try{var bytes=await AuthClient.Current.DownloadPrivate(apiPath??"/safety/documents/"+id,ct);if(!ct.IsCancellationRequested)image.Source=ImageSource.FromStream(()=>new MemoryStream(bytes));}catch(Exception ex){if(!ct.IsCancellationRequested)Content=Ui.Text(ex.Message,12,Ui.Red);}};
Unloaded+=(_,_)=>{cancel?.Cancel();cancel?.Dispose();cancel=null;image.Source=null;};
}
}
internal sealed class PrivatePhotos
{
private readonly string purpose;private readonly int max;private readonly List<(string Local,string Nonce,string? Id)> photos=[];private readonly VerticalStackLayout list=Ui.Stack();
public View View=>list;
public PrivatePhotos(string purpose,int max,IEnumerable<string>? initial=null){this.purpose=purpose;this.max=max;foreach(var id in initial??[])photos.Add(("",Guid.NewGuid().ToString("N"),id));Render();}
private void Render()
{
list.Clear();foreach(var p in photos.ToList())list.Add(Ui.Stack(p.Id is { } id?new PrivateImage(id):new Image{Source=MediaFiles.Source(p.Local),HeightRequest=150,Aspect=Aspect.AspectFit},Ui.Button("移除此照片",()=>{photos.Remove(p);Render();return Task.CompletedTask;},false)));
list.Add(Ui.Button("+ 添加资料照片",async()=>{if(photos.Count>=max)throw new ArgumentException($"最多 {max} 张照片");var page=Application.Current!.Windows[0].Page!;var mode=await page.DisplayActionSheetAsync("添加清晰照片","取消",null,"拍照","相册");if(mode is not ("拍照" or "相册"))return;var local=await MediaFiles.Pick(mode=="拍照");if(local is not null){photos.Add((local,Guid.NewGuid().ToString("N"),null));Render();}},false));
}
public async Task<List<string>> Upload(bool required=true)
{
if(required&&photos.Count==0)throw new ArgumentException("请上传必填资料照片");for(int i=0;i<photos.Count;i++){var p=photos[i];if(p.Id is null){var bytes=Convert.FromBase64String(p.Local[(p.Local.IndexOf(',')+1)..]);var result=await AuthClient.Current.UploadDocument(bytes,purpose,p.Nonce);photos[i]=(p.Local,p.Nonce,result.Id);}}return photos.Select(p=>p.Id!).ToList();
}
}
public sealed class IdentityPage : PrivatePage
{
private readonly VerticalStackLayout body=Ui.Stack();private readonly string account=AuthClient.Current.AccountId;
public IdentityPage(){Title="个人 / 企业认证";body.Padding=18;Content=new ScrollView{Content=body};}
protected override async void OnAppearing(){base.OnAppearing();try{var app=await AuthClient.Current.SafetyRequest<IdentityApplication>("/identity");if(account==AuthClient.Current.AccountId)Build(app);}catch(Exception ex){body.Clear();body.Add(Ui.Text(ex.Message,13,Ui.Red));}}
private void Build(IdentityApplication app)
{
body.Clear();body.Add(Ui.Card(Ui.Stack(Ui.Section("认证状态"),Ui.Text(CommunitySafetyUi.Status(app.State.Status),16,Ui.Bronze,true),Ui.Text(app.State.Reason,12,Ui.Red),Ui.Text("仅浏览无需认证;发布、聊天需通过人工审核。",12,Ui.Muted),CommunitySafetyUi.Trial())));
var f=app.Form;var kind=new Picker{Title="认证身份",ItemsSource=new[]{"个人","企业"},SelectedIndex=f?.Kind=="ENTERPRISE"?1:0};
Entry Input(string label,string? value,int max=120){var e=new Entry{Placeholder=label,Text=value,MaxLength=max};body.Add(Ui.Stack(Ui.Text(label,12,Ui.Muted),Ui.Card(e,12,new Thickness(10,0),Ui.Paper)));return e;}
body.Add(kind);var company=Input("企业名(企业必填)",f?.CompanyName);var name=Input("姓名 / 法人姓名 *",f?.Name,64);var number=Input("身份证号 *",f?.IdNumber,18);var phone=Input("电话 / 手机号 *",f?.Phone,11);phone.Keyboard=Keyboard.Telephone;
body.Add(Ui.Button("手机号短信验证",()=>Navigation.PushAsync(new PhonePage()),false));var address=Input("地址(选填)",f?.Address,300);var holder=Ui.Text(name.Text??"",14);body.Add(Ui.Stack(Ui.Text("银行卡户名(与姓名保持一致,不可修改)",12,Ui.Muted),holder));name.TextChanged+=(_,_)=>holder.Text=name.Text??"";
var bank=Input("银行卡号(企业必填)",f?.BankNumber,32);bank.Keyboard=Keyboard.Numeric;var bankName=Input("开户行(企业必填)",f?.BankName);
var photos=new PrivatePhotos("IDENTITY",2,f?.IdPhotos);body.Add(Ui.Card(Ui.Stack(Ui.Section("身份证照片 *"),Ui.Text("人像面必填,可附国徽面;资料不会公开展示。",11,Ui.Muted),photos.View)));
var business=new PrivatePhotos("IDENTITY",1,f?.BusinessPhoto is { } photo?new[]{photo}:null);var businessBox=Ui.Card(Ui.Stack(Ui.Section("营业执照照片 *"),business.View));businessBox.IsVisible=kind.SelectedIndex==1;company.IsEnabled=kind.SelectedIndex==1;kind.SelectedIndexChanged+=(_,_)=>{company.IsEnabled=businessBox.IsVisible=kind.SelectedIndex==1;};body.Add(businessBox);
var consent=new CheckBox{Color=Ui.Bronze};body.Add(Ui.Row(consent,Ui.Text("同意提交资料供平台人工审核",13)));
body.Add(Ui.Button("提交人工审核",async()=>{if(account!=AuthClient.Current.AccountId)throw new InvalidOperationException("账户已切换");if(!consent.IsChecked)throw new ArgumentException("请勾选资料审核授权");var ids=await photos.Upload();var license=kind.SelectedIndex==1?(await business.Upload()).Single():null;var result=await AuthClient.Current.SafetyRequest<IdentityStatus>("/identity",new IdentityForm(kind.SelectedIndex==1?"ENTERPRISE":"PERSONAL",name.Text??"",number.Text??"",phone.Text??"",address.Text??"",company.Text??"",bank.Text??"",bankName.Text??"",ids,license,true,name.Text??""));await DisplayAlertAsync("已提交",CommunitySafetyUi.Status(result.Status),"完成");await Navigation.PopAsync();}));
}
}
public sealed class ReportPage : PrivatePage
{
public ReportPage(string peerId,long? postId=null)
{
Title="举报用户";var text=new Editor{Placeholder="描述违规行为、发生时间和证据内容(必填)",HeightRequest=160,MaxLength=2000};var photos=new PrivatePhotos("REPORT",5);var nonce=Guid.NewGuid().ToString("N");
var body=Ui.Stack(Ui.Section("举报说明与图片均必填"),CommunitySafetyUi.Trial(),text,photos.View,Ui.Button("提交举报",async()=>{if(string.IsNullOrWhiteSpace(text.Text))throw new ArgumentException("请填写举报文字说明");var ids=await photos.Upload();await AuthClient.Current.SafetyRequest<CommunityReport>("/reports",new ReportRequest(peerId,text.Text,ids,nonce,postId));await DisplayAlertAsync("已提交","管理员将审核图片与说明,可在我的举报中查看结果。","完成");await Navigation.PopAsync();}));body.Padding=18;Content=new ScrollView{Content=body};
}
}
public sealed class MyReportsPage : PrivatePage
{
private readonly VerticalStackLayout body=Ui.Stack();
public MyReportsPage(){Title="我的举报";body.Padding=18;Content=new ScrollView{Content=body};}
protected override async void OnAppearing(){base.OnAppearing();try{var reports=await AuthClient.Current.SafetyRequest<List<CommunityReport>>("/reports");body.Clear();foreach(var r in reports)body.Add(Ui.Card(Ui.Stack(Ui.Text(r.Description,14),Ui.Text(r.Status=="OPEN"?"待处理":r.Status=="RESOLVED"?"已处理":"未成立",12,Ui.Bronze),Ui.Text(r.Result,12,Ui.Muted))));if(reports.Count==0)body.Add(Ui.Empty("暂无举报","举报材料仅自己与管理员可见。"));}catch(Exception ex){body.Add(Ui.Text(ex.Message,12,Ui.Red));}}
}
public sealed class ExchangePage : PrivatePage
{
private readonly string id;private readonly VerticalStackLayout body=Ui.Stack();
public ExchangePage(string id){this.id=id;Title="双方证照互换";body.Padding=18;Content=new ScrollView{Content=body};}
public static async Task Request(ContentPage page,string peer)
{
if(!await CommunitySafetyUi.Write(page))return;
if(!await page.DisplayAlertAsync("申请互换证照","对方确认后,双方姓名、电话、地址、证件及营业执照照片、银行卡号和开户行会互相发送。照片自动带本次核验水印。是否同意并申请?","同意并申请","取消"))return;
var e=await AuthClient.Current.SafetyRequest<CertificateExchange>("/exchanges",new ExchangeRequest(peer,Guid.NewGuid().ToString("N"),true));await page.Navigation.PushAsync(new ExchangePage(e.Id));
}
protected override async void OnAppearing(){base.OnAppearing();try{await Load();}catch(Exception ex){body.Clear();body.Add(Ui.Text(ex.Message,13,Ui.Red));}}
private async Task Load()
{
var e=await AuthClient.Current.SafetyRequest<CertificateExchange>("/exchanges/"+id);body.Clear();body.Add(CommunitySafetyUi.Trial());body.Add(Ui.Text(e.Status=="PENDING"?"等待对方确认":e.Status=="CONFIRMED"?"双方已确认,仅用于本次交易核验":e.Status=="CANCELLED"?"已取消":"已拒绝",16,Ui.Bronze,true));
if(e.Status=="PENDING")
{
bool own=e.RequesterId==AuthClient.Current.AccountId;body.Add(Ui.Text("确认后将互发姓名、电话、地址、证照照片、卡号、开户行。请只向交易对方分享。",13,Ui.Muted));
if(!own)body.Add(Ui.Button("同意并互换双方证照",async()=>{if(await DisplayAlertAsync("确认分享资料","同意将本人认证信息和加水印证件照片发送给对方,并接收对方信息?","同意互换","取消")){await AuthClient.Current.SafetyRequest<CertificateExchange>("/exchanges/"+id,new ExchangeDecision("CONFIRM",true));await Load();}}));
body.Add(Ui.Button(own?"取消申请":"拒绝申请",async()=>{await AuthClient.Current.SafetyRequest<CertificateExchange>("/exchanges/"+id,new ExchangeDecision(own?"CANCEL":"REJECT",false));await Load();},false));body.Add(Ui.Button("刷新互换状态",Load,false));
}
body.Add(CommunityUi.TradeNotice("CERTIFICATE"));if(e.PeerCard is { } other)body.Add(Card("对方信息卡",other));if(e.MyCard is { } mine)body.Add(Card("我的已发送信息卡",mine));
}
private View Card(string title,CertificateCard card)
{
var info=Ui.Stack(Ui.Section(title),Ui.Text(card.CompanyName,16,bold:true),Ui.Text(card.Name+"\n"+card.Phone+"\n"+card.Address,14),Ui.Button("复制姓名 / 电话 / 地址",()=>Clipboard.Default.SetTextAsync(card.Name+" "+card.Phone+" "+card.Address),false),Ui.Button("单独复制姓名",()=>Clipboard.Default.SetTextAsync(card.Name),false),Ui.Text("卡号:"+card.BankNumber+"\n开户行:"+card.BankName,13),Ui.Button("单独复制卡号",()=>Clipboard.Default.SetTextAsync(card.BankNumber),false),Ui.Button("单独复制开户行",()=>Clipboard.Default.SetTextAsync(card.BankName),false),Ui.Text(card.Watermark,11,Ui.Red));
foreach(var image in card.IdPhotos)info.Add(new PrivateImage(image));if(card.BusinessPhoto is { } business)info.Add(new PrivateImage(business));return Ui.Card(info);
}
}