【增加】物流及可视化

This commit is contained in:
liqianjin 2023-08-12 18:46:10 +08:00
parent bfb1ea283c
commit 02516b7825
23 changed files with 640 additions and 799 deletions

View File

@ -57,4 +57,16 @@ class CountryController extends Controller
return json_success(trans('common.deleted_success'));
}
/**
* @param Request $request
* @return array
*/
public function autocomplete(Request $request): array
{
$brands = CountryRepo::autocomplete($request->get('name') ?? '', 0);
return json_success(trans('common.get_success'), $brands);
}
}

View File

@ -7,6 +7,7 @@ use Beike\Admin\Http\Resources\LogisticsAttributeResource;
use Beike\Admin\Http\Resources\LogisticsResource;
use Beike\Admin\Repositories\TaxClassRepo;
use Beike\Admin\Services\LogisticsService;
use Beike\Admin\View\DesignBuilders\Product;
use Beike\Libraries\Weight;
use Beike\Models\Logistics;
use Beike\Repositories\CategoryRepo;
@ -24,24 +25,23 @@ class LogisticsController extends Controller
if (! isset($requestData['sort'])) {
$requestData['sort'] = 'logistics.updated_at';
}
$productList = LogisticsRepo::list($requestData);
$products = LogisticsResource::collection($productList);
$productsFormat = $products->jsonSerialize();
$logisticsList = LogisticsRepo::list($requestData);
$logistics = LogisticsResource::collection($logisticsList);
$logisticsFormat = $logistics->jsonSerialize();
$data = [
'categories' => CategoryRepo::flatten(locale()),
'products_format' => $productsFormat,
'products' => $products,
'type' => 'products',
'logistics_format' => $logisticsFormat,
'logistics' => $logistics,
'type' => 'logistics',
];
$data = hook_filter('admin.product.index.data', $data);
$data = hook_filter('admin.logistics.index.data', $data);
if ($request->expectsJson()) {
return $productsFormat;
return $logisticsFormat;
}
return view('admin::pages.products.index', $data);
return view('admin::pages.logistics.index', $data);
}
public function trashed(Request $request)
@ -77,40 +77,40 @@ class LogisticsController extends Controller
{
try {
$requestData = $request->all();
$product = (new LogisticsService)->create($requestData);
$logistics = (new LogisticsService)->create($requestData);
$data = [
'request_data' => $requestData,
'product' => $product,
'logistics' => $logistics,
];
hook_action('admin.product.store.after', $data);
hook_action('admin.logistics.store.after', $data);
return redirect()->to(admin_route('products.index'))
return redirect()->to(admin_route('logistics.index'))
->with('success', trans('common.created_success'));
} catch (\Exception $e) {
return redirect(admin_route('products.create'))
return redirect(admin_route('logistics.create'))
->withInput()
->withErrors(['error' => $e->getMessage()]);
}
}
public function edit(Request $request, Logistics $product)
public function edit(Request $request, Logistics $logistics)
{
return $this->form($request, $product);
return $this->form($request, $logistics);
}
public function update(LogisticsRequest $request, Logistics $product)
public function update(LogisticsRequest $request, Logistics $logistics)
{
try {
$requestData = $request->all();
$product = (new LogisticsService)->update($product, $requestData);
$logistics = (new LogisticsService)->update($logistics, $requestData);
$data = [
'request_data' => $requestData,
'product' => $product,
'logistics' => $logistics,
];
hook_action('admin.product.update.after', $data);
hook_action('admin.logistics.update.after', $data);
return redirect()->to($this->getRedirect())->with('success', trans('common.updated_success'));
} catch (\Exception $e) {
@ -134,10 +134,10 @@ class LogisticsController extends Controller
}
}
public function destroy(Request $request, Logistics $product)
public function destroy(Request $request, Logistics $logistics)
{
$product->delete();
hook_action('admin.product.destroy.after', $product);
$logistics->delete();
hook_action('admin.logistics.destroy.after', $logistics);
return json_success(trans('common.deleted_success'));
}
@ -155,12 +155,15 @@ class LogisticsController extends Controller
protected function form(Request $request, Logistics $logistics)
{
$logistics = hook_filter('admin.logistics.form.product', $logistics);
$logistics = hook_filter('admin.logistics.form.logistics', $logistics);
$types = [
['title' => 'weight','name'=>trans('admin/logistics.type_weight')],
['title' => 'num','name'=>trans('admin/logistics.type_num')],
['title' => 'free','name'=>trans('admin/logistics.type_free')],
];
$data = [
'logistics' => $logistics,
'languages' => LanguageRepo::all(),
'weight_classes' => Weight::getWeightUnits(),
'types' => $types,
'_redirect' => $this->getRedirect(),
];

View File

@ -0,0 +1,70 @@
<?php
/**
* PageRequest.php
*
* @copyright 2022 beikeshop.com - All Rights Reserved
* @link https://beikeshop.com
* @author Edward Yang <yangjin@guangda.work>
* @created 2022-08-19 21:58:20
* @modified 2022-08-19 21:58:20
*/
namespace Beike\Admin\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class LogisticsRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules(): array
{
return [
'name' => 'string',
'warehouse_name' => 'string',
'country_id' => 'int',
'type' => 'string',
'first_weight' => 'numeric',
'first_weight_fee' => 'numeric',
'continuation_weight_max' => 'numeric',
'add_weight' => 'numeric',
'continuation_weight_fee' => 'numeric',
'num_fee' => 'numeric',
'throwing_ratio' => 'int',
'day_min' => 'int',
'day_max' => 'int',
];
}
public function attributes()
{
return [
'name' => trans('logistics.name'),
'warehouse_name' => trans('logistics.warehouse_name'),
'country_id' => trans('logistics.country_id'),
'type' => trans('logistics.type'),
'first_weight' => trans('logistics.first_weight'),
'first_weight_fee' => trans('logistics.first_weight_fee'),
'continuation_weight_max' => trans('logistics.continuation_weight_max'),
'add_weight' => trans('logistics.add_weight'),
'continuation_weight_fee' => trans('logistics.continuation_weight_fee'),
'num_fee' => trans('logistics.num_fee'),
'throwing_ratio' => trans('logistics.throwing_ratio'),
'day_min' => trans('logistics.day_min'),
'day_max' => trans('logistics.day_max'),
];
}
}

View File

@ -0,0 +1,45 @@
<?php
namespace Beike\Admin\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class LogisticsResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param Request $request
* @return array
* @throws \Exception
*/
public function toArray($request): array
{
$data = [
'id' => $this->id,
'name' => $this->name ?? '',
'warehouse_name' => $this->warehouse_name ?? '',
'country_id' => $this->country_id,
'country' => $this->country ? $this->country->name : '',
'type' => $this->type,
'first_weight' => $this->first_weight,
'first_weight_fee' => $this->first_weight_fee,
'continuation_weight_max' => $this->continuation_weight_max,
'add_weight' => $this->add_weight,
'continuation_weight_fee' => $this->continuation_weight_fee,
'num_fee' => $this->num_fee,
'throwing_ratio' => $this->throwing_ratio,
'day_min' => $this->day_min,
'day_max' => $this->day_max,
'position' => $this->position,
'created_at' => time_format($this->created_at),
'deleted_at' => $this->deleted_at ? time_format($this->deleted_at) : '',
'url_edit' => admin_route('logistics.edit', $this->id),
];
return hook_filter('resource.product', $data);
}
}

View File

@ -71,6 +71,7 @@ Route::prefix($adminName)
Route::middleware('can:countries_create')->post('countries', [Controllers\CountryController::class, 'store'])->name('countries.store');
Route::middleware('can:countries_update')->put('countries/{id}', [Controllers\CountryController::class, 'update'])->name('countries.update');
Route::middleware('can:countries_delete')->delete('countries/{id}', [Controllers\CountryController::class, 'destroy'])->name('countries.destroy');
Route::middleware('can:countries_index')->get('countries/autocomplete', [Controllers\CountryController::class, 'autocomplete'])->name('countries.autocomplete');
// 省份
Route::middleware('can:zones_index')->get('zones', [Controllers\ZoneController::class, 'index'])->name('zones.index');
@ -222,6 +223,10 @@ Route::prefix($adminName)
// 物流
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:logistics_create')->post('logistics', [Controllers\LogisticsController::class, 'store'])->name('logistics.store');
Route::middleware('can:logistics_show')->get('logistics/{logistics}/edit', [Controllers\LogisticsController::class, 'edit'])->name('logistics.edit');
Route::middleware('can:logistics_update')->put('logistics/{logistics}', [Controllers\LogisticsController::class, 'update'])->name('logistics.update');
Route::middleware('can:logistics_delete')->delete('logistics/{logistics}', [Controllers\LogisticsController::class, 'destroy'])->name('logistics.destroy');
// 区域组
Route::middleware('can:regions_index')->get('regions', [Controllers\RegionController::class, 'index'])->name('regions.index');

View File

@ -0,0 +1,80 @@
<?php
namespace Beike\Admin\Services;
use Beike\Models\Logistics;
use Beike\Models\Product;
use Illuminate\Support\Facades\DB;
class LogisticsService
{
public function create(array $data): Logistics
{
$logistics = new Logistics;
return $this->createOrUpdate($logistics, $data);
}
public function update(Logistics $logistics, array $data): Logistics
{
return $this->createOrUpdate($logistics, $data);
}
public function copy(Product $oldProduct): Product
{
$product = new Product;
$data = $oldProduct->toArray();
$data['id'] = 0;
$data['created_at'] = now();
$data['variables'] = json_encode($data['variables'],JSON_UNESCAPED_UNICODE);
$data['descriptions'] = $oldProduct->descriptions()->get()->toArray();
foreach ($data['descriptions'] as $locale => $description) {
$data['descriptions'][$description['locale']] = $description;
unset($data['descriptions'][$locale]);
}
$data['attributes'] = $oldProduct->attributes()->get()->toArray();
$data['skus'] = $oldProduct->skus()->get()->toArray();
$data['numPrices'] = $oldProduct->numPrices()->get()->toArray();
$data['categories'] = $oldProduct->categories()->get()->toArray();
$data['relations'] = $oldProduct->relations()->get()->toArray();
$data['categories'] = array_column($data['categories'],'id');
$data['relations'] = array_column($data['relations'],'id');
return $this->createOrUpdate($product, $data);
}
protected function createOrUpdate(Logistics $logistics, array $data): Logistics
{
$isUpdating = $logistics->id > 0;
try {
DB::beginTransaction();
$data['country_id'] = (int) ($data['country_id'] ?? 0);
$data['throwing_ratio'] = (int) ($data['throwing_ratio'] ?? 0);
$data['day_min'] = (int) ($data['day_min'] ?? 0);
$data['day_max'] = (int) ($data['day_max'] ?? 0);
$data['first_weight'] = (float) ($data['first_weight'] ?? 0);
$data['first_weight_fee'] = (float) ($data['first_weight_fee'] ?? 0);
$data['continuation_weight_max'] = (float) ($data['continuation_weight_max'] ?? 0);
$data['add_weight'] = (float) ($data['add_weight'] ?? 0);
$data['continuation_weight_fee'] = (float) ($data['continuation_weight_fee'] ?? 0);
$data['num_fee'] = (float) ($data['num_fee'] ?? 0);
$data['variables'] = json_decode($data['variables'] ?? '[]');
$logistics->fill($data);
$logistics->updated_at = now();
$logistics->save();
if ($isUpdating) {
}
DB::commit();
return $logistics;
} catch (\Exception $e) {
DB::rollBack();
throw $e;
}
}
}

View File

@ -10,7 +10,7 @@ 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'];
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','position'];
public function country()
{

View File

@ -121,4 +121,16 @@ class CountryRepo
{
return Country::query()->select('id', 'name')->get();
}
public static function autocomplete($name, $onlyActive = 1)
{
$builder = Country::query()
->where('name', 'like', "$name%")
->select('id', 'name', 'status');
// if ($onlyActive) {
// $builder->where('status', 1);
// }
return $builder->limit(10)->get();
}
}

View File

@ -81,18 +81,12 @@ class LogisticsRepo
*/
public static function getBuilder(array $filters = []): Builder
{
$builder = Logistics::query()->with('description', 'skus', 'masterSku', 'attributes');
$builder = Logistics::query()->with('country');
$builder->leftJoin('product_descriptions as pd', function ($build) {
$build->whereColumn('pd.product_id', 'products.id')
->where('locale', locale());
$builder->leftJoin('countries as c', function ($build) {
$build->whereColumn('c.id', 'logistics.id');
});
$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']);
$builder->select(['logistics.*', 'c.name as country_name']);
if (isset($filters['category_id'])) {
$builder->whereHas('categories', function ($query) use ($filters) {
@ -104,12 +98,6 @@ class LogisticsRepo
});
}
$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']) {
@ -145,54 +133,22 @@ class LogisticsRepo
});
}
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']);
$builder->where('logistics.created_at', '>', $filters['created_start']);
}
if (isset($filters['created_end'])) {
$builder->where('products.created_at', '>', $filters['created_end']);
$builder->where('logistics.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';
$sort = $filters['sort'] ?? 'logistics.position';
$order = $filters['order'] ?? 'desc';
$builder->orderBy($sort, $order);
return hook_filter('repo.product.builder', $builder);
return hook_filter('repo.logistics.builder', $builder);
}
public static function parseFilterParamsAttr($attr)

View File

@ -75,6 +75,7 @@ class ProductDetail extends JsonResource
return array_map(function ($item) use ($lang) {
return [
'name' => $item['name'][$lang] ?? '',
'isNumSelect' => $item['isNumSelect'] ?? FALSE,
'values' => array_map(function ($item) use ($lang) {
return [
'name' => $item['name'][$lang] ?? '',

View File

@ -1,8 +1,8 @@
@extends('admin::layouts.master')
@section('title', __('admin/product.logistics_show'))
@section('title', __('admin/logistics.logistics_show'))
@section('body-class', 'page-product-form')
@section('body-class', 'page-logistics-form')
@push('header')
<script src="{{ asset('vendor/vue/Sortable.min.js') }}"></script>
@ -28,591 +28,96 @@
<ul class="nav nav-tabs nav-bordered mb-3" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#tab-basic" type="button" >{{ __('admin/product.basic_information') }}</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#tab-descriptions" type="button">{{ __('admin/product.product_details') }}</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#tab-attribute" type="button">{{ __('admin/attribute.index') }}</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#tab-seo" type="button" >SEO</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#tab-relations" type="button">{{ __('admin/product.product_relations') }}</button>
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#tab-basic" type="button" >{{ __('admin/logistics.basic_information') }}</button>
</li>
</ul>
<div class="card">
<div class="card-body h-min-600">
<form novalidate class="needs-validation" action="{{ $product->id ? admin_route('logistics.update', $product) : admin_route('logistics.store') }}"
<form novalidate class="needs-validation" action="{{ $logistics->id ? admin_route('logistics.update', $logistics) : admin_route('logistics.store') }}"
method="POST" id="app">
@csrf
@method($product->id ? 'PUT' : 'POST')
@method($logistics->id ? 'PUT' : 'POST')
<input type="hidden" name="_redirect" value="{{ $_redirect }}" />
<div class="tab-content">
<div class="tab-pane fade show active" id="tab-basic">
<h6 class="border-bottom pb-3 mb-4">{{ __('common.data') }}</h6>
<x-admin-form-input-locale
:width="600" name="descriptions.*.name" title="{{ __('common.name') }}"
:value="$descriptions" :required="true" />
@hook('admin.product.name.after')
<x-admin::form.row title="{{ __('common.image') }}">
<draggable
element="div"
ghost-class="dragabble-ghost"
class="product-images d-flex flex-wrap"
:list="form.images"
:options="{animation: 200, handle: '.product-item'}"
>
<div v-for="image, index in form.images" :key="index" class="wh-80 rounded-2 product-item position-relative me-2 mb-2 border d-flex justify-content-center align-items-center max-h-100 overflow-hidden">
<div class="position-absolute top-0 end-0">
<button class="btn btn-danger btn-sm wh-20 p-0" @click="removeImages(index)" type="button"><i class="bi bi-trash"></i></button>
</div>
<img :src="thumbnail(image)" class="img-fluid rounded-2">
<input type="hidden" name="images[]" :value="image">
</div>
<div v-if="!form.images.length" class="d-none"><input type="hidden" name="images[]" value=""></div>
<div class="set-product-img wh-80 rounded-2" @click="addProductImages"><i class="bi bi-plus fs-1 text-muted"></i></div>
</draggable>
<div class="help-text mb-1 mt-1">{{ __('admin/product.image_help') }}</div>
</x-admin::form.row>
<h5 class="border-bottom pb-3 mb-4">{{ __('admin/logistics.basic_information') }}</h5>
<x-admin::form.row title="{{ __('product.video') }}">
<div class="d-flex align-items-end">
<div class="set-product-img wh-80 rounded-2 me-2" @click="addProductVideo">
<i v-if="form.video.path" class="bi bi-play-circle fs-1"></i>
<i v-else class="bi bi-plus fs-1 text-muted"></i>
</div>
<input type="hidden" name="video" :value="form.video.path">
<a v-if="form.video.path" target="_blank" :href="form.video.url">{{ __('common.view') }}</a>
</div>
<div class="help-text mb-1 mt-1">{{ __('admin/product.video_help') }}</div>
</x-admin::form.row>
<x-admin-form-input name="name" :title="__('admin/logistics.logistics_name')" :value="old('name', $logistics->name ?? '')" />
<x-admin-form-input name="warehouse_name" :title="__('admin/logistics.warehouse_name')" :value="old('name', $logistics->warehouse_name ?? '仓到门')" />
<x-admin-form-input name="position" :title="__('common.sort_order')" :value="old('position', $product->position ?? '0')" />
<x-admin::form.row :title="__('admin/product.length_width_height')">
<div class="d-flex wp-400">
<input type="text" name="length" placeholder="{{ __('admin/product.length') }}" value="{{ old('length', $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('width', $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('height', $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>
<x-admin::form.row :title="__('admin/product.weight_text')">
<div class="d-flex wp-400">
<input type="text" name="weight" placeholder="{{ __('admin/product.weight_text') }}" value="{{ old('weight', $product->weight ?? '') }}" class="form-control" style="flex: 0 0 260px" />
<select class="form-select ms-4 bg-white" name="weight_class">
@foreach ($weight_classes as $item)
<option value="{{ $item }}" {{ $product->weight_class == $item ? 'selected' : '' }}>{{ __('product.' . $item) }}</option>
@endforeach
</select>
</div>
</x-admin::form.row>
@hookwrapper('admin.product.edit.brand')
<x-admin::form.row :title="__('admin/brand.index')">
<input type="text" value="{{ $product->brand->name ?? '' }}" id="brand-autocomplete" class="form-control wp-400 " />
<input type="hidden" name="brand_id" value="{{ old('brand_id', $product->brand_id ?? '') }}" />
@hookwrapper('admin.logistics.edit.country')
<x-admin::form.row :title="__('admin/country.index')">
<input type="text" value="{{ $logistics->country->name ?? '' }}" id="country-autocomplete" class="form-control wp-400 " />
<input type="hidden" name="country_id" value="{{ old('country_id', $logistics->country_id ?? '') }}" />
</x-admin::form.row>
@endhookwrapper
<x-admin-form-select :title="__('admin/tax_class.index')" name="tax_class_id" :value="old('tax_class_id', $product->tax_class_id ?? '')" :options="$tax_classes" key="id" label="title" />
{{-- <x-admin-form-select :title="__('admin/logistics.type')" name="type" :value="old('type', $logistics->type ?? '')" :options="$types" key="title" label="name" @change="isVariableChange"/>--}}
<x-admin::form.row :title="__('admin/category.index')">
<div class="wp-400 form-control" style="max-height: 240px;overflow-y: auto">
@foreach ($source['categories'] as $_category)
<div class="form-check">
<input class="form-check-input" type="checkbox" name="categories[]" value="{{ $_category->id }}"
id="category-{{ $_category->id }}" {{ in_array($_category->id, old('categories', $category_ids)) ? 'checked' : '' }}>
<label class="form-check-label" for="category-{{ $_category->id }}">
{{ $_category->name }}
</label>
</div>
<x-admin::form.row :title="__('admin/logistics.type')">
<select class="form-select me-3 wp-400" v-model="form.type" name="type">
@foreach ($types as $option)
<option value="{{ $option['title'] }}" {{ $option['title'] == old('type', $logistics->type ?? '') ? 'selected': '' }}>{{ $option['name'] }}</option>
@endforeach
</div>
</select>
</x-admin::form.row>
<x-admin-form-switch-status name="active" :title="__('common.status')" :value="old('active', $product->active ?? 1)" />
@hook('admin.product.edit.extra')
<div>
<h5 class="border-bottom pb-3 mb-4">{{ __('admin/product.stocks') }}</h5>
<x-admin::form.row title="{{ __('admin/product.price_setting_by.num') }}">
<div class="mb-1 mt-2">
<div class="form-check form-check-inline">
<input v-model="form.price_setting" class="form-check-input" id="price_setting-sku" type="radio" name="price_setting" id="price_setting-sku" value="sku" {{ $product->price_setting == 'sku' ? 'checked' : '' }}>
<label class="form-check-label" for="price_setting-sku">{{ __('common.disable') }}</label>
</div>
<div class="form-check form-check-inline">
<input v-model="form.price_setting" class="form-check-input" id="price_setting-num" type="radio" name="price_setting" id="price_setting-num" value="num" {{ $product->price_setting == 'num' ? 'checked' : '' }}>
<label class="form-check-label" for="price_setting-num">{{ __('common.enable') }}</label>
</div>
</div>
</x-admin::form.row>
<span v-if="form.price_setting === 'num'">
{{-- 阶梯价格设置表格 (最小起订量,产品价格),预览,可以增加删除价格区间--}}
<div class="price_setting_by_num">
<div class="left">
<div class="head">
<div class="num" >最小起订量</div>
<div class="price">产品价格</div>
<div class="delete">删除</div>
</div>
<div class="body">
<div class="item" v-for="(item,index) in form.numPrices">
<div class="num">
<div class="top">
<div class="title"></div>
<input type="number" :name="'numPrices[' + index + '][num]'" v-model="item.num" value="" placeholder="数量" required="required" class="form-control wp-100">
</div>
<div class="tip" ><span style="visibility: hidden;">_</span><span v-if="item.num">@{{ item.num }}</span></div>
</div>
<div class="price">
<input type="number" :name="'numPrices[' + index + '][price]'" v-model="item.price" value="" placeholder="价格" required="required" step="any" class="form-control wp-100">
<div class="tip" v-if="item.price">@{{ item.num }}件等于价格:@{{ item.num * item.price }}</div>
</div>
<div class="delete" @click="removeNumPrices(index)"><a style="cursor: pointer;color: #0072ff;">删除</a></div>
</div>
</div>
<div class="bottom">
<a style="cursor: pointer;color: #0072ff;" @click="addNumPrices"> {{ __('admin/product.add_variable') }}</a>
</div>
</div>
<div class="right">
<div class="head">
预览
</div>
<div class="body">
<div class="item" v-for="(item,index) in form.numPrices">
<div class="num" v-if="index < form.numPrices.length - 1">@{{ item.num }} ~ @{{ form.numPrices[index + 1].num - 1 }}</div>
<div class="num" v-else-if="item.num">@{{ item.num }}</div>
<div class="price" v-if="item.price">价格@{{ item.price }}</div>
</div>
</div>
</div>
</div>
<input class="form-control d-none" :value="numPricesIsEmpty" required>
<div class="invalid-feedback" style="font-size: 16px;margin-left: 200px;"><i class="bi bi-exclamation-circle-fill"></i> 新增价格区间</div>
<template>
{{-- <input type="hidden" value="{{ old('skus.0.image', $product->skus[0]->image ?? '') }}" name="skus[0][image]">--}}
{{-- <x-admin-form-input name="skus[0][model]" :title="__('admin/product.model')" :value="old('skus.0.model', $product->skus[0]->model ?? '')" />--}}
{{-- <x-admin-form-input name="skus[0][sku]" title="sku" :value="old('skus.0.sku', $product->skus[0]->sku ?? '')" required />--}}
{{-- <x-admin-form-input name="skus[0][price]" type="number" :title="__('admin/product.price')" :value="old('skus.0.price', $product->skus[0]->price ?? '')" step="any" required />--}}
{{-- --}}{{-- <x-admin-form-input name="skus[0][origin_price]" type="number" :title="__('admin/product.origin_price')" :value="old('skus.0.origin_price', $product->skus[0]->origin_price ?? '')" step="any" required />--}}
{{-- <x-admin-form-input name="skus[0][cost_price]" type="number" :title="__('admin/product.cost_price')" :value="old('skus.0.cost_price', $product->skus[0]->cost_price ?? '')" />--}}
{{-- <x-admin-form-input name="skus[0][quantity]" type="number" :title="__('admin/product.quantity')" :value="old('skus.0.quantity', $product->skus[0]->quantity ?? '')" />--}}
{{-- <input type="hidden" name="skus[0][price]" placeholder="variants" :value="form.numPrices.length !== 0 ? form.numPrices[0].price : ''">--}}
{{-- <input type="hidden" name="skus[0][origin_price]" placeholder="position" :value="form.numPrices.length !== 0 ? form.numPrices[form.numPrices.length - 1].price : ''">--}}
{{-- <input type="hidden" name="skus[0][variants]" placeholder="variants" value="">--}}
{{-- <input type="hidden" name="skus[0][position]" placeholder="position" value="0">--}}
{{-- <input type="hidden" name="skus[0][is_default]" placeholder="is_default" value="1">--}}
</template>
<span v-if="form.type !== 'free'">
<span v-if="form.type === 'weight'">
<x-admin-form-input name="first_weight" type="number" :title="__('admin/logistics.first_weight')"
:value="old('first_weight', $logistics->first_weight ?? '')" step="any" required/>
<x-admin-form-input name="first_weight_fee" type="number" :title="__('admin/logistics.first_weight_fee')"
:value="old('first_weight_fee', $logistics->first_weight_fee ?? '')" step="any"
required/>
<x-admin-form-input name="add_weight" type="number" :title="__('admin/logistics.add_weight')"
:value="old('add_weight', $logistics->add_weight ?? '')" step="any" required/>
<x-admin-form-input name="continuation_weight_fee" type="number"
:title="__('admin/logistics.continuation_weight_fee')"
:value="old('continuation_weight_fee', $logistics->continuation_weight_fee ?? '')"
step="any" required/>
<x-admin-form-input name="throwing_ratio" type="number" :title="__('admin/logistics.throwing_ratio')"
:value="old('throwing_ratio', $logistics->throwing_ratio ?? '')" step="any" required/>
{{-- <x-admin-form-input name="continuation_weight_max" type="number" :title="__('admin/logistics.continuation_weight_max')" :value="old('continuation_weight_max', $logistics->continuation_weight_max ?? '')" step="any" required />--}}
</span>
<span v-if="form.type === 'num'">
<x-admin-form-input name="first_weight" type="number" :title="__('admin/logistics.first_num')"
:value="old('first_weight', $logistics->first_weight ?? '')" step="any" required/>
<x-admin-form-input name="first_weight_fee" type="number" :title="__('admin/logistics.first_num_fee')"
:value="old('first_weight_fee', $logistics->first_weight_fee ?? '')" step="any"
required/>
<x-admin-form-input name="add_weight" type="number" :title="__('admin/logistics.add_num')"
:value="old('add_weight', $logistics->add_weight ?? '')" step="any" required/>
<x-admin-form-input name="continuation_weight_fee" type="number"
:title="__('admin/logistics.continuation_num_fee')"
:value="old('continuation_weight_fee', $logistics->continuation_weight_fee ?? '')"
step="any" required/>
{{-- <x-admin-form-input name="num_fee" type="number" :title="__('admin/logistics.num_fee')" :value="old('num_fee', $logistics->continuation_weight_fee ?? '')" step="any" required />--}}
</span>
</span>
<input type="hidden" name="variables" :value="JSON.stringify(form.variables)">
<input type="hidden" name="numPrices" value="" v-if="form.price_setting === 'sku'">
@hookwrapper('admin.product.edit.switch')
<x-admin::form.row :title="__('admin/product.enable_multi_spec')">
<el-switch v-model="editing.isVariable" @change="isVariableChange" class="mt-2"></el-switch>
</x-admin::form.row>
@endhookwrapper
<input type="hidden" name="variables" :value="JSON.stringify(form.variables)">
<div class="row g-3 mb-3" v-if="editing.isVariable">
<label for="" class="wp-200 col-form-label text-end"></label>
<div class="col-auto wp-200-">
<div class="selectable-variants">
<div>
<draggable :list="source.variables" :options="{animation: 100}">
<div v-for="(variant, variantIndex) in source.variables" :id="'selectable-variant-' + variantIndex">
<div class="title">
<div>
<b>@{{ variant.name[current_language_code] }}</b>
<el-link type="primary" @click="modalVariantOpenButtonClicked(variantIndex, null)">{{ __('common.edit') }}</el-link>
<el-link type="danger" class="ms-2" @click="removeSourceVariant(variantIndex)">{{ __('common.delete') }}</el-link>
</div>
<div>
<el-checkbox v-model="variant.isImage" @change="(e) => {variantIsImage(e, variantIndex)}" border size="mini" class="me-2 bg-white">{{ __('admin/product.add_variable_image') }}</el-checkbox>
<el-button type="primary" plain size="mini" @click="modalVariantOpenButtonClicked(variantIndex, -1)">{{ __('admin/product.add_variable_value') }}</el-button>
</div>
</div>
<template v-if="variant.values.length">
<draggable
element="div"
@start="isMove = true"
class="variants-wrap"
@update="(e) => {swapSourceVariantValue(e, variantIndex)}"
@end="isMove = false"
ghost-class="dragabble-ghost"
:list="variant.values"
:options="{animation: 100}"
>
<div v-for="(value, value_index) in variant.values" :key="value_index" class="variants-item" @dblclick="modalVariantOpenButtonClicked(variantIndex, value_index)">
<div class="open-file-manager variant-value-img" v-if="variant.isImage">
<div>
<img :src="thumbnail(value.image)" class="img-fluid">
</div>
</div>
<input type="hidden" v-model="value.image">
<div class="btn-remove" @click="removeSourceVariantValue(variantIndex, value_index)"><i class="el-icon-error"></i></div>
<div class="name">
@{{ value.name[current_language_code] }}
</div>
</div>
</draggable>
<div class="ps-2 mt-2 mb-3 opacity-50"><i class="bi bi-exclamation-circle"></i> {{ __('admin/product.modify_order') }}</div>
</template>
<div v-else class="d-flex justify-content-center align-items-center">
<div class="p-4 fs-5 btn" @click="modalVariantOpenButtonClicked(variantIndex, -1)"><i class="bi bi-plus-square-dotted"></i> {{ __('admin/product.add_variable_value') }}</div>
</div>
</div>
</draggable>
<el-button type="primary" plain size="small" @click="modalVariantOpenButtonClicked(-1, null)" class="btn btn-xs mr-1 mb-1">{{ __('admin/product.add_variable') }}</el-button>
</div>
<div v-if="form.skus.length && form.variables.length" class="mt-3 table-push table-responsive">
<div class="batch-setting d-flex align-items-center mb-3 p-2 bg-body">
<div v-for="(variant, index) in form.variables" :key="index" class="me-2">
<select class="form-select me-2 bg-white" aria-label="Default select example" v-model="variablesBatch.variables[index]">
<option selected value="">@{{ variant.name[current_language_code] || 'No name' }}</option>
<option :value="value_index" v-for="value, value_index in variant.values" :key="index+'-'+value_index" >
@{{ value.name[current_language_code]}}
</option>
</select>
</div>
<div role="button" class="border d-flex justify-content-center align-items-center border-dashed bg-light wh-40 me-2 bg-white" @click="batchSettingVariantImage">
<img :src="thumbnail(variablesBatch.image)" class="img-fluid" v-if="variablesBatch.image" style="max-height: 40px;">
<i class="bi bi-plus fs-3 text-muted" v-else></i>
</div>
<input type="text" class="form-control me-2 bg-white" v-model="variablesBatch.model" placeholder="{{ __('admin/product.model') }}">
<input type="text" class="form-control me-2 bg-white" v-model="variablesBatch.sku" placeholder="sku">
<input type="number" v-if="form.price_setting === 'sku'" class="form-control me-2 bg-white" v-model="variablesBatch.price" placeholder="{{ __('admin/product.price') }}">
<input type="number" v-if="form.price_setting === 'sku'" class="form-control me-2 bg-white" v-model="variablesBatch.origin_price" placeholder="{{ __('admin/product.origin_price') }}">
<input type="number" class="form-control me-2 bg-white" v-model="variablesBatch.cost_price" placeholder="{{ __('admin/product.cost_price') }}">
<input type="number" class="form-control me-2 bg-white" v-model="variablesBatch.quantity" placeholder="{{ __('admin/product.quantity') }}">
<button type="button" class="btn btn-primary text-nowrap" @click="batchSettingVariant">{{ __('common.batch_setting') }}</button>
</div>
<table class="table table-bordered table-hover">
<thead>
<th v-for="(variant, index) in form.variables" :key="'pv-header-' + index">
@{{ variant.name[current_language_code] || 'No name' }}
</th>
<th width="106px">{{ __('common.image') }}</th>
<th class="w-min-100">{{ __('admin/product.model') }}</th>
<th class="w-min-100">sku</th>
<th v-if="form.price_setting === 'sku'" class="w-min-100">{{ __('admin/product.price') }}</th>
<th v-if="form.price_setting === 'sku'" class="w-min-100">{{ __('admin/product.origin_price') }}</th>
<th class="w-min-100">{{ __('admin/product.cost_price') }}</th>
<th class="w-min-100">{{ __('admin/product.quantity') }}</th>
</thead>
<tbody>
<tr v-for="(sku, skuIndex) in form.skus" :key="skuIndex">
<template v-for="(variantValueIndex, j) in sku.variants">
<td v-if="skuIndex % variantValueRepetitions[j] == 0" :key="'pvv' + skuIndex + '-' + j"
:rowspan="variantValueRepetitions[j]">
<span>@{{ form.variables[j].values[variantValueIndex].name[current_language_code] || 'No name' }}</span>
</td>
</template>
<td>
<div class="product-images d-flex flex-wrap" style="margin-right: -8px">
<div v-for="image, index in sku.images" :key="index" class="product-item wh-40 border d-flex justify-content-center align-items-center me-2 mb-2 position-relative">
<div class="position-absolute top-0 end-0">
<button class="btn btn-danger btn-sm wh-20 p-0" @click="removeSkuImages(skuIndex, index)" type="button"><i class="bi bi-trash"></i></button>
</div>
<img :src="thumbnail(image)" class="img-fluid" style="max-height: 40px;">
<input type="hidden" class="form-control" v-model="sku.images[index]" :name="'skus[' + skuIndex + '][images][]'"
placeholder="image">
</div>
<div class="border d-flex justify-content-center align-items-center border-dashed bg-light wh-40" role="button" @click="addProductImages(skuIndex)"><i class="bi bi-plus fs-3 text-muted"></i></div>
</div>
<input type="hidden" class="form-control" :name="'skus[' + skuIndex + '][is_default]'" :value="skuIndex == 0 ? 1 : 0">
<input v-for="(variantValueIndex, j) in sku.variants" type="hidden"
:name="'skus[' + skuIndex + '][variants][' + j + ']'" :value="variantValueIndex">
</td>
<td><input type="text" class="form-control" v-model="sku.model" :name="'skus[' + skuIndex + '][model]'"
placeholder="{{ __('admin/product.model') }}"></td>
<td>
<input type="text" class="form-control" v-model="sku.sku" :name="'skus[' + skuIndex + '][sku]'" placeholder="sku" :style="sku.is_default ? 'margin-top: 19px;' : ''" required>
<span role="alert" class="invalid-feedback">{{ __('common.error_required', ['name' => 'sku']) }}</span>
<span v-if="sku.is_default" class="text-success">{{ __('admin/product.default_main_product') }}</span>
</td>
<td v-if="form.price_setting === 'sku'">
<input type="number" class="form-control" v-model="sku.price" :name="'skus[' + skuIndex + '][price]'" step="any"
placeholder="{{ __('admin/product.price') }}" required>
<span role="alert" class="invalid-feedback">{{ __('common.error_required', ['name' => __('admin/product.price')]) }}</span>
</td>
<td v-if="form.price_setting === 'sku'">
<input type="number" class="form-control" v-model="sku.origin_price" :name="'skus[' + skuIndex + '][origin_price]'" step="any"
placeholder="{{ __('admin/product.origin_price') }}" required>
<span role="alert" class="invalid-feedback">{{ __('common.error_required', ['name' => __('admin/product.origin_price')]) }}</span>
</td>
<input type="hidden" :name="'skus[' + skuIndex + '][price]'" v-if="form.price_setting === 'num'" placeholder="variants" :value="form.numPrices.length !== 0 ? form.numPrices[0].price : ''">
<input type="hidden" :name="'skus[' + skuIndex + '][origin_price]'" v-if="form.price_setting === 'num'" placeholder="position" :value="form.numPrices.length !== 0 ? form.numPrices[form.numPrices.length - 1].price : ''">
<td><input type="number" class="form-control" v-model="sku.cost_price" :name="'skus[' + skuIndex + '][cost_price]'"
placeholder="{{ __('admin/product.cost_price') }}">
</td>
<td><input type="number" class="form-control" v-model="sku.quantity" :name="'skus[' + skuIndex + '][quantity]'"
placeholder="{{ __('admin/product.quantity') }}"></td>
</tr>
</tbody>
</table>
</div>
</div>
<input class="form-control d-none" :value="skuIsEmpty" required>
<div class="invalid-feedback" style="font-size: 16px"><i class="bi bi-exclamation-circle-fill"></i> {{ __('admin/product.add_variable') }}</div>
</div>
</div>
@hookwrapper('admin.product.edit.variable')
<div v-if="!editing.isVariable">
<input type="hidden" value="{{ old('skus.0.image', $product->skus[0]->image ?? '') }}" name="skus[0][image]">
<x-admin-form-input name="skus[0][model]" :title="__('admin/product.model')" :value="old('skus.0.model', $product->skus[0]->model ?? '')" />
<x-admin-form-input name="skus[0][sku]" title="sku" :value="old('skus.0.sku', $product->skus[0]->sku ?? '')" required />
<span v-if="form.price_setting === 'sku'">
<x-admin-form-input name="skus[0][price]" type="number" :title="__('admin/product.price')" :value="old('skus.0.price', $product->skus[0]->price ?? '')" step="any" required />
<x-admin-form-input name="skus[0][origin_price]" type="number" :title="__('admin/product.origin_price')" :value="old('skus.0.origin_price', $product->skus[0]->origin_price ?? '')" step="any" required />
</span>
<x-admin-form-input name="skus[0][cost_price]" type="number" :title="__('admin/product.cost_price')" :value="old('skus.0.cost_price', $product->skus[0]->cost_price ?? '')" />
<x-admin-form-input name="skus[0][quantity]" type="number" :title="__('admin/product.quantity')" :value="old('skus.0.quantity', $product->skus[0]->quantity ?? '')" />
<input type="hidden" name="skus[0][price]" v-if="form.price_setting === 'num'" placeholder="variants" :value="form.numPrices.length !== 0 ? form.numPrices[0].price : ''">
<input type="hidden" name="skus[0][origin_price]" v-if="form.price_setting === 'num'" placeholder="position" :value="form.numPrices.length !== 0 ? form.numPrices[form.numPrices.length - 1].price : ''">
<input type="hidden" name="skus[0][variants]" placeholder="variants" value="">
<input type="hidden" name="skus[0][position]" placeholder="position" value="0">
<input type="hidden" name="skus[0][is_default]" placeholder="is_default" value="1">
</div>
@endhookwrapper
</div>
</div>
<div class="tab-pane fade" id="tab-descriptions">
<h6 class="border-bottom pb-3 mb-4">{{ __('admin/product.product_details') }}</h6>
<x-admin::form.row :title="__('admin/product.product_details')">
<ul class="nav nav-tabs mb-3" role="tablist">
@foreach ($languages as $language)
<li class="nav-item" role="presentation">
<button class="nav-link {{ $loop->first ? 'active' : '' }}" data-bs-toggle="tab" data-bs-target="#tab-descriptions-{{ $language->code }}" type="button" >{{ $language->name }}</button>
</li>
@endforeach
</ul>
<div class="tab-content">
@foreach ($languages as $language)
<div class="tab-pane fade {{ $loop->first ? 'show active' : '' }}" id="tab-descriptions-{{ $language->code }}">
<textarea name="descriptions[{{ $language->code }}][content]" class="form-control tinymce">
{{ old('content', $product->descriptions->keyBy('locale')[$language->code]->content ?? '') }}
</textarea>
</div>
@endforeach
@hook('admin.product.content.after')
</div>
</x-admin::form.row>
</div>
<div class="tab-pane fade" id="tab-attribute">
<h6 class="border-bottom pb-3 mb-4">{{ __('admin/attribute.index') }}</h6>
<x-admin::form.row title="{{ __('admin/attribute.set_attribute') }}">
<div class="pdf-table">
<table class="table table-bordered w-max-600">
<thead><th>{{ __('admin/attribute.index') }}</th><th>{{ __('admin/attribute.attribute_value') }}</th><th width="50px"></th></thead>
<tbody>
<tr v-for="item, index in form.attributes" :key="index">
<td>
<el-autocomplete
v-model="item.attribute.name"
:fetch-suggestions="attributeQuerySearch"
placeholder="{{ __('admin/builder.modules_keywords_search') }}"
value-key="name"
class="w-100"
size="small"
@select="(e) => {attributeHandleSelect(e, index, 'attribute')}"
></el-autocomplete>
<input type="text" required :name="'attributes['+ index +'][attribute_id]'" v-model="item.attribute.id" class="form-control d-none">
<div class="invalid-feedback">{{ __('common.error_required', ['name' => __('admin/attribute.index')]) }}</div>
</td>
<td>
<el-autocomplete
v-model="item.attribute_value.name"
:fetch-suggestions="((query, cb) => {attributeValueQuerySearch(query, cb, index)})"
size="small"
:disabled="item.attribute.id == ''"
value-key="name"
class="w-100"
:placeholder="item.attribute.id == '' ? '{{ __('admin/attribute.before_attribute') }}' : '{{ __('admin/builder.modules_keywords_search') }}'"
@select="(e) => {attributeHandleSelect(e, index, 'attribute_value')}"
></el-autocomplete>
<input type="text" required :name="'attributes['+ index +'][attribute_value_id]'" v-model="item.attribute_value.id" class="form-control d-none">
<div class="invalid-feedback">{{ __('common.error_required', ['name' => __('admin/attribute.attribute_value')]) }}</div>
</td>
<td class="text-end">
<i @click="form.attributes.splice(index, 1)" class="bi bi-x-circle fs-4 text-danger cursor-pointer"></i>
</td>
</tr>
<tr>
<td colspan="2"></td>
<td class="text-end"><i class="bi bi-plus-circle cursor-pointer fs-4" @click="addAttribute"></i></td>
</tr>
</tbody>
</table>
</div>
</x-admin::form.row>
</div>
<div class="tab-pane fade" id="tab-seo">
<h6 class="border-bottom pb-3 mb-4">SEO</h6>
@hook('admin.product.seo.before')
<x-admin-form-input-locale :width="600" name="descriptions.*.meta_title" title="Meta title" :value="$descriptions"/>
<x-admin::form.row title="Meta keywords">
<div class="input-locale-wrap">
@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_keywords]" class="form-control wp-400" placeholder="Meta keywords">{{ old('meta_keywords', $product->descriptions->keyBy('locale')[$language->code]->meta_keywords ?? '') }}</textarea>
</div>
@endforeach
</div>
</x-admin::form.row>
<x-admin::form.row title="Meta description">
<div class="input-locale-wrap">
@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_description]" class="form-control wp-400" placeholder="Meta description">{{ old('meta_description', $product->descriptions->keyBy('locale')[$language->code]->meta_description ?? '') }}</textarea>
</div>
@endforeach
</div>
</x-admin::form.row>
@hook('admin.product.seo.after')
</div>
<div class="tab-pane fade" id="tab-relations">
<h6 class="border-bottom pb-3 mb-4">{{ __('admin/product.product_relations') }}</h6>
<x-admin::form.row title="{{ __('admin/product.product_relations') }}">
<div class="module-edit-group wp-600">
<div class="autocomplete-group-wrapper">
<el-autocomplete
class="inline-input"
v-model="relations.keyword"
value-key="name"
size="small"
:fetch-suggestions="relationsQuerySearch"
placeholder="{{ __('admin/builder.modules_keywords_search') }}"
@select="relationsHandleSelect"
></el-autocomplete>
<div class="item-group-wrapper" v-loading="relations.loading">
<template v-if="relations.products.length">
<draggable
ghost-class="dragabble-ghost"
:list="relations.products"
:options="{animation: 330}"
>
<div v-for="(item, index) in relations.products" :key="index" class="item">
<div>
<i class="el-icon-s-unfold"></i>
<span>@{{ item.name }}</span>
</div>
<i class="el-icon-delete right" @click="relationsRemoveProduct(index)"></i>
<input type="text" :name="'relations['+ index +']'" v-model="item.id" class="form-control d-none">
</div>
</draggable>
</template>
<template v-else>{{ __('admin/builder.modules_please_products') }}</template>
</div>
</div>
<x-admin::form.row :title="__('admin/logistics.day')">
<div class="d-flex wp-400">
<input type="number" name="day_min" placeholder="{{ __('admin/logistics.day_min') }}" value="{{ old('day_min', $logistics->day_min ?? '') }}" class="form-control" style="flex: 0 0 95px" /><div style="width: 40px;line-height: 34px;text-align:center"> - </div>
<input type="number" name="day_max" placeholder="{{ __('admin/logistics.day_max') }}" value="{{ old('day_max', $logistics->day_max ?? '') }}" class="form-control" style="flex: 0 0 95px" /><div style="width: 40px;line-height: 34px;text-align:center">{{ __('admin/logistics.work_day') }}</div>
</div>
</x-admin::form.row>
</div>
<x-admin-form-input name="position" :title="__('common.sort_order')" :value="old('position', $logistics->position ?? '0')" />
</div>
<x-admin::form.row title="">
<button type="submit" @click="productsSubmit" class="btn d-none btn-primary btn-submit mt-3 btn-lg">{{ __('common.save') }}</button>
<button type="submit" @click="logisticsSubmit" class="btn d-none btn-primary btn-submit mt-3 btn-lg">{{ __('common.save') }}</button>
</x-admin::form.row>
<el-dialog
title="{{ __('common.edit') }}"
:visible.sync="dialogVariables.show"
width="400"
@close="closedialogVariablesFormDialog('form')"
:close-on-click-modal="false"
>
<el-form ref="form" :rules="rules" :model="dialogVariables.form" label-width="100px">
<el-form-item label="{{ __('common.name') }}" required class="language-inputs">
<el-form-item :prop="'name.' + lang.code" :inline-message="true" v-for="lang, lang_i in source.languages" :key="lang_i"
:rules="[
{ required: true, message: '{{ __('common.error_input_required') }}', trigger: 'blur' },
]"
>
<el-input size="mini" v-model="dialogVariables.form.name[lang.code]" placeholder="{{ __('common.name') }}"><template slot="prepend">@{{lang.name}}</template></el-input>
</el-form-item>
@hook('admin.product.sku.edit.item.after')
</el-form-item>
<el-form-item>
<el-button type="primary" @click="dialogVariablesFormSubmit('form')">{{ __('common.save') }}</el-button>
<el-button @click="closedialogVariablesFormDialog('form')">{{ __('common.cancel') }}</el-button>
</el-form-item>
</el-form>
</el-dialog>
<el-dialog title="{{ __('admin/attribute.attribute_value') }}" :visible.sync="attributeDialog.show" width="670px"
@close="attributeCloseDialog('attribute_form')" :close-on-click-modal="false">
<el-form ref="attribute_form" :model="attributeDialog.form" label-width="155px">
<el-form-item label="{{ __('common.name') }}" required class="language-inputs">
<el-form-item :prop="'name.' + lang.code" :inline-message="true" v-for="lang, lang_i in source.languages" :key="lang_i"
:rules="[
{ required: true, message: '{{ __('common.error_required', ['name' => __('common.name')]) }}', trigger: 'blur' },
]"
>
<el-input size="mini" v-model="attributeDialog.form.name[lang.code]" placeholder="{{ __('common.name') }}"><template slot="prepend">@{{lang.name}}</template></el-input>
</el-form-item>
</el-form-item>
<el-form-item>
<div class="d-flex d-lg-block mt-4">
<el-button type="primary" @click="attributeSubmit('attribute_form')">{{ __('common.save') }}</el-button>
<el-button @click="attributeCloseDialog('attribute_form')">{{ __('common.cancel') }}</el-button>
</div>
</el-form-item>
</el-form>
</el-dialog>
</form>
</div>
</div>
@hook('admin.product.form.footer')
@hook('admin.logistics.form.footer')
@endsection
@push('footer')
<script>
$('.submit-form').on('click', function () {
// 关闭多规格提交 清空 variables
if (!app.editing.isVariable) {
app.source.variables = [];
}
setTimeout(() => {
$(`form#app`).find('button[type="submit"]')[0].click();
@ -625,21 +130,19 @@
current_language_code: '{{ locale() }}',
isMove: false,
form: {
attributes: @json(old('pickups', $product_attributes) ?? []),
images: @json(old('images', $product->images) ?? []),
video: {
path: @json(old('video', $product->video ?? '')),
url: @json(image_origin(old('video', $product->video ?? ''))),
},
model: @json($product->skus[0]['model'] ?? ''),
price: @json($product->skus[0]['price'] ?? ''),
quantity: @json($product->skus[0]['quantity'] ?? ''),
sku: @json($product->skus[0]['sku'] ?? ''),
status: @json($product->skus[0]['status'] ?? false),
variables: @json($product->variables ?? []),
skus: @json(old('skus', $product->skus) ?? []),
price_setting: @json(old('price_setting', $product->price_setting) ?? 'sku'),
numPrices: @json(old('numPrices', $product->numprices) ?? []),
name: @json($logistics->name ?? ''),
warehouse_name: @json($logistics->warehouse_name ?? ''),
country_id: @json($logistics->country_id ?? ''),
type: '{{$logistics->type ?? "weight"}}',
first_weight: @json($logistics->first_weight ?? ''),
first_weight_fee: @json($logistics->first_weight_fee ?? ''),
continuation_weight_max: @json($logistics->continuation_weight_max ?? ''),
add_weight: @json($logistics->add_weight ?? ''),
continuation_weight_fee: @json($logistics->continuation_weight_fee ?? ''),
num_fee: @json($logistics->num_fee ?? ''),
throwing_ratio: @json($logistics->throwing_ratio ?? ''),
day_min: @json($logistics->day_min ?? ''),
day_max: @json($logistics->day_max ?? ''),
},
variablesBatch: {
@ -656,17 +159,17 @@
relations: {
keyword: '',
products: @json($relations ?? []),
logistics: @json($relations ?? []),
loading: null,
},
source: {
variables: @json($product->variables ?? []),
variables: @json($logistics->variables ?? []),
languages: @json($languages ?? []),
},
editing: {
isVariable: @json(($product->variables ?? null) != null),
isVariable: @json(($logistics->variables ?? NULL) != NULL),
},
dialogVariables: {
@ -713,9 +216,7 @@
},
beforeMount() {
if (this.form.variables.length) {
this.variablesBatch.variables = this.form.variables.map((v, i) => '');
}
},
watch: {
@ -744,12 +245,16 @@
},
created() {
if(this.form.numPrices.length === 0){
this.addNumPrices();
}
},
methods: {
dev(){
console.log(1);
return true;
},
logisticsType(e){
console.log(1);
},
addNumPrices() {
this.form.numPrices.push({num: '', price: ''});
},
@ -759,27 +264,27 @@
},
// 表单提交,检测是否开启多规格 做处理
productsSubmit() {
logisticsSubmit() {
if (!this.editing.isVariable) {
this.source.variables = [];
}
},
relationsQuerySearch(keyword, cb) {
$http.get('products/autocomplete?name=' + encodeURIComponent(keyword), null, {hload:true}).then((res) => {
$http.get('logistics/autocomplete?name=' + encodeURIComponent(keyword), null, {hload:true}).then((res) => {
cb(res.data);
})
},
relationsHandleSelect(item) {
if (!this.relations.products.find(v => v == item.id)) {
this.relations.products.push(item);
if (!this.relations.logistics.find(v => v == item.id)) {
this.relations.logistics.push(item);
}
this.relations.keyword = ""
},
relationsRemoveProduct(index) {
this.relations.products.splice(index, 1);
this.relations.logistics.splice(index, 1);
},
isVariableChange(e) {
@ -974,7 +479,7 @@
this.source.variables.splice(variantIndex, 1);
if (!this.source.variables.length) {
setTimeout(() => { // 等 remakeSkus 完成 this.form.skus = [];
this.form.skus = [{product_sku_id: 0,position: 1,variants: [],image: '',model: '',sku: '',price: null,quantity: null,is_default: 1}];
this.form.skus = [{logistics_sku_id: 0,position: 1,variants: [],image: '',model: '',sku: '',price: null,quantity: null,is_default: 1}];
this.editing.isVariable = false;
}, 0);
}
@ -1054,16 +559,16 @@
}
// 找出已存在的组合
const productVariantCombos = this.form.skus.map(v => v.variants.join()); // ['0,0,0', '0,0,1']
const logisticsVariantCombos = this.form.skus.map(v => v.variants.join()); // ['0,0,0', '0,0,1']
let skus = [];
for (var i = 0; i < combos.length; i++) {
const combo = combos[i]; // 0,0,0
const index = productVariantCombos.indexOf(combo.join());
const index = logisticsVariantCombos.indexOf(combo.join());
if (index > -1) {
skus.push(this.form.skus[index]);
} else {
skus.push({
product_sku_id: 0,
logistics_sku_id: 0,
position: i,
variants: combo,
images: [],
@ -1145,6 +650,22 @@
}
});
});
$(document).ready(function ($) {
$('#country-autocomplete').autocomplete({
'source': function(request, response) {
$http.get(`countries/autocomplete?name=${encodeURIComponent(request)}`, null, {hload: true}).then((res) => {
response($.map(res.data, function(item) {
return {label: item['name'], value: item['id']}
}));
})
},
'select': function(item) {
$(this).val(item['label']);
$('input[name="country_id"]').val(item['value']);
}
});
});
</script lang="scss">
<style>
.price_setting_by_num{

View File

@ -1,6 +1,6 @@
@extends('admin::layouts.master')
@section('title', __('admin/common.product'))
@section('title', __('admin/common.logistics'))
@section('content')
@if ($errors->has('error'))
@ -14,154 +14,82 @@
<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>
<a href="{{ admin_route('logistics.create') }}" class="me-1 nowrap">
<button class="btn btn-primary">{{ __('admin/logistics.logistics_create') }}</button>
</a>
@else
@if ($products->total())
<button class="btn btn-primary" @click="clearRestore">{{ __('admin/product.clear_restore') }}</button>
@if ($logistics->total())
<button class="btn btn-primary" @click="clearRestore">{{ __('admin/logistics.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())
@if ($logistics->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>
<tr>
<th><input type="checkbox" v-model="allSelected" /></th>
<th>{{ __('common.id') }}</th>
<th>{{ __('admin/logistics.logistics_name') }}</th>
<th>{{ __('admin/logistics.warehouse_name') }}</th>
<th>{{ __('admin/country.index') }}</th>
<th>{{ __('admin/logistics.type') }}</th>
<th>{{ __('admin/logistics.first_weight') }}</th>
<th>{{ __('admin/logistics.first_weight_fee') }}</th>
<th>{{ __('admin/logistics.add_weight') }}</th>
<th>{{ __('admin/logistics.continuation_weight_fee') }}</th>
<th>{{ __('admin/logistics.throwing_ratio') }}</th>
<th>{{ __('admin/logistics.day') }}</th>
<th>{{ __('common.sort_order') }}</th>
<th>{{ __('common.created_at') }}</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>
@hook('admin.product.list.column')
<th class="text-end">{{ __('common.action') }}</th>
</tr>
</thead>
<tbody>
@foreach ($products_format as $product)
@foreach ($logistics_format as $logistic)
<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><input type="checkbox" :value="{{ $logistic['id'] }}" v-model="selectedIds" /></td>
<td>{{ $logistic['id'] }}</td>
<td>{{ $logistic['name'] }}</td>
<td>{{ $logistic['warehouse_name'] }}</td>
<td>{{ $logistic['country'] }}</td>
<td>{{ __('admin/logistics.type_' . $logistic['type']) }}</td>
<td>{{ $logistic['first_weight'] }}</td>
<td>{{ $logistic['first_weight_fee'] }}</td>
{{-- <td>{{ $logistic['continuation_weight_max'] }}</td>--}}
<td>{{ $logistic['add_weight'] }}</td>
<td>{{ $logistic['continuation_weight_fee'] }}</td>
{{-- <td>{{ $logistic['num_fee'] }}</td>--}}
<td>{{ $logistic['throwing_ratio'] }}</td>
<td>{{ $logistic['day_min'] }} - {{ $logistic['day_max'] }} {{ __('admin/logistics.work_day') }}</td>
<td>{{ $logistic['position'] }}</td>
<td>{{ $logistic['created_at'] }}</td>
@hook('admin.logistic.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>
@if ($logistic['deleted_at'] == '')
<a href="{{ admin_route('logistics.edit', [$logistic['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')
@hook('admin.logistics.trashed.action')
@endif
</td>
</tr>
@endforeach
@endforeach
</tbody>
</table>
</div>
{{ $products->withQueryString()->links('admin::vendor/pagination/bootstrap-4') }}
{{ $logistics->withQueryString()->links('admin::vendor/pagination/bootstrap-4') }}
@else
<x-admin-no-data />
@endif
@ -188,7 +116,7 @@
order_by: bk.getQueryString('order_by', ''),
},
selectedIds: [],
productIds: @json($products->pluck('id')),
productIds: @json($logistics->pluck('id')),
},
computed: {
@ -224,7 +152,7 @@
cancelButtonText: '{{ __('common.cancel') }}',
type: 'warning'
}).then(() => {
$http.delete('products/delete', {ids: this.selectedIds}).then((res) => {
$http.delete('logistics/delete', {ids: this.selectedIds}).then((res) => {
layer.msg(res.message)
location.reload();
})
@ -277,7 +205,7 @@
this.$confirm('{{ __('common.confirm_delete') }}', '{{ __('common.text_hint') }}', {
type: 'warning'
}).then(() => {
$http.delete('products/' + id).then((res) => {
$http.delete('logistics/' + id).then((res) => {
this.$message.success(res.message);
location.reload();
})

View File

@ -244,6 +244,7 @@
<el-link type="danger" class="ms-2" @click="removeSourceVariant(variantIndex)">{{ __('common.delete') }}</el-link>
</div>
<div>
<el-checkbox v-model="variant.isNumSelect" @change="(e) => {variantIsNumSelect(e, variantIndex)}" border size="mini" class="me-2 bg-white">{{ __('admin/product.num_select') }}</el-checkbox>
<el-checkbox v-model="variant.isImage" @change="(e) => {variantIsImage(e, variantIndex)}" border size="mini" class="me-2 bg-white">{{ __('admin/product.add_variable_image') }}</el-checkbox>
<el-button type="primary" plain size="mini" @click="modalVariantOpenButtonClicked(variantIndex, -1)">{{ __('admin/product.add_variable_value') }}</el-button>
</div>
@ -792,6 +793,13 @@
})
}
},
variantIsNumSelect(e, index) {
this.source.variables.forEach(v => {
v.isNumSelect = false;
})
this.source.variables[index].isNumSelect = true;
}
,
batchSettingVariantImage() {
bk.fileManagerIframe(images => {

View File

@ -234,6 +234,58 @@ body.page-product {
}
}
}
.variable-info-select {
.variable-info-select-num {
.variable-info-select-num-show {
> div {
display: inline-flex;
align-items: center;
justify-content: center;
margin-right: 0.5rem;
margin-bottom: 0.5rem;
border: 1px solid #ddd;
margin-left: 0;
min-width: 3rem;
padding: 0.5rem;
cursor: pointer;
text-align: center;
font-weight: bold;
flex-direction: column;
// border-radius: 2rem;
transition: all .1s ease-in-out;
&:hover, &.selected {
border-color: #222;
}
> span.image {
width: 50px;
margin-top: -0.5rem;
margin-left: -0.5rem;
margin-right: -0.5rem;
margin-bottom: 6px;
}
&:not(.selected) {
&.disabled {
border: 1px dashed #bfbfbf;
color: #999;
font-weight: initial;
}
}
}
}
}
.quantity-wrap {
height: 43px;
@media (max-width: 768px) {
margin-bottom: 10px;
}
}
}
}
.peoduct-info {
@ -278,6 +330,36 @@ body.page-product {
margin-bottom: 10px;
}
}
}
.totals {
padding-left: 0;
list-style: none;
padding-bottom: .3rem;
margin-bottom: 1.5rem;
border-bottom: 1px solid #E6E6E6;
> li {
display: flex;
align-items: center; // flex-start | center
justify-content: space-between; // flex-end | center | space-between
margin-bottom: 14px;
&:last-of-type {
font-weight: bold;
> span {
&:last-of-type {
color: #dc3545;
}
}
}
> span {
&:first-of-type {
font-size: .8rem;
}
}
}
}
}
@ -326,4 +408,4 @@ body.page-product {
}
}
}
}
}

View File

@ -10,6 +10,7 @@
*/
return [
'country_name' => 'Country Name',
'country' => 'Country',
'countries_index' => 'Country List',
'countries_create' => 'Create Country',

View File

@ -55,4 +55,25 @@ return [
'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? ',
'warehouse_name' => 'Warehouse name',
'type'=>'Type',
'first_weight'=>'First weight',
'first_weight_fee'=>'First weight fee',
'continuation_weight_max'=>'Continuation weight max',
'add_weight'=>'Add weight',
'continuation_weight_fee'=>'Continuation weight fee',
'num_fee'=>'Num fee',
'throwing_ratio'=>'Throwing ratio',
'day_min'=>'Day min',
'day_max'=>'Day max',
'day'=>'Day',
'work_day'=>'Work day',
'type_weight'=>'Billing by weight',
'type_num'=>'Quantity based billing',
'type_free'=>'Merchant package shipping',
'first_num'=>'First num',
'first_num_fee'=>'First num fee',
'add_num'=>'Add num',
'continuation_num_fee'=>'Continuation num fee',
];

View File

@ -20,4 +20,5 @@ return [
'pages_update' => 'Edit',
'pages_delete' => 'Delete',
'inquiry' => 'Inquiry',
'logistics' => 'Logistics',
];

View File

@ -55,4 +55,5 @@ return [
'confirm_batch_status' => 'Confirm to modify the status of the selected products in batches? ',
'confirm_batch_restore' => 'Confirm to restore the selected product? ',
'confirm_delete_restore' => 'Are you sure you want to empty the recycle bin? ',
'num_select' => 'Num select ',
];

View File

@ -10,6 +10,7 @@
*/
return [
'country_name' => '国家名称',
'index' => '国家',
'countries_index' => '国家列表',
'countries_create' => '创建国家',

View File

@ -59,5 +59,25 @@ return [
'price_setting_by' =>[
'sku' => '根据规格设置价格',
'num' => '根据数量设置价格',
]
],
'warehouse_name' => '仓库名称',
'type'=>'类型',
'first_weight'=>'首重',
'first_weight_fee'=>'首重运费',
'continuation_weight_max'=>'续重范围',
'add_weight'=>'每增加重量',
'continuation_weight_fee'=>'续重运费',
'num_fee'=>'一件价格',
'throwing_ratio'=>'计抛比',
'day_min'=>'最短时间',
'day_max'=>'最长时间',
'day'=>'时效',
'work_day'=>'工作天',
'type_weight'=>'按重量计费',
'type_num'=>'按数量计费',
'type_free'=>'商家包邮',
'first_num'=>'首件数量',
'first_num_fee'=>'首件运费',
'add_num'=>'每增加件',
'continuation_num_fee'=>'续件运费',
];

View File

@ -21,4 +21,5 @@ return [
'pages_delete' => '删除文章',
'inquiry' => '询盘管理',
'logistics' => '物流管理',
];

View File

@ -59,5 +59,6 @@ return [
'price_setting_by' =>[
'sku' => '根据规格设置价格',
'num' => '根据数量设置价格',
]
],
'num_select' => '数量选择',
];

View File

@ -144,7 +144,7 @@
<div class="variables-wrap mb-4" v-if="source.variables.length">
<div class="variable-group mb-2" v-for="variable, variable_index in source.variables" :key="variable_index">
<p class="mb-2">@{{ variable.name }}</p>
<div class="variable-info">
<div class="variable-info" v-if="!variable.isNumSelect">
<div
v-for="value, value_index in variable.values"
@click="checkedVariableValue(variable_index, value_index, value)"
@ -154,6 +154,28 @@
@{{ value.name }}
</div>
</div>
<div class="variable-info-select" v-if="variable.isNumSelect">
<div class="variable-info-select-num" v-for="value, value_index in variable.values" style="display: flex;
align-items: center;
justify-content: space-between;
width: 500px;">
<div class="variable-info-select-num-show">
<div
@click="checkedVariableValue(variable_index, value_index, value)"
:key="value_index">
<span class="image" v-if="value.image"><img :src="value.image" class="img-fluid"></span>
@{{ value.name }}
</div>
</div>
<div class="quantity-wrap">
<input type="text" @change="quantityChange" class="form-control" :disabled="!product.quantity" onkeyup="this.value=this.value.replace(/\D/g,'')" v-model="value.quantity" value="0" :name="'variables['+ variable_index +'].value['+value_index+'].quantity'">
<div class="right">
<i class="bi bi-chevron-up" @click="quantityChange"></i>
<i class="bi bi-chevron-down" @click="quantityChange"></i>
</div>
</div>
</div>
</div>
</div>
</div>
@ -162,8 +184,8 @@
@hook('product.detail.buy.before')
@hookwrapper('product.detail.quantity.input')
<div class="quantity-wrap">
<input type="text" class="form-control" :disabled="!product.quantity" onkeyup="this.value=this.value.replace(/\D/g,'')" v-model="quantity" name="quantity">
<div class="right">
<input type="text" readonly class="form-control" :disabled="!product.quantity" onkeyup="this.value=this.value.replace(/\D/g,'')" v-model="quantity" name="quantity">
<div class="right" v-if="!isNumSelect">
<i class="bi bi-chevron-up"></i>
<i class="bi bi-chevron-down"></i>
</div>
@ -208,7 +230,7 @@
@hookwrapper('product.detail.quantity.input')
<div class="quantity-wrap">
<input type="text" class="form-control" :disabled="!product.quantity" onkeyup="this.value=this.value.replace(/\D/g,'')" v-model="quantity" name="quantity">
<div class="right">
<div class="right" v-if="!isNumSelect">
<i class="bi bi-chevron-up"></i>
<i class="bi bi-chevron-down"></i>
</div>
@ -226,6 +248,11 @@
@endhookwrapper
</div>
@endif
<ul class="totals">
<li><span>Product Total</span><span>@{{ product_total }}</span></li>
<li><span>Shipping Fee</span><span>@{{ shipping_fee }}</span></li>
<li><span>Order Total</span><span>@{{ order_tota }}</span></li>
</ul>
<button
style="width: 16em;"
class="btn btn-outline-dark my-lg-2 add-cart fw-bold"
@ -370,6 +397,10 @@
el: '#product-app',
data: {
product_total:0,
shipping_fee:0,
order_tota:0,
isNumSelect:false,
selectedVariantsIndex: [], // 选中的变量索引
images: [],
product: {
@ -384,7 +415,7 @@
quantity: 0,
sku: "",
},
quantity: 1,
quantity: 0,
source: {
skus: @json($product['skus']),
variables: @json($product['variables'] ?? []),
@ -418,10 +449,37 @@
},
},
watch:{
quantity: function(newval,oldval){
let price = 0;
if(this.numPrices.light == 0){
price = this.product.price
}else {
price = this.numPrices[0].price
this.numPrices.forEach(v => {
if(this.quantity > v.num){}
price = v.price
})
}
this.product_total = (price * this.quantity).toFixed(2)
this.shipping_fee = (this.product_total * 0.1 ).toFixed(2)
this.order_tota = (this.product_total *1 + this.shipping_fee*1).toFixed(2)
}
},
created() {
if(this.price_setting === 'num'){
this.quantity = this.numPrices[0].num;
// this.quantity = this.numPrices[0].num;
}
this.source.variables.forEach(variable => {
if(variable.isNumSelect == true){
this.isNumSelect = true;
}
variable.values.forEach(value => {
value.quantity = 0
})
})
},
beforeMount () {
@ -449,6 +507,19 @@
},
methods: {
quantityChange(){
setTimeout(()=>{
let num = 0;
this.source.variables.forEach(variable => {
variable.values.forEach(value => {
num += parseInt(value.quantity);
})
})
this.quantity = num;
console.log(this.quantity,num)
},200);
},
addSinger(form){ // 点击确定按钮添加方法
this.$refs[form].validate((valid) => {