【增加】物流管理

This commit is contained in:
liqianjin 2023-08-11 18:41:28 +08:00
parent fa79136adc
commit bfb1ea283c
16 changed files with 2387 additions and 3 deletions

View File

@ -0,0 +1,221 @@
<?php
namespace Beike\Admin\Http\Controllers;
use Beike\Admin\Http\Requests\LogisticsRequest;
use Beike\Admin\Http\Resources\LogisticsAttributeResource;
use Beike\Admin\Http\Resources\LogisticsResource;
use Beike\Admin\Repositories\TaxClassRepo;
use Beike\Admin\Services\LogisticsService;
use Beike\Libraries\Weight;
use Beike\Models\Logistics;
use Beike\Repositories\CategoryRepo;
use Beike\Repositories\LanguageRepo;
use Beike\Repositories\LogisticsRepo;
use Illuminate\Http\Request;
class LogisticsController extends Controller
{
protected string $defaultRoute = 'logistics.index';
public function index(Request $request)
{
$requestData = $request->all();
if (! isset($requestData['sort'])) {
$requestData['sort'] = 'logistics.updated_at';
}
$productList = LogisticsRepo::list($requestData);
$products = LogisticsResource::collection($productList);
$productsFormat = $products->jsonSerialize();
$data = [
'categories' => CategoryRepo::flatten(locale()),
'products_format' => $productsFormat,
'products' => $products,
'type' => 'products',
];
$data = hook_filter('admin.product.index.data', $data);
if ($request->expectsJson()) {
return $productsFormat;
}
return view('admin::pages.products.index', $data);
}
public function trashed(Request $request)
{
$requestData = $request->all();
$requestData['trashed'] = true;
$productList = LogisticsRepo::list($requestData);
$products = LogisticsResource::collection($productList);
$productsFormat = $products->jsonSerialize();
$data = [
'categories' => CategoryRepo::flatten(locale()),
'products_format' => $productsFormat,
'products' => $products,
'type' => 'trashed',
];
$data = hook_filter('admin.product.trashed.data', $data);
if ($request->expectsJson()) {
return $products;
}
return view('admin::pages.products.index', $data);
}
public function create(Request $request)
{
return $this->form($request, new Logistics());
}
public function store(LogisticsRequest $request)
{
try {
$requestData = $request->all();
$product = (new LogisticsService)->create($requestData);
$data = [
'request_data' => $requestData,
'product' => $product,
];
hook_action('admin.product.store.after', $data);
return redirect()->to(admin_route('products.index'))
->with('success', trans('common.created_success'));
} catch (\Exception $e) {
return redirect(admin_route('products.create'))
->withInput()
->withErrors(['error' => $e->getMessage()]);
}
}
public function edit(Request $request, Logistics $product)
{
return $this->form($request, $product);
}
public function update(LogisticsRequest $request, Logistics $product)
{
try {
$requestData = $request->all();
$product = (new LogisticsService)->update($product, $requestData);
$data = [
'request_data' => $requestData,
'product' => $product,
];
hook_action('admin.product.update.after', $data);
return redirect()->to($this->getRedirect())->with('success', trans('common.updated_success'));
} catch (\Exception $e) {
return redirect(admin_route('products.edit', $product))->withErrors(['error' => $e->getMessage()]);
}
}
public function copy(LogisticsRequest $request, Logistics $product)
{
try {
$product = (new LogisticsService)->copy($product);
$data = [
'product' => $product,
];
hook_action('admin.product.copy.after', $data);
return json_success(trans('common.copy_success'), []);
} catch (\Exception $e) {
return json_encode($e->getMessage());
}
}
public function destroy(Request $request, Logistics $product)
{
$product->delete();
hook_action('admin.product.destroy.after', $product);
return json_success(trans('common.deleted_success'));
}
public function restore(Request $request)
{
$productId = $request->id ?? 0;
Logistics::withTrashed()->find($productId)->restore();
hook_action('admin.product.restore.after', $productId);
return ['success' => true];
}
protected function form(Request $request, Logistics $logistics)
{
$logistics = hook_filter('admin.logistics.form.product', $logistics);
$data = [
'logistics' => $logistics,
'languages' => LanguageRepo::all(),
'weight_classes' => Weight::getWeightUnits(),
'_redirect' => $this->getRedirect(),
];
$data = hook_filter('admin.logistics.form.data', $data);
return view('admin::pages.logistics.form.form', $data);
}
public function name(int $id)
{
$name = LogisticsRepo::getName($id);
return json_success(trans('common.get_success'), $name);
}
/**
* 根据商品ID批量获取商品名称
*
* @param Request $request
* @return array
*/
public function getNames(Request $request): array
{
$productIds = explode(',', $request->get('product_ids'));
$name = LogisticsRepo::getNames($productIds);
return json_success(trans('common.get_success'), $name);
}
public function autocomplete(Request $request)
{
$products = LogisticsRepo::autocomplete($request->get('name') ?? '');
return json_success(trans('common.get_success'), $products);
}
public function updateStatus(Request $request)
{
LogisticsRepo::updateStatusByIds($request->get('ids'), $request->get('status'));
return json_success(trans('common.updated_success'), []);
}
public function destroyByIds(Request $request)
{
$productIds = $request->get('ids');
LogisticsRepo::DeleteByIds($productIds);
hook_action('admin.product.destroy_by_ids.after', $productIds);
return json_success(trans('common.deleted_success'), []);
}
public function trashedClear()
{
LogisticsRepo::forceDeleteTrashed();
}
}

View File

@ -219,6 +219,10 @@ Route::prefix($adminName)
Route::middleware('can:products_update')->put('products/{product}', [Controllers\ProductController::class, 'update'])->name('products.update');
Route::middleware('can:products_delete')->delete('products/{product}', [Controllers\ProductController::class, 'destroy'])->name('products.destroy');
// 物流
Route::middleware('can:logistics_index')->get('logistics', [Controllers\LogisticsController::class, 'index'])->name('logistics.index');
Route::middleware('can:logistics_create')->get('logistics/create', [Controllers\LogisticsController::class, 'create'])->name('logistics.create');
// 区域组
Route::middleware('can:regions_index')->get('regions', [Controllers\RegionController::class, 'index'])->name('regions.index');
Route::middleware('can:regions_create')->post('regions', [Controllers\RegionController::class, 'store'])->name('regions.store');

View File

@ -124,6 +124,13 @@ class Sidebar extends Component
'prefixes' => $this->getInquirySubPrefix(),
'children' => $this->getInquirySubRoutes(),
],
[
'route' => 'logistics.index',
'title' => trans('admin/common.logistics'),
'icon' => 'bi bi-gear',
'prefixes' => $this->getInquirySubPrefix(),
'children' => $this->getInquirySubRoutes(),
],
];
return hook_filter('admin.components.sidebar.menus', $menus);

View File

@ -0,0 +1,19 @@
<?php
namespace Beike\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
class Logistics extends Base
{
use HasFactory;
use SoftDeletes;
protected $fillable = ['name', 'warehouse_name', 'country_id', 'type', 'first_weight', 'first_weight_fee', 'continuation_weight_max', 'add_weight', 'continuation_weight_fee', 'throwing_ratio', 'num_fee', 'day_min', 'day_max'];
public function country()
{
return $this->belongsTo(country::class, 'country_id', 'id');
}
}

View File

@ -0,0 +1,420 @@
<?php
/**
* LogisticsRepo.php
*
* @copyright 2022 beikeshop.com - All Rights Reserved
* @link https://beikeshop.com
* @author Edward Yang <yangjin@guangda.work>
* @created 2022-06-23 11:19:23
* @modified 2022-06-23 11:19:23
*/
namespace Beike\Repositories;
use Beike\Models\Attribute;
use Beike\Models\AttributeValue;
use Beike\Models\Logistics;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\HigherOrderBuilderProxy;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Illuminate\Support\Facades\DB;
class LogisticsRepo
{
private static $allLogisticssWithName;
/**
* 获取商品详情
*/
public static function getLogisticsDetail($product)
{
if (is_int($product)) {
$product = Logistics::query()->findOrFail($product);
}
$product->load('description', 'skus', 'masterSku', 'brand', 'relations', 'numPrices');//, 'price_setting', 'numPrices');
hook_filter('repo.product.get_detail', $product);
return $product;
}
/**
* 通过单个或多个商品分类获取商品列表
*
* @param $categoryId
* @param $filterData
* @return LengthAwarePaginator
*/
public static function getLogisticssByCategory($categoryId, $filterData)
{
$builder = static::getBuilder(array_merge(['category_id' => $categoryId, 'active' => 1], $filterData));
return $builder->with('inCurrentWishlist')
->paginate($filterData['per_page'] ?? perPage())
->withQueryString();
}
/**
* 通过商品ID获取商品列表
* @param $productIds
* @return AnonymousResourceCollection
*/
public static function getLogisticssByIds($productIds): AnonymousResourceCollection
{
if (! $productIds) {
return LogisticsSimple::collection(new Collection());
}
$builder = static::getBuilder(['product_ids' => $productIds])->whereHas('masterSku');
$products = $builder->with('inCurrentWishlist')->get();
return LogisticsSimple::collection($products);
}
/**
* 获取商品筛选对象
*
* @param array $filters
* @return Builder
* @throws \Exception
*/
public static function getBuilder(array $filters = []): Builder
{
$builder = Logistics::query()->with('description', 'skus', 'masterSku', 'attributes');
$builder->leftJoin('product_descriptions as pd', function ($build) {
$build->whereColumn('pd.product_id', 'products.id')
->where('locale', locale());
});
$builder->leftJoin('product_skus', function ($build) {
$build->whereColumn('product_skus.product_id', 'products.id')
->where('is_default', 1)
->where('product_skus.deleted_at', null);
});
$builder->select(['products.*', 'pd.name', 'pd.content', 'pd.meta_title', 'pd.meta_description', 'pd.meta_keywords', 'pd.name', 'product_skus.price']);
if (isset($filters['category_id'])) {
$builder->whereHas('categories', function ($query) use ($filters) {
if (is_array($filters['category_id'])) {
$query->whereIn('category_id', $filters['category_id']);
} else {
$query->where('category_id', $filters['category_id']);
}
});
}
$productIds = $filters['product_ids'] ?? [];
if ($productIds) {
$builder->whereIn('products.id', $productIds);
$productIds = implode(',', $productIds);
$builder->orderByRaw("FIELD(products.id, {$productIds})");
}
// attr 格式:attr=10:10,13|11:34,23|3:4
if (isset($filters['attr']) && $filters['attr']) {
$attributes = self::parseFilterParamsAttr($filters['attr']);
foreach ($attributes as $attribute) {
$builder->whereHas('attributes', function ($query) use ($attribute) {
$query->where('attribute_id', $attribute['attr'])
->whereIn('attribute_value_id', $attribute['value']);
});
}
}
if (isset($filters['sku']) || isset($filters['model'])) {
$builder->whereHas('skus', function ($query) use ($filters) {
if (isset($filters['sku'])) {
$query->where('sku', 'like', "%{$filters['sku']}%");
}
if (isset($filters['model'])) {
$query->where('model', 'like', "%{$filters['model']}%");
}
});
}
if (isset($filters['price']) && $filters['price']) {
$builder->whereHas('skus', function ($query) use ($filters) {
// price 格式:price=30-100
$prices = explode('-', $filters['price']);
if (! $prices[1]) {
$query->where('price', '>', $prices[0] ?: 0)->where('is_default', 1);
} else {
$query->whereBetween('price', [$prices[0] ?? 0, $prices[1]])->where('is_default', 1);
}
});
}
if (isset($filters['name'])) {
$builder->where('pd.name', 'like', "%{$filters['name']}%");
}
$keyword = trim($filters['keyword'] ?? '');
if ($keyword) {
$keywords = explode(' ', $keyword);
$keywords = array_unique($keywords);
$keywords = array_diff($keywords, ['']);
$builder->where(function (Builder $query) use ($keywords) {
$query->whereHas('skus', function (Builder $query) use ($keywords) {
$keywordFirst = array_shift($keywords);
$query->where('sku', 'like', "%{$keywordFirst}%")
->orWhere('model', 'like', "%{$keywordFirst}%");
foreach ($keywords as $keyword) {
$query->orWhere('sku', 'like', "%{$keyword}%")
->orWhere('model', 'like', "%{$keyword}%");
}
});
foreach ($keywords as $keyword) {
$query->orWhere('pd.name', 'like', "%{$keyword}%");
}
});
}
if (isset($filters['created_start'])) {
$builder->where('products.created_at', '>', $filters['created_start']);
}
if (isset($filters['created_end'])) {
$builder->where('products.created_at', '>', $filters['created_end']);
}
if (isset($filters['active'])) {
$builder->where('active', (int) $filters['active']);
}
// 回收站
if (isset($filters['trashed']) && $filters['trashed']) {
$builder->onlyTrashed();
}
$sort = $filters['sort'] ?? 'products.position';
$order = $filters['order'] ?? 'desc';
$builder->orderBy($sort, $order);
return hook_filter('repo.product.builder', $builder);
}
public static function parseFilterParamsAttr($attr)
{
$attributes = explode('|', $attr);
$attributes = array_map(function ($item) {
$itemArr = explode(':', $item);
if (count($itemArr) != 2) {
throw new \Exception('Params attr has an error format!');
}
return [
'attr' => $itemArr[0],
'value' => explode(',', $itemArr[1]),
];
}, $attributes);
return $attributes;
}
public static function getFilterAttribute($data): array
{
$builder = static::getBuilder(array_diff_key($data, ['attr' => '', 'price' => '']))
->select(['pa.attribute_id', 'pa.attribute_value_id'])
->with(['attributes.attribute.description', 'attributes.attribute_value.description'])
->leftJoin('product_attributes as pa', 'pa.product_id', 'products.id')
->whereNotNull('pa.attribute_id')
->distinct()
->reorder('pa.attribute_id');
if ($attributesIds = system_setting('base.multi_filter', [])['attribute'] ?? []) {
$builder->whereIn('pa.attribute_id', $attributesIds);
}
$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');
$attributes = isset($data['attr']) ? self::parseFilterParamsAttr($data['attr']) : [];
$attributeMaps = array_column($attributes, 'value', 'attr');
$results = [];
foreach ($productAttributes as $item) {
if (! isset($attributeMap[$item['attribute_id']]) || ! isset($attributeValueMap[$item['attribute_value_id']])) {
continue;
}
$attribute = $attributeMap[$item['attribute_id']];
$attributeValue = $attributeValueMap[$item['attribute_value_id']];
if (! isset($results[$item['attribute_id']])) {
$results[$item['attribute_id']] = [
'id' => $attribute['id'],
'name' => $attribute['description']['name'],
];
}
if (! isset($results[$item['attribute_id']]['values'][$item['attribute_value_id']])) {
$results[$item['attribute_id']]['values'][$item['attribute_value_id']] = [
'id' => $attributeValue['id'],
'name' => $attributeValue['description']['name'],
'selected' => in_array($attributeValue['id'], $attributeMaps[$attribute['id']] ?? []),
];
}
}
$results = array_map(function ($item) {
$item['values'] = array_values($item['values']);
return $item;
}, $results);
return array_values($results);
}
public static function getFilterPrice($data)
{
$selectPrice = $data['price'] ?? '-';
// unset($data['price']);
$builder = static::getBuilder(['category_id' => $data['category_id']])->leftJoin('product_skus as ps', 'products.id', 'ps.product_id')
->where('ps.is_default', 1);
$min = $builder->min('ps.price');
$max = $builder->max('ps.price');
$priceArr = explode('-', $selectPrice);
$selectMin = $priceArr[0];
$selectMax = $priceArr[1];
return [
'min' => $min,
'max' => $max,
'select_min' => ($selectMin && $selectMin > $min) ? $selectMin : $min,
'select_max' => ($selectMax && $selectMax < $max) ? $selectMax : $max,
];
}
public static function list($data = [])
{
return static::getBuilder($data)->paginate($data['per_page'] ?? 20);
}
public static function autocomplete($name)
{
$products = Logistics::query()->with('description')
->whereHas('description', function ($query) use ($name) {
$query->where('name', 'like', "%{$name}%");
})->limit(10)->get();
$results = [];
foreach ($products as $product) {
$results[] = [
'id' => $product->id,
'name' => $product->description->name,
'status' => $product->active,
'image' => $product->image,
];
}
return $results;
}
/**
* 获取商品ID获取单个商品名称
*
* @param $id
* @return HigherOrderBuilderProxy|mixed|string
*/
public static function getNameById($id)
{
$product = Logistics::query()->find($id);
if ($product) {
return $product->description->name;
}
return '';
}
/**
* 通过商品ID获取商品名称
* @param $id
* @return mixed|string
*/
public static function getName($id)
{
return self::getNameById($id);
}
/**
* 获取所有商品ID和名称列表
*
* @return array|null
*/
public static function getAllLogisticssWithName(): ?array
{
if (self::$allLogisticssWithName !== null) {
return self::$allLogisticssWithName;
}
$items = [];
$products = static::getBuilder()->select('id')->get();
foreach ($products as $product) {
$items[$product->id] = [
'id' => $product->id,
'name' => $product->description->name ?? '',
];
}
return self::$allLogisticssWithName = $items;
}
/**
* @param $productIds
* @return array
*/
public static function getNames($productIds): array
{
$products = self::getListByLogisticsIds($productIds);
return $products->map(function ($product) {
return [
'id' => $product->id,
'name' => $product->description->name ?? '',
];
})->toArray();
}
/**
* 通过商品ID获取商品列表
* @return array|Builder[]|Collection
*/
public static function getListByLogisticsIds($productIds)
{
if (empty($productIds)) {
return [];
}
$products = Logistics::query()
->with(['description'])
->whereIn('id', $productIds)
->orderByRaw(DB::raw('FIELD(id, ' . implode(',', $productIds) . ')'))
->get();
return $products;
}
public static function DeleteByIds($ids)
{
Logistics::query()->whereIn('id', $ids)->delete();
}
public static function updateStatusByIds($ids, $status)
{
Logistics::query()->whereIn('id', $ids)->update(['active' => $status]);
}
public static function forceDeleteTrashed()
{
$products = Logistics::onlyTrashed();
$productsIds = $products->pluck('id')->toArray();
LogisticsRelation::query()->whereIn('product_id', $productsIds)->orWhere('relation_id', $productsIds)->delete();
LogisticsAttribute::query()->whereIn('product_id', $productsIds)->delete();
LogisticsCategory::query()->whereIn('product_id', $productsIds)->delete();
LogisticsSku::query()->whereIn('product_id', $productsIds)->delete();
LogisticsDescription::query()->whereIn('product_id', $productsIds)->delete();
$products->forceDelete();
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
class LogisticsFactory extends Factory{
/**
* Define the model's default state.
*
* @return array
*/
public function definition()
{
return [
'id' => 0,
'image' => 'path/to/image.jpg',
'video' => '',
'sort_order' => 0,
'status' => true,
'variable' => '',
];
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,311 @@
@extends('admin::layouts.master')
@section('title', __('admin/common.product'))
@section('content')
@if ($errors->has('error'))
<x-admin-alert type="danger" msg="{{ $errors->first('error') }}" class="mt-4" />
@endif
@if (session()->has('success'))
<x-admin-alert type="success" msg="{{ session('success') }}" class="mt-4" />
@endif
<div id="product-app">
<div class="card h-min-600">
<div class="card-body">
<div class="bg-light p-4">
<div class="row">
<div class="col-xxl-20 col-xl-3 col-lg-4 col-md-4 d-flex align-items-center mb-3">
<label class="filter-title">{{ __('product.name') }}</label>
<input @keyup.enter="search" type="text" v-model="filter.name" class="form-control" placeholder="{{ __('product.name') }}">
</div>
<div class="col-xxl-20 col-xl-3 col-lg-4 col-md-4 d-flex align-items-center mb-3">
<label class="filter-title">{{ __('product.sku') }}</label>
<input @keyup.enter="search" type="text" v-model="filter.sku" class="form-control" placeholder="{{ __('product.sku') }}">
</div>
<div class="col-xxl-20 col-xl-3 col-lg-4 col-md-4 d-flex align-items-center mb-3">
<label class="filter-title">{{ __('product.model') }}</label>
<input @keyup.enter="search" type="text" v-model="filter.model" class="form-control" placeholder="{{ __('product.model') }}">
</div>
<div class="col-xxl-20 col-xl-3 col-lg-4 col-md-4 d-flex align-items-center mb-3">
<label class="filter-title">{{ __('product.category') }}</label>
<select v-model="filter.category_id" class="form-select">
<option value="">{{ __('common.all') }}</option>
@foreach ($categories as $_category)
<option :value="{{ $_category->id }}">{{ $_category->name }}</option>
@endforeach
</select>
</div>
<div class="col-xxl-20 col-xl-3 col-lg-4 col-md-4 d-flex align-items-center mb-3">
<label class="filter-title">{{ __('common.status') }}</label>
<select v-model="filter.active" class="form-select">
<option value="">{{ __('common.all') }}</option>
variant-value-img <option value="1">{{ __('product.active') }}</option>
<option value="0">{{ __('product.inactive') }}</option>
</select>
</div>
@hook('admin.product.list.filter')
</div>
<div class="row">
<label class="filter-title"></label>
<div class="col-auto">
<button type="button" @click="search" class="btn btn-outline-primary btn-sm">{{ __('common.filter') }}</button>
<button type="button" @click="resetSearch" class="btn btn-outline-secondary btn-sm">{{ __('common.reset') }}</button>
</div>
</div>
</div>
<div class="d-flex justify-content-between my-4">
@if ($type != 'trashed')
<a href="{{ admin_route('products.create') }}" class="me-1 nowrap">
<button class="btn btn-primary">{{ __('admin/product.products_create') }}</button>
</a>
@else
@if ($products->total())
<button class="btn btn-primary" @click="clearRestore">{{ __('admin/product.clear_restore') }}</button>
@endif
@endif
@if ($type != 'trashed' && $products->total())
<div class="right nowrap">
<button class="btn btn-outline-secondary" :disabled="!selectedIds.length" @click="batchDelete">{{ __('admin/product.batch_delete') }}</button>
<button class="btn btn-outline-secondary" :disabled="!selectedIds.length"
@click="batchActive(true)">{{ __('admin/product.batch_active') }}</button>
<button class="btn btn-outline-secondary" :disabled="!selectedIds.length"
@click="batchActive(false)">{{ __('admin/product.batch_inactive') }}</button>
</div>
@endif
</div>
@if ($products->total())
<div class="table-push">
<table class="table table-hover">
<thead>
<tr>
<th><input type="checkbox" v-model="allSelected" /></th>
<th>{{ __('common.id') }}</th>
<th>{{ __('product.image') }}</th>
<th>{{ __('product.name') }}</th>
<th>{{ __('product.price') }}</th>
<th>
<div class="d-flex align-items-center">
{{ __('common.created_at') }}
<div class="d-flex flex-column ml-1 order-by-wrap">
<i class="el-icon-caret-top" @click="checkedOrderBy('created_at:asc')"></i>
<i class="el-icon-caret-bottom" @click="checkedOrderBy('created_at:desc')"></i>
</div>
</div>
</th>
<th class="d-flex align-items-center">
<div class="d-flex align-items-center">
{{ __('common.sort_order') }}
<div class="d-flex flex-column ml-1 order-by-wrap">
<i class="el-icon-caret-top" @click="checkedOrderBy('position:asc')"></i>
<i class="el-icon-caret-bottom" @click="checkedOrderBy('position:desc')"></i>
</div>
</div>
</th>
@if ($type != 'trashed')
<th>{{ __('common.status') }}</th>
@endif
@hook('admin.product.list.column')
<th class="text-end">{{ __('common.action') }}</th>
</tr>
</thead>
<tbody>
@foreach ($products_format as $product)
<tr>
<td><input type="checkbox" :value="{{ $product['id'] }}" v-model="selectedIds" /></td>
<td>{{ $product['id'] }}</td>
<td>
<div class="wh-60 border d-flex rounded-2 justify-content-center align-items-center"><img src="{{ $product['images'][0] ?? 'image/placeholder.png' }}" class="img-fluid max-h-100"></div>
</td>
<td>
<a href="{{ $product['url'] }}" target="_blank" title="{{ $product['name'] }}" class="text-dark">{{ $product['name'] }}</a>
</td>
<td>{{ $product['price_formatted'] }}</td>
<td>{{ $product['created_at'] }}</td>
<td>{{ $product['position'] }}</td>
@if ($type != 'trashed')
<td>
{{-- <div class="form-check form-switch">--}}
{{-- <input class="form-check-input cursor-pointer" type="checkbox" role="switch" data-active="{{ $product['active'] ? true : false }}" data-id="{{ $product['id'] }}" @change="turnOnOff($event)" {{ $product['active'] ? 'checked' : '' }}>--}}
{{-- </div>--}}
<span class="{{ $product['active'] ? 'text-success' : 'text-secondary' }}">
{{ $product['active'] ? __('common.enable_status') : __('common.disable_status') }}
</span>
</td>
@endif
@hook('admin.product.list.column_value')
<td class="text-end text-nowrap">
@if ($product['deleted_at'] == '')
<a href="{{ admin_route('products.edit', [$product['id']]) }}" class="btn btn-outline-secondary btn-sm">{{ __('common.edit') }}</a>
<a href="javascript:void(0)" class="btn btn-outline-secondary btn-sm" @click.prevent="copyProduct({{ $loop->index }})">{{ __('common.copy') }}</a>
<a href="javascript:void(0)" class="btn btn-outline-danger btn-sm" @click.prevent="deleteProduct({{ $loop->index }})">{{ __('common.delete') }}</a>
@hook('admin.product.list.action')
@else
<a href="javascript:void(0)" class="btn btn-outline-secondary btn-sm" @click.prevent="restoreProduct({{ $loop->index }})">{{ __('common.restore') }}</a>
@hook('admin.products.trashed.action')
@endif
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
{{ $products->withQueryString()->links('admin::vendor/pagination/bootstrap-4') }}
@else
<x-admin-no-data />
@endif
</div>
</div>
</div>
@hook('admin.product.list.content.footer')
@endsection
@push('footer')
<script>
let app = new Vue({
el: '#product-app',
data: {
url: '{{ $type == 'trashed' ? admin_route("products.trashed") : admin_route("products.index") }}',
filter: {
name: bk.getQueryString('name'),
page: bk.getQueryString('page'),
category_id: bk.getQueryString('category_id'),
sku: bk.getQueryString('sku'),
model: bk.getQueryString('model'),
active: bk.getQueryString('active'),
order_by: bk.getQueryString('order_by', ''),
},
selectedIds: [],
productIds: @json($products->pluck('id')),
},
computed: {
allSelected: {
get(e) {
return this.selectedIds.length == this.productIds.length;
},
set(val) {
return val ? this.selectedIds = this.productIds : this.selectedIds = [];
}
}
},
created() {
bk.addFilterCondition(this);
},
methods: {
turnOnOff() {
let id = event.currentTarget.getAttribute("data-id");
let checked = event.currentTarget.getAttribute("data-active");
let type = true;
if (checked) type = false;
$http.post('products/status', {ids: [id], status: type}).then((res) => {
layer.msg(res.message)
location.reload();
})
},
batchDelete() {
this.$confirm('{{ __('admin/product.confirm_batch_product') }}', '{{ __('common.text_hint') }}', {
confirmButtonText: '{{ __('common.confirm') }}',
cancelButtonText: '{{ __('common.cancel') }}',
type: 'warning'
}).then(() => {
$http.delete('products/delete', {ids: this.selectedIds}).then((res) => {
layer.msg(res.message)
location.reload();
})
}).catch(()=>{});
},
batchActive(type) {
this.$confirm('{{ __('admin/product.confirm_batch_status') }}', '{{ __('common.text_hint') }}', {
confirmButtonText: '{{ __('common.confirm') }}',
cancelButtonText: '{{ __('common.cancel') }}',
type: 'warning'
}).then(() => {
$http.post('products/status', {ids: this.selectedIds, status: type}).then((res) => {
layer.msg(res.message)
location.reload();
})
}).catch(()=>{});
},
search() {
this.filter.page = '';
location = bk.objectToUrlParams(this.filter, this.url)
},
checkedOrderBy(orderBy) {
this.filter.order_by = orderBy;
location = bk.objectToUrlParams(this.filter, this.url)
},
resetSearch() {
this.filter = bk.clearObjectValue(this.filter)
location = bk.objectToUrlParams(this.filter, this.url)
},
copyProduct(index) {
const id = this.productIds[index];
this.$confirm('{{ __('common.confirm_copy') }}', '{{ __('common.text_hint') }}', {
type: 'warning'
}).then(() => {
$http.post('products/' + id + '/copy').then((res) => {
this.$message.success(res.message);
location.reload();
})
}).catch(()=>{});
},
deleteProduct(index) {
const id = this.productIds[index];
this.$confirm('{{ __('common.confirm_delete') }}', '{{ __('common.text_hint') }}', {
type: 'warning'
}).then(() => {
$http.delete('products/' + id).then((res) => {
this.$message.success(res.message);
location.reload();
})
}).catch(()=>{});
},
restoreProduct(index) {
const id = this.productIds[index];
this.$confirm('{{ __('admin/product.confirm_batch_restore') }}', '{{ __('common.text_hint') }}', {
type: 'warning'
}).then(() => {
$http.put('products/restore', {id: id}).then((res) => {
location.reload();
})
}).catch(()=>{});;
},
clearRestore() {
this.$confirm('{{ __('admin/product.confirm_delete_restore') }}', '{{ __('common.text_hint') }}', {
type: 'warning'
}).then(() => {
$http.post('products/trashed/clear').then((res) => {
location.reload();
})
}).catch(()=>{});;
}
}
});
</script>
@endpush

View File

@ -96,9 +96,9 @@
<x-admin::form.row :title="__('admin/product.length_width_height')">
<div class="d-flex wp-400">
<input type="text" name="length" placeholder="" value="{{ old('weight', $product->length ?? '') }}" class="form-control" style="flex: 0 0 95px" /><div style="width: 40px;line-height: 34px;text-align:center">CM X</div>
<input type="text" name="width" placeholder="" value="{{ old('weight', $product->width ?? '') }}" class="form-control" style="flex: 0 0 95px" /><div style="width: 40px;line-height: 34px;text-align:center">CM X</div>
<input type="text" name="height" placeholder="" value="{{ old('weight', $product->height ?? '') }}" class="form-control" style="flex: 0 0 95px" /><div style="width: 30px;line-height: 34px;text-align:center">CM</div>
<input type="text" name="length" placeholder="{{ __('admin/product.length') }}" value="{{ old('weight', $product->length ?? '') }}" class="form-control" style="flex: 0 0 95px" /><div style="width: 40px;line-height: 34px;text-align:center">CM X</div>
<input type="text" name="width" placeholder="{{ __('admin/product.width') }}" value="{{ old('weight', $product->width ?? '') }}" class="form-control" style="flex: 0 0 95px" /><div style="width: 40px;line-height: 34px;text-align:center">CM X</div>
<input type="text" name="height" placeholder="{{ __('admin/product.height') }}" value="{{ old('weight', $product->height ?? '') }}" class="form-control" style="flex: 0 0 95px" /><div style="width: 30px;line-height: 34px;text-align:center">CM</div>
</div>
</x-admin::form.row>

View File

@ -101,4 +101,5 @@ return [
'update_date' => 'Update date',
'update_btn' => 'Download',
'expired_at' => 'Expired',
'logistics' => 'Logistics',
];

View File

@ -0,0 +1,58 @@
<?php
/**
* order.php
*
* @copyright 2022 beikeshop.com - All Rights Reserved
* @link https://beikeshop.com
* @author Edward Yang <yangjin@guangda.work>
* @created 2022-08-02 14:22:41
* @modified 2022-08-02 14:22:41
*/
return [
'logistics_index' => 'Index',
'logistics_name' => 'Logistics Name',
'logistics_img' => 'Logistics Image',
'logistics_create' => 'Create',
'logistics_show' => 'Detail',
'logistics_update' => 'Edit',
'logistics_delete' => 'Delete',
'logistics_trashed' => 'Trashed',
'logistics_restore' => 'Restore',
'clear_restore' => 'Empty Recycle Bin',
'logistics_filter_index' => 'View Filters',
'logistics_filter_update' => 'Modify Filters',
'batch_delete' => 'Batch Delete',
'batch_active' => 'Batch Active',
'batch_inactive' => 'Batch Inactive',
'basic_information' => 'Basic Information',
'product_details' => 'Product details',
'product_relations' => 'Related Logistics',
'stocks' => 'Product inventory',
'model' => 'Model',
'price' => 'Price',
'origin_price' => 'Original Price',
'cost_price' => 'Cost Price',
'quantity' => 'Quantity',
'enable_multi_spec' => 'Enable multi-spec',
'image_help' => 'The first picture will be used as the main picture of the product, and multiple pictures can be uploaded at the same time, and the position of multiple pictures can be adjusted at will',
'video_help' => 'If the prompt exceeds the system size limit, please modify the php.ini parameter post_max_size',
'add_variable' => 'Add Specs',
'add_variable_value' => 'Add Specification Value',
'add_variable_image' => 'Add Spec Image',
'default_main_product' => 'Default main product',
'modify_order' => 'Double-click to modify, drag to adjust the order',
'weight_text' => 'weight',
'length_width_height' => 'Length, width and height',
'length' => 'Length',
'width' => 'Width',
'height' => 'Height',
'weight_class' => 'weight unit',
'confirm_batch_product' => 'Are you sure you want to delete the selected logistics in batches? ',
'confirm_batch_status' => 'Confirm to modify the status of the selected logistics in batches? ',
'confirm_batch_restore' => 'Confirm to restore the selected product? ',
'confirm_delete_restore' => 'Are you sure you want to empty the recycle bin? ',
];

View File

@ -46,6 +46,9 @@ return [
'modify_order' => 'Double-click to modify, drag to adjust the order',
'weight_text' => 'weight',
'length_width_height' => 'Length, width and height',
'length' => 'Length',
'width' => 'Width',
'height' => 'Height',
'weight_class' => 'weight unit',
'confirm_batch_product' => 'Are you sure you want to delete the selected products in batches? ',

View File

@ -99,4 +99,5 @@ return [
'update_date' => '更新日期',
'update_btn' => '前往下载',
'expired_at' => '到期时间',
'logistics' => '物流管理',
];

View File

@ -0,0 +1,63 @@
<?php
/**
* order.php
*
* @copyright 2022 beikeshop.com - All Rights Reserved
* @link https://beikeshop.com
* @author Edward Yang <yangjin@guangda.work>
* @created 2022-08-02 14:22:41
* @modified 2022-08-02 14:22:41
*/
return [
'logistics_index' => '物流列表',
'logistics_name' => '物流名称',
'logistics_img' => '物流图片',
'logistics_create' => '创建物流',
'logistics_show' => '物流详情',
'logistics_update' => '更新物流',
'logistics_delete' => '删除物流',
'logistics_trashed' => '回收站',
'logistics_restore' => '恢复回收站',
'clear_restore' => '清空回收站',
'logistics_filter_index' => '查看高级筛选',
'logistics_filter_update' => '修改高级筛选',
'batch_delete' => '批量删除',
'batch_active' => '批量上架',
'batch_inactive' => '批量下架',
'basic_information' => '基础信息',
'product_details' => '物流详情',
'product_relations' => '相关物流',
'stocks' => '物流库存',
'model' => '型号',
'price' => '价格',
'origin_price' => '原价',
'cost_price' => '成本价',
'quantity' => '数量',
'enable_multi_spec' => '启用多规格',
'image_help' => '第一张图片将作为物流主图,支持同时上传多张图片,多张图片之间可随意调整位置',
'video_help' => '如果提示超出系统大小限制,请修改 php.ini 参数 post_max_size',
'add_variable' => '添加规格',
'add_variable_value' => '添加规格值',
'add_variable_image' => '添加规格图片',
'default_main_product' => '默认主物流',
'modify_order' => '双击修改、拖动调整顺序',
'weight_text' => '重量',
'length_width_height' => '长宽高',
'length' => '长',
'width' => '宽',
'height' => '高',
'weight_class' => '重量单位',
'confirm_batch_product' => '确认要批量删除选中的物流吗?',
'confirm_batch_status' => '确认要批量修改选中的物流的状态吗?',
'confirm_batch_restore' => '确认要恢复选中的物流吗?',
'confirm_delete_restore' => '确认要清空回收站吗?',
'price_setting' => '价格设置',
'price_setting_by' =>[
'sku' => '根据规格设置价格',
'num' => '根据数量设置价格',
]
];

View File

@ -46,6 +46,9 @@ return [
'modify_order' => '双击修改、拖动调整顺序',
'weight_text' => '重量',
'length_width_height' => '长宽高',
'length' => '长',
'width' => '宽',
'height' => '高',
'weight_class' => '重量单位',
'confirm_batch_product' => '确认要批量删除选中的商品吗?',

View File

@ -99,6 +99,7 @@ return [
'contacts' => '联系人',
'content' => '内容',
'sku' => 'Sku',
'logistics' => '物流',
'order' => [