vue.js 2 cách xem giá trị cửa hàng từ vuex


168

Tôi đang sử dụng vuexvuejs 2cùng nhau.

Tôi mới đến vuex, tôi muốn xem một storesự thay đổi.

Tôi muốn thêm watchchức năng trongvue component

Đây là những gì tôi có cho đến nay:

import Vue from 'vue';
import {
  MY_STATE,
} from './../../mutation-types';

export default {
  [MY_STATE](state, token) {
    state.my_state = token;
  },
};

Tôi muốn biết nếu có bất kỳ thay đổi trong my_state

Làm thế nào để tôi xem store.my_statetrong thành phần vuejs của tôi?


sử dụng plugin Vuejs đi kèm với chrome, nó sẽ trở nên tiện dụng
AKASH PANDEY

Câu trả lời:


205

Ví dụ, giả sử bạn có một giỏ trái cây và mỗi lần bạn thêm hoặc loại bỏ một loại trái cây khỏi giỏ, bạn muốn (1) hiển thị thông tin về số lượng trái cây, nhưng bạn cũng (2) muốn được thông báo về số lượng trái cây trong một số thời trang ưa thích ...

trái cây-đếm-thành phần.vue

<template>
  <!-- We meet our first objective (1) by simply -->
  <!-- binding to the count property. -->
  <p>Fruits: {{ count }}</p>
</template>

<script>
import basket from '../resources/fruit-basket'

export default () {
  computed: {
    count () {
      return basket.state.fruits.length
      // Or return basket.getters.fruitsCount
      // (depends on your design decisions).
    }
  },
  watch: {
    count (newCount, oldCount) {
      // Our fancy notification (2).
      console.log(`We have ${newCount} fruits now, yay!`)
    }
  }
}
</script>

Xin lưu ý rằng tên của hàm trong watchđối tượng, phải khớp với tên của hàm trong computedđối tượng. Trong ví dụ trên tên là count.

Các giá trị mới và cũ của thuộc tính được xem sẽ được chuyển vào hàm gọi lại theo dõi (hàm đếm) dưới dạng tham số.

Cửa hàng giỏ có thể trông như thế này:

fruit-Basket.js

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

const basket = new Vuex.Store({
  state: {
    fruits: []
  },
  getters: {
    fruitsCount (state) {
      return state.fruits.length
    }
  }
  // Obviously you would need some mutations and actions,
  // but to make example cleaner I'll skip this part.
})

export default basket

Bạn có thể đọc thêm trong các tài nguyên sau:


Tôi chỉ tự hỏi tôi nên làm gì khi watchhành động nên chia thành hai bước: 1) Đầu tiên, kiểm tra xem dữ liệu mong muốn có được lưu trong bộ nhớ cache không và liệu nó có trả về dữ liệu được lưu trong bộ nhớ cache không; 2) Nếu bộ đệm không thành công, tôi cần một hành động async ajax để tìm nạp dữ liệu, nhưng đây có vẻ actionlà công việc của. Hy vọng câu hỏi của tôi có ý nghĩa, cảm ơn bạn!
1Cr18Ni9

Lợi ích của việc này là gì, qua câu trả lời của micah5, chỉ đặt một người theo dõi trong thành phần, trên giá trị cửa hàng? Nó có ít mã để duy trì.
Exrialric

@Exrialric Khi tôi viết câu trả lời, câu hỏi không rõ ràng đối với tôi. Không có bối cảnh tại sao nó là cần thiết để xem tài sản. Hãy nghĩ về nó như: "Tôi muốn xem biến X, vì vậy tôi có thể làm Y." Có lẽ đó là lý do tại sao hầu hết các câu trả lời đề xuất các cách tiếp cận rất khác nhau. Không ai biết ý định là gì. Đó là lý do tại sao tôi đưa "mục tiêu" vào câu trả lời của mình. Nếu bạn có các mục tiêu khác nhau, câu trả lời khác nhau có thể phù hợp với chúng. Ví dụ của tôi chỉ là một điểm khởi đầu cho thử nghiệm. Nó không có nghĩa là một giải pháp plug & play. Không có "lợi ích", vì lợi ích phụ thuộc vào tình huống của bạn.
Anastazy

@ 1Cr18Ni9 Tôi nghĩ bộ nhớ đệm không thuộc mã thành phần. Bạn sẽ kết thúc quá kỹ thuật một cái gì đó thực sự đơn giản (tìm nạp dữ liệu và ràng buộc nó với chế độ xem). Bộ nhớ đệm đã được thực hiện trong trình duyệt. Bạn có thể tận dụng lợi thế của nó bằng cách gửi các tiêu đề chính xác từ máy chủ. Giải thích đơn giản tại đây: csswizardry.com/2019/03/cache-control-for-civilians . Bạn cũng có thể xem ServiceWorkers cho phép trang web hoạt động ngay cả khi không có kết nối internet.
Anastazy

61

Bạn không nên sử dụng người theo dõi thành phần để lắng nghe sự thay đổi trạng thái. Tôi khuyên bạn nên sử dụng các hàm getters và sau đó ánh xạ chúng bên trong thành phần của bạn.

import { mapGetters } from 'vuex'

export default {
  computed: {
    ...mapGetters({
      myState: 'getMyState'
    })
  }
}

Trong cửa hàng của bạn:

const getters = {
  getMyState: state => state.my_state
}

Bạn sẽ có thể lắng nghe mọi thay đổi được thực hiện cho cửa hàng của mình bằng cách sử dụng this.myState trong thành phần của bạn.

https://vuex.vuejs.org/en/getters.html#the-mapgetters-rcper


1
Tôi không biết cách triển khai mapGetters. Bạn có thể chỉ cho tôi một ví dụ Nó sẽ là một trợ giúp lớn. Tôi chỉ thực hiện câu trả lời GONG vào lúc này. TY
Rbex

1
@Rbex "mapGetters" là một phần của thư viện 'vuex'. Bạn không cần phải thực hiện nó.
Gabriel Robert

69
Câu trả lời này chỉ sai. Anh ta thực sự cần phải xem các thuộc tính tính toán.
Juan

15
Các getter một khi được gọi sẽ chỉ lấy lại trạng thái tại thời điểm đó. Nếu bạn muốn tài sản đó phản ánh sự thay đổi trạng thái từ một thành phần khác, bạn phải xem nó.
C Tierney

3
Tại sao "Bạn không nên sử dụng người theo dõi thành phần để lắng nghe sự thay đổi trạng thái"? Dưới đây là ví dụ bạn có thể không nghĩ tới, nếu tôi muốn xem mã thông báo từ trạng thái và khi nó thay đổi để chuyển hướng đến một trang khác. Vì vậy, có một số trường hợp bạn cần phải làm điều đó. có lẽ bạn cần thêm kinh nghiệm để biết điều đó
Shlomi Levi

46

Nó đơn giản như:

watch: {
  '$store.state.drawer': function() {
    console.log(this.$store.state.drawer)
  }
}

6
Đây là một cảnh tượng chết tiệt đơn giản hơn bất kỳ câu trả lời nào ở đây ... có lập luận nào chống lại việc này không ..?
Inigo

19
Nó quá đơn giản nên không giống js, js phải phức tạp hơn.
đào

1
Sẽ còn đơn giản hơn nữa nếufunction(n) { console.log(n); }
WofWca

2
Siêu mát. Quan tâm cũng như trong bất kỳ nhược điểm của phương pháp này. Cho đến nay nó dường như hoạt động tốt.
namero999

1
Nghiêm túc. Điều này có vẻ tốt hơn nhiều so với câu trả lời được chấp nhận, đòi hỏi các tên hàm trùng lặp trong đồng hồ và được tính toán. Một chuyên gia có thể nhận xét về lý do tại sao hoặc tại sao không làm theo cách này?
Exrialric

42

Như đã đề cập ở trên, không nên xem các thay đổi trực tiếp trong cửa hàng

Nhưng trong một số trường hợp rất hiếm, nó có thể hữu ích cho ai đó, vì vậy tôi sẽ để lại câu trả lời này. Đối với các trường hợp khác, vui lòng xem câu trả lời @ gabriel-robert

Bạn có thể làm điều này thông qua state.$watch. Thêm điều này trong phương thức của bạn created(hoặc nơi bạn cần thực thi) trong thành phần

this.$store.watch(
    function (state) {
        return state.my_state;
    },
    function () {
        //do something on data change
    },
    {
        deep: true //add this if u need to watch object properties change etc.
    }
);

Thêm chi tiết: https://vuex.vuejs.org/api/#watch


3
Tôi không nghĩ rằng đó là một ý tưởng tốt để xem trạng thái trực tiếp. Chúng ta nên sử dụng getters. vuex.vuejs.org/en/getters.html#the-mapgetters-helper
Gabriel Robert

14
@GabrielRobert Tôi nghĩ rằng có một nơi cho cả hai. Nếu bạn cần thay đổi một cách phản ứng các điều kiện mẫu dựa trên, sử dụng giá trị được tính toán với mapState, v.v. Nhưng nếu không, giống như để kiểm soát dòng chảy trong một thành phần, bạn cần một chiếc đồng hồ đầy đủ. Bạn đã đúng, bạn không nên sử dụng đồng hồ thành phần đơn giản, nhưng trạng thái. Đồng hồ $ được thiết kế cho các trường hợp sử dụng này
roberto tomás

14
Mọi người đều đề cập đến nó, nhưng không ai nói lý do tại sao! Tôi đang cố gắng xây dựng một cửa hàng vuex tự động đồng bộ hóa với DB khi có thay đổi. Tôi cảm thấy người theo dõi trên cửa hàng là cách ma sát nhất! Bạn nghĩ sao? Vẫn không phải là một ý tưởng tốt?
mesqueeb

16

Tôi nghĩ rằng người hỏi muốn sử dụng đồng hồ với Vuex.

this.$store.watch(
      (state)=>{
        return this.$store.getters.your_getter
      },
      (val)=>{
       //something changed do something

      },
      {
        deep:true
      }
      );

13

Điều này dành cho tất cả những người không thể giải quyết vấn đề của họ bằng getters và thực sự cần một người theo dõi, ví dụ như nói chuyện với những thứ bên thứ ba không hợp lý (xem Vue Watchers khi nào nên sử dụng người theo dõi).

Cả hai bộ theo dõi và giá trị tính toán của thành phần Vue cũng hoạt động trên các giá trị được tính toán. Vì vậy, nó không khác với vuex:

import { mapState } from 'vuex';

export default {
    computed: {
        ...mapState(['somestate']),
        someComputedLocalState() {
            // is triggered whenever the store state changes
            return this.somestate + ' works too';
        }
    },
    watch: {
        somestate(val, oldVal) {
            // is triggered whenever the store state changes
            console.log('do stuff', val, oldVal);
        }
    }
}

nếu chỉ kết hợp trạng thái cục bộ và toàn cầu, tài liệu của mapState cũng cung cấp một ví dụ:

computed: {
    ...mapState({
        // to access local state with `this`, a normal function must be used
        countPlusLocalState (state) {
          return state.count + this.localCount
        }
    }
})

hack đẹp, nhưng quá tẻ nhạt, bạn không nghĩ sao?
Sao Hỏa2049

2
Nó không phải là hack nếu nó có trong tài liệu, phải không? Nhưng sau đó, nó cũng không phải là một lý lẽ ủng hộ cho vue / vuex
dube

8

nếu bạn sử dụng bản in thì bạn có thể:

import { Watch } from "vue-property-decorator";

..

@Watch("$store.state.something")
private watchSomething() {
   // use this.$store.state.something for access
   ...
}


Tại sao chính xác điều này bị hạ cấp? Chỉ vì giải pháp dành cho thành phần lớp vue và TO đã yêu cầu các kiểu lớp vue cũ? Tôi tìm thấy trước đây thích hợp hơn. Có lẽ @Zhang Sol có thể đề cập đến trong phần giới thiệu, rằng đây rõ ràng là thành phần vue-class?
JackLeEmmerdeur

Lưu ý chắc chắn lý do tại sao một trình trang trí bản thảo sẽ thích hợp hơn với một giải pháp gốc vue đơn giản như giải pháp này: stackoverflow.com/a/56461539/3652783
yann_yinn

6

Tạo trạng thái Địa phương của biến cửa hàng của bạn bằng cách xem và thiết lập thay đổi giá trị. Sao cho biến cục bộ thay đổi cho mô hình v đầu vào mẫu không trực tiếp làm biến đổi biến lưu trữ .

data() {
  return {
    localState: null
  };
 },
 computed: {
  ...mapGetters({
    computedGlobalStateVariable: 'state/globalStateVariable'
  })
 },
 watch: {
  computedGlobalStateVariable: 'setLocalState'
 },
 methods: {
  setLocalState(value) {
   this.localState = Object.assign({}, value);
  }
 }

5

Cách tốt nhất để theo dõi các thay đổi của cửa hàng là sử dụng mapGettersnhư Gabriel nói. Nhưng có một trường hợp khi bạn không thể thực hiện được, mapGettersví dụ: bạn muốn lấy thứ gì đó từ cửa hàng bằng tham số:

getters: {
  getTodoById: (state, getters) => (id) => {
    return state.todos.find(todo => todo.id === id)
  }
}

trong trường hợp đó bạn không thể sử dụng mapGetters. Thay vào đó, bạn có thể thử làm một cái gì đó như thế này:

computed: {
    todoById() {
        return this.$store.getters.getTodoById(this.id)
    }
}

Nhưng thật không may todoById sẽ chỉ được cập nhật nếu this.idđược thay đổi

Nếu bạn muốn bạn cập nhật thành phần trong trường hợp đó, hãy sử dụng this.$store.watch giải pháp do Gong cung cấp . Hoặc xử lý thành phần của bạn một cách có ý thức và cập nhật this.idkhi bạn cần cập nhật todoById.


cảm ơn bạn. Đó chính xác là trường hợp sử dụng của tôi, và thực sự thì getter không thể được xem sau đó ...
pscheit

5

Nếu bạn chỉ muốn xem một tài sản nhà nước và sau đó hành động trong thành phần tương ứng với những thay đổi của tài sản đó thì hãy xem ví dụ dưới đây.

Trong store.js:

export const state = () => ({
 isClosed: false
})
export const mutations = {
 closeWindow(state, payload) {
  state.isClosed = payload
 }
}

Trong kịch bản này, tôi đang tạo một booleantài sản nhà nước mà tôi sẽ thay đổi ở những nơi khác nhau trong ứng dụng như sau:

this.$store.commit('closeWindow', true)

Bây giờ, nếu tôi cần xem thuộc tính trạng thái đó trong một số thành phần khác và sau đó thay đổi thuộc tính cục bộ, tôi sẽ viết như sau trong mountedhook:

mounted() {
 this.$store.watch(
  state => state.isClosed,
  (value) => {
   if (value) { this.localProperty = 'edit' }
  }
 )
}

Đầu tiên, tôi đang thiết lập trình theo dõi trên thuộc tính trạng thái và sau đó trong chức năng gọi lại, tôi sử dụng valuethuộc tính đó để thay đổilocalProperty .

Tôi hy vọng nó sẽ giúp!


3

Khi bạn muốn xem ở cấp tiểu bang, có thể thực hiện theo cách này:

let App = new Vue({
    //...
    store,
    watch: {
        '$store.state.myState': function (newVal) {
            console.log(newVal);
            store.dispatch('handleMyStateChange');
        }
    },
    //...
});

Không nên xử lý store.statethay đổi bằng dispatchhành động trạng thái từ thành phần vì hành vi này chỉ hoạt động nếu bạn sử dụng thành phần đó. Ngoài ra, bạn có thể kết thúc với vòng lặp vô hạn. Theo dõi để store.statethay đổi hiếm khi sử dụng, ví dụ: nếu bạn có một thành phần hoặc một trang nên thực hiện một số hành động dựa trên store.statethay đổi không thể xử lý bằng cách sử dụng mapState được tính toán chỉ khi bạn không thể so sánh newValuevớioldValue
Januartha

@Januartha đề nghị của bạn cho vấn đề này là gì?
Billal Begueradj

@Andy tất nhiên là công việc của nó. Tôi chỉ muốn lưu ý tại sao bạn gọi store.dispatch? nếu bạn muốn xử lý store.statethay đổi cho store' why not handle it inside store.mutations`?
Januartha

@BillalBEGUERADJ Tôi giải pháp dube sạch hơn
Januartha

@Januartha, vì có thể có một cuộc gọi ajax xảy ra trước khi thực hiện đột biến, đó là lý do tại sao tôi sử dụng store.dispatchđầu tiên. Ví dụ, tôi muốn nhận tất cả các thành phố từ một quốc gia bất cứ khi nào $store.state.countrythay đổi, vì vậy tôi thêm phần này vào trình theo dõi. Sau đó, tôi sẽ viết một cuộc gọi ajax: trong store.dispatch('fetchCities')tôi viết:axios.get('cities',{params:{country: state.country }}).then(response => store.commit('receiveCities',response) )
Andy

2

Bạn có thể sử dụng kết hợp các hành động , getters , thuộc tính và trình theo dõi của Vuex để lắng nghe những thay đổi trên giá trị trạng thái Vuex.

Mã HTML:

<div id="app" :style='style'>
  <input v-model='computedColor' type="text" placeholder='Background Color'>
</div>

Mã JavaScript:

'use strict'

Vue.use(Vuex)

const { mapGetters, mapActions, Store } = Vuex

new Vue({
    el: '#app',
  store: new Store({
    state: {
      color: 'red'
    },
    getters: {
      color({color}) {
        return color
      }
    },
    mutations: {
      setColor(state, payload) {
        state.color = payload
      }
    },
    actions: {
      setColor({commit}, payload) {
        commit('setColor', payload)
      }
    }
  }),
  methods: {
    ...mapGetters([
        'color'
    ]),
    ...mapActions([
        'setColor'
    ])
  },
  computed: {
    computedColor: {
        set(value) {
        this.setColor(value)
      },
      get() {
        return this.color()
      }
    },
    style() {
        return `background-color: ${this.computedColor};`
    }
  },
  watch: {
    computedColor() {
        console.log(`Watcher in use @${new Date().getTime()}`)
    }
  }
})

Xem bản demo của JSFiddle .


1

Bạn cũng có thể đăng ký các đột biến cửa hàng:

store.subscribe((mutation, state) => {
  console.log(mutation.type)
  console.log(mutation.payload)
})

https://vuex.vuejs.org/api/#subscribe


Bạn có thể kích hoạt tính năng này trong hook beforeMount () của thành phần sau đó lọc các đột biến đến bằng Câu lệnh if. ví dụ: if (m mut.type == "name / SET_NAMES") {... làm gì đó}
Alejandro

1

Bên trong thành phần, tạo một hàm tính toán

computed:{
  myState:function(){
    return this.$store.state.my_state; // return the state value in `my_state`
  }
}

Bây giờ tên hàm tính toán có thể được xem, như

watch:{
  myState:function(newVal,oldVal){
    // this function will trigger when ever the value of `my_state` changes
  }
}

Những thay đổi được thực hiện trong vuextrạng thái my_statesẽ phản ánh trong hàm tính toánmyState và kích hoạt chức năng đồng hồ.

Nếu trạng thái my_statecó dữ liệu lồng nhau, thì handlertùy chọn sẽ giúp nhiều hơn

watch:{
  myState:{
    handler:function(newVal,oldVal){
      // this function will trigger when ever the value of `my_state` changes
    },
    deep:true
  }
}

Điều này sẽ xem tất cả các giá trị lồng nhau trong cửa hàng my_state.


0

Bạn cũng có thể sử dụng mapState trong thành phần vue của mình để nhận trạng thái trực tiếp từ cửa hàng.

Trong thành phần của bạn:

computed: mapState([
  'my_state'
])

Trường hợp my_statelà một biến từ các cửa hàng.


0

====== store =====
import Vue from 'vue'
import Vuex from 'vuex'
import axios from 'axios'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    showRegisterLoginPage: true,
    user: null,
    allitem: null,
    productShow: null,
    userCart: null
  },
  mutations: {
    SET_USERS(state, payload) {
      state.user = payload
    },
    HIDE_LOGIN(state) {
      state.showRegisterLoginPage = false
    },
    SHOW_LOGIN(state) {
      state.showRegisterLoginPage = true
    },
    SET_ALLITEM(state, payload) {
      state.allitem = payload
    },
    SET_PRODUCTSHOW(state, payload) {
      state.productShow = payload
    },
    SET_USERCART(state, payload) {
      state.userCart = payload
    }
  },
  actions: {
    getUserLogin({ commit }) {
      axios({
        method: 'get',
        url: 'http://localhost:3000/users',
        headers: {
          token: localStorage.getItem('token')
        }
      })
        .then(({ data }) => {
          // console.log(data)
          commit('SET_USERS', data)
        })
        .catch(err => {
          console.log(err)
        })
    },
    addItem({ dispatch }, payload) {
      let formData = new FormData()
      formData.append('name', payload.name)
      formData.append('file', payload.file)
      formData.append('category', payload.category)
      formData.append('price', payload.price)
      formData.append('stock', payload.stock)
      formData.append('description', payload.description)
      axios({
        method: 'post',
        url: 'http://localhost:3000/products',
        data: formData,
        headers: {
          token: localStorage.getItem('token')
        }
      })
        .then(({ data }) => {
          // console.log('data hasbeen created ', data)
          dispatch('getAllItem')
        })
        .catch(err => {
          console.log(err)
        })
    },
    getAllItem({ commit }) {
      axios({
        method: 'get',
        url: 'http://localhost:3000/products'
      })
        .then(({ data }) => {
          // console.log(data)
          commit('SET_ALLITEM', data)
        })
        .catch(err => {
          console.log(err)
        })
    },
    addUserCart({ dispatch }, { payload, productId }) {
      let newCart = {
        count: payload
      }
      // console.log('ini dari store nya', productId)

      axios({
        method: 'post',
        url: `http://localhost:3000/transactions/${productId}`,
        data: newCart,
        headers: {
          token: localStorage.getItem('token')
        }
      })
        .then(({ data }) => {
          dispatch('getUserCart')
          // console.log('cart hasbeen added ', data)
        })
        .catch(err => {
          console.log(err)
        })
    },
    getUserCart({ commit }) {
      axios({
        method: 'get',
        url: 'http://localhost:3000/transactions/user',
        headers: {
          token: localStorage.getItem('token')
        }
      })
        .then(({ data }) => {
          // console.log(data)
          commit('SET_USERCART', data)
        })
        .catch(err => {
          console.log(err)
        })
    },
    cartCheckout({ commit, dispatch }, transactionId) {
      let count = null
      axios({
        method: 'post',
        url: `http://localhost:3000/transactions/checkout/${transactionId}`,
        headers: {
          token: localStorage.getItem('token')
        },
        data: {
          sesuatu: 'sesuatu'
        }
      })
        .then(({ data }) => {
          count = data.count
          console.log(count, data)

          dispatch('getUserCart')
        })
        .catch(err => {
          console.log(err)
        })
    },
    deleteTransactions({ dispatch }, transactionId) {
      axios({
        method: 'delete',
        url: `http://localhost:3000/transactions/${transactionId}`,
        headers: {
          token: localStorage.getItem('token')
        }
      })
        .then(({ data }) => {
          console.log('success delete')

          dispatch('getUserCart')
        })
        .catch(err => {
          console.log(err)
        })
    }
  },
  modules: {}
})


1
Chào mừng đến với trang web. Chỉ đặt một đoạn mã là không đủ. Vui lòng cung cấp một số giải thích về mã của bạn.
pgk

0

Tôi đã sử dụng cách này và nó hoạt động:

store.js:

const state = {
  createSuccess: false
};

mutations.js

[mutations.CREATE_SUCCESS](state, payload) {
    state.createSuccess = payload;
}

Action.js

async [mutations.STORE]({ commit }, payload) {
  try {
    let result = await axios.post('/api/admin/users', payload);
    commit(mutations.CREATE_SUCCESS, user);
  } catch (err) {
    console.log(err);
  }
}

getters.js

isSuccess: state => {
    return state.createSuccess
}

Và trong thành phần của bạn, nơi bạn sử dụng trạng thái từ cửa hàng:

watch: {
    isSuccess(value) {
      if (value) {
        this.$notify({
          title: "Success",
          message: "Create user success",
          type: "success"
        });
      }
    }
  }

Khi người dùng gửi biểu mẫu, hành động STORE sẽ được gọi, sau khi tạo thành công, đột biến CREATE_SUCCESS được cam kết sau đó. Rẽ createSuccess là đúng và trong thành phần, trình theo dõi sẽ thấy giá trị đã thay đổi và kích hoạt thông báo.

isSuccess phải khớp với tên bạn khai báo trong getters.js

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.