Merge pull request #244 from jianyi-gronk/feature/ui/traffic_control

add traffic control pages
diff --git a/ui-vue3/src/api/mock/mockDestinationRule.ts b/ui-vue3/src/api/mock/mockDestinationRule.ts
new file mode 100644
index 0000000..4206b29
--- /dev/null
+++ b/ui-vue3/src/api/mock/mockDestinationRule.ts
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import Mock from 'mockjs'
+
+Mock.mock('/mock/destinationRule/search', 'get', () => {
+  const total = Mock.mock('@integer(8, 1000)')
+  const list = []
+  for (let i = 0; i < total; i++) {
+    list.push({
+      ruleName: 'app_' + Mock.mock('@string(2,10)'),
+      createTime: Mock.mock('@datetime')
+    })
+  }
+  return {
+    code: 200,
+    message: 'success',
+    data: Mock.mock({
+      total: total,
+      curPage: 1,
+      pageSize: 10,
+      data: list
+    })
+  }
+})
diff --git a/ui-vue3/src/api/mock/mockDynamicConfig.ts b/ui-vue3/src/api/mock/mockDynamicConfig.ts
new file mode 100644
index 0000000..879df89
--- /dev/null
+++ b/ui-vue3/src/api/mock/mockDynamicConfig.ts
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import Mock from 'mockjs'
+
+Mock.mock('/mock/dynamicConfig/search', 'get', () => {
+  const total = Mock.mock('@integer(8, 1000)')
+  const list = []
+  for (let i = 0; i < total; i++) {
+    list.push({
+      ruleName: 'app_' + Mock.mock('@string(2,10)'),
+      ruleGranularity: Mock.mock('@boolean'),
+      enable: Mock.mock('@boolean'),
+      createTime: Mock.mock('@datetime')
+    })
+  }
+  return {
+    code: 200,
+    message: 'success',
+    data: Mock.mock({
+      total: total,
+      curPage: 1,
+      pageSize: 10,
+      data: list
+    })
+  }
+})
diff --git a/ui-vue3/src/api/mock/mockRoutingRule.ts b/ui-vue3/src/api/mock/mockRoutingRule.ts
index bd352da..e9fe0b5 100644
--- a/ui-vue3/src/api/mock/mockRoutingRule.ts
+++ b/ui-vue3/src/api/mock/mockRoutingRule.ts
@@ -23,10 +23,9 @@
   for (let i = 0; i < total; i++) {
     list.push({
       ruleName: 'app_' + Mock.mock('@string(2,10)'),
-      ruleGranularity: Mock.mock('@integer(80, 200)'),
+      ruleGranularity: Mock.mock('@boolean'),
       enable: Mock.mock('@boolean'),
-      effectiveTime: Mock.mock('@datetime'),
-      protection: Mock.mock('@boolean')
+      createTime: Mock.mock('@datetime')
     })
   }
   return {
diff --git a/ui-vue3/src/api/mock/mockTagRule.ts b/ui-vue3/src/api/mock/mockTagRule.ts
new file mode 100644
index 0000000..6dd1984
--- /dev/null
+++ b/ui-vue3/src/api/mock/mockTagRule.ts
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import Mock from 'mockjs'
+
+Mock.mock('/mock/tagRule/search', 'get', () => {
+  const total = Mock.mock('@integer(8, 1000)')
+  const list = []
+  for (let i = 0; i < total; i++) {
+    list.push({
+      ruleName: 'app_' + Mock.mock('@string(2,10)'),
+      enable: Mock.mock('@boolean'),
+      createTime: Mock.mock('@datetime')
+    })
+  }
+  return {
+    code: 200,
+    message: 'success',
+    data: Mock.mock({
+      total: total,
+      curPage: 1,
+      pageSize: 10,
+      data: list
+    })
+  }
+})
diff --git a/ui-vue3/src/api/mock/mockVirtualService.ts b/ui-vue3/src/api/mock/mockVirtualService.ts
new file mode 100644
index 0000000..5469277
--- /dev/null
+++ b/ui-vue3/src/api/mock/mockVirtualService.ts
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import Mock from 'mockjs'
+
+Mock.mock('/mock/virtualService/search', 'get', () => {
+  const total = Mock.mock('@integer(8, 1000)')
+  const list = []
+  for (let i = 0; i < total; i++) {
+    list.push({
+      ruleName: 'app_' + Mock.mock('@string(2,10)'),
+      createTime: Mock.mock('@datetime'),
+      lastModifiedTime: Mock.mock('@datetime')
+    })
+  }
+  return {
+    code: 200,
+    message: 'success',
+    data: Mock.mock({
+      total: total,
+      curPage: 1,
+      pageSize: 10,
+      data: list
+    })
+  }
+})
diff --git a/ui-vue3/src/api/service/traffic.ts b/ui-vue3/src/api/service/traffic.ts
index 380f230..24f97d6 100644
--- a/ui-vue3/src/api/service/traffic.ts
+++ b/ui-vue3/src/api/service/traffic.ts
@@ -24,3 +24,35 @@
     params
   })
 }
+
+export const searchTagRule = (params: any): Promise<any> => {
+  return request({
+    url: '/tagRule/search',
+    method: 'get',
+    params
+  })
+}
+
+export const searchDynamicConfig = (params: any): Promise<any> => {
+  return request({
+    url: '/dynamicConfig/search',
+    method: 'get',
+    params
+  })
+}
+
+export const searchVirtualService = (params: any): Promise<any> => {
+  return request({
+    url: '/virtualService/search',
+    method: 'get',
+    params
+  })
+}
+
+export const searchDestinationRule = (params: any): Promise<any> => {
+  return request({
+    url: '/destinationRule/search',
+    method: 'get',
+    params
+  })
+}
diff --git a/ui-vue3/src/base/i18n/en.ts b/ui-vue3/src/base/i18n/en.ts
index c95132c..03deca7 100644
--- a/ui-vue3/src/base/i18n/en.ts
+++ b/ui-vue3/src/base/i18n/en.ts
@@ -332,6 +332,8 @@
   editMockRule: 'Edit Mock Rule',
   deleteRuleTitle: 'Are you sure to delete this mock rule?',
 
+  createTime: 'Create Time',
+  lastModifiedTime: 'Last Modified Time',
   trafficTimeout: 'Timeout',
   trafficRetry: 'Retry',
   trafficRegion: 'Region Aware',
diff --git a/ui-vue3/src/base/i18n/zh.ts b/ui-vue3/src/base/i18n/zh.ts
index aa36396..8428a8c 100644
--- a/ui-vue3/src/base/i18n/zh.ts
+++ b/ui-vue3/src/base/i18n/zh.ts
@@ -291,7 +291,8 @@
 
   ruleName: '规则名',
   ruleGranularity: '规则粒度',
-  effectiveTime: '生效时间',
+  createTime: '创建时间',
+  lastModifiedTime: '最后修改时间',
   enable: '是否启用',
   protection: '容错保护',
   trafficTimeout: '超时时间',
diff --git a/ui-vue3/src/components/SearchTable.vue b/ui-vue3/src/components/SearchTable.vue
index db40551..d9f53e8 100644
--- a/ui-vue3/src/components/SearchTable.vue
+++ b/ui-vue3/src/components/SearchTable.vue
@@ -18,7 +18,7 @@
   <div class="__container_search_table">
     <div class="search-query-container">
       <a-row>
-        <a-col :span="20">
+        <a-col :span="18">
           <a-form>
             <a-flex wrap="wrap" gap="large">
               <template v-for="q in searchDomain.params">
@@ -67,28 +67,31 @@
             </a-flex>
           </a-form>
         </a-col>
-        <a-col :span="4">
-          <div class="common-tool">
-            <a-dropdown placement="bottom" :trigger="['click']">
-              <div class="custom-column button">
-                <Icon icon="material-symbols-light:format-list-bulleted-rounded"></Icon>
-              </div>
+        <a-col :span="6">
+          <a-flex style="justify-content: flex-end;">
+            <slot name="customOperation"></slot>
+            <div class="common-tool">
+              <a-dropdown placement="bottom" :trigger="['click']">
+                <div class="custom-column button">
+                  <Icon icon="material-symbols-light:format-list-bulleted-rounded"></Icon>
+                </div>
 
-              <template #overlay>
-                <a-card title="Custom Column">
-                  <a-menu>
-                    <a-menu-item>
-                      <Icon
-                        style="margin-bottom: -3px; font-size: 1rem"
-                        icon="material-symbols-light:format-list-bulleted-rounded"
-                      ></Icon>
-                      3
-                    </a-menu-item>
-                  </a-menu>
-                </a-card>
-              </template>
-            </a-dropdown>
-          </div>
+                <template #overlay>
+                  <a-card title="Custom Column">
+                    <a-menu>
+                      <a-menu-item>
+                        <Icon
+                          style="margin-bottom: -3px; font-size: 1rem"
+                          icon="material-symbols-light:format-list-bulleted-rounded"
+                        ></Icon>
+                        3
+                      </a-menu-item>
+                    </a-menu>
+                  </a-card>
+                </template>
+              </a-dropdown>
+            </div>
+          </a-flex>
         </a-col>
       </a-row>
     </div>
@@ -125,13 +128,15 @@
 
 <script setup lang="ts">
 import type { ComponentInternalInstance } from 'vue'
-import { computed, getCurrentInstance, inject, reactive } from 'vue'
+import { computed, getCurrentInstance, inject, reactive, useSlots } from 'vue'
 
 import { PROVIDE_INJECT_KEY } from '@/base/enums/ProvideInject'
 import type { SearchDomain } from '@/utils/SearchUtil'
 import { Icon } from '@iconify/vue'
 import { PRIMARY_COLOR } from '@/base/constants'
 
+const customOperation = !!useSlots().customOperation;
+
 const commonTool = reactive({
   customColumns: false
 })
@@ -215,7 +220,7 @@
       }
 
       svg {
-        margin-left: 10px;
+        margin-left: 20px;
       }
     }
   }
diff --git a/ui-vue3/src/router/defaultRoutes.ts b/ui-vue3/src/router/defaultRoutes.ts
index 70884b9..f2e528c 100644
--- a/ui-vue3/src/router/defaultRoutes.ts
+++ b/ui-vue3/src/router/defaultRoutes.ts
@@ -290,6 +290,22 @@
             name: 'dynamicConfig',
             component: () => import('../views/traffic/dynamicConfig/index.vue'),
             meta: {}
+          },
+          {
+            path: '/meshRule',
+            name: 'meshRule',
+            children: [
+              {
+                path: '/virtualService',
+                name: 'virtualService',
+                component: () => import('../views/traffic/virtualService/index.vue')
+              },
+              {
+                path: '/destinationRule',
+                name: 'destinationRule',
+                component: () => import('../views/traffic/destinationRule/index.vue')
+              }
+            ]
           }
         ]
       },
diff --git a/ui-vue3/src/views/traffic/destinationRule/index.vue b/ui-vue3/src/views/traffic/destinationRule/index.vue
new file mode 100644
index 0000000..05b92be
--- /dev/null
+++ b/ui-vue3/src/views/traffic/destinationRule/index.vue
@@ -0,0 +1,103 @@
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one or more
+  ~ contributor license agreements.  See the NOTICE file distributed with
+  ~ this work for additional information regarding copyright ownership.
+  ~ The ASF licenses this file to You under the Apache License, Version 2.0
+  ~ (the "License"); you may not use this file except in compliance with
+  ~ the License.  You may obtain a copy of the License at
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+-->
+<template>
+  <div class="__container_resources_application_index">
+    <search-table :search-domain="searchDomain">
+      <template #customOperation>
+        <a-button type="primary">新增 Destination Rule</a-button>
+      </template>
+      <template #bodyCell="{ text, column }">
+        <template v-if="column.dataIndex === 'ruleName'">
+          <a-button type="link">{{ text }}</a-button>
+        </template>
+        <template v-if="column.dataIndex === 'operation'">
+          <a-button type="link">查看</a-button>
+          <a-button type="link">修改</a-button>
+          <a-popconfirm
+            title="确认删除该动态配置?"
+            ok-text="Yes"
+            cancel-text="No"
+            @confirm="confirm"
+          >
+            <a-button type="link">删除</a-button>
+          </a-popconfirm>
+        </template>
+      </template>
+    </search-table>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { onMounted, provide, reactive } from 'vue'
+import { searchDestinationRule } from '@/api/service/traffic'
+import SearchTable from '@/components/SearchTable.vue'
+import { SearchDomain, sortString } from '@/utils/SearchUtil'
+import { PROVIDE_INJECT_KEY } from '@/base/enums/ProvideInject'
+
+let columns = [
+  {
+    title: 'ruleName',
+    key: 'ruleName',
+    dataIndex: 'ruleName',
+    sorter: (a: any, b: any) => sortString(a.appName, b.appName),
+    width: 140
+  },
+  {
+    title: 'createTime',
+    key: 'createTime',
+    dataIndex: 'createTime',
+    width: 120,
+    sorter: (a: any, b: any) => sortString(a.instanceNum, b.instanceNum)
+  },
+  {
+    title: 'operation',
+    key: 'operation',
+    dataIndex: 'operation',
+    width: 200
+  }
+]
+const searchDomain = reactive(
+  new SearchDomain(
+    [
+      {
+        label: 'serviceGovernance',
+        param: 'serviceGovernance',
+        placeholder: 'typeRoutingRules',
+        style: {
+          width: '200px'
+        }
+      }
+    ],
+    searchDestinationRule,
+    columns
+  )
+)
+
+onMounted(() => {
+  searchDomain.onSearch()
+})
+
+const confirm = () => {}
+
+provide(PROVIDE_INJECT_KEY.SEARCH_DOMAIN, searchDomain)
+</script>
+<style lang="less" scoped>
+.search-table-container {
+  min-height: 60vh;
+  //max-height: 70vh; //overflow: auto;
+}
+</style>
diff --git a/ui-vue3/src/views/traffic/dynamicConfig/index.vue b/ui-vue3/src/views/traffic/dynamicConfig/index.vue
index c97d821..fbef790 100644
--- a/ui-vue3/src/views/traffic/dynamicConfig/index.vue
+++ b/ui-vue3/src/views/traffic/dynamicConfig/index.vue
@@ -15,15 +15,112 @@
   ~ limitations under the License.
 -->
 <template>
-  <div class="__container_home_index">
-    <h1>{{ $t(routeName) }}</h1>
+  <div class="__container_resources_application_index">
+    <search-table :search-domain="searchDomain">
+      <template #customOperation>
+        <a-button type="primary">新增动态配置</a-button>
+        <a-button type="primary" style="margin-left: 10px">从模版创建</a-button>
+      </template>
+      <template #bodyCell="{ text, column }">
+        <template v-if="column.dataIndex === 'ruleName'">
+          <a-button type="link">{{ text }}</a-button>
+        </template>
+        <template v-if="column.dataIndex === 'ruleGranularity'">
+          {{ text ? '服务' : '应用' }}
+        </template>
+        <template v-if="column.dataIndex === 'enable'">
+          {{ text ? '启用' : '禁用' }}
+        </template>
+        <template v-if="column.dataIndex === 'operation'">
+          <a-button type="link">查看</a-button>
+          <a-button type="link">修改</a-button>
+          <a-popconfirm
+            title="确认删除该动态配置?"
+            ok-text="Yes"
+            cancel-text="No"
+            @confirm="confirm"
+          >
+            <a-button type="link">删除</a-button>
+          </a-popconfirm>
+        </template>
+      </template>
+    </search-table>
   </div>
 </template>
 
 <script setup lang="ts">
-import { Icon } from '@iconify/vue'
+import { onMounted, provide, reactive } from 'vue'
+import { searchDynamicConfig } from '@/api/service/traffic'
+import SearchTable from '@/components/SearchTable.vue'
+import { SearchDomain, sortString } from '@/utils/SearchUtil'
+import { PROVIDE_INJECT_KEY } from '@/base/enums/ProvideInject'
 
-import { useRoute } from 'vue-router'
-const routeName = <string>useRoute().name
+let columns = [
+  {
+    title: 'ruleName',
+    key: 'ruleName',
+    dataIndex: 'ruleName',
+    sorter: (a: any, b: any) => sortString(a.appName, b.appName),
+    width: 140
+  },
+  {
+    title: 'ruleGranularity',
+    key: 'ruleGranularity',
+    dataIndex: 'ruleGranularity',
+    render: (text, record) => (record.isService ? '服务' : '应用'),
+    width: 100,
+    sorter: (a: any, b: any) => sortString(a.instanceNum, b.instanceNum)
+  },
+  {
+    title: 'createTime',
+    key: 'createTime',
+    dataIndex: 'createTime',
+    width: 120,
+    sorter: (a: any, b: any) => sortString(a.instanceNum, b.instanceNum)
+  },
+  {
+    title: 'enable',
+    key: 'enable',
+    dataIndex: 'enable',
+    render: (text, record) => (record.enable ? '是' : '否'),
+    width: 120,
+    sorter: (a: any, b: any) => sortString(a.instanceNum, b.instanceNum)
+  },
+  {
+    title: 'operation',
+    key: 'operation',
+    dataIndex: 'operation',
+    width: 200
+  }
+]
+const searchDomain = reactive(
+  new SearchDomain(
+    [
+      {
+        label: 'serviceGovernance',
+        param: 'serviceGovernance',
+        placeholder: 'typeRoutingRules',
+        style: {
+          width: '200px'
+        }
+      }
+    ],
+    searchDynamicConfig,
+    columns
+  )
+)
+
+onMounted(() => {
+  searchDomain.onSearch()
+})
+
+const confirm = () => {}
+
+provide(PROVIDE_INJECT_KEY.SEARCH_DOMAIN, searchDomain)
 </script>
-<style lang="less" scoped></style>
+<style lang="less" scoped>
+.search-table-container {
+  min-height: 60vh;
+  //max-height: 70vh; //overflow: auto;
+}
+</style>
diff --git a/ui-vue3/src/views/traffic/routingRule/index.vue b/ui-vue3/src/views/traffic/routingRule/index.vue
index c899666..14e82ed 100644
--- a/ui-vue3/src/views/traffic/routingRule/index.vue
+++ b/ui-vue3/src/views/traffic/routingRule/index.vue
@@ -17,12 +17,30 @@
 <template>
   <div class="__container_resources_application_index">
     <search-table :search-domain="searchDomain">
-      <template #bodyCell="{ text, record, index, column }">
-        <template v-if="column.dataIndex === 'enable'">
-          {{ text ? '是' : '否' }}
+      <template #customOperation>
+        <a-button type="primary">新增条件路由规则</a-button>
+      </template>
+      <template #bodyCell="{ text, column }">
+        <template v-if="column.dataIndex === 'ruleName'">
+          <a-button type="link">{{ text }}</a-button>
         </template>
-        <template v-if="column.dataIndex === 'protection'">
-          {{ text ? '是' : '否' }}
+        <template v-if="column.dataIndex === 'ruleGranularity'">
+          {{ text ? '服务' : '应用' }}
+        </template>
+        <template v-if="column.dataIndex === 'enable'">
+          {{ text ? '启用' : '禁用' }}
+        </template>
+        <template v-if="column.dataIndex === 'operation'">
+          <a-button type="link">查看</a-button>
+          <a-button type="link">修改</a-button>
+          <a-popconfirm
+            title="确认删除该条件路由规则?"
+            ok-text="Yes"
+            cancel-text="No"
+            @confirm="confirm"
+          >
+            <a-button type="link">删除</a-button>
+          </a-popconfirm>
         </template>
       </template>
     </search-table>
@@ -48,27 +66,29 @@
     title: 'ruleGranularity',
     key: 'ruleGranularity',
     dataIndex: 'ruleGranularity',
+    render: (text, record) => (record.isService ? '服务' : '应用'),
     width: 100,
     sorter: (a: any, b: any) => sortString(a.instanceNum, b.instanceNum)
   },
   {
+    title: 'createTime',
+    key: 'createTime',
+    dataIndex: 'createTime',
+    width: 120,
+    sorter: (a: any, b: any) => sortString(a.instanceNum, b.instanceNum)
+  },
+  {
     title: 'enable',
     key: 'enable',
     dataIndex: 'enable',
     render: (text, record) => (record.enable ? '是' : '否'),
-    width: 120
+    width: 120,
+    sorter: (a: any, b: any) => sortString(a.instanceNum, b.instanceNum)
   },
   {
-    title: 'effectiveTime',
-    key: 'effectiveTime',
-    dataIndex: 'effectiveTime',
-    width: 120
-  },
-  {
-    title: 'protection',
-    key: 'protection',
-    dataIndex: 'protection',
-    render: (text, record) => (record.protection ? '是' : '否'),
+    title: 'operation',
+    key: 'operation',
+    dataIndex: 'operation',
     width: 200
   }
 ]
@@ -93,6 +113,8 @@
   searchDomain.onSearch()
 })
 
+const confirm = () => {}
+
 provide(PROVIDE_INJECT_KEY.SEARCH_DOMAIN, searchDomain)
 </script>
 <style lang="less" scoped>
diff --git a/ui-vue3/src/views/traffic/tagRule/index.vue b/ui-vue3/src/views/traffic/tagRule/index.vue
index c97d821..d72618b 100644
--- a/ui-vue3/src/views/traffic/tagRule/index.vue
+++ b/ui-vue3/src/views/traffic/tagRule/index.vue
@@ -15,15 +15,100 @@
   ~ limitations under the License.
 -->
 <template>
-  <div class="__container_home_index">
-    <h1>{{ $t(routeName) }}</h1>
+  <div class="__container_resources_application_index">
+    <search-table :search-domain="searchDomain">
+      <template #customOperation>
+        <a-button type="primary">新增标签路由规则</a-button>
+      </template>
+      <template #bodyCell="{ text, column }">
+        <template v-if="column.dataIndex === 'ruleName'">
+          <a-button type="link">{{ text }}</a-button>
+        </template>
+        <template v-if="column.dataIndex === 'enable'">
+          {{ text ? '启用' : '禁用' }}
+        </template>
+        <template v-if="column.dataIndex === 'operation'">
+          <a-button type="link">查看</a-button>
+          <a-button type="link">修改</a-button>
+          <a-popconfirm
+            title="确认删除该标签路由规则?"
+            ok-text="Yes"
+            cancel-text="No"
+            @confirm="confirm"
+          >
+            <a-button type="link">删除</a-button>
+          </a-popconfirm>
+        </template>
+      </template>
+    </search-table>
   </div>
 </template>
 
 <script setup lang="ts">
-import { Icon } from '@iconify/vue'
+import { onMounted, provide, reactive } from 'vue'
+import { searchTagRule } from '@/api/service/traffic'
+import SearchTable from '@/components/SearchTable.vue'
+import { SearchDomain, sortString } from '@/utils/SearchUtil'
+import { PROVIDE_INJECT_KEY } from '@/base/enums/ProvideInject'
 
-import { useRoute } from 'vue-router'
-const routeName = <string>useRoute().name
+let columns = [
+  {
+    title: 'ruleName',
+    key: 'ruleName',
+    dataIndex: 'ruleName',
+    sorter: (a: any, b: any) => sortString(a.appName, b.appName),
+    width: 140
+  },
+  {
+    title: 'createTime',
+    key: 'createTime',
+    dataIndex: 'createTime',
+    width: 120,
+    sorter: (a: any, b: any) => sortString(a.instanceNum, b.instanceNum)
+  },
+  {
+    title: 'enable',
+    key: 'enable',
+    dataIndex: 'enable',
+    render: (text, record) => (record.enable ? '是' : '否'),
+    width: 120,
+    sorter: (a: any, b: any) => sortString(a.instanceNum, b.instanceNum)
+  },
+  {
+    title: 'operation',
+    key: 'operation',
+    dataIndex: 'operation',
+    width: 200
+  }
+]
+const searchDomain = reactive(
+  new SearchDomain(
+    [
+      {
+        label: 'serviceGovernance',
+        param: 'serviceGovernance',
+        placeholder: 'typeRoutingRules',
+        style: {
+          width: '200px'
+        }
+      }
+    ],
+    searchTagRule,
+    columns
+  )
+)
+
+onMounted(() => {
+  searchDomain.onSearch()
+})
+
+const confirm = () => {}
+
+provide(PROVIDE_INJECT_KEY.SEARCH_DOMAIN, searchDomain)
 </script>
-<style lang="less" scoped></style>
+<style lang="less" scoped>
+.search-table-container {
+  min-height: 60vh;
+  //max-height: 70vh; //overflow: auto;
+}
+</style>
diff --git a/ui-vue3/src/views/traffic/virtualService/index.vue b/ui-vue3/src/views/traffic/virtualService/index.vue
new file mode 100644
index 0000000..4e1f049
--- /dev/null
+++ b/ui-vue3/src/views/traffic/virtualService/index.vue
@@ -0,0 +1,110 @@
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one or more
+  ~ contributor license agreements.  See the NOTICE file distributed with
+  ~ this work for additional information regarding copyright ownership.
+  ~ The ASF licenses this file to You under the Apache License, Version 2.0
+  ~ (the "License"); you may not use this file except in compliance with
+  ~ the License.  You may obtain a copy of the License at
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+-->
+<template>
+  <div class="__container_resources_application_index">
+    <search-table :search-domain="searchDomain">
+      <template #customOperation>
+        <a-button type="primary">新增路由规则</a-button>
+      </template>
+      <template #bodyCell="{ text, column }">
+        <template v-if="column.dataIndex === 'ruleName'">
+          <a-button type="link">{{ text }}</a-button>
+        </template>
+        <template v-if="column.dataIndex === 'operation'">
+          <a-button type="link">查看</a-button>
+          <a-button type="link">修改</a-button>
+          <a-popconfirm
+            title="确认删除该动态配置?"
+            ok-text="Yes"
+            cancel-text="No"
+            @confirm="confirm"
+          >
+            <a-button type="link">删除</a-button>
+          </a-popconfirm>
+        </template>
+      </template>
+    </search-table>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { onMounted, provide, reactive } from 'vue'
+import { searchVirtualService } from '@/api/service/traffic'
+import SearchTable from '@/components/SearchTable.vue'
+import { SearchDomain, sortString } from '@/utils/SearchUtil'
+import { PROVIDE_INJECT_KEY } from '@/base/enums/ProvideInject'
+
+let columns = [
+  {
+    title: 'ruleName',
+    key: 'ruleName',
+    dataIndex: 'ruleName',
+    sorter: (a: any, b: any) => sortString(a.appName, b.appName),
+    width: 140
+  },
+  {
+    title: 'createTime',
+    key: 'createTime',
+    dataIndex: 'createTime',
+    width: 120,
+    sorter: (a: any, b: any) => sortString(a.instanceNum, b.instanceNum)
+  },
+  {
+    title: 'lastModifiedTime',
+    key: 'lastModifiedTime',
+    dataIndex: 'lastModifiedTime',
+    width: 120,
+    sorter: (a: any, b: any) => sortString(a.instanceNum, b.instanceNum)
+  },
+  {
+    title: 'operation',
+    key: 'operation',
+    dataIndex: 'operation',
+    width: 200
+  }
+]
+const searchDomain = reactive(
+  new SearchDomain(
+    [
+      {
+        label: 'serviceGovernance',
+        param: 'serviceGovernance',
+        placeholder: 'typeRoutingRules',
+        style: {
+          width: '200px'
+        }
+      }
+    ],
+    searchVirtualService,
+    columns
+  )
+)
+
+onMounted(() => {
+  searchDomain.onSearch()
+})
+
+const confirm = () => {}
+
+provide(PROVIDE_INJECT_KEY.SEARCH_DOMAIN, searchDomain)
+</script>
+<style lang="less" scoped>
+.search-table-container {
+  min-height: 60vh;
+  //max-height: 70vh; //overflow: auto;
+}
+</style>