iT邦幫忙

第 12 屆 iT 邦幫忙鐵人賽

DAY 12
0
Modern Web

關於我用 Laravel 寫 SPA 卻不寫 API 的那檔事系列 第 12

Day 12 Lightning 編輯個人資料

https://ithelp.ithome.com.tw/upload/images/20200929/20113602Mt0thIrs6S.jpg

註冊完帳號後,本篇要來更新資料和上傳大頭照。

帳號設定頁面

首先先新增 Controller:

php artisan make:controller User/UserController

再來開2個路由,顯示頁面和更新用戶:

routes/web.php

// User
Route::get('user/setting', 'User\UserController@edit');
Route::put('user', 'User\UserController@update');

為了方便在 Controller 裡呼叫 已登入用戶,在主要的 Controller 裡增加一個 user()

app/Http/Controllers/Controller.php

use App\User;
use Illuminate\Support\Facades\Auth;

protected function user(): ?User
{
    return Auth::user();
}

然後要輸出編輯用戶的頁面。還有在 UserController 裡都是已登入用戶才能操作,因此要在 __construct() 裡加一個 auth Middleware:

app/Http/Controllers/User/UserController.php

use App\Presenters\UserPresenter;

public function __construct()
{
    $this->middleware('auth');
}

public function edit()
{
    return Inertia::render('User/Edit', [
        'user' => UserPresenter::make($this->user())->get(),
    ]);
}

還有頁面:

resources/js/Pages/User/Edit.vue

<template>
  <div class="py-6 md:py-8">
    <form @submit.prevent="submit" class="card card-main">
      <h1 class="text-3xl font-semibold text-center">帳號設定</h1>
      <div class="w-12 mt-1 mx-auto border-b-4 border-purple-400"></div>

      <div class="grid gap-6 mt-6 md:grid-cols-2">
        <text-input v-model="form.name" :error="$page.errors.name" label="姓名" autocomplete="name" />
        <text-input v-model="form.email" :error="$page.errors.email" label="E-mail" autocomplete="email" />
        <textarea-input v-model="form.description" :error="$page.errors.description" class="md:col-span-2" label="個人簡介" />
        <text-input v-model="form.password" :error="$page.errors.password" type="password" label="密碼" />
        <text-input v-model="form.password_confirmation" type="password" label="確認密碼" />
        <file-input v-model="form.avatar" :error="$page.errors.avatar" accept="image/*" label="大頭照" browseText="選擇圖片" />
        <div class="md:col-span-2">
          <loading-button :loading="loading" class="btn btn-purple">更新帳號</loading-button>
        </div>
      </div>
    </form>
  </div>
</template>

<script>
import AppLayout from '@/Layouts/AppLayout'
import TextInput from '@/Components/TextInput'
import TextareaInput from '@/Components/TextareaInput'
import FileInput from '@/Components/FileInput'
import LoadingButton from '@/Components/LoadingButton'

export default {
  layout: AppLayout,
  metaInfo: {
    title: '帳號設定'
  },
  components: {
    TextInput,
    TextareaInput,
    FileInput,
    LoadingButton
  },
  props: {
    user: Object
  },
  data() {
    return {
      form: {
        name: this.user.name,
        email: this.user.email,
        description: this.user.description,
        password: '',
        password_confirmation: '',
        avatar: null
      },
      loading: false
    }
  },
  methods: {
    submit() {
      const data = new FormData()
      for (const key in this.form) {
        data.append(key, this.form[key] || '')
      }
      data.append('_method', 'put')

      this.$inertia.post('/user', data, {
        onStart: () => this.loading = true,
        onFinish: () => this.loading = false,
        onSuccess: () => {
          if (! Object.keys(this.$page.errors).length) {
            this.form.password = ''
            this.form.password_confirmation = ''
            this.form.avatar = null
          }
        }
      })
    }
  }
}
</script>

Inertia.js v0.3 已棄用 Promise 調用方式

現在全系列已更新為 Inertia.js v0.3,增加了 Event system (事件系統),Promise 調用的方式已棄用,若尚未更新至 v0.3 請更新版本:

yarn add @inertiajs/inertia@^0.3 @inertiajs/inertia-vue@^0.2.4

並參考 Day 09 Lightning 用戶登入 的「載入進度條」篇安裝進度條套件。

但如果你還是想要使用舊方法或者不想升級,請參考以下用法:

submit() {
  this.loading = true

  const data = new FormData()
  for (const key in this.form) {
    data.append(key, this.form[key] || '')
  }
  data.append('_method', 'put')

  this.$inertia.post('/user', data).then(() => {
    this.loading = false
    if (! Object.keys(this.$page.errors).length) {
      this.form.password = ''
      this.form.password_confirmation = ''
      this.form.avatar = null
    }
  })
}

這次多了 TextareaInputFileInput,為了不佔版面,可以直接去我的 Lightning GitHub 倉庫的 Components 裡拿。

再來要注意的是,雖然 Inertia 可以直接呼叫 this.$inertia.put(),但這裡有用 FormData 傳大頭照過去,不能用 put 方法傳送。只能跟 Laravel 一樣,要用 post_method=put

還有這次用到的 CSS。@screen 是設定斷點,算是 @media ... 的 Tailwind CSS 縮寫:

resources/css/components.css

/* Card */
...
.card-main {
  @apply max-w-screen-md p-6 mx-auto;
  @screen md {
    @apply p-8;
  }
}

resources/css/button.css

/* Card */
...
.btn-purple-light {
  @apply bg-purple-100 text-purple-700;
  &:hover {
    @apply bg-purple-200;
  }
}

.btn-red {
  @apply bg-red-500 text-white;
  &:hover {
    @apply bg-red-700;
  }
  &:disabled {
    @apply bg-red-300 !important;
  }
}
.btn-red-light {
  @apply bg-red-100 text-red-700;
  &:hover {
    @apply bg-red-200;
  }
}

然後瀏覽 /user/setting

https://ithelp.ithome.com.tw/upload/images/20200914/20113602csKVEDzQXR.jpg

最後在右上選單裡增加連結:

resources/js/Layouts/AppLayout.vue

<template #menu="{ close }">
  <dropdown-item href="/user/setting" icon="heroicons-outline:cog" @click="close">
    帳號設定
  </dropdown-item>
  <hr class="border-gray-200 my-2">
  ...
</template>

https://ithelp.ithome.com.tw/upload/images/20200914/20113602o6EzntVUcv.jpg

更新資料

表單頁面好了之後,再來是處裡後端表單驗證和儲存資料的部分。新增一個 UpdateUserRequest,把驗證表單的規則都寫在裡面:

php artisan make:request UpdateUserRequest

預設的 authorize() 不會用到,直接刪掉沒關係。然後在 rules() 裡寫驗證規則:

app/Http/Requests/UpdateUserRequest.php

use Illuminate\Validation\Rule;

public function rules()
{
    return [
        'name' => 'required|string|max:255',
        'email' => [
            'required', 'string', 'email', 'max:255',
            Rule::unique('users')->ignore($this->user()->id),
        ],
        'description' => 'nullable|string|max:100',
        'password' => 'nullable|string|min:8|confirmed',
        'avatar' => 'nullable|image|max:5120',
    ];
}

passwordavatar 欄位都是希望有輸入(或上傳檔案)才更新,沒有輸入也要可以通過驗證,這時可以使用 nullable 規則,允許 null 或不存在的值。但如果直接設 null 進資料庫會有問題,還要在 UpdateUserRequest 裡增加 validationData(),調整需要驗證的資料:

app/Http/Requests/UpdateUserRequest.php

public function validationData()
{
    $data = parent::validationData();

    if (! $data['password']) {
        unset($data['password']);
    }

    if (! $data['avatar']) {
        unset($data['avatar']);
    }

    return $data;
}

然後把 Hash 密碼和儲存上傳圖片的動作放在 User Model 裡:

app/User.php

use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Storage;

public function setPasswordAttribute($value)
{
    $this->attributes['password'] = Hash::needsRehash($value) ? Hash::make($value) : $value;
}

public function setAvatarAttribute($avatar)
{
    $this->attributes['avatar'] = $avatar instanceof UploadedFile
        ? Storage::url($avatar->store('avatars'))
        : $avatar;
}

回到 UserController 更新用戶資料。除了可以回傳 Inertia 響應 也可以重新導向。更新後還要返回表單頁 (上一頁),只要使用 back() 即可:

app/Http/Controllers/User/UserController.php

use App\Http\Requests\UpdateUserRequest;

public function update(UpdateUserRequest $request)
{
    $this->user()->update($request->validated());

    return back();
}

現在可以去修改看看用戶資料囉!但更新後沒有任何回應訊息,有點沒安全感。這裡可以用 Flash Session 功能 (只會出現一次的 Session),在 redirect 物件後串上 with()

app/Http/Controllers/User/UserController.php

public function update(UpdateUserRequest $request)
{
    ...
    return back()->with('success', '帳號更新成功');
}

然後新增成功/錯誤訊息的共享資料:

app/Providers/AppServiceProvider.php

protected function registerInertia()
{
    Inertia::share([
        ...
        'flash' => fn () => [
            'success' => session('success'),
            'error' => session('error'),
        ],
    ]);
}

新增一個 Alert 組件,這就是提示訊息用的組件:

resources/js/Components/Alert.vue

<template>
  <div v-if="type === 'success'" class="alert alert-success">
    <icon class="mr-1" icon="heroicons-outline:check-circle" />
    <slot />
  </div>
  <div v-else-if="type === 'error'" class="alert alert-error">
    <icon class="mr-1" icon="heroicons-outline:x-circle" />
    <slot />
  </div>
</template>

<script>
export default {
  props: {
    type: {
      type: String,
      default: 'success'
    }
  }
}
</script>

resources/css/components.css

/* Alert */
.alert {
  @apply flex items-center px-5 py-3 text-lg rounded-md;
}
.alert-success {
  @apply bg-green-100 text-green-700;
}
.alert-error {
  @apply bg-red-100 text-red-700;
}

引入 Alert 組件,成功訊息可以在 $page.flash.success 讀取:

resources/js/Pages/User/Edit.vue

<template>
  <div class="py-6 md:py-8">
    <form @submit.prevent="submit" class="card card-main">
      <h1 class="text-3xl font-semibold text-center">帳號設定</h1>
      <div class="w-12 mt-1 mx-auto border-b-4 border-purple-400"></div>

      <alert v-if="$page.flash.success" class="mt-6">{{ $page.flash.success }}</alert>
      ...
    </form>
    ...
  </div>
</template>

<script>
import Alert from '@/Components/Alert'

export default {
  components: {
    Alert,
    ...
  }
}
</script>

https://ithelp.ithome.com.tw/upload/images/20200914/20113602TwZ7VKhafK.jpg

總結

雖然我本來想要加上刪除帳號的篇章,但無奈...實在是太長了,只能移到下次。下篇是刪除帳號和用戶頁面,用戶功能的最後一篇。

Lightning 範例程式碼:https://github.com/ycs77/lightning


上一篇
Day 11 Lightning 用戶註冊
下一篇
Day 13 Lightning 刪除帳號 & 用戶頁面
系列文
關於我用 Laravel 寫 SPA 卻不寫 API 的那檔事30

尚未有邦友留言

立即登入留言