页面完成后的最终布局
电影视频网站首页面
会员登录页面
会员注册页面
点击退出和会员按钮,直接进入会员登录页面
视频播放页面
可以看到,页面共同的部分是顶部导航和底部导航
所以我们可以把页面顶部导航和底部导航部分单独定义一个文件home.html,然后让需要使用顶部导航和底部导航的页面都继承home.html页面
1.创建父模板home.html
在templates目录的home目录下创建home.html页面`,用来`定义页面顶部导航和底部导航部分
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="renderer" content="webkit">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1 , user-scalable=no">
<title>微电影
</title>
<link rel="shortcut icon" href="{{ url_for('static',filename='base/images/logo.png') }}">
<link rel="stylesheet" href="{{ url_for('static',filename='base/css/bootstrap.min.css') }}">
<link rel="stylesheet" href="{{ url_for('static',filename='base/css/bootstrap-movie.css') }}">
<link rel="stylesheet" href="{{ url_for('static',filename='base/css/animate.css') }}">
<style>
.navbar-brand > img {
display: inline;
}
.media {
padding: 3px;
border: 1px solid #ccc
}
</style>
</head>
{% block css %}
{% endblock %}
<body>
<nav class="navbar navbar-default navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a href="{{ url_for('home.index',page=1) }}" class="navbar-brand" style="width:250px;">
<img src="{{ url_for('static',filename='base/images/logo.png') }}" style="height:30px;"> 微电影
</a>
</div>
<div class="navbar-collapse collapse">
<form class="navbar-form navbar-left" role="search" style="margin-top:18px;">
<div class="form-group input-group">
<input type="text" class="form-control" placeholder="请输入电影名!">
<span class="input-group-btn">
<a class="btn btn-default" id='do-search'><span
class="glyphicon glyphicon-search"></span> 搜索
</a>
</span>
</div>
</form>
<ul class="nav navbar-nav navbar-right">
<li>
<a class="curlink" href="{{ url_for('home.index',page=1) }}"><span
class="glyphicon glyphicon-film"></span> 电影
</a>
</li>
<li>
<a class="curlink" href="{{ url_for('home.login') }}"><span
class="glyphicon glyphicon-log-in"></span> 登录
</a>
</li>
<li>
<a class="curlink" href="{{ url_for('home.register') }}"><span
class="glyphicon glyphicon-plus"></span> 注册
</a>
</li>
<li>
<a class="curlink" href="{{ url_for('home.logout') }}"><span
class="glyphicon glyphicon-log-out"></span> 退出
</a>
</li>
<li>
<a class="curlink" href="{{ url_for('home.user') }}"><span class="glyphicon glyphicon-user"></span> 会员
</a>
</li>
</ul>
</div>
</div>
</nav>
<div class="container" style="margin-top:76px">
{% block content %}
{% endblock %}
</div>
<footer>
<div class="container">
<div class="row">
<div class="col-md-12">
<p>
©
2017
flaskmovie.com
京ICP备 123456789号
</p>
</div>
</div>
</div>
</footer>
<script src="{{ url_for('static',filename='base/js/jquery.min.js') }}"></script>
<script src="{{ url_for('static',filename='base/js/bootstrap.min.js') }}"></script>
<script src="{{ url_for('static',filename='base/js/jquery.singlePageNav.min.js') }}"></script>
<script src="{{ url_for('static',filename='base/js/wow.min.js') }}"></script>
<script src="{{ url_for('static',filename='lazyload/jquery.lazyload.min.js') }}"></script>
<script src="//cdn.bootcss.com/holder/2.9.4/holder.min.js"></script>
<script>
$(function () {
new WOW().init();
})
</script>
<script>
$(document).ready(function () {
$("img.lazy").lazyload({
effect: "fadeIn"
});
$("#do_search").click(function () {
var key = $("#key_movie").val();
location.href = "{{ url_for('home.search',page=1) }}?key=" + key;
});
});
</script>
{% block js %}
{% endblock %}
</body>
</html>
2. 完成登录页面
2.1 定义登录视图函数login
在home目录下创建forms.py文件,用来定义登录的表单LoginForm
可以通过调用LoginForm表单类直接在前端页面上渲染生成登录需要的字段标签
from flask_wtf
import FlaskForm
from wtforms
.fields
import StringField
, PasswordField
, SubmitField
, FileField
, TextAreaField
from wtforms
.validators
import DataRequired
, EqualTo
, Email
, Regexp
, ValidationError
from app
.models
import User
class LoginForm(FlaskForm
):
name
= StringField
(
label
="账号",
validators
=[
DataRequired
("请输入帐号!")
],
description
="账号",
render_kw
={
"class": "form-control input-lg",
"placeholder": "请输入帐号!"
}
)
pwd
= PasswordField
(
label
="密码",
validators
=[
DataRequired
("请输入密码!")
],
description
="密码",
render_kw
={
"class": "form-control input-lg",
"placeholder": "请输入密码!",
}
)
submit
= SubmitField
(
"登录",
render_kw
={
"class": "btn btn-lg btn-primary btn-block"
}
)
def validate_name(self
, field
):
name
= field
.data
user
= User
.query
.filter_by
(name
=name
).count
()
if user
== 0:
raise ValidationError
("会员账号不存在!")
def validata_pwd(self
, field
):
from app
.models
import User
pwd
= field
.data
name
= self
.name
.data
user
= User
.query
.filter_by
(name
=name
).count
()
if not user
.check_pwd
(pwd
):
raise ValidationError
("密码错误!")
2.2 定义登录视图函数login
@home
.route
("/login/", methods
=['GET', 'POST'])
def login():
form
= LoginForm
()
if form
.validate_on_submit
():
data
= form
.data
user
= User
.query
.filter_by
(name
=data
.get
("name")).first
()
if user
== None:
flash
("会员账号不存在,请重新输入!", "err")
return redirect
(url_for
("home.login"))
elif not user
.check_pwd
(data
.get
("pwd")):
flash
("用户名或密码错误!", "err")
return redirect
(url_for
("home.login"))
session
['user'] = user
.name
session
['user_id'] = user
.id
userlog
= Userlog
(
user_id
=user
.id,
ip
=request
.remote_addr
)
db
.session
.add
(userlog
)
db
.session
.commit
()
return redirect
(url_for
("home.user"))
return render_template
("home/login.html", form
=form
)
2.3 在home目录下创建登录前端页面login.html,继承home.html页面
{% extends "home/home.html" %}
{% block content %}
<div class="row">
<div class="col-md-4 col-md-offset-4">
<div class="panel panel-primary">
<div class="panel-heading">
<h3 class="panel-title"><span class="glyphicon glyphicon-log-in"></span> 会员登录
</h3>
</div>
<div class="panel-body">
{% for msg in get_flashed_messages(category_filter=["err"]) %}
<p style="color:red">{{ msg }}
</p>
{% endfor %}
{% for msg in get_flashed_messages(category_filter=["ok"]) %}
<p style="color:green">{{ msg }}
</p>
{% endfor %}
<form role="form" method="post">
<fieldset>
<div class="form-group">
<label for="input_contact"><span
class="glyphicon glyphicon-user"></span> {{ form.name.label }}
</label>
{{ form.name }}
</div>
{% for err in form.name.errors %}
<div class="col-md-12">
<font style="color:red">{{ err }}
</font>
</div>
{% endfor %}
<div class="form-group">
<label for="input_password"><span
class="glyphicon glyphicon-lock"></span> {{ form.pwd.label }}
</label>
{{ form.pwd }}
</div>
{% for err in form.pwd.errors %}
<div class="col-md-12">
<font style="color:red">{{ err }}
</font>
</div>
{% endfor %}
{{ form.csrf_token }}
{{ form.submit }}
</fieldset>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
3. 完成退出页面
3.1 定义退出视图函数logout
@home
.route
("/logout/")
def logout():
session
.pop
("user", None)
session
.pop
("user_id", None)
return redirect
(url_for
("home.login"))
3.2 在home目录下创建登录前端页面logout.html,继承home.html页面
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="renderer" content="webkit">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1 , user-scalable=no">
<title>微电影
</title>
<link rel="shortcut icon" href="{{ url_for('static',filename='base/images/logo.png') }}">
<link rel="stylesheet" href="{{ url_for('static',filename='base/css/bootstrap.min.css') }}">
<link rel="stylesheet" href="{{ url_for('static',filename='base/css/bootstrap-movie.css') }}">
<link rel="stylesheet" href="{{ url_for('static',filename='base/css/animate.css') }}">
<style>
.navbar-brand > img {
display: inline;
}
.media {
padding: 3px;
border: 1px solid #ccc
}
</style>
{% block css %}{% endblock %}
</head>
<body>
<nav class="navbar navbar-default navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a href="{{ url_for('home.index',page=1) }}" class="navbar-brand" style="width:250px;">
<img src="{{ url_for('static',filename='base/images/logo.png') }}" style="height:30px;"> 微电影
</a>
</div>
<div class="navbar-collapse collapse">
<form class="navbar-form navbar-left" role="search" style="margin-top:18px;">
<div class="form-group input-group">
<input type="text" class="form-control" placeholder="请输入电影名!" id="key_movie">
<span class="input-group-btn">
<a class="btn btn-default" id="do_search"><span class="glyphicon glyphicon-search"></span> 搜索
</a>
</span>
</div>
</form>
<ul class="nav navbar-nav navbar-right">
<li>
<a class="curlink" href="{{ url_for('home.index',page=1) }}"><span
class="glyphicon glyphicon-film"></span> 电影
</a>
</li>
<li>
<a class="curlink" href="{{ url_for('home.login') }}"><span
class="glyphicon glyphicon-log-in"></span> 登录
</a>
</li>
<li>
<a class="curlink" href="{{ url_for('home.regist') }}"><span
class="glyphicon glyphicon-plus"></span> 注册
</a>
</li>
<li>
<a class="curlink" href="{{ url_for('home.logout') }}"><span
class="glyphicon glyphicon-log-out"></span> 退出
</a>
</li>
<li>
<a class="curlink" href="{{ url_for('home.user') }}"><span class="glyphicon glyphicon-user"></span> 会员
</a>
</li>
</ul>
</div>
</div>
</nav>
{% block content %}
{% endblock %}
<footer>
<div class="container">
<div class="row">
<div class="col-md-12">
<p>
©
2017
flaskmovie.com
京ICP备 123456789号
</p>
</div>
</div>
</div>
</footer>
<script src="{{ url_for('static',filename='base/js/jquery.min.js') }}"></script>
<script src="{{ url_for('static',filename='base/js/bootstrap.min.js') }}"></script>
<script src="{{ url_for('static',filename='base/js/jquery.singlePageNav.min.js') }}"></script>
<script src="{{ url_for('static',filename='base/js/wow.min.js') }}"></script>
<script src="{{ url_for('static',filename='lazyload/jquery.lazyload.min.js') }}"></script>
<script src="//cdn.bootcss.com/holder/2.9.4/holder.min.js"></script>
<script>
$(function () {
new WOW().init();
})
</script>
<script>
$(document).ready(function () {
$("img.lazy").lazyload({
effect: "fadeIn"
});
$("#do_search").click(function () {
var key = $("#key_movie").val();
location.href = "{{ url_for('home.search',page=1) }}?key=" + key;
});
});
</script>
{% block js %}{% endblock %}
</body>
</html>
4. 完成注册页面
4.1 在home目录下的forms.py文件中,定义注册用的表单类RegistForm
可以通过调用RegistForm类直接在前端页面渲染生成注册使用的字段标签
from flask_wtf
import FlaskForm
from wtforms
.fields
import StringField
, PasswordField
, SubmitField
, FileField
, TextAreaField
from wtforms
.validators
import DataRequired
, EqualTo
, Email
, Regexp
, ValidationError
from app
.models
import User
class RegistForm(FlaskForm
):
name
= StringField
(
label
="呢称",
validators
=[
DataRequired
("请输入呢称!")
],
description
="呢称",
render_kw
={
"class": "form-control input-lg",
"placeholder": "请输入呢称!",
}
)
email
= StringField
(
label
="邮箱",
validators
=[
DataRequired
("请输入邮箱!"),
Email
("邮箱格式不正确!"),
],
description
="邮箱",
render_kw
={
"class": "form-control input-lg",
"placeholder": "请输入邮箱!"
}
)
phone
= StringField
(
label
="手机号",
validators
=[
DataRequired
("请输入手机号!"),
Regexp
("1[34578]\\d{9}", message
="输入的手机号格式不正确!"),
],
description
="手机号",
render_kw
={
"class": "form-control input-lg",
"placeholder": "请输入手机号!"
}
)
pwd
= PasswordField
(
label
="密码",
validators
=[
DataRequired
("请输入密码!")
],
description
="密码",
render_kw
={
"class": "form-control input-lg",
"placeholder": "请输入密码!"
}
)
repwd
= PasswordField
(
label
="确认密码",
validators
=[
DataRequired
("请输入确认密码!"),
EqualTo
("pwd", message
="两次密码不一致!")
],
description
="确认密码",
render_kw
={
"class": "form-control input-lg",
"placeholder": "请输入确认密码!"
}
)
submit
= SubmitField
(
"注册",
render_kw
={
"class": "btn btn-lg btn-success btn-block"
}
)
def validate_name(self
, field
):
name
= field
.data
user
= User
.query
.filter_by
(name
=name
).count
()
if user
== 1:
raise ValidationError
("呢称已经存在,请重新输入!")
def validate_email(self
, field
):
email
= field
.data
user
= User
.query
.filter_by
(email
=email
).count
()
if user
== 1:
raise ValidationError
("邮箱已经存在,请重新输入!")
def validate_phone(self
, field
):
phone
= field
.data
user
= User
.query
.filter_by
(phone
=phone
).count
()
if user
== 1:
raise ValidationError
("手机号已经存在,请重新输入!")
4.2 定义注册视图函数register
@home
.route
("/register/", methods
=["GET", "POST"])
def register():
form
= RegistForm
()
if form
.validate_on_submit
():
data
= form
.data
user
= User
(
name
=data
.get
("name"),
email
=data
.get
("email"),
phone
=data
.get
("phone"),
pwd
=generate_password_hash
(data
.get
("pwd")),
uuid
=uuid
.uuid4
().hex
)
db
.session
.add
(user
)
db
.session
.commit
()
flash
("注册成功!", "ok")
return redirect
(url_for
("home.login"))
return render_template
("home/register.html", form
=form
)
4.3 在home目录下创建登录前端页面register.html,继承home.html页面
{% extends 'home/home.html' %}
{% block content %}
<div class="row">
<div class="col-md-4 col-md-offset-4">
<div class="panel panel-success">
<div class="panel-heading">
<h3 class="panel-title"><span class="glyphicon glyphicon-plus"></span> 会员注册
</h3>
</div>
<div class="panel-body">
{% for msg in get_flashed_messages(category_filter=["err"]) %}
<p style="color:red">{{ msg }}
</p>
{% endfor %}
{% for msg in get_flashed_messages(category_filter=["ok"]) %}
<p style="color:green">{{ msg }}
</p>
{% endfor %}
<form role="form" method="post">
<fieldset>
<div class="form-group">
<label for="input_name"><span
class="glyphicon glyphicon-user"></span> {{ form.name.label }}
</label>
{{ form.name }}
</div>
{% for err in form.name.errors %}
<div class="col-md-12">
<font style="color:red">{{ err }}
</font>
</div>
{% endfor %}
<div class="form-group">
<label for="input_email"><span
class="glyphicon glyphicon-envelope"></span> {{ form.email.label }}
</label>
{{ form.email }}
</div>
{% for err in form.email.errors %}
<div class="col-md-12">
<font style="color:red">{{ err }}
</font>
</div>
{% endfor %}
<div class="form-group">
<label for="input_phone"><span
class="glyphicon glyphicon-phone"></span> {{ form.phone.label }}
</label>
{{ form.phone }}
</div>
{% for err in form.phone.errors %}
<div class="col-md-12">
<font style="color:red">{{ err }}
</font>
</div>
{% endfor %}
<div class="form-group">
<label for="input_password"><span
class="glyphicon glyphicon-lock"></span> {{ form.pwd.label }}
</label>
{{ form.pwd }}
</div>
{% for err in form.pwd.errors %}
<div class="col-md-12">
<font style="color:red">{{ err }}
</font>
</div>
{% endfor %}
<div class="form-group">
<label for="input_repassword"><span
class="glyphicon glyphicon-lock"></span> {{ form.repwd.label }}
</label>
{{ form.repwd }}
</div>
{% for err in form.repwd.errors %}
<div class="col-md-12">
<font style="color:red">{{ err }}
</font>
</div>
{% endfor %}
{{ form.submit }}
{{ form.csrf_token }}
</fieldset>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
5. 完成index页面
5.1 定义主页视图函数index
@home
.route
("/<int:page>/", methods
=['GET'])
def index(page
=None):
tags
= Tag
.query
.all()
page_data
= Movie
.query
tid
= request
.args
.get
("tid", 0)
if int(tid
) != 0:
page_data
= page_data
.filter_by
(tag_id
=int(tid
))
star
= request
.args
.get
("star", 0)
if int(star
) != 0:
page_data
= page_data
.filter(star
=int(star
))
time
= request
.args
.get
("time", 0)
if int(time
) != 0:
page_data
= page_data
.order_by
(Movie
.addtime
)
pm
= request
.args
.get
("pm", 0)
if int(pm
) != 0:
page_data
= page_data
.order_by
(Movie
.playnum
)
cm
= request
.args
.get
("cm", 0)
if int(cm
) != 0:
page_data
= page_data
.order_by
(Movie
.commentnum
)
if page
is None:
page
= 1
page_data
= page_data
.paginate
(page
=page
, per_page
=10)
p
= dict(
tid
=tid
,
star
=star
,
time
=time
,
pm
=pm
,
cm
=cm
)
return render_template
("home/index.html", tags
=tags
, p
=p
, page_data
=page_data
)
5.2 在templates目录下创建ui目录,在ui目录下创建home_page.html,定义分页显示组件
{% macro page(data,url) -%}
{% if data %}
<nav aria-label="Page navigation">
<ul class="pagination">
<li><a href="{{ url_for(url,page=1) }}">首页
</a></li>
{% if data.has_prev %}
<li><a href="{{ url_for(url,page=data.prev_num) }}">上一页
</a></li>
{% else %}
<li class="disabled"><a href="#">上一页
</li>
{% endif %}
{% for v in data.iter_pages() %}
{% if v == data.page %}
<li class="active"><a href="#">{{ v }}
</a></li>
{% else %}
<li><a href="{{ url_for(url,page=v) }}">{{ v }}
</a></li>
{% endif %}
{% endfor %}
{% if data.has_next %}
<li><a href="{{ url_for(url,page=data.next_num) }}">下一页
</a></li>
{% else %}
<li class="disabled"><a href="#">下一页
</a></li>
{% endif %}
<li><a href="{{ url_for(url,page=data.pages) }}">尾页
</a></li>
</ul>
</nav>
{% endif %}
{%- endmacro %}
5.3 在home目录下创建登录前端页面index.html,继承home.html页面
{% extends "home/layout.html" %}
{% import "ui/home_page.html" as pg %}
{% block content %}
<section id="hotmovie" style="margin-top:76px">
<div class="container">
<div class="row wow fadeInRight" data-wow-delay="0.6s">
<div class="row">
<iframe class="wow fadeIn" width="100%" height="375px" frameborder=0 scrolling=no
src="{{ url_for('home.animation') }}"></iframe>
</div>
</div>
</div>
</section>
<section id="movielist">
<div class="container">
<div class="row wow fadeIn" data-wow-delay="0.6s">
<div class="col-md-12 table-responsive">
<table class="table text-left table-bordered" id="movietags">
<tr>
<td style="width:10%;">电影标签
</td>
<td style="width:90%;">
{% for v in tags %}
<a href="{{ url_for('home.index',page=1) }}?tid={{ v.id }}&star={{ p['star'] }}&time={{ p['time'] }}&pm={{ p['pm'] }}&cm={{ p['cm'] }}"
class="label label-info"><span
class="glyphicon glyphicon-tag"></span> {{ v.name }}
</a>
{% endfor %}
</tr>
<tr>
<td>电影星级
</td>
<td>
{% for v in range(1,6) %}
<a href="{{ url_for('home.index',page=1) }}?tid={{ p['tid'] }}&star={{ v }}&time={{ p['time'] }}&pm={{ p['pm'] }}&cm={{ p['cm'] }}"
class="label label-warning"><span
class="glyphicon glyphicon-star"></span> {{ v }}星
</a>
{% endfor %}
</td>
</tr>
<tr>
<td>上映时间
</td>
<td>
<a href="{{ url_for('home.index',page=1) }}?tid={{ p['tid'] }}&star={{ p['star'] }}&time=1&pm={{ p['pm'] }}&cm={{ p['cm'] }}"
class="label label-default"><span
class="glyphicon glyphicon-time"></span> 最近
</span></a>
<a href="{{ url_for('home.index',page=1) }}?tid={{ p['tid'] }}&star={{ p['star'] }}&time=2&pm={{ p['pm'] }}&cm={{ p['cm'] }}"
class="label label-default"><span
class="glyphicon glyphicon-time"></span> 更早
</span></a>
</td>
</tr>
<tr>
<td>播放数量
</td>
<td>
<a href="{{ url_for('home.index',page=1) }}?tid={{ p['tid'] }}&star={{ p['star'] }}&time={{ p['time'] }}&pm=1&cm={{ p['cm'] }}"
class="label label-success"><span class="glyphicon glyphicon-arrow-down"></span> 从高到底
</span>
</a>
<a href="{{ url_for('home.index',page=1) }}?tid={{ p['tid'] }}&star={{ p['star'] }}&time={{ p['time'] }}&pm=2&cm={{ p['cm'] }}"
class="label label-danger"><span class="glyphicon glyphicon-arrow-up"></span> 从低到高
</span>
</a>
</td>
</tr>
<tr>
<td>评论数量
</td>
<td>
<a href="{{ url_for('home.index',page=1) }}?tid={{ p['tid'] }}&star={{ p['star'] }}&time={{ p['time'] }}&pm={{ p['pm'] }}&cm=1"
class="label label-success"><span class="glyphicon glyphicon-arrow-down"></span> 从高到底
</span>
</a>
<a href="{{ url_for('home.index',page=1) }}?tid={{ p['tid'] }}&star={{ p['star'] }}&time={{ p['time'] }}&pm={{ p['pm'] }}&cm=2"
class="label label-danger"><span class="glyphicon glyphicon-arrow-up"></span> 从低到高
</span>
</a>
</td>
</tr>
</table>
</div>
{% for v in page_data.items %}
<div class="col-md-3">
<div class="movielist text-center">
<img src="{{ url_for('static',filename='uploads/'+v.logo) }}"
class="img-responsive center-block" alt="">
<div class="text-left" style="margin-left:auto;margin-right:auto;width:210px;">
<span style="color:#999;font-style: italic;">{{ v.title }}
</span><br>
<div>
{% for val in range(1,v.star+1) %}
<span class="glyphicon glyphicon-star" style="color:#FFD119"></span>
{% endfor %}
{% for val in range(1,6-v.star) %}
<span class="glyphicon glyphicon-star-empty" style="color:#FFD119"></span>
{% endfor %}
</div>
</div>
<a href="{{ url_for('home.play',id=v.id,page=1) }}" class="btn btn-primary" target="_blank"
role="button"><span
class="glyphicon glyphicon-play"></span> 播放
</a>
</div>
</div>
{% endfor %}
<div class="col-md-12">
{{ pg.page(page_data,'home.index') }}
</div>
</div>
</div>
</section>
{% endblock %}
注:本文转载于:https://www.cnblogs.com/renpingsheng/p/9074025.html