Day 27 - 依 Yachts - Specification 前台页面分析拆解後,逐步建立 Specification 後台功能 - 换行跳脱字元 - ASP.NET Web Forms C#

=x= 🌵 建立 Specification Manager - Content Page 後台页面。


Yachts 前台 Specification 页面分析 :

https://ithelp.ithome.com.tw/upload/images/20211011/20139487QCDS6VRq25.jpg

📌 从 Yachts 页面的 Specification 分页内容里可以观察到 :

  1. 红色区块 : 每个游艇的特殊部位标题及多个细项规格,并且有很多组。
  2. 绿色区块 : 游艇的特殊部位标题,对应个别的细项规格。
  3. 紫色区块 : 单个细项规格内容允许换行


Specification Manager 後台页面介绍 :

🧠 下半区块 - 新增游艇共用的特殊部位标题及列表。

https://ithelp.ithome.com.tw/upload/images/20211011/20139487Zq48uA9uPZ.jpg


🧠 左上区块 - 型号下拉选单 + Layout & Deck Plan Image 组图上传。

🧠 右上区块 - 游艇部位标题下拉选单 + 部位细项细节添加。

https://ithelp.ithome.com.tw/upload/images/20211011/20139487QAUPLWazOy.jpg

  • 🌵 型号下拉选单用来决定是哪个型号的资料。

  • 🌵 细节标题下拉选单资料来源为上方下半区块的表格。



Specification Manager 後台实作 :

资料库资料表设定 :

🧠 细项规格分别对应游艇型号及游艇部位标题的 ID,并设定串联删除。

https://ithelp.ithome.com.tw/upload/images/20211011/20139487Ljcuf7eT9o.jpg

  • 🌵 Layout & Deck Plan Image 组图为图档档名 JSON 资料存在 Yachts 资料表内。

  • 🌵 游艇部位标题因为是共用所以独立一张表。


左上区块页面设计 :

🧠 型号下拉选单设定直接使用精灵设定并将值设为 id,页面设计参考如下

<h6>Yacht Model :</h6>
<asp:DropDownList ID="DListModel" runat="server" DataSourceID="SqlDataSource1" DataTextField="yachtModel" DataValueField="id" AutoPostBack="True" Width="100%" Font-Bold="True" class="btn btn-outline-primary dropdown-toggle" OnSelectedIndexChanged="DListModel_SelectedIndexChanged"></asp:DropDownList>
<asp:SqlDataSource ID="SqlDataSource1" runat="server" ConnectionString="<%$ ConnectionStrings:TayanaYachtConnectionString %>" SelectCommand="SELECT [yachtModel], [id] FROM [Yachts]"></asp:SqlDataSource>
<hr />
<h6>Layout & Deck Plan Image :</h6>
<h6><span class="badge badge-pill badge-warning text-dark">* The maximum upload size at once is 10MB !</span></h6>
<div class="input-group my-3">
    <asp:FileUpload ID="imageUpload" runat="server" class="btn btn-outline-primary btn-block" AllowMultiple="True" />
    <asp:Button ID="UploadImgBtn" runat="server" Text="Upload" class="btn btn-primary" OnClick="UploadImgBtn_Click"/>
</div>
<hr />
<h6>Group Image List :</h6>
<asp:RadioButtonList ID="RadioButtonListImg" runat="server" class="my-3 mx-auto" AutoPostBack="True" OnSelectedIndexChanged="RadioButtonListImg_SelectedIndexChanged"></asp:RadioButtonList>
<asp:Button ID="DelImageBtn" runat="server" Text="Delete Image" type="button" class="btn btn-danger btn-sm" OnClientClick="return confirm('Are you sure you want to delete?')" Visible="False" OnClick="DelImageBtn_Click"/>


右上区块页面设计 :

🧠 部位标题下拉选单设定直接使用精灵设定并将值设为 id,页面设计参考如下

<h6>Detail Title :</h6>
<asp:DropDownList ID="DListDetailTitle" runat="server" DataSourceID="SqlDataSource2" DataTextField="detailTitleSort" DataValueField="id" AutoPostBack="True" Width="100%" Font-Bold="True" class="btn btn-outline-primary dropdown-toggle" OnSelectedIndexChanged="DListDetailTitle_SelectedIndexChanged"></asp:DropDownList>
<asp:SqlDataSource ID="SqlDataSource2" runat="server" ConnectionString="<%$ ConnectionStrings:TayanaYachtConnectionString %>" SelectCommand="SELECT [detailTitleSort], [id] FROM [DetailTitleSort]"></asp:SqlDataSource>
<hr />
<h6>Add Detail :</h6>
<asp:TextBox ID="TboxDetail" runat="server" type="text" class="form-control" placeholder="Enter detail text" TextMode="MultiLine" Height="100px"></asp:TextBox>
<asp:Button ID="BtnAddDetail" runat="server" Text="Add Detail" class="btn btn-outline-primary btn-block mt-3" OnClick="BtnAddDetail_Click"/>
<hr />
<h6>Detail List :</h6>
<asp:RadioButtonList ID="RadioButtonListDetail" runat="server" class="my-3 mx-auto" AutoPostBack="True" RepeatDirection="Vertical" OnSelectedIndexChanged="RadioButtonListD_SelectedIndexChanged" Width="100%"></asp:RadioButtonList>
<asp:Button ID="BtnDelDetail" runat="server" Text="Delete Detail" type="button" class="btn btn-danger btn-sm" OnClientClick="return confirm('Are you sure you want to delete?')" Visible="False" OnClick="BtnDelDetail_Click"/>


左下区块页面设计 :

🧠 结合共用标题输入功能与新增按钮的页面设计参考如下

<h6>Add New Title :</h6>
<div class="input-group mb-3">
  <asp:TextBox ID="TBoxAddNewTitle" runat="server" type="text" class="form-control" placeholder="Enter new title" ></asp:TextBox>
  <div class="input-group-append">
    <asp:Button ID="BtnAddNewTitle" runat="server" Text="Add" class="btn btn-outline-primary btn-block" OnClick="BtnAddNewTitle_Click" />
  </div>
</div>


右下区块页面设计 :

🧠 共用标题的 GridView 表格页面设计参考如下

<asp:GridView ID="GridView1" runat="server" AutoGenerateColumns="False" DataKeyNames="id" DataSourceID="SqlDataSource3" BackColor="White" BorderColor="#CCCCCC" BorderStyle="None" BorderWidth="1px" CellPadding="3" Width="100%" OnRowDeleted="DeltedTitle" OnRowUpdated="UpdatedTitle">
  <Columns>
      <asp:CommandField ButtonType="Button" CancelText="Cancel" DeleteText="Delete" EditText="Edit" HeaderText="Edit" InsertText="Insert" NewText="New" SelectText="Select" ShowEditButton="True"  ControlStyle-CssClass='btn btn-primary btn-block' ControlStyle-BorderColor="#66CCFF" ControlStyle-BorderStyle="Solid" ControlStyle-BorderWidth="1px" ControlStyle-ForeColor="White" >
      <ControlStyle BorderColor="#66CCFF" BorderWidth="1px" BorderStyle="Solid" CssClass="btn btn-primary btn-block" ForeColor="White"></ControlStyle>
      </asp:CommandField>
      <asp:BoundField DataField="id" HeaderText="ID Number" InsertVisible="False" ReadOnly="True" SortExpression="id" >
      <ItemStyle HorizontalAlign="Center" />
      </asp:BoundField>
      <asp:BoundField DataField="detailTitleSort" HeaderText="Detail Title" SortExpression="detailTitleSort" />
      <asp:BoundField DataField="initDate" HeaderText="Creation Date" SortExpression="initDate" ReadOnly="True" InsertVisible="False" />
      <asp:TemplateField HeaderText="Delete" ShowHeader="False">
          <ItemTemplate>
              <asp:LinkButton ID="BtnDeleteTitle" runat="server" CommandName="Delete" Text="Delete" OnClientClick="return confirm('Are you sure you want to delete?')" CausesValidation="False"></asp:LinkButton>
          </ItemTemplate>
          <ControlStyle BorderColor="#66CCFF" BorderStyle="Solid" BorderWidth="1px" CssClass="btn btn-danger btn-block" ForeColor="White" />
      </asp:TemplateField>
  </Columns>
  <FooterStyle BackColor="White" ForeColor="#000066" />
  <HeaderStyle BackColor="#006699" Font-Bold="True" ForeColor="White" />
  <PagerStyle BackColor="White" ForeColor="#000066" HorizontalAlign="Left" />
  <RowStyle ForeColor="#000066" />
  <SelectedRowStyle BackColor="#669999" Font-Bold="True" ForeColor="White" />
  <SortedAscendingCellStyle BackColor="#F1F1F1" />
  <SortedAscendingHeaderStyle BackColor="#007DBB" />
  <SortedDescendingCellStyle BackColor="#CAC9C9" />
  <SortedDescendingHeaderStyle BackColor="#00547E" />
</asp:GridView>
<asp:SqlDataSource ID="SqlDataSource3" runat="server" ConnectionString="<%$ ConnectionStrings:TayanaYachtConnectionString %>" SelectCommand="SELECT * FROM [DetailTitleSort]" DeleteCommand="DELETE FROM [DetailTitleSort] WHERE [id] = @id" UpdateCommand="UPDATE [DetailTitleSort] SET [detailTitleSort] = @detailTitleSort WHERE [id] = @id">
  <DeleteParameters>
      <asp:Parameter Name="id" Type="Int32" />
  </DeleteParameters>
  <UpdateParameters>
      <asp:Parameter Name="detailTitleSort" Type="String" />
      <asp:Parameter Name="id" Type="Int32" />
  </UpdateParameters>
</asp:SqlDataSource>


後置程序码方法内容 :

1. 在 Page_Load 加入画面资料读取及渲染等相关方法如下

//宣告 List<T> 方便用 Add 依序添加资料
private List<ImagePath> savePathList = new List<ImagePath>();
protected void Page_Load(object sender, EventArgs e)
{
    if (!IsPostBack) {
        DListModel.DataBind(); //先取得型号预设选取值
        DListDetailTitle.DataBind(); //先取得细节标题预设选取值
        loadImageList(); //取得 Layout 组图
        loadDetailList(); //取得标题细节
    }
}


2. 建立 Layout & Deck Plan Image 组图上传管理 loadImageList(); 方法逻辑内容如下

#region Group Image List
private void loadImageList()
{
    //依型号取得组图图片资料
    string selectModel_id = DListModel.SelectedValue;
    SqlConnection connection = new SqlConnection(WebConfigurationManager.ConnectionStrings["TayanaYachtConnectionString"].ConnectionString);
    string sqlLoad = "SELECT layoutDeckPlanImgPathJSON FROM Yachts WHERE id = @selectModel_id";
    SqlCommand command = new SqlCommand(sqlLoad, connection);
    command.Parameters.AddWithValue("@selectModel_id", selectModel_id);
    connection.Open();
    SqlDataReader reader = command.ExecuteReader();
    if (reader.Read()) {
        //将特殊符号解码
        string loadJson = HttpUtility.HtmlDecode(reader["layoutDeckPlanImgPathJSON"].ToString());
        //反序列化JSON格式
        savePathList = JsonConvert.DeserializeObject<List<ImagePath>>(loadJson);
    }
    connection.Close();

    //渲染图片选项
    if (savePathList?.Count > 0) {
        foreach (var item in savePathList) {
            ListItem listItem = new ListItem($"<img src='/Tayanahtml/upload/Images/{item.SavePath}' alt='thumbnail' class='img-thumbnail' width='250px'/>", item.SavePath);
            RadioButtonListImg.Items.Add(listItem);
        }
    }

    DelImageBtn.Visible = false; //删除钮有选择图片时才显示
}

protected void UploadImgBtn_Click(object sender, EventArgs e)
{
    //有选择档案才执行
    if (imageUpload.HasFile) {
        //取得上传档案大小 (限制 10MB)
        int fileSize = imageUpload.PostedFile.ContentLength;
        if (fileSize < 1024 * 1000 * 10) {
            //先读取资料库原有资料
            loadImageList();
            string savePath = Server.MapPath("~/Tayanahtml/upload/Images/");
            //添加图档资料
            foreach (HttpPostedFile postedFile in imageUpload.PostedFiles) {
                //储存图片档案及图片名称
                //检查专案资料夹内有无同名档案,有同名就加流水号
                DirectoryInfo directoryInfo = new DirectoryInfo(savePath);
                string fileName = postedFile.FileName;
                string[] fileNameArr = fileName.Split('.');
                int count = 0;
                foreach (var fileItem in directoryInfo.GetFiles()) {
                    if (fileItem.Name.Contains(fileNameArr[0])) {
                        count++;
                    }
                }
                fileName = fileNameArr[0] + $"({count + 1})." + fileNameArr[1];
                postedFile.SaveAs(savePath + "temp" + fileName);
                savePathList.Add(new ImagePath { SavePath = fileName });

                //压缩图档
                var image = NetVips.Image.NewFromFile(savePath + "temp" + fileName);
                if (image.Width > 672 * 2) {
                    var newImg = image.Resize(0.5);
                    while (newImg.Width > 672 * 2) {
                        newImg = newImg.Resize(0.5);
                    }
                    newImg.WriteToFile(savePath + fileName);
                }
                else {
                    postedFile.SaveAs(savePath + fileName);
                }
                File.Delete(savePath + "temp" + fileName);
            }

              //依游艇型号更新资料
              string selectModel_id = DListModel.SelectedValue;
              //将 List<T> 资料转为 JSON 格式字串
              string savePathJsonStr = JsonConvert.SerializeObject(savePathList);
              SqlConnection connection = new SqlConnection(WebConfigurationManager.ConnectionStrings["TayanaYachtConnectionString"].ConnectionString);
              string sql = "UPDATE Yachts SET layoutDeckPlanImgPathJSON = @layoutDeckPlanImgPathJSON WHERE id = @selectModel_id";
              SqlCommand command = new SqlCommand(sql, connection);
              command.Parameters.AddWithValue("@layoutDeckPlanImgPathJSON", savePathJsonStr);
              command.Parameters.AddWithValue("@selectModel_id", selectModel_id);
              connection.Open();
              command.ExecuteNonQuery();
              connection.Close();

              //渲染画面
              RadioButtonListImg.Items.Clear();
              loadImageList();
          }
          else {
              Response.Write("<script>alert('*The maximum upload size is 10MB!');</script>");
          }

      }
}

// Layout 图片 JSON 资料
public class ImagePath
{
    public string SavePath { get; set; }
}

protected void RadioButtonListImg_SelectedIndexChanged(object sender, EventArgs e)
{
    DelImageBtn.Visible = true;
}

protected void DelImageBtn_Click(object sender, EventArgs e)
{
    //依选取项目删除 List<T> 资料
    loadImageList(); //先取得 List<T> 资料
    string selImageStr = RadioButtonListImg.SelectedValue;
    string savePath = Server.MapPath("~/Tayanahtml/upload/Images/");
    File.Delete(savePath + selImageStr);
    for (int i = 0; i < savePathList.Count; i++) {
        if (savePathList[i].SavePath.Equals(selImageStr)) {
            savePathList.RemoveAt(i);
        }
    }

    //将 List<T> 资料转为 JSON 格式字串
    string savePathJsonStr = JsonConvert.SerializeObject(savePathList);
    string selectModel_id = DListModel.SelectedValue;

    //依选取型号更新图档资料
    SqlConnection connection = new SqlConnection(WebConfigurationManager.ConnectionStrings["TayanaYachtConnectionString"].ConnectionString);
    string sql = "UPDATE Yachts SET layoutDeckPlanImgPathJSON = @layoutDeckPlanImgPathJSON WHERE id = @selectModel_id";
    SqlCommand command = new SqlCommand(sql, connection);
    command.Parameters.AddWithValue("@layoutDeckPlanImgPathJSON", savePathJsonStr);
    command.Parameters.AddWithValue("@selectModel_id", selectModel_id);
    connection.Open();
    command.ExecuteNonQuery();
    connection.Close();

    //渲染画面
    RadioButtonListImg.Items.Clear();
    loadImageList();
}
#endregion
  • 🌵 可以发现有#region#endregion,因为前面相簿建立好後就可以整组沿用。


3. 建立取得标题细节内容方法 loadDetailList(); 方法逻辑内容如下

private void loadDetailList()
{
    //取得 Model 代表 id
    string selectModel_id = DListModel.SelectedValue;
    //取得 Title 代表 id
    string selectTitle_id = DListDetailTitle.SelectedValue;

    //依游艇型号 id 及标题 id 取得 Detail
    SqlConnection connection = new SqlConnection(WebConfigurationManager.ConnectionStrings["TayanaYachtConnectionString"].ConnectionString);
    string sql = "SELECT detail FROM Specification WHERE yachtModel_ID = @selectModel_id AND detailTitleSort_ID = @selectTitle_id";
    SqlCommand command = new SqlCommand(sql, connection);
    command.Parameters.AddWithValue("@selectModel_id", selectModel_id);
    command.Parameters.AddWithValue("@selectTitle_id", selectTitle_id);
    connection.Open();
    SqlDataReader reader = command.ExecuteReader();
    while (reader.Read()) {
        string detail = reader["detail"].ToString();
        //将转成字元实体的编码转回 HTML 标签语法渲染
        ListItem listItem = new ListItem(HttpUtility.HtmlDecode(detail), detail);
        RadioButtonListDetail.Items.Add(listItem);
    }
    connection.Close();

    BtnDelDetail.Visible = false; //删除钮有选择项目时才显示
}
  • 🌵 读取资料条件设为游艇型号 ID + 部位标题 ID。

  • 🌵 这里需要进行 HtmlDecode 是因为细项资料包含 <br> 换行标签。



分别建立按钮事件功能 :

1. 先建立下半区块的共用的部位标题名称新增按钮 Add 的 OnClick 事件逻辑如下

protected void BtnAddNewTitle_Click(object sender, EventArgs e)
{
    //取得输入标题字串
    string newTitleStr = TBoxAddNewTitle.Text;
    //1.连线资料库
    SqlConnection connection = new SqlConnection(WebConfigurationManager.ConnectionStrings["TayanaYachtConnectionString"].ConnectionString);
    //2.sql语法
    string sql = "INSERT INTO DetailTitleSort (detailTitleSort) VALUES(@newTitleStr)";
    //3.创建command物件
    SqlCommand command = new SqlCommand(sql, connection);
    //4.参数化
    command.Parameters.AddWithValue("@newTitleStr", newTitleStr);
    //5.资料库连线开启
    connection.Open();
    //6.执行sql (新增删除修改)
    command.ExecuteNonQuery(); //单纯执行无回传值
    //7.资料库关闭
    connection.Close();
    //画面渲染
    GridView1.DataBind();
    DListDetailTitle.DataBind();
    //下拉选单改为选取最新项
    DListDetailTitle.SelectedIndex = DListDetailTitle.Items.Count - 1;
    //清空输入栏位
    TBoxAddNewTitle.Text = "";
}


2. 建立共用的部位标题名称列表 GridView 删除按钮 OnRowDeleted 事件刷新如下

protected void DeltedTitle(object sender, GridViewDeletedEventArgs e)
{
    //刷新下拉选单
    DListDetailTitle.DataBind();
    //刷新细节项目
    RadioButtonListDetail.Items.Clear();
    RadioButtonListDetail.DataBind();
    loadDetailList();
}


3. 建立共用的部位标题名称列表 GridView 更新按钮 OnRowUpdated 事件刷新如下

protected void UpdatedTitle(object sender, GridViewUpdatedEventArgs e)
{
    //刷新下拉选单
    DListDetailTitle.DataBind();
    //刷新细节项目
    RadioButtonListDetail.Items.Clear();
    RadioButtonListDetail.DataBind();
    loadDetailList();
}


4. 建立部位细项细节新增按钮 Add Detail 的 OnClick 事件逻辑如下

protected void BtnAddDetail_Click(object sender, EventArgs e)
{
    //取得新增 Detail
    string newDetailStr = TboxDetail.Text;
    //将换行跳脱字元改成 HTML 换行标签
    newDetailStr = newDetailStr.Replace("\r\n", "<br>");

    //依取得下拉选项的值 (id) 存入 Detail 资料
    string selectModel_id = DListModel.SelectedValue;
    string selectTitle_id = DListDetailTitle.SelectedValue;
    SqlConnection connection = new SqlConnection(WebConfigurationManager.ConnectionStrings["TayanaYachtConnectionString"].ConnectionString);
    string sql = "INSERT INTO Specification (yachtModel_ID, detailTitleSort_ID, detail) VALUES (@selectModel_id, @selectTitle_id, @detail)";
    SqlCommand command = new SqlCommand(sql, connection);
    command.Parameters.AddWithValue("@selectModel_id", selectModel_id);
    command.Parameters.AddWithValue("@selectTitle_id", selectTitle_id);
    //特殊符号要转成字元实体才能正常存进资料库
    command.Parameters.AddWithValue("@detail", HttpUtility.HtmlEncode(newDetailStr));
    connection.Open();
    command.ExecuteNonQuery(); 
    connection.Close();

    //将改成 HTML 换行标签资料加入选项渲染画面
    ListItem listItem = new ListItem(newDetailStr, newDetailStr);
    RadioButtonListDetail.Items.Add(listItem);
    TboxDetail.Text = "";
}
  • 🌵 这里需要将换行的跳脱字元r\n换成<br>换行标签。

  • 🌵 想看跳脱字元,可以安装 Notepad++,将文字贴到记事本用 Notepad++开启。


5. 建立部位细项细节删除按钮 Delete Detail 的 OnClick 事件逻辑如下

protected void BtnDelDetail_Click(object sender, EventArgs e)
{
    //依选取资料删除 Detail 资料
    string selectModel_id = DListModel.SelectedValue;
    string selectTitle_id = DListDetailTitle.SelectedValue;
    string selectDetailStr = RadioButtonListDetail.SelectedValue;
    SqlConnection connection = new SqlConnection(WebConfigurationManager.ConnectionStrings["TayanaYachtConnectionString"].ConnectionString);
    string sql = "DELETE FROM Specification WHERE yachtModel_ID = @selectModel_id AND detailTitleSort_ID = @selectTitle_id AND detail = @selectDetailStr";
    SqlCommand command = new SqlCommand(sql, connection);
    command.Parameters.AddWithValue("@selectModel_id", selectModel_id);
    command.Parameters.AddWithValue("@selectTitle_id", selectTitle_id);
    command.Parameters.AddWithValue("@selectDetailStr", selectDetailStr);
    connection.Open();
    command.ExecuteNonQuery();
    connection.Close();

    //渲染画面
    RadioButtonListDetail.Items.Clear();
    loadDetailList();
}


6. 建立部位细项细节选取改变 SelectedIndexChanged 事件逻辑如下

protected void RadioButtonListD_SelectedIndexChanged(object sender, EventArgs e)
{
    //显示细节项目删除按钮
    BtnDelDetail.Visible = true;
}


7. 建立部位标题下拉选单选取改变 SelectedIndexChanged 事件逻辑如下

protected void DListDetailTitle_SelectedIndexChanged(object sender, EventArgs e)
{
    //刷新细节选项
    RadioButtonListDetail.Items.Clear();
    loadDetailList();
}


8. 建立游艇型号下拉选单选取改变 SelectedIndexChanged 事件逻辑如下

protected void DListModel_SelectedIndexChanged(object sender, EventArgs e)
{
    //刷新全部选项
    RadioButtonListImg.Items.Clear();
    RadioButtonListDetail.Items.Clear();
    loadImageList();
    loadDetailList();
}


9. 模拟页面後测试功能,Specification Manager 後台完成 ~



本日总结 :

📢 本日後台页面,因为主要内容为细项规格,但由於细项规格是由两个下拉选单所决定,在取值跟画面的更新时,要特别注意是否刷新,另外就是多行文字记得要将跳脱字元,转换为 HTML 标签,这样才能正常呈现换行的效果。

  • 明日将介绍制作前台 Specification - Content Page 後端相关细节。

<<:  Day 27 : Python - 什麽是列表推导式?又该如何将它和if、if-else一起做使用?

>>:  世界上最快乐的人 (3) 慈心与悲心

Day 17:图与演算法

有一些演算法是在图(graph)上操作,我们可以先想一些实际的例子,例如: 开车的时候,使用导航系统...

Day29 假设我们生活在别人的AR世界 那我们是不是很有可能明天就被删除了!?

上一篇说到了,AR宠物的部分,如果我们真的能用程序模拟出动物的习性、动作,那是不是代表说我们也能模拟...

Day 21. Hashicorp Vault: Path limit

Hashicorp Vault: Path limit 在Vault里,当enable一个Secre...

我们的基因体时代-AI, Data和生物资讯 Day21- 基因注释资料在Bioconductor中物件:IRanges和GenomicRanges

上一篇我们的基因体时代-AI, Data和生物资讯 Day20-注释基因资讯的BED档案格式和bed...

30天零负担轻松学会制作APP介面及设计【DAY 12】

大家好,我是YIYI,今天我要来制作HOMEPAGE中MONTH与TODAY的切换以及右下角的加号点...