Browse Source

feat: tanstack query demos (#4276)

* chore(@vben/request): add axios-retry

* feat: error retry

* feat: paginated queries

* feat: infinite queries

* chore: update

* chore: update

* fix: ci error

* chore: update

* chore: remove axios-retry

* chore: update deps

* chore: update deps

* chore: update deps

* chore: update pnpm.lock

---------

Co-authored-by: vince <vince292007@gmail.com>
Li Kui 6 months ago
parent
commit
86ed732ca8

+ 1 - 0
playground/package.json

@@ -26,6 +26,7 @@
     "#/*": "./src/*"
   },
   "dependencies": {
+    "@tanstack/vue-query": "^5.53.1",
     "@vben/access": "workspace:*",
     "@vben/common-ui": "workspace:*",
     "@vben/constants": "workspace:*",

+ 5 - 0
playground/src/bootstrap.ts

@@ -5,6 +5,8 @@ import { initStores } from '@vben/stores';
 import '@vben/styles';
 import '@vben/styles/antd';
 
+import { VueQueryPlugin } from '@tanstack/vue-query';
+
 import { setupI18n } from '#/locales';
 
 import App from './app.vue';
@@ -25,6 +27,9 @@ async function bootstrap(namespace: string) {
   // 配置路由及路由守卫
   app.use(router);
 
+  // 配置@tanstack/vue-query
+  app.use(VueQueryPlugin);
+
   app.mount('#app');
 }
 

+ 9 - 0
playground/src/router/routes/modules/demos.ts

@@ -183,6 +183,15 @@ const routes: RouteRecordRaw[] = [
               title: $t('page.demos.features.clipboard'),
             },
           },
+          {
+            name: 'VueQueryDemo',
+            path: '/demos/features/vue-query',
+            component: () =>
+              import('#/views/demos/features/vue-query/index.vue'),
+            meta: {
+              title: 'Tanstack Query',
+            },
+          },
         ],
       },
       // 面包屑导航

+ 25 - 0
playground/src/views/demos/features/vue-query/index.vue

@@ -0,0 +1,25 @@
+<script setup lang="ts">
+import { Page } from '@vben/common-ui';
+
+import { Card } from 'ant-design-vue';
+
+import InfiniteQueries from './infinite-queries.vue';
+import PaginatedQueries from './paginated-queries.vue';
+import QueryRetries from './query-retries.vue';
+</script>
+
+<template>
+  <Page title="Vue Query示例">
+    <div class="grid grid-cols-1 gap-4 md:grid-cols-2">
+      <Card title="分页查询">
+        <PaginatedQueries />
+      </Card>
+      <Card title="无限滚动">
+        <InfiniteQueries class="h-[300px] overflow-auto" />
+      </Card>
+      <Card title="错误重试">
+        <QueryRetries />
+      </Card>
+    </div>
+  </Page>
+</template>

+ 58 - 0
playground/src/views/demos/features/vue-query/infinite-queries.vue

@@ -0,0 +1,58 @@
+<script setup lang="ts">
+import type { IProducts } from './typing';
+
+import { useInfiniteQuery } from '@tanstack/vue-query';
+import { Button } from 'ant-design-vue';
+
+const LIMIT = 10;
+const fetchProducts = async ({ pageParam = 0 }): Promise<IProducts> => {
+  const res = await fetch(
+    `https://dummyjson.com/products?limit=${LIMIT}&skip=${pageParam * LIMIT}`,
+  );
+  return res.json();
+};
+
+const {
+  data,
+  error,
+  fetchNextPage,
+  hasNextPage,
+  isError,
+  isFetching,
+  isFetchingNextPage,
+  isPending,
+} = useInfiniteQuery({
+  getNextPageParam: (current, allPages) => {
+    const nextPage = allPages.length + 1;
+    const lastPage = current.skip + current.limit;
+    if (lastPage === current.total) return;
+    return nextPage;
+  },
+  initialPageParam: 0,
+  queryFn: fetchProducts,
+  queryKey: ['products'],
+});
+</script>
+
+<template>
+  <div>
+    <span v-if="isPending">加载...</span>
+    <span v-else-if="isError">出错了: {{ error }}</span>
+    <div v-else-if="data">
+      <span v-if="isFetching && !isFetchingNextPage">Fetching...</span>
+      <ul v-for="(group, index) in data.pages" :key="index">
+        <li v-for="product in group.products" :key="product.id">
+          {{ product.title }}
+        </li>
+      </ul>
+      <Button
+        :disabled="!hasNextPage || isFetchingNextPage"
+        @click="() => fetchNextPage()"
+      >
+        <span v-if="isFetchingNextPage">加载中...</span>
+        <span v-else-if="hasNextPage">加载更多</span>
+        <span v-else>没有更多了</span>
+      </Button>
+    </div>
+  </div>
+</template>

+ 51 - 0
playground/src/views/demos/features/vue-query/paginated-queries.vue

@@ -0,0 +1,51 @@
+<script setup lang="ts">
+import type { IProducts } from './typing';
+
+import { type Ref, ref } from 'vue';
+
+import { keepPreviousData, useQuery } from '@tanstack/vue-query';
+import { Button } from 'ant-design-vue';
+
+const LIMIT = 10;
+const fetcher = async (page: Ref<number>): Promise<IProducts> => {
+  const res = await fetch(
+    `https://dummyjson.com/products?limit=${LIMIT}&skip=${(page.value - 1) * LIMIT}`,
+  );
+  return res.json();
+};
+
+const page = ref(1);
+const { data, error, isError, isPending, isPlaceholderData } = useQuery({
+  // The data from the last successful fetch is available while new data is being requested.
+  placeholderData: keepPreviousData,
+  queryFn: () => fetcher(page),
+  queryKey: ['products', page],
+});
+const prevPage = () => {
+  page.value = Math.max(page.value - 1, 1);
+};
+const nextPage = () => {
+  if (!isPlaceholderData.value) {
+    page.value = page.value + 1;
+  }
+};
+</script>
+
+<template>
+  <div class="flex gap-4">
+    <Button size="small" @click="prevPage">上一页</Button>
+    <p>当前页: {{ page }}</p>
+    <Button size="small" @click="nextPage">下一页</Button>
+  </div>
+  <div class="p-4">
+    <div v-if="isPending">加载中...</div>
+    <div v-else-if="isError">出错了: {{ error }}</div>
+    <div v-else-if="data">
+      <ul>
+        <li v-for="item in data.products" :key="item.id">
+          {{ item.title }}
+        </li>
+      </ul>
+    </div>
+  </div>
+</template>

+ 34 - 0
playground/src/views/demos/features/vue-query/query-retries.vue

@@ -0,0 +1,34 @@
+<script setup lang="ts">
+import { ref } from 'vue';
+
+import { useQuery } from '@tanstack/vue-query';
+import { Button } from 'ant-design-vue';
+
+const count = ref(-1);
+async function fetchApi() {
+  count.value += 1;
+  return new Promise((_resolve, reject) => {
+    setTimeout(() => {
+      reject(new Error('something went wrong!'));
+    }, 1000);
+  });
+}
+
+const { error, isFetching, refetch } = useQuery({
+  enabled: false, // Disable automatic refetching when the query mounts
+  queryFn: fetchApi,
+  queryKey: ['queryKey'],
+  retry: 3, // Will retry failed requests 3 times before displaying an error
+});
+
+const onClick = async () => {
+  count.value = -1;
+  await refetch();
+};
+</script>
+
+<template>
+  <Button :loading="isFetching" @click="onClick"> 发起错误重试 </Button>
+  <p v-if="count > 0" class="my-3">重试次数{{ count }}</p>
+  <p>{{ error }}</p>
+</template>

+ 18 - 0
playground/src/views/demos/features/vue-query/typing.ts

@@ -0,0 +1,18 @@
+export interface IProducts {
+  limit: number;
+  products: {
+    brand: string;
+    category: string;
+    description: string;
+    discountPercentage: string;
+    id: string;
+    images: string[];
+    price: string;
+    rating: string;
+    stock: string;
+    thumbnail: string;
+    title: string;
+  }[];
+  skip: number;
+  total: number;
+}

+ 38 - 0
pnpm-lock.yaml

@@ -1156,6 +1156,9 @@ importers:
 
   playground:
     dependencies:
+      '@tanstack/vue-query':
+        specifier: ^5.53.1
+        version: 5.54.2(vue@3.5.3(typescript@5.5.4))
       '@vben/access':
         specifier: workspace:*
         version: link:../packages/effects/access
@@ -3976,12 +3979,28 @@ packages:
     peerDependencies:
       tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20'
 
+  '@tanstack/match-sorter-utils@8.19.4':
+    resolution: {integrity: sha512-Wo1iKt2b9OT7d+YGhvEPD3DXvPv2etTusIMhMUoG7fbhmxcXCtIjJDEygy91Y2JFlwGyjqiBPRozme7UD8hoqg==}
+    engines: {node: '>=12'}
+
+  '@tanstack/query-core@5.54.1':
+    resolution: {integrity: sha512-hKS+WRpT5zBFip21pB6Jx1C0hranWQrbv5EJ7qPoiV5MYI3C8rTCqWC9DdBseiPT1JgQWh8Y55YthuYZNiw3Xw==}
+
   '@tanstack/store@0.5.5':
     resolution: {integrity: sha512-EOSrgdDAJExbvRZEQ/Xhh9iZchXpMN+ga1Bnk8Nmygzs8TfiE6hbzThF+Pr2G19uHL6+DTDTHhJ8VQiOd7l4tA==}
 
   '@tanstack/virtual-core@3.9.0':
     resolution: {integrity: sha512-Saga7/QRGej/IDCVP5BgJ1oDqlDT2d9rQyoflS3fgMS8ntJ8JGw/LBqK2GorHa06+VrNFc0tGz65XQHJQJetFQ==}
 
+  '@tanstack/vue-query@5.54.2':
+    resolution: {integrity: sha512-GYIYee9WkUbPDD28t1kdNNtLCioiIva0MhKCvODGWoEML5MNONCX4/i4y2GGFi8i9nSbcA8MpvD+nt/tdZ+yJw==}
+    peerDependencies:
+      '@vue/composition-api': ^1.1.2
+      vue: 3.5.3
+    peerDependenciesMeta:
+      '@vue/composition-api':
+        optional: true
+
   '@tanstack/vue-store@0.5.5':
     resolution: {integrity: sha512-j+CDrxVhtQQNOjWzLmCqJeDwmmTAQGvEaNbLr1uPJ9rxJITodJtFNdBFj7l+Nd5o34v2ayEv64Ugh6+1BtuGNg==}
     peerDependencies:
@@ -8442,6 +8461,9 @@ packages:
     resolution: {integrity: sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==}
     engines: {node: '>= 0.10'}
 
+  remove-accents@0.5.0:
+    resolution: {integrity: sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==}
+
   repeat-string@1.6.1:
     resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==}
     engines: {node: '>=0.10'}
@@ -12909,10 +12931,24 @@ snapshots:
       postcss-selector-parser: 6.0.10
       tailwindcss: 3.4.10
 
+  '@tanstack/match-sorter-utils@8.19.4':
+    dependencies:
+      remove-accents: 0.5.0
+
+  '@tanstack/query-core@5.54.1': {}
+
   '@tanstack/store@0.5.5': {}
 
   '@tanstack/virtual-core@3.9.0': {}
 
+  '@tanstack/vue-query@5.54.2(vue@3.5.3(typescript@5.5.4))':
+    dependencies:
+      '@tanstack/match-sorter-utils': 8.19.4
+      '@tanstack/query-core': 5.54.1
+      '@vue/devtools-api': 6.6.3
+      vue: 3.5.3(typescript@5.5.4)
+      vue-demi: 0.14.10(vue@3.5.3(typescript@5.5.4))
+
   '@tanstack/vue-store@0.5.5(vue@3.5.3(typescript@5.5.4))':
     dependencies:
       '@tanstack/store': 0.5.5
@@ -17933,6 +17969,8 @@ snapshots:
 
   relateurl@0.2.7: {}
 
+  remove-accents@0.5.0: {}
+
   repeat-string@1.6.1: {}
 
   require-directory@2.1.1: {}