ソースを参照

feat: 添加SDK管理模块及相关组件

新增SDK管理路由页面,包含以下功能:
- 添加面包屑导航组件
- 实现表格卡片组件,支持列筛选和分页
- 添加标签页过滤组件
- 配置Element Plus中文语言包
- 优化主题样式和按钮样式
- 注册全局组件和指令
piks 6 日 前
コミット
5d96c8da3f

+ 18 - 0
components.d.ts

@@ -11,10 +11,17 @@ export {}
 /* prettier-ignore */
 declare module 'vue' {
   export interface GlobalComponents {
+    Breadcrumb: typeof import('./src/components/Breadcrumb/index.vue')['default']
+    ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb']
+    ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem']
     ElButton: typeof import('element-plus/es')['ElButton']
+    ElCard: typeof import('element-plus/es')['ElCard']
+    ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
+    ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
     ElDropdown: typeof import('element-plus/es')['ElDropdown']
     ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
     ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
+    ElEmpty: typeof import('element-plus/es')['ElEmpty']
     ElForm: typeof import('element-plus/es')['ElForm']
     ElFormItem: typeof import('element-plus/es')['ElFormItem']
     ElIcon: typeof import('element-plus/es')['ElIcon']
@@ -22,12 +29,23 @@ declare module 'vue' {
     ElLink: typeof import('element-plus/es')['ElLink']
     ElMenu: typeof import('element-plus/es')['ElMenu']
     ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
+    ElPagination: typeof import('element-plus/es')['ElPagination']
+    ElPopover: typeof import('element-plus/es')['ElPopover']
     ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
     ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
     ElSwitch: typeof import('element-plus/es')['ElSwitch']
+    ElTable: typeof import('element-plus/es')['ElTable']
+    ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
+    ElTag: typeof import('element-plus/es')['ElTag']
+    Pagination: typeof import('./src/components/Pagination/index.vue')['default']
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterView: typeof import('vue-router')['RouterView']
     SvgIcon: typeof import('./src/components/SvgIcon/index.vue')['default']
+    TabFilter: typeof import('./src/components/TabFilter/index.vue')['default']
+    TableCard: typeof import('./src/components/TableCard/index.vue')['default']
     ThemeToggle: typeof import('./src/components/ThemeToggle/index.vue')['default']
   }
+  export interface GlobalDirectives {
+    vLoading: typeof import('element-plus/es')['ElLoadingDirective']
+  }
 }

+ 106 - 0
src/components/Breadcrumb/index.vue

@@ -0,0 +1,106 @@
+<script setup lang="ts">
+import { computed } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
+import { ArrowLeft } from '@element-plus/icons-vue'
+
+const route = useRoute()
+const router = useRouter()
+
+interface BreadcrumbItem {
+  title: string
+  path?: string
+  isCurrent?: boolean
+}
+
+const breadcrumbs = computed<BreadcrumbItem[]>(() => {
+  const matched = route.matched.filter(item => item.meta?.title && !item.meta?.hidden)
+
+  return matched.map((item, index) => ({
+    title: item.meta?.title as string,
+    path: item.path,
+    isCurrent: index === matched.length - 1
+  }))
+})
+
+const canGoBack = computed(() => {
+  return window.history.length > 1
+})
+
+function goBack() {
+  router.back()
+}
+
+function handleClick(item: BreadcrumbItem) {
+  if (item.isCurrent || !item.path) return
+  router.push(item.path)
+}
+</script>
+
+<template>
+  <div class="breadcrumb-wrapper">
+    <!-- <el-button v-if="canGoBack" class="back-btn" :icon="ArrowLeft" circle size="small" @click="goBack" /> -->
+    <el-breadcrumb separator="/">
+      <el-breadcrumb-item v-for="item in breadcrumbs" :key="item.path"
+        :class="{ 'is-clickable': !item.isCurrent && item.path }" @click="handleClick(item)">
+        <span :class="{ 'current-title': item.isCurrent }">
+          {{ item.title }}
+        </span>
+      </el-breadcrumb-item>
+    </el-breadcrumb>
+  </div>
+</template>
+
+<style scoped lang="scss">
+.breadcrumb-wrapper {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  margin-bottom: 20px;
+}
+
+.back-btn {
+  border: none;
+  background: transparent;
+  color: var(--admin-text-secondary);
+
+  &:hover {
+    background: rgba(255, 255, 255, 0.1);
+    color: var(--admin-text-primary);
+  }
+}
+
+:deep(.el-breadcrumb) {
+  font-size: 14px;
+
+  .el-breadcrumb__item {
+    .el-breadcrumb__inner {
+      color: var(--admin-text-secondary);
+    }
+
+    &.is-clickable {
+      .el-breadcrumb__inner {
+        cursor: pointer;
+        transition: color 0.2s;
+
+        &:hover {
+          color: var(--el-color-primary);
+        }
+      }
+    }
+
+    &:last-child {
+      .el-breadcrumb__inner {
+        color: var(--admin-text-primary);
+      }
+    }
+  }
+
+  .el-breadcrumb__separator {
+    color: var(--admin-text-secondary);
+  }
+}
+
+.current-title {
+  font-weight: 500;
+}
+</style>

+ 60 - 0
src/components/Pagination/index.vue

@@ -0,0 +1,60 @@
+<script setup lang="ts">
+interface PaginationProps {
+  total: number
+  currentPage?: number
+  pageSize?: number
+  pageSizes?: number[]
+  layout?: string
+  background?: boolean
+  small?: boolean
+  disabled?: boolean
+  hideOnSinglePage?: boolean
+}
+
+const props = withDefaults(defineProps<PaginationProps>(), {
+  currentPage: 1,
+  pageSize: 10,
+  pageSizes: () => [10, 20, 50, 100],
+  layout: 'total, sizes, prev, pager, next, jumper',
+  background: true,
+  small: false,
+  disabled: false,
+  hideOnSinglePage: false
+})
+
+const emit = defineEmits<{
+  (e: 'update:currentPage', value: number): void
+  (e: 'update:pageSize', value: number): void
+  (e: 'change', currentPage: number, pageSize: number): void
+}>()
+
+const currentPageValue = computed({
+  get: () => props.currentPage,
+  set: (val: number) => emit('update:currentPage', val)
+})
+
+const pageSizeValue = computed({
+  get: () => props.pageSize,
+  set: (val: number) => emit('update:pageSize', val)
+})
+
+function handleChange(currentPage: number, pageSize: number) {
+  emit('change', currentPage, pageSize)
+}
+</script>
+
+<template>
+  <div class="pagination-wrapper">
+    <el-pagination v-model:current-page="currentPageValue" v-model:page-size="pageSizeValue" :total="total"
+      :page-sizes="pageSizes" :layout="layout" :background="background" :small="small" :disabled="disabled"
+      :hide-on-single-page="hideOnSinglePage" @change="handleChange" />
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.pagination-wrapper {
+  display: flex;
+  justify-content: center;
+  padding: 16px 0;
+}
+</style>

+ 83 - 0
src/components/TabFilter/index.vue

@@ -0,0 +1,83 @@
+<script setup lang="ts">
+import { computed } from 'vue'
+
+export interface TabOption {
+  label: string
+  value: string | number
+}
+
+const props = defineProps<{
+  options: TabOption[]
+  modelValue: string | number
+}>()
+
+const emit = defineEmits<{
+  (e: 'update:modelValue', value: string | number): void
+  (e: 'change', value: string | number): void
+}>()
+
+const activeValue = computed({
+  get: () => props.modelValue,
+  set: (val) => {
+    emit('update:modelValue', val)
+    emit('change', val)
+  }
+})
+
+function handleClick(value: string | number) {
+  activeValue.value = value
+}
+</script>
+
+<template>
+  <div class="tab-filter">
+    <div v-for="option in options" :key="option.value" class="tab-item"
+      :class="{ active: activeValue === option.value }" @click="handleClick(option.value)">
+      {{ option.label }}
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.tab-filter {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  padding: 22px 4px;
+  background: var(--admin-card-bg);
+  border-radius: 8px;
+}
+
+.tab-item {
+  position: relative;
+  padding: 8px 16px;
+  font-size: 14px;
+  font-weight: 400;
+  color: var(--admin-text-secondary);
+  cursor: pointer;
+  border-radius: 6px;
+  transition: all 0.3s ease;
+  user-select: none;
+
+  &:hover {
+    color: var(--admin-text-primary);
+  }
+
+  &.active {
+    color: var(--el-color-primary);
+    background: rgba(128, 119, 255, 0.1);
+
+    &::after {
+      content: '';
+      position: absolute;
+      bottom: -8px;
+      left: 50%;
+      transform: translateX(-50%);
+      width: 80%;
+      height: 2px;
+      background: var(--el-color-primary);
+      border-radius: 1px;
+    }
+  }
+}
+</style>

+ 236 - 0
src/components/TableCard/index.vue

@@ -0,0 +1,236 @@
+<script setup lang="ts">
+import Pagination from '@/components/Pagination/index.vue'
+import { Setting, Refresh } from '@element-plus/icons-vue'
+
+interface BaseColumn {
+  width?: number | string
+  minWidth?: number | string
+  fixed?: 'left' | 'right' | boolean
+  sortable?: boolean | string
+  align?: 'left' | 'center' | 'right'
+  formatter?: (row: any, column: any, cellValue: any, index: number) => any
+  hide?: boolean
+  isAction?: boolean
+}
+
+interface TypeColumn extends BaseColumn {
+  type: 'selection' | 'index' | 'expand'
+  label?: string
+  prop?: string
+}
+
+interface PropColumn extends BaseColumn {
+  type?: never
+  prop?: string
+  label: string
+}
+
+type TableColumn = TypeColumn | PropColumn
+
+interface TableCardProps {
+  data: any[]
+  columns: TableColumn[]
+  loading?: boolean
+  stripe?: boolean
+  border?: boolean
+  showPagination?: boolean
+  showColumnFilter?: boolean
+  pageNo?: number
+  pageSize?: number
+  total?: number
+}
+
+const props = withDefaults(defineProps<TableCardProps>(), {
+  data: () => [],
+  columns: () => [],
+  loading: false,
+  stripe: true,
+  border: false,
+  showPagination: true,
+  showColumnFilter: true,
+  pageNo: 1,
+  pageSize: 10,
+  total: 0
+})
+
+const emit = defineEmits<{
+  (e: 'update:pageNo', value: number): void
+  (e: 'update:pageSize', value: number): void
+  (e: 'change', pageNo: number, pageSize: number): void
+  (e: 'selection-change', selection: any[]): void
+  (e: 'sort-change', sortInfo: { prop: string; order: string }): void
+}>()
+
+const visibleColumns = ref<string[]>([])
+
+const filterableColumns = computed(() => {
+  return props.columns.filter(col => col.prop && !col.type && !col.isAction)
+})
+
+const displayColumns = computed(() => {
+  return props.columns.filter(col => {
+    if (col.type) return true      // selection/index/expand 始终显示
+    if (col.isAction) return true  // 操作列始终显示
+    if (!col.prop) return false
+    return visibleColumns.value.includes(col.prop)
+  })
+})
+
+function initVisibleColumns() {
+  visibleColumns.value = filterableColumns.value
+    .filter(col => !col.hide)
+    .map(col => col.prop as string)
+}
+
+function handleCheckAll(val: boolean | string | number) {
+  if (val) {
+    visibleColumns.value = filterableColumns.value.map(col => col.prop as string)
+  } else {
+    visibleColumns.value = []
+  }
+}
+
+const isAllChecked = computed(() => {
+  return filterableColumns.value.length > 0 &&
+    visibleColumns.value.length === filterableColumns.value.length
+})
+
+const isIndeterminate = computed(() => {
+  const len = visibleColumns.value.length
+  return len > 0 && len < filterableColumns.value.length
+})
+
+function handlePageChange(pageNo: number, pageSize: number) {
+  emit('update:pageNo', pageNo)
+  emit('update:pageSize', pageSize)
+  emit('change', pageNo, pageSize)
+}
+
+function handleSelectionChange(selection: any[]) {
+  emit('selection-change', selection)
+}
+
+function handleSortChange({ prop, order }: { prop: string; order: string | null }) {
+  emit('sort-change', { prop, order: order || '' })
+}
+
+watch(
+  () => props.columns,
+  () => { initVisibleColumns() },
+  { immediate: true, deep: true }
+)
+</script>
+
+<template>
+  <el-card class="table-card" shadow="never">
+    <div class="table-toolbar">
+      <div class="toolbar-left">
+        <slot name="toolbar-left" />
+      </div>
+      <div class="toolbar-right">
+        <slot name="toolbar-right" />
+
+        <el-popover v-if="showColumnFilter && filterableColumns.length > 0" placement="bottom-end" :width="200"
+          trigger="click">
+          <template #reference>
+            <el-button :icon="Setting" circle />
+          </template>
+
+          <div class="column-filter">
+            <div class="filter-header">
+              <el-checkbox :model-value="isAllChecked" :indeterminate="isIndeterminate" @change="handleCheckAll">
+                全部
+              </el-checkbox>
+              <el-button type="primary" link size="small" :icon="Refresh" @click="initVisibleColumns">
+                重置
+              </el-button>
+            </div>
+
+            <el-checkbox-group v-model="visibleColumns" class="filter-list">
+              <el-checkbox v-for="col in filterableColumns" :key="col.prop" :value="col.prop">
+                {{ col.label }}
+              </el-checkbox>
+            </el-checkbox-group>
+          </div>
+        </el-popover>
+      </div>
+    </div>
+
+    <el-table :data="data" :stripe="stripe" :border="border" v-loading="loading"
+      @selection-change="handleSelectionChange" @sort-change="handleSortChange">
+      <template v-for="col in displayColumns" :key="col.prop ?? col.type ?? col.label">
+        <el-table-column v-if="col.type" :type="col.type" :width="col.width" :align="col.align ?? 'center'" />
+        <el-table-column v-else :prop="col.prop" :label="col.label" :width="col.width" :min-width="col.minWidth"
+          :sortable="col.sortable" :align="col.align ?? 'center'"
+          :fixed="col.isAction ? (col.fixed ?? 'right') : col.fixed">
+          <template #default="scope">
+            <slot :name="`column-${col.prop ?? col.label}`" :row="scope.row" :$index="scope.$index">
+              {{ col.formatter
+                ? col.formatter(scope.row, scope.column, col.prop ? scope.row[String(col.prop)] : undefined, scope.$index)
+                : scope.row[String(col.prop)]
+              }}
+            </slot>
+          </template>
+        </el-table-column>
+      </template>
+
+      <template #empty>
+        <slot name="empty">
+          <el-empty description="暂无数据" />
+        </slot>
+      </template>
+    </el-table>
+
+    <Pagination v-if="showPagination" :current-page="pageNo" :page-size="pageSize" :total="total"
+      @update:current-page="(val) => emit('update:pageNo', val)"
+      @update:page-size="(val) => emit('update:pageSize', val)" @change="handlePageChange" />
+
+    <slot name="footer" />
+  </el-card>
+</template>
+
+<style lang="scss" scoped>
+.table-card {
+  margin-top: 10px;
+}
+
+.table-toolbar {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 16px;
+}
+
+.toolbar-left,
+.toolbar-right {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.column-filter {
+
+  // ✅ 全部 + 重置:左右分布,垂直居中
+  .filter-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding-bottom: 8px;
+    margin-bottom: 8px;
+    border-bottom: 1px solid var(--el-border-color-lighter);
+  }
+
+  // ✅ 一行一个 checkbox
+  .filter-list {
+    display: flex;
+    flex-direction: column;
+    max-height: 300px;
+    overflow-y: auto;
+
+    :deep(.el-checkbox) {
+      height: 32px;
+      margin-right: 0;
+    }
+  }
+}
+</style>

+ 9 - 5
src/layout/components/AppMain.vue

@@ -1,14 +1,18 @@
 <script setup lang="ts">
 // AppMain 组件 - 主内容区域
+import { ElConfigProvider } from 'element-plus'
+import zhCn from 'element-plus/es/locale/lang/zh-cn'
 </script>
 
 <template>
   <main class="app-main">
-    <router-view v-slot="{ Component }">
-      <transition name="fade" mode="out-in">
-        <component :is="Component" />
-      </transition>
-    </router-view>
+    <ElConfigProvider :locale="zhCn">
+      <router-view v-slot="{ Component }">
+        <transition name="fade" mode="out-in">
+          <component :is="Component" />
+        </transition>
+      </router-view>
+    </ElConfigProvider>
   </main>
 </template>
 

+ 7 - 2
src/main.ts

@@ -2,13 +2,14 @@ import { createApp } from 'vue'
 import App from './App.vue'
 import router from './router'
 import { createPinia } from 'pinia'
-import ElementPlus from 'element-plus'
 import * as ElementPlusIconsVue from '@element-plus/icons-vue'
 import 'element-plus/dist/index.css'
 import 'element-plus/theme-chalk/dark/css-vars.css'
 import 'normalize.css/normalize.css'
 import '@/styles/theme.scss'
 import '@/styles/element.scss'
+import Breadcrumb from '@/components/Breadcrumb/index.vue'
+import Pagination from '@/components/Pagination/index.vue'
 
 // 注册 SVG 图标组件
 import SvgIcon from '@/components/SvgIcon/index.vue'
@@ -26,6 +27,10 @@ for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
 }
 // 注册 SVG 图标组件
 app.component('SvgIcon', SvgIcon)
+// 注册面包屑组件
+app.component('Breadcrumb', Breadcrumb)
+// 注册分页组件
+app.component('Pagination', Pagination)
 
-app.use(pinia).use(router).use(ElementPlus)
+app.use(pinia).use(router)
 app.mount('#app')

+ 6 - 0
src/mock/index.ts

@@ -21,6 +21,12 @@ const allAsyncRoutes: MenuItem[] = [
     component: 'dashboard/index',
     meta: { title: '首页', icon: 'HomeFilled', roles: ['admin', 'editor'] }
   },
+  {
+    path:"/sdk",
+    name:"Sdk",
+    component:"sdk/index",
+    meta:{title:"SDK管理",icon:"Box",roles:["admin"]}
+  },
   {
     path: '/system',
     name: 'System',

+ 33 - 0
src/styles/element.scss

@@ -17,4 +17,37 @@
   }
 }
 
+// 普通主按钮 —— 纯色背景,不使用渐变色
+.el-button--primary.is-solid {
+  border: none;
+  background: var(--el-color-primary-dark);
+  color: #fff;
+
+  // &:hover {
+  //   background: var(--el-color-primary-light-3);
+  // }
+
+  &.is-disabled {
+    color: rgba(255, 255, 255, 0.5);
+    transform: none;
+    cursor: not-allowed;
+    background: var(--el-color-primary-dark);
+  }
+}
 
+// 使用 :root 提高优先级,覆盖 Element Plus 默认样式
+:root .el-card {
+  border: none;
+  border-radius: 10px;
+}
+
+:root .el-table {
+  border: 1px solid var(--admin-border-color);
+  border-radius: 8px;
+  .el-table__header-wrapper,
+  .el-table__fixed-header-wrapper {
+    th {
+      background: var(--admin-table-header-bg);
+    }
+  }
+}

+ 5 - 4
src/styles/theme.scss

@@ -1,7 +1,7 @@
 // ========== 亮色模式(默认) ==========
 :root {
   // 覆盖 Element Plus 主色
-  --el-color-primary: #8077FF;
+  --el-color-primary: #8077ff;
   --el-color-primary-light-3: #79bbff;
   --el-color-primary-light-5: #a0cfff;
   --el-color-primary-dark-2: #337ecc;
@@ -24,10 +24,10 @@
 // ========== 暗色模式 ==========
 html.dark {
   // 覆盖 Element Plus 暗色下的主色(可选调整)
-  --el-color-primary: #8077FF;
+  --el-color-primary: #8077ff;
   --el-color-primary-light-3: #3375b9;
   --el-color-primary-light-5: #2a598a;
-  --el-color-primary-dark-2: #66b1ff;
+  --el-color-primary-dark: rgba(255, 255, 255, 0.08);
 
   // 暗色下的渐变按钮
   --btn-primary-gradient: linear-gradient(135deg, #5a6fe0 0%, #6a3f9a 100%);
@@ -41,7 +41,8 @@ html.dark {
   --admin-card-bg: #1d1e1f;
   --admin-text-primary: #e5eaf3;
   --admin-text-secondary: #a3a6ad;
-  --admin-border-color: #414243;
+  --admin-border-color: rgba(255, 255, 255, 0.1);
+  --admin-table-header-bg: rgba(217, 217, 217, 0.1);
 }
 
 // 全局平滑过渡(主题切换时)

+ 122 - 0
src/views/sdk/index.vue

@@ -0,0 +1,122 @@
+<template>
+  <div>
+    <Breadcrumb />
+    <TabFilter v-model="activeTab" :options="options" @change="handleTabChange" />
+    <TableCard :data="tableData" :columns="columns" :current-page="pageNo" :page-size="pageSize" :total="total"
+      @update:current-page="pageNo = $event" @update:page-size="pageSize = $event" @change="handlePageChange"
+      @selection-change="handleSelectionChange">
+      <template #toolbar-left>
+        <el-button type="primary" size="large" class="is-solid" @click="handleAdd">新增</el-button>
+        <el-button size="large" @click="handleBatchDelete">批量删除</el-button>
+      </template>
+
+      <template #toolbar-right>
+        <el-input v-model="searchKeyword" placeholder="请输入关键词搜索" style="width: 200px" clearable />
+        <el-button type="primary" size="large" @click="handleSearch">搜索</el-button>
+      </template>
+
+      <template #column-status="{ row }">
+        <el-tag :type="row.status === '正常' ? 'success' : 'danger'">
+          {{ row.status }}
+        </el-tag>
+      </template>
+
+      <template #column-operation="{ row }">
+        <el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
+        <el-button type="danger" link @click="handleDelete(row)">删除</el-button>
+      </template>
+    </TableCard>
+  </div>
+</template>
+
+<script setup lang="ts">
+import TabFilter from '@/components/TabFilter/index.vue'
+import TableCard from '@/components/TableCard/index.vue'
+
+const activeTab = ref('all')
+const searchKeyword = ref('')
+const selectedRows = ref<any[]>([])
+
+const state = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  total: 0
+})
+
+const { pageNo, pageSize, total } = toRefs(state)
+
+const options = [
+  { label: '全部', value: 'all' },
+  { label: '正常', value: 'normal' },
+  { label: '即将到期', value: 'expiring' },
+  { label: '禁用', value: 'disabled' }
+]
+
+const columns = [
+  { type: 'selection' as const, width: 55 },
+  { prop: 'name', label: '实例唯一标识', minWidth: 120 },
+  { prop: 'userId', label: '所属用户', minWidth: 100 },
+  { prop: 'address', label: '实例名称', minWidth: 100 },
+  { prop: 'expireTime', label: '到期时间', minWidth: 120 },
+  { prop: 'instanceType', label: '实例类型', minWidth: 100 },
+  { prop: 'status', label: '状态', minWidth: 80 },
+  { prop: 'noHeartbeat', label: '无心跳通讯', minWidth: 100 },
+  { prop: 'noDataTimeout', label: '无数据超时时间', minWidth: 120 },
+  { prop: 'enableLTS', label: '是否启用LTS', minWidth: 110 },
+  { prop: 'autoReconnect', label: '断线自动重连', minWidth: 110 },
+  { prop: 'maxConcurrent', label: '客户端最大并发数', minWidth: 130 },
+  { prop: 'createTime', label: '创建时间', minWidth: 150 },
+  { prop: 'operation', label: '操作', minWidth: 120, isAction: true }
+]
+
+const tableData = ref([
+  {
+    name: '实例1',
+    userId: '用户1',
+    address: '实例1',
+    expireTime: '2023-12-31',
+    instanceType: '普通实例',
+    status: '正常',
+    noHeartbeat: '是',
+    noDataTimeout: '10秒',
+    enableLTS: '是',
+    autoReconnect: '是',
+    maxConcurrent: '100',
+    createTime: '2023-01-01 10:00:00'
+  }
+])
+
+function handleTabChange(value: string | number) {
+  console.log('切换到:', value)
+}
+
+function handlePageChange(page: number, size: number) {
+  console.log('分页变化:', page, size)
+}
+
+function handleSelectionChange(selection: any[]) {
+  selectedRows.value = selection
+}
+
+function handleSearch() {
+  console.log('搜索:', searchKeyword.value)
+}
+
+function handleAdd() {
+  console.log('新增')
+}
+
+function handleBatchDelete() {
+  console.log('批量删除:', selectedRows.value)
+}
+
+function handleEdit(row: any) {
+  console.log('编辑:', row)
+}
+
+function handleDelete(row: any) {
+  console.log('删除:', row)
+}
+</script>
+
+<style lang="scss" scoped></style>