功能模块重构 & CSS 整体优化:实现编辑/删除咖啡店功能
在这篇教程中,我们将实现咖啡店的编辑和删除功能,在实现过新增咖啡店功能后,咖啡店的编辑功能实现起来非常简单,无论是前台表单还是后台逻辑,思路都是一样的,无非是最后一个在数据库中新增,一个更新而已,此外,编辑咖啡店时需要先获取待编辑数据渲染到表单中。下面我们就来一步步实现编辑和删除功能。
第一步:更新模型类
由于我们要实现删除功能,并且实现的是软删除,之前已经在数据表迁移类中通过 $table->softDeletes();
为 cafes
表添加了 deleted_at
字段,所以接下来还要在 app/Models/Cafe.php
模型类中通过如下方式使其支持软删除:
class Cafe extends Model
{
use SoftDeletes;
...
第二步:新增编辑/删除路由:
在 routes/api.php
的私有路由分组中,新增以下三个路由:
/*
|-------------------------------------------------------------------------------
| 获取待编辑咖啡店数据
|-------------------------------------------------------------------------------
| URL: /api/v1/cafes/{slug}/edit
| Controller: API\CafesController@getCafeEditData
| Method: GET
| Description: 获取待编辑咖啡店数据
*/
Route::get('/cafes/{id}/edit', 'API\CafesController@getCafeEditData');
/*
|-------------------------------------------------------------------------------
| 执行更新咖啡店请求
|-------------------------------------------------------------------------------
| URL: /api/v1/cafes/{slug}
| Controller: API\CafesController@putEditCafe
| Method: PUT
| Description: 执行更新咖啡店请求
*/
Route::put('/cafes/{id}', 'API\CafesController@putEditCafe');
/*
|-------------------------------------------------------------------------------
| 删除指定咖啡店
|-------------------------------------------------------------------------------
| URL: /api/v1/cafes/{slug}
| Controller: API\CafesController@deleteCafe
| Method: DELETE
| Description: 删除指定咖啡店
*/
Route::delete('/cafes/{id}', 'API\CafesController@deleteCafe');
第三步:初始化控制器方法
接下来我们需要在控制器 app/Http/Controllers/API/CafesController.php
中编写路由中指定的三个方法:
// 获取咖啡店编辑表单数据
public function getCafeEditData($id)
{
$cafe = Cafe::where('id', '=', $id)
->with('brewMethods')
->withCount('userLike')
->with(['company' => function ($query) {
$query->withCount('cafes');
}])
->first();
return response()->json($cafe);
}
// 更新咖啡店数据
public function putEditCafe($id, Request $request)
{
}
// 删除咖啡店
public function deleteCafe($id)
{
$cafe = Cafe::where('id', '=', $id)->first();
$cafe->delete();
return response()->json(['message' => '删除成功'], 204);
}
getCafeEditData
和 deleteCafe
比较简单,直接实现了,putEditCafe
方法留到前端页面实现之后再实现。
第四步:更新前端路由文件
接下来,我们在前端路由文件 resources/assets/js/routes.js
中新增编辑咖啡店页面路由:
{
path: 'cafes/:id/edit',
name: 'editcafe',
component: Vue.component('EditCafe', require('./pages/EditCafe.vue')),
beforeEnter: requireAuth
},
因为编辑咖啡店需要用户登录后才能访问,所以我们为这个路由添加了导航守卫。
在 resources/assets/js/api/cafe.js
中新增三个后端 API 调用:
/**
* GET /api/v1/cafes/{id}/edit
*/
getCafeEdit: function (id) {
return axios.get(ROAST_CONFIG.API_URL + '/cafes/' + id + '/edit');
},
/**
* PUT /api/v1/cafes/{slug}
*/
putEditCafe: function (id, companyName, companyID, companyType, subscription, website, locationName, address, city, state, zip, brewMethods, matcha, tea) {
let formData = new FormData();
formData.append('company_name', companyName);
formData.append('company_id', companyID);
formData.append('company_type', companyType);
formData.append('subscription', subscription);
formData.append('website', website);
formData.append('location_name', locationName);
formData.append('address', address);
formData.append('city', city);
formData.append('state', state);
formData.append('zip', zip);
formData.append('brew_methods', JSON.stringify(brewMethods));
formData.append('matcha', matcha);
formData.append('tea', tea);
formData.append('_method', 'PUT');
return axios.post(ROAST_CONFIG.API_URL + '/cafes/' + id,
formData
);
},
deleteCafe: function (id) {
return axios.delete(ROAST_CONFIG.API_URL + '/cafes/' + id);
}
此外,还要在 resources/assets/js/modules/cafes.js
中新增调用相应 API 的 actions
:
loadCafeEdit({commit}, data) {
commit('setCafeEditLoadStatus', 1);
CafeAPI.getCafeEdit(data.id)
.then(function (response) {
commit('setCafeEdit', response.data);
commit('setCafeEditLoadStatus', 2);
})
.catch(function () {
commit('setCafeEdit', {});
commit('setCafeEditLoadStatus', 3);
});
},
editCafe({commit, state, dispatch}, data) {
commit('setCafeEditStatus', 1);
CafeAPI.putEditCafe(data.id, data.company_name, data.company_id, data.company_type, data.subscription, data.website, data.location_name, data.address, data.city, data.state, data.zip, data.brew_methods, data.matcha, data.tea)
.then(function (response) {
if (typeof response.data.cafe_updates_pending !== 'undefined') {
commit('setCafeEditText', response.data.cafe_updates_pending + ' 正在编辑中!');
} else {
commit('setCafeEditText', response.data.name + ' 已经编辑成功!');
}
commit('setCafeEditStatus', 2);
dispatch('loadCafes');
})
.catch(function (error) {
commit('setCafeEditStatus', 3);
});
},
deleteCafe({commit, state, dispatch}, data) {
commit('setCafeDeleteStatus', 1);
CafeAPI.deleteCafe(data.id)
.then(function (response) {
if (typeof response.data.cafe_delete_pending !== 'undefined') {
commit('setCafeDeletedText', response.data.cafe_delete_pending + ' 正在删除中!');
} else {
commit('setCafeDeletedText', '咖啡店删除成功!');
}
commit('setCafeDeleteStatus', 2);
dispatch('loadCafes');
})
.catch(function () {
commit('setCafeDeleteStatus', 3);
});
},
以及对应的 state
:
cafeEdit: {},
cafeEditLoadStatus: 0,
cafeEditStatus: 0,
cafeEditText: '',
cafeDeletedStatus: 0,
cafeDeleteText: '',
还有这些新增的 state
对应的 mutations
和 getters
:
// mutations
setCafeEdit(state, cafe) {
state.cafeEdit = cafe;
},
setCafeEditStatus(state, status) {
state.cafeEditStatus = status;
},
setCafeEditText(state, text) {
state.cafeEditText = text;
},
setCafeEditLoadStatus(state, status) {
state.cafeEditLoadStatus = status;
},
setCafeDeleteStatus(state, status) {
state.cafeDeletedStatus = status;
},
setCafeDeletedText(state, text) {
state.cafeDeleteText = text;
}
// getters
getCafeEdit(state) {
return state.cafeEdit;
},
getCafeEditStatus(state) {
return state.cafeEditStatus;
},
getCafeEditText(state) {
return state.cafeEditText;
},
getCafeEditLoadStatus(state) {
return state.cafeEditLoadStatus;
},
getCafeDeletedStatus(state) {
return state.cafeDeletedStatus;
},
getCafeDeletedText(state) {
return state.cafeDeleteText;
}
第五步:实现编辑咖啡店页面组件
在 resources/assets/js/pages
目录下创建 EditCafe.vue
,编写组件代码如下:
<style scoped lang="scss">
@import '~@/abstracts/_variables.scss';
div#new-cafe-page {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: white;
z-index: 99999;
overflow: auto;
img#back {
float: right;
margin-top: 20px;
margin-right: 20px;
}
.centered {
margin: auto;
}
h2.page-title {
color: #342C0C;
font-size: 36px;
font-weight: 900;
font-family: "Lato", sans-serif;
margin-top: 60px;
}
label.form-label {
font-family: "Lato", sans-serif;
text-transform: uppercase;
font-weight: bold;
color: black;
margin-top: 10px;
margin-bottom: 10px;
}
input[type="text"].form-input {
border: 1px solid #BABABA;
border-radius: 3px;
&.invalid {
border: 1px solid #D0021B;
}
}
div.validation {
color: #D0021B;
font-family: "Lato", sans-serif;
font-size: 14px;
margin-top: -15px;
margin-bottom: 15px;
}
div.location-type {
text-align: center;
font-family: "Lato", sans-serif;
font-size: 16px;
width: 25%;
display: inline-block;
height: 55px;
line-height: 55px;
cursor: pointer;
margin-bottom: 5px;
margin-right: 10px;
background-color: #EEE;
color: $black;
&.active {
color: white;
background-color: $secondary-color;
}
&.roaster {
border-top-left-radius: 3px;
border-bottom-left-radius: 3px;
border-right: 0px;
}
&.cafe {
border-top-right-radius: 3px;
border-bottom-right-radius: 3px;
}
}
div.company-selection-container {
position: relative;
div.company-autocomplete-container {
border-radius: 3px;
border: 1px solid #BABABA;
background-color: white;
margin-top: -17px;
width: 80%;
position: absolute;
z-index: 9999;
div.company-autocomplete {
cursor: pointer;
padding-left: 12px;
padding-right: 12px;
padding-top: 8px;
padding-bottom: 8px;
span.company-name {
display: block;
color: #0D223F;
font-size: 16px;
font-family: "Lato", sans-serif;
font-weight: bold;
}
span.company-locations {
display: block;
font-size: 14px;
color: #676767;
font-family: "Lato", sans-serif;
}
&:hover {
background-color: #F2F2F2;
}
}
div.new-company {
cursor: pointer;
padding-left: 12px;
padding-right: 12px;
padding-top: 8px;
padding-bottom: 8px;
font-family: "Lato", sans-serif;
color: #054E7A;
font-style: italic;
&:hover {
background-color: #F2F2F2;
}
}
}
}
a.edit-location-button {
display: block;
text-align: center;
height: 50px;
color: white;
border-radius: 3px;
font-size: 18px;
font-family: "Lato", sans-serif;
background-color: #A7BE4D;
line-height: 50px;
margin-bottom: 10px;
}
a.delete-location {
color: #D0021B;
font-size: 14px;
text-decoration: underline;
display: inline-block;
margin-bottom: 50px;
}
}
/* Small only */
@media screen and (max-width: 39.9375em) {
div#new-cafe-page {
div.location-type {
width: 50%;
}
}
}
</style>
<template>
<transition name="scale-in-center">
<div id="new-cafe-page">
<router-link :to="{ name: 'cafes' }">
<img src="/storage/img/close-modal.svg" id="back"/>
</router-link>
<div class="grid-container">
<div class="grid-x grid-padding-x">
<div class="large-8 medium-9 small-12 cell centered">
<h2 class="page-title">编辑咖啡店</h2>
</div>
</div>
<div class="grid-x grid-padding-x">
<div class="large-8 medium-9 small-12 cell centered company-selection-container">
<label class="form-label">公司名称</label>
<input type="text" class="form-input" v-model="companyName" v-on:keyup="searchCompanies()"
v-bind:class="{'invalid' : !validations.companyName.is_valid }"/>
<div class="validation" v-show="!validations.companyName.is_valid">{{
validations.companyName.text }}
</div>
<input type="hidden" v-model="companyID"/>
<div class="company-autocomplete-container" v-show="companyName.length > 0 && showAutocomplete">
<div class="company-autocomplete" v-for="companyResult in companyResults"
v-on:click="selectCompany( companyResult )">
<span class="company-name">{{ companyResult.name }}</span>
<span class="company-locations">{{ companyResult.cafes_count }} location<span
v-if="companyResult.cafes_count > 1">s</span></span>
</div>
<div class="new-company" v-on:click="addNewCompany()">
Add new company called "{{ companyName }}"
</div>
</div>
</div>
</div>
<div class="grid-x grid-padding-x">
<div class="large-8 medium-9 small-12 cell centered">
<label class="form-label">网站</label>
<input type="text" class="form-input" v-model="website"
v-bind="{ 'invalid' : !validations.website.is_valid }"/>
<div class="validation" v-show="!validations.website.is_valid">{{ validations.website.text }}
</div>
</div>
</div>
<div class="grid-x grid-padding-x">
<div class="large-8 medium-9 small-12 cell centered">
<label class="form-label">类型</label>
</div>
</div>
<div class="grid-x grid-padding-x">
<div class="large-8 medium-9 small-12 cell centered">
<div class="location-type roaster" v-bind:class="{ 'active': companyType === 'roaster' }"
v-on:click="setCompanyType('roaster')">
烘焙店
</div>
<div class="location-type cafe" v-bind:class="{ 'active': companyType === 'cafe' }"
v-on:click="setCompanyType('cafe')">
咖啡店
</div>
</div>
</div>
<div class="grid-x grid-padding-x" v-show="companyType === 'roaster'">
<div class="large-8 medium-9 small-12 cell centered">
<label class="form-label">是否提供订购服务?</label>
</div>
</div>
<div class="grid-x grid-padding-x" v-show="companyType === 'roaster'">
<div class="large-8 medium-9 small-12 cell centered">
<div class="subscription-option option"
v-on:click="subscription === 0 ? subscription = 1 : subscription = 0"
v-bind:class="{'active': subscription === 1}">
<div class="option-container">
<img src="/storage/img/coffee-pack.svg" class="option-icon"/> <span class="option-name">咖啡订购</span>
</div>
</div>
</div>
</div>
<div class="grid-x grid-padding-x">
<div class="large-8 medium-9 small-12 cell centered">
<label class="form-label">支持的冲泡方法</label>
</div>
</div>
<div class="grid-x grid-padding-x">
<div class="large-8 medium-9 small-12 cell centered">
<div class="brew-method option" v-on:click="toggleSelectedBrewMethod(method.id)"
v-for="method in brewMethods"
v-bind:class="{'active': brewMethodsSelected.indexOf(method.id) >= 0 }">
<div class="option-container">
<img v-bind:src="method.icon" class="option-icon"/> <span class="option-name">{{ method.method }}</span>
</div>
</div>
</div>
</div>
<div class="grid-x grid-padding-x">
<div class="large-8 medium-9 small-12 cell centered">
<label class="form-label">支持的饮料选项</label>
</div>
</div>
<div class="grid-x grid-padding-x">
<div class="large-8 medium-9 small-12 cell centered">
<div class="drink-option option" v-on:click="matcha === 0 ? matcha = 1 : matcha = 0"
v-bind:class="{'active': matcha === 1 }">
<div class="option-container">
<img v-bind:src="'/storage/img/matcha-latte.svg'" class="option-icon"/> <span
class="option-name">抹茶</span>
</div>
</div>
<div class="drink-option option" v-on:click="tea === 0 ? tea = 1 : tea = 0"
v-bind:class="{'active': tea === 1 }">
<div class="option-container">
<img v-bind:src="'/storage/img/tea-bag.svg'" class="option-icon"/> <span
class="option-name">茶包</span>
</div>
</div>
</div>
</div>
<div class="grid-x grid-padding-x">
<div class="large-8 medium-9 small-12 cell centered">
<label class="form-label">位置名称</label>
<input type="text" class="form-input" v-model="locationName"/>
</div>
</div>
<div class="grid-x grid-padding-x">
<div class="large-8 medium-9 small-12 cell centered">
<label class="form-label">街道地址</label>
<input type="text" v-model="address" placeholder="街道地址"
class="form-input" v-bind:class="{'invalid' : !validations.address.is_valid }"/>
<div class="validation" v-show="!validations.address.is_valid">{{ validations.address.text }}
</div>
</div>
</div>
<div class="grid-x grid-padding-x">
<div class="large-8 medium-9 small-12 cell centered">
<label class="form-label">城市</label>
<input type="text" class="form-input" v-model="city"
v-bind:class="{'invalid' : !validations.city.is_valid }"/>
<div class="validation" v-show="!validations.city.is_valid">{{ validations.city.text }}</div>
</div>
</div>
<div class="grid-x grid-padding-x">
<div class="large-8 medium-9 small-12 cell centered">
<div class="grid-x grid-padding-x">
<div class="large-6 medium-6 small-12 cell">
<label class="form-label">省份</label>
<select v-model="state" v-bind:class="{'invalid' : !validations.state.is_valid }">
<option value=""></option>
<option value="北京">北京</option>
<option value="上海">上海</option>
<option value="天津">天津</option>
<option value="重庆">重庆</option>
<option value="江苏">江苏</option>
<option value="浙江">浙江</option>
<option value="安徽">安徽</option>
<option value="广东">广东</option>
<option value="山东">山东</option>
<option value="四川">四川</option>
<option value="湖北">湖北</option>
<option value="湖南">湖南</option>
<option value="山西">山西</option>
<option value="陕西">陕西</option>
<option value="辽宁">辽宁</option>
<option value="吉林">吉林</option>
<option value="黑龙江">黑龙江</option>
<option value="内蒙古">内蒙古</option>
<option value="河南">河南</option>
<option value="河北">河北</option>
<option value="广西">广西</option>
<option value="贵州">贵州</option>
<option value="云南">云南</option>
<option value="西藏">西藏</option>
<option value="青海">青海</option>
<option value="新疆">新疆</option>
<option value="甘肃">甘肃</option>
<option value="宁夏">宁夏</option>
<option value="江西">江西</option>
<option value="海南">海南</option>
<option value="福建">福建</option>
<option value="台湾">台湾</option>
</select>
<div class="validation" v-show="!validations.state.is_valid">{{ validations.state.text
}}
</div>
</div>
<div class="large-6 medium-6 small-12 cell">
<label class="form-label">邮编</label>
<input type="text" class="form-input" v-model="zip"
v-bind:class="{'invalid' : !validations.zip.is_valid }"/>
<div class="validation" v-show="!validations.zip.is_valid">{{ validations.zip.text }}
</div>
</div>
</div>
</div>
</div>
<div class="grid-x grid-padding-x">
<div class="large-8 medium-9 small-12 cell centered">
<a class="edit-location-button" v-on:click="submitEditCafe()">提交更改</a>
</div>
</div>
<div class="grid-x grid-padding-x">
<div class="large-8 medium-9 small-12 cell centered">
<a class="delete-location" v-on:click="deleteCafe()">删除这个咖啡店</a>
</div>
</div>
</div>
</div>
</transition>
</template>
<script>
import {EventBus} from '../event-bus.js';
import _ from 'lodash';
import {ROAST_CONFIG} from '../config.js';
export default {
data() {
return {
companyResults: [],
showAutocomplete: true,
companyName: '',
companyID: '',
newCompany: false,
companyType: 'roaster',
subscription: 0,
website: '',
locationName: '',
address: '',
city: '',
state: '',
zip: '',
brewMethodsSelected: [],
matcha: 0,
tea: 0,
validations: {
companyName: {
is_valid: true,
text: ''
},
website: {
is_valid: true,
text: ''
},
address: {
is_valid: true,
text: ''
},
city: {
is_valid: true,
text: ''
},
state: {
is_valid: true,
text: ''
},
zip: {
is_valid: true,
text: ''
}
}
}
},
created() {
this.$store.dispatch('loadCafeEdit', {
id: this.$route.params.id
});
},
computed: {
brewMethods() {
return this.$store.getters.getBrewMethods;
},
editCafeStatus() {
return this.$store.getters.getCafeEditStatus;
},
editCafeLoadStatus() {
return this.$store.getters.getCafeEditLoadStatus;
},
editCafe() {
return this.$store.getters.getCafeEdit;
},
editCafeText() {
return this.$store.getters.getCafeEditText;
},
deleteCafeStatus() {
return this.$store.getters.getCafeDeletedStatus;
},
deleteCafeText() {
return this.$store.getters.getCafeDeletedText;
}
},
watch: {
'editCafeStatus': function () {
if (this.editCafeStatus === 2) {
EventBus.$emit('show-success', {
notification: this.editCafeText
});
this.$router.push({name: 'cafe', params: {id: this.$route.params.id}});
}
},
'editCafeLoadStatus': function () {
if (this.editCafeLoadStatus === 2) {
this.populateForm();
}
},
'deleteCafeStatus': function () {
if (this.deleteCafeStatus === 2) {
this.$router.push({name: 'cafes'});
EventBus.$emit('show-success', {
notification: this.deleteCafeText
});
}
}
},
methods: {
setCompanyType(type) {
this.companyType = type;
},
toggleSelectedBrewMethod(id) {
if (this.brewMethodsSelected.indexOf(id) >= 0) {
this.brewMethodsSelected.splice(this.brewMethodsSelected.indexOf(id), 1);
} else {
this.brewMethodsSelected.push(id);
}
},
searchCompanies: _.debounce(function (e) {
if (this.companyName.length > 1) {
this.showAutocomplete = true;
axios.get(ROAST_CONFIG.API_URL + '/companies/search', {
params: {
search: this.companyName
}
}).then(function (response) {
this.companyResults = response.data.companies;
}.bind(this));
}
}, 300),
// 渲染表单
populateForm() {
this.companyName = this.editCafe.company.name;
this.companyID = this.editCafe.company.id;
this.newCompany = false;
this.companyType = this.editCafe.company.roaster == 1 ? 'roaster' : 'cafe';
this.subscription = this.editCafe.company.subscription;
this.website = this.editCafe.company.website;
this.locationName = this.editCafe.location_name;
this.address = this.editCafe.address;
this.city = this.editCafe.city;
this.state = this.editCafe.state;
this.zip = this.editCafe.zip;
this.matcha = this.editCafe.matcha;
this.tea = this.editCafe.tea;
for (let i = 0; i < this.editCafe.brew_methods.length; i++) {
this.brewMethodsSelected.push(this.editCafe.brew_methods[i].id);
}
this.showAutocomplete = false;
},
// 提交更改
submitEditCafe() {
if (this.validateEditCafe()) {
this.$store.dispatch('editCafe', {
id: this.editCafe.id,
company_name: this.companyName,
company_id: this.companyID,
company_type: this.companyType,
subscription: this.subscription,
website: this.website,
location_name: this.locationName,
address: this.address,
city: this.city,
state: this.state,
zip: this.zip,
brew_methods: this.brewMethodsSelected,
matcha: this.matcha,
tea: this.tea
});
}
},
addNewCompany() {
this.showAutocomplete = false;
this.newCompany = true;
this.companyResults = [];
},
selectCompany(company) {
this.showAutocomplete = false;
this.companyName = company.name;
this.companyID = company.id;
this.newCompany = false;
this.companyResults = [];
},
validateEditCafe() {
let validNewCafeForm = true;
if (this.companyName.trim() === '') {
validNewCafeForm = false;
this.validations.companyName.is_valid = false;
this.validations.companyName.text = '请输入公司名称';
} else {
this.validations.companyName.is_valid = true;
this.validations.companyName.text = '';
}
if (this.website.trim !== '' && !this.website.match(/((([A-Za-z]{3,9}:(?:\/\/)?)(?:[\-;:&=\+\$,\w]+@)?[A-Za-z0-9\.\-]+|(?:www\.|[\-;:&=\+\$,\w]+@)[A-Za-z0-9\.\-]+)((?:\/[\+~%\/\.\w\-_]*)?\??(?:[\-\+=&;%@\.\w_]*)#?(?:[\.\!\/\\\w]*))?)/)) {
validNewCafeForm = false;
this.validations.website.is_valid = false;
this.validations.website.text = '请输入有效的网址信息';
} else {
this.validations.website.is_valid = true;
this.validations.website.text = '';
}
if (this.address.trim() === '') {
validNewCafeForm = false;
this.validations.address.is_valid = false;
this.validations.address.text = '请输入咖啡店地址';
} else {
this.validations.address.is_valid = true;
this.validations.address.text = '';
}
if (this.city.trim() === '') {
validNewCafeForm = false;
this.validations.city.is_valid = false;
this.validations.city.text = '请输入咖啡店所在城市';
} else {
this.validations.city.is_valid = true;
this.validations.city.text = '';
}
if (this.state.trim() === '') {
validNewCafeForm = false;
this.validations.state.is_valid = false;
this.validations.state.text = '请输入咖啡店所在省份/直辖市';
} else {
this.validations.state.is_valid = true;
this.validations.state.text = '';
}
if (this.zip.trim() === '' || !this.zip.match(/(^\d{6}$)/)) {
validNewCafeForm = false;
this.validations.zip.is_valid = false;
this.validations.zip.text = '请输入咖啡店所在地区邮政编码';
} else {
this.validations.zip.is_valid = true;
this.validations.zip.text = '';
}
return validNewCafeForm;
},
deleteCafe() {
if (confirm('确定要删除这个咖啡店吗?')) {
this.$store.dispatch('deleteCafe', {
id: this.editCafe.id
});
}
},
clearForm() {
this.companyResults = [];
this.companyName = '';
this.companyID = '';
this.newCompany = false;
this.companyType = 'roaster';
this.subscription = 0;
this.website = '';
this.locationName = '';
this.address = '';
this.city = '';
this.state = '';
this.zip = '';
this.brewMethodsSelected = [];
this.matcha = 0;
this.tea = 0;
this.validations = {
companyName: {
is_valid: true,
text: ''
},
website: {
is_valid: true,
text: ''
},
address: {
is_valid: true,
text: ''
},
city: {
is_valid: true,
text: ''
},
state: {
is_valid: true,
text: ''
},
zip: {
is_valid: true,
text: ''
}
};
}
}
}
</script>
具体的实现逻辑和新增咖啡店一致,不再赘述细节,需要注意的一点是我们将删除咖啡店的逻辑也放到这个页面里面,在「提交更改」按钮下面有一个「点击删除咖啡店」链接,点击该链接会触发 Vuex 中的删除 Action 执行删除咖啡店请求。
下面我们到后端编写更新咖啡店业务逻辑代码。
第六步:实现后端更新咖啡店逻辑
首先,要创建一个新的表单请求验证类:
php artisan make:request EditCafeRequest
编写新生成的 app/Http/Requests/EditCafeRequest.php
代码如下:
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class EditCafeRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'company_name' => 'required_without:company_id',
'address' => 'required',
'city' => 'required',
'state' => 'required',
'zip' => 'required',
'website' => 'sometimes|url'
];
}
/**
* Get the error messages for the defined validation rules.
*
* @return array
*/
public function messages()
{
return [
'company_name.required_without' => '咖啡店所属公司名称不能为空',
'address' => ['required' => '街道地址不能为空'],
'city' => ['required' => '城市字段不能为空'],
'state' => ['required' => '省份字段不能为空'],
'zip' => [
'required' => '邮编字段不能为空'
],
'website.url' => '请输入有效的网址信息'
];
}
}
然后和新增咖啡店一样,我们先在 app/Services/CafeService.php
中实现编辑咖啡店方法:
/**
* 更新咖啡店数据
* @param $id
* @param $data
* @param $updatedBy
* @return mixed
*/
public function editCafe($id, $data, $updatedBy)
{
// 如果选择已有的公司,则更新公司信息,否则新增
if (isset($data['company_id'])) {
$company = Company::where('id', '=', $data['company_id'])->first();
if (isset($data['company_name'])) {
$company->name = $data['company_name'];
}
if (isset($data['company_type'])) {
$company->roaster = $data['company_type'] == 'roaster' ? 1 : 0;
}
if (isset($data['subscription'])) {
$company->subscription = $data['subscription'];
}
if (isset($data['website'])) {
$company->website = $data['website'];
}
$company->logo = '';
$company->description = '';
$company->save();
} else {
$company = new Company();
if (isset($data['company_name'])) {
$company->name = $data['company_name'];
}
if (isset($data['company_type'])) {
$company->roaster = $data['company_type'] == 'roaster' ? 1 : 0;
} else {
$company->roaster = 0;
}
if (isset($data['subscription'])) {
$company->subscription = $data['subscription'];
}
if (isset($data['website'])) {
$company->website = $data['website'];
}
$company->logo = '';
$company->description = '';
$company->added_by = $updatedBy;
$company->save();
}
$cafe = Cafe::where('id', '=', $id)->first();
if (isset($data['city_id'])) {
$cityID = $data['city_id'];
} else {
$cityID = $cafe->city_id;
}
if (isset($data['address'])) {
$address = $data['address'];
} else {
$address = $cafe->address;
}
if (isset($data['city'])) {
$city = $data['city'];
} else {
$city = $cafe->city;
}
if (isset($data['state'])) {
$state = $data['state'];
} else {
$state = $cafe->state;
}
if (isset($data['zip'])) {
$zip = $data['zip'];
} else {
$zip = $cafe->zip;
}
if (isset($data['location_name'])) {
$locationName = $data['location_name'];
} else {
$locationName = $cafe->location_name;
}
if (isset($data['brew_methods'])) {
$brewMethods = $data['brew_methods'];
}
$coordinates = GaodeMaps::geocodeAddress($address, $city, $state);
$lat = $coordinates['lat'];
$lng = $coordinates['lng'];
$cafe->company_id = $company->id;
if (!$cityID) {
$cityID = GaodeMaps::findClosestCity($city, $lat, $lng);
}
$cafe->city_id = $cityID;
$cafe->location_name = $locationName != null ? $locationName : '';
$cafe->address = $address;
$cafe->city = $city;
$cafe->state = $state;
$cafe->zip = $zip;
$cafe->latitude = $lat;
$cafe->longitude = $lng;
if (isset($data['matcha'])) {
$cafe->matcha = $data['matcha'];
}
if (isset($data['tea'])) {
$cafe->tea = $data['tea'];
}
$cafe->save();
// 更新关联的冲泡方法
if (isset($data['brew_methods'])) {
$cafe->brewMethods()->sync(json_decode($brewMethods));
}
return $cafe;
}
然后在控制器 CafesController
中调用这个方法,同时将注入依赖调整为 EditCafeRequest
:
// 更新咖啡店数据
public function putEditCafe($id, EditCafeRequest $request)
{
$cafe = Cafe::where('id', '=', $id)->with('brewMethods')->first();
$cafeService = new CafeService();
$updatedCafe = $cafeService->editCafe($cafe->id, $request->all(), Auth::user()->id);
$company = Company::where('id', '=', $updatedCafe->company_id)
->with('cafes')
->first();
return response()->json($company, 200);
}
这样,就完成了编辑咖啡店和删除咖啡店的所有功能代码编写,运行 npm run dev
重新编译前端资源,直接访问某个咖啡店的编辑链接,如 http://roast.test/#/cafes/5/edit
,就可以看到编辑页面了(当然需要登录后才能访问):
将页面拉到最下面,就可以看到删除链接:
点击删除链接,会有一个确认提示框,点击确定即可删除这个咖啡店,并跳转到应用首页:
如果你执行的是更新操作,更新完成后页面会跳转到咖啡店详情页,关于咖啡店详情页,我们将在下一篇教程中进行重构,并将编辑咖啡店的入口链接放到详情页中显示。
注:项目完整代码可在 nonfu/roastapp 中查看。
4 Comments
请教一下单页面应用会导致js文件过大从而首次加载太慢,有什么好的解决方案么
可以对单页内部路由进行拆包 实现按需加载 当进入特定路由才加载该路由的 JavaScript 文件 从而减少首屏的加载时间 同时 服务器端还可以通过渲染一部分首屏页面数据 避免造成首页产生过多请求
是路由懒加载的意思么,我用了nginx的gzip原本需要15s加载js文件现在大概需要4-5s左右
你的js这么大 如果是在生产环境的话 通过CDN加载吧 文件太大也会受服务器带宽影响