大家好,欢迎来到IT知识分享网。
学习目标
- 了解搭建一般网站的简便方式
- 了解最原始一般站点搭建
- 了解内容管理站点搭建
- 了解权限设计及完成
- 了解使用设计模式减少代码冗余
- 了解前端拖拽页面生成及生成
- 了解自定义数据的创建
- 了解动态生成的前端页如何绑定自定义数据
开发环境
- Windows7 *64 SP1
- php5.6
- apache/nginx
- thinkphp5.1
- mysql
- phpstudy2018
- sqlyog
- layoutit
声明
文章为从0到1了解内容管理系统搭建与编写,由于一篇文章内容篇幅过长,文章内容经过压缩,该项目中相同逻辑的实现只以一个实例作为描述,主要以核心关键功能的开发作为主要的讲解步骤。如有想学习完整内容系统编写可在留言区留言,我会尽快完成完整版的实战教程发布。谢谢。本篇不涉及vue、nodejs的前端框架。
知识门槛
以下内容有过一些了解即可:
- html
- sql
- php
- tp框架
面向人群
- 刚学了php不懂怎么用的同学
- 会一点点建站但是又不清楚流程的同学
- 学习完了一些框架不懂怎么使用的同学
- 有过一些web开发经验的同学等
- 希望本篇文章对每一个阅读完的同学都有帮助
注意:本篇文章部分细节由于篇幅关系并不会去深入完善,并且相同逻辑的实现只以一个实例作为描述,主要以核心功能的开发作为主要的讲解步骤。本篇不涉及vue、nodejs的前端框架。
一、 了解一些专业术语及概念
在了解搭建网站前,需要普及一些基本的知识概念,防止某些同学在一方面有概念性的错误,并且我个人认为在学习一方面知识前需要对这一方面的知识有一个广度的了解,这里所指的广度为这东西是用来干什么的、作用是什么、为什么要这样写;所以在正式开始介绍如何编写CMS前将会介绍这一部分内容。为了方便阅读第一点内容引入我另外一篇原创文章。
1.1了解浏览一个网页的基本流程方式
在学习一门技术的时候,往往是了解整体体系架构才能更好的学习,不然在学习的过程中会出现不知道为什么这样做,做出这一部分是该整体部分的哪个区域,只会跟着做,但是并不了解这是在干啥。可能一些萌新体会颇深,就照着打,老师教怎么写,我就怎么写,反正做出来了。
本篇博文,就来用最接地气的方式对基本的web开发做一个整体的讲解,带各个萌新过一遍web开发的流程,好让各位萌新知道学习的时候学习了什么知识点,这个知识点能够干哈。
最开始,我们就以个人浏览网站的方式给大家说一下这一个过程是如何运作的。
我们访问网站,一般先打开浏览器(不要杠),输入一个网址,随后浏览器打开一个网页。在你在请求这一个网址数据的时候,已经发生了一系列的操作。
1.2了解IP地址
假设你输入的是“csdn.net”,浏览器想要去访问你这个网站,首先需要的是获得你这个网站的IP地址。可能就有萌新问了**“什么是IP地址?”。IP地址就是“指互联网协议地址,或者说网际协议地址”。又有萌新说了“你这么说我怎么懂?”**,好了现在容我慢慢道来。
IP地址就是在网络中,定位你这台电脑,或者说是设备的一个标记,这个标记是人们指定好的标准协议而产生的(协议就是你和我说好了一件事,拉钩了,以后要这样做)。就像你家的门牌号例如叫做“CSDN市,CSDN区,CSDN街道的CSDN小区第CSDN栋的第CSDN号”…这是由有关机构制定的一套规范名称,不允许随意更改;我们换个例子,例如你家是“深圳市南山区深南大道某某小区第八栋808”,你写快递的收件地址肯定是写这个,难道你写“宇宙第一星球第一市第一栋第一号”?地址是由专门组织规范且制定的一套定位规范,遵循这个规范可以使遵循该规范的设备或者人之间相互通信,这个通信指可以传达交互,能够定位、找到。综上所述,IP地址就不要纠结为什么要这样写,只要知道这个IP地址是你要用的就行。
1.3了解DNS
现在IP地址知道是什么了,那么怎么获得IP地址?这个时候就需要用到DNS了,啥是DNS??!!
DNS的英文全称是 Domain Name System,翻译过来就是域名系统。好了,这个时候问题又来了。
1.4了解域名
啥是域名?域名就是用来标识IP地址的一个标记,或者说是昵称。“为什么不直接用IP地址?”这个问题问得好,如果我们人不用名称,就用身份证号,我叫你的时候就会叫“450333333333333333…”。。。我觉得这样不是很好。。。当人们觉得使用IP地址不方便记忆后,就产生了域名地址,就像CSDN,我们就知道是CSDN就好了,难道还要去记她的IP地址吗?例如CSDN的地址是192.168.1.1,难不难受…以后可能你记网站名称就在记数字了,又不方便又崩溃。好了,回归正题,我们输入了网址后,按下Enter键后,浏览器将会去DNS请求这个域名对应的IP是什么,如果找到了,就返回一个IP地址。可能又有萌新问了,“浏览器会自动去找DNS?”,会是会,但是我们也会给它一个目标,在我们的网络连接里面,本地连接右键属性,里面有个IPV4,双击进去就可以查看自己配置的DNS了,一般别乱改,不然很难过的,有时候浏览器打不开网址,就是这个原因。
记住,网络IP冲突可能会导致上不了网,这种情况在学校的机房里很常见,只要改成自动获取IP就ok了,会自动分配闲置的IP地址。
1.5 了解数据请求
当找到了IP地址,这个时候就会向该IP地址的设备去请求数据,请求数据的意思就是,这个设备或者说服务器就像一个大型的分发机构,就是送情报的一个部门,一共有65535个窗口,每个窗口送不同的情报;例如我们需要请求网站之类的数据,就通过第80个窗口请求,这个时候浏览器派来的小弟来到这个80号窗口,可能会排一下队,拿到数据后,回到浏览器,浏览器把拿到的数据显示给你看。
1.6 了解“ 渲染”
其实在这个时候,浏览器显示的数据会根据一些标记,进行排版,这些标记就称是HTML,HTML是 Hyper Text Markup Language 的缩写,中文名是超级文本标记语言,其实说那么深奥还不方便理解;简单来说就是通过特定的标签,把一段文本信息标记起来,表示这段文本信息要怎么样去进行显示,或者是这个文本信息是啥东西;例如 <title>CSDN-专业IT技术社区</title>
是CSDN官网首页的标题,用了title这个标签把文本信息标记,标记好后,浏览器就知道这个文本要显示在哪里,要怎么进行显示,最终浏览器把这一段信息显示在了浏览器标题头位置:
我们再看看另外的一个例子:
这一段HTML语言所标记了一个博客的文本,整个标记的情况为了清晰的看清楚,我在这里列出:<a href="//blog.csdn.net/" class="toolbar_to_feed" title="博客">博客</a>
,标记语言HTML那一些标记并不会进行显示,只显示了博客这个这个文本在网页上:
那是因为浏览器是通过标记语言的内容去进行显示,标记语言的作用就是告诉浏览器这里你要怎么显示这个内容,或者说这个内容有什么功能。这里是博客的一个跳转,使用的是a标签,a标签是什么?a标签就是<a>这里是要显示的文本</a>
,在a标签里面可以添加一些固定的操作,例如a标签的作用是跳转到指定的页面,那么这个页面肯定是有一个链接的,那么这个链接需要什么来指定呢?
答案就很简单了,使用href来指定,这个href呢就需要把要跳转到的页面的地址给加上,在我们查看到的HTML代码中是href="//blog.csdn.net/"
,这就表示会跳转到blog.csdn.net
这个地址,有人点击就会跳转到博客了。
那 class="toolbar_to_feed"
是什么东西?在这里我们可以把它当做给定了一个样式,给定了一个style,要怎么样显示,你要显示的样子是什么?可能红色的底,绿色的字,俗话说,红配绿。。。这个样式的名称就叫做 toolbar_to_feed 。在这里并不会深入的讲解这个样式要让博客这个文本显示成啥样,大家只要通过例子知道这个html是用来告诉浏览器怎么样显示这个文本,或者这个文本有什么用就ok了。其实还有些动态的数据,但是在这里并不会讲解,基本的理解这样就没问题了。专业点的说法就是构件编排用户界面。
1.7 了解前端
通过以上描述就很清楚的知道,如果我们做web开发的话,做html相关的就是给页面制作布局,怎么样好看,甚至可以做特效,让页面显示多姿多彩;一般我们称做HTML这种,是为了数据的显示的排版工作,或者说是为了包装数据工作的这类职位叫做前端;不过前端是个相对概念,在web上可以这样理解是没问题的,不过现在的前端,如果不去大厂,基本上要做的不止是包装数据的排版那么简单,可能还会做得更多。如果我们去做前端工作的话,还要掌握跟服务器交互的一些操作,打个比方,用户点击了一个按钮,这个按钮的功能是获取到你们的用户人数,这个时候你需要编写一个逻辑,去服务器获取到这个用户想要的数据。不过这点只是作为一个提醒,当真正接触前端的话会了解的。
1.8 了解后端
有很多小问号的朋友可能会记得刚刚说的,前端可能要向服务器请求数据,那么这个数据,是不是就是传说中的后端做的?(听没听过后端某问题,反正就是后端)
后端可以理解为一些业务逻辑的代码编写实现,就是需要后端,什么是业务逻辑?简单的举个例子,就像你淘宝买东西,你点了这个物品,下单了,我要在代码上怎么实现这个下单这个背后的操作;因为下单后你还需要交易,交易要收钱,收钱你还要把这个记录记载到你存放数据的地方,我们可以叫做数据库,存进去后,用户查看自己的下单记录,你还需要把这个记录取出来,用代码实现这个取出来这个过程给用户看到,不然没有记录那就很尴尬了,只收钱不卖货!流批!所以一般是指的是数据库(因为要存储数据,例如你网站的用户数据,肯定要用东西来存储,这个东西就是数据库)进行交互以处理相应的业务逻辑。虽然后端要考虑很多东西,但是一般来说这样举例子就比较方便理解,就不过多的谈论其它东西了。
现在整个逻辑基本上就通了,简单的理解,后端就是实现一些数据操作,业务逻辑的实现(其实可能会运维),前端呢就是负责用户的页面数据的展示排版;嗯,大体这样理解问题不大。
1.9 了解建站
既然理解通了,我们就来说说一个网站搭建的流程是什么吧!
首先我们需要租一个服务器,嗯…这个萌新不理解,那我们降一个档次,那就是我们在我们自己的本地电脑进行试验,这样就问题不大了,方便快捷。
搭建一个简单企业门户网站其实贼简单,不吹不黑,几年前的时候,做这个还是挺得钱的,接接外包,舒舒服服,现在就不行了,毕竟技术在更新,过时的技术也变得更加廉价了,但是依旧是基本。
以下我使用一个静态网站作为例子演示一个网站的搭建;“啥是静态网站?”。静态网站就是没有后端,好吧,简单来说就是这样,由于后端需要一些其它语言,本篇博文针对于普遍人群,为了方便理解就不用后端了,直接静态网站作为演示,列出html的代码,到时候萌新们可以直接复制代码拿去自己试验,舒舒服服,美滋滋。
1.10 了解集成环境
首先我们下载一个集成环境。“啥是集成环境?”。
集成环境打个比方,就像你做菜、需要火源、锅、锅铲,这种就是环境;我做网站也要一个环境,这个环境有人给你做好了,你直接拿过来用就好,就不需要自己搭建,有些初学者就喜欢自己搭建,然后发现一堆问题,搞着搞着发现太难就不学了,简直嘤嘤嘤!初学者我个人建议先别增加自己的难度,先学,不然没搞懂就上会一脸懵圈的。现在我们下载一个叫做phpstudy的软件,下载点这里
去官网。然后进行傻瓜式安装。
安装完后打开服务:
Apache可能会有人问是什么,Apache是服务器软件,它就是你做菜需要的必要工具之一,开启了就对了,可能你只开启Apache只能做汤,那也没事,毕竟我现在演示的是静态网站。
首先我们把我们的资源文件带到网站根目录下:
根目录不会找?没关系,我们打开网站,点击管理找到根目录就ok:
找到后把资源文件放到根目录下,删除以前的根目录下的内容即可。
然后在浏览器输入:http://127.0.0.1/ 或者输入 http://localhost/ 就可以访问我们本地电脑上的网站了!
二、给所搭建的静态网站添加后端
在以上第一节内容中,我们已经做好了一个静态的网站,但该网站并没有一些后台功能。例如后台设置网页的所展示的内容,那为什么要后台设置网页展示的内容呢?当我们的网站成功架设后,假设该网站是双十一的推广网站,图片这些全部都是标有双十一字样,当双十一过后该网站难道就不能继续使用了吗?答案当然是不,只需要编写一管理后台,用户在后台中可自由设置图片要显示哪一张。该功能完成后,用户可根据自己的需要更改对应的图片;既然图片都可以更改了,那么文章也同样可以更改,这时网站的自由度将会更高。
更改网站图片的显示与更改文字内容的显示都需要使用数据库,当然其它方式也可以,但我们在这里使用一种较为常规与成熟的数据库方式进行存储,并且使用一个php的开发框架thinkphp来方便我们的搭建。thinkphp的版本是5.1版本。可能有些小伙伴们问为什么要使用框架?这不是增加学习成本吗?其实使用框架并不会增加你的开发时长,并且会增加你的开发效率;框架就像搭建房子时的地基,直接使用一个地基比你自己再去做一个地基更加简单方便,而且更为标准;如果你是一个新手,自己去搭建一个地基,往往会做到一半就“塌”了,这种情况也不是不可能。
2.1 了解thinkphp5.1 的使用
首先我们下载thinkphp5.1,解压后目录如下:
目录参考可以根据thinkpp5.1手册:
thinkphp5.1的目录结构在本文并不需要了解过多,本文将会说明需要了解的目录。
我们复制解压出来的文件至网站根目录下,并且删除原有网站根目录下的内容:
由于thinkphp框架的入口在public目录下,我们打开public目录进行查看:
在public目录下找到了index.php文件。由于该框架的入口文件是index.php,需要更改网站的根目录为public。打开phpstudy,依次点击其它菜单选项->软件设置->端口常规设置:
在弹出来的根目录设置中,选择public作为根目录:
此时输入localhost进行访问:
出现如上示例则表示当前thinkphp部署成功。接下来就可以进行相应的代码编写了。
2.2 完成第一节静态网站的移植部署
在第一节中,我们实现了一个静态网站的搭建,现在将第一节编写好的静态网站index.html文件复制到如下路径中:
我的目录是 E:\devlop\phpstuy\PHPTutorial\WWW\application\index\view\index,如果没有该目录可以自己创建。我们浏览器再次输入localhost查看,发现依旧出现之前的web页提示,这是什么回事呢?因为我们需要在thinkphp的控制器中,添加一行跳转到该html文件的代码。控制器文件在 E:\devlop\phpstuy\PHPTutorial\WWW\application\index\controller 下:
该目录是存放当前模块下所有控制器的地方(当然你可以不这样),控制器在thinkphp框架中用于对用户访问进行控制,例如用户需要访问首页则需要访问首页的控制器,默认是index控制器;index控制器可以对index这个页面进行逻辑控制,可以传值、权限控制等一些列操作。换句话说则是控制用户访问指定资源的逻辑(不理解也没关系)。我们打开index.php这个控制器:
<?php
namespace app\index\controller;
class Index
{
public function index()
{
return '<style type="text/css">*{ padding: 0; margin: 0; } .think_default_text{ padding: 4px 48px;} a{color:#2E5CD5;cursor: pointer;text-decoration: none} a:hover{text-decoration:underline; } body{ background: #fff; font-family: "Century Gothic","Microsoft yahei"; color: #333;font-size:18px} h1{ font-size: 100px; font-weight: normal; margin-bottom: 12px; } p{ line-height: 1.6em; font-size: 42px }</style><div style="padding: 24px 48px;"> <h1>:)</h1><p> ThinkPHP V5.1<br/><span style="font-size:30px">十年磨一剑 - 为API开发设计的高性能框架</span></p></div><script type="text/javascript" src="http://tajs.qq.com/stats?sId=9347272" charset="UTF-8"></script><script type="text/javascript" src="http://ad.topthink.com/Public/static/client.js"></script><thinkad id="ad_bd568ce7058a1091"></thinkad>';
}
public function hello($name = 'ThinkPHP5')
{
return 'hello,' . $name;
}
}
改php控制器默认index为访问方法,index方法将会返回一条html的字符串,改字符串通过渲染将会显示成我们之前所看到的部署成功的欢迎界面。在这里需要将该代码删除。换成:
return $this->view->fetch();
整个php文件则为:
<?php
namespace app\index\controller;
use think\Controller;
class Index extends Controller{
public function index()
{
return $this->view->fetch();
}
public function hello($name = 'ThinkPHP5')
{
return 'hello,' . $name;
}
}
return $this->view->fetch(); 我们可以查看thinkphp5.1手册:
使用 fetch 方法将会自动定位到模板文件。thinkphp已经帮我们写好了一定的规则,自动定位到默认view目录下对应控制器名下的index文件。在此注意,是自动定位到view目录下与控制器同名的目录下的文件,不加参数自动定位到index.html,也就是view/控制器名/index.html,由于控制器名是index,则是view/index/index.html;view目录下的index目录则是之前复制静态网站html文件的目录。
保存php文件,访问localhost:
这时发现整个web页错乱,这时因为所有css文件、js文件、img文件的路径都有所改变,这时需要更改到正确的资源加载目录。为了方便加载,在网站根目录public目录下新建一个home目录,复制该页面所需的资源文件到该目录下:
网站根目录资源的访问路径是“/”表示网站根目录下,由于在根目录下创建了一个home目录,则进一步可以写为“/home/”,在home目录下有一个asset,则可以写为“/home/assets/”,assets下的文件访问则可以根据目录进行具体访问,例如asset下的目录img有一个图片叫做1.png,那么访问则可以写成“/home/assets/img/1.png”。
了解了访问的规则后,修改index.html文件,将所有 assets/ 都更换为 /home/assets/,我使用的编辑器是 vscode,快捷键 ctrl+h 即可调出一键替换:
点击如上图中的一键替换即可完成资源内容的目录修改,随后保存,再次访问:
完美呈现,是不是贼爽?那么接下来就实现这些图片资源的可后台更换。
三、完成后台模块的编写
3.1 完成管理后台模块搭建
首先复制application目录下的index目录:
更改index-副本名为admin:
随后更改admin目录下controller目录中的index.php文件内容,原文件内容如下:
<?php
namespace app\index\controller;
use think\Controller;
class Index extends Controller{
public function index()
{
return $this->view->fetch();
}
}
更改为:
<?php
namespace app\admin\controller;
use think\Controller;
class Index extends Controller{
public function index()
{
return $this->view->fetch();
}
}
以上的内容主要更改在命名空间,从 namespace app\index\controller;更改为了 namespace app\admin\controller;。命名空间主要是为了区分不同区域或空间内的不同“东西”。例如学校中A班的小明与B班的小明,这两者有着班别的区别,命名空间也是如此,表示不同区域不同空间内的值。
更改完成后访问 http://localhost/index.php/admin/index,这行url地址表示该网站中admin模块下的index方法,其中index.php在访问首页的时候是默认隐藏,即http://localhost/index.php等于localhost,由于当下访问其他模块在此需要写全(当然可以配置隐藏,但不是本节内容则不过多增加难度)。访问后发现该页面与访问localhost出现的内容一致,这是因为admin模块中的index方法也用了return $this->view->fetch();这一行代码输出了html文件的代码,这个html文件并不是index模块下的view/index下的index.html,而是admin模块下的view/index下的index.html,因为刚刚整个模块我们都进行了复制。这时该html不符合我们的需求,需要更换html内容,在此我使用了一模板(该模板编写是前端内容,在此并不过多赘述,实现逻辑与index.html类型,均是修改页面的资源路径即可),访问效果如下:
注:本节项目代码将会打包分享给大家。
3.2 完成数据库的导入
完成后台管理页的搭建后,发现该后台所有用户均可访问,这对于一个网站是不好的权限行为;必须实现可控的权限管理,使得网站内容不得随意更改。
首先打开sqlyog,输入数据库的帐号密码,一般帐号为root密码为root或空:
连接成功后,邮件你本地数据库点击创建数据:
输入数据库名,我创建数据库名为minimalism_cms,并且选择字符集,字符集为utf8即可,点击创建:
在出现的新建数据库中,选择创建表:
输入表信息如以下:
以上所有所需的数据库表我将会导出sql文件,同学们使用时在数据库导入即可,导入步骤如下:
在对应数据库中右键选择导入点击执行sql脚本即可。
导入完将会出现如下的数据库表:
以上数据库表考虑排错等操作并没有过多约束。
3.3 完成权限内容添加功能编写
权限管理首先需要有账户,账户属于什么角色,该角色又有什么权限,这是实现权限管理的思想。例如有个账户名为admin,admin属于超级管理员这个角色,该角色拥有所有的权限。接下来首先创建管理员用户。
在权限管理下拉列表中选择管理员管理进入页面:
我们查看url连接:http://localhost/index.php/admin/auth/adminauth.html
以上链接中,admin表示admin这个模块,auth表示控制器,adminauth表示方法名;auth控制器我们还未创建,在admin模块下的index控制器同目录创建一个名为Auth.php文件,内容如下:
<?php
/** * |----------------------- * | 页面跳转 * |----------------------- */
namespace app\admin\controller;
use think\Controller;
class Auth extends Controller{
//Auth 管理首页
public function adminAuth(){
return $this->view->fetch();
}
}
通过以上控制器,可以使url连接访问到该控制器并且访问adminAuth所对应的html文件,该html对应的文件在view目录下的auth目录中。在thinkphp中,对应的view目录根据控制器名分配,Auth控制器需要一个名为auth的目录存放该控制器下的html文件;在auth目录下创建一个名为admin_auth的html文件,为什么要名为admin_auth?thinkphp会访问方法名默认控制器对应的目录中一同方法名的html文件,如方法名有大写,则表示在该名称前有一下划线,则adminAuth则为admin_auth。该html代码将会打包下载即用。
点击添加,添加管理员进入页面:
该url为:http://localhost/index.php/admin/auth/adminadd.html
在控制器中添加方法:
<?php
/** * |----------------------- * | 页面跳转 * |----------------------- */
namespace app\admin\controller;
use think\Controller;
class Auth extends Controller{
//Auth 管理首页
public function adminAuth(){
return $this->view->fetch();
}
//管理员添加页
public function adminAdd(){
return $this->view->fetch();
}
}
该页面拥有管理员账户、管理员密码、名称及角色组内容。暂时我们并没有角色组,首先创建一个管理员账户。查看html中的关键代码:
<form class="form-horizontal" role="form">
<div class="form-group">
<label class="col-md-2 control-label">管理员账户</label>
<div class="col-md-10">
<input id="user" type="text" class="form-control" placeholder="帐号">
</div>
</div>
<div class="form-group">
<label class="col-md-2 control-label">管理员密码</label>
<div class="col-md-10">
<input id="password" type="password" class="form-control" placeholder="密码">
</div>
</div>
<div class="form-group">
<label class="col-md-2 control-label">名称</label>
<div class="col-md-10">
<input id="realname" type="text" class="form-control" placeholder="输入名称或代号">
</div>
</div>
</form>
通过以上html得知,id为user是账户,id为password为密码,id为realname为真实姓名。在此使用ajax进行数据提交到php后台实现内容访问。查看ajax代码:
<script> function add(){
var user=$('#user').val(); var password=$('#password').val(); var realname=$('#realname').val(); $.ajax({
type:'post', url:'/index.php?s=/admin/Authpost/adminAdd/', data:{
"user":user,"password":md5(password),"realname":realname,"group":group}, dataType:"json", success:function(data){
if(data.success==1){
alert(data.msg); }else{
alert(data.msg); } },error:function(jqXHR){
} }) } </script>
从以上ajax代码中,使用jq获取了id为user、password、realname元素的值,在此并没有做检查是否合规,希望小伙伴们在使用该代码的时候注意。在获取密码时使用了md5加密,md5我是在线引入的,引入如下:
<script src="https://cdn.bootcss.com/blueimp-md5/2.10.0/js/md5.js"></script>
获取值后使用ajax传递给 /index.php?s=/admin/Authpost/adminAdd/ 这个url地址。该地址使用了兼容模式,因为担心一些同学本地环境有问题,所以特地在此使用该模式进行传值。该模式的格式为:http://serverName/index.php(或者其它应用入口文件)?s=/模块/控制器/操作/[参数名/参数值…]。则admin为模块名,Authpost表示控制器名,adminAdd表示控制器中的方法。我们在admin的控制器目录创建一个名为Authpost的控制器,并且编写adminAdd方法,代码如下:
<?php
/** * |----------------------- * | 对数据库操作 * |----------------------- */
namespace app\admin\controller;
use think\Controller;
use think\Db;
use think\facade\Request;
use app\admin\model\Admin;
use app\admin\code\ReturnCodeInfo;
class Authpost extends Controller{
//administartor add
public function adminadd(){
$request_data = Request::post();
$data['password'] = md5(trim($request_data['password']));
$data['username']=$request_data['user'];
$data['realname']=$request_data['realname'];
$data['group']=$request_data['group'];
$data['logintime'] = time();
$data['create_time'] = time();
$data['loginip'] = Request::ip();
$data['status'] = 1;
$res = Admin::create($data);
if($res){
return json((new ReturnCodeInfo())->actionSuccess());
}else{
return json((new ReturnCodeInfo())->actionError());
}
}
}
先不看以上代码,我们查看需要存储值的数据库字段有哪些:
通过表得知,数据库字段包括 username、password、logintime、loginip、realname、create_time。我们接收值需要设定这几个初始字段,Authpost 控制器adminadd方法中这部分代码为:
$request_data = Request::post();
$data['password'] = md5(trim($request_data['password']));
$data['username']=$request_data['user'];
$data['realname']=$request_data['realname'];
$data['group']=$request_data['group'];
$data['create_time'] = time();
$data['loginip'] = Request::ip();
$data['status'] = 1;
以上代码使用了 Request::post();接收post值,在使用Request时必须引用use think\facade\Request;;随后将值赋给$request_data变量。随后使用 $data 变量存储即将要存储到数据库的值。在存储password密码时使用了md5加密,提高安全性。最后使用模型的create方法将数据库的值存储:
$res = Admin::create($data);
模型方法可以方便的使值进行存储。模型对应的是一个数据库,例如我数据库名为tp_admin,设置前缀为tp_后可以直接创建一个名为Admin的模型,其实也就是名为Admin的php文件,文件中类名也为Admin,该类集成model基类故此有模型特性。创建模型的方法如下,在admin下的controller同目录,注意是同目录创建一个model文件夹,在该文件夹下创建一个Admin的php文件,内容如下:
<?php
namespace app\admin\model;
use think\Model;
class Admin extends Model {
}
创建完成后,在需要使用到该模型的文件中引入,我们在Authpost头部引入 ,添加代码:
use app\admin\model\Admin;
其实以上代码是通过模型所在目录进行引入,app表示根目录,根目录下的admin模块中model目录下的Admin模型。
在引入后还差很关键的一步,需要配置数据的连接。
在application目录下config文件夹中找到database.php文件,打开修改hostname为127.0.0.1或者是localhost、修改database为我们创建的数据库的名称例如minimalism_cms、修改username帐号为root、修改password密码为root。配置内容如下:
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
return [
// 数据库类型
'type' => 'mysql',
// 服务器地址
'hostname' => '127.0.0.1',
// 数据库名
'database' => 'minimalism_cms',
// 用户名
'username' => 'root',
// 密码
'password' => 'root',
// 端口
'hostport' => '',
// 连接dsn
'dsn' => '',
// 数据库连接参数
'params' => [],
// 数据库编码默认采用utf8
'charset' => 'utf8',
// 数据库表前缀
'prefix' => 'tp_',
// 数据库调试模式
'debug' => true,
// 数据库部署方式:0 集中式(单一服务器),1 分布式(主从服务器)
'deploy' => 0,
// 数据库读写是否分离 主从式有效
'rw_separate' => false,
// 读写分离后 主服务器数量
'master_num' => 1,
// 指定从服务器序号
'slave_no' => '',
// 自动读取主库数据
'read_master' => false,
// 是否严格检查字段是否存在
'fields_strict' => true,
// 数据集返回类型
'resultset_type' => 'array',
// 自动写入时间戳字段
'auto_timestamp' => false,
// 时间字段取出后的默认时间格式
'datetime_format' => 'Y-m-d H:i:s',
// 是否需要进行SQL性能分析
'sql_explain' => false,
// Builder类
'builder' => '',
// Query类
'query' => '\\think\\db\\Query',
// 是否需要断线重连
'break_reconnect' => false,
// 断线标识字符串
'break_match_str' => [],
];
修改完成后保存。
修改完成后查看 adminadd 方法中:
if($res){
return json((new ReturnCodeInfo())->actionSuccess());
}else{
return json((new ReturnCodeInfo())->actionError());
}
以上代码为返回通用的操作返回值码,减少代码冗余。使用 json返回json值,该值为new ReturnCodeInfo中的actionSuccess方法返回值,代码内容为:
<?php
namespace app\admin\code;
class ReturnCodeInfo{
//验证器成功代码 10001,错误 10002
private $validate_success=10001;
private $validate_error=10002;
private $validate_info='验证成功';
//数据库存
private $action_success=10003;
private $action_error=10004;
private $action_sinfo='操作成功';
private $action_einfo='操作失败';
//验证器 Code
public function validataSuccess(){
return ['code'=>$this->validate_success,'msg'=>$this->validate_info];
}
public function validataError($msg){
return ['code'=>$this->validate_error,'msg'=>$msg];
}
//规则 Code
public function actionSuccess(){
return ['code'=>$this->action_success,'msg'=>$this->action_sinfo];
}
public function actionError(){
return ['code'=>$this->action_error,'msg'=>$this->action_einfo];
}
}
以上类定义了操作失败或成功的返回状态,方便之后的操作调用该状态码。该php文件我写在controller同目录下的code目录中,名为ReturnCodeInfo的php文件。则在Authpost代码中使用了如下代码引入:
use app\admin\code\ReturnCodeInfo;
接着我们返回到html中,在提交按钮上绑定onclick事件,当然你使用别的方式也行,代码如下:
<button type="button" onclick="add()" class="btn btn-success btn-bordered waves-effect w-md waves-light m-b-5">提交</button>
随后在页面中填入内容:
点击提交:
操作成功。
我们按照如上方式创建角色组的创建,点击角色组管理:
角色组实现逻辑与管理员实现逻辑类似,不再赘述。点击添加进入添加页:
填入组名后点击提交,操作成功:
该逻辑实现一致,均是创建模型后进行数据插入。
随后开始添加规则,进入规则添加页:
点击提交进行添加。
上述相同过程完成后开始实现权限认证的逻辑。
接下来完全用户对角色组的绑定,进入管理员管理页,点击编辑:
进入编辑页后选择超级管理员:
点击提交,完成绑定:
提交方法使用ajax,所访问的接口为Authpost下的groupBindUser方法:
//组绑定用户
public function groupBindUser(){
$request_data = Request::post();
$data['uid']=$request_data['uid'];
$data['group_id']=$request_data['gid'];
$data['create_time']=$data['update_time']=time();
//存储
$res = AuthGroupAccess::create($data);
if($res){
return json((new ReturnCodeInfo())->actionSuccess());
}else{
return json((new ReturnCodeInfo())->actionError());
}
}
该方法主要使用AuthGroupAccess模型调用create方法进行数据插入。数据库存储如下:
为用户id对应的组id。
随后进入组页面,进行组绑定规则的操作,点击编辑进入超级管理员编辑页:
选择需要的规则,点击提交完成规则与组的绑定:
数据库存储如下:
以上表中,id为组id,rules则为规则的id。
3.4 完成权限管理逻辑编写
为了使验证层能够灵活的使用,在admin目录下创建一个AuthRuleValidate目录,新建一php文件名为AuthRuleValidateBase,内容如下:
<?php
namespace app\admin\AuthRuleValidate;
use think\Controller;
use think\Db;
class AuthRuleValidateBase extends Controller{
//传入uid 与当前 路由验证是否有此权限
public function check($uid,$access){
$res=Db::table('tp_admin')
->alias('a')
->field('rules')
->join('tp_auth_group_access agc','a.id = '.$uid)
->join('tp_auth_group ag','ag.id = agc.group_id')
->find();
$rules=Db::name('auth_rule')->field('rule')->where('id','in',$res['rules'])->select();
$rules=array_column($rules, 'rule');
in_array($access,$rules)?:$this->error('权限不足');
}
}
逻辑很简单,该方法接受当前的uid用于查询用户所属组,改组拥有的规则,再通过规则与当前规则进行匹配,如含有则表示拥有该权限。
首先查询tp_admin管理员表所在的组:
$res=Db::table('tp_admin')
->alias('a')
->field('rules')
->join('tp_auth_group_access agc','a.id = '.$uid)
->join('tp_auth_group ag','ag.id = agc.group_id')
->find();
得到结果后,查询与改组id匹配的规则,最后判断该权限是否在当前的规则内,是的话不做任何操作,否则提示权限不足。
随后在controller控制器目录下创建一基类php文件,名为Base。内容为:
<?php
namespace app\admin\controller;
use think\Controller;
use think\facade\Session;
use app\admin\AuthRuleValidate\AuthRuleValidateBase;
class Base extends Controller{
protected $beforeActionList = [
'ruleCheck'=>['except' => 'login']
];
protected function ruleCheck()
{
session('?admin')?:$this->error('未登录或已失效','Index/login');
$AuthRuleValidate=new AuthRuleValidateBase();
$s=session('admin');
/*echo request()->module().'/'.request()->controller().'/'.request()->action(); */
$AuthRuleValidate->check($s['id'],strtolower(request()->controller()).'/'.strtolower(request()->action()));
}
}
该文件引入了刚刚创建的权限判断类,在此基础上并且判断了该用户是否登录。
查看代码:
protected $beforeActionList = [
'ruleCheck'=>['except' => 'login']
];
该代码为设置前置曹祖,其中except表示除什么方法之外,在这里设置除login登录方法外,因为所有用户都必须登录后才能判断权限,登录方法则不受此影响。随后查看ruleCheck方法,该方法首先判断用户是否登录:
session('?admin')?:$this->error('未登录或已失效','Index/login');
随后新建权限判断类:
$AuthRuleValidate=new AuthRuleValidateBase();
接着使用seesion获取uid:
$s=session('admin');
最后调用权限判断方法传入当前控制器方法与uid进行权限判断:
$AuthRuleValidate->check($s['id'],strtolower(request()->controller()).'/'.strtolower(request()->action()));
完全权限判断基类后,使所有管理后台的控制器继承与该方法,例Auth控制器(该操作可以等待登录页编写后再进行):
class Auth extends Base{
3.5 完成登录功能编写
在admin模块中,index控制器添加方法login,内容为:
public function login(){
return $this->view->fetch();
}
前端代码使用ajax传值,前端页显示如下:
随后填入帐号及密码通过ajax传值到admin模块下的Authpost控制器中login方法中,内容如下:
//登录
public function login(){
$request_data = Request::post();
$data['username']=$request_data['user'];
$data['password']=md5(trim($request_data['password']));
$res=db('admin')->where($data)->find();
$res?session('admin', $res):$this->error('帐号或密码错误');
}
使用find方法对传入值进行对比,密码正确则将值传入到seesion否则将提示帐号密码错误。
3.6 完成传入值的判断
在基本权限实现完成后,使用验证器对传入值进行判断,毕竟外部值都是不可靠的值。
在controller同级下创建一目录validate,创建目录后在该目录下创建一php文件名为BaseValidate作为对数据进行判断类的基类,代码内容如下:
<?php
namespace app\admin\validate;
use think\Validate;
use think\Controller;
use app\admin\code\ReturnCodeInfo;
class BaseValidate extends Validate{
public function gocheck($validata){
if(!$this->check($validata)){
return (new ReturnCodeInfo())->validataError($this->getError());
}
return (new ReturnCodeInfo())->validataSuccess();
}
}
该类继承验证器类,具有验证器特性。验证器的使用查看tp5.1文档。
查看gocheck方法,gocheck方法调用了验证器本身的check方法,其接收的参数$validata为需要验证的数据。check判断错误则调用 ReturnCodeInfo类中的报错数据返回,否则则返回正确。
假设在管理员添加时需要验证数据是否合规,那么在validate目录中创建一名为AdminValidate的php文件,内容为:
<?php
namespace app\admin\validate;
use app\admin\validate\BaseValidate;
class AdminValidate extends BaseValidate{
protected $rule = [
'password' => 'require|max:50',
'username' => 'require|max:30',
'realname' => 'require|max:30',
'group' => 'require|max:30'
];
}
在管理员添加方法中(Authpost控制器中的adminadd方法)添加:
//验证器
$valires=(new AdminValidate())->gocheck($data);
if ($valires['code']==10002){
return json($valires);
}
即可完成,但一定要注意,需要引入该验证器:
use app\admin\validate\AdminValidate;
四、完成内容管理功能的编写
4.1 完成管理后台模块搭建
我们首先实现查看轮播图区域元素:
发现元素包含轮播图标题、简介,以及轮播图标题1、简介1以及背景图。数据库设计如下:
我们通过sqlyog的可视化操作添加轮播图所需要资源的数据,可以通过邮件检查直接获取资源路径及内容:
首先得到轮播图第一张图片的数据:
复制内容填入sqlyog表中:
同理获取所有的内容填入至表:
所有内容填入数据库:
回到index模块下的index控制器中,在index方法中添加获取轮播图数据表中数据:
<?php
namespace app\index\controller;
use think\Controller;
use think\Db;
class Index extends Controller{
public function index()
{
$banner_res=Db::table('tp_home_banner')
->order('id', 'desc')
->limit(4)
->select();
print_r($banner_res);
die;
return $this->view->fetch();
}
}
在以上代码中,使用select方法查询轮播图数据表中的数据,查询方式是id的降序,这样使轮播图将会以最新添加的作为显示依据,并且每次只查询前4条;查询结构复制给变量banner_res,使用print_r对该变量进行输出,随后在输出模板前使用die终止,查看输出。
访问localhost成功获得数据:
在index方法中添加代码,像前端传递banner_res变量,并且删除die代码:
<?php
namespace app\index\controller;
use think\Controller;
use think\Db;
class Index extends Controller{
public function index()
{
$banner_res=Db::table('tp_home_banner')
->order('id', 'desc')
->limit(4)
->select();
$this->view->assign('banner',$banner_res);
return $this->view->fetch();
}
}
接下来我们将在html代码中使用tp的前端模板语法对一些html元素进行控制。我们通过元素查询得知轮播图元素id为homev1:
在代码中找到id为homev1的元素,查看代码,每个轮播图标签类似,只有默认选项多了个class修饰:
但是有些小伙伴觉得很麻烦,那我们换一种方式,使用tp框架前端的模板语法,类似if判断,从而输出内容:
首先使用volist标签进行循环,在标签中设置循环变量key,该key循环第一次的值为1,当为1使用eq标签判断,是1则输出第一个轮播图的html代码:
{
eq name="k" value="1"}
需要输出的html代码需要使用成对的eq标签包含,结束的eq标签为 {/eq}。
代码如下:
<div class="carousel-inner" role="listbox">
<!-- Third Slide -->
{volist name="banner" id="vo" key="k" }
{eq name="k" value="1"}
<div class="item active">
<!-- Slide Background -->
<img src="{$vo.img}" alt="SeoPress Slider" />
<div class="bs-slider-overlay"></div>
<div class="container">
<div class="row">
<div class="col-md-8 col-md-offset-2">
<div class="slide-text slide_style_center">
<h1 class="text-white" data-animation="animated zoomInRight">{$vo.title}</h1>
<p class="text-white m-top-10" data-animation="animated fadeInLeft">{$vo.content}</p>
<a class="btn btn-primary btn-round m-top-30" data-animation="animated fadeInLeft" href="" target="_blank">Read More</a>
<a class="btn btn-default btn-round m-top-30" data-animation="animated fadeInRight" href="" target="_blank">Read More</a>
</div>
</div>
</div>
</div>
</div>
{/eq}
<!-- End of Slide -->
{eq name="k" value="2"}
<!-- Second Slide -->
<div class="item">
<img src="{$vo.img}" alt="SeoPress Slider" />
<div class="bs-slider-overlay"></div>
<div class="container">
<div class="row">
<!-- Slide Text Layer -->
<div class="col-md-6">
<div class="slide-text slide_style_left">
<h1 class="text-white" data-animation="animated fadeInRight">{$vo.title}</h1>
<p class="text-white m-top-10" data-animation="animated zoomInLeft">{$vo.content}
</p>
<a class="btn btn-default btn-round m-top-30" data-animation="animated fadeInRight" href="" target="_blank">Read More</a>
<a class="btn btn-primary btn-round m-top-30" data-animation="animated fadeInLeft" href="" target="_blank">Read More</a>
</div>
</div>
</div>
</div>
</div>
<!-- End of Slide -->
{/eq}
{eq name="k" value="3"}
<!-- Third Slide -->
<div class="item">
<img src="{$vo.img}" alt="SeoPress Slider" />
<div class="bs-slider-overlay"></div>
<div class="container">
<div class="row">
<!-- Slide Text Layer -->
<div class="col-md-6">
<div class="slide-text slide_style_left">
<h1 class="text-white" data-animation="animated fadeInDown">{$vo.title}</h1>
<p class="text-white m-top-10" data-animation="animated fadeInLeft">{$vo.content}
</p>
<a class="btn btn-primary btn-round m-top-30" data-animation="animated fadeInLeft" href="" target="_blank">Read More</a>
<a class="btn btn-default btn-round m-top-30" data-animation="animated fadeInRight" href="" target="_blank">Read More</a>
</div>
</div>
</div>
</div>
</div>
{/eq}
{eq name="k" value="4"}
<!-- Fourth Slide -->
<div class="item">
<img src="{$vo.img}" alt="SeoPress Slider" />
<div class="bs-slider-overlay"></div>
<div class="container">
<div class="row">
<!-- Slide Text Layer -->
<div class="col-md-6">
<div class="slide-text slide_style_left">
<h1 class="text-white" data-animation="animated fadeInLeft">{$vo.title} <br />
Online Marketing Needs</h1>
<p class="text-white m-top-10" data-animation="animated fadeInRight">{$vo.content}
</p>
<a class="btn btn-primary btn-round m-top-30" data-animation="animated fadeInLeft" href="" target="_blank">Read More</a>
<a class="btn btn-default btn-round m-top-30" data-animation="animated fadeInRight" href="" target="_blank">Read More</a>
</div>
</div>
</div>
</div>
</div>
{/eq}
{/volist}
<!-- End of Slide -->
</div><!-- End of Wrapper For Slides -->
接着我们往下查看首页内容:
个人觉得该区域可以放一个“有利于”之类的宣传语,那么建一表存放标题、图片、内容信息:
在该表中填入网页中原有的数据:
在index控制器中添加查询tp_home_advantageous表数据的代码并将结果传至前端:
<?php
namespace app\index\controller;
use think\Controller;
use think\Db;
class Index extends Controller{
public function index()
{
$banner_res=Db::table('tp_home_banner')
->order('id', 'desc')
->limit(4)
->select();
$advantageous_res=Db::table('tp_home_advantageous')
->order('id', 'desc')
->limit(6)
->select();
$this->view->assign('advantageous',$advantageous_res);
$this->view->assign('banner',$banner_res);
return $this->view->fetch();
}
}
修改前端代码,发现该区域代码的html几乎一致,前3个的class=“service-item sm-m-top-65”,后3个的class=“service-item m-top-65”:
<div class="main-service-area text-center m-top-80">
<div class="col-md-4 col-sm-6">
<div class="service-item sm-m-top-65">
<div class="service-icon">
<img src="/home/assets/images/service1.png" alt="" />
</div>
<h5 class="text-info m-top-50">Search Engine Optimization</h5>
<p class="text-black m-top-20">With our 17+ years of experience, our SEO services will get your site ranking.</p>
</div>
</div>
<div class="col-md-4 col-sm-6">
<div class="service-item sm-m-top-65">
<div class="service-icon">
<img src="/home/assets/images/service3.png" alt="" />
</div>
<h5 class="text-info m-top-50">Content Marketing</h5>
<p class="text-black m-top-20">From blogs and social posts to
infographics videos we create and promote quality.</p>
</div>
</div>
<div class="col-md-4 col-sm-6">
<div class="service-item sm-m-top-65">
<div class="service-icon">
<img src="/home/assets/images/service2.png" alt="" />
</div>
<h5 class="text-info m-top-50">Social Media Marketing</h5>
<p class="text-black m-top-20">Boost brand awareness and reach your
customers on a human level.</p>
</div>
</div>
<div class="col-md-4 col-sm-6">
<div class="service-item m-top-65">
<div class="service-icon">
<img src="/home/assets/images/service4.png" alt="" />
</div>
<h5 class="text-info m-top-50">Web Design & Development</h5>
<p class="text-black m-top-20">Our designers and developers will create an attractive, SEO-friendly & fully functional.</p>
</div>
</div>
<div class="col-md-4 col-sm-6">
<div class="service-item m-top-65">
<div class="service-icon">
<img src="/home/assets/images/service5.png" alt="" />
</div>
<h5 class="text-info m-top-50">eCommerce Solutions</h5>
<p class="text-black m-top-20">With our 17+ years of experience,
our SEO services will get your site ranking.</p>
</div>
</div>
<div class="col-md-4 col-sm-6">
<div class="service-item m-top-65">
<div class="service-icon">
<img src="/home/assets/images/service6.png" alt="" />
</div>
<h5 class="text-info m-top-50">Inbound Marketing</h5>
<p class="text-black m-top-20">With our ecommerce solutions,
you'll provide an enjoyable, seamless.</p>
</div>
</div>
</div>
这是把其它div删除,留下1个div,使用volist标签进行遍历输出值,并且设置循环变量key,使用tp框架的前端判断标签,判断小于4时输出class为col-sm-6:
{lt name="k" value="4"}col-sm-6{/eq}
当循环后3三位,则是k值大于3,大于3输出col-sm-6,使用gt标签:
{gt name="k" value="3"}col-sm-6{/eq}
将两个前端代码编写与div中,完整代码如下:
<div class="main-service-area text-center m-top-80">
{volist name="advantageous" id="vo" key="k" }
<div class="col-md-4 col-sm-6">
<div class='service-item {lt name="k" value="4"}col-sm-6{/eq} {gt name="k" value="3"}col-sm-6{/eq}'>
<div class="service-icon">
<img src="{$vo.img}" alt="" />
</div>
<h5 class="text-info m-top-50">{$vo.title}</h5>
<p class="text-black m-top-20">{$vo.content}</p>
</div>
</div>
{/volist}
</div>
运行结果:
接着往下,查看页面区域:
我们将该页面编写成产品展示区域。新建一数据库表:
填入内容:
在index控制器index方法中添加product数据库查询代码并传至前端:
<?php
namespace app\index\controller;
use think\Controller;
use think\Db;
class Index extends Controller{
public function index()
{
$banner_res=Db::table('tp_home_banner')
->order('id', 'desc')
->limit(4)
->select();
$advantageous_res=Db::table('tp_home_advantageous')
->order('id', 'desc')
->limit(6)
->select();
$product_res=Db::table('tp_home_product')
->order('id', 'desc')
->limit(2)
->select();
$this->view->assign('product',$product_res);
$this->view->assign('advantageous',$advantageous_res);
$this->view->assign('banner',$banner_res);
return $this->view->fetch();
}
}
随后在html代码中输出内容即可:
<section id="leading" class="leading bg-primary sections2">
<div class="container">
<div class="row">
<div class="main-leading">
<div class="col-md-6">
<div class="leading-content">
<div class="head-title">
<h2 class="text-white">{$product[0]['title']}</h2>
<p class="m-top-30 text-white">{$product[0]['specs']}</p>
<div class="separator2 hv2"><span></span><span></span><span></span></div>
</div>
<p class="m-top-40">{$product[0]['content']}</p>
<a href="" class="btn btn-default btn-round m-top-20">Our Team</a>
</div>
</div>
<div class="col-md-5">
<div class="leading-img sm-m-top-50">
<img src="{$product[0]['img']}" alt="" />
</div>
</div>
</div>
</div><!-- End off row-->
</div><!-- End off container -->
</section><!-- End off leading section-->
<!--Allies Section-->
<section id="allies" class="allies sections">
<div class="container">
<div class="row">
<div class="main-allies">
<div class="col-md-5">
<div class="allies-img">
<img src="{$product[1]['img']}" alt="" />
</div>
</div>
<div class="col-md-7">
<div class="allies-content sm-m-top-50">
<div class="head-title">
<h2 class="text-black">O{$product[1]['title']}</h2>
<h5 class="text-black m-top-30">{$product[1]['content']}</h5>
<div class="separator2"><span></span><span></span><span></span></div>
</div>
<p class="m-top-40">{$product[1]['specs']}</p>
<a href="" class="btn btn-primary btn-round m-top-30">Portfolio</a>
</div>
</div>
</div>
</div><!-- End off row -->
</div><!--End off container -->
</section><!-- End off Allies Section-->
接着往下看:
该区域可以更改成文章的展示,创建已数据库表:
添加内容:
查看html代码:
<div class="col-md-4 col-sm-6">
<div class="studies-item">
<div class="studies-feature border">
<img class="img-rounded" src="/home/assets/images/studies-img-01.jpg" alt="" />
<div class="studies-overlay img-rounded"><a href=""><span class="icon icon-arrows-2 hvr-hang"></span></a></div>
<div class="custom-hover"></div>
</div>
<div class="studies-conten m-top-30">
<h4><a href="">Acme Corporation</a></h4>
<p class="m-top-10">Objective: Build a larger twitter community</p>
</div>
</div>
</div>
<div class="col-md-4 col-sm-6">
<div class="studies-item xs-m-top-35">
<div class="studies-feature border">
<img class="img-rounded" src="/home/assets/images/studies-img-02.jpg" alt="" />
<div class="studies-overlay img-rounded"><a href=""><span class="icon icon-arrows-2 hvr-hang"></span></a></div>
</div>
<div class="studies-conten m-top-30">
<h4><a href="">Soylent Corp </a></h4>
<p class="m-top-10">Objective: Make tone & branding consistency</p>
</div>
</div>
</div>
<div class="col-md-4 col-sm-6">
<div class="studies-item sm-m-top-35">
<div class="studies-feature border">
<img class="img-rounded" src="/home/assets/images/studies-img-03.jpg" alt="" />
<div class="studies-overlay img-rounded"><a href=""><span class="icon icon-arrows-2 hvr-hang"></span></a></div>
</div>
<div class="studies-conten m-top-30">
<h4><a href="">Umbrella Corporation</a></h4>
<p class="m-top-10">Objective: Eliminate the residue of black-hat methods</p>
</div>
</div>
</div>
<div class="col-md-4 col-sm-6">
<div class="studies-item m-top-35">
<div class="studies-feature border">
<img class="img-rounded" src="/home/assets/images/studies-img-04.jpg" alt="" />
<div class="studies-overlay img-rounded"><a href=""><span class="icon icon-arrows-2 hvr-hang"></span></a></div>
</div>
<div class="studies-conten m-top-30">
<h4><a href="">Initech</a></h4>
<p class="m-top-10">Objective: Improve site load speed & functionality</p>
</div>
</div>
</div>
<div class="col-md-4 col-sm-6">
<div class="studies-item m-top-35">
<div class="studies-feature border">
<img class="img-rounded" src="/home/assets/images/studies-img-05.jpg" alt="" />
<div class="studies-overlay img-rounded">
<a href="">
<span class="icon icon-arrows-2 hvr-hang"></span>
</a>
</div>
</div>
<div class="studies-conten m-top-30">
<h4><a href="">Vehement Capital Partners </a></h4>
<p class="m-top-10">Objective: Increase nationwide sales</p>
</div>
</div>
</div>
<div class="col-md-4 col-sm-6">
<div class="studies-item m-top-35">
<div class="studies-feature border">
<img class="img-rounded" src="/home/assets/images/studies-img-06.jpg" alt="" />
<div class="studies-overlay img-rounded">
<a href=""><span class="icon icon-arrows-2 hvr-hang"></span></a>
</div>
</div>
<div class="studies-conten m-top-30">
<h4><a href="">Massive Dynamic</a></h4>
<p class="m-top-10">Objective: Increase qualified traffic</p>
</div>
</div>
</div>
发现该html代码中前3个div的class有所变化,后3个div布局内容则无变化。我们使用eq标签使前3个div照原样输出,后3个div遍历输出:
{volist name="article" id="vo" key="k" }
{eq name="k" value="1"}
<div class="col-md-4 col-sm-6">
<div class="studies-item">
<div class="studies-feature border">
<img class="img-rounded" src="{$vo.img}" alt="" />
<div class="studies-overlay img-rounded"><a href=""><span class="icon icon-arrows-2 hvr-hang"></span></a></div>
<div class="custom-hover"></div>
</div>
<div class="studies-conten m-top-30">
<h4><a href="">{$vo.title}</a></h4>
<p class="m-top-10">{$vo.content}</p>
</div>
</div>
</div>
{/eq}
{eq name="k" value="2"}
<div class="col-md-4 col-sm-6">
<div class="studies-item xs-m-top-35">
<div class="studies-feature border">
<img class="img-rounded" src="{$vo.img}" alt="" />
<div class="studies-overlay img-rounded"><a href=""><span class="icon icon-arrows-2 hvr-hang"></span></a></div>
</div>
<div class="studies-conten m-top-30">
<h4><a href="">{$vo.title}</a></h4>
<p class="m-top-10">{$vo.content}</p>
</div>
</div>
</div>
{/eq}
{eq name="k" value="3"}
<div class="col-md-4 col-sm-6">
<div class="studies-item sm-m-top-35">
<div class="studies-feature border">
<img class="img-rounded" src="{$vo.img}" alt="" />
<div class="studies-overlay img-rounded"><a href=""><span class="icon icon-arrows-2 hvr-hang"></span></a></div>
</div>
<div class="studies-conten m-top-30">
<h4><a href="">{$vo.title}</a></h4>
<p class="m-top-10">{$vo.content}</p>
</div>
</div>
</div>
{/eq}
{gt name="k" value="3"}
<div class="col-md-4 col-sm-6">
<div class="studies-item m-top-35">
<div class="studies-feature border">
<img class="img-rounded" src="{$vo.img}" alt="" />
<div class="studies-overlay img-rounded"><a href=""><span class="icon icon-arrows-2 hvr-hang"></span></a></div>
</div>
<div class="studies-conten m-top-30">
<h4><a href="">{$vo.title}</a></h4>
<p class="m-top-10">{$vo.content}</p>
</div>
</div>
</div>
{/gt}
{/volist}
在index控制器的首页方法index中添加对article表数据的查询:
<?php
namespace app\index\controller;
use think\Controller;
use think\Db;
class Index extends Controller{
public function index()
{
$banner_res=Db::table('tp_home_banner')
->order('id', 'desc')
->limit(4)
->select();
$advantageous_res=Db::table('tp_home_advantageous')
->order('id', 'desc')
->limit(6)
->select();
$product_res=Db::table('tp_home_product')
->order('id', 'desc')
->limit(2)
->select();
$article_res=Db::table('tp_home_article')
->order('id', 'desc')
->limit(6)
->select();
$this->view->assign('article',$article_res);
$this->view->assign('product',$product_res);
$this->view->assign('advantageous',$advantageous_res);
$this->view->assign('banner',$banner_res);
return $this->view->fetch();
}
}
其它的前端内容通过数据库更改不再赘述。
4.2 完成通过后台设置更改与添加前端内容
创建控制器Contentmanger,添加方法bannerManger,bannerManger方法跳转到一页面用于显示banner数据,点击每条数据可进行编辑:
由于有数据的查询,在控制器中需要查询banner表数据,代码为:
<?php
/** * |----------------------- * | 页面跳转 * |----------------------- */
namespace app\admin\controller;
use think\Controller;
use think\Db;
use think\facade\Request;
class Contentmanger extends Base{
//官网首页内容管理
public function bannerManger(){
$list=Db::table('tp_home_banner')
->order('id')
->select();
$this->view->assign('list',$list);
return $this->view->fetch();
}
}
此处可添加验证器检测传入值是否正确,为了节省篇幅接下来的代码中不再过多的添加其它内容。
html代码前端的编辑修改按钮,使用了url方法传参,传参后获取该id的内容,方便进行修改:
点击编辑后将会可以随意修改banner的值:
点击choosefile可选择img文件,修改banner图片。
创建一控制器用来管理内容修改操作的逻辑,创建一php文件名为 Contentmangerpost ,添加 bannerEdit 方法:
<?php
/** * |----------------------- * | 页面跳转 * |----------------------- */
namespace app\admin\controller;
use think\Controller;
use think\Db;
use think\facade\Request;
use \think\File;
use app\admin\model\Goods;
use app\admin\validate\IdValidate;
use app\admin\code\ReturnCodeInfo;
class Contentmangerpost extends Base{
//轮播图编辑
public function bannerEdit(){
$save_path='/public';
$save_path_='/uploads/imgs/';
$request_data = Request::post();
$con['id']=$request_data['id'];
$data['title']=$request_data['title'];
$data['title_2']=$request_data['title_2'];
$data['content']=$request_data['content'];
$data['content_2']=$request_data['content_2'];
// 获取表单上传文件 例如上传了001.jpg
$file = request()->file('file');
if($file){
// 移动到框架应用根目录/uploads/ 目录下
$info = $file->move('..'.$save_path.$save_path_);
if($info){
// 成功上传后 获取上传信息
$data['img']=$save_path_.str_replace('\\','/',$info->getSaveName());
}else{
// 上传失败获取错误信息
echo $file->getError();
}
}else{
$data['img']='/home/assets/images/slide-bg-01.jpg';
}
$res=Db::name('home_banner')
->where($con)
->update($data);
if($res){
return json((new ReturnCodeInfo())->actionSuccess());
}else{
return json((new ReturnCodeInfo())->actionError());
}
}
}
以上代码使用request()->file(‘file’);判断是否接收到file值,如接收到说明用户选择了新图片,那么使用move方法保存图片,通过getSaveName方法获取保存图片名。最终更新至数据库中。其它数据的更新方法与该步骤类似,不再赘述。接下来通过拖拽实现web并且绑定数据。
五、完成页面拖拽生成并绑定数据功能的编写
拖拽页面在此提供一个思想,通过bootstrap的layoutit可视化布局可以完成简单页面拖拽生成,需要完成更多复杂的界面需要对layoutit进行二次开发。本篇内容为一个demo,通过可视化layoutit生成界面且进行代码替换完成对于thinkphp模板的制作,最后通过可视化数据绑定生成php代码。
5.1 完成拖拽界面的前端搭建
首先为layoutit添加一个控制器并更改资源路径,此操作不再赘述。部署完成后打开界面:
可拖拽布局实现界面编辑:
拖拽成如下界面:
点击下载可查看生成的html代码:
在点击下载时,我通过js保存了生成的代码:
var template='';
$("[data-target=#downloadModal]").click(function(e) {
e.preventDefault();
downloadLayoutSrc();
});
function downloadLayoutSrc() {
var e = "";
$("#download-layout").children().html($(".demo").html());
var t = $("#download-layout").children();
t.find(".preview, .configuration, .drag, .remove").remove();
t.find(".lyrow").addClass("removeClean");
t.find(".box-element").addClass("removeClean");
t.find(".lyrow .lyrow .lyrow .lyrow .lyrow .removeClean").each(function() {
cleanHtml(this)
});
t.find(".lyrow .lyrow .lyrow .lyrow .removeClean").each(function() {
cleanHtml(this)
});
t.find(".lyrow .lyrow .lyrow .removeClean").each(function() {
cleanHtml(this)
});
t.find(".lyrow .lyrow .removeClean").each(function() {
cleanHtml(this)
});
t.find(".lyrow .removeClean").each(function() {
cleanHtml(this)
});
t.find(".removeClean").each(function() {
cleanHtml(this)
});
t.find(".removeClean").remove();
$("#download-layout .column").removeClass("ui-sortable");
$("#download-layout .row-fluid").removeClass("clearfix").children().removeClass("column");
if ($("#download-layout .container").length > 0) {
changeStructure("row-fluid", "row")
}
formatSrc = $.htmlClean($("#download-layout").html(), {
format: true,
allowedAttributes: [
["id"],
["class"],
["data-toggle"],
["data-target"],
["data-parent"],
["role"],
["data-dismiss"],
["aria-labelledby"],
["aria-hidden"],
["data-slide-to"],
["data-slide"]
]
});
$("#download-layout").html(formatSrc);
$("#downloadModal textarea").empty();
$("#downloadModal textarea").val(formatSrc)
console.log(formatSrc);
template=formatSrc;
}
此代码为layoutit的js代码,在此基础上我新建了已js全局变量保存数据,变量为template,在js代码清洗完成后把清洗后的值赋值给全局变量template。
随后点击保存:
<button class="btn btn-primary" onclick="bc()">保存</button>
保存按钮点击后对应的js代码为:
function bc(){
{literal}
template='{$head|raw}'+'<body style="min-height: 816px; cursor: auto;" class="devpreview sourcepreview">'+template+'</body>';
{/literal}
$.ajax({
type:'post',
url:'/index.php?s=/admin/Autoviewpost/test/',
data:{"template":template},
dataType:"json",
success:function(data){
},error:function(jqXHR){
}
})
}
在因为生成的代码需要一定的js文件引入,在此我添加了{$head|raw}为前端的模板代码,使用了{literal} 标签对thinkphp的模板代码进行修饰,表示不解析其中内容。head变量的内容为:
$head='<link href="/autoview/css/bootstrap-combined.min.css" rel="stylesheet"> <link href="/autoview/css/layoutit.css" rel="stylesheet"> <!-- Le styles --> <link href="/autoview/css/bootstrap-combined.min.css" rel="stylesheet"> <link href="/autoview/css/layoutit.css" rel="stylesheet"> <!-- HTML5 shim, for IE6-8 support of HTML5 elements --> <!--[if lt IE 9]> <script src="/autoview/js/html5shiv.js"></script> <![endif]--> <!-- Fav and touch icons --> <link rel="shortcut icon" href="/autoview/img/favicon.png"> <script type="text/javascript" src="/autoview/js/jquery-2.0.0.min.js"></script> <!--[if lt IE 9]> <script type="text/javascript" src="http://code.jquery.com/jquery-1.9.1.min.js"></script> <![endif]--> <script type="text/javascript" src="/autoview/js/bootstrap.min.js"></script> <script type="text/javascript" src="/autoview/js/jquery-ui.js"></script> <script type="text/javascript" src="/autoview/js/jquery.ui.touch-punch.min.js"></script> <script type="text/javascript" src="/autoview/js/jquery.htmlClean.js"></script> <script type="text/javascript" src="/autoview/ckeditor/ckeditor.js"></script> <script type="text/javascript" src="/autoview/ckeditor/config.js"></script> <script type="text/javascript" src="/autoview/js/scripts.js"></script>';
点击保存后,生成的html代码文本将会传到Autoviewpost控制器下的test方法中,test方法代码如下:
public function test(){
$request_data = Request::post();
$template=$request_data['template'];
/* $regex4="/<div class=\"media\".*?>.*?<\/div>.*?<\/div>/ism"; */
$template=preg_replace(getMediaReStr(),getMediaHtmlStr(),$template);//media 替换
$template=preg_replace(getCarouselReStr(),getCarouselHtmlStr(),$template);//轮播图 替换
$template=preg_replace(getThumbnailsReStr(),getThumbnailsHtmlStr(),$template);//缩略图 替换
$template=preg_replace(getUnitReStr(),getUnitHtmlStr(),$template);//概述 替换
$template=str_replace("{eq name="key" value="1"}active{/eq}",'{eq name="key" value="1"}active{/eq}',$template);
file_put_contents(dirname(dirname(__FILE__)).'\view\templates\t1.html',$template);
/* print_r($request_data['template']); */
}
该方法在接收值后对一部分进行替换。使用preg_replace对文本进行替换,在该对比中我使用了正则对数据进行匹配,该方法我编写在common公共函数的php文件中,地址为application\common.php,内容为:
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006-2016 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: 流年 <liu21st@gmail.com>
// +----------------------------------------------------------------------
// 应用公共文件
function arrunset(&$arr){
array_splice($arr,0,1);
}
//Media php code
function getMediaHtmlStr(){
$str='{volist name="media" id="data" }'.
'<div class="media">'.
'<a href="{$data.src}" class="pull-left"><img src="{$data.img}" class="media-object" alt=\'\' /></a>'.
'<div class="media-body">'.
'<h4 class="media-heading">'.
'{$data.title}'.
'</h4> {$data.content}'.
'</div>'.
'</div>'.
'{:arrunset($media)}'.
'{/volist}';
return $str;
}
//Media regex str
function getMediaReStr(){
$re="/<div class=\"media\".*?>.*?<\/div>.*?<\/div>/ism";
return $re;
}
//轮播图 php code
function getCarouselHtmlStr(){
$str='<div class="carousel slide" id="carousel-998124"><div class="carousel-inner"> '.
'{volist name="banner" id="data"}'.
'<div class=\'item {eq name="key" value="1"}active{/eq} \'>'.
'<img alt="" src="{$data.img}" />'.
'<div class="carousel-caption">'.
'<h4>'.
'{$data.title}'.
'</h4>'.
'<p>'.
'{$data.content}'.
'</p>'.
'</div>'.
'</div>'.
'{/volist} '.
'</div> '.
'<a data-slide="prev" href="#carousel-998124" class="left carousel-control">‹</a><a data-slide="next" href="#carousel-998124" class="right carousel-control">›</a></div>';
return $str;
}
//轮播图 regex str
function getCarouselReStr(){
$re="/<div class=\"carousel slide\".*?>.*?class=\"right carousel-control\">.*?<\/div>/ism";
return $re;
}
//缩略图 php code
function getThumbnailsHtmlStr(){
$str='<ul class="thumbnails">'.
'{volist name="article" id="data"}'.
'<li class="span4">'.
'<div class="thumbnail"> <img alt="300x200" src="{$data.faceimg}">'.
'<div class="caption" contenteditable="true">'.
'<h3>{$data.title}</h3>'.
'<p>{$data.content}</p>'.
'<p><a class="btn btn-primary" href="#">浏览</a> <a class="btn" href="#">分享</a></p>'.
'</div>'.
'</div>'.
'</li>'.
'{/volist}'.
'</ul>';
return $str;
}
//缩略图 regex str
function getThumbnailsReStr(){
$re="/<ul class=\"thumbnails\".*?>.*?<\/ul>/ism";
return $re;
}
//概述 php code
function getUnitHtmlStr(){
$str='{volist name="ad" id="data" offset="0" length=\'1\'}'.
'<div class="hero-unit" contenteditable="true">'.
'<h1>{$data.title}</h1>'.
'<p>{$data.content} </p>'.
'<p><a class="btn btn-primary btn-large" href="#">参看更多 »</a></p>'.
'</div>'.
'{:arrunset($ad)}'.
'{/volist}' ;
return $str;
}
//概述 regex str
function getUnitReStr(){
$re="/<div class=\"hero-unit\".*?>.*?<\/div>/ism";
return $re;
}
使用不同的方法返回不同组件、html代码的正则匹配,替换成所需的带有thinkphp框架语法的html代码,这些代码同样在common文件中。完成替换后由于发现某些字符需要进行替换,编写代码:
$template=str_replace("{eq name="key" value="1"}active{/eq}",'{eq name="key" value="1"}active{/eq}',$template);
完成清洗替换后生成html模板生成危机:
file_put_contents(dirname(dirname(__FILE__)).'\view\templates\t1.html',$template);
由于是demo,所以位置写死了。
随后访问Autoview控制器下的createcontrol方法(页面没写):
输入你想要生成的控制器名、方法名,该方法需要绑定数据表中哪些元素,以及绑定的页面路径:
输入完成后点击提交,数据将会传到Autoview控制器中的buildcontrol方法中,方法代码如下:
//控制器生成 方法名及数据库
public function buildcontrol(){
$request_data = Request::post();
$data['controll'] = $request_data['controll'];
$data['function']=$request_data['function'];
$data['datas']=$request_data['datas'];
$data['templatepath']=$request_data['templatepath'];
$controlcode='<?php namespace app\admin\controller; use think\Controller; use think\Db; class '.$data['controll'].' extends AutoviewBase{ public function '.$data['function'].'(){ return $this->view->fetch(dirname(dirname(__FILE__)).'.$data['templatepath'].'); } }';
file_put_contents(dirname(dirname(__FILE__)).'\controller\\'.$data['controll'].'.php',$controlcode);
$res = Url_datas::create($data);
if($res){
return json((new ReturnCodeInfo())->actionSuccess());
}else{
return json((new ReturnCodeInfo())->actionError());
}
}
该方法controlcode变量为控制器模板变量,该模板文本可以得知该控制器名称为自定义名称,继承于AutoviewBase基类,方法名也是自定义,模板位置根据指定路径进行输出渲染。最后使用 file_put_contents 进行控制器生成。最后将数据存入到Url_datas模型中,也是Url_datas表中,数据表结构数据如下:
我们从控制器生成路径中可以得知,是admin内的控制器,我们访问生成的控制器方法查看效果:
数据页面得到显示,这些数据都是数据库中的数据。在创建控制器时,我们在指定数据表及字段时使用的格式内容为如下:
{
"banner":"id,title,img,content",
"article":"id,title,content,faceimg",
"media":"id,src,img,title,content",
"ad":"id,title,content,img"
}
数据指定格式为“数据表”:“字段1,字段2…”,通过在AutoviewBase的前置方法中对该json数据进行解析,AutoviewBase基类如下:
<?php
/** * |----------------------- * | 前端页面自定义 * |----------------------- */
namespace app\admin\controller;
use think\Controller;
use think\Db;
class AutoviewBase extends Controller{
protected $beforeActionList = [
'toview'
];
public function toview(){
$head='<link href="/autoview/css/bootstrap-combined.min.css" rel="stylesheet"> <link href="/autoview/css/layoutit.css" rel="stylesheet"> <!-- Le styles --> <link href="/autoview/css/bootstrap-combined.min.css" rel="stylesheet"> <link href="/autoview/css/layoutit.css" rel="stylesheet"> <!-- HTML5 shim, for IE6-8 support of HTML5 elements --> <!--[if lt IE 9]> <script src="/autoview/js/html5shiv.js"></script> <![endif]--> <!-- Fav and touch icons --> <link rel="shortcut icon" href="/autoview/img/favicon.png"> <script type="text/javascript" src="/autoview/js/jquery-2.0.0.min.js"></script> <!--[if lt IE 9]> <script type="text/javascript" src="http://code.jquery.com/jquery-1.9.1.min.js"></script> <![endif]--> <script type="text/javascript" src="/autoview/js/bootstrap.min.js"></script> <script type="text/javascript" src="/autoview/js/jquery-ui.js"></script> <script type="text/javascript" src="/autoview/js/jquery.ui.touch-punch.min.js"></script> <script type="text/javascript" src="/autoview/js/jquery.htmlClean.js"></script> <script type="text/javascript" src="/autoview/ckeditor/ckeditor.js"></script> <script type="text/javascript" src="/autoview/ckeditor/config.js"></script> <script type="text/javascript" src="/autoview/js/scripts.js"></script>';
$con['controll']=strtolower(request()->controller());
$con['function']=strtolower(request()->action());
//by controll and action select rules
$data_rules=Db::name('url_datas')
->where($con)
->order('id', 'desc')
->field('datas')
->find();
//get datas
$datas=[];
$tables = json_decode($data_rules['datas'], true);
foreach($tables as $key => $value){
$datas[$key]=Db::name($key)
->column($value);
}
//输出到前端
foreach($datas as $key => $value){
$this->view->assign($key,$value);
}
$this->view->assign('head',$head);
return $this->view->fetch(dirname(dirname(__FILE__)).'\view\templates\t1.html');
}
}
以上代码中定义了前置操作toview方法,在toview方法中定义了head为头部资源文件,之后使用如下代码获取当前控制器及方法名:
$con['controll']=strtolower(request()->controller());
$con['function']=strtolower(request()->action());
把控制器及方法名当作条件至url_datas数据表中查询所需的数据要求及格式:
//by controll and action select rules
$data_rules=Db::name('url_datas')
->where($con)
->order('id', 'desc')
->field('datas')
->find();
得到了所需数据后,对该数据进行json解析,解析后遍历该数据作为对指定表与数据的查询:
$datas=[];
$tables = json_decode($data_rules['datas'], true);
foreach($tables as $key => $value){
$datas[$key]=Db::name($key)
->column($value);
}
之后使用遍历把得到的数据结果输出到前端:
//输出到前端
foreach($datas as $key => $value){
$this->view->assign($key,$value);
}
最后把head传递值前端代码,渲染输出:
$this->view->assign('head',$head);
return $this->view->fetch(dirname(dirname(__FILE__)).'\view\templates\t1.html');
以上内容准备过于匆忙,只讲解了实现中较为重要的地方,很多优化及细节没有说明,希望下次将会编写一份完全的教程给大家!如有错误欢迎指出,想要深入学习可以关注博主,点赞博主、收藏博文,谢谢~
原创作品@CSDN 1_bit https://blog.csdn.net/A757291228
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://yundeesoft.com/26642.html