WPF应用程序(.NET Core)项目创建+登录布局+登录实现+注册布局+wpf弹窗消息提示+wpf加载动画
WPF应用程序框架(.NET Core)
--此博客编写WPF为.net Core型,编写.net Framework型参考博客--https://www.cnblogs.com/ZhuMeng-Chao/p/16362541.html
--WPF教程视频可以参考 https://www.bilibili.com/video/BV1nY411a7T8?p=1&spm_id_from=pageDriver
1、创建程序

2、创建成功初始信息![]()
---wpf所含文件(文件夹建立想法)
--只是在当下完成对于功能建立的文件夹--后续可扩充

3、配置基础设施
3-1:App.xaml引入materialDesign ui资源 (类似于引用element ui)
3-1-1 安装包MaterialDesignThemes

3-1-2 App.xaml中代码引入materialDesign ui资源

xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
<ResourceDictionary>
<!-- 引入materialDesign ui资源 -->
<ResourceDictionary.MergedDictionaries>
<materialDesign:BundledTheme
BaseTheme="Light"
PrimaryColor="DeepPurple"
SecondaryColor="Lime" />
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.Defaults.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
3-1-3 应用materialDesign ui资源效果(类似用element ui控件)

3-1-3 加入工具--实现代码格式化
https://zhuanlan.zhihu.com/p/345257435

4、引入prism框架
4-1 默认没有注入之类的 --所以引入第三方架构prism
4-1-1 安装prism (prism.Dryioc包)

4-1-2 代码引入prism
xmlns:prism="http://prismlibrary.com/"
4-2 配置上prism框架
4-2-1 App.xaml.cs文件中修改其基类

PrismApplication
4-2-2 修改基类后又对应错误(未配置)信息

4-2-3 解决 1: 其.cs文件对应的.xaml文件也应该修改

<prism:PrismApplication></prism:PrismApplication>
--未解决完错误信息:

4-2-4 解决 2: 其加入对应代码

1 protected override Window CreateShell() 2 { 3 //默认启动页面 4 return Container.Resolve<MainWindow>(); 5 } 6 7 /// <summary> 8 /// prism的视图和model的映射 9 /// </summary> 10 protected override void RegisterTypes(IContainerRegistry containerRegistry) 11 { 12 ////注册 view 与 viewmodel 的映射关系 13 //containerRegistry.RegisterForNavigation<LoginView, LoginViewModel>(); 14 15 ////注册 home 与 homeViewModel 的映射关系 16 //containerRegistry.RegisterForNavigation<Home, HomeViewModel>(); 17 }
4-3 创建对应登录文件完成登录---主要在讲解如何创建wpf所需窗体
4-3-1 创建 .xaml 文件
4-3-2 创建其对应ViewModel.cs 文件
目的:编写.xaml要操作的方法(增删改查)对应按钮点击事件 和 实现类似与vue的双向绑定

4-3-3 在Appp.xaml.cs中将两者(.xaml与其对应的.cs文件)建立起联系-关联(必须建立联系)

containerRegistry.RegisterForNavigation<LoginView, LoginViewModel>();
4-3-4 创建一个项目基类--继承接口,实现数据双向绑定(后续项目每一个viewModel.cs类都需要继承基类)

1 public class Class1:BindableBase 2 { 3 }
4-3-5 封装一个Http请求工具HttpRequestTools
bs项目使用ajax请求webapi cs项目使用httpclient请求webapi =》 因为使用频繁所以直接封装一个工具,用的时候直接使用该工具就可

1 public class HttpRequestTools 2 { 3 public static string apiHost = "http://localhost:5295/"; 4 public static string apiServicePrefixUrl = apiHost + "api/"; 5 public static string token = ""; 6 public static LoginUser loginUser = null; 7 /// <summary> 8 /// 执行成功的返回方式 9 /// </summary> 10 /// <typeparam name="T">返回结果类型</typeparam> 11 /// <param name="data">返回结果数据</param> 12 /// <param name="mess">返回结果信息</param> 13 /// <returns></returns> 14 protected static ResultModel<T> MyOk<T>(T data, string mess = "执行成功") 15 { 16 ResultModel<T> resultModel = new ResultModel<T>(); 17 resultModel.data = data; 18 resultModel.code = ResultCode.Ok; 19 resultModel.mess = mess; 20 return resultModel; 21 } 22 /// <summary> 23 /// 执行代码报错返回结果 24 /// </summary> 25 /// <typeparam name="T"></typeparam> 26 /// <param name="ex"></param> 27 /// <param name="mess"></param> 28 /// <returns></returns> 29 protected static ResultModel<T> MyError<T>(Exception ex, string mess = "出现错误") 30 { 31 ResultModel<T> resultModel = new ResultModel<T>(); 32 33 resultModel.code = ResultCode.Error; 34 resultModel.mess = mess + "错误信息是:" + ex.ToString(); 35 return resultModel; 36 } 37 /// <summary> 38 /// 执行没有达到预期的效果返回问题 39 /// </summary> 40 /// <typeparam name="T"></typeparam> 41 /// <param name="mess"></param> 42 /// <returns></returns> 43 protected static ResultModel<T> MyFail<T>(string mess = "执行出现问题") 44 { 45 ResultModel<T> resultModel = new ResultModel<T>(); 46 47 resultModel.code = ResultCode.Fail; 48 resultModel.mess = mess; 49 return resultModel; 50 } 51 52 /// <summary> 53 /// 发起Get请求 54 /// </summary> 55 /// <param name="url">请求Url</param> 56 /// <param name="queryParams">请求参数</param> 57 /// <param name="headerParams">请求头部参数</param> 58 /// <param name="contentType">请求内容类型</param> 59 /// <param name="timeOut">超时时间 单位:秒</param> 60 /// <returns></returns> 61 public static ResultModel<T> Get<T>(string url, Dictionary<string, string> queryParams = null, Dictionary<string, string> headerParams = null, int timeOut = 30) 62 { 63 64 if (string.IsNullOrEmpty(url)) 65 { 66 return MyFail<T>("Url不能是空的"); 67 } 68 try 69 { 70 using (HttpClient client = new HttpClient()) 71 { 72 client.Timeout = new TimeSpan(0, 0, timeOut); 73 if (headerParams != null) 74 { 75 foreach (var header in headerParams) 76 client.DefaultRequestHeaders.Add(header.Key, header.Value); 77 } 78 79 string strQueryParams = ""; 80 if (queryParams != null) 81 { 82 url = url + "?"; 83 foreach (var param in queryParams) 84 { 85 url += param.Key + "=" + param.Value + "&"; 86 } 87 url = url.TrimEnd('&'); 88 } 89 90 string response = client.GetAsync(url).Result.Content.ReadAsStringAsync().Result; 91 if (!string.IsNullOrEmpty(response)) 92 return JsonConvert.DeserializeObject<ResultModel<T>>(response); //序列化json 93 94 return MyFail<T>("返回信息为null"); 95 } 96 } 97 catch (Exception ex) 98 { 99 return MyError<T>(ex); 100 } 101 102 } 103 /// <summary> 104 /// 发起基于token的Get请求 105 /// </summary> 106 /// <param name="method"></param> 107 /// <param name="queryParams"></param> 108 /// <param name="timeOut"></param> 109 /// <returns></returns> 110 public static ResultModel<T> GetByToken<T>(string method, Dictionary<string, string> queryParams = null, int timeOut = 30) 111 { 112 if (string.IsNullOrEmpty(token)) 113 { 114 return MyFail<T>("没有请求的Token"); 115 } 116 if (string.IsNullOrEmpty(method)) 117 { 118 return MyFail<T>("method不能是空的"); 119 } 120 string url = apiServicePrefixUrl + method; 121 Dictionary<string, string> header = new Dictionary<string, string>() 122 { 123 { "mytoken",token } 124 }; 125 return Get<T>(url, queryParams, header, timeOut); 126 } 127 /// <summary> 128 /// 发起基于token的Get请求,传入参数是指定类型 129 /// </summary> 130 /// <param name="method"></param> 131 /// <param name="queryObj"></param> 132 /// <param name="timeOut"></param> 133 /// <returns></returns> 134 public static ResultModel<Result> GetByToken<Result, Params>(string method, Params queryObj, int timeOut = 30) 135 { 136 Dictionary<string, string> queryParams = ObjectToMap(queryObj); 137 return GetByToken<Result>(method, queryParams, timeOut); 138 } 139 /// <summary> 140 /// 发起Post请求 141 /// </summary> 142 /// <param name="url">请求url</param> 143 /// <param name="body">请求内容</param> 144 /// <param name="contentType">请求内容类型</param> 145 /// <param name="headerParams">请求头部参数</param> 146 /// <param name="timeOut">超时时间</param> 147 /// <returns></returns> 148 public static ResultModel<Result> Post<Result, Params>(string url, Params body, string contentType = "application/json", Dictionary<string, string> headerParams = null, int timeOut = 30) 149 { 150 if (string.IsNullOrEmpty(url)) 151 { 152 return MyFail<Result>("Url不能是空的"); 153 } 154 string json = ""; 155 if (body != null) 156 { 157 json = JsonConvert.SerializeObject(body); 158 } 159 try 160 { 161 using (HttpClient client = new HttpClient()) 162 { 163 client.Timeout = new TimeSpan(0, 0, timeOut); 164 if (headerParams != null) 165 { 166 foreach (var header in headerParams) 167 client.DefaultRequestHeaders.Add(header.Key, header.Value); 168 } 169 using (HttpContent httpContent = new StringContent(json, Encoding.UTF8)) 170 { 171 if (contentType != null) 172 httpContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(contentType); 173 174 string result = client.PostAsync(url, httpContent).Result.Content.ReadAsStringAsync().Result; 175 if (!string.IsNullOrEmpty(result)) 176 return JsonConvert.DeserializeObject<ResultModel<Result>>(result); //序列化json 177 178 return MyFail<Result>("返回信息为null"); 179 } 180 } 181 } 182 catch (Exception ex) 183 { 184 return MyError<Result>(ex); 185 } 186 187 } 188 /// <summary> 189 /// 发起带有token的Post请求 190 /// </summary> 191 /// <typeparam name="T"></typeparam> 192 /// <param name="method"></param> 193 /// <param name="body"></param> 194 /// <param name="contentType"></param> 195 /// <param name="timeOut"></param> 196 /// <returns></returns> 197 public static ResultModel<Result> PostByToken<Result, Params>(string method, Params body, string contentType = "application/json", int timeOut = 30) 198 { 199 if (string.IsNullOrEmpty(token)) 200 { 201 return MyFail<Result>("没有请求的Token"); 202 } 203 if (string.IsNullOrEmpty(method)) 204 { 205 return MyFail<Result>("method不能是空的"); 206 } 207 208 string url = apiServicePrefixUrl + method; 209 Dictionary<string, string> header = new Dictionary<string, string>() 210 { 211 { "mytoken",token } 212 }; 213 return Post<Result, Params>(url, body, contentType, header, timeOut); 214 215 216 } 217 #region 对象转成字典 218 /// <summary> 219 /// 对象转换为字典 220 /// </summary> 221 /// <param name="obj">待转化的对象</param> 222 /// <returns></returns> 223 public static Dictionary<string, string> ObjectToMap(object obj) 224 { 225 Dictionary<string, string> map = new Dictionary<string, string>(); 226 227 Type t = obj.GetType(); // 获取对象对应的类, 对应的类型 228 229 PropertyInfo[] pi = t.GetProperties(BindingFlags.Public | BindingFlags.Instance); // 获取当前type公共属性 230 231 foreach (PropertyInfo p in pi) 232 { 233 MethodInfo m = p.GetGetMethod(); 234 if (m != null && m.IsPublic) 235 { 236 // 进行判NULL处理 237 if (m.Invoke(obj, new object[] { }) != null) 238 { 239 map.Add(p.Name, m.Invoke(obj, new object[] { }).ToString()); // 向字典添加元素 240 } 241 } 242 } 243 return map; 244 } 245 #endregion 246 247 }
至此基本项目所需搭建成功
登录
4-3-4 登录页面显示布局
4-3-4.1 实现登录时页面布局上有个图片(切记第三步那里改为资源,授课时说的容易不显示)

4-3-4.2 页面布局1--图片显示


4-3-4.2 页面布局2---实现有默认提示文字
例如:布局文本框里面的提示文字--注意--需要申明一下这样布局

完成基本布局 
4-3-5 实现以弹出层方式运行该登录页面
4-3-5.1 新建一个.xmal页和对应.cs文件并将其设置为起始页,然后运行开始通过dialog方式弹出登录弹出层
--右键项目--添加--用户控件=》实现添加一个xmal页;并新建一个对应.cs文件;设置启示页+添加映射;

4-3-5.2 App.xaml中编写初始化方法--实现项目启动的时候, 首先弹出登录框,进行登录

1 /// <summary> 2 /// 初始化方法 3 /// </summary> 4 protected override void OnInitialized() 5 { 6 //项目启动的时候, 首先弹出登录框,进行登录 7 var dialog = Container.Resolve<IDialogService>(); 8 dialog.ShowDialog("LoginView", callback => 9 { 10 //如果选择的不是登录,则直接关闭程序 11 if (callback.Result != ButtonResult.OK) 12 { 13 //关闭程序 14 Environment.Exit(0); 15 return; 16 } 17 //var service = App.Current.MainWindow.DataContext as IConfigureService; 18 //if (service != null) 19 // service.Configure(); 20 base.OnInitialized(); 21 }); 22 23 }
异常报错:
解决:
1、登录页引入dialog配置

1 <prism:Dialog.WindowStyle> 2 <Style TargetType="Window"> 3 <Setter Property="Width" Value="600" /> 4 <Setter Property="Height" Value="350" /> 5 <Setter Property="SizeToContent" Value="WidthAndHeight" /> 6 <Setter Property="ResizeMode" Value="NoResize" /> 7 <Setter Property="prism:Dialog.WindowStartupLocation" Value="CenterScreen" /> 8 </Style> 9 </prism:Dialog.WindowStyle>
xmlns:prism="http://prismlibrary.com/"
2、对应cs文件添加继承

--再次运行出现错误信息

解决---对应的继承实现方法最开始先按照如图红框里的编辑(目前先可正常运行)
实现窗口显示:

4、登录页布局完善(项目引入加载转圈动画+项目引入弹窗信息提示)
1、点击登录系统按钮,有一个加载转圈的实现
2、密码或者账号输入错误有对应信息弹出提示
实现一:

1 xmlns:md="http://materialdesigninxaml.net/winfx/xaml/themes" 2 3 <md:DialogHost 4 x:Name="DialogHost" 5 DialogTheme="Inherit" 6 Identifier="Root"> 7 </md:DialogHost>
LoginView.xaml.cs文件使用prism 事件聚合器(https://www.cnblogs.com/ZhuMeng-Chao/p/16398225.html)对应“加载动画”事件实现加载转圈效果

1 /// <summary> 2 /// LoginView.xaml 的交互逻辑 3 /// </summary> 4 public partial class LoginView : UserControl 5 { 6 public LoginView(IEventAggregator aggregator, LoginViewModel view) 7 { 8 InitializeComponent(); 9 //使用事件聚合器 加载动画 10 aggregator.WaitForResgiter(arg => 11 { 12 Dispatcher.Invoke(new Action(() => 13 { 14 DialogHost.IsOpen = arg.IsOpen; 15 if (DialogHost.IsOpen) 16 DialogHost.DialogContent = new ProgressView(); 17 })); 18 }); 19 // DialogHost 20 } 21 }
创建一个放置加载转圈的那个控件xmal页面 --右键--新增--用户控件

1 <StackPanel Width="100"> 2 <ProgressBar 3 Width="40" 4 Height="40" 5 Margin="20" 6 IsIndeterminate="True" 7 Style="{StaticResource MaterialDesignCircularProgressBar}" /> 8 </StackPanel>
对应model.cs文件(LoginViewModel.cs)中对应按钮触发事件里编写使用事件聚合器里的事件

改进:改进原因--因为按上图实现是,如果没有测试休眠2秒,当登录时不会有动画显示效果--所以有问题
1、

case "login": WaitFor(true); //先显示加载动画--基类中加载动画方法调用 //1、起一个线程执行Login()登录方法 //2、---如果用之前的方式--只是写Login()--加载动画上面告诉要显示,但是他是在登录方法执行完后才会显示这个动画 //所以这种情况当时测试时我们在下面让他休眠了2秒,所以看到了转圈效果,但是去掉休眠他没有休眠,看的页面像是闪了一下,这个动画没有出来 //3、所以用一个线程,让他另起一个线程走,不然弹出动画弹不出来 Task.Factory.StartNew(() => { Login(); }); //Login(); break;
2、

if (RequestClose != null) { //App.Current.Dispatcher.Invoke 解决调用线程无法访问此对象,因为另一个线程拥有该对象的问题 App.Current.Dispatcher.Invoke(() => { RequestClose?.Invoke(new DialogResult(ButtonResult.OK)); }); }
完成效果

实现二:

1 <!-- -消息提示 --> 2 <md:Snackbar 3 x:Name="LoginSnackBar" 4 Grid.ColumnSpan="2" 5 Panel.ZIndex="1" 6 MessageQueue="{md:MessageQueue}" />
LoginView.xaml.cs文件使用prism 事件聚合器(https://www.cnblogs.com/ZhuMeng-Chao/p/16398225.html)对应“消息提示”事件实现弹出信息框显示

1 //使用事件聚合器 消息提示 2 aggregator.ResgiterMessage(arg => 3 { 4 Dispatcher.Invoke(new Action(() => 5 { 6 LoginSnackBar.MessageQueue.Enqueue(arg.Message); 7 })); 8 });
对应所需要弹框出编辑内容

实现效果

5、调用api接口实现登录
1、创建请求服务类

public class LoginService { //登录 public ResultModel<LoginUser> Login(string username, string password) { string url = HttpRequestTools.apiServicePrefixUrl + $"SystemBase/LoginManager/login?userName={username}&pwd={password}"; ResultModel<LoginUser> result = HttpRequestTools.Get<LoginUser>(url); if (result.code ==ResultCode.Ok) { HttpRequestTools.token = result.data.Token; HttpRequestTools.loginUser = result.data; } return result; } }
2、对应窗体.cs文件依赖注入请求服务类,调用其方法,实现与api控制器交互完成登录

3、实现登录
用户注册
.xaml布局
<!--Transitioner实现两个界面过渡效果-->
<!--用它要引入 xmlns:md="http://materialdesigninxaml.net/winfx/xaml/themes"-->
<md:Transitioner> <md:TransitionerSlide> 页内容 </md:TransitionerSlide> </md:Transitioner>
1、先在登录页--登录按钮下加入--用于过度到注册页的按钮---在上面布局好了的登录页上添加
注意:在触发器事件中EventName="MouseLeftButtonDown"编辑过程中有问题(暂时不清楚原因)<他有两种MouseLeftButtonUp MouseLeftButtonDown>

<!-- 注册 +忘记密码 -->
<DockPanel Margin="0,5" LastChildFill="False">
<TextBlock Text="注册账号">
<!-- 触发器 点注册切换页面 -->
<i:Interaction.Triggers>
<!-- 事件触发器 MouseLeftButtonUp MouseLeftButtonDown -->
<i:EventTrigger EventName="MouseLeftButtonDown">
<i:InvokeCommandAction Command="{Binding LoginCommand}" CommandParameter="RegisterViews" />
</i:EventTrigger>
</i:Interaction.Triggers>
</TextBlock>
<TextBlock DockPanel.Dock="Right" Text="忘记密码" />
</DockPanel>

2、在同一Xaml编辑注册页

<!-- 注册页面 -->
<DockPanel
Margin="15"
VerticalAlignment="Center"
LastChildFill="False">
<TextBlock
Margin="0,5"
DockPanel.Dock="Top"
FontSize="22"
FontWeight="Bold"
Text="注册账号" />
<TextBox
Margin="0,10"
md:HintAssist.Hint="请输入登录用户名"
DockPanel.Dock="Top"
Text="{Binding NewUsername, Mode=TwoWay}">
<!-- 是否登录名重复验证 ValidatesOnTargetUpdated="True" -->
<!-- https://blog.csdn.net/hd51cc/article/details/116668619 -->
</TextBox>
<TextBox
Margin="0,10"
md:HintAssist.Hint="请输入真实姓名"
DockPanel.Dock="Top"
Text="{Binding Newuser_full_name, Mode=TwoWay}" />
<PasswordBox
Margin="0,10"
md:HintAssist.Hint="请输入密码"
passWord:PassWordExtension.Password="{Binding NewPassword, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
DockPanel.Dock="Top">
<i:Interaction.Behaviors>
<passWord:PasswordBehavior />
</i:Interaction.Behaviors>
</PasswordBox>
<PasswordBox
Margin="0,10"
md:HintAssist.Hint="请再次输入密码"
passWord:PassWordExtension.Password="{Binding VPassword, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
DockPanel.Dock="Top">
<i:Interaction.Behaviors>
<passWord:PasswordBehavior />
</i:Interaction.Behaviors>
</PasswordBox>
<Button
Margin="0,5"
Command="{Binding LoginCommand}"
CommandParameter="Register"
Content="注册"
DockPanel.Dock="Top" />
<Button
Margin="0,5"
Command="{Binding LoginCommand}"
CommandParameter="ReturnLogin"
Content="返回登录"
DockPanel.Dock="Top" />
</DockPanel>

3、将登录页内容和注册页内容放入<md:Transitioner><md:TransitionerSlide>实现两个界面过渡效果

<!--Transitioner实现两个界面过渡效果-->
<!-- SelectedIndex这个面板可以容纳多个独立内容窗口=>切换选中页就可以 SelectedIndex从0开始,是几代表打开哪个页面 -->
<md:Transitioner Grid.Column="1" SelectedIndex="{Binding SelectedIndex}">
<md:TransitionerSlide>
</md:TransitionerSlide>
<md:TransitionerSlide>
</md:TransitionerSlide>
</md:Transitioner>
3、对应.cs文件编辑

//用来跳转到不同的界面 切换 private int _SelectedIndex; //实现通知更新--双向绑定 public int SelectedIndex { get { return _SelectedIndex; } set { _SelectedIndex = value; RaisePropertyChanged(); } } case "RegisterViews": SelectedIndex = 1; break;/*注册账号按钮 跳转注册页*/ case "Register": WaitFor(true); Register(); break;/*注册保存账号按钮*/ case "ReturnLogin": SelectedIndex = 0; break; /*返回登录页*/ //注册用户名信息 private string _NewUsername; /// <summary> /// 注册用户民获取 /// </summary> public string NewUsername { get { return _NewUsername; } set { _NewUsername = value; RaisePropertyChanged(); } } //注册用户全名信息 private string _Newuser_full_name; public string Newuser_full_name { get { return _Newuser_full_name; } set { _Newuser_full_name = value; RaisePropertyChanged(); } } //注册密码信息 private string _NewPassword; /// <summary> /// 注册密码获取 /// </summary> public string NewPassword { get { return _NewPassword; } set { _NewPassword = value; RaisePropertyChanged(); } } //再次输入密码信息 private string _VPassword; /// <summary> /// 再次输入密码信息 /// </summary> public string VPassword { get { return _VPassword; } set { _VPassword = value; RaisePropertyChanged(); } } //调用api注册保存注册信息 54节课 public async void Register() { LoginUser l = new LoginUser() { pwd = _NewPassword, CreateName = _Newuser_full_name, user_name = _NewUsername, CreateTime = DateTime.Now, user_full_name = _Newuser_full_name }; if (_NewPassword != VPassword) { aggregator.SendMessage("两次输入密码不一致"); WaitFor(false); return; } await Task.Delay(2000); //线程休眠2秒 用于测试加载动画 //调用注册服务类调用api实现注册 ResultModel<int> result = _loginService.RegisterUser(l); WaitFor(false); if (result.code == ResultCode.Ok) { if (result.data == 111) { aggregator.SendMessage(result.mess); //SelectedIndex =1; return; } SelectedIndex = 0; aggregator.SendMessage(result.mess); //RequestClose?.Invoke(new DialogResult(ButtonResult.OK)); } else { //注册失败提示... aggregator.SendMessage(result.mess); } WaitFor(false); }
4、实现登录页,注册页过渡
5、参照实现登录调用api控制器操作那样实现注册
本文来自博客园,作者:じ逐梦,转载请注明原文链接:https://www.cnblogs.com/ZhuMeng-Chao/p/16393226.html


浙公网安备 33010602011771号