NodeおよびVueのWebアプリケーション、パート5:プロジェクトの完成

Node.js、Vue.js、およびMongoDBに基づいたWebソリューションを開発するためのガイドの第5部の翻訳は次のとおりです。 第1部第2部第3部、および第4部では、Budget Managerアプリケーションのクライアント部分とサーバー部分の段階的な作成について説明しました。 この資料の著者が最終的に何になったかを実際に見るのが待ちきれない人は、 こちらをご覧ください さらに、 ここに GitHubプロジェクトリポジトリがあります。 あなたが強い型付けを好む人の一人であるなら、 ここここがBudget ManagerをTypeScriptに移植した結果です。







本日、このトレーニングプロジェクトの作業は完了します。 つまり、この記事では、新しい顧客の記録と財務書類をシステムに追加するページの開発と、このデータを編集するメカニズムの作成について説明します。 ここでは、APIの改善点をいくつか見て、Budget Managerを稼働状態にします。



APIの改訂



まず、 models



フォルダーに移動し、 budget.js



ファイルを開きます。 モデルのdescription



フィールドを追加します。



 description: {   type: String,   required: true },
      
      





app/api



フォルダーに移動し、その中にあるbudget.js



ファイルを開きます。 ここでは、新しいドキュメントが正しく処理されるようにデータストレージ関数store



を編集し、ドキュメントを編集できるedit



機能を追加し、ドキュメントを削除するために必要なremove



関数を追加し、ドキュメントをフィルター処理できるgetByState



関数を追加します。 完全なファイルコードを次に示します。 表示するには、対応するブロックを展開します。 将来、同じ方法で大きなコードフラグメントが発行されます。



ソースコード
 const mongoose = require('mongoose'); const api = {}; api.store = (User, Budget, Client, Token) => (req, res) => { if (Token) {   Client.findOne({ _id: req.body.client }, (error, client) => {     if (error) res.status(400).json(error);     if (client) {       const budget = new Budget({         client_id: req.body.client,         user_id: req.query.user_id,         client: client.name,         state: req.body.state,         description: req.body.description,         title: req.body.title,         total_price: req.body.total_price,         items: req.body.items       });       budget.save(error => {         if (error) return res.status(400).json(error)         res.status(200).json({ success: true, message: "Budget registered successfully" })       })     } else {       res.status(400).json({ success: false, message: "Invalid client" })     }   }) } else return res.status(401).send({ success: false, message: 'Unauthorized' }); } api.getAll = (User, Budget, Token) => (req, res) => { if (Token) {   Budget.find({ user_id: req.query.user_id }, (error, budget) => {     if (error) return res.status(400).json(error);     res.status(200).json(budget);     return true;   }) } else return res.status(403).send({ success: false, message: 'Unauthorized' }); } api.getAllFromClient = (User, Budget, Token) => (req, res) => { if (Token) {   Budget.find({ client_id: req.query.client_id }, (error, budget) => {     if (error) return res.status(400).json(error);     res.status(200).json(budget);     return true;   }) } else return res.status(401).send({ success: false, message: 'Unauthorized' }); } api.index = (User, Budget, Client, Token) => (req, res) => { if (Token) {   User.findOne({ _id: req.query.user_id }, (error, user) => {     if (error) res.status(400).json(error);     if (user) {       Budget.findOne({ _id: req.query._id }, (error, budget) => {         if (error) res.status(400).json(error);         res.status(200).json(budget);       })     } else {       res.status(400).json({ success: false, message: "Invalid budget" })     }   }) } else return res.status(401).send({ success: false, message: 'Unauthorized' }); } api.edit = (User, Budget, Client, Token) => (req, res) => { if (Token) {   User.findOne({ _id: req.query.user_id }, (error, user) => {     if (error) res.status(400).json(error);     if (user) {       Budget.findOneAndUpdate({ _id: req.body._id }, req.body, (error, budget) => {         if (error) res.status(400).json(error);         res.status(200).json(budget);       })     } else {       res.status(400).json({ success: false, message: "Invalid budget" })     }   }) } else return res.status(401).send({ success: false, message: 'Unauthorized' }); } api.getByState = (User, Budget, Client, Token) => (req, res) => { if (Token) {   User.findOne({ _id: req.query.user_id }, (error, user) => {     if (error) res.status(400).json(error);     if (user) {       Budget.find({ state: req.query.state }, (error, budget) => {         console.log(budget)         if (error) res.status(400).json(error);         res.status(200).json(budget);       })     } else {       res.status(400).json({ success: false, message: "Invalid budget" })     }   }) } else return res.status(401).send({ success: false, message: 'Unauthorized' }); } api.remove = (User, Budget, Client, Token) => (req, res) => { if (Token) {   Budget.remove({ _id: req.query._id }, (error, removed) => {     if (error) res.status(400).json(error);     res.status(200).json({ success: true, message: 'Removed successfully' });   }) } else return res.status(401).send({ success: false, message: 'Unauthorized' }); } module.exports = api;
      
      





api



フォルダーからclient.jsファイルに同様の変更を加えます。



ソースコード
 const mongoose = require('mongoose'); const api = {}; api.store = (User, Client, Token) => (req, res) => { if (Token) {   User.findOne({ _id: req.query.user_id }, (error, user) => {     if (error) res.status(400).json(error);     if (user) {       const client = new Client({         user_id: req.query.user_id,         name: req.body.name,         email: req.body.email,         phone: req.body.phone,       });       client.save(error => {         if (error) return res.status(400).json(error);         res.status(200).json({ success: true, message: "Client registration successful" });       })     } else {       res.status(400).json({ success: false, message: "Invalid client" })     }   }) } else return res.status(403).send({ success: false, message: 'Unauthorized' }); } api.getAll = (User, Client, Token) => (req, res) => { if (Token) {   Client.find({ user_id: req.query.user_id }, (error, client) => {     if (error) return res.status(400).json(error);     res.status(200).json(client);     return true;   }) } else return res.status(403).send({ success: false, message: 'Unauthorized' }); } api.index = (User, Client, Token) => (req, res) => { if (Token) {   User.findOne({ _id: req.query.user_id }, (error, user) => {     if (error) res.status(400).json(error);     if (user) {       Client.findOne({ _id: req.query._id }, (error, client) => {         if (error) res.status(400).json(error);         res.status(200).json(client);       })     } else {       res.status(400).json({ success: false, message: "Invalid client" })     }   }) } else return res.status(401).send({ success: false, message: 'Unauthorized' }); } api.edit = (User, Client, Token) => (req, res) => { if (Token) {   User.findOne({ _id: req.query.user_id }, (error, user) => {     if (error) res.status(400).json(error);     if (user) {       Client.findOneAndUpdate({ _id: req.body._id }, req.body, (error, client) => {         if (error) res.status(400).json(error);         res.status(200).json(client);       })     } else {       res.status(400).json({ success: false, message: "Invalid client" })     }   }) } else return res.status(401).send({ success: false, message: 'Unauthorized' }); } api.remove = (User, Client, Token) => (req, res) => { if (Token) {   User.findOne({ _id: req.query.user_id }, (error, user) => {     if (error) res.status(400).json(error);     if (user) {       Client.remove({ _id: req.query._id }, (error, removed) => {         if (error) res.status(400).json(error);         res.status(200).json({ success: true, message: 'Removed successfully' });       })     } else {       res.status(400).json({ success: false, message: "Invalid client" })     }   }) } else return res.status(401).send({ success: false, message: 'Unauthorized' }); } module.exports = api;
      
      





最後に、システムに新しいルートを追加します。 これを行うには、 routes



フォルダーに移動してbudget.js



ファイルを開きます。



ソースコード
 const passport = require('passport'),     config = require('@config'),     models = require('@BudgetManager/app/setup'); module.exports = (app) => { const api = app.BudgetManagerAPI.app.api.budget; app.route('/api/v1/budget')    .post(passport.authenticate('jwt', config.session), api.store(models.User, models.Budget, models.Client, app.get('budgetsecret')))    .get(passport.authenticate('jwt', config.session), api.getAll(models.User, models.Budget, app.get('budgetsecret')))    .get(passport.authenticate('jwt', config.session), api.getAllFromClient(models.User, models.Budget, app.get('budgetsecret')))    .delete(passport.authenticate('jwt', config.session), api.remove(models.User, models.Budget, models.Client, app.get('budgetsecret'))) app.route('/api/v1/budget/single')    .get(passport.authenticate('jwt', config.session), api.index(models.User, models.Budget, models.Client, app.get('budgetsecret')))    .put(passport.authenticate('jwt', config.session), api.edit(models.User, models.Budget, models.Client, app.get('budgetsecret'))) app.route('/api/v1/budget/state')    .get(passport.authenticate('jwt', config.session), api.getByState(models.User, models.Budget, models.Client, app.get('budgetsecret'))) }
      
      





同じフォルダーにあるclient.js



ファイルに同様の変更を加えます。



ソースコード
 const passport = require('passport'),     config = require('@config'),     models = require('@BudgetManager/app/setup'); module.exports = (app) => { const api = app.BudgetManagerAPI.app.api.client; app.route('/api/v1/client')    .post(passport.authenticate('jwt', config.session), api.store(models.User, models.Client, app.get('budgetsecret')))    .get(passport.authenticate('jwt', config.session), api.getAll(models.User, models.Client, app.get('budgetsecret')))    .delete(passport.authenticate('jwt', config.session), api.remove(models.User, models.Client, app.get('budgetsecret'))) app.route('/api/v1/client/single')   .get(passport.authenticate('jwt', config.session), api.index(models.User, models.Client, app.get('budgetsecret')))   .put(passport.authenticate('jwt', config.session), api.edit(models.User, models.Client, app.get('budgetsecret'))) }
      
      





APIに対して行う必要があるすべての変更です。



ルーターの改良



次に、ルートに新しいコンポーネントを追加します。 これを行うには、 router



フォルダー内にあるindex.js



ファイルを開きます。



ソースコード
 ... // Global components import Header from '@/components/Header' import List from '@/components/List/List' import Create from '@/components/pages/Create' // Register components Vue.component('app-header', Header) Vue.component('list', List) Vue.component('create', Create) Vue.use(Router) const router = new Router({ routes: [   {     path: '/',     name: 'Home',     components: {       default: Home,       header: Header,       list: List,       create: Create     }   },   {     path: '/login',     name: 'Authentication',     component: Authentication   } ] }) …
      
      





ここで、 Create



コンポーネントをインポートして定義し、 Home



ルートコンポーネントに割り当てました(以下でコンポーネントを作成します)。



新しいコンポーネントの作成



componentコンポーネントの作成



Create



コンポーネントから始めましょう。 components/pages



フォルダーに移動し、 Create.vue



新しいCreate.vue



ファイルを作成します。



ソースコード
 <template> <div class="l-create-page">   <budget-creation v-if="budgetCreation && !editPage" slot="budget-creation" :clients="clients" :saveBudget="saveBudget"></budget-creation>   <client-creation v-if="!budgetCreation && !editPage" slot="client-creation" :saveClient="saveClient"></client-creation>   <budget-edit v-else-if="budgetEdit && editPage"     slot="budget-creation"     :clients="clients"     :selectedBudget="budget"     :fixClientNameAndUpdate="fixClientNameAndUpdate">   </budget-edit>   <client-edit v-else-if="!budgetEdit && editPage"     slot="client-creation"     :selectedClient="client"     :updateClient="updateClient">   </client-edit> </div> </template> <script> import BudgetCreation from './../Creation/BudgetCreation' import ClientCreation from './../Creation/ClientCreation' import BudgetEdit from './../Creation/BudgetEdit' import ClientEdit from './../Creation/ClientEdit' export default {   props: [     'budgetCreation', 'clients', 'saveBudget',     'saveClient', 'budget', 'client', 'updateClient',     'fixClientNameAndUpdate', 'editPage', 'budgetEdit'   ],   components: {     'budget-creation': BudgetCreation,     'client-creation': ClientCreation,     'budget-edit': BudgetEdit,     'client-edit': ClientEdit   } } </script>
      
      





最初の名前付きスロットはbudget-creation



です。 これは、新しい財務ドキュメントを作成するために使用するコンポーネントを表します。 budgetCreation



プロパティbudgetCreation



true



設定され、 editPage



false



に設定されている場合にのみ表示されfalse



。すべてのクライアントとsaveBudget



メソッドをsaveBudget



ます。



2番目の名前付きスロットはclient-creation



です。 これは、新しい顧客を作成するために使用されるコンポーネントです。 budgetCreation



プロパティbudgetCreation



false



に設定され、 editPage



false



場合にのみ表示されfalse



。 ここで、saveClientメソッドを渡します。



3番目の名前付きスロットはbudget-edit



です。 これは、選択したドキュメントの編集に使用されるコンポーネントです。 editPage



editPage



true



設定されてeditPage



場合にのみbudgetEdit



されtrue



。 ここでは、すべてのクライアント、選択した財務ドキュメント、およびfixClientNameAndUpdate



メソッドを転送します。



そして最後に、顧客情報の編集に使用される最後の名前付きスロットがあります。 budgetEdit



プロパティbudgetEdit



false



に設定され、 editPage



true



設定されているtrue



されtrue



。 選択したクライアントとupdateClient



メソッドを渡します。



予算BudgetCreationコンポーネント



新しい財務ドキュメントの作成に使用されるコンポーネントを開発します。 components



フォルダーに移動して、その中に新しいフォルダーを作成し、それにCreation



という名前を付けます。 このフォルダーで、 BudgetCreation.vue



コンポーネントBudgetCreation.vue



作成しBudgetCreation.vue







コンポーネントは非常に大きいため、テンプレートから始めて段階的に分析します。



BudgetCreationコンポーネントテンプレート



コンポーネントテンプレートコードは次のとおりです
 <template> <div class="l-budget-creation">   <v-layout row wrap>     <span class="md-budget-state-hint uppercased white--text">status</span>     <v-flex xs12 md2>       <v-select         label="Status"         :items="states"         v-model="budget.state"       >       </v-select>     </v-flex>     <v-flex xs12 md9 offset-md1>       <v-select         label="Client"         :items="clients"         v-model="budget.client"         item-text="name"         item-value="_id"       >       </v-select>     </v-flex>     <v-flex xs12 md12>       <v-text-field label="Title"                     v-model="budget.title"                     required                     color="light-blue lighten-1">       </v-text-field>       <v-text-field label="Description"                     v-model="budget.description"                     textarea                     required                     color="light-blue lighten-1">       </v-text-field>     </v-flex>     <v-layout row wrap v-for="item in budget.items" class="l-budget-item" :key="item.id">       <v-flex xs12 md1>         <v-btn block dark color="red lighten-1" @click.native="removeItem(item)">Remove</v-btn>       </v-flex>       <v-flex xs12 md3 offset-md1>         <v-text-field label="Title"                       box dark                       v-model="item.title"                       required                       color="light-blue lighten-1">         </v-text-field>       </v-flex>       <v-flex xs12 md1 offset-md1>         <v-text-field label="Price"                       box dark                       prefix="$"                       v-model="item.price"                       required                       color="light-blue lighten-1">         </v-text-field>       </v-flex>       <v-flex xs12 md2 offset-md1>         <v-text-field label="Quantity"                       box dark                       min="0"                       v-model="item.quantity"                       type="number"                       required                       color="light-blue lighten-1">         </v-text-field>       </v-flex>       <v-flex xs12 md2>         <span class="md-budget-item-subtotal white--text">ITEM PRICE $ {{ item.subtotal }}</span>       </v-flex>     </v-layout>     <v-flex xs12 md2 offset-md10>       <v-btn block color="md-add-item-btn light-blue lighten-1" @click.native="addItem()">Add item</v-btn>     </v-flex>     <v-flex xs12 md2 offset-md10>       <span class="md-budget-item-total white--text">TOTAL $ {{ budget.total_price }}</span>     </v-flex>     <v-flex xs12 md2 offset-md10>       <v-btn block color="md-add-item-btn green lighten-1" @click.native="saveBudget(budget)">Save</v-btn>     </v-flex>   </v-layout> </div> </template>
      
      





ここでは、最初にv-select



要素をテンプレートに追加してドキュメントの状態を設定し、次にv-select



を使用して必要なクライアントを選択します。 次に、ドキュメントのタイトルを入力するv-text-field



と説明を表示するv-text-field



があります。



次に、 budget.items



要素をbudget.items



します。これにより、ドキュメントに要素を追加して削除する機会が与えられます。 また、 removeItem



関数を呼び出して、削除する要素を渡すことができる赤いボタンもあります。



さらに、製品名、単価、数量をそれぞれ対象とする3つのv-text-fields



があります。



シリーズの最後には単純なspan



要素があり、商品の数量と価格の積であるsubtotal



という行にsubtotal



を表示します。



製品リストの下にはさらに3つのアイテムがあります。 これは、 addItem



関数、ドキュメント内のすべての商品の合計コストを表示するspan



要素(すべての要素のsubtotal



合計)を呼び出して新しい要素を追加するために使用される青いボタン、およびドキュメントをデータベースに保存するために使用される緑のボタンsaveBudget



関数を呼び出して、保存するドキュメントをパラメーターとして渡します。



BudgetCreationコンポーネントスクリプト



BudgetCreationコンポーネントを強化するコードは次のとおりです。
 <script> export default {   props: ['clients', 'saveBudget'],   data () {     return {       budget: {         title: null,         description: null,         state: 'writing',         client: null,         get total_price () {           let value = 0           this.items.forEach(({ subtotal }) => {             value += parseInt(subtotal)           })           return value         },         items: [           {             title: null,             quantity: 0,             price: 0,             get subtotal () {               return this.quantity * this.price             }           }         ]       },       states: [         'writing', 'editing', 'pending', 'approved', 'denied', 'waiting'       ]     }   },   methods: {     addItem () {       const items = this.budget.items       const item = {         title: '',         quantity: 0,         price: 0,         get subtotal () {           return this.quantity * this.price         }       }       items.push(item)     },     removeItem (selected) {       const items = this.budget.items       items.forEach((item, index) => {         if (item === selected) {           items.splice(index, 1)         }       })     }   } } </script>
      
      





このコードでは、 clients



saveBudget



2つのプロパティを最初に取得しclients



。 これらのプロパティのソースは、 Home



コンポーネントです。



次に、データの役割を果たすオブジェクトと配列を定義します。 オブジェクトの名前はbudget



です。 ドキュメントの作成に使用され、値を追加してデータベースに保存できます。 このオブジェクトには、プロパティtitle



(title)、 description



(description)、 state



(デフォルトのstateをwriting



設定)、 client



(client)、 total_price



(ドキュメントの総コスト)、items items



配列がありitems



。 商品には、 title



(名前)、 quantity



(数量)、 price



(価格)、およびsubtotal



(小計)のプロパティがあります。



ここでは、ドキュメントのstates



の配列states



が定義されています。 その値は、ドキュメントの状態を設定するために使用されます。 これらの状態は、 writing



editing



pending



pending



approved



denied



waiting



です。



以下に、データ構造の説明の後に、 addItem



(製品を追加するための)とremoveItem



(それらを削除するための)の2つのメソッドがありaddItem







青いボタンをクリックするたびに、 addItem



メソッドがaddItem



メソッドは、要素をbudget



オブジェクト内のitems



配列に追加します。



removeItem



メソッドは反対のアクションを実行します。 つまり、赤いボタンをクリックすると、指定されたアイテムがitems



配列から削除されitems







BudgetCreationコンポーネントスタイル



問題のコンポーネントのスタイルは次のとおりです。
 <style lang="scss"> @import "./../../assets/styles"; .uppercased {   text-transform: uppercase; } .l-budget-creation {   label, input, .icon, .input-group__selections__comma, textarea {     color: #29b6f6!important;   }   .input-group__details {     &:before {       background-color: $border-color-input !important;     }   }   .input-group__input {     border-color: $border-color-input !important;     .input-group--text-field__prefix {       margin-bottom: 3px !important;     }   }   .input-group--focused {     .input-group__input {       border-color: #29b6f6!important;     }   } } .md-budget-state-hint {   margin: 10px 0;   display: block;   width: 100%; } .md-budget-state {   background-color: rgba(41, 182, 246, .6);   display: flex;   height: 35px;   width: 100%;   font-size: 14px;   align-items: center;   justify-content: center;   border-radius: 2px;   margin: 10px 0 15px; } .l-budget-item {   align-items: center; } .md-budget-item-subtotal {   font-size: 16px;   text-align: center;   display: block; } .md-budget-item-total {   font-size: 22px;   text-align: center;   display: block;   width: 100%;   margin: 30px 0 10px; } .md-add-item-btn {   margin-top: 30px !important;   display: block; } .list__tile__title, .input-group__selections {   text-transform: uppercase !important; } </style>
      
      





次に、次のコンポーネントを検討します。



▍ClientCreationコンポーネント



実際、このコンポーネントは、レビューしたばかりのBudgetCreation



コンポーネントの簡易バージョンです。 上記で行ったように、我々はそれを部分的に検討します。 BudgetCreation



コンポーネントを理解すれば、 BudgetCreation



を簡単に理解できます。



ClientCreationコンポーネントテンプレート
 <template> <div class="l-client-creation">   <v-layout row wrap>     <v-flex xs12 md4>       <v-text-field label="Name"                     v-model="client.name"                     required                     color="green lighten-1">       </v-text-field>     </v-flex>     <v-flex xs12 md3 offset-md1>       <v-text-field label="Email"                     v-model="client.email"                     required                     color="green lighten-1">       </v-text-field>     </v-flex>     <v-flex xs12 md3 offset-md1>       <v-text-field label="Phone"                     v-model="client.phone"                     required                     mask="phone"                     color="green lighten-1">       </v-text-field>     </v-flex>     <v-flex xs12 md2 offset-md10>       <v-btn block color="md-add-item-btn green lighten-1" @click.native="saveClient(client)">Save</v-btn>     </v-flex>   </v-layout> </div> </template>
      
      





ClientCreationコンポーネントスクリプト
 <script> export default {   props: ['saveClient'],   data () {     return {       client: {         name: null,         email: null,         phone: null       }     }   } } </script>
      
      





ClientCreationコンポーネントスタイル
 <style lang="scss"> @import "./../../assets/styles"; .uppercased {   text-transform: uppercase; } .l-client-creation {   label, input, .icon, .input-group__selections__comma, textarea {     color: #66bb6a!important;   }   .input-group__details {     &:before {       background-color: $border-color-input !important;     }   }   .input-group__input {     border-color: $border-color-input !important;     .input-group--text-field__prefix {       margin-bottom: 3px !important;     }   }   .input-group--focused {     .input-group__input {       border-color: #66bb6a!important;     }   } } </style>
      
      





次に、 BudgetEdit



コンポーネントのBudgetEdit



です。



▍BudgetEditコンポーネント



実際、このコンポーネントは、すでに考慮されているBudgetCreation



コンポーネントの修正バージョンです。 そのコンポーネントを検討してください。



BudgetEditコンポーネントテンプレート
 <template> <div class="l-budget-creation">   <v-layout row wrap>     <span class="md-budget-state-hint uppercased white--text">status</span>     <v-flex xs12 md2>       <v-select         label="Status"         :items="states"         v-model="budget.state"       >       </v-select>     </v-flex>     <v-flex xs12 md9 offset-md1>       <v-select         label="Client"         :items="clients"         v-model="budget.client_id"         item-text="name"         item-value="_id"       >       </v-select>     </v-flex>     <v-flex xs12 md12>       <v-text-field label="Title"                     v-model="budget.title"                     required                     color="light-blue lighten-1">       </v-text-field>       <v-text-field label="Description"                     v-model="budget.description"                     textarea                     required                     color="light-blue lighten-1">       </v-text-field>     </v-flex>     <v-layout row wrap v-for="item in budget.items" class="l-budget-item" :key="item.id">       <v-flex xs12 md1>         <v-btn block dark color="red lighten-1" @click.native="removeItem(item)">Remove</v-btn>       </v-flex>       <v-flex xs12 md3 offset-md1>         <v-text-field label="Title"                       box dark                       v-model="item.title"                       required                       color="light-blue lighten-1">         </v-text-field>       </v-flex>       <v-flex xs12 md1 offset-md1>         <v-text-field label="Price"                       box dark                       prefix="$"                       v-model="item.price"                       required                       color="light-blue lighten-1">         </v-text-field>       </v-flex>       <v-flex xs12 md2 offset-md1>         <v-text-field label="Quantity"                       box dark                       min="0"                       v-model="item.quantity"                       type="number"                       required                       color="light-blue lighten-1">         </v-text-field>       </v-flex>       <v-flex xs12 md2>         <span class="md-budget-item-subtotal white--text">ITEM PRICE $ {{ item.subtotal }}</span>       </v-flex>     </v-layout>     <v-flex xs12 md2 offset-md10>       <v-btn block color="md-add-item-btn light-blue lighten-1" @click.native="addItem()">Add item</v-btn>     </v-flex>     <v-flex xs12 md2 offset-md10>       <span class="md-budget-item-total white--text">TOTAL $ {{ budget.total_price }}</span>     </v-flex>     <v-flex xs12 md2 offset-md10>       <v-btn block color="md-add-item-btn green lighten-1" @click.native="fixClientNameAndUpdate(budget)">Update</v-btn>     </v-flex>   </v-layout> </div> </template>
      
      





BudgetEdit



BudgetCreation



コンポーネントテンプレートの唯一の違いは、保存ボタンとその関連ロジックです。 つまり、 BudgetCreation



ではSave



と表示され、 saveBudget



メソッドが呼び出されSave



BudgetEdit



このボタンに碑文のUpdate



が表示され、 fixClientNameAndUpdate



メソッドがfixClientNameAndUpdate



ます。



BudgetCreationコンポーネントスクリプト
 <script> export default {   props: ['clients', 'fixClientNameAndUpdate', 'selectedBudget'],   data () {     return {       budget: {         title: null,         description: null,         state: 'pending',         client: null,         get total_price () {           let value = 0           this.items.forEach(({ subtotal }) => {             value += parseInt(subtotal)           })           return value         },         items: [           {             title: null,             quantity: 0,             price: null,             get subtotal () {               return this.quantity * this.price             }           }         ]       },       states: [         'writing', 'editing', 'pending', 'approved', 'denied', 'waiting'       ]     }   },   mounted () {     this.parseBudget()   },   methods: {     addItem () {       const items = this.budget.items       const item = {         title: '',         quantity: 0,         price: 0,         get subtotal () {           return this.quantity * this.price         }       }       items.push(item)     },     removeItem (selected) {       const items = this.budget.items       items.forEach((item, index) => {         if (item === selected) {           items.splice(index, 1)         }       })     },     parseBudget () {       for (let key in this.selectedBudget) {         if (key !== 'total' && key !== 'items') {           this.budget[key] = this.selectedBudget[key]         }         if (key === 'items') {           const items = this.selectedBudget.items           const buildItems = item => ({             title: item.title,             quantity: item.quantity,             price: item.price,             get subtotal () {               return this.quantity * this.price             }           })           const parseItems = items => items.map(buildItems)           this.budget.items = parseItems(items)         }       }     }   } } </script>
      
      





すべて3つのプロパティで始まります。 これらは、 clients



fixClientNameAndUpdate



、およびselectedBudget



です。 このデータは、 BudgetCreation



コンポーネントのデータと同じです。 つまり、 Budget



オブジェクトとstates



配列がありstates







さらに、ここでは、 mounted



コンポーネントライフサイクルイベントハンドラーを確認できます。このハンドラーでは、 parseBudget



メソッドを呼び出します。 最後に、 BudgetCreation



コンポーネントでおなじみのaddItem



removeItem



メソッド、および新しいparseBudget



メソッドを含むmethods



オブジェクトがありaddItem



。 このメソッドは、 budget



オブジェクトの値をselectedBudget



プロパティで渡される値に設定するために使用selectedBudget



ますが、ドキュメントの商品の小計とドキュメントの合計金額の計算にも使用されます。



BudgetCreationコンポーネントスタイル
 <style lang="scss"> @import "./../../assets/styles"; .uppercased {   text-transform: uppercase; } .l-budget-creation {   label, input, .icon, .input-group__selections__comma, textarea {     color: #29b6f6!important;   }   .input-group__details {     &:before {       background-color: $border-color-input !important;     }   }   .input-group__input {     border-color: $border-color-input !important;     .input-group--text-field__prefix {       margin-bottom: 3px !important;     }   }   .input-group--focused {     .input-group__input {       border-color: #29b6f6!important;     }   } } .md-budget-state-hint {   margin: 10px 0;   display: block;   width: 100%; } .md-budget-state {   background-color: rgba(41, 182, 246, .6);   display: flex;   height: 35px;   width: 100%;   font-size: 14px;   align-items: center;   justify-content: center;   border-radius: 2px;   margin: 10px 0 15px; } .l-budget-item {   align-items: center; } .md-budget-item-subtotal {   font-size: 16px;   text-align: center;   display: block; } .md-budget-item-total {   font-size: 22px;   text-align: center;   display: block;   width: 100%;   margin: 30px 0 10px; } .md-add-item-btn {   margin-top: 30px !important;   display: block; } .list__tile__title, .input-group__selections {   text-transform: uppercase !important; } </style>
      
      





▍ClientEditコンポーネント



検討したばかりのこのコンポーネントは、クライアントの作成に使用する対応するコンポーネントClientCreation



似ています。 主な違いは、 saveClient



メソッドの代わりにupdateClient



メソッドがupdateClient



です。 デバイスコンポーネントClientEdit



検討してください。



ClientEditコンポーネントテンプレート
 <template> <div class="l-client-creation">   <v-layout row wrap>     <v-flex xs12 md4>       <v-text-field label="Name"                     v-model="client.name"                     required                     color="green lighten-1">       </v-text-field>     </v-flex>     <v-flex xs12 md3 offset-md1>       <v-text-field label="Email"                     v-model="client.email"                     required                     color="green lighten-1">       </v-text-field>     </v-flex>     <v-flex xs12 md3 offset-md1>       <v-text-field label="Phone"                     v-model="client.phone"                     required                     mask="phone"                     color="green lighten-1">       </v-text-field>     </v-flex>     <v-flex xs12 md2 offset-md10>       <v-btn block color="md-add-item-btn green lighten-1" @click.native="updateClient(client)">Update</v-btn>     </v-flex>   </v-layout> </div> </template>
      
      





ClientEditコンポーネントスクリプト
 <script> export default {   props: ['updateClient', 'selectedClient'],   data () {     return {       client: {         name: null,         email: null,         phone: null       }     }   },   mounted () {     this.client = this.selectedClient   } } </script>
      
      





ClientEditコンポーネントスタイル
 <style lang="scss"> @import "./../../assets/styles"; .uppercased {   text-transform: uppercase; } .l-client-creation {   label, input, .icon, .input-group__selections__comma, textarea {     color: #66bb6a!important;   }   .input-group__details {     &:before {       background-color: $border-color-input !important;     }   }   .input-group__input {     border-color: $border-color-input !important;     .input-group--text-field__prefix {       margin-bottom: 3px !important;     }   }   .input-group--focused {     .input-group__input {       border-color: #66bb6a!important;     }   } } </style>
      
      





これにより、新しいコンポーネントの作成が完了し、システムに既に存在していたコンポーネントの処理に進みます。



既存のコンポーネントの改良



これで、既存のコンポーネントにいくつかの変更を加えるだけで、アプリケーションを使用する準備が整います。



ListBody



コンポーネントから始めましょう。



▍ListBodyコンポーネント



ListBodyコンポーネントテンプレート



このコンポーネントのコードはListBody.vue



ファイルに保存されていることを思い出してください



ソースコード
 <template> <section class="l-list-body">   <div class="md-list-item"        v-if="data != null && parsedBudgets === null"        v-for="item in data">     <div :class="budgetsVisible ? 'md-budget-info white--text' : 'md-client-info white--text'"           v-for="info in item"           v-if="info != item._id && info != item.client_id">       {{ info }}     </div>     <div :class="budgetsVisible ? 'l-budget-actions white--text' : 'l-client-actions white--text'">       <v-btn small flat color="yellow accent-1" @click.native="getItemAndEdit(item)">         <v-icon>mode_edit</v-icon>       </v-btn>       <v-btn small flat color="red lighten-1" @click.native="deleteItem(item, data, budgetsVisible)">         <v-icon>delete_forever</v-icon>       </v-btn>     </div>   </div>   <div class="md-list-item"        v-if="parsedBudgets !== null"        v-for="item in parsedBudgets">     <div :class="budgetsVisible ? 'md-budget-info white--text' : 'md-client-info white--text'"           v-for="info in item"           v-if="info != item._id && info != item.client_id">       {{ info }}     </div>     <div :class="budgetsVisible ? 'l-budget-actions white--text' : 'l-client-actions white--text'">       <v-btn small flat color="yellow accent-1" @click.native="getItemAndEdit(item)">         <v-icon>mode_edit</v-icon>       </v-btn>       <v-btn small flat color="red lighten-1" @click.native="deleteItem(item, data, budgetsVisible)">         <v-icon>delete_forever</v-icon>       </v-btn>     </div>   </div> </section> </template>
      
      





このコンポーネントでは、いくつかの変更と追加を実行するだけで済みます。 そのため、最初にmd-list-item



ブロックのv-if



コンストラクトに新しい条件を追加します。



 parsedBudgets === null
      
      





さらに、編集ボタンをクリックしてドキュメントを表示できるため、ドキュメントを表示するために使用した最初のボタンを削除します。



ここでは、 getItemAndEdit



メソッドを新しい最初のボタンに、 deleteItem



メソッドを最後のボタンに追加して、この要素、データ、およびbudgetsVisible



変数をパラメーターとしてbudgetsVisible



ます。



この下にはmd-item-list



ブロックがあり、検索後にフィルター処理されたドキュメントのリストを表示するために使用します。



ListBodyコンポーネントスクリプト
 <script> export default {   props: ['data', 'budgetsVisible', 'deleteItem', 'getBudget', 'getClient', 'parsedBudgets'],   methods: {     getItemAndEdit (item) {       !item.phone ? this.getBudget(item) : this.getClient(item)     }   } } </script>
      
      





このコンポーネントでは、多くのプロパティを取得します。 それらについて説明します。





コンポーネントにはgetItemAndEdit



1つだけあります。 彼は、パラメータとして要素を受け取り、電話番号を含む要素のプロパティの分析に基づいて、要素が顧客のカードであるか財務書類であるかを決定します。



ListBodyコンポーネントスタイル
 <style lang="scss"> @import "./../../assets/styles"; .l-list-body {   display: flex;   flex-direction: column;   .md-list-item {     width: 100%;     display: flex;     flex-direction: column;     margin: 15px 0;     @media (min-width: 960px) {       flex-direction: row;       margin: 0;     }     .md-budget-info {       flex-basis: 25%;       width: 100%;       background-color: rgba(0, 175, 255, 0.45);       border: 1px solid $border-color-input;       padding: 0 15px;       display: flex;       height: 35px;       align-items: center;       justify-content: center;       &:first-of-type, &:nth-of-type(2) {         text-transform: capitalize;       }       &:nth-of-type(3) {         text-transform: uppercase;       }       @media (min-width: 601px) {         justify-content: flex-start;       }     }     .md-client-info {       @extend .md-budget-info;       background-color: rgba(102, 187, 106, 0.45)!important;       &:nth-of-type(2) {         text-transform: none;       }     }     .l-budget-actions {       flex-basis: 25%;       display: flex;       background-color: rgba(0, 175, 255, 0.45);       border: 1px solid $border-color-input;       align-items: center;       justify-content: center;       .btn {         min-width: 45px !important;         margin: 0 5px !important;       }     }     .l-client-actions {       @extend .l-budget-actions;       background-color: rgba(102, 187, 106, 0.45)!important;     }   } } </style>
      
      





ListBody



, Header



.



▍ Header



Header
 <template> <header class="l-header-container">   <v-layout row wrap :class="budgetsVisible ? 'l-budgets-header' : 'l-clients-header'">     <v-flex xs12 md5>       <v-text-field v-model="searchValue"                     label="Search"                     append-icon="search"                     :color="budgetsVisible ? 'light-blue lighten-1' : 'green lighten-1'">       </v-text-field>     </v-flex>     <v-flex xs12 offset-md1 md1>       <v-btn block              :color="budgetsVisible ? 'light-blue lighten-1' : 'green lighten-1'"              @click.native="$emit('toggleVisibleData')">              {{ budgetsVisible ? "Clients" : "Budgets" }}       </v-btn>     </v-flex>     <v-flex xs12 offset-md1 md2>       <v-select label="Status"                 :color="budgetsVisible ? 'light-blue lighten-1' : 'green lighten-1'"                 v-model="status"                 :items="statusItems"                 single-line                 @change="selectState">       </v-select>     </v-flex>     <v-flex xs12 offset-md1 md1>       <v-btn block color="red lighten-1 white--text" @click.native="submitSignout()">Sign out</v-btn>     </v-flex>   </v-layout> </header> </template>
      
      





, , v-model



searchValue



.



, v-select



, change



selectState



.



Header
 <script> import Authentication from '@/components/pages/Authentication' export default {   props: ['budgetsVisible', 'selectState', 'search'],   data () {     return {       searchValue: '',       status: '',       statusItems: [         'all', 'approved', 'denied', 'waiting', 'writing', 'editing'       ]     }   },   watch: {     'searchValue': function () {       this.$emit('input', this.searchValue)     }   },   created () {     this.searchValue = this.search   },   methods: {     submitSignout () {       Authentication.signout(this, '/login')     }   } } </script>
      
      





selectState



, , search



, . search



searchValue



statusItems



.



Header
 <script> import Authentication from '@/components/pages/Authentication' export default {   props: ['budgetsVisible', 'selectState', 'search'],   data () {     return {       searchValue: '',       status: '',       statusItems: [         'all', 'approved', 'denied', 'waiting', 'writing', 'editing'       ]     }   },   watch: {     'searchValue': function () {       this.$emit('input', this.searchValue)     }   },   created () {     this.searchValue = this.search   },   methods: {     submitSignout () {       Authentication.signout(this, '/login')     }   } } </script>
      
      





Header



, Home



.



▍ Home



Home
 <template> <main class="l-home-page">   <app-header :budgetsVisible="budgetsVisible"     @toggleVisibleData="budgetsVisible = !budgetsVisible; budgetCreation = !budgetCreation"     :selectState="selectState"     :search="search"     v-model="search">   </app-header>   <div class="l-home">     <h4 class="white--text text-xs-center my-0">       Focus Budget Manager     </h4>     <list v-if="listPage">       <list-header slot="list-header" :headers="budgetsVisible ? budgetHeaders : clientHeaders"></list-header>       <list-body slot="list-body"                  :budgetsVisible="budgetsVisible"                  :data="budgetsVisible ? budgets : clients"                  :search="search"                  :deleteItem="deleteItem"                  :getBudget="getBudget"                  :getClient="getClient"                  :parsedBudgets="parsedBudgets">       </list-body>     </list>     <create v-else-if="createPage"       :budgetCreation="budgetCreation"       :budgetEdit="budgetEdit"       :editPage="editPage"       :clients="clients"       :budget="budget"       :client="client"       :saveBudget="saveBudget"       :saveClient="saveClient"       :fixClientNameAndUpdate="fixClientNameAndUpdate"       :updateClient="updateClient">     </create>   </div>   <v-snackbar :timeout="timeout"               bottom="bottom"               :color="snackColor"               v-model="snackbar">     {{ message }}   </v-snackbar>   <v-fab-transition>     <v-speed-dial v-model="fab"                   bottom                   right                   fixed                   direction="top"                   transition="scale-transition">         <v-btn slot="activator"                color="red lighten-1"                dark                fab                v-model="fab">               <v-icon>add</v-icon>               <v-icon>close</v-icon>         </v-btn>         <v-tooltip left>           <v-btn color="light-blue lighten-1"                  dark                  small                  fab                  slot="activator"                  @click.native="budgetCreation = true; listPage = false; editPage = false; createPage = true">                 <v-icon>assignment</v-icon>           </v-btn>           <span>Add new Budget</span>         </v-tooltip>         <v-tooltip left>           <v-btn color="green lighten-1"                  dark                  small                  fab                  slot="activator"                  @click.native="budgetCreation = false; listPage = false; editPage = false; createPage = true">                 <v-icon>account_circle</v-icon>           </v-btn>           <span>Add new Client</span>         </v-tooltip>         <v-tooltip left>           <v-btn color="purple lighten-2"                  dark                  small                  fab                  slot="activator"                  @click.native="budgetCreation = false; listPage = true; budgetsVisible = true">                 <v-icon>assessment</v-icon>           </v-btn>           <span>List Budgets</span>         </v-tooltip>         <v-tooltip left>           <v-btn color="deep-orange lighten-2"                  dark                  small                  fab                  slot="activator"                  @click.native="budgetCreation = false; listPage = true; budgetsVisible = false;">                 <v-icon>supervisor_account</v-icon>           </v-btn>           <span>List Clients</span>         </v-tooltip>     </v-speed-dial>   </v-fab-transition> </main> </template>
      
      





, . budgetsVisible



, selectState



, search



toggleVisibleData



, , toggleVisibleData



, v-model



search



.



list



v-if



, , . , list-body



.



create



, list



, , . , .



v-fab-transition



, , .



Home
 <script> import Axios from 'axios' import Authentication from '@/components/pages/Authentication' import ListHeader from './../List/ListHeader' import ListBody from './../List/ListBody' const BudgetManagerAPI = `http://${window.location.hostname}:3001` export default {   components: {     'list-header': ListHeader,     'list-body': ListBody   },   data () {     return {       parsedBudgets: null,       budget: null,       client: null,       state: null,       search: null,       budgets: [],       clients: [],       budgetHeaders: ['Client', 'Title', 'Status', 'Actions'],       clientHeaders: ['Client', 'Email', 'Phone', 'Actions'],       budgetsVisible: true,       snackbar: false,       timeout: 6000,       message: '',       fab: false,       listPage: true,       createPage: true,       editPage: false,       budgetCreation: true,       budgetEdit: true,       snackColor: 'red lighten-1'     }   },   mounted () {     this.getAllBudgets()     this.getAllClients()     this.hidden = false   },   watch: {     'search': function () {       if (this.search !== null || this.search !== '') {         const searchTerm = this.search         const regex = new RegExp(`^(${searchTerm})`, 'g')         const results = this.budgets.filter(budget => budget.client.match(regex))         this.parsedBudgets = results       } else {         this.parsedBudgets = null       }     }   },   methods: {     getAllBudgets () {       Axios.get(`${BudgetManagerAPI}/api/v1/budget`, {         headers: { 'Authorization': Authentication.getAuthenticationHeader(this) },         params: { user_id: this.$cookie.get('user_id') }       }).then(({data}) => {         this.budgets = this.dataParser(data, '_id', 'client', 'title', 'state', 'client_id')       }).catch(error => {         this.errorHandler(error)       })     },     getAllClients () {       Axios.get(`${BudgetManagerAPI}/api/v1/client`, {         headers: { 'Authorization': Authentication.getAuthenticationHeader(this) },         params: { user_id: this.$cookie.get('user_id') }       }).then(({data}) => {         this.clients = this.dataParser(data, 'name', 'email', '_id', 'phone')       }).catch(error => {         this.errorHandler(error)       })     },     getBudget (budget) {       Axios.get(`${BudgetManagerAPI}/api/v1/budget/single`, {         headers: { 'Authorization': Authentication.getAuthenticationHeader(this) },         params: {           user_id: this.$cookie.get('user_id'),           _id: budget._id         }       }).then(({data}) => {         this.budget = data         this.enableEdit('budget')       }).catch(error => {         this.errorHandler(error)       })     },     getClient (client) {       Axios.get(`${BudgetManagerAPI}/api/v1/client/single`, {         headers: { 'Authorization': Authentication.getAuthenticationHeader(this) },         params: {           user_id: this.$cookie.get('user_id'),           _id: client._id         }       }).then(({data}) => {         this.client = data         this.enableEdit('client')       }).catch(error => {         this.errorHandler(error)       })     },     enableEdit (type) {       if (type === 'budget') {         this.listPage = false         this.budgetEdit = true         this.budgetCreation = false         this.editPage = true       } else if (type === 'client') {         this.listPage = false         this.budgetEdit = false         this.budgetCreation = false         this.editPage = true       }     },     saveBudget (budget) {       Axios.post(`${BudgetManagerAPI}/api/v1/budget`, budget, {         headers: { 'Authorization': Authentication.getAuthenticationHeader(this) },         params: { user_id: this.$cookie.get('user_id') }       })       .then(res => {         this.resetFields(budget)         this.snackbar = true         this.message = res.data.message         this.snackColor = 'green lighten-1'         this.getAllBudgets()       })       .catch(error => {         this.errorHandler(error)       })     },     fixClientNameAndUpdate (budget) {       this.clients.find(client => {         if (client._id === budget.client_id) {           budget.client = client.name         }       })       this.updateBudget(budget)     },     updateBudget (budget) {       Axios.put(`${BudgetManagerAPI}/api/v1/budget/single`, budget, {         headers: { 'Authorization': Authentication.getAuthenticationHeader(this) },         params: { user_id: this.$cookie.get('user_id') }       })       .then(() => {         this.snackbar = true         this.message = 'Budget updated'         this.snackColor = 'green lighten-1'         this.listPage = true         this.budgetCreation = false         this.budgetsVisible = true         this.getAllBudgets()       })       .catch(error => {         this.errorHandler(error)       })     },     updateClient (client) {       Axios.put(`${BudgetManagerAPI}/api/v1/client/single`, client, {         headers: { 'Authorization': Authentication.getAuthenticationHeader(this) },         params: { user_id: this.$cookie.get('user_id') }       })       .then(() => {         this.snackbar = true         this.message = 'Client updated'         this.snackColor = 'green lighten-1'         this.listPage = true         this.budgetCreation = false         this.budgetsVisible = false         this.getAllClients()       })       .catch(error => {         this.errorHandler(error)       })     },     saveClient (client) {       Axios.post(`${BudgetManagerAPI}/api/v1/client`, client, {         headers: { 'Authorization': Authentication.getAuthenticationHeader(this) },         params: { user_id: this.$cookie.get('user_id') }       })       .then(res => {         this.resetFields(client)         this.snackbar = true         this.message = res.data.message         this.snackColor = 'green lighten-1'         this.getAllClients()       })       .catch(error => {         this.errorHandler(error)       })     },     deleteItem (selected, items, api) {       let targetApi = ''       api ? targetApi = 'budget' : targetApi = 'client'       Axios.delete(`${BudgetManagerAPI}/api/v1/${targetApi}`, {         headers: { 'Authorization': Authentication.getAuthenticationHeader(this) },         params: {           user_id: this.$cookie.get('user_id'),           _id: selected._id         }       })       .then(() => {         this.removeItem(selected, items)       })       .then(() => {         api ? this.getAllBudgets() : this.getAllClients()       })       .catch(error => {         this.errorHandler(error)       })     },     errorHandler (error) {       const status = error.response.status       this.snackbar = true       this.snackColor = 'red lighten-1'       if (status === 404) {         this.message = 'Invalid request'       } else if (status === 401 || status === 403) {         this.message = 'Unauthorized'       } else if (status === 400) {         this.message = 'Invalid or missing information'       } else {         this.message = error.message       }     },     removeItem (selected, items) {       items.forEach((item, index) => {         if (item === selected) {           items.splice(index, 1)         }       })     },     dataParser (targetedArray, ...options) {       let parsedData = []       targetedArray.forEach(item => {         let parsedItem = {}         options.forEach(option => (parsedItem[option] = item[option]))         parsedData.push(parsedItem)       })       return parsedData     },     resetFields (item) {       for (let key in item) {         item[key] = null         if (key === 'quantity' || key === 'price') {           item[key] = 0         }         item['items'] = []       }     },     selectState (state) {       this.state = state       state === 'all' ? this.getAllBudgets() : this.getBudgetsByState(state)     },     getBudgetsByState (state) {       Axios.get(`${BudgetManagerAPI}/api/v1/budget/state`, {         headers: { 'Authorization': Authentication.getAuthenticationHeader(this) },         params: { user_id: this.$cookie.get('user_id'), state }       }).then(({data}) => {         this.budgets = this.dataParser(data, '_id', 'client', 'title', 'state', 'client_id')       }).catch(error => {         this.errorHandler(error)       })     }   } } </script>
      
      





. .





, , , mounted



, .



, , watch



. @mrmonkeytech , ( ).



.





Home
 <style lang="scss"> @import "./../../assets/styles"; .l-home {   background-color: $background-color;   margin: 25px auto;   padding: 15px;   min-width: 272px; } .snack__content {   justify-content: center !important; } </style>
      
      





まとめ



- Budget Manager . .









, , — .

, , , , .



親愛なる読者! , ?






All Articles