分拆 beike namespace (#2)

* 分拆 beike namespace

Co-authored-by: Sam Chen <samchen945@gmail.com>
This commit is contained in:
Sam Chen 2022-05-17 15:37:27 +08:00 committed by GitHub
parent bc91c4a323
commit 22b7783312
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
87 changed files with 1921 additions and 692 deletions

2
.gitignore vendored
View File

@ -15,3 +15,5 @@ yarn-error.log
/.vscode
mix-manifest.json
package-lock.json
public/beike
beike/node_modules

View File

@ -1,67 +0,0 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\CategoryRequest;
use App\Http\Resources\Admin\CategoryResource;
use App\Models\Category;
use App\Services\CategoryService;
use Illuminate\Http\Request;
class CategoriesController extends Controller
{
public function index()
{
$categories = Category::with('description', 'children.description', 'children.children.description')
->where('parent_id', 0)
->get();
$data = [
'categories' => CategoryResource::collection($categories),
];
return view('admin.pages.categories.index', $data);
}
public function create(Request $request)
{
$category = new Category();
$data = [
'category' => $category,
];
return view('admin.pages.categories.form', $data);
}
public function store(CategoryRequest $request)
{
$redirect = $request->_redirect ?? admin_route('categories.index');
$category = (new CategoryService())->create($request->all());
return redirect($redirect)->with('success', 'Category created successfully');
}
public function edit(Category $category)
{
$descriptions = $category->descriptions->keyBy('locale');
$data = [
'category' => $category,
'descriptions' => $descriptions,
];
return view('admin.pages.categories.form', $data);
}
public function update(CategoryRequest $request, Category $category)
{
$redirect = $request->_redirect ?? admin_route('categories.index');
$category = (new CategoryService())->update($category, $request->all());
return redirect($redirect)->with('success', 'Category created successfully');
}
}

View File

@ -1,81 +0,0 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Http\Resources\Admin\ProductResource;
use App\Models\Product;
use App\Services\ProductService;
use Illuminate\Http\Request;
class ProductsController extends Controller
{
public function index(Request $request)
{
$query = Product::query()
->with('description')
->withCount('skus');
if ($request->trashed) {
$query->onlyTrashed();
}
$products = $query->paginate();
$data = [
'products' => ProductResource::collection($products),
];
return view('admin.pages.products.index', $data);
}
public function create(Request $request)
{
return view('admin.pages.products.form.form');
}
public function store(Request $request)
{
$product = (new ProductService)->create($request->all());
$redirect = $request->_redirect ?? route('admin.products.index');
return redirect($redirect)->with('success', 'product created');
}
public function show($id)
{
//
}
public function edit(Product $product)
{
$product->loadMissing('descriptions');
$data = [
'product' => $product,
];
return view('admin.pages.products.form.form', $data);
}
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(Request $request, Product $product)
{
$product->delete();
return ['success' => true];
}
public function restore(Request $request)
{
$productId = $request->id ?? 0;
Product::withTrashed()->find($productId)->restore();
return ['success' => true];
}
}

View File

@ -2,7 +2,6 @@
namespace App\Providers;
use App\Models\Setting;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
@ -24,15 +23,6 @@ class AppServiceProvider extends ServiceProvider
*/
public function boot()
{
$settingsFromDB = Setting::all(['name', 'value', 'json'])
->keyBy('name')
->transform(function ($setting) {
if ($setting->json) {
return \json_decode($setting->value, true);
}
return $setting->value;
})
->toArray();
config(['global' => $settingsFromDB]);
}
}

View File

@ -43,15 +43,9 @@ class RouteServiceProvider extends ServiceProvider
->namespace($this->namespace)
->group(base_path('routes/api.php'));
// Shop
Route::middleware('web')
->namespace($this->namespace)
->group(base_path('routes/web_shop.php'));
// Admin
Route::middleware('web')
->namespace($this->namespace)
->group(base_path('routes/web_admin.php'));
->group(base_path('routes/web.php'));
});
}

View File

@ -0,0 +1,70 @@
<?php
namespace Beike\Admin\Http\Controllers;
use Beike\Admin\Http\Requests\CategoryRequest;
use Beike\Admin\Http\Resources\CategoryResource;
use Beike\Admin\Repositories\CategoryRepo;
use Beike\Models\Category;
use Beike\Admin\Services\CategoryService;
use Illuminate\Http\Request;
class CategoryController extends Controller
{
protected string $defaultRoute = 'categories.index';
public function index()
{
$categories = Category::with('description', 'children.description', 'children.children.description')
->where('parent_id', 0)
->get();
$data = [
'categories' => CategoryResource::collection($categories),
];
return view('admin::pages.categories.index', $data);
}
public function create(Request $request)
{
return $this->form($request);
}
public function store(CategoryRequest $request)
{
return $this->save($request);
}
public function edit(Request $request, Category $category)
{
return $this->form($request, $category);
}
public function update(CategoryRequest $request, Category $category)
{
return $this->save($request, $category);
}
protected function form(Request $request, Category $category = null)
{
if ($category) {
$descriptions = $category->descriptions->keyBy('locale');
}
$data = [
'category' => $category ?? new Category(),
'descriptions' => $descriptions ?? null,
'categories' => CategoryRepo::flatten(locale()),
'_redirect' => $this->getRedirect(),
];
return view('admin::pages.categories.form', $data);
}
protected function save(Request $request, ?Category $category = null)
{
(new CategoryService())->createOrUpdate($request->all(), $category);
return redirect($this->getRedirect())->with('success', 'Category created successfully');
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace Beike\Admin\Http\Controllers;
use App\Http\Controllers\Controller as BaseController;
abstract class Controller extends BaseController
{
protected string $defaultRoute;
/**
* 表单页面获跳转页面链接
* @return array|\Illuminate\Contracts\Foundation\Application|\Illuminate\Http\Request|string|null
*/
public function getRedirect()
{
return request('_redirect') ?? request()->header('referer', admin_route($this->defaultRoute));
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace Beike\Admin\Http\Controllers;
use Illuminate\Http\Request;
class FileController extends Controller
{
public function upload(Request $request)
{
$path = $request->file('file')->store('avatars');
}
}

View File

@ -1,6 +1,6 @@
<?php
namespace App\Http\Controllers\Admin;
namespace Beike\Admin\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
@ -9,6 +9,6 @@ class HomeController extends Controller
{
public function index()
{
return view('admin.pages.home');
return view('admin::pages.home');
}
}

View File

@ -1,9 +1,9 @@
<?php
namespace App\Http\Controllers\Admin;
namespace Beike\Admin\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Models\AdminUser;
use Beike\Models\AdminUser;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
@ -11,7 +11,7 @@ class LoginController extends Controller
{
public function show()
{
return view('admin.pages.login.login');
return view('admin::pages.login.login');
}
public function store(Request $request)

View File

@ -1,9 +1,9 @@
<?php
namespace App\Http\Controllers\Admin;
namespace Beike\Admin\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Models\AdminUser;
use Beike\Models\AdminUser;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

View File

@ -0,0 +1,124 @@
<?php
namespace Beike\Admin\Http\Controllers;
use Beike\Admin\Http\Resources\ProductResource;
use Beike\Admin\Repositories\CategoryRepo;
use Beike\Models\Product;
use Beike\Models\ProductDescription;
use Beike\Models\ProductSku;
use Beike\Admin\Services\ProductService;
use Illuminate\Http\Request;
class ProductController extends Controller
{
protected string $defaultRoute = 'products.index';
public function index(Request $request)
{
if ($request->expectsJson()) {
$query = Product::query()
->select('products.*')
->with('description')
->withCount('skus');
if ($request->sku) {
$query->whereHas('skus', function ($q) {
$q->where('sku', 'like', '%'.request('sku').'%');
});
}
// 关键字搜索:名称
if ($request->keyword) {
$query->whereHas('description', function ($q) {
$q->where('name', 'like', '%'.request('keyword').'%');
});
}
$query->when($request->active !== null, function ($q) {
$q->where('active', (int)request('active'));
});
// 回收站
if ($request->trashed) {
$query->onlyTrashed();
}
// 排序
$orderBy = $request->orderBy ?? 'products.id:desc';
$orderBy = explode(':', $orderBy);
$query->orderBy($orderBy[0], $orderBy[1] ?? 'desc');
$products = $query->paginate($request->per_page ?? 10);
return ProductResource::collection($products);
}
$data = [
'categories' => CategoryRepo::flatten(locale()),
];
return view('admin::pages.products.index', $data);
}
public function create(Request $request)
{
return $this->form($request, new Product());
}
public function store(Request $request)
{
return $this->save($request, new Product());
}
public function edit(Request $request, Product $product)
{
return $this->form($request, $product);
}
public function update(Request $request, Product $product)
{
return $this->save($request, $product);
}
public function destroy(Request $request, Product $product)
{
$product->delete();
return ['success' => true];
}
public function restore(Request $request)
{
$productId = $request->id ?? 0;
Product::withTrashed()->find($productId)->restore();
return ['success' => true];
}
protected function form(Request $request, Product $product)
{
if ($product->id) {
$descriptions = $product->descriptions->keyBy('locale');
}
$data = [
'product' => $product,
'descriptions' => $descriptions ?? [],
'categories' => CategoryRepo::flatten(locale()),
'_redirect' => $this->getRedirect(),
];
return view('admin::pages.products.form.form', $data);
}
protected function save(Request $request, Product $product)
{
if ($product->id) {
$product = (new ProductService)->update($product, $request->all());
} else {
$product = (new ProductService)->create($request->all());
}
return redirect($this->getRedirect())->with('success', 'product created');
}
}

View File

@ -1,6 +1,6 @@
<?php
namespace App\Http\Requests\Admin;
namespace Beike\Admin\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;

View File

@ -1,6 +1,6 @@
<?php
namespace App\Http\Resources\Admin;
namespace Beike\Admin\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;

View File

@ -1,6 +1,6 @@
<?php
namespace App\Http\Resources\Admin;
namespace Beike\Admin\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;

View File

@ -0,0 +1,81 @@
<?php
namespace Beike\Admin\Providers;
use Beike\Console\Commands\MakeRootAdminUser;
use Beike\Models\AdminUser;
use Beike\Models\Setting;
use Beike\Admin\View\Components\Filter;
use Beike\Admin\View\Components\Header;
use Beike\Admin\View\Components\Sidebar;
use Beike\Admin\View\Components\Form\Input;
use Beike\Admin\View\Components\Form\InputLocale;
use Beike\Admin\View\Components\Form\SwitchRadio;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Str;
class AdminServiceProvider extends ServiceProvider
{
public function boot()
{
$uri = request()->getRequestUri();
if (! Str::startsWith($uri, '/admin')) {
return;
}
// $this->loadRoutesFrom(__DIR__ . '/../Routes/shop.php');
$this->loadRoutesFrom(__DIR__ . '/../Routes/admin.php');
$this->mergeConfigFrom(__DIR__ . '/../../Config/beike.php', 'beike');
$this->loadViewsFrom(__DIR__ . '/../Resources/views', 'admin');
$this->loadViewComponentsAs('admin', [
'header' => Header::class,
'sidebar' => Sidebar::class,
'filter' => Filter::class,
'form-input-locale' => InputLocale::class,
'form-switch' => SwitchRadio::class,
'form-input' => Input::class,
]);
$this->loadSettings();
$this->registerGuard();
if ($this->app->runningInConsole()) {
$this->commands([
MakeRootAdminUser::class,
]);
}
}
protected function loadSettings()
{
$settings = Setting::all(['name', 'value', 'json'])
->keyBy('name')
->transform(function ($setting) {
if ($setting->json) {
return \json_decode($setting->value, true);
}
return $setting->value;
})
->toArray();
config(['global' => $settings]);
}
protected function registerGuard()
{
Config::set('auth.guards.'.AdminUser::AUTH_GUARD, [
'driver' => 'session',
'provider' => 'admin_users',
]);
Config::set('auth.providers.admin_users', [
'driver' => 'eloquent',
'model' => AdminUser::class,
]);
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace Beike\Admin\Repositories;
use Illuminate\Support\Facades\DB;
class CategoryRepo
{
public static function flatten(string $locale, $separator = ' > '): array
{
$sql = "SELECT cp.category_id AS id, TRIM(LOWER(GROUP_CONCAT(cd1.name ORDER BY cp.level SEPARATOR '{$separator}'))) AS name, c1.parent_id, c1.position";
$sql .= " FROM category_paths cp";
$sql .= " LEFT JOIN categories c1 ON (cp.category_id = c1.id)";
$sql .= " LEFT JOIN categories c2 ON (cp.path_id = c2.id)";
$sql .= " LEFT JOIN category_descriptions cd1 ON (cp.path_id = cd1.category_id)";
$sql .= " WHERE cd1.locale = '" . $locale . "' GROUP BY cp.category_id ORDER BY name ASC";
$results = DB::select($sql);
return $results;
}
}

View File

@ -0,0 +1,154 @@
@charset "UTF-8";
//
// @copyright 2020 opencart.cn - All Rights Reserved
// @link http://www.guangdawangluo.com
// @author Sam Chen <sam.chen@opencart.cn>
// @created 2021-08-24 14:38:09
// @modified 2021-10-13 10:38:17
//
hr.horizontal {
background-color: transparent;
height: 1px;
margin: 1rem 0;
color: inherit;
border: 0;
opacity: .25;
}
hr.horizontal.dark {
background-image: linear-gradient(90deg,transparent,rgba(0,0,0,.4),transparent);
}
.card {
// box-shadow: 0 20px 27px 0 rgba(0, 0, 0, .05);
box-shadow: 0 0.75rem 1.5rem rgb(18 38 63 / 3%);
// border: 0 solid rgba(0,0,0,.125);
border: 1px solid #edf2f9;
// border-radius: 0.5rem;
.card-header {
padding: 1rem 1rem .3rem;
background-color: #fff;
font-weight: bold;
font-size: .8rem;
border-bottom: 0 solid rgba(0,0,0,.125);
&:first-child {
border-radius: 1rem 1rem 0 0;
}
}
.card-body {
padding: 1rem;
}
}
table.table {
thead {
th {
background-color: #f9fbfd;
color: #74859e;
font-size: .825rem;
border-top-width: 0;
border-bottom: none;
white-space:nowrap;
}
}
td {
border-bottom: 0;
font-size: .8125rem;
vertical-align: middle;
border-top: 1px solid #edf2f9;
}
&.table-striped>tbody>tr {
&:nth-of-type(odd) {
background: transparent;
}
&:nth-of-type(2n) {
background: #f9fbfd;
}
}
}
.form-group {
margin-bottom: 1.375rem;
}
.btn {
font-size: 0.8rem;
}
.btn-check {
position: absolute;
clip: rect(0,0,0,0);
pointer-events: none;
}
.btn-group-radio {
.btn-group {
label {
&:first-of-type {
border-top-left-radius: .25rem;
border-bottom-left-radius: .25rem;
}
}
}
.btn-check:active+.btn-outline-primary, .btn-check:checked+.btn-outline-primary, .btn-outline-primary.active, .btn-outline-primary.dropdown-toggle.show, .btn-outline-primary:active {
color: #fff;
background-color: #0d6efd;
border-color: #0d6efd;
}
&.btn-group-radio-pay {
.btn-check:active+.btn, .btn-check:checked+.btn, .btn.active, .btn.dropdown-toggle.show, .btn:active {
color: #fff;
background-color: transparent;
padding: .32rem .70rem;
border: 2px solid #0d6efd;
// border-color: #0d6efd;
}
.btn-group-radios {
display: flex;
}
label.btn {
height: 56px;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid #ddd;
&:hover {
border-color: #0d6efd;
}
}
img {
max-width: 140px;
height: 42px;
}
}
}
.bd-callout {
padding: 1.25rem;
margin-top: 1.25rem;
margin-bottom: 1.25rem;
border: 1px solid #eee;
border-left-width: .25rem;
border-radius: .25rem;
&.bd-callout-info {
border-left-color: #5bc0de;
}
p {
margin-bottom: 0;
}
}

View File

@ -0,0 +1,10 @@
@charset "UTF-8";
//
// @copyright 2020 opencart.cn - All Rights Reserved
// @link http://www.guangdawangluo.com
// @author Sam Chen <sam.chen@opencart.cn>
// @created 2021-08-24 14:38:09
// @modified 2021-08-24 15:07:05
//

View File

@ -0,0 +1,27 @@
@charset "UTF-8";
//
// @copyright 2020 opencart.cn - All Rights Reserved
// @link http://www.guangdawangluo.com
// @author Sam Chen <sam.chen@opencart.cn>
// @created 2022-05-09 11:35:44
// @modified 2022-05-09 13:48:22
//
.form-group {
.form-control {
&.short {
max-width: 300px;
}
}
.input-group {
&.short {
max-width: 300px;
}
}
.col-form-label {
text-align: right;
}
}

View File

@ -0,0 +1,170 @@
@charset "UTF-8";
//
// @copyright 2020 opencart.cn - All Rights Reserved
// @link http://www.guangdawangluo.com
// @author Sam Chen <sam.chen@opencart.cn>
// @created 2021-08-24 14:38:09
// @modified 2021-08-24 15:07:05
//
body {
// min-height: 100vh;
// background-color: #f9f9f9;
font-weight: 400;
line-height: 1.6;
font-size: 0.875rem;
background-color: #f9fbfd;
}
@for $i from 1 through 6 {
.min-h#{$i} {
min-height: #{$i}00px;
}
}
.main-content {
display: flex;
// flex-direction: column;
// min-height: 100vh;
transition: margin-left .25s ease-in-out,left .25s ease-in-out,margin-right .25s ease-in-out,right .25s ease-in-out;
width: 100%;
height: calc(100vh - 60px);
overflow: hidden;
@media screen and (max-width: 991px) {
margin-left: 260px;
}
&:not(.active) {
margin-left: 0;
}
> #content {
flex: 1;
padding: 1rem;
overflow-y: auto;
@media screen and (max-width: 991px) {
padding: 0 1rem 1.5rem;
}
}
}
.page-title-box {
.page-title {
font-size: 15px;
margin: 0;
// line-height: 35px;
margin-bottom: 20px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
color: inherit;
}
}
.switch-plus {
position: relative;
width: 50px;
height: 24px;
margin-bottom: 1rem;
input {
position: absolute;
top: 0;
z-index: 2;
opacity: 0;
cursor: pointer;
height: 20px;
width: 50px;
left: 0;
margin: 0;
&:checked {
z-index: 1;
& + label {
opacity: 1;
cursor: default;
&:hover {
opacity: 0.5;
}
}
~ .toggle-outside .toggle-inside {
left: 0.25rem;
background-color: #fff;
box-shadow: 0 3px 6px 0 rgba(140,152,164,.25);
}
}
& ~ input:checked ~ .toggle-outside {
background-color: $main_color;
.toggle-inside {
left: 23px;
background-color: #fff;
}
}
}
label {
color: #fff;
opacity: 0.33;
transition: opacity 0.25s ease;
cursor: pointer;
font-size: 1.5rem;
line-height: 3rem;
display: inline-block;
width: 50px;
height: 100%;
margin: 0;
text-align: center;
&:last-of-type {
margin-left: 50px;
}
}
.toggle-outside {
height: 100%;
border-radius: 2rem;
padding: 2px;
overflow: hidden;
transition: 0.25s ease all;
// background: #f1f3fa;
background-color: #e7eaf3;
position: absolute;
width: 46px;
left: 0;
}
.toggle-inside {
border-radius: 50%;
// background: #4a4a4a;
position: absolute;
transition: 0.25s ease all;
height: 20px;
width: 20px;
}
}
body.page-seller-product {
.share-link-pop {
.share-links-td {
padding-top: 4px;
padding-bottom: 4px;
}
.share-links-code {
width: 80px;
height: 80px;
overflow: hidden;
img {
max-width: 100%;
}
}
}
}

View File

@ -0,0 +1,75 @@
@charset "UTF-8";
//
// @copyright 2020 opencart.cn - All Rights Reserved
// @link http://www.guangdawangluo.com
// @author Sam Chen <sam.chen@opencart.cn>
// @created 2021-08-24 14:38:09
// @modified 2021-08-24 15:07:05
//
.header-wrap {
background-color: #fff;
height: 60px;
border-bottom: 1px solid #f1f1f1;
display: flex;
align-items: center;
.header-left {
width: 190px;
}
.header-right {
display: flex;
align-items: center;
justify-content: space-between;
flex: 1;
.navbar {
margin-bottom: 0;
list-style: none;
li {
padding: 0.5rem 1.3rem;
a {
color: #333;
padding: 0.5rem 0;
}
}
}
.navbar.navbar-right {
li {
a {
position: relative;
&:after {
content: '';
position: absolute;
left: 0;
display: none;
bottom: 0;
width: 100%;
height: 3px;
background-color: $main_color;
}
}
&.active, &:hover {
a {
&:after {
display: block;
}
}
}
}
}
}
.avatar {
height: 36px;
margin-bottom: -15px;
margin-top: -15px;
width: 36px;
}
}

View File

@ -0,0 +1,27 @@
@charset "UTF-8";
//
// @copyright 2020 opencart.cn - All Rights Reserved
// @link http://www.guangdawangluo.com
// @author Sam Chen <sam.chen@opencart.cn>
// @created 2021-08-24 14:38:09
// @modified 2021-08-24 15:07:05
//
@font-face {
font-family: 'iconfont'; /* Project id 2787822 */
src: url('//at.alicdn.com/t/font_2787822_7mtbg56vojp.woff2?t=1634612961708') format('woff2'),
url('//at.alicdn.com/t/font_2787822_7mtbg56vojp.woff?t=1634612961708') format('woff'),
url('//at.alicdn.com/t/font_2787822_7mtbg56vojp.ttf?t=1634612961708') format('truetype');
}
.iconfont {
font-family:"iconfont" !important;
font-size:16px;
font-style:normal;
-webkit-font-smoothing: antialiased;
// -webkit-text-stroke-width: 0.2px;
-webkit-text-stroke-width: 0;
-moz-osx-font-smoothing: grayscale;
}

View File

@ -0,0 +1,61 @@
@charset "UTF-8";
//
// @copyright 2020 opencart.cn - All Rights Reserved
// @link http://www.guangdawangluo.com
// @author Sam Chen <sam.chen@opencart.cn>
// @created 2021-08-24 14:38:09
// @modified 2021-08-24 15:07:05
//
.sidebar {
background: #293042;
direction: ltr;
width: 190px;
transition: all .2s ease-in-out;
background: #fff;
border-right: 1px solid #f1f1f1;
@media screen and (max-width: 991px) {
position: fixed;
top: 0;
bottom: 0;
}
.navbar-nav {
> li.nav-item {
position: relative;
a {
padding: .6rem 1rem .6rem;
// color: rgba(58,65,111, .9);
color: #333;
transition: all .1s ease-in-out;
i {
margin-right: 7px;
}
}
&.active, &:hover {
a {
color: $main_color;
background-color: #f4f4f4;
}
}
&.active {
&:before {
content: '';
position: absolute;
left: 0;
top: 0;
width: 2px;
height: 100%;
z-index: 1;
background: $main_color;
}
}
}
}
}

View File

@ -0,0 +1,9 @@
@charset "UTF-8";
//
// @copyright 2020 opencart.cn - All Rights Reserved
// @link http://www.guangdawangluo.com
// @author Sam Chen <sam.chen@opencart.cn>
// @created 2021-08-24 14:38:09
// @modified 2021-08-24 15:07:05
//

View File

@ -0,0 +1,13 @@
@charset "UTF-8";
//
// @copyright 2020 opencart.cn - All Rights Reserved
// @link http://www.guangdawangluo.com
// @author Sam Chen <sam.chen@opencart.cn>
// @created 2021-10-25 11:40:51
// @modified 2021-10-25 11:41:02
//
[v-cloak] {
display: none;
}

View File

@ -0,0 +1,21 @@
@charset "UTF-8";
//
// @copyright 2020 opencart.cn - All Rights Reserved
// @link http://www.guangdawangluo.com
// @author Sam Chen <sam.chen@opencart.cn>
// @created 2021-08-24 14:38:09
// @modified 2022-05-09 11:36:06
//
$main_color: #fd560f;
@import 'global';
@import 'vue';
@import 'sidebar';
@import 'header';
@import 'bootstrap-extra';
@import 'iconfont';
@import 'dashboard';
@import 'subscription';
@import 'form';

View File

@ -0,0 +1,120 @@
@charset "UTF-8";
//
// @copyright 2020 opencart.cn - All Rights Reserved
// @link http://www.guangdawangluo.com
// @author Sam Chen <sam.chen@opencart.cn>
// @created 2021-08-24 14:38:09
// @modified 2021-08-24 15:07:05
//
$main_color: #fd560f;
.form-text.text-danger {
font-size: 0.8rem;
}
body {
background-image: url("/images/login-bg.svg");
background-size: cover;
background-position: 50%;
&.seller-register, &.password-reset {
.form-group {
margin-bottom: 1.5rem;
label {
display: none;
}
}
}
}
.card {
box-shadow: 0 20px 27px 0 rgba(0, 0, 0, .05);
border: none;
border-radius: .85rem;
overflow: hidden;
}
.card-header {
border-bottom: none;
background: #fff;
font-size: 17px;
padding-top: 40px;
h5 {
color: #344767;
font-weight: 400;
margin-bottom: 0;
}
}
button[type="submit"] {
background-image: linear-gradient(310deg, $main_color, #fdb504);
// background-image: linear-gradient(310deg, $main_color, #fdb504);
border: none;
}
.form-group {
margin-bottom: 2rem;
input.form-control {
border: 1px solid #d2d6da;
border-radius: .5rem;
font-size: .875rem;
font-weight: 400;
color: #495057;
line-height: 1.4rem;
padding: .5rem .75rem;
height: 40px;
}
}
.partition {
font-size: 12px;
&:after, &:before {
content: "";
display: inline-block;
width: 30%;
height: 1px;
position: relative;
vertical-align: middle;
}
&:after {
left: .5em;
margin-right: -50%;
background: linear-gradient(90deg,rgba(117, 117, 117, .40),rgb(117, 117, 117, .40), transparent);
}
&:before {
background: linear-gradient(90deg,transparent,rgba(117, 117, 117, .40),rgb(117, 117, 117, .40));
right: .5em;
margin-left: -50%;
}
}
.btn:not(.btn-link) {
padding: .50rem 1.5rem;
border-radius: .5rem;
transition: all .15s ease-in;
box-shadow: 0 4px 7px -1px rgba(0, 0, 0, .11), 0 2px 4px -1px rgba(0, 0, 0, .07);
&:hover {
transform: scale(1.02);
color: #fff;
}
}
.bg-gradient-dark {
background-image: linear-gradient(310deg,#141727,#3a416f);
background-color: #344767;
border: none;
color: #fff;
}

4
beike/Admin/Resources/css/app.scss vendored Normal file
View File

@ -0,0 +1,4 @@
body {
background: #f2f2f2;
// color: #fff;
}

View File

@ -0,0 +1,13 @@
@charset "UTF-8";
//
// @copyright 2020 opencart.cn - All Rights Reserved
// @link http://www.guangdawangluo.com
// @author Sam Chen <sam.chen@opencart.cn>
// @created 2021-08-24 14:38:09
// @modified 2021-08-24 15:07:05
//
$primary: #fd560f;
@import '../../../../node_modules/bootstrap/scss/bootstrap';

View File

@ -0,0 +1,21 @@
<div class="form-group">
<div class="row">
<label for="" class="col-sm-2 col-form-label">{{ $title }}</label>
<div class="col-sm-10">
@foreach (locales() as $index => $locale)
<div class="input-group input-group-sm short mb-1">
<input type="text" class="form-control" name="{{ $formatName($locale['code']) }}" placeholder="{{ $locale['name'] }}" value="{{ $formatValue($locale['code']) }}">
<div class="input-group-append">
<span class="input-group-text" id="input-{{ $name }}-{{ $locale['code'] }}">{{ $locale['name'] }}</span>
</div>
</div>
@if ($attributes->has('required'))
@error($errorKey($locale['code']))
<x-admin::form.error :message="$message" />
@enderror
@endif
@endforeach
</div>
</div>
</div>

View File

@ -0,0 +1,3 @@
<x-admin::form.row :title="$title">
<input type="text" name="{{ $name }}" class="form-control form-control-sm short" value="{{ $value }}" placeholder="{{ $title }}">
</x-admin::form.row>

View File

@ -0,0 +1,8 @@
<div class="form-group">
<div class="row">
<label for="" class="col-sm-2 col-form-label">{{ $title }}</label>
<div class="col-sm-10">
{{ $slot }}
</div>
</div>
</div>

View File

@ -0,0 +1,15 @@
<div class="form-group">
<div class="row">
<label for="" class="col-sm-2 col-form-label">{{ $title }}</label>
<div class="col-sm-10">
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="{{ $name }}" id="{{ $name }}-1" value="1" {{ $value ? 'checked' : '' }}>
<label class="form-check-label" for="{{ $name }}-1">启用</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="{{ $name }}" id="{{ $name }}-0" value="0" {{ !$value ? 'checked' : '' }}>
<label class="form-check-label" for="{{ $name }}-0">禁用</label>
</div>
</div>
</div>
</div>

View File

@ -10,14 +10,14 @@
<script src="{{ asset('vendor/axios/0.21.1/axios.min.js') }}"></script>
<script src="{{ asset('vendor/layer/3.5.1/layer.js') }}"></script>
<script src="{{ mix('build/js/app.js') }}"></script> --}}
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/element-ui/2.15.8/index.min.js"></script>
<link href="{{ mix('build/css/bootstrap.css') }}" rel="stylesheet">
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
<link href="{{ mix('beike/build/css/bootstrap.css') }}" rel="stylesheet">
<link href="https://cdn.bootcdn.net/ajax/libs/element-ui/2.15.8/theme-chalk/index.min.css" rel="stylesheet">
@if (0)
<link rel="stylesheet" href="{{ asset('vendor/element-ui/2.15.6/css.css') }}">
@endif
<link href="{{ mix('build/css/admin/app.css') }}" rel="stylesheet">
<link href="{{ mix('beike/build/css/admin/app.css') }}" rel="stylesheet">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>beike admin</title>
@stack('header')
@ -26,11 +26,11 @@
<body class="@yield('body-class')">
<!-- <div style="height: 80px; background: white;"></div> -->
<x-admin.header />
<x-admin-header />
<div class="main-content">
<aside class="sidebar navbar-expand-xs border-radius-xl">
<x-admin.sidebar />
<x-admin-sidebar />
</aside>
<div id="content">
<div class="container-fluid p-0">

View File

@ -1,35 +1,45 @@
@extends('admin.layouts.master')
@extends('admin::layouts.master')
@section('title', '分类管理')
@section('content')
<div id="category-app" class="card">
<div class="card-header">
所有分类
编辑分类
</div>
<div class="card-body">
<form action="{{ admin_route($category->id ? 'categories.update' : 'categories.store', $category) }}" method="POST">
@csrf
@method($category->id ? 'PUT' : 'POST')
<input type="hidden" name="_redirect" value="{{ request()->header('referer') ?? admin_route('categories.index') }}">
<input type="hidden" name="_redirect" value="{{ $_redirect }}">
@foreach (locales() as $index => $locale)
<input type="hidden" name="descriptions[{{ $index }}][locale]" value="{{ $locale['code'] }}">
{{-- <input type="hidden" name="descriptions[{{ $index }}][locale]" value="{{ $locale['code'] }}"> --}}
@endforeach
@foreach (locales() as $index => $locale)
<input type="text" name="descriptions[{{ $index }}][name]" placeholder="Name {{ $locale['name'] }}" value="{{ old('descriptions.'.$index.'.name', $descriptions[$locale['code']]->name ?? '') }}">
@error('descriptions.'.$index.'.name')
<x-form.error :message="$message" />
@enderror
<input type="text" name="descriptions[{{ $index }}][content]" placeholder="content {{ $locale['name'] }}" value="{{ old('descriptions.'.$index.'.content', $descriptions[$locale['code']]->content ?? '') }}">
<hr>
@endforeach
<x-admin-form-input-locale name="descriptions.*.name" title="名称" :value="$descriptions" required />
<x-admin-form-input-locale name="descriptions.*.content" title="内容" :value="$descriptions" />
<input type="text" name="parent_id" value="{{ old('parent_id', $category->parent_id ?? 0) }}" placeholder="上级分类">
<input type="text" name="active" value="{{ old('active', $category->active ?? 1) }}" placeholder="状态">
<x-admin::form.row title="上级分类">
@php
$_parent_id = old('parent_id', $category->parent_id ?? 0);
@endphp
<select name="parent_id" id="" class="form-control form-control-sm short">
<option value="0">--请选择--</option>
@foreach ($categories as $_category)
<option value="{{ $_category->id }}" {{ $_parent_id == $_category->id ? 'selected' : ''}}>
{{ $_category->name }}
</option>
@endforeach
</select>
</x-admin::form.row>
<button type="submit" class="btn btn-primary">保存</button>
<x-admin-form-switch title="状态" name="active" :value="old('active', $category->active ?? 1)" />
<div>
<button type="submit" class="btn btn-primary">保存</button>
<a href="{{ $_redirect }}" class="btn btn-danger">返回</a>
</div>
</form>
</div>

View File

@ -1,4 +1,4 @@
@extends('admin.layouts.master')
@extends('admin::layouts.master')
@section('title', '分类管理')

View File

@ -1,4 +1,4 @@
@extends('admin.layouts.master')
@extends('admin::layouts.master')
@section('title', '后台管理')
@ -12,4 +12,4 @@
</div>
</div>
@endfor
@endsection
@endsection

View File

@ -17,7 +17,7 @@
<input type="text" name="email" class="form-control" value="{{ old('email') }}" placeholder="邮箱地址">
</div>
@error('email')
<x-form.error :message="$message" />
<x-admin::form.error :message="$message" />
@enderror
</div>
@ -29,7 +29,7 @@
<input type="password" name="password" class="form-control" placeholder="密码">
</div>
@error('password')
<x-form.error :message="$message" />
<x-admin::form.error :message="$message" />
@enderror
</div>

View File

@ -0,0 +1,269 @@
@extends('admin::layouts.master')
@push('header')
<script src="https://cdn.bootcdn.net/ajax/libs/vue/2.6.14/vue.js"></script>
@endpush
@section('content')
<div class="card">
<div class="card-body">
<h2>product</h2>
<form action="{{ $product->id ? route('admin.products.update', $product) : route('admin.products.store') }}"
method="POST" id="app">
@csrf
@method($product->id ? 'PUT' : 'POST')
<input type="hidden" name="_redirect" value="{{ $_redirect }}"/>
@foreach (locales() as $index => $locale)
{{-- <input type="hidden" name="descriptions[{{ $index }}][locale]" value="{{ $locale['code'] }}"> --}}
@endforeach
<x-admin-form-input-locale name="descriptions.*.name" title="名称" :value="$descriptions" required/>
<x-admin-form-input name="image" title="主图" :value="old('image', $product->image ?? '')"/>
<x-admin-form-input name="video" title="视频" :value="old('video', $product->video ?? '')"/>
<x-admin-form-input name="position" title="排序" :value="old('position', $product->position ?? '')"/>
<x-admin-form-switch name="active" title="状态" :value="old('active', $product->active ?? 1)"/>
<x-admin::form.row title="分类">
<select name="category_id" id="" class="form-control form-control-sm short">
@foreach ($categories as $category)
<option value="{{ $category->id }}">{{ $category->name }}</option>
@endforeach
</select>
</x-admin::form.row>
<div>
<h2>skus</h2>
<input type="radio" v-model="editing.isVariable" :value="false"> 单规格
<input type="radio" v-model="editing.isVariable" :value="true"> 多规格
<div v-if="editing.isVariable">
<div>
<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 v-if="form.skus.length">
<input v-if="form.skus.length" type="hidden" name="variables"
:value="JSON.stringify(form.variables)">
<table>
<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 + '][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"
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>
<button type="submit" class="btn btn-primary">Save</button>
</form>
</div>
</div>
@endsection
@push('footer')
<script>
var app = new Vue({
el: '#app',
data: {
form: {
variables: @json($product->variables_decoded ?? []),
skus: @json($product->skus ?? []),
},
source: {
variables: @json($product->variables_decoded ?? []),
},
editing: {
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

@ -0,0 +1,192 @@
@extends('admin::layouts.master')
@section('title', '商品管理')
@push('header')
<script src="https://cdn.bootcdn.net/ajax/libs/axios/0.27.2/axios.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/underscore.js/1.13.3/underscore.min.js"></script>
@endpush
@section('content')
<div id="product-app">
<div class="card">
<div class="card-body">
<div class="form-inline">
<input type="text" v-model="filter.keyword" class="form-control mr-2" placeholder="keyword">
<input type="text" v-model="filter.sku" class="form-control mr-2" placeholder="sku">
<select v-model="filter.category_id" class="form-control">
<option value="0">全部</option>
@foreach ($categories as $_category)
<option :value="{{ $_category->id }}">{{ $_category->name }}</option>
@endforeach
</select>
<select v-model="filter.active" class="form-control">
<option value="">全部</option>
<option value="1">上架</option>
<option value="0">下架</option>
</select>
<button type="button" @click="search" class="btn btn-primary">筛选</button>
</div>
</div>
</div>
<div class="card mt-4">
<div class="card-header">
<a href="{{ route('admin.products.create') }}" class="btn btn-primary">Create</a>
</div>
<div class="card-body">
<template v-if="items.length">
<table class="table" v-loading="loading">
<thead>
<tr>
<th></th>
<th>ID</th>
<th>图片</th>
<th>商品名称</th>
<th>价格</th>
<th>创建时间</th>
<th>上架</th>
<th>操作</th>
</tr>
</thead>
<tr v-for="(item, index) in items" :key="item.id">
<td>
<input type="checkbox" :value="item.id" v-model="selected" />
</td>
<td>@{{ item.id }}</td>
<td><img :src="item.image" alt="" srcset=""></td>
<td>@{{ item.name || '无名称' }}</td>
<td>@{{ item.price_formatted }}</td>
<td>@{{ item.created_at }}</td>
<td>@{{ item.active ? '上架' : '下架' }}</td>
<td>
<a :href="item.url_edit">编辑</a>
<template>
<a v-if="item.deleted_at == ''" href="javascript:void(0)" @click.prevent="deleteProduct(index)">删除</a>
<a v-else href="javascript:void(0)" @click.prevent="restoreProduct(index)">恢复</a>
</template>
</td>
</tr>
</table>
<el-pagination
layout="prev, pager, next"
background
:page-size="perPage"
:current-page.sync="page"
:total="totals"
></el-pagination>
</template>
<p v-else>无商品</p>
</div>
</div>
</div>
@endsection
@push('footer')
<script>
new Vue({
el: '#product-app',
data: {
filter: {
keyword: @json(request('keyword') ?? ''),
category_id: @json(request('category_id') ?? ''),
sku: @json(request('sku') ?? ''),
active: @json(request('active') ?? ''),
},
items: [],
selected: [],
page: @json((int)request('page') ?? 1),
totals: 0,
perPage: @json((int)(request('per_page') ?? 1)),
loading: false,
orderBy: @json(request('order_by', 'products.id:desc')),
},
mounted: function () {
this.load();
},
computed: {
url: function () {
let filter = {};
filter.per_page = this.perPage;
if (this.orderBy != 'products.id:desc') {
filter.order_by = this.orderBy;
}
if (this.page > 1) {
filter.page = this.page;
}
for (key in this.filter) {
const value = this.filter[key];
if (value !== '' && value !== null) {
filter[key] = value;
}
}
const query = Object.keys(filter).map(key => key + '=' + filter[key]).join('&');
const url = @json(admin_route('products.index'));
if (query) {
return url + '?' + query;
}
return url;
}
},
watch: {
page: function () {
this.load();
}
},
methods: {
load: function () {
const url = this.url;
window.history.pushState('', '', url);
this.loading = true;
axios.get(url).then(response => {
this.loading = false;
this.items = response.data.data;
this.totals = response.data.meta.total;
}).catch(error => {
// this.$message.error(error.response.data.message);
});
},
search: function () {
this.page = 1;
this.load();
},
deleteProduct: function (index) {
const product = this.items[index];
this.$confirm('确认要删除选中的商品吗?', '删除商品', {
// confirmButtonText: '确定',
// cancelButtonText: '取消',
type: 'warning'
}).then(() => {
axios.delete('products/' + product.id).then(response => {
location.reload();
}).catch(error => {
// this.$message.error(error.response.data.message);
});
});
},
restoreProduct: function (index) {
const product = this.items[index];
this.$confirm('确认要恢复选中的商品吗?', '恢复商品', {
type: 'warning'
}).then(() => {
axios.put('products/restore', {
id: product.id
}).then(response => {
location.reload();
}).catch(error => {
// this.$message.error(error.response.data.message);
});
});
}
}
});
</script>
@endpush

View File

@ -0,0 +1,25 @@
<?php
use Illuminate\Support\Facades\Route;
Route::prefix('admin')
->middleware(['web'])
->name('admin.')
->group(function () {
Route::get('login', [\Beike\Admin\Http\Controllers\LoginController::class, 'show'])->name('login.show');
Route::post('login', [\Beike\Admin\Http\Controllers\LoginController::class, 'store'])->name('login.store');
Route::middleware('auth:'.\Beike\Models\AdminUser::AUTH_GUARD)
->group(function () {
Route::get('/', [\Beike\Admin\Http\Controllers\HomeController::class, 'index'])->name('home.index');
Route::Resource('categories', \Beike\Admin\Http\Controllers\CategoryController::class);
Route::put('products/restore', [\Beike\Admin\Http\Controllers\ProductController::class, 'restore']);
Route::resource('products', \Beike\Admin\Http\Controllers\ProductController::class);
Route::get('logout', [\Beike\Admin\Http\Controllers\LogoutController::class, 'index'])->name('logout.index');
});
});

View File

@ -9,58 +9,31 @@
* @modified 2022-05-07 15:15:25
*/
namespace App\Services;
namespace Beike\Admin\Services;
use App\Models\Category;
use App\Models\CategoryPath;
use Beike\Models\Category;
use Beike\Models\CategoryPath;
use Illuminate\Support\Facades\DB;
class CategoryService
{
public function create(array $data)
public function createOrUpdate(array $data, ?Category $category)
{
try {
DB::beginTransaction();
$category = new \App\Models\Category();
$category->fill($data);
$category->saveOrFail();
$descriptions = [];
foreach ($data['descriptions'] as $description) {
$descriptions[] = [
'locale' => $description['locale'],
'name' => $description['name'],
'content' => $description['content'] ?? '',
'meta_title' => $description['meta_title'] ?? '',
'meta_description' => $description['meta_description'] ?? '',
'meta_keyword' => $description['meta_keyword'] ?? '',
];
}
$category->descriptions()->createMany($descriptions);
$this->createPath($category);
DB::commit();
} catch (\Exception $e) {
DB::rollBack();
throw $e;
$isUpdating = $category !== null;
if ($category === null) {
$category = new Category();
}
return $category;
}
public function update(Category $category, array $data)
{
try {
DB::beginTransaction();
$category->updateOrFail($data);
$category->fill($data);
$category->save();
$descriptions = [];
foreach ($data['descriptions'] as $description) {
foreach ($data['descriptions'] as $locale => $description) {
$descriptions[] = [
'locale' => $description['locale'],
'locale' => $locale,
'name' => $description['name'],
'content' => $description['content'] ?? '',
'meta_title' => $description['meta_title'] ?? '',
@ -68,10 +41,16 @@ class CategoryService
'meta_keyword' => $description['meta_keyword'] ?? '',
];
}
$category->descriptions()->delete();
if ($isUpdating) {
$category->descriptions()->delete();
}
$category->descriptions()->createMany($descriptions);
$this->updatePath($category);
if ($isUpdating) {
$this->updatePath($category);
} else {
$this->createPath($category);
}
DB::commit();
} catch (\Exception $e) {

View File

@ -1,9 +1,9 @@
<?php
namespace App\Services;
namespace Beike\Admin\Services;
use App\Models\Product;
use App\Models\ProductDescription;
use Beike\Models\Product;
use Beike\Models\ProductDescription;
use Illuminate\Support\Facades\DB;
class ProductService

View File

@ -1,6 +1,6 @@
<?php
namespace App\View\Components;
namespace Beike\Admin\View\Components;
use Illuminate\View\Component;
@ -27,6 +27,6 @@ class Filter extends Component
*/
public function render()
{
return view('components.filter');
return view('Resources::components.admin.filter');
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace Beike\Admin\View\Components\Form;
use Illuminate\View\Component;
class Input extends Component
{
public string $name;
public string $title;
public string $value;
public function __construct(string $name, string $title, ?string $value)
{
$this->name = $name;
$this->title = $title;
$this->value = $value ?? '';
}
public function render()
{
return view('admin::components.form.input');
}
}

View File

@ -0,0 +1,60 @@
<?php
namespace Beike\Admin\View\Components\Form;
use Illuminate\Support\Arr;
use Illuminate\View\Component;
class InputLocale extends Component
{
public string $name;
public string $title;
public $value;
public function __construct(string $name, string $title, $value)
{
$this->name = $name;
$this->title = $title;
$this->value = $value;
}
public function render()
{
return view('admin::components.form.input-locale');
}
public function formatName(string $code)
{
// descriptions.*.name => descriptions[zh_cn][name]
$segments = explode('.', $this->name);
$key = $segments[0];
for ($i = 1; $i < count($segments); $i++) {
$segment = $segments[$i];
if ($segment == '*') {
$key .= '[' . $code . ']';
} else {
$key .= '[' . $segment . ']';
}
}
return $key;
}
public function formatValue($code)
{
$oldKey = str_replace('*', $code, $this->name);
// descriptions.*.name
$segments = explode('.', $this->name);
array_shift($segments);
$valueKey = implode('.', $segments);
$valueKey = str_replace('*', $code, $valueKey);
return old($oldKey, Arr::get($this->value, $valueKey, ''));
}
public function errorKey($code)
{
return str_replace('*', $code, $this->name);
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace Beike\Admin\View\Components\Form;
use Illuminate\View\Component;
class SwitchRadio extends Component
{
public string $name;
public string $value;
public string $title;
public function __construct(string $name, string $value, string $title)
{
$this->name = $name;
$this->title = $title;
$this->value = $value;
}
public function render()
{
return view('admin::components.form.switch-radio');
}
}

View File

@ -1,6 +1,6 @@
<?php
namespace App\View\Components\Admin;
namespace Beike\Admin\View\Components;
use Illuminate\View\Component;
@ -30,7 +30,7 @@ class Header extends Component
*/
public function render()
{
return view('components.admin.header');
return view('admin::components.header');
}
public function addLink($title, $url, $active = false)

View File

@ -1,6 +1,6 @@
<?php
namespace App\View\Components\Admin;
namespace Beike\Admin\View\Components;
use Illuminate\Support\Str;
use Illuminate\View\Component;
@ -34,7 +34,7 @@ class Sidebar extends Component
$this->addLink('回收站', admin_route('products.index', ['trashed' => 1]), 'fa fa-tachometer-alt', false);
}
return view('components.admin.sidebar');
return view('admin::components.sidebar');
}
public function addLink($title, $url, $icon, $active)

14
beike/Config/beike.php Normal file
View File

@ -0,0 +1,14 @@
<?php
/**
* Resources.php
*
* @copyright 2022 opencart.cn - All Rights Reserved
* @link http://www.guangdawangluo.com
* @author Sam Chen <sam.chen@opencart.cn>
* @created 2022-05-09 10:32:41
* @modified 2022-05-09 10:32:41
*/
return [
//
];

View File

@ -1,8 +1,8 @@
<?php
namespace App\Console\Commands;
namespace Beike\Console\Commands;
use App\Models\AdminUser;
use Beike\Models\AdminUser;
use Illuminate\Console\Command;
class MakeRootAdminUser extends Command

View File

@ -1,6 +1,6 @@
<?php
namespace App\Models;
namespace Beike\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;

View File

@ -1,6 +1,6 @@
<?php
namespace App\Models;
namespace Beike\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

View File

@ -1,6 +1,6 @@
<?php
namespace App\Models;
namespace Beike\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

View File

@ -1,6 +1,6 @@
<?php
namespace App\Models;
namespace Beike\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

View File

@ -1,6 +1,6 @@
<?php
namespace App\Models;
namespace Beike\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

View File

@ -1,6 +1,6 @@
<?php
namespace App\Models;
namespace Beike\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

View File

@ -1,6 +1,6 @@
<?php
namespace App\Models;
namespace Beike\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

View File

@ -1,6 +1,6 @@
<?php
namespace App\Models;
namespace Beike\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

View File

@ -1,6 +1,6 @@
<?php
namespace App\Models;
namespace Beike\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;

View File

@ -1,13 +1,12 @@
<?php
namespace App\Http\Controllers\Shop;
namespace Beike\Shop\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Models\ProductSku;
use App\Services\CartService;
use Beike\Models\ProductSku;
use Beike\Services\CartService;
use Illuminate\Http\Request;
class CartsController extends Controller
class CartController extends Controller
{
public function store(Request $request)
{

View File

@ -1,13 +1,12 @@
<?php
namespace App\Http\Controllers\Shop;
namespace Beike\Shop\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Models\Category;
use App\Models\Product;
use Beike\Models\Category;
use Beike\Models\Product;
use Illuminate\Http\Request;
class CategoriesController extends Controller
class CategoryController extends Controller
{
public function show(Request $request, Category $category)
{

View File

@ -0,0 +1,8 @@
<?php
namespace Beike\Shop\Http\Controllers;
class Controller extends \App\Http\Controllers\Controller
{
}

View File

@ -1,9 +1,8 @@
<?php
namespace App\Http\Controllers\Shop;
namespace Beike\Shop\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Models\Category;
use Beike\Models\Category;
use Plugin\Guangda\Seller\Models\Product;
class HomeController extends Controller

View File

@ -1,12 +1,11 @@
<?php
namespace App\Http\Controllers\Shop;
namespace Beike\Shop\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Models\Product;
use Beike\Models\Product;
use Illuminate\Http\Request;
class ProductsController extends Controller
class ProductController extends Controller
{
public function show(Request $request, Product $product)
{

View File

@ -0,0 +1,49 @@
<?php
namespace Beike\Shop\Providers;
use Beike\Console\Commands\MakeRootAdminUser;
use Beike\Models\AdminUser;
use Beike\Models\Setting;
use Beike\Admin\View\Components\Filter;
use Beike\Admin\View\Components\Header;
use Beike\Admin\View\Components\Sidebar;
use Beike\Admin\View\Components\Form\Input;
use Beike\Admin\View\Components\Form\InputLocale;
use Beike\Admin\View\Components\Form\SwitchRadio;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Str;
class ShopServiceProvider extends ServiceProvider
{
public function boot()
{
$uri = request()->getRequestUri();
if (Str::startsWith($uri, '/admin')) {
return;
}
$this->loadRoutesFrom(__DIR__ . '/../Routes/shop.php');
$this->mergeConfigFrom(__DIR__ . '/../../Config/beike.php', 'beike');
$this->loadSettings();
}
protected function loadSettings()
{
$settings = Setting::all(['name', 'value', 'json'])
->keyBy('name')
->transform(function ($setting) {
if ($setting->json) {
return \json_decode($setting->value, true);
}
return $setting->value;
})
->toArray();
config(['global' => $settings]);
}
}

View File

@ -0,0 +1,16 @@
<?php
use Illuminate\Support\Facades\Route;
Route::prefix('/')
->name('shop.')
->middleware(['web'])
->group(function () {
Route::get('/', [Beike\Shop\Http\Controllers\HomeController::class, 'index'])->name('home.index');
Route::get('carts', [Beike\Shop\Http\Controllers\CartController::class, 'store'])->name('carts.store');
Route::get('categories/{category}', [Beike\Shop\Http\Controllers\CategoryController::class, 'show'])->name('categories.show');
Route::get('products/{product}', [Beike\Shop\Http\Controllers\ProductController::class, 'show'])->name('products.show');
});

View File

@ -9,11 +9,11 @@
* @modified 2022-01-05 10:12:57
*/
namespace App\Services;
namespace Beike\Services;
use App\Models\Cart;
use App\Models\ProductSku;
use Beike\Models\Cart;
use Beike\Models\ProductSku;
class CartService
{

21
beike/package.json Normal file
View File

@ -0,0 +1,21 @@
{
"private": true,
"scripts": {
"dev": "npm run development",
"development": "mix",
"watch": "mix watch",
"watch-poll": "mix watch -- --watch-options-poll=1000",
"hot": "mix watch --hot",
"prod": "npm run production",
"production": "mix --production"
},
"devDependencies": {
"axios": "^0.21",
"bootstrap": "^4.6.1",
"laravel-mix": "^6.0.6",
"lodash": "^4.17.19",
"resolve-url-loader": "^4.0.0",
"sass": "^1.38.1",
"sass-loader": "^12.1.0"
}
}

28
beike/webpack.mix.js vendored Normal file
View File

@ -0,0 +1,28 @@
const mix = require('laravel-mix');
/*
|--------------------------------------------------------------------------
| Mix Asset Management
|--------------------------------------------------------------------------
|
| Mix provides a clean, fluent API for defining some Webpack build steps
| for your Laravel applications. By default, we are compiling the CSS
| file for the application as well as bundling up all the JS files.
|
*/
// mix.js('resources/js/app.js', 'public/build/js')
// .postCss('resources/css/app.css', 'public/build/css', [
// //
// ]);
mix.setPublicPath('../public');
mix.sass('Resources/css/app.scss', 'Resources/build/css/shop/app.css');
mix.sass('Resources/css/bootstrap/bootstrap.scss', 'Resources/build/css/bootstrap.css');
mix.sass('Resources/css/admin/app.scss', 'Resources/build/css/admin/app.css');
if (mix.inProduction()) {
mix.version();
}

View File

@ -26,10 +26,11 @@
"App\\": "app/",
"Database\\Factories\\": "database/factories/",
"Database\\Seeders\\": "database/seeders/",
"Plugin\\": "plugins/"
"Plugin\\": "plugins/",
"Beike\\": "beike/"
},
"files": [
"app/Helpers.php"
"beike/Helpers.php"
]
},
"autoload-dev": {

View File

@ -175,6 +175,9 @@ return [
App\Providers\EventServiceProvider::class,
App\Providers\RouteServiceProvider::class,
\Beike\Admin\Providers\AdminServiceProvider::class,
\Beike\Shop\Providers\ShopServiceProvider::class,
],
/*

View File

@ -1,7 +1,5 @@
<?php
use App\Models\AdminUser;
return [
/*
@ -42,11 +40,6 @@ return [
'driver' => 'session',
'provider' => 'users',
],
AdminUser::AUTH_GUARD => [
'driver' => 'session',
'provider' => 'admins',
],
],
/*
@ -76,11 +69,6 @@ return [
// 'driver' => 'database',
// 'table' => 'users',
// ],
'admins' => [
'driver' => 'eloquent',
'model' => AdminUser::class,
],
],
/*

View File

@ -1,2 +0,0 @@
*
!.gitignore

View File

@ -1,250 +0,0 @@
@extends('admin.layouts.master')
@push('header')
<script src="https://cdn.bootcdn.net/ajax/libs/vue/2.6.14/vue.js"></script>
@endpush
@section('content')
<div class="card">
<div class="card-body">
<h2>product</h2>
<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="hidden" name="_redirect" value="{{ old('_redirect', request()->header('referer')) }}" />
@php
if ($product ?? null) {
$descriptions = $product->descriptions->keyBy('locale');
} else {
$descriptions = [];
}
@endphp
<div>
@foreach (locales() as $locale)
<input type="text" name="descriptions[{{ $locale['code'] }}][name]" placeholder="Name {{ $locale['name'] }}" value="{{ old('descriptions.'.$locale['code'].'.name', $descriptions[$locale['code']]->name ?? '') }}">
@endforeach
</div>
<div>
<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="position" placeholder="position" value="{{ old('position', $product->position ?? 0) }}">
<input type="text" name="active" placeholder="active" value="{{ old('active', $product->active ?? 1) }}">
</div>
<div>
<h2>skus</h2>
<input type="radio" v-model="editing.isVariable" :value="false"> 单规格
<input type="radio" v-model="editing.isVariable" :value="true"> 多规格
<div v-if="editing.isVariable">
<div>
<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 v-if="form.skus.length">
<input v-if="form.skus.length" type="hidden" name="variables" :value="JSON.stringify(form.variables)">
<table>
<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 + '][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" 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>
<button type="submit" class="btn btn-primary">Save</button>
</form>
</div>
</div>
@endsection
@push('footer')
<script>
var app = new Vue({
el: '#app',
data: {
form: {
variables: @json($product->variables_decoded ?? []),
skus: @json($product->skus ?? []),
},
source: {
variables: @json($product->variables_decoded ?? []),
},
editing: {
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

@ -1,105 +0,0 @@
@extends('admin.layouts.master')
@section('title', '商品管理')
@push('header')
<script src="https://cdn.bootcdn.net/ajax/libs/axios/0.27.2/axios.min.js"></script>
@endpush
@section('content')
<div class="card">
<div class="card-body">
<x-filter :url="route('admin.products.index')" />
</div>
</div>
<div class="card mt-4" id="product-app">
<div class="card-header">
<a href="{{ route('admin.products.create') }}" class="btn btn-primary">Create</a>
</div>
<div class="card-body">
<table v-if="items.length" class="table">
<thead>
<tr>
<th></th>
<th>ID</th>
<th>图片</th>
<th>商品名称</th>
<th>价格</th>
<th>创建时间</th>
<th>上架</th>
<th>操作</th>
</tr>
</thead>
<tr v-for="(item, index) in items" :key="item.id">
<td>
<input type="checkbox" :value="item.id" v-model="selected" />
</td>
<td>@{{ item.id }}</td>
<td><img :src="item.image" alt="" srcset=""></td>
<td>@{{ item.name || '无名称' }}</td>
<td>@{{ item.price_formatted }}</td>
<td>@{{ item.created_at }}</td>
<td>@{{ item.active ? '上架' : '下架' }}</td>
<td>
<a :href="item.url_edit">编辑</a>
<template>
<a v-if="item.deleted_at == ''" href="javascript:void(0)" @click.prevent="deleteProduct(index)">删除</a>
<a v-else href="javascript:void(0)" @click.prevent="restoreProduct(index)">恢复</a>
</template>
</td>
</tr>
</table>
{{ $products->links() }}
<p v-if="items.length < 1">无商品</p>
</div>
</div>
@endsection
@push('footer')
<script>
new Vue({
el: '#product-app',
data: {
items: @json($products->items()),
selected: [],
},
methods: {
deleteProduct: function (index) {
const product = this.items[index];
this.$confirm('确认要删除选中的商品吗?', '删除商品', {
// confirmButtonText: '确定',
// cancelButtonText: '取消',
type: 'warning'
}).then(() => {
axios.delete('products/' + product.id).then(response => {
location.reload();
}).catch(error => {
// this.$message.error(error.response.data.message);
});
});
},
restoreProduct: function (index) {
const product = this.items[index];
this.$confirm('确认要恢复选中的商品吗?', '恢复商品', {
type: 'warning'
}).then(() => {
axios.put('products/restore', {
id: product.id
}).then(response => {
location.reload();
}).catch(error => {
// this.$message.error(error.response.data.message);
});
});
}
}
});
</script>
@endpush

1
routes/web.php Normal file
View File

@ -0,0 +1 @@
<?php

View File

@ -1,24 +0,0 @@
<?php
use Illuminate\Support\Facades\Route;
Route::prefix('admin')
->name('admin.')
->group(function () {
Route::get('login', [\App\Http\Controllers\Admin\LoginController::class, 'show'])->name('login.show');
Route::post('login', [\App\Http\Controllers\Admin\LoginController::class, 'store'])->name('login.store');
Route::middleware('auth:'.\App\Models\AdminUser::AUTH_GUARD)
->group(function () {
Route::get('/', [\App\Http\Controllers\Admin\HomeController::class, 'index'])->name('home.index');
Route::Resource('categories', \App\Http\Controllers\Admin\CategoriesController::class);
Route::put('products/restore', [\App\Http\Controllers\Admin\ProductsController::class, 'restore']);
Route::resource('products', \App\Http\Controllers\Admin\ProductsController::class);
Route::get('logout', [\App\Http\Controllers\Admin\LogoutController::class, 'index'])->name('logout.index');
});
});

View File

@ -1,15 +0,0 @@
<?php
use Illuminate\Support\Facades\Route;
Route::prefix('/')
->name('shop.')
->group(function () {
Route::get('/', [App\Http\Controllers\Shop\HomeController::class, 'index'])->name('home.index');
Route::get('carts', [App\Http\Controllers\Shop\CartsController::class, 'store'])->name('carts.store');
Route::get('categories/{category}', [App\Http\Controllers\Shop\CategoriesController::class, 'show'])->name('categories.show');
Route::get('products/{product}', [App\Http\Controllers\Shop\ProductsController::class, 'show'])->name('products.show');
});