【Vue.js・PHP】メールフォームをつくってみる

Vue.jsとPHPを使用して、メールフォームを作ってみます。

基本的な機能としては、

  1. いたずら防止のために簡単にtokenが一致すれば送信できる
  2. 項目は、名前・電話番号・メールアドレス・メディアを入力できます。
  3. 名前・電話番号・メールアドレスが空または正確でない場合、バリデーション表示
  4. ファイルはドラッグアンドドロップでアップロードで複数送信できます。
  5. 送信ボタンは名前・電話番号・メールアドレスが正確であれば表示
  6. 送信ボタンをクリックすると、送信中を表示
  7. 送信が成功するとフォームを非表示にして、お問い合わせありがとうございます。の文言表示。失敗すると失敗しましたの表示。
  8. ファイルを後から×ボタンをクリックで削除可能

 
 

プレビュー

1、初期画面

2,すべて入力した後の画面

3、送信中の画面

4、送信成功

 入力側PHP


<?php
unset($_SESSION['csrf_token']);
session_start();
$toke_byte = openssl_random_pseudo_bytes(16);
$csrf_token = bin2hex($toke_byte);
$_SESSION['csrf_token'] = $csrf_token;
?>
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>メールフォーム</title>
    <script src="https://unpkg.com/vue@2.5.17"></script>
    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
    <style>
       
    </style>
</head>
<body>
    <div id="app">
        <h2>お問い合わせ</h2>
        <Mail-Form></Mail-Form>
    </div>
</body>
</html>

JavaScript


        var MailForm = {
            data: function () {
                return {
                    name: '',
                    tel: '',
                    email: '',
                    result: '',
                    param: {},
                    params: {},
                    isEnter: false,
                    files: [],
                    sendClick:false
                }
            },
            template: `<div>
            <form class="contact-form" method="post" action="">
            <table>
                <tbody>
                    <tr>
                        <th>お名前</th>
                        <td>
                            <input v-model="name" placeholder="例)山本 太郎">
                            <p class="error">{{ errors.name }}</p>
                        </td>
                    </tr>
                    <tr>
                        <th>電話番号</th>
                        <td>
                            <input v-model="tel" placeholder="例)08012345678" maxlength="11">
                            <p class="error">{{ errors.tel }}</p>
                        </td>
                    </tr>
                    <tr>
                        <th>メールアドレス</th>
                        <td>
                            <input v-model="email" placeholder="例)info@example.co.jp">
                            <p class="error">{{ errors.email }}</p>
                        </td>
                    </tr>
                </tbody>
            </table>
            <div
                class="drop_area"
                @dragenter="dragEnter"
                @dragleave="dragLeave"
                @dragover.prevent
                @drop.prevent="dropFile($event)"
                :class="{enter: isEnter}"
                >
                    アップロードエリア
                </div>
            <div>
            <ul>
                <li v-for="(file, index) in files":key="index">{{ file.name }}<span @click="FileClear(index)" class="batsu">×</span></li>
            </ul>
            </div>
            <button v-if="valid" type="button" @click="submit">メール送信</button>
            <p v-show="sendClick">送信中。。。</p>
        </form>
        <div v-if="result" class="contact-result">{{ result }}</div>
        </div>`,//コンポーネントの要素は一つでなければならない為、divで囲って一つにしています。
            computed: {
                chackName: function () {
                    if (!this.name) {
                        return '名前を入力してください';
                    }
                    return '';
                },
                checkTel: function () {
                    if (!this.tel) {
                        return '電話番号を入力してください';
                    } else if (!this.validTel(this.tel)) {
                        return '電話番号が正しくありません。';
                    }
                    return '';
                },
                chackEmail: function () {
                    if (!this.email) {
                        return 'メールアドレスを入力してください';
                    } else if (!this.validEmail(this.email)) {
                        return 'メールアドレスを正しくありません。';
                    }
                    return '';
                },
                errors: function () {
                    const errors = {
                        'name': this.chackName,
                        'tel': this.checkTel,
                        'email': this.chackEmail,
                    };
                    for (var key in errors) {
                        if (errors[key] === '' || errors[key] === null || errors[key] === undefined) {
                            delete errors[key];
                        }
                    }
                    return errors;
                },
                valid: function () {
                    return !Object.keys(this.errors).length;
                }
            },
            methods: {
                submit: async function () {
                    this.sendClick =true;
                    const result = await this.send();
                    valid =false;
                    if (result === '送信完了') {
                        this.name = '';
                        this.tel = '';
                        this.email = '';
                        this.files = [];
                        document.querySelector('.contact-form').style.display = 'none';
                        this.result = 'お問い合わせありがとうございました。';
                    }
                },
                send: async function () {
                    const url = '【送信側PHPのリンク】';
                    const token = "<?php echo $csrf_token; ?>";
                    this.param = {
                        'name': this.name,
                        'tel': this.tel,
                        'email': this.email,
                        'files': this.files
                    }
                    console.log(this.files);
                    this.params = {
                        'param': this.param,
                        'token': token
                    }
                    return await axios
                        .post(url, this.params)
                        .then(function (res) {
                            console.log(res.data);
                            return res.data;
                        })
                        .catch(function (error) {
                            console.log(error);
                        });
                },
                validTel: function (tel) {
                    const re = /^(0[5-9]0[0-9]{8}|0[1-9][1-9][0-9]{7})$/;
                    return re.test(tel);
                },
                validEmail: function (email) {
                    const re = /^[a-zA-Z0-9_+-]+(.[a-zA-Z0-9_+-]+)*@([a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]*\.)+[a-zA-Z]{2,}$/;
                    return re.test(email);
                },
                dropFile(event) {
                    const files = event.target.files ? event.target.files : event.dataTransfer.files;
                    const reader = new FileReader();
                    let name = '';
                    name = files[0].name;
                    const fileData = {
                        'name': name
                    }
                    reader.onload = (event) => {
                        preview = event.target.result;
                        fileData['filePath'] = preview;
                    };
                    reader.readAsDataURL(files[0]);
                    this.files.push(fileData);
                    this.isEnter = false;
                },
                dragEnter() {
                    this.isEnter = true;
                },
                dragLeave() {
                    this.isEnter = false;
                },
                FileClear(index){
                    this.files.splice(index,1);
                }
            }
        }
        new Vue({
            el: '#app',
            components: {
                MailForm: MailForm
            }
        });

CSS


*{
            padding: 0;
            margin: 0;
            list-style: none;
        }
        html,
        body {
            height: 100%;
        }
       
        body {
            display: flex;
            justify-content: center;
            align-items: center;
        }
        h2{
            text-align: center;
            margin-bottom: 50px;
        }
        .drop_area {
            color: gray;
            font-weight: bold;
            font-size: 1.2em;
            display: flex;
            justify-content: center;
            align-items: center;
            width: 500px;
            height: 300px;
            border: 5px solid gray;
            border-radius: 15px;
            margin-bottom: 50px;
        }
        .error {
            color: red;
        }
        button {
            appearance: none;
            border: 0;
            border-radius: 5px;
            background: #4676D7;
            color: #fff;
            padding: 8px 16px;
            font-size: 16px;
            cursor: pointer;
        }
        input {
            width: 100%;
            padding: 10px 15px;
            font-size: 16px;
            border-radius: 3px;
            border: 2px solid #ddd;
            box-sizing: border-box;
            margin-top: 15px;
        }
        .contact-form{
            text-align: center;
        }
        .contact-form table{
            margin-bottom: 50px;
        }
        .contact-form th {
            text-align: left;
            padding-top: 13px;
            padding-right: 15px;
        }
        .batsu{
            cursor: pointer;
        }

送信側PHP


<?php
    mb_language("Japanese");
    mb_internal_encoding("UTF-8");
    $to = '【送り先のメールアドレス】';
    $title = '【タイトル】';
    $from = '【送信元メールアドレス】';
    //ヘッダー設定
    $header = "Content-Type: multipart/mixed;boundary=\"__BOUNDARY__\"\n";
    $header .= "Return-Path: " . $to . " \n";
    $header .= "From: " . $from ." \n";
    $header .= "Sender: " . $from ." \n";
    $header .= "Reply-To: " . $to . " \n";
    //アクセスを許可するオリジンを指定する場合、指定したいURLを設定
    $header .= "Access-Control-Allow-Origin: *";
    $json = file_get_contents("php://input");
    $inputs = json_decode($json, true);
    $bodys = $inputs["param"];
    //本文
    $content = "--__BOUNDARY__\n";
    $content .= "Content-Type: text/plain; charset=\"ISO-2022-JP\"\n\n";
    $content .= "名前:". $bodys["name"] ."さんからのお問い合わせ". "\n";
    $content .= "電話番号:". $bodys["tel"] . "\n";
    $content .= "メールアドレス:". $bodys["email"] . "\n";
    $content .= "--__BOUNDARY__\n";
    foreach($bodys["files"] as $file){
    $content .= "Content-Type: application/octet-stream; name=\"{$file['name']}\"\n";
    $content .= "Content-Disposition: attachment; filename=\"{$file['name']}\"\n";
    $content .= "Content-Transfer-Encoding: base64\n";
    $content .= "\n";
    $content .= chunk_split(base64_encode(file_get_contents($file['filePath'])));
    $content .= "--__BOUNDARY__\n";
    }
    session_start();
    if (isset($inputs["token"]) && $inputs["token"] === $_SESSION['csrf_token']) {
        if (mb_send_mail($to, $title, $content, $header)) {
            echo "送信完了";
        } else {
            echo "送信に失敗しました。";
        }
    } else {
        echo "不正なリクエストです";
    }
   
?>

投稿者 PASOMEN

関連投稿