商品多规格表单

This commit is contained in:
Sam Chen 2022-01-02 22:50:29 +08:00
parent 42830e4938
commit 274918cb8f
7 changed files with 224 additions and 50 deletions

View File

@ -48,9 +48,11 @@ class ProductsController extends Controller
return view('admin.pages.products.form.form', $data);
}
public function update(Request $request, $id)
public function update(Request $request, Product $product)
{
//
$product = (new ProductService)->update($product, $request->all());
return redirect()->route('admin.products.index')->with('success', 'product updated');
}
public function destroy($id)

View File

@ -9,15 +9,15 @@ class Product extends Model
{
use HasFactory;
protected $fillable = ['image', 'video', 'sort_order', 'status', 'variable'];
protected $fillable = ['image', 'video', 'position', 'active', 'variables'];
public function skus()
{
return $this->hasMany(ProductSku::class);
}
public function getVariableDecodedAttribute()
public function getVariablesDecodedAttribute()
{
return json_decode($this->variable, true);
return json_decode($this->variables, true);
}
}

View File

@ -9,5 +9,9 @@ class ProductSku extends Model
{
use HasFactory;
protected $fillable = ['product_id', 'image', 'model', 'sku', 'price', 'quantity', 'is_default'];
protected $fillable = ['product_id', 'variants', 'position', 'image', 'model', 'sku', 'price', 'origin_price', 'cost_price', 'quantity', 'is_default'];
protected $casts = [
'variants' => 'array',
];
}

View File

@ -7,24 +7,35 @@ use Illuminate\Support\Facades\DB;
class ProductService
{
public function create($data)
public function create(array $data): Product
{
$product = new Product;
return $this->createOrUpdate($product, $data);
}
public function update(Product $product, array $data): Product
{
return $this->createOrUpdate($product, $data);
}
protected function createOrUpdate(Product $product, array $data): Product
{
$isUpdating = $product->id > 0;
try {
DB::beginTransaction();
$product = new Product($data);
if (isset($data['variant'])) {
$product->variable = json_encode($data['variant']);
}
$product->fill($data);
$product->saveOrFail();
$skus = [];
foreach ($data['skus'] as $index => $rawSku) {
$sku = $rawSku;
$sku['is_default'] = $index == 0;
$skus[] = $sku;
$skus[] = $rawSku;
}
if ($isUpdating) {
$product->skus()->delete();
}
$product->skus()->createMany($skus);
DB::commit();

View File

@ -15,11 +15,11 @@ class CreateTables extends Migration
{
Schema::create('products', function (Blueprint $table) {
$table->id()->startingValue(100_000);
$table->string('image');
$table->string('video');
$table->integer('sort_order');
$table->boolean('status');
$table->json('variable');
$table->string('image')->default('');
$table->string('video')->default('');
$table->integer('position')->default(0);
$table->boolean('active');
$table->json('variables')->nullable();
$table->timestamps();
$table->softDeletes();
});
@ -27,11 +27,15 @@ class CreateTables extends Migration
Schema::create('product_skus', function (Blueprint $table) {
$table->id()->startingValue(100_000);
$table->unsignedBigInteger('product_id');
$table->string('image');
$table->string('model');
$table->string('sku');
$table->double('price');
$table->integer('quantity');
$table->string('variants')->default(0);
$table->integer('position')->default(0);
$table->string('image')->default('');
$table->string('model')->default('');
$table->string('sku')->default('');
$table->double('price')->default(0);
$table->double('origin_price')->default(0);
$table->double('cost_price')->default(0);
$table->integer('quantity')->default(0);
$table->boolean('is_default');
$table->timestamps();
});

View File

@ -6,14 +6,15 @@
@section('content')
<h2>product</h2>
<form action="{{ route('admin.products.store') }}" method="POST" id="app">
<form action="{{ ($product ?? null) ? route('admin.products.update', $product) : route('admin.products.store') }}" method="POST" id="app">
@csrf
@method(($product ?? null) ? 'PUT' : 'POST')
<input type="text" name="name" placeholder="Name" value="{{ old('name', $product->name ?? '') }}">
<input type="text" name="image" placeholder="image" value="{{ old('image', $product->image ?? '') }}">
<input type="text" name="video" placeholder="video" value="{{ old('video', $product->video ?? '') }}">
<input type="text" name="sort_order" placeholder="sort_order" value="{{ old('sort_order', $product->sort_order ?? 0) }}">
<input type="text" name="status" placeholder="status" value="{{ old('status', $product->status ?? 1) }}">
<input type="text" name="position" placeholder="position" value="{{ old('position', $product->position ?? 0) }}">
<input type="text" name="active" placeholder="active" value="{{ old('active', $product->active ?? 1) }}">
<div>
<h2>skus</h2>
@ -21,35 +22,72 @@
<input type="radio" v-model="editing.isVariable" :value="true"> 多规格
<div v-if="editing.isVariable">
<div>
<div v-for="variant in form.variable">
<input type="text" v-model="variant.name" name="variant[0][name]" placeholder="variant name">
<input v-for="(value, valueIndex) in variant.values" v-model="variant.values[valueIndex]" type="text" name="variant[0][values][0]" placeholder="variant value name">
<div v-for="(variant, variantIndex) in source.variables">
<div>
<input type="text" v-model="variant.name" placeholder="variant name">
<div v-for="(value, valueIndex) in variant.values">
<input v-model="variant.values[valueIndex].name" type="text" placeholder="variant value name">
</div>
<button type="button" @click="addVariantValue(variantIndex)">Add value</button>
</div>
</div>
<button type="button" @click="addVariant">Add variant</button>
</div>
<div>
<div v-if="form.skus.length">
<input v-if="form.skus.length" type="hidden" name="variables" :value="JSON.stringify(form.variables)">
<table>
<tr v-for="(sku, skuIndex) in form.skus">
<td><input type="text" v-model="sku.image" name="skus[0][image]" placeholder="image"></td>
<td><input type="text" v-model="sku.model" name="skus[0][model]" placeholder="model"></td>
<td><input type="text" v-model="sku.sku" name="skus[0][sku]" placeholder="sku"></td>
<td><input type="text" v-model="sku.price" name="skus[0][price]" placeholder="price" value="10"></td>
<td><input type="text" v-model="sku.quantity" name="skus[0][quantity]" placeholder="quantity" value="10"></td>
<td><input type="text" v-model="sku.is_default" name="skus[0][is_default]" placeholder="is_default" value="1"></td>
</tr>
<thead>
<th v-for="(variant, index) in form.variables" :key="'pv-header-'+index">
@{{ variant.name || 'No name' }}
</th>
<th>image</th>
<th>model</th>
<th>sku</th>
<th>price</th>
<th>orgin price</th>
<th>cost price</th>
<th>quantity</th>
</thead>
<tbody>
<tr v-for="(sku, skuIndex) in form.skus">
<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 || 'No name' }}</span>
</td>
</template>
<td>
<input type="text" v-model="sku.image" :name="'skus[' + skuIndex + '][image]'" placeholder="image">
<input type="hidden" :name="'skus[' + skuIndex + '][position]'" :value="skuIndex">
<input type="hidden" :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" v-model="sku.model" :name="'skus[' + skuIndex + '][model]'" placeholder="model"></td>
<td><input type="text" v-model="sku.sku" :name="'skus[' + skuIndex + '][sku]'" placeholder="sku"></td>
<td><input type="text" v-model="sku.price" :name="'skus[' + skuIndex + '][price]'" placeholder="price"></td>
<td><input type="text" v-model="sku.origin_price" :name="'skus[' + skuIndex + '][origin_price]'" placeholder="origin_price"></td>
<td><input type="text" v-model="sku.cost_price" :name="'skus[' + skuIndex + '][cost_price]'" placeholder="cost_price"></td>
<td><input type="text" v-model="sku.quantity" :name="'skus[' + skuIndex + '][quantity]'" placeholder="quantity"></td>
</tr>
</tbody>
</table>
</div>
</div>
<div v-if="!editing.isVariable">
<div>
<input type="text" name="skus[0][image]" placeholder="image">
<input type="text" name="skus[0][model]" placeholder="model">
<input type="text" name="skus[0][sku]" placeholder="sku">
<input type="text" name="skus[0][price]" placeholder="price" value="10">
<input type="text" name="skus[0][quantity]" placeholder="quantity" value="10">
<input type="text" name="skus[0][is_default]" placeholder="is_default" value="1">
<input type="text" name="skus[0][image]" placeholder="image" value="{{ old('skus.0.image', $product->skus[0]->image ?? '') }}">
<input type="text" name="skus[0][model]" placeholder="model" value="{{ old('skus.0.model', $product->skus[0]->model ?? '') }}">
<input type="text" name="skus[0][sku]" placeholder="sku" value="{{ old('skus.0.sku', $product->skus[0]->sku ?? '') }}">
<input type="text" name="skus[0][price]" placeholder="price" value="{{ old('skus.0.price', $product->skus[0]->price ?? '') }}">
<input type="text" name="skus[0][origin_price]" placeholder="origin_price" value="{{ old('skus.0.origin_price', $product->skus[0]->origin_price ?? '') }}">
<input type="text" name="skus[0][cost_price]" placeholder="cost_price" value="{{ old('skus.0.cost_price', $product->skus[0]->cost_price ?? '') }}">
<input type="text" name="skus[0][quantity]" placeholder="quantity" value="{{ old('skus.0.quantity', $product->skus[0]->quantity ?? '') }}">
<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>
</div>
</div>
@ -60,17 +98,132 @@
@push('footer')
<script>
new Vue({
var app = new Vue({
el: '#app',
data: {
form: {
variable: @json($product->variable_decoded ?? []),
variables: @json($product->variables_decoded ?? []),
skus: @json($product->skus ?? []),
},
source: {
variables: @json($product->variables_decoded ?? []),
},
editing: {
isVariable: @json(($product->variable ?? null) != null),
isVariable: @json(($product->variables ?? null) != null),
}
},
computed: {
// variant value 重复次数
variantValueRepetitions() {
var repeats = [];
var repeat = 1;
for (var index = this.form.variables.length - 2; index >= 0; index--) {
repeat *= this.form.variables[index + 1].values.length;
repeats[index] = repeat;
}
// 最后一组只重复1次
repeats.push(1);
return repeats;
},
},
watch: {
'source.variables': {
deep: true,
handler: function () {
// 原始规格数据变动,过滤有效规格并同步至 form.variables
let variants = [];
const sourceVariants = JSON.parse(JSON.stringify(this.source.variables));
for (var i = 0; i < sourceVariants.length; i++) {
const sourceVariant = sourceVariants[i];
// 排除掉没有规格值的
if (sourceVariant.values.length > 0) {
variants.push(sourceVariant);
}
}
this.form.variables = variants;
this.remakeSkus();
}
}
},
methods: {
addVariant() {
this.source.variables.push({name: '', values: []});
},
addVariantValue(variantIndex) {
this.source.variables[variantIndex].values.push({name: '', image: ''});
},
remakeSkus() {
const combos = makeVariableIndexes();
if (combos.length < 1) {
this.form.skus = [];
return;
}
// 找出已存在的组合
const productVariantCombos = 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());
if (index > -1) {
skus.push(this.form.skus[index]);
} else {
skus.push({
product_sku_id: 0,
position: i,
variants: combo,
image: '',
model: '',
sku: '',
price: null,
quantity: null,
is_default: i == 0,
});
}
}
// 第一个子商品用主商品的值
skus[0].model = this.form.model;
skus[0].sku = this.form.sku;
skus[0].price = this.form.price;
skus[0].quantity = this.form.quantity;
skus[0].status = this.form.status;
this.form.skus = skus;
},
}
});
function makeVariableIndexes() {
// 每组值重复次数
var repeats = app.variantValueRepetitions;
var results = [];
if (app.form.variables.length < 1) {
return results;
}
for (let i = 0; i < repeats[0] * app.form.variables[0].values.length; i++) {
results.push([]);
}
for (let xIndex = 0; xIndex < repeats.length; xIndex++) { // 0 - 3
let repeat = 0;
let itemIndex = 0;
for (let yIndex = 0; yIndex < results.length; yIndex++) { // 0 - 36
results[yIndex].push(itemIndex);
repeat++;
if (repeat >= repeats[xIndex]) {
repeat = 0;
itemIndex++;
if (itemIndex >= app.form.variables[xIndex].values.length) {
itemIndex = 0;
}
}
}
}
return results;
}
</script>
@endpush

View File

@ -6,7 +6,7 @@
@foreach ($products as $product)
<tr>
<td>{{ $product->id }}</td>
<td>{{ $product->variable ? '多规格' : '单规格' }}</td>
<td>{{ $product->variables ? '多规格' : '单规格' }}</td>
<td>
<a href="{{ route('admin.products.edit', $product) }}">编辑</a>
</td>