分类筛选前端

This commit is contained in:
pushuo 2023-01-11 17:46:45 +08:00
parent 79f6f77e2d
commit fe66b21e9c
22 changed files with 456 additions and 50 deletions

View File

@ -41,14 +41,14 @@ class ProductRepo
* 通过单个或多个商品分类获取商品列表
*
* @param $categoryId
* @return AnonymousResourceCollection
* @return
*/
public static function getProductsByCategory($categoryId, $filterData): AnonymousResourceCollection
public static function getProductsByCategory($categoryId, $filterData)
{
$builder = self::getBuilder(array_merge(['category_id' => $categoryId, 'active' => 1], $filterData));
$products = $builder->with('inCurrentWishlist')->paginate(perPage());
$products = $builder->with('inCurrentWishlist')->paginate($filterData['per_page'] ?? perPage());
return ProductSimple::collection($products);
return $products;
}
/**
@ -161,7 +161,7 @@ class ProductRepo
$builder->onlyTrashed();
}
$sort = $data['sort'] ?? 'products.updated_at';
$sort = $data['sort'] ?? 'products.position';
$order = $data['order'] ?? 'desc';
$builder->orderBy($sort, $order);
@ -193,7 +193,7 @@ class ProductRepo
->select(['pa.attribute_id', 'pa.attribute_value_id'])
->distinct()
->reorder('pa.attribute_id');
$productAttributes = $builder->get();
$productAttributes = $builder->get()->toArray();
$attributeMap = array_column(Attribute::query()->with('description')->orderBy('sort_order')->get()->toArray(), null, 'id');
$attributeValueMap = array_column(AttributeValue::query()->with('description')->get()->toArray(), null, 'id');
@ -222,7 +222,12 @@ class ProductRepo
}
}
return $results;
$results = array_map(function($item) {
$item['values'] = array_values($item['values']);
return $item;
}, $results);
return array_values($results);
}
public static function getFilterPrice($data)

View File

@ -5,6 +5,7 @@ namespace Beike\Shop\Http\Controllers;
use Beike\Models\Category;
use Beike\Repositories\CategoryRepo;
use Beike\Repositories\ProductRepo;
use Beike\Shop\Http\Resources\ProductSimple;
use Illuminate\Http\Request;
class CategoryController extends Controller
@ -16,16 +17,16 @@ class CategoryController extends Controller
public function show(Request $request, Category $category)
{
$filterData = $request->only('attr', 'price');
$filterData = $request->only('attr', 'price', 'sort', 'order', 'per_page');
$products = ProductRepo::getProductsByCategory($category->id, $filterData);
$category->load('description');
$filterData = array_merge($filterData, ['category_id' => $category->id, 'active' => 1]);
$data = [
'all_categories' => CategoryRepo::getTwoLevelCategories(),
'category' => $category,
'filter_data' => ['attr' => ProductRepo::getFilterAttribute($filterData), 'price' => ProductRepo::getFilterPrice($filterData)],
'products_format' => $products->jsonSerialize(),
'products_format' => ProductSimple::collection($products)->jsonSerialize(),
'products' => $products,
'per_pages' => CategoryRepo::getPerPages(),
];

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -185,6 +185,7 @@
$http.post('design/builder/preview?design=1', data, {hload: true}).then((res) => {
$(previewWindow.document).find('#module-' + data.module_id).replaceWith(res);
$(previewWindow.document).find('.tooltip').remove();
const tooltipTriggerList = previewWindow.document.querySelectorAll('[data-bs-toggle="tooltip"]')
const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new previewWindow.bootstrap.Tooltip(tooltipTriggerEl))
})

View File

@ -324,14 +324,7 @@
<div class="tab-pane fade" id="tab-seo">
<h6 class="border-bottom pb-3 mb-4">SEO</h6>
<x-admin::form.row title="Meta title">
@foreach ($languages as $language)
<div class="input-group w-max-600">
<span class="input-group-text wp-100">{{ $language['name'] }}</span>
<textarea rows="2" type="text" name="descriptions[{{ $language['code'] }}][meta_title]" class="form-control wp-400" placeholder="Meta title">{{ old('meta_title', $product->descriptions->keyBy('locale')[$language->code]->meta_title ?? '') }}</textarea>
</div>
@endforeach
</x-admin::form.row>
<x-admin-form-input-locale :width="600" name="descriptions.*.meta_title" title="Meta title" :value="$descriptions"/>
<x-admin::form.row title="Meta keywords">
@foreach ($languages as $language)
<div class="input-group w-max-600">

View File

@ -21,6 +21,7 @@ $primary: #fd560f;
@import './page-product';
@import './cart';
@import './page-checkout';
@import './page-categories';
@import './element-ui';
@import './order-success';
@import './page-account-order';

View File

@ -9,5 +9,100 @@
*/
body.page-categories {
.right-column {
@media (min-width: 992px) {
width: 78%;
}
}
.left-column {
@media (min-width: 992px) {
width: 22%;
}
.card:not(:last-of-type) {
border-bottom: 1px solid #E6E6E6;
margin-bottom: 1.4rem;
padding-bottom: 1.4rem;
}
}
.style-wrap {
span {
cursor: pointer;
&.active {
svg {
fill: $primary;
}
}
svg {
fill: #999;
}
}
}
.filter-value-wrap {
.list-group {
display: block;
.list-group-item {
display: inline-block;
cursor: pointer;
font-size: 12px;
background: #f3f3f3;
border: none;
color: #666;
padding: 4px 12px;
&.delete-all {
background: $primary;
color: #fff;
}
&:hover {
background: $primary;
color: #fff;
}
}
}
}
.product-tool {
.order-wrap {
min-width: 150px;
}
}
.ui-widget-content {
border-radius: 0;
position: relative;
border: none;
margin-right: 4px;
margin-bottom: 0;
background: none;
cursor: pointer;
.ui-widget-header {
background: $primary;
position: absolute;
top: 50%;
border-radius: 0;
height: 2px;
margin-top: -1px;
}
.ui-slider-active {
border: none;
}
.ui-slider-handle {
width: 4px;
margin-left: 0;
cursor: ew-resize;
border: none !important;
border-radius: 0;
background: $primary;
}
}
}
}

View File

@ -8,6 +8,28 @@
* @LastEditTime 2022-09-16 20:56:27
*/
.product-list-wrap {
.col-12:not(:last-of-type) {
.product-wrap {
border-bottom: 1px solid #E6E6E6;
margin-bottom: 1.4rem;
padding-bottom: 1.4rem;
&:hover {
box-shadow: none;
.image {
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
}
.button-wrap {
bottom: 10px;
opacity: 1;
}
}
}
}
}
.product-wrap {
margin-bottom: 20px;
text-align: center;
@ -15,6 +37,28 @@
transition: all 0.3s ease-in-out;
background-color: #fff;
&.list {
display: flex;
padding-bottom: 0;
.image {
width: 200px;
margin-bottom: 0;
transition: all 0.3s ease-in-out;
}
.product-bottom-info {
padding-top: 10px;
padding-left: 20px;
flex: 1;
text-align: left;
.product-name {
font-size: 18px;
}
}
}
.image {
margin-bottom: 10px;
position: relative;

View File

@ -144,4 +144,20 @@ export default {
return typeof(defaultValue) != 'undefined' ? defaultValue : '';
},
removeURLParameters(url, ...parameters) {
const parsed = new URL(url);
parameters.forEach(e => parsed.searchParams.delete(e))
return parsed.toString()
},
updateQueryStringParameter(uri, key, value) {
var re = new RegExp("([?&])" + key + "=.*?(&|$)", "i");
var separator = uri.indexOf('?') !== -1 ? "&" : "?";
if (uri.match(re)) {
return uri.replace(re, '$1' + key + "=" + value + '$2');
} else {
return uri + separator + key + "=" + value;
}
},
}

View File

@ -38,6 +38,7 @@ return [
'no' => 'nein',
'yes' => 'ja',
'delete' => 'löschen',
'delete_all' => 'alle löschen',
'sign_out' => 'Abmelden',
'contact_us' => 'kontaktiere uns',
'input' => 'Geben Sie hier Ihre Suche ein',

View File

@ -39,6 +39,7 @@ return [
'no' => 'No',
'yes' => 'Yes',
'delete' => 'Delete',
'delete_all' => 'Delete all',
'sign_out' => 'Sign Out',
'contact_us' => 'Contact us',
'input' => 'Type your search here',
@ -67,6 +68,9 @@ return [
'whether_open' => 'Status',
'default' => 'Default',
'to_setting' => 'to configure',
'low' => 'Low',
'high' => 'High',
'sales' => 'Sales',
'id' => 'ID',
'created_at' => 'Created At',

View File

@ -38,6 +38,7 @@ return [
'no' => 'no',
'yes' => 'Sí',
'delete' => 'Eliminar',
'delete_all' => 'borrar todo',
'sign_out' => 'desconectar',
'contact_us' => 'Contáctenos',
'input' => 'ingrese su búsqueda aquí',

View File

@ -38,6 +38,7 @@ return [
'no' => 'non',
'yes' => 'Oui',
'delete' => 'effacer',
'delete_all' => 'supprimer tout',
'sign_out' => 'se déconnecter',
'contact_us' => 'Nous contacter',
'input' => 'entrez votre recherche ici',

View File

@ -38,6 +38,7 @@ return [
'no' => 'No',
'yes' => 'Yes',
'delete' => 'Elimina',
'delete_all' => 'elimina tutto',
'sign_out' => 'disconnessione',
'contact_us' => 'Contattaci',
'input' => 'inserisci qui la tua ricerca',

View File

@ -38,6 +38,7 @@ return [
'no' => '番号',
'yes' => 'はい',
'delete' => '消去',
'delete_all' => 'すべて削除',
'sign_out' => 'サインアウト',
'contact_us' => 'お問い合わせ',
'input' => 'ここに検索を入力してください',

View File

@ -38,6 +38,7 @@ return [
'no' => 'нет',
'yes' => 'да',
'delete' => 'удалить',
'delete_all' => 'удалить все',
'sign_out' => 'Выйти',
'contact_us' => 'свяжитесь с нами',
'input' => 'Введите свой поиск здесь',

View File

@ -38,6 +38,7 @@ return [
'no' => '否',
'yes' => '是',
'delete' => '删除',
'delete_all' => '删除所有',
'sign_out' => '退出登录',
'contact_us' => '联系我们',
'input' => '在此处输入您的搜索',
@ -67,6 +68,9 @@ return [
'whether_open' => '是否开启',
'default' => '默认',
'to_setting' => '去配置',
'low' => '低',
'high' => '高',
'sales' => '销量',
'id' => 'ID',
'created_at' => '创建时间',

View File

@ -38,6 +38,7 @@ return [
'no' => '否',
'yes' => '是',
'delete' => '刪除',
'delete_all' => '刪除所有',
'sign_out' => '退出登錄',
'contact_us' => '聯繫我們',
'input' => '在此處輸入您的搜索',
@ -66,6 +67,9 @@ return [
'menu' => '菜單',
'whether_open' => '是否開啟',
'to_setting' => '去配置',
'low' => '低',
'high' => '高',
'sales' => '銷量',
'id' => 'ID',
'created_at' => '創建時間',

View File

@ -31,7 +31,9 @@
<el-input @keyup.enter.native="checkedBtnLogin('loginForm')" type="password" v-model="loginForm.password" placeholder="{{ __('shop/login.password') }}"></el-input>
</el-form-item>
<a class="text-muted forgotten-link" href="{{ shop_route('forgotten.index') }}"><i class="bi bi-question-circle"></i> {{ __('shop/login.forget_password') }}</a>
@if (!request('iframe'))
<a class="text-muted forgotten-link" href="{{ shop_route('forgotten.index') }}"><i class="bi bi-question-circle"></i> {{ __('shop/login.forget_password') }}</a>
@endif
<div class="mt-4 mb-3">
<button type="button" @click="checkedBtnLogin('loginForm')" class="btn btn-dark btn-lg w-100 fw-bold"><i class="bi bi-box-arrow-in-right"></i> {{ __('shop/login.login') }}</button>

View File

@ -4,19 +4,114 @@
@section('keywords', $category->description->meta_keywords ?: system_setting('base.meta_keyword'))
@section('description', $category->description->meta_description ?: system_setting('base.meta_description'))
@push('header')
<script src="{{ asset('vendor/jquery/jquery-ui/jquery-ui.min.js') }}"></script>
<link rel="stylesheet" href="{{ asset('vendor/jquery/jquery-ui/jquery-ui.min.css') }}">
@endpush
@section('content')
<div class="container">
<x-shop-breadcrumb type="category" :value="$category" />
<div class="row">
@if (count($products_format))
@foreach ($products_format as $product)
<div class="col-6 col-md-3">@include('shared.product')</div>
@endforeach
@else
<div class="col-12 col-lg-3 pe-lg-4 left-column">
<div class="filter-box">
@if ($filter_data['price']['min'] != $filter_data['price']['max'])
<div class="card">
<div class="card-header p-0">
<h4 class="mb-3">{{ __('product.price') }}</h4>
</div>
<div class="card-body p-0">
<div class="text-secondary mb-3 price-range">
{{ currency_format($filter_data['price']['select_min'], current_currency_code()) }}
-
{{ currency_format($filter_data['price']['select_max'], current_currency_code()) }}
</div>
<input value="{{ $filter_data['price']['select_min'] }}" class="price-min d-none">
<input value="{{ $filter_data['price']['select_max'] }}" class="price-max d-none">
<div id="slider" class="mb-2"></div>
</div>
</div>
@endif
@foreach ($filter_data['attr'] as $index => $attr)
<div class="card">
<div class="card-header fw-bold p-0">
<h4 class="mb-3">{{ $attr['name'] }}</h4>
</div>
<ul class="list-group list-group-flush attribute-item" data-attribute-id="{{ $attr['id'] }}">
@foreach ($attr['values'] as $value_index => $value)
<li class="list-group-item border-0 px-0">
<label class="form-check-label d-block">
<input class="form-check-input attr-value-check me-2" data-attr="{{ $index }}" data-attrval="{{ $value_index }}" {{ $value['selected'] ? 'checked' : '' }} name="6" type="checkbox" value="{{ $value['id'] }}">{{ $value['name'] }}
</label>
</li>
@endforeach
</ul>
</div>
@endforeach
</div>
</div>
<div class="col-12 col-lg-9 right-column">
<div class="filter-value-wrap mb-2 d-none">
<ul class="list-group list-group-horizontal">
@foreach ($filter_data['attr'] as $index => $attr)
@foreach ($attr['values'] as $value_index => $value)
@if ($value['selected'])
<li class="list-group-item me-1 mb-1" data-attr="{{ $index }}" data-attrval="{{ $value_index }}">
{{ $attr['name'] }}: {{ $value['name'] }} <i class="bi bi-x-lg ms-1"></i>
</li>
@endif
@endforeach
@endforeach
<li class="list-group-item me-1 mb-1 delete-all">{{ __('common.delete_all') }}</li>
</ul>
</div>
<div class="product-tool d-flex justify-content-between align-items-center mb-4">
<div class="style-wrap">
<span class="{{ !request('style_list') || request('style_list') == 'grid' ? 'active' : ''}}">
<svg viewBox="0 0 19 19" xmlns="http://www.w3.org/2000/svg" width="18" height="18"><rect width="5" height="5"></rect><rect x="7" width="5" height="5"></rect><rect x="14" width="5" height="5"></rect><rect y="7" width="5" height="5"></rect><rect x="7" y="7" width="5" height="5"></rect><rect x="14" y="7" width="5" height="5"></rect><rect y="14" width="5" height="5"></rect><rect x="7" y="14" width="5" height="5"></rect><rect x="14" y="14" width="5" height="5"></rect></svg>
</span>
<span class="ms-1 class="{{ request('style_list') == 'list' ? 'active' : ''}}">
<svg viewBox="0 0 19 19" xmlns="http://www.w3.org/2000/svg" width="18" height="18"><rect width="5" height="5"></rect><rect x="7" height="5" width="12"></rect><rect y="7" width="5" height="5"></rect><rect x="7" y="7" height="5" width="12"></rect><rect y="14" width="5" height="5"></rect><rect x="7" y="14" height="5" width="12"></rect></svg>
</span>
</div>
<div class="d-flex">
{{-- <div>Showing 1-19 of 20 item(s)</div> --}}
<select class="form-select perpage-select">
@foreach ($per_pages as $val)
<option value="{{ $val }}" {{ request('per_page') == $val ? 'selected' : '' }}>{{ $val }}</option>
@endforeach
</select>
<select class="form-select order-select ms-2">
<option value="">{{ __('common.default') }}</option>
<option value="products.sales|asc" {{ request('sort') == 'products.sales' && request('order') == 'asc' ? 'selected' : '' }}>{{ __('common.sales') }} ({{ __('common.low') . '-' . __('common.high')}})</option>
<option value="products.sales|desc" {{ request('sort') == 'products.sales' && request('order') == 'desc' ? 'selected' : '' }}>{{ __('common.sales') }} ({{ __('common.high') . '-' . __('common.low')}})</option>
<option value="pd.name|asc" {{ request('sort') == 'pd.name' && request('order') == 'asc' ? 'selected' : '' }}>{{ __('common.name') }} (A - Z)</option>
<option value="pd.name|desc" {{ request('sort') == 'pd.name' && request('order') == 'desc' ? 'selected' : '' }}>{{ __('common.name') }} (Z - A)</option>
<option value="product_skus.price|asc" {{ request('sort') == 'product_skus.price' && request('order') == 'asc' ? 'selected' : '' }}>{{ __('product.price') }} ({{ __('common.low') . '-' . __('common.high')}})</option>
<option value="product_skus.price|desc" {{ request('sort') == 'product_skus.price' && request('order') == 'desc' ? 'selected' : '' }}>{{ __('product.price') }} ({{ __('common.high') . '-' . __('common.low')}})</option>
</select>
</div>
</div>
@if (count($products_format))
<div class="row {{ request('style_list') == 'list' ? 'product-list-wrap' : ''}}">
@foreach ($products_format as $product)
<div class="{{ !request('style_list') || request('style_list') == 'grid' ? 'col-6 col-md-4' : 'col-12'}}">
@include('shared.product')
</div>
@endforeach
</div>
@else
<x-shop-no-data />
@endif
@endif
</div>
</div>
{{ $products->links('shared/pagination/bootstrap-4') }}
@ -24,3 +119,104 @@
</div>
@endsection
@push('add-scripts')
<script>
let filterAttr = @json($filter_data['attr'] ?? []);
$('.filter-value-wrap li').click(function(event) {
let [attr, val] = [$(this).data('attr'),$(this).data('attrval')];
if ($(this).hasClass('delete-all')) {
deleteFilterAll();
return;
}
filterAttr[attr].values[val].selected = false;
filterProductData();
});
if ($('.filter-value-wrap li').length > 1) {
$('.filter-value-wrap').removeClass('d-none')
}
$(document).ready(function () {
$("#slider").slider({
range: true,
min: {{ $filter_data['price']['min'] }},
max: {{ $filter_data['price']['max'] }},
values: [{{ $filter_data['price']['select_min'] }}, {{ $filter_data['price']['select_max'] }}],
change: function(event, ui) {
$('input.price-min').val(ui.values[0])
$('input.price-max').val(ui.values[1])
filterProductData();
},
slide: function(event, ui) {
$('.price-range').html(`${ui.values[0]} - ${ui.values[1]}`)
}
});
});
$('.attr-value-check').change(function(event) {
let [attr, val] = [$(this).data('attr'),$(this).data('attrval')];
filterAttr[attr].values[val].selected = $(this).is(":checked");
filterProductData();
});
$('.form-select').change(function(event) {
filterProductData();
});
function filterAttrChecked(data) {
let filterAtKey = [];
data.forEach((item) => {
let checkedAtValues = [];
item.values.forEach((val) => {
if (val.selected) {
checkedAtValues.push(val.id)
}
})
if (checkedAtValues.length) {
filterAtKey.push(`${item.id}:${checkedAtValues.join('/')}`)
}
})
return filterAtKey.join('|')
}
function filterProductData() {
let url = bk.removeURLParameters(window.location.href, 'attr', 'price', 'sort', 'order');
let [priceMin, priceMax] = [$('.price-min').val(), $('.price-max').val()];
let order = $('.order-select').val();
let perpage = $('.perpage-select').val();
layer.load(2, {shade: [0.3,'#fff'] })
if (filterAttrChecked(filterAttr)) {
url = bk.updateQueryStringParameter(url, 'attr', filterAttrChecked(filterAttr));
}
if (priceMin || priceMax) {
url = bk.updateQueryStringParameter(url, 'price', `${priceMin}-${priceMax}`);
}
if (order) {
let orderKeys = order.split('|');
url = bk.updateQueryStringParameter(url, 'sort', orderKeys[0]);
url = bk.updateQueryStringParameter(url, 'order', orderKeys[1]);
}
if (perpage) {
url = bk.updateQueryStringParameter(url, 'per_page', perpage);
}
location = url;
}
function deleteFilterAll() {
let url = bk.removeURLParameters(window.location.href, 'attr', 'price');
location = url;
}
</script>
@endpush

View File

@ -1,33 +1,54 @@
<div class="product-wrap">
<div class="product-wrap {{ $style_list ?? '' }}">
<div class="image">
<a href="{{ $product['url'] }}">
<div class="image-old">
<img src="{{ $product['images'][0] ?? image_resize('', 400, 400) }}" class="img-fluid">
</div>
</a>
<div class="button-wrap">
<button
class="btn btn-dark text-light mx-1 rounded-3"
data-bs-toggle="tooltip"
data-bs-placement="top"
title="{{ __('shop/products.add_to_favorites') }}"
data-in-wishlist="{{ $product['in_wishlist'] }}"
onclick="bk.addWishlist('{{ $product['id'] }}', this)">
<i class="bi bi-heart{{ $product['in_wishlist'] ? '-fill' : '' }}"></i>
</button>
<button
class="btn btn-dark text-light mx-1 rounded-3"
data-bs-toggle="tooltip"
data-bs-placement="top"
title="{{ __('shop/products.add_to_cart') }}"
onclick="bk.addCart({sku_id: '{{ $product['sku_id'] }}'}, this)">
<i class="bi bi-cart"></i>
</button>
</div>
@if (!request('style_list') || request('style_list') == 'grid')
<div class="button-wrap">
<button
class="btn btn-dark text-light mx-1 rounded-3"
data-bs-toggle="tooltip"
data-bs-placement="top"
title="{{ __('shop/products.add_to_favorites') }}"
data-in-wishlist="{{ $product['in_wishlist'] }}"
onclick="bk.addWishlist('{{ $product['id'] }}', this)">
<i class="bi bi-heart{{ $product['in_wishlist'] ? '-fill' : '' }}"></i>
</button>
<button
class="btn btn-dark text-light mx-1 rounded-3"
data-bs-toggle="tooltip"
data-bs-placement="top"
title="{{ __('shop/products.add_to_cart') }}"
onclick="bk.addCart({sku_id: '{{ $product['sku_id'] }}'}, this)">
<i class="bi bi-cart"></i>
</button>
</div>
@endif
</div>
<div class="product-name">{{ $product['name_format'] }}</div>
<div class="product-price">
<span class="price-new">{{ $product['price_format'] }}</span>
<span class="price-lod">{{ $product['origin_price_format'] }}</span>
<div class="product-bottom-info">
<div class="product-name">{{ $product['name_format'] }}</div>
<div class="product-price">
<span class="price-new">{{ $product['price_format'] }}</span>
<span class="price-lod">{{ $product['origin_price_format'] }}</span>
</div>
@if (request('style_list') == 'list')
<div class="button-wrap mt-3">
<button
class="btn btn-dark text-light"
onclick="bk.addCart({sku_id: '{{ $product['sku_id'] }}'}, this)">
<i class="bi bi-cart"></i>
{{ __('shop/products.add_to_cart') }}
</button>
</div>
<div>
<button class="btn btn-link ps-0 text-secondary" data-in-wishlist="{{ $product['in_wishlist'] }}" onclick="bk.addWishlist('{{ $product['id'] }}', this)">
<i class="bi bi-heart{{ $product['in_wishlist'] ? '-fill' : '' }} me-1"></i> {{ __('shop/products.add_to_favorites') }}
</button>
</div>
@endif
</div>
</div>