优化后台 订单管理、商品管理、客户管理 列表筛选等

This commit is contained in:
pushuo 2023-02-03 15:03:15 +08:00 committed by Edward Yang
parent db6614e7a3
commit ee833b9eaa
8 changed files with 188 additions and 242 deletions

View File

@ -3,10 +3,11 @@
* @link https://beikeshop.com
* @Author pu shuo <pushuo@guangda.work>
* @Date 2022-08-22 18:32:26
* @LastEditTime 2022-09-16 20:57:51
* @LastEditTime 2023-02-03 10:12:59
*/
export default {
// 打开文件管理器
fileManagerIframe(callback) {
const base = document.querySelector('base').href;
@ -28,6 +29,7 @@ export default {
});
},
// 防抖
debounce(fn, delay) {
var timeout = null; // 创建一个标记用来存放定时器的返回值
@ -41,12 +43,14 @@ export default {
}
},
// 生成随机字符串
randomString(length) {
let str = '';
for (; str.length < length; str += Math.random().toString(36).substr(2));
return str.substr(0, length);
},
// 获取url参数
getQueryString(name, defaultValue) {
const reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)');
const r = window.location.search.substr(1).match(reg);
@ -57,11 +61,44 @@ export default {
return typeof(defaultValue) != 'undefined' ? defaultValue : '';
},
// 控制字符串长度 超出显示...
stringLengthInte(text, length = 50) {
if (text.length) {
return text.slice(0, length) + (text.length > length ? '...' : '');
}
return '';
},
// 给列表页筛选开发插件使用场景开发者需要添加筛选条件不需要到filter添加筛选key
addFilterCondition(app) {
if (location.search) {
const params = location.search.substr(1).split('&');
params.forEach(param => {
const [key, value] = param.split('=');
app.$set(app.filter, key, value);
});
}
},
// 将对象内不为空的转换为url参数 并 添加到url后面
objectToUrlParams(obj, url) {
const params = [];
for (const key in obj) {
if (obj[key] !== '') {
params.push(`${key}=${obj[key]}`);
}
}
return `${url}${params.length ? '?' : ''}${params.join('&')}`;
},
// 清空对象内的值
clearObjectValue(obj) {
for (const key in obj) {
obj[key] = '';
}
return obj;
}
}

View File

@ -36,7 +36,7 @@
<div id="content">
<div class="page-title-box py-1 d-flex align-items-center justify-content-between">
<h5 class="page-title">@yield('title')</h5>
@yield('page-title-right')
<div>@yield('page-title-right')</div>
</div>
<div class="container-fluid p-0">
@yield('content')

View File

@ -47,55 +47,59 @@
<button type="button" class="btn btn-primary" @click="checkedCustomerSclearRestore">{{ __('admin/product.clear_restore') }}</button>
@endif
</div>
<div class="table-push">
<table class="table">
<thead>
<tr>
<th>{{ __('common.id') }}</th>
<th>{{ __('customer.email') }}</th>
<th>{{ __('customer.name') }}</th>
<th>{{ __('customer.from') }}</th>
<th>{{ __('customer.customer_group') }}</th>
<th>{{ __('common.status') }}</th>
<th>{{ __('common.created_at') }}</th>
<th>{{ __('common.action') }}</th>
</tr>
</thead>
<tbody v-if="customers.data.length">
<tr v-for="customer, index in customers.data" :key="index">
<td>@{{ customer.id }}</td>
<td>@{{ customer.email }}</td>
<td>
<div class="d-flex align-items-center">
{{-- <img src="@{{ customer.avatar }}" class="img-fluid rounded-circle me-2" style="width: 40px;"> --}}
<div>@{{ customer.name }}</div>
</div>
</td>
<td>@{{ customer.from }}</td>
<td>@{{ customer.customer_group_name }}</td>
<td>
<span v-if="customer.status" class="text-success">{{ __('common.enable') }}</span>
<span v-else class="text-secondary">{{ __('common.disable') }}</span>
</td>
<td>@{{ customer.created_at }}</td>
<td>
@if ($type != 'trashed')
<a class="btn btn-outline-secondary btn-sm" :href="customer.edit">{{ __('common.edit') }}</a>
<button class="btn btn-outline-danger btn-sm ml-1" type="button" @click="deleteCustomer(customer.delete, index)">{{ __('common.delete') }}</button>
@else
<a href="javascript:void(0)" class="btn btn-outline-secondary btn-sm"
@click.prevent="restore(customer.id, index)">{{ __('common.restore') }}</a>
<button class="btn btn-outline-danger btn-sm ml-1" type="button" @click="deleteTrashedCustomer(customer.id, index)">{{ __('common.delete') }}</button>
@endif
</td>
</tr>
</tbody>
<tbody v-else><tr><td colspan="9" class="border-0"><x-admin-no-data /></td></tr></tbody>
</table>
</div>
<el-pagination v-if="customers.data.length" layout="total, prev, pager, next" background :page-size="customers.per_page" :current-page.sync="page"
:total="customers.total"></el-pagination>
@if ($customers->total())
<div class="table-push">
<table class="table">
<thead>
<tr>
<th>{{ __('common.id') }}</th>
<th>{{ __('customer.email') }}</th>
<th>{{ __('customer.name') }}</th>
<th>{{ __('customer.from') }}</th>
<th>{{ __('customer.customer_group') }}</th>
<th>{{ __('common.status') }}</th>
<th>{{ __('common.created_at') }}</th>
<th>{{ __('common.action') }}</th>
</tr>
</thead>
<tbody>
@foreach ($customers as $customer)
<tr>
<td>{{ $customer['id'] }}</td>
<td>{{ $customer['email'] }}</td>
<td>
<div class="d-flex align-items-center">
<div>{{ $customer['name'] }}</div>
</div>
</td>
<td>{{ $customer['from'] }}</td>
<td>{{ $customer->customerGroup->description->name ?? '' }}</td>
<td>
<span class="{{ $customer['active'] ? 'text-success' : 'text-secondary' }}">
{{ $customer['active'] ? __('common.enable') : __('common.disable') }}
</span>
</td>
<td>{{ $customer['created_at'] }}</td>
<td>
@if ($type != 'trashed')
<a class="btn btn-outline-secondary btn-sm" href="{{ admin_route('customers.edit', [$customer->id]) }}">{{ __('common.edit') }}</a>
<button class="btn btn-outline-danger btn-sm ml-1" type="button" @click="deleteCustomer({{ $customer['id'] }})">{{ __('common.delete') }}</button>
@else
<a href="javascript:void(0)" class="btn btn-outline-secondary btn-sm"
@click.prevent="restore({{ $customer['id'] }})">{{ __('common.restore') }}</a>
<button class="btn btn-outline-danger btn-sm ml-1" type="button" @click="deleteTrashedCustomer({{ $customer['id'] }})">{{ __('common.delete') }}</button>
@endif
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
{{ $customers->withQueryString()->links('admin::vendor/pagination/bootstrap-4') }}
@else
<x-admin-no-data />
@endif
</div>
<el-dialog title="{{ __('admin/customer.customers_create') }}" :visible.sync="dialogCustomers.show" width="670px"
@ -135,9 +139,6 @@
el: '#customer-app',
data: {
page: 1,
customers: @json($customers ?? []),
source: {
customer_group: @json($customer_groups ?? []),
},
@ -163,48 +164,24 @@
password: [{required: true,message: '{{ __('common.error_required', ['name' => __('shop/login.password')] ) }}',trigger: 'blur'}, ],
},
url: @json(admin_route('customers.index')),
url: '{{ $type == 'trashed' ? admin_route('customers.trashed') : admin_route('customers.index') }}',
filter: {
page: bk.getQueryString('page'),
email: bk.getQueryString('email'),
name: bk.getQueryString('name'),
customer_group_id: bk.getQueryString('customer_group_id'),
status: bk.getQueryString('status'),
},
customerIds: @json($customers->pluck('id')),
},
mounted () {
},
watch: {
page: function() {
this.loadData();
},
},
computed: {
query() {
let query = '';
const filter = Object.keys(this.filter)
.filter(key => this.filter[key])
.map(key => key + '=' + this.filter[key])
.join('&');
if (filter) {
query += '?' + filter;
}
return query;
}
created() {
bk.addFilterCondition(this);
},
methods: {
loadData() {
$http.get(`customers?page=${this.page}`).then((res) => {
this.customers = res.data.customers;
})
},
checkedCustomersCreate() {
this.dialogCustomers.show = true
},
@ -254,23 +231,22 @@
$http.post('customers', this.dialogCustomers.form).then((res) => {
this.$message.success(res.message);
this.loadData();// this.customers.data.push(res.data);
window.location.reload();
this.dialogCustomers.show = false
})
});
},
deleteCustomer(url, index) {
deleteCustomer(id) {
const self = this;
this.$confirm('{{ __('common.confirm_delete') }}', '{{ __('common.text_hint') }}', {
confirmButtonText: '{{ __('common.confirm') }}',
cancelButtonText: '{{ __('common.cancel') }}',
type: 'warning'
}).then(() => {
$http.delete(url).then((res) => {
$http.delete(`customers/${id}`).then((res) => {
self.$message.success(res.message);
window.location.reload();
// self.customers.splice(index, 1)
})
}).catch(()=>{})
},
@ -281,12 +257,12 @@
},
search() {
location = this.url + this.query
location = bk.objectToUrlParams(this.filter, this.url)
},
resetSearch() {
Object.keys(this.filter).forEach(key => this.filter[key] = '')
location = this.url + this.query
this.filter = bk.clearObjectValue(this.filter)
location = bk.objectToUrlParams(this.filter, this.url)
},
}
})

View File

@ -10,11 +10,10 @@
<div id="customer-app" class="card h-min-600">
<div class="card-body">
<div class="bg-light p-4 mb-3" id="app">
<el-form :inline="true" :model="filter" class="demo-form-inline" label-width="100px">
<el-form :inline="true" ref="filterForm" :model="filter" class="demo-form-inline" label-width="100px">
<div>
<el-form-item label="{{ __('order.number') }}">
<el-input @keyup.enter.native="search" v-model="filter.number" size="small" placeholder="{{ __('order.number') }}"></el-input>
{{-- <input @keyup.enter="search" type="text" v-model="filter.number" class="form-control" placeholder="{{ __('order.number') }}"> --}}
</el-form-item>
<el-form-item label="{{ __('order.customer_name') }}">
<el-input @keyup.enter.native="search" v-model="filter.customer_name" size="small" placeholder="{{ __('order.customer_name') }}">
@ -109,13 +108,13 @@
@endif
</div>
</div>
@endsection
@hook('admin.order.list.content.footer')
@hook('admin.order.list.content.footer')
@endsection
@push('footer')
<script>
new Vue({
let app = new Vue({
el: '#app',
data: {
url: @json(admin_route('orders.index')),
@ -130,34 +129,22 @@
},
},
computed: {
query() {
let query = '';
const filter = Object.keys(this.filter)
.filter(key => this.filter[key])
.map(key => key + '=' + this.filter[key])
.join('&');
if (filter) {
query += '?' + filter;
}
return query;
}
created() {
bk.addFilterCondition(this);
},
methods: {
search() {
location = this.url + this.query
location = bk.objectToUrlParams(this.filter, this.url)
},
resetSearch() {
Object.keys(this.filter).forEach(key => this.filter[key] = '')
location = this.url + this.query
this.filter = bk.clearObjectValue(this.filter)
location = bk.objectToUrlParams(this.filter, this.url)
},
exportOrder() {
location = this.exportUrl + this.query
location = bk.objectToUrlParams(this.filter, this.exportUrl)
},
}
});

View File

@ -68,18 +68,18 @@
<button class="btn btn-primary" @click="clearRestore">{{ __('admin/product.clear_restore') }}</button>
@endif
@if ($type != 'trashed')
<div class="right nowrap" v-if="product.data.length">
<button class="btn btn-outline-secondary" :disabled="!selected.length" @click="batchDelete">{{ __('admin/product.batch_delete') }}</button>
<button class="btn btn-outline-secondary" :disabled="!selected.length"
@click="batchActive(true)">{{ __('admin/product.batch_active') }}</button>
<button class="btn btn-outline-secondary" :disabled="!selected.length"
@click="batchActive(false)">{{ __('admin/product.batch_inactive') }}</button>
</div>
@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>
<template v-if="product.data.length">
@if ($products->total())
<div class="table-push">
<table class="table table-hover">
<thead>
@ -93,8 +93,8 @@
<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="orderBy = 'created_at:asc'"></i>
<i class="el-icon-caret-bottom" @click="orderBy = 'created_at:desc'"></i>
<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>
@ -103,8 +103,8 @@
<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="orderBy = 'position:asc'"></i>
<i class="el-icon-caret-bottom" @click="orderBy = 'position:desc'"></i>
<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>
@ -115,45 +115,44 @@
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in product.data" :key="item.id">
<td><input type="checkbox" :value="item.id" v-model="selected" /></td>
<td>@{{ item.id }}</td>
@foreach ($products as $product)
<tr>
<td><input type="checkbox" :value="{{ $product['id'] }}" v-model="selectedIds" /></td>
<td>{{ $product['id'] }}</td>
<td>
<div class="wh-60 border d-flex justify-content-between align-items-center"><img :src="item.images[0] || 'image/placeholder.png'" class="img-fluid"></div>
<div class="wh-60 border d-flex justify-content-between align-items-center"><img src="{{ $product['images'][0] ?? 'image/placeholder.png' }}" class="img-fluid"></div>
</td>
<td>
<a :href="item.url" target="_blank" :title="item.name" class="text-dark">@{{ stringLengthInte(item.name, 90) }}</a>
<a href="{{ $product['url'] }}" target="_blank" title="{{ $product['name'] }}" class="text-dark">{{ $product['name'] }}</a>
</td>
<td>@{{ item.price_formatted }}</td>
<td>@{{ item.created_at }}</td>
<td>@{{ item.position }}</td>
<td>{{ $product['price_formatted'] }}</td>
<td>{{ $product['created_at'] }}</td>
<td>{{ $product['position'] }}</td>
@if ($type != 'trashed')
<td>
<span v-if="item.active" class="text-success">{{ __('common.enable') }}</span>
<span v-else class="text-secondary">{{ __('common.disable') }}</span>
<span class="{{ $product['active'] ? 'text-success' : 'text-secondary' }}">
{{ $product['active'] ? __('common.enable') : __('common.disable') }}
</span>
</td>
@endif
<td width="140" class="text-end">
<template v-if="item.deleted_at == ''">
<a :href="item.url_edit" class="btn btn-outline-secondary btn-sm">{{ __('common.edit') }}</a>
<a href="javascript:void(0)" class="btn btn-outline-danger btn-sm"
@click.prevent="deleteProduct(index)">{{ __('common.delete') }}</a>
</template>
<template v-else>
<a href="javascript:void(0)" class="btn btn-outline-secondary btn-sm"
@click.prevent="restoreProduct(index)">{{ __('common.restore') }}</a>
</template>
@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-danger btn-sm" @click.prevent="deleteProduct({{ $loop->index }})">{{ __('common.delete') }}</a>
@else
<a href="javascript:void(0)" class="btn btn-outline-secondary btn-sm" @click.prevent="restoreProduct({{ $loop->index }})">{{ __('common.restore') }}</a>
@endif
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
<el-pagination layout="total, prev, pager, next" background :page-size="product.per_page" :current-page.sync="page"
:total="product.total"></el-pagination>
</template>
<div v-else><x-admin-no-data /></div>
{{ $products->withQueryString()->links('admin::vendor/pagination/bootstrap-4') }}
@else
<x-admin-no-data />
@endif
</div>
</div>
</div>
@ -161,92 +160,46 @@
@push('footer')
<script>
new Vue({
let app = new Vue({
el: '#product-app',
data: {
product: @json($products),
url: '{{ $type == 'trashed' ? admin_route("products.trashed") : admin_route("products.index") }}',
filter: {
name: bk.getQueryString('name'),
page: bk.getQueryString('page'),
category_id: bk.getQueryString('category_id'),
sku: bk.getQueryString('sku'),
model: bk.getQueryString('model'),
active: bk.getQueryString('active'),
order_by: bk.getQueryString('order_by', ''),
},
selected: [],
page: bk.getQueryString('page', 1) * 1,
orderBy: bk.getQueryString('order_by', 'products.id:desc'),
selectedIds: [],
productIds: @json($products->pluck('id')),
},
computed: {
url: function() {
let filter = {};
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 ($type == 'products')
const url = @json(admin_route('products.index'));
@else
const url = @json(admin_route('products.trashed'));
@endif
if (query) {
return url + '?' + query;
}
return url;
},
allSelected: {
get() {
return this.selected.length == this.product.data.length;
get(e) {
return this.selectedIds.length == this.productIds.length;
},
set(val) {
return this.selected = val ? this.product.data.map(e => e.id) : [];
return val ? this.selectedIds = this.productIds : this.selectedIds = [];
}
}
},
watch: {
page: function() {
this.loadData();
},
orderBy: function() {
this.loadData();
}
created() {
bk.addFilterCondition(this);
},
methods: {
loadData: function() {
window.history.pushState('', '', this.url);
$http.get(this.url).then((res) => {
this.product = res;
})
},
methods: {
batchDelete() {
this.$confirm('{{ __('admin/product.confirm_batch_product') }}', '{{ __('common.text_hint') }}', {
confirmButtonText: '{{ __('common.confirm') }}',
cancelButtonText: '{{ __('common.cancel') }}',
type: 'warning'
}).then(() => {
$http.delete('products/delete', {
ids: this.selected
}).then((res) => {
$http.delete('products/delete', {ids: this.selectedIds}).then((res) => {
layer.msg(res.message)
location.reload();
})
@ -259,50 +212,51 @@
cancelButtonText: '{{ __('common.cancel') }}',
type: 'warning'
}).then(() => {
$http.post('products/status', {
ids: this.selected,
status: type
}).then((res) => {
$http.post('products/status', {ids: this.selectedIds, status: type}).then((res) => {
layer.msg(res.message)
location.reload();
})
}).catch(()=>{});
},
search: function() {
this.page = 1;
this.loadData();
search() {
this.filter.page = '';
location = bk.objectToUrlParams(this.filter, this.url)
},
checkedOrderBy(orderBy) {
this.filter.order_by = orderBy;
location = bk.objectToUrlParams(this.filter, this.url)
},
resetSearch() {
Object.keys(this.filter).forEach(key => this.filter[key] = '')
this.loadData();
this.filter = bk.clearObjectValue(this.filter)
location = bk.objectToUrlParams(this.filter, this.url)
},
deleteProduct: function(index) {
const product = this.product.data[index];
deleteProduct(index) {
const id = this.productIds[index];
this.$confirm('{{ __('common.confirm_delete') }}', '{{ __('common.text_hint') }}', {
type: 'warning'
}).then(() => {
$http.delete('products/' + product.id).then((res) => {
$http.delete('products/' + id).then((res) => {
this.$message.success(res.message);
location.reload();
})
});
}).catch(()=>{});;
},
restoreProduct: function(index) {
const product = this.product.data[index];
restoreProduct(index) {
const id = this.productIds[index];
this.$confirm('{{ __('admin/product.confirm_batch_restore') }}', '{{ __('common.text_hint') }}', {
type: 'warning'
}).then(() => {
$http.put('products/restore', {
id: product.id
}).then((res) => {
$http.put('products/restore', {id: id}).then((res) => {
location.reload();
})
});
}).catch(()=>{});;
},
clearRestore() {
@ -312,7 +266,7 @@
$http.post('products/trashed/clear').then((res) => {
location.reload();
})
});
}).catch(()=>{});;
}
}
});

View File

@ -63,7 +63,7 @@ footer {
}
.logo {
max-width: 150px;
max-width: 240px;
margin-bottom: 20px;
}

View File

@ -46,7 +46,8 @@ header {
.header-content {
position: relative;
background-color: #fff;
> .container {
display: flex;
align-items: center;
@ -95,8 +96,6 @@ header {
> .nav-link {
font-size: 15px;
padding: 1rem;
// padding-right: 1rem;
// padding-left: 1rem;
position: relative;
.badge {
@ -136,18 +135,11 @@ header {
}
}
}
// .nav-link {
// color: #333;
// // font-weight: bold;
// font-size: .9rem;
// padding-left: 1rem;
// padding-right: 1rem;
// }
}
.logo {
img {
max-width: 180px;
max-width: 200px;
max-height: 50px;
}
}

View File

@ -3,7 +3,7 @@
* @link https://beikeshop.com
* @Author pu shuo <pushuo@guangda.work>
* @Date 2022-08-29 17:32:51
* @LastEditTime 2023-01-16 11:28:28
* @LastEditTime 2023-02-02 11:06:01
*/
import http from "../../../../js/http";