商品详情页
商品详情页依然采用页面静态化技术。
商品详情页的静态化由运营人员在编辑商品信息时触发生成静态化页面。
先来实现静态化异步任务,在celery_tasks中新建html/tasks.py任务
from celery_tasks.main import app
from django.template import loader
from django.conf import settings
import os
from goods.utils import get_categories
from goods.models import SKU
@app.task(name='generate_static_sku_detail_html')
def generate_static_sku_detail_html(sku_id):
"""
生成静态商品详情页面
:param sku_id: 商品sku id
"""
# 商品分类菜单
categories = get_categories()
# 获取当前sku的信息
sku = SKU.objects.get(id=sku_id)
sku.images = sku.skuimage_set.all()
# 面包屑导航信息中的频道
goods = sku.goods
goods.channel = goods.category1.goodschannel_set.all()[0]
# 构建当前商品的规格键
# sku_key = [规格1参数id, 规格2参数id, 规格3参数id, ...]
sku_specs = sku.skuspecification_set.order_by('spec_id')
sku_key = []
for spec in sku_specs:
sku_key.append(spec.option.id)
# 获取当前商品的所有SKU
skus = goods.sku_set.all()
# 构建不同规格参数(选项)的sku字典
# spec_sku_map = {
# (规格1参数id, 规格2参数id, 规格3参数id, ...): sku_id,
# (规格1参数id, 规格2参数id, 规格3参数id, ...): sku_id,
# ...
# }
spec_sku_map = {}
for s in skus:
# 获取sku的规格参数
s_specs = s.skuspecification_set.order_by('spec_id')
# 用于形成规格参数-sku字典的键
key = []
for spec in s_specs:
key.append(spec.option.id)
# 向规格参数-sku字典添加记录
spec_sku_map[tuple(key)] = s.id
# 获取当前商品的规格信息
#specs = [
# {
# 'name': '屏幕尺寸',
# 'options': [
# {'value': '13.3寸', 'sku_id': xxx},
# {'value': '15.4寸', 'sku_id': xxx},
# ]
# },
# {
# 'name': '颜色',
# 'options': [
# {'value': '银色', 'sku_id': xxx},
# {'value': '黑色', 'sku_id': xxx}
# ]
# },
# ...
#]
specs = goods.goodsspecification_set.order_by('id')
# 若当前sku的规格信息不完整,则不再继续
if len(sku_key) < len(specs):
return
for index, spec in enumerate(specs):
# 复制当前sku的规格键
key = sku_key[:]
# 该规格的选项
options = spec.specificationoption_set.all()
for option in options:
# 在规格参数sku字典中查找符合当前规格的sku
key[index] = option.id
option.sku_id = spec_sku_map.get(tuple(key))
spec.options = options
# 渲染模板,生成静态html文件
context = {
'categories': categories,
'goods': goods,
'specs': specs,
'sku': sku
}
template = loader.get_template('detail.html')
html_text = template.render(context)
file_path = os.path.join(settings.GENERATED_STATIC_HTML_FILES_DIR, 'goods/'+str(sku_id)+'.html')
with open(file_path, 'w') as f:
f.write(html_text)
将形成商品类别部分的数据封装成一个公共函数,放在goods/utils.py中
from collections import OrderedDict
from .models import GoodsChannel
def get_categories():
"""
获取商城商品分类菜单
:return 菜单字典
"""
# 商品频道及分类菜单
# 使用有序字典保存类别的顺序
# categories = {
# 1: { # 组1
# 'channels': [{'id':, 'name':, 'url':},{}, {}...],
# 'sub_cats': [{'id':, 'name':, 'sub_cats':[{},{}]}, {}, {}, ..]
# },
# 2: { # 组2
#
# }
# }
categories = OrderedDict()
channels = GoodsChannel.objects.order_by('group_id', 'sequence')
for channel in channels:
group_id = channel.group_id # 当前组
if group_id not in categories:
categories[group_id] = {'channels': [], 'sub_cats': []}
cat1 = channel.category # 当前频道的类别
# 追加当前频道
categories[group_id]['channels'].append({
'id': cat1.id,
'name': cat1.name,
'url': channel.url
})
# 构建当前类别的子类别
for cat2 in cat1.goodscategory_set.all():
cat2.sub_cats = []
for cat3 in cat2.goodscategory_set.all():
cat2.sub_cats.append(cat3)
categories[group_id]['sub_cats'].append(cat2)
return categories
页面模板
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<title>美多商城-商品详情</title>
<link rel="stylesheet" type="text/css" href="/css/reset.css">
<link rel="stylesheet" type="text/css" href="/css/main.css">
<script type="text/javascript" src="/js/host.js"></script>
<script type="text/javascript" src="/js/vue-2.5.16.js"></script>
<script type="text/javascript" src="/js/axios-0.18.0.min.js"></script>
</head>
<body>
<div id="app" v-cloak>
<div class="header_con">
<div class="header">
<div class="welcome fl">欢迎来到美多商城!</div>
<div class="fr">
<div v-if="username" class="login_btn fl">
欢迎您:<em>[[ username ]]</em>
<span>|</span>
<a @click="logout">退出</a>
</div>
<div v-else class="login_btn fl">
<a href="/login.html">登录</a>
<span>|</span>
<a href="/register.html">注册</a>
</div>
<div class="user_link fl">
<span>|</span>
<a href="/user_center_info.html">用户中心</a>
<span>|</span>
<a href="/cart.html">我的购物车</a>
<span>|</span>
<a href="/user_center_order.html">我的订单</a>
</div>
</div>
</div>
</div>
<div class="search_bar clearfix">
<a href="/index.html" class="logo fl"><img src="/images/logo.png"></a>
<div class="search_wrap fl">
<form method="get" action="/search.html" class="search_con">
<input type="text" class="input_text fl" name="q" placeholder="搜索商品">
<input type="submit" class="input_btn fr" name="" value="搜索">
</form>
<ul class="search_suggest fl">
<li><a href="#">索尼微单</a></li>
<li><a href="#">优惠15元</a></li>
<li><a href="#">美妆个护</a></li>
<li><a href="#">买2免1</a></li>
</ul>
</div>
<div class="guest_cart fr">
<a href="#" class="cart_name fl">我的购物车</a>
<div class="goods_count fl" id="show_count">15</div>
<ul class="cart_goods_show">
<li>
<img src="images/goods/goods001.jpg" alt="商品图片">
<h4>商品名称手机</h4>
<div>4</div>
</li>
<li>
<img src="images/goods/goods002.jpg" alt="商品图片">
<h4>商品名称手机</h4>
<div>5</div>
</li>
<li>
<img src="images/goods/goods003.jpg" alt="商品图片">
<h4>商品名称手机</h4>
<div>6</div>
</li>
<li>
<img src="images/goods/goods003.jpg" alt="商品图片">
<h4>商品名称手机</h4>
<div>6</div>
</li>
</ul>
</div>
</div>
<div class="navbar_con">
<div class="navbar">
<div class="sub_menu_con fl">
<h1 class="fl">商品分类</h1>
<ul class="sub_menu">
{% for group in categories.values %}
<li>
<div class="level1">
{% for channel in group.channels %}
<a href="{{ channel.url }}">{{ channel.name }}</a>
{% endfor %}
</div>
<div class="level2">
{% for cat2 in group.sub_cats %}
<div class="list_group">
<div class="group_name fl">{{cat2.name}} ></div>
<div class="group_detail fl">
{% for cat3 in cat2.sub_cats %}
<a href="/list.html?cat={{cat3.id}}">{{cat3.name}}</a>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
</li>
{% endfor %}
</ul>
</div>
<ul class="navlist fl">
<li><a href="">首页</a></li>
<li class="interval">|</li>
<li><a href="">真划算</a></li>
<li class="interval">|</li>
<li><a href="">抽奖</a></li>
</ul>
</div>
</div>
<div class="breadcrumb">
<a href="{{ goods.channel.url }}">{{ goods.category1.name }}</a>
<span>></span>
<span>{{ goods.category2.name }}</span>
<span>></span>
<a href="/list.html?cat={{ goods.category3.id }}">{{goods.category3.name }}</a>
</div>
<div class="goods_detail_con clearfix">
<div class="goods_detail_pic fl"><img src="{{ sku.default_image_url }}"></div>
<div class="goods_detail_list fr">
<h3>{{ sku.name }}</h3>
<p>{{ sku.caption }}</p>
<div class="prize_bar">
<span class="show_pirze">¥<em>{{ sku.price }}</em></span><span> 市场价¥{{sku.market_price}}</span>
<a href="javascript:;" class="goods_judge">{{ sku.comments }}人评价</a>
</div>
<div class="goods_num clearfix">
<div class="num_name fl">数 量:</div>
<div class="num_add fl">
<input v-model="sku_count" type="text" class="num_show fl">
<a @click="sku_count++" class="add fr">+</a>
<a @click="on_minus()" class="minus fr">-</a>
</div>
</div>
{% for spec in specs %}
<div class="type_select">
<label>{{ spec.name }}:</label>
{% for option in spec.options %}
{% if option.sku_id == sku.id %}
<a href="javascript:;" class="select">{{ option.value }}</a>
{% elif option.sku_id %}
<a href="/goods/{{option.sku_id}}.html">{{ option.value }}</a>
{% else %}
<a href="javascript:;">{{ option.value }}</a>
{% endif %}
{% endfor %}
</div>
{% endfor %}
<div class="total">总价:<em>[[sku_amount]]元</em></div>
<div class="operate_btn">
<a @click="add_cart" class="add_cart" id="add_cart">加入购物车</a>
</div>
</div>
</div>
<div class="main_wrap clearfix">
<div class="l_wrap fl clearfix">
<div class="new_goods">
<h3>热销排行</h3>
<ul>
<li v-for="sku in hots">
<a :href="sku.url"><img :src="sku.default_image_url"></a>
<h4><a :href="sku.url">[[sku.name]]</a></h4>
<div class="prize">¥[[sku.price]]</div>
</li>
</ul>
</div>
</div>
<div class="r_wrap fr clearfix">
<ul class="detail_tab clearfix">
<li @click="on_tab_content('detail')" :class="tab_content.detail?'active':''">商品详情</li>
<li @click="on_tab_content('pack')" :class="tab_content.pack?'active':''">规格与包装</li>
<li @click="on_tab_content('comment')" :class="tab_content.comment?'active':''">商品评价([[comments.length]])</li>
<li @click="on_tab_content('service')" :class="tab_content.service?'active':''">售后服务</li>
</ul>
<div @click="on_tab_content('detail')" class="tab_content" :class="tab_content.detail?'current':''">
<dl>
<dt>商品详情:</dt>
<dd>{{ goods.desc_detail|safe }}</dd>
</dl>
</div>
<div @click="on_tab_content('pack')" class="tab_content" :class="tab_content.pack?'current':''">
<dl>
<dt>规格与包装:</dt>
<dd>{{ goods.desc_pack|safe }}</dd>
</dl>
</div>
<div @click="on_tab_content('comment')" class="tab_content" :class="tab_content.comment?'current':''">
<ul class="judge_list_con">
<li class="judge_list fl" v-for="comment in comments">
<div class="user_info fl">
<b>[[comment.username]]</b>
</div>
<div class="judge_info fl">
<div :class="comment.score_class"></div>
<div class="judge_detail">[[comment.comment]]</div>
</div>
</li>
</ul>
</div>
<div @click="on_tab_content('service')" class="tab_content" :class="tab_content.service?'current':''">
<dl>
<dt>售后服务:</dt>
<dd>{{ goods.desc_service|safe }}</dd>
</dl>
</div>
</div>
</div>
<div class="footer">
<div class="foot_link">
<a href="#">关于我们</a>
<span>|</span>
<a href="#">联系我们</a>
<span>|</span>
<a href="#">招聘人才</a>
<span>|</span>
<a href="#">友情链接</a>
</div>
<p>CopyRight © 2016 北京美多商业股份有限公司 All Rights Reserved</p>
<p>电话:010-****888 京ICP备*******8号</p>
</div>
</div>
<script type="text/javascript"></script>
<script type="text/javascript" src="/js/detail.js"></script>
</body>
</html>
页面的detail.js
var vm = new Vue({
el: '#app',
delimiters: ['[[', ']]'],
data: {
host,
username: sessionStorage.username || localStorage.username,
user_id: sessionStorage.user_id || localStorage.user_id,
token: sessionStorage.token || localStorage.token,
tab_content: {
detail: true,
pack: false,
comment: false,
service: false
},
sku_id: '',
sku_count: 1,
sku_price: price,
cart_total_count: 0, // 购物车总数量
cart: [], // 购物车数据
hots: [], // 热销商品
cat: cat, // 商品类别
comments: [], // 评论信息
score_classes: {
1: 'stars_one',
2: 'stars_two',
3: 'stars_three',
4: 'stars_four',
5: 'stars_five',
}
},
computed: {
sku_amount: function(){
return (this.sku_price * this.sku_count).toFixed(2);
}
},
mounted: function(){
// 添加用户浏览历史记录
this.get_sku_id();
this.get_cart();
this.get_hot_goods();
this.get_comments();
},
methods: {
// 退出
logout: function(){
sessionStorage.clear();
localStorage.clear();
location.href = '/login.html';
},
// 控制页面标签页展示
on_tab_content: function(name){
this.tab_content = {
detail: false,
pack: false,
comment: false,
service: false
};
this.tab_content[name] = true;
},
// 从路径中提取sku_id
get_sku_id: function(){
var re = /^\/goods\/(\d+).html$/;
this.sku_id = document.location.pathname.match(re)[1];
},
// 减小数值
on_minus: function(){
if (this.sku_count > 1) {
this.sku_count--;
}
},
// 添加购物车
add_cart: function(){
},
// 获取购物车数据
get_cart: function(){
},
// 获取热销商品数据
get_hot_goods: function(){
},
// 获取商品评价信息
get_comments: function(){
}
}
});
异步任务的触发
运营人员在Admin站点保存商品信息时,应该触发生成商品静态页的异步任务。
我们需要调整Admin站点保存和删除商品信息时行为。
在Admin站点保存或删除数据时,Django是调用的Admin站点管理器类的save_model()方法和delete_model()方法,我们只需重新实现这两个方法,在这两个方法中调用异步任务即可。
编辑goods/admin.py
from django.contrib import admin
# Register your models here.
from . import models
class SKUAdmin(admin.ModelAdmin):
def save_model(self, request, obj, form, change):
obj.save()
from celery_tasks.html.tasks import generate_static_sku_detail_html
generate_static_sku_detail_html.delay(obj.id)
class SKUSpecificationAdmin(admin.ModelAdmin):
def save_model(self, request, obj, form, change):
obj.save()
from celery_tasks.html.tasks import generate_static_sku_detail_html
generate_static_sku_detail_html.delay(obj.sku.id)
def delete_model(self, request, obj):
sku_id = obj.sku.id
obj.delete()
from celery_tasks.html.tasks import generate_static_sku_detail_html
generate_static_sku_detail_html.delay(sku_id)
class SKUImageAdmin(admin.ModelAdmin):
def save_model(self, request, obj, form, change):
obj.save()
from celery_tasks.html.tasks import generate_static_sku_detail_html
generate_static_sku_detail_html.delay(obj.sku.id)
# 设置SKU默认图片
sku = obj.sku
if not sku.default_image_url:
sku.default_image_url = obj.image.url
sku.save()
def delete_model(self, request, obj):
sku_id = obj.sku.id
obj.delete()
from celery_tasks.html.tasks import generate_static_sku_detail_html
generate_static_sku_detail_html.delay(sku_id)
admin.site.register(models.GoodsCategory)
admin.site.register(models.GoodsChannel)
admin.site.register(models.Goods)
admin.site.register(models.Brand)
admin.site.register(models.GoodsSpecification)
admin.site.register(models.SpecificationOption)
admin.site.register(models.SKU, SKUAdmin)
admin.site.register(models.SKUSpecification, SKUSpecificationAdmin)
admin.site.register(models.SKUImage, SKUImageAdmin)
脚本工具
为了开发方便,我们还可以编写手动生成所有商品静态页面的脚本regenerate_detail_html.py
#!/usr/bin/env python
"""
功能:手动生成所有SKU的静态detail html文件
使用方法:
./regenerate_detail_html.py
"""
import sys
sys.path.insert(0, '../')
import os
if not os.getenv('DJANGO_SETTINGS_MODULE'):
os.environ['DJANGO_SETTINGS_MODULE'] = 'meiduo_mall.settings.dev'
import django
django.setup()
from django.template import loader
from django.conf import settings
from goods.utils import get_categories
from goods.models import SKU
def generate_static_sku_detail_html(sku_id):
"""
生成静态商品详情页面
:param sku_id: 商品sku id
"""
# 商品分类菜单
categories = get_categories()
# 获取当前sku的信息
sku = SKU.objects.get(id=sku_id)
sku.images = sku.skuimage_set.all()
# 面包屑导航信息中的频道
goods = sku.goods
goods.channel = goods.category1.goodschannel_set.all()[0]
# 构建当前商品的规格键
sku_specs = sku.skuspecification_set.order_by('spec_id')
sku_key = []
for spec in sku_specs:
sku_key.append(spec.option.id)
# 获取当前商品的所有SKU
skus = goods.sku_set.all()
# 构建不同规格参数(选项)的sku字典
# spec_sku_map = {
# (规格1参数id, 规格2参数id, 规格3参数id, ...): sku_id,
# (规格1参数id, 规格2参数id, 规格3参数id, ...): sku_id,
# ...
# }
spec_sku_map = {}
for s in skus:
# 获取sku的规格参数
s_specs = s.skuspecification_set.order_by('spec_id')
# 用于形成规格参数-sku字典的键
key = []
for spec in s_specs:
key.append(spec.option.id)
# 向规格参数-sku字典添加记录
spec_sku_map[tuple(key)] = s.id
# 获取当前商品的规格信息
specs = goods.goodsspecification_set.order_by('id')
# 若当前sku的规格信息不完整,则不再继续
if len(sku_key) < len(specs):
return
for index, spec in enumerate(specs):
# 复制当前sku的规格键
key = sku_key[:]
# 该规格的选项
options = spec.specificationoption_set.all()
for option in options:
# 在规格参数sku字典中查找符合当前规格的sku
key[index] = option.id
option.sku_id = spec_sku_map.get(tuple(key))
spec.options = options
# 渲染模板,生成静态html文件
context = {
'categories': categories,
'goods': goods,
'specs': specs,
'sku': sku
}
template = loader.get_template('detail.html')
html_text = template.render(context)
file_path = os.path.join(settings.GENERATED_STATIC_HTML_FILES_DIR, 'goods/'+str(sku_id)+'.html')
with open(file_path, 'w') as f:
f.write(html_text)
if __name__ == '__main__':
skus = SKU.objects.all()
for sku in skus:
print(sku.id)
generate_static_sku_detail_html(sku.id)