vue3を使用してAppleシステムのサイドメッセージプロンプト効果を模倣する

vue3を使用してAppleシステムのサイドメッセージプロンプト効果を模倣する

アニメーションプレビュー

最近、卒業プロジェクトに取り組んでいます。卒業プロジェクト システムに、Apple システムに似たサイド メッセージ プロンプト ボックスを追加したいと考えています。まずは効果を見てみましょう。

その他のUIライブラリ

フロントエンド開発に精通している学生であれば、このコンポーネントが Element UI では Notification、Bootstrap では Toasts と呼ばれていることに気付いたかもしれません。

始める

このコンポーネントを初めて見たとき、とてもクールだと思いました。今日は、これをどのように実装したかを段階的に紹介します。間違いや最適化できる部分があれば、コメントしてください。🥳(このコンポーネントはVue3に基づいて実装されています)

コンポーネントディレクトリ構造

乾杯

|

| -- index.js // コンポーネントを登録し、簡単に呼び出せるようにグローバル変数を定義します

|

| -- instance.js // 手動でインスタンスを作成する前後のロジック

|

| -- toasts.vue // メッセージプロンプト HTML 部分

|

| -- toastsBus.js // vue3 で $on と $emit を削除するソリューション

トースト

おおよそのDOM構造

<!-- ポップアップウィンドウ -->
<div class="トーストコンテナ">
    <!-- アイコン アイコン -->
    <テンプレート>
        ...
    </テンプレート>
    <!-- メインコンテンツ -->
    <div class="トーストコンテンツ">
        <!-- タイトルとカウントダウン -->
        <div class="toast-head">
            ...
        </div>
        <!-- 本文 -->
        <div class="toast-body">...</div>
        <!-- アクション ボタン -->
        <div class="toast-operate">
            ...
        </div>
    </div>
    <!-- 閉じる -->
    <div class="トーストを閉じる">
        <i class="fi fi-rr-cross-small"></i>
    </div>
</div>

インデックス

コンポーネントを登録し、グローバル変数を定義する

ここでコンポーネントを登録し、呼び出し用のグローバル変数を定義します。

'./instance' からトーストをインポートします
'./toasts.vue' から Toast をインポートします。

エクスポートデフォルト(アプリ)=> {
    // コンポーネントを登録します。app.component(Toast.name, Toast);
    // グローバル変数を登録し、$Toast({}) を呼び出すだけです。app.config.globalProperties.$Toast = toast;
}

インスタンス.js

インスタンスを手動でマウントする

🌟🌟🌟 記事全体の要点はこちら🌟🌟🌟

まず、コンポーネントをページに手動でマウントする方法を学びましょう。

'vue' から {createApp} をインポートします。
'./toasts' から Toasts をインポートします

const toasts = (オプション) => {
    // 親コンテナを作成します。let root = document.createElement('div');
    document.body.appendChild(ルート)
    // Toastsインスタンスを作成する let ToastsConstructor = createApp(Toasts, options)
    // 親要素をマウントする let instance = ToastsConstructor.mount(root)
    // インスタンス自体をvueに投げる
    インスタンスを返す
}
デフォルトのトーストをエクスポートします。

作成された各トーストの正しい配置

図に示すように、作成された各トーストは前のトーストの下に配置されます (ここでのギャップは 16 ピクセルです)。この効果を実現するには、既存のトーストの高さを知る必要があります。

// インスタンス.js

// ここで、現在有効なトーストを保存するための配列を定義する必要があります
インスタンス = []

const toasts = (オプション) => {
    ...
    // 作成後、インスタンスを配列に追加しますinstances.push(instance)
    
    // 高さをリセットします。let verticalOffset = 0
    // トラバースして、現在存在するトーストの高さとギャップの累積を取得します。instances.forEach(item => {
        垂直オフセット += item.$el.offsetHeight + 16
    })
    // 必要なギャップを蓄積する verticalOffset += 16
    // 現在のインスタンスの y 軸の長さを割り当てます。instance.toastPosition.y = verticalOffset
    ...
}
デフォルトのトーストをエクスポートします。

アクティブおよび時間指定シャットダウン機能を追加

まずここでビジネスを分析してみましょう:

  • 時間指定による終了: トーストの作成時に自動終了時間を指定すると、タイマーが終了すると自動的に閉じます。
  • アクティブクローズ: トーストを閉じるには閉じるボタンをクリックします。

これを基に、マウスがトースト内に入ったときにトーストの自動閉じを停止する(他のトーストには影響しません)ことや、マウスがトースト外に移動したときにトーストの自動閉じを再度有効にするなど、人間的な操作を追加できます。

<!-- toasts.vue -->
<テンプレート>
    <トランジション名="トースト" @after-leave="afterLeave" @after-enter="afterEnter">
        <div ref="container" class="toast-container" :style="toastStyle" v-show="visible" @mouseenter="clearTimer" @mouseleave="createTimer">
            ...
            <!-- 閉じる -->
            <div class="toast-close" @click="破壊">
                <i class="fi fi-rr-cross-small"></i>
            </div>
        </div>
    </トランジション>
</テンプレート>

<スクリプト>
'./toastsBus' から Bus をインポートします
'vue' から {ref、computed、onMounted、onBeforeUnmount} をインポートします。
エクスポートデフォルト{
    小道具: {
        // 自動シャットダウン時間(ミリ秒単位)
        自動クローズ: {
            タイプ: 数値、
            デフォルト: 4500
        }
    },
    セットアップ(props){
        // 表示するかどうか const visible = ref(false);  
        
        //トーストコンテナインスタンス const container = ref(null);
        // トースト自体の高さ const height = ref(0);
        
        // トーストの位置 const toastPosition = ref({
            x: 16,
            年: 16
        })
        const toastStyle = 計算された(()=>{
            戻る {
                上: `${toastPosition.value.y}px`,
                右: `${toastPosition.value.x}px`、
            }
        })
        
        //トーストID
        定数 id = ref('')
        
        //トーストがアニメーションを終えた後、function afterLeave(){
            // instance.js に閉じる必要があることを伝えます()
            バス.$emit('closed',id.value);
        }
        //トーストがアニメーションに入った後、function afterEnter(){
            高さ.値 = コンテナ.値.オフセット高さ
        }

        // タイマー const timer = ref(null);

        // マウスがトーストに入る
        関数clearTimer(){
             if(タイマー.値)
                clearTimeout(タイマー値)
        }
        //トーストからマウスを外す
        関数createTimer(){
           props.autoClose() の場合
                タイマー値 = setTimeout(() => {
                    表示値 = false
                }, props.autoClose)
            }
        }

        //破壊関数destruction(){
            表示値 = false
        }
        
        マウント時(()=>{
            タイマーを作成します。
        })

        マウント解除前に(()=>{
            if(タイマー.値)
                clearTimeout(タイマー値)
        })
        
        
        戻る {
            見える、
            容器、
            身長、
            トースト位置、
            トーストスタイル、
            id、
            休暇後、
            入力後、
            タイマー、
            クリアタイマー、
            作成タイマー、
            破壊
        }
    }
}
</スクリプト>

instance.js でトーストを閉じるロジックを分析してみましょう

  1. このトーストを survival 配列から削除し、配列を走査して、このトーストからトーストの位置を上方にシフトします。
  2. <body> から DOM 要素を削除します。
  3. インスタンスを破棄するには unmount() を呼び出します。

// インスタンス.js
'vue' から {createApp} をインポートします。
'./toasts' から Toasts をインポートします
'./toastsBus' から Bus をインポートします

インスタンス = []
シード = 1 とする

const toasts = (オプション) => {
    // インスタンスを手動でマウントする let ToastsConstructor = createApp(Toasts, options)
    インスタンスを ToastsConstructor.mount(root) にします。
    // インスタンスに一意の識別子を追加します。instance.id = id
    // インスタンスを表示する instance.visible = true
    
    ...
    
    // toasts.vue からの終了イベントをリッスンします。Bus.$on('closed', (id) => {
        // ここではすべての「closed」イベントが監視されるため、id が一致していることを確認する必要があります if (instance.id == id) {
            //削除ロジックを呼び出すremoveInstance(instance)
            // <body> 上の DOM 要素を削除します。 document.body.removeChild(root)
            //インスタンスを破棄します ToastsConstructor.unmount();
        }
    })
    
    インスタンスをプッシュする
    インスタンスを返す
}

デフォルトのトーストをエクスポートします。

// 削除ロジック const removeInstance = (instance) => {
    if (!インスタンス) 戻り値
    len = インスタンスの長さ
    // 破棄する必要があるインデックスを検索します const index = instances.findIndex(item => {
        item.id === instance.id を返します
    })
    // 配列からインスタンスを削除します。splice(index, 1)
    // 現在の配列にまだ残っているトーストがある場合は、次のトーストをトラバースして上に移動し、変位を再計算する必要があります if (len <= 1) return
    // 削除されたインスタンスの高さを取得します。const h = instance.height
    // 削除されたインスタンスの後に添え字が付けられたトーストを走査します
    (i = インデックスとします; i < len - 1; i++) {
        // 式: 生き残ったインスタンスは、削除された高さとそのギャップの高さをその y 軸オフセットから減算します。instances[i].toastPosition.y = parseInt(instances[i].toastPosition.y - h - 16)
    }
}

完全なコード

インデックス

'./instance' からトーストをインポートします
'./toasts.vue' から Toast をインポートします。

エクスポートデフォルト(アプリ)=> {
    Toast コンポーネントの名前。
    app.config.globalProperties.$Toast = トースト;
}

トーストBus.js

'tiny-emitter/instance' からエミッターをインポートします。

エクスポートデフォルト{
    $on: (...args) => エミッター.on(...args),
    $once: (...args) => エミッター.once(...args),
    $off: (...args) => エミッター.off(...args),
    $emit: (...引数) => エミッター.emit(...引数)
}

インスタンス.js

'vue' から {createApp} をインポートします。
'./toasts' から Toasts をインポートします
'./toastsBus' から Bus をインポートします

インスタンス = []
シード = 1 とする

const toasts = (オプション) => {
    // 親コンテナを作成する const id = `toasts_${seed++}`
    root を document.createElement('div') にします。
    root.setAttribute('データID', id)
    document.body.appendChild(ルート)
    ToastsConstructor = createApp(Toasts, オプション) を作成します。
    インスタンスを ToastsConstructor.mount(root) にします。
    インスタンスID = ID
    インスタンス.visible = true
        // 高さをリセットします。let verticalOffset = 0
    インスタンスごとに(アイテム => {
        垂直オフセット += item.$el.offsetHeight + 16
    })
    垂直オフセット += 16

    インスタンス.toastPosition.y = 垂直オフセット

    バス.$on('closed', (id) => {
        (インスタンスID == ID)の場合{
            インスタンスを削除します
            document.body.removeChild(ルート)
            ToastsConstructor.unmount();
        }
    })
    インスタンスをプッシュする
    インスタンスを返す
}

デフォルトのトーストをエクスポートします。

const removeInstance = (インスタンス) => {
    if (!インスタンス) 戻り値
    len = インスタンスの長さ
    const インデックス = インスタンス.findIndex(item => {
        item.id === instance.id を返します
    })
    インスタンス.splice(インデックス, 1)
    長さ<= 1の場合、戻り値
    定数 h = インスタンスの高さ
    (i = インデックスとします; i < len - 1; i++) {
        インスタンス[i].toastPosition.y = parseInt(インスタンス[i].toastPosition.y - h - 16)
    }
}

トースト

カスタマイズ可能なアイコンや画像、キャンセル閉じるボタン、自動閉じる時間の設定、自動閉じる機能の無効化などの詳細を少し追加します。

<テンプレート>
<トランジション名="トースト" @after-leave="afterLeave" @after-enter="afterEnter">
  <!-- ポップアップウィンドウ -->
  <div ref="container" class="toast-container" :style="toastStyle" v-show="visible" @mouseenter="clearTimer" @mouseleave="createTimer">
    <!-- アイコン -->
    <テンプレート v-if="type || type != 'カスタム' || type != 'img'">
        <div class="toast-icon success" v-if="type==='success'">
            <i class="fi fi-br-check"></i>
        </div>
        <div class="toast-icon warning" v-if="type==='warning'">
            ?
        </div>
        <div class="toast-icon info" v-if="type==='info'">
            <i class="fi fi-sr-bell-ring"></i>
        </div>
        <div class="toast-icon error" v-if="type==='error'">
            <i class="fi fi-br-cross-small"></i>
        </div>
    </テンプレート>
    <div :style="{'backgroundColor': customIconBackground}" class="toast-icon" v-if="type==='custom'" v-html="customIcon"></div>
    <img class="toast-custom-img" :src="customImg" v-if="type==='img'"/>
    <!-- コンテンツ -->
    <div class="トーストコンテンツ">
        <!-- ヘッド -->
        <div class="toast-head" v-if="title">
            <!-- タイトル -->
            <span class="toast-title">{{title}}</span>
            <!-- 時間 -->
            <span class="toast-countdown">{{countDown}}</span>
        </div>
        <!-- 本文 -->
        <div class="toast-body" v-if="message" v-html="message"></div>
        <!-- 操作 -->
        <div class="toast-operate">
            <a class="トーストボタンの確認" 
               :class="[{'成功':type==='成功'},
                        {'警告':type==='警告'},
                        {'情報':type==='情報'},
                        {'error':type==='error'}]">{{confirmText}}</a>
        </div>
    </div>
    <!-- 閉じる -->
    <div v-if="closeIcon" class="toast-close" @click="破壊">
        <i class="fi fi-rr-cross-small"></i>
    </div>
  </div>
  </トランジション>
</テンプレート>

<スクリプト>
'./toastsBus' から Bus をインポートします
'vue' から {ref、computed、onMounted、onBeforeUnmount} をインポートします。
エクスポートデフォルト{
    小道具: {
        タイトル: 文字列、
        閉じるアイコン: {
            タイプ: ブール値、
            デフォルト: true
        },
        メッセージ: 文字列、
        タイプ: {
            タイプ: 文字列、
            バリデータ: function(val) {
                ['success', 'warning', 'info', 'error', 'custom', 'img'].includes(val); を返します。
            }
        },
        確認テキスト: 文字列、
        カスタムアイコン: 文字列、
        customIconBackground: 文字列、
        customImg: 文字列、
        自動クローズ: {
            タイプ: 数値、
            デフォルト: 4500
        }
    },
    セットアップ(props){
        // 表示 const visible = ref(false);

        //コンテナインスタンス const container = ref(null);

        // 高さ const height = ref(0);

        // 位置 const toastPosition = ref({
            x: 16,
            年: 16
        })
        const toastStyle = 計算された(()=>{
            戻る {
                上: `${toastPosition.value.y}px`,
                右: `${toastPosition.value.x}px`、
            }
        })

        // カウントダウン const countDown = computed(()=>{
            '2秒前'を返す
        })

        定数 id = ref('')

        // 関数を終了した後 afterLeave(){
            バス.$emit('closed',id.value);
        }
        // 入力後 function afterEnter(){
            高さ.値 = コンテナ.値.オフセット高さ
        }

        // タイマー const timer = ref(null);

        // マウスが関数clearTimer(){に入る
             if(タイマー.値)
                clearTimeout(タイマー値)
        }
        //マウスアウト関数createTimer(){
           props.autoClose() の場合
                タイマー値 = setTimeout(() => {
                    表示値 = false
                }, props.autoClose)
            }
        }

        //破壊関数destruction(){
            表示値 = false
        }

        マウント時(()=>{
            タイマーを作成します。
        })

        マウント解除前に(()=>{
            if(タイマー.値)
                clearTimeout(タイマー値)
        })

        戻る {
            見える、
            トースト位置、
            トーストスタイル、
            カウントダウン、
            休暇後、
            入力後、
            クリアタイマー、
            作成タイマー、
            タイマー、
            破壊、
            容器、
            身長、
            id
        }
    }
}
</スクリプト>

<style lang="scss" スコープ>
//外部コンテナ.toast-container{
    幅: 330ピクセル;
    ボックスシャドウ: rgba(0, 0, 0, 0.1) 0px 2px 12px 0px;
    背景色: rgba(#F7F7F7, .6);
    境界線: 1px 実線 #E5E5E5;
    パディング: 14px 13px;
    zインデックス: 1001;
    位置: 固定;
    上: 0;
    右: 0;
    境界線の半径: 10px;
    背景フィルター: ぼかし(15px);
    ディスプレイ: フレックス;
    アイテムの位置を揃える: ストレッチ;
    遷移: すべて .3 秒の緩和;
    変更されます: 上、左;
}
// -  -  -  -  -  -  - アイコン -  -  -  -  -  -  - 
.toast-icon、.toast-close{
    フレックス収縮: 0;
}
.トーストアイコン{
    幅: 30ピクセル;
    高さ: 30px;
    境界線の半径: 100%;
    ディスプレイ: インラインフレックス;
    アイテムの位置を中央揃えにします。
    コンテンツの中央揃え: 中央;
}
// 正しい.toast-icon.success{
    背景色: rgba(#2BB44A, .15);
    色: #2BB44A;
}
//Exception.toast-icon.warning{
    背景色: rgba(#ffcc00, .15);
    色: #F89E23;
    フォントの太さ: 600;
    フォントサイズ: 18px;
}
// エラー.toast-icon.error{
    フォントサイズ: 18px;
    背景色: rgba(#EB2833, .1);
    色: #EB2833;
}
// 情報.トーストアイコン.info{
    背景色: rgba(#3E71F3, .1);
    色: #3E71F3;
}
// カスタム画像.toast-custom-img{
    幅: 40px;
    高さ: 40px;
    境界線の半径: 10px;
    オーバーフロー: 非表示;
    フレックス収縮: 0;
}
//  -  -  -  -  -  - - コンテンツ  -  -  -  -  - -
.トーストコンテンツ{
    パディング: 0 8px 0 13px;
    フレックス: 1;
}
// -  -  -  -  -  -  -  頭  -  -  -  -  -  -  - 
.トーストヘッド{
    ディスプレイ: フレックス;
    アイテムの位置を中央揃えにします。
    コンテンツの両端揃え: スペースの間;
}
//タイトル
.トーストタイトル{
    フォントサイズ: 16px;
    行の高さ: 24px;
    色: #191919;
    フォントの太さ: 600;
    オーバーフロー: 非表示;
    テキストオーバーフロー: 省略記号;
    空白: ラップなし;
}
// 時間
.トーストカウントダウン{
    フォントサイズ: 12px;
    色: #929292;
    行の高さ: 18.375px;
}
// -  -  -  -  -  -  -  体  -  -  -  -  - -
.トーストボディ{
    色: #191919;
    行の高さ: 21px;
    パディング上部: 5px;
}
// -  -  -  -  -  近い  -  -  - -
.トーストを閉じる{
    パディング: 3px;
    カーソル: ポインタ;
    フォントサイズ: 18px;
    幅: 24px;
    高さ: 24px;
    境界線の半径: 8px;
    ディスプレイ: インラインフレックス;
    アイテムの位置を中央揃えにします。
    コンテンツの中央揃え: 中央;
}
.トースト-close:hover{
    背景色: rgba(#E4E4E4, .5);
}
// --------- 操作 ----------
.トーストボタンの確認{
    フォントの太さ: 600;
    色: #3E71F3;
}
.toast-button-confirm:hover{
    色: #345ec9;
}
// 成功。トーストボタンの確認が成功{
    色: #2BB44A;
}
.toast-button-confirm.success:hover{
    色: #218a3a;
}
//Exception.toast-button-confirm.warning{
    色: #F89E23;
}
.toast-button-confirm.warning:hover{
    色: #df8f1f;
}
// 情報.トーストボタン確認.info{
    色: #3E71F3;
}
.toast-button-confirm.info:hover{
    色: #345ec9;
}
// エラー.toast-button-confirm.error{
    色: #EB2833;
}
.toast-button-confirm.error:hover{
    色: #c9101a;
}


/* アニメーション */
.トーストから入る、
.トーストを残す{
  変換: translateX(120%);
}
.v-離れる、
.トースト-enter-to{
  変換: translateX(00%);
}
</スタイル>

メイン.js

'vue' から {createApp} をインポートします。
'./App.vue' からアプリをインポートします。

const app = createApp(App)

'@/assets/font/UIcons/font.css' をインポートします

// トーストをインストールする
'./components/toasts' からトーストをインポートします

app.use(トースト).mount('#app') を実行します。

使用

<テンプレート>
    <button @click="clickHandle">送信</button>
</テンプレート>

<スクリプト>
'vue' から { getCurrentInstance } をインポートします。
エクスポートデフォルト{
  設定(){
    定数インスタンス = getCurrentInstance()
    関数clickHandle(){
      // ここで vue3 のグローバル変数を呼び出すのは残念です。他に何か良いアイデアがあるかどうかはわかりません。instance.appContext.config.globalProperties.$Toast({
        タイプ: '情報'、
        タイトル: 「これはタイトルです」
        メッセージ: 'この記事は、マウント関数の主なロジックを整理し、基本的な処理フローを明らかにすることを目的としています (Vue 3.1.1 バージョン)。 '
      })
    }
    戻る {
      クリックハンドル
    }
  }
}
</スクリプト>

アイコンフォントを取得する

フラットコン

要約する

これで、vue3 を使用して Apple システムのサイドメッセージプロンプト効果を模倣する方法に関するこの記事は終了です。vue3 を使用して Apple のサイドメッセージプロンプトコンテンツを模倣する関連情報については、123WORDPRESS.COM の以前の記事を検索するか、次の関連記事を引き続き参照してください。今後とも 123WORDPRESS.COM を応援してください。

以下もご興味があるかもしれません:
  • Vue3管理システムを使用して動的ルーティングと動的サイドメニューバーを実装する方法
  • Vue3.0 レスポンシブ システムのソース コードの行ごとの分析
  • Vue3.x 最小プロトタイプシステムの説明

<<:  PSを使用して2分でxhtml+cssウェブサイトのホームページを作成します

>>:  HTML でファイルをアップロードするときに使用する <input type="file"> 要素のスタイルをカスタマイズします。

推薦する

Vue でクラスとスタイルを使用して v-bind バインディングを使用するいくつかの方法

要素にクラスを追加/削除することは、プロジェクト開発では非常に一般的な動作です。たとえば、Web サ...

圧縮パッケージを使用して Linux 環境に JDK 13 をインストールする方法

JDK とは何ですか?まあ、この質問がわからないのであれば、なぜこれをインストールするのか本当にわか...

MySQLデータベースに他のIPアドレスからアクセスできない問題の解決策

序文先ほどのプロジェクトを参考にすると、環境は整いました。プロジェクトの準備と検証の段階で、問題が発...

Linux で ping は成功するがポートが利用できない問題を解決する方法

ping は成功したがポートにアクセスできない場合のポート可用性検出の説明ポート可用性検出ツールの紹...

MySQL テーブル分割後にスムーズにオンラインになる方法

目次テーブルの目的例えばテーブル分割戦略すでにオンラインになっている実行中のテーブルはどうすればよい...

バックエンド サーバー プロキシとして Nginx を推奨する理由 (理由分析)

1. はじめに実際のサーバーはパブリックインターネットに直接公開されるべきではありません。そうしな...

MySQLはbinlogを通じてデータを復元する

目次MySQL ログファイルバイナリログBinlogログがオンになっていますログ記録を有効にする方法...

border-radiusは要素に丸い境界線を追加する方法です

border-radius:10px; /* すべての角は半径 10px で丸められます*/ bor...

Deepin で virtualenv をインストールして使用するチュートリアル

virtualenv は、分離された Python 仮想環境を作成するためのツールです。独立したディ...

Docker でコンテナのポート マッピングを動的に変更する方法

前書き: Docker のポート マッピングは、多くの場合、Docker Run コマンド中に -p...

CSS で順序付きリスト項目と順序なしリスト項目のスタイルを設定する方法

順序なしリストでは、順序なしリストのシンボルは各リストの前に表示されるドットです。順序付きリスト o...

Node.js の fs モジュールと Path モジュールのメソッドの詳細な説明

概要:ファイルシステム モジュールは、標準の POSIX ファイル I/O 操作セットをラップしたシ...

HTM と HTML の違いは何ですか? HTM と HTML の違いは何ですか?

Web デザインを学習する過程で、html と htm の関係など、遭遇した多くの問題について深く...

ウェブデザインレイアウトの理解

<br />矛盾が生じます。私たちのような小さな工房では、デザインとレイアウトは基本的に...

JavaScriptはパスワードボックスの検証情報を実装します

この記事では、パスワードボックスの検証情報を実装するためのJavaScriptの具体的なコードを例と...