↓↓クリックして頂けると励みになります。
【57 | 配達依頼を受けた時の連動】 << 【ホーム】 >> 【59 | Heroku デプロイ】
PWA(Progressive Web App)は、ウェブアプリケーションの一種であり、モバイルデバイスやデスクトップコンピューターで動作するための新しいアプローチです。
PWAは、モバイルアプリケーションのような機能をウェブアプリケーションに取り入れることができます。
何でもいいので、「logo.png」ファイルを「core/static/img」フォルダに入れておいてください。
「core/static」フォルダに「manifest.json」ファイルを新規作成します。
新規作成 【Desktop/crowdsource/core/static/manifest.json】
{ "short_name": "Crowd Source", "name": "ようこそ! クラウドソーシングアプリへ", "description": "商品を低価格でお届けします", "icons": [ { "src": "/static/img/logo.png", "type": "image/png", "sizes": "512x512" } ], "start_url": "/courier/", "display": "standalone" }
「core/templates/base.html」を編集します。
記述編集 【Desktop/crowdsource/core/templates/base.html】
{% load static %} <!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width,initial-scale=1" /> <title>クラウドソーシングアプリ</title> <link rel="shortcut icon" href="{% static 'img/logo.png' %}"> {% load bootstrap4 %} {% bootstrap_css %} {% bootstrap_javascript jquery='full' %} <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.9.0/css/all.css"> {% block head %}{% endblock %} </head> <body> <nav class="navbar {% if not request.user.is_authenticated %} navbar-expand-lg {% endif %} navbar-dark bg-dark"> <a class="navbar-brand" href="/">クラウドソーシングアプリ</a> {% if not request.user.is_authenticated %} <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarSupportedContent"> <ul class="navbar-nav"> <li class="nav-item mr-2 ml-4 {% if request.GET.next != '/courier/' %} active {% endif %}"> <a class="nav-link btn btn-light {% if request.GET.next != '/courier/' %}bg-success{% else %}bg-dark{% endif %}" href="/sign-in/?next=/customer/">依頼人</a> </li> <li class="nav-item {% if request.GET.next == '/courier/' %} active {% endif %}"> <a class="nav-link btn btn-light {% if request.GET.next == '/courier/' %}bg-primary{% else %}bg-dark{% endif %}" href="/sign-in/?next=/courier/">配達人</a> </li> </ul> </div> {% else %} <form class="form-inline"> <span class="mr-4 text-light"> {{ request.user.get_full_name | title }} </span> <span> <a href="/sign-out" class="btn btn-outline-dark bg-light">ログアウト</a> </span> </form> {% endif %} </nav> {% block content %}{% endblock %} <footer class="text-center mt-5 mb-5"> © クラウドソーシング </footer> <script src="https://unpkg.com/bootoast@1.0.1/dist/bootoast.min.js"></script> <link rel="stylesheet" href="https://unpkg.com/bootoast@1.0.1/dist/bootoast.min.css"> <script> function toast(message, type) { bootoast.toast({ position: 'leftBottom', message, type: 'success', animationDuration: 300, dismissible: true, }); } {% if messages %} {% for message in messages %} toast('{{ message }}', '{{ message.tags }}'); {% endfor %} {% endif %} </script> </body> </html>
「core/templates/courier/base.html」ファイルを編集します。
記述編集 【Desktop/crowdsource/core/templates/courier/base.html】
{% load static %} <!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width,initial-scale=1" /> <title>配達人 | クラウドソーシングアプリ</title> <link rel="shortcut icon" href="{% static 'img/logo.png' %}"> <link rel="apple-touch-icon" sizes="180x180" href="{% static 'img/logo.png' %}"> <link rel="manifest" href="{% static 'manifest.json' %}"> {% load bootstrap4 %} {% bootstrap_css %} {% bootstrap_javascript jquery='full' %} {% include 'load_firebase.html' %} <script> const messaging = firebase.messaging(); messaging .getToken({ <!-- ご自分のkey pairを入れてください --> validKey: "BNKwo-bS7wQjKLaB4RkxL6Ta1lkbk-cJ7BBaw8FW-9Wim8fRY7HHkgXv9yLR8FxNvqh9qv59zfswrlSZDci2LSQ" }) .then((currentToken) => { console.log(currentToken); if (currentToken) { fetch('{% url "courier:fcm_token_update_api" %}?fcm_token=' + currentToken); } }) .catch((err) => { console.log('トークンの取得中にエラーが発生しました。', err); }) </script> <script> let vh = window.innerHeight * 0.01; document.documentElement.style.setProperty('--vh', `${vh}px`); </script> <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.9.0/css/all.css"> <style> #content { height: calc(var(--vh, 1vh) * 100); } </style> {% block head %}{% endblock %} </head> <body> <div id="content"> {% block content %}{% endblock %} </div> <script src="https://unpkg.com/bootoast@1.0.1/dist/bootoast.min.js"></script> <link rel="stylesheet" href="https://unpkg.com/bootoast@1.0.1/dist/bootoast.min.css"> <script> function toast(message, type) { bootoast.toast({ position: 'centerBottom', message, type, }); } {% if messages %} {% for message in messages %} toast('{{ message }}', '{{ message.tags }}'); {% endfor %} {% endif %} </script> </body> </html>
Visual Studio Codeのターミナルで「bash」を起動します。
「ngrok」を起動します。
コマンド
./ngrok http 8000
「core/templates/courier/current_job.html」ファイルを編集します。
記述編集 【Desktop/crowdsource/core/templates/courier/current_job.html】65行目
{% extends 'courier/base.html' %} {% load static %} {% block head %} <script src="https://maps.googleapis.com/maps/api/js?key={{ GOOGLE_MAP_API_KEY }}&callback=initMap&libraries=places&v=weekly" defer></script> <script> var pickupLat = parseFloat(" {{ job.pickup_lat }} "); var pickupLng = parseFloat(" {{ job.pickup_lng }} "); var deliveryLat = parseFloat(" {{ job.delivery_lat }} "); var deliveryLng = parseFloat(" {{ job.delivery_lng }} "); function initMap() { if (!document.getElementById("map")) { return; } const directionsService = new google.maps.DirectionsService(); const directionsRenderer = new google.maps.DirectionsRenderer(); const map = new google.maps.Map(document.getElementById("map"), { zoom: 7, center: { lat: 43.062087, lng: 141.354404 }, }); directionsRenderer.setMap(map); calculateAndDisplayRoute(map, directionsService, directionsRenderer); } function calculateAndDisplayRoute(map, directionsService, directionsRenderer) { directionsService.route( { origin: new google.maps.LatLng(pickupLat, pickupLng), destination: new google.maps.LatLng(deliveryLat, deliveryLng), travelMode: google.maps.TravelMode.DRIVING, }, (response, status) => { if (status === "OK") { new google.maps.DirectionsRenderer({ map: map, directions: response, suppressMarkers: true, polylineOptions: { strokeColor: "red", strokeWeight: 5, strokeOpacity: 0.8 } }); var leg = response.routes[0].legs[0]; new google.maps.Marker({ position: leg.start_location, map: map, icon: "{% static 'img/start.png' %}" }); new google.maps.Marker({ position: leg.end_location, map: map, icon: "{% static 'img/end.png' %}" }); updateCourierPosition(map); } else { window.alert("Directions request failed due to " + status); } } ); } function updateCourierPosition(map) { const jobSocket = new WebSocket( "ws{% if request.get_host != 'localhost:8000' %}s{% endif %}://" + window.location.host + "/ws/jobs/{{ job.id }}/" ); navigator.geolocation.watchPosition( pos => { var courierPostion = new google.maps.LatLng(pos.coords.latitude, pos.coords.longitude); if (!window.courierMarker) { window.courierMarker = new google.maps.Marker({ position: courierPostion, map, icon: "{% static 'img/courier.png' %}" }); } else { window.courierMarker.setPosition(courierPostion); } map.panTo(courierPostion); try { jobSocket.send(JSON.stringify({ job: { courier_lat: pos.coords.latitude, courier_lng: pos.coords.longitude, } })) } catch (error) { console.log(error); } }, pos => console.log(pos)) } </script> <style> #map { flex: 1; } small { font-size: 12px; line-height: 1.2rem; } .card { border: none; } </style> {% endblock %} {% block content %} <div class="d-flex flex-column h-100" style="padding-bottom: 60px"> <div class="text-center"> <div class="btn-group mt-1 mb-1 align-item-center" role="group"> <a href="{% url 'courier:current_job' %}" class="btn btn-info">進行中の配達</a> <a href="{% url 'courier:archived_jobs' %}" class="btn btn-outline-info">終了した配達</a> </div> </div> {% if job %} <div id="map"></div> <div class="card"> <div class="card-body p-2"> <div class="media"> <img src="{{ job.photo.url }}" class="rounded-lg mr-3" width="50px" height="50px"> <div class="media-body"> <b>{{ job.name }}</b> <div class="d-flex"> <div class="flex-grow-1 mr-2"> <small class="text-success"> <i class="fas fa-car"></i> <span>{{ job.distance }}</span> km <i class="far fa-clock ml-2"></i> <span>{{ job.duration }}</span> 分 </small> <div class="d-flex align-items-center mt-2"> <i class="fas fa-map-marker-alt"></i> <small class="text-secondary ml-2">{{ job.pickup_address }}</small> </div> <div class="d-flex align-items-center mt-2"> <i class="fas fa-flag-checkered"></i> <small class="text-secondary ml-2">{{ job.delivery_address }}</small> </div> </div> <h3>{{ job.price }}</h3>円 </div> </div> </div> <a href="{% url 'courier:current_job_take_photo' job.id %}" class="btn btn-block btn-info btn-md mt-3"> {% if job.status == 'picking' %}配達開始の写真を撮影{% else %}配達完了の写真を撮影{% endif %} </a> </div> </div> {% else %} <div id="main" class="text-center"> <p> 現在、何も配達依頼を受けていません。<br/>配達依頼を選んで受けてみましょう。 </p> </div> {% endif %} </div> {% include 'courier/bottom_tabs.html' %} {% endblock %}
「core/templates/customer/job.html」ファイルを編集します。
記述編集 【Desktop/crowdsource/core/templates/customer/job.html】
{% extends 'customer/base.html' %} {% load static %} {% block head %} <style> .photo { object-fit: cover; } .photo-blank { border: 2px dashed #DFDFDF; height: 130px; width: 130px; border-radius: 5px; align-items: center; display: flex; justify-content: center; text-align: center; padding: 10px; } </style> <script src="https://maps.googleapis.com/maps/api/js?key={{ GOOGLE_MAP_API_KEY }}&callback=initMap&libraries=places&v=weekly" defer></script> <script> var pickupLat = parseFloat(" {{ job.pickup_lat }} "); var pickupLng = parseFloat(" {{ job.pickup_lng }} "); var deliveryLat = parseFloat(" {{ job.delivery_lat }} "); var deliveryLng = parseFloat(" {{ job.delivery_lng }} "); var courierLat = parseFloat(" {{ job.courier.lat }} "); var courierLng = parseFloat(" {{ job.courier.lng }} "); function initMap() { const directionsService = new google.maps.DirectionsService(); const directionsRenderer = new google.maps.DirectionsRenderer(); const map = new google.maps.Map(document.getElementById("map"), { zoom: 7, center: { lat: 43.062087, lng: 141.354404 }, }); directionsRenderer.setMap(map); calculateAndDisplayRoute(map, directionsService, directionsRenderer); } function calculateAndDisplayRoute(map, directionsService, directionsRenderer) { directionsService.route( { origin: new google.maps.LatLng(pickupLat, pickupLng), destination: new google.maps.LatLng(deliveryLat, deliveryLng), travelMode: google.maps.TravelMode.DRIVING, }, (response, status) => { if (status === "OK") { new google.maps.DirectionsRenderer({ map: map, directions: response, suppressMarkers: true, polylineOptions: { strokeColor: "#000", strokeWeight: 5, strokeOpacity: 0.8 } }); var leg = response.routes[0].legs[0]; new google.maps.Marker({ position: leg.start_location, map: map, icon: "{% static 'img/start.png' %}" }); new google.maps.Marker({ position: leg.end_location, map: map, icon: "{% static 'img/end.png' %}" }); window.courierMarker = new google.maps.Marker({ position: new google.maps.LatLng(courierLat, courierLng), map, icon: '/static/img/courier.png', }) } else { window.alert("リクエストは次の理由で失敗しました:" + status); } } ); } const jobSocket = new WebSocket( "ws{% if request.get_host != 'localhost:8000' %}s{% endif %}://" + window.location.host + "/ws/jobs/{{ job.id }}/" ); // このページが WebSocket 経由でイベントを受信するたびにこの関数を実行します jobSocket.onmessage = function (e) { var data = JSON.parse(e.data); var job = data.job; console.log(job); if (job.courier_lat && job.courier_lng) { var courierPosition = new google.maps.LatLng(job.courier_lat, job.courier_lng); window.courierMarker.setPosition(courierPosition); } if (job.status) { $("#job_status").html(job.status); $("form").css("display", "none"); //配達依頼が配達人に受けられた時、キャンセルボタンを消す } if (job.pickup_photo) { $("#pickup_photo").html('<img src="' + job.pickup_photo + '" class="rounded-lg photo" width="130" height="130">'); } if (job.delivery_photo) { $("#delivery_photo").html('<img src="' + job.delivery_photo + '" class="rounded-lg photo" width="130" height="130">'); } } </script> {% endblock %} {% block main %} <!-- JOB DESCRIPTION --> <div class="media mb-4"> <img src="{{ job.photo.url }}" class="rounded-lg mr-3" width="150" height="150"> <div class="media-body"> {% if job.status == 'processing' %} <form method="POST" class="float-right"> {% csrf_token %} <button type="submit" class="btn btn-warning">配達依頼をキャンセル</button> </form> {% endif %} <h4>{{ job.name }}</h4> <p class="text-secondary">{{ job.description }}</p> <div class="row"> <div class="col-lg-3"> <small class="text-secondary">カテゴリー</small><br /> <span><b>{{ job.category.name }}</b></span> </div> <div class="col-lg-3"> <small class="text-secondary">サイズ</small><br /> <span><b>{{ job.get_size_display }}</b></span> </div> <div class="col-lg-3"> <small class="text-secondary">料金</small><br /> <span><b>{{ job.price }}円</b></span> </div> <div class="col-lg-3"> <small class="text-secondary">数量</small><br /> <span><b>{{ job.quantity }}</b></span> </div> </div> </div> </div> <!-- DELIVERY INFORMATION --> <b class="text-secondary">配達依頼情報</b><br /> <div class="card bg-white mt-2 mb-5"> <div class="card-body p-4"> <h4 class="mb-3"> 荷物受取先 </h4> <div class="row"> <div class="col-lg-4"> <b>荷物受取先住所</b><br /> <span>{{ job.pickup_address }}</span> </div> <div class="col-lg-4"> <b>{{ job.pickup_name }}</b><br /> <span>{{ job.pickup_phone }}</span> </div> <div id="pickup_photo" class="col-lg-4"> {% if job.pickup_photo %} <img src="{{ job.pickup_photo.url }}" class="rounded-lg photo" width="130" height="130"> {% else %} <div class="photo-blank">写真があればここに表示されます。</div> {% endif %} </div> </div> <hr class="my-4" /> <h4 class="mb-3"> 配達先 </h4> <div class="row"> <div class="col-lg-4"> <b>配達先住所</b><br /> <span>{{ job.delivery_address }}</span> </div> <div class="col-lg-4"> <b>{{ job.delivery_name }}</b><br /> <span>{{ job.delivery_phone }}</span> </div> <div id="delivery_photo" class="col-lg-4"> {% if job.delivery_photo %} <img src="{{ job.delivery_photo.url }}" class="rounded-lg photo" width="130" height="130"> {% else %} <div class="photo-blank">写真があればここに表示されます。</div> {% endif %} </div> </div> </div> </div> <!-- マップ --> <div class="d-flex justify-content-between"> <b class="text-secondary">配達の追跡</b> <div> <span id="job_status" class="badge badge-warning"> {% if job.get_status_display == 'Processing' %} 進行中 {{ job.get_status_display }} {% else %} 完了 {{ job.get_status_display }} {% endif %} </span> </div> </div> <div class="card bg-white mt-2"> <div class="card-body p-0"> <div id="map" style="height: 500px;"></div> </div> </div> {% endblock %}
ngrokのURLで配達依頼人を表示させます。
次にスマートフォンのChromeでngrokのURLを開きます。
配達人でログインします。
ホーム画面にURLを登録すると、登録したアイコンでショートカットを作ることができます。
写真を撮影すれば、リアルタイムで連動します。
一通り動作を確認してください。
↓↓クリックして頂けると励みになります。
【57 | 配達依頼を受けた時の連動】 << 【ホーム】 >> 【59 | Heroku デプロイ】