Просмотр исходного кода

feat: add excel copy toggle beside the list refresh icon

Co-authored-by: Cursor <cursoragent@cursor.com>
wuzilong 2 недель назад
Родитель
Сommit
bf0099c2ad
1 измененных файлов с 172 добавлено и 81 удалено
  1. 172 81
      web-admin/src/components/business/list-toolbar-end-actions/index.vue

+ 172 - 81
web-admin/src/components/business/list-toolbar-end-actions/index.vue

@@ -1,6 +1,38 @@
 <!-- 列表工具栏右侧:刷新 + 列显隐(勾选持久化由 usePersistedListColumns 负责) -->
 <template>
-  <div class="list-toolbar-end">
+  <div ref="rootEl" class="list-toolbar-end">
+    <el-tooltip
+      v-if="showExcelCopy"
+      :content="excelActive ? '退出 Excel 复制' : 'Excel 复制'"
+      placement="top"
+    >
+      <el-button
+        class="list-toolbar-end-btn"
+        :class="{ 'is-excel-active': excelActive }"
+        :aria-pressed="excelActive"
+        aria-label="Excel 复制"
+        @click="toggleExcelCopy"
+      >
+        <svg class="list-excel-icon" viewBox="0 0 16 16" aria-hidden="true">
+          <rect
+            x="1.5"
+            y="2"
+            width="13"
+            height="12"
+            rx="1.5"
+            fill="none"
+            stroke="currentColor"
+            stroke-width="1.4"
+          />
+          <path
+            d="M1.5 6.2h13M1.5 10h13M6.2 2v12M10.4 2v12"
+            stroke="currentColor"
+            stroke-width="1.1"
+          />
+        </svg>
+      </el-button>
+    </el-tooltip>
+
     <el-tooltip content="刷新列表" placement="top">
       <el-button
         class="list-toolbar-end-btn"
@@ -61,96 +93,155 @@
 </template>
 
 <script setup lang="ts">
-import { computed } from 'vue'
-import { Refresh, Setting } from '@element-plus/icons-vue'
-import type { ListColumnDef } from '@/composables/usePersistedListColumns'
-
-defineOptions({ name: 'ListToolbarEndActions' })
-
-const props = withDefaults(
-  defineProps<{
-    loading?: boolean
-    columnDefs?: ListColumnDef[]
-    visibleColumns?: string[]
-    checkAll?: boolean
-    isIndeterminate?: boolean
-    showColumns?: boolean
-  }>(),
-  {
-    loading: false,
-    columnDefs: () => [],
-    visibleColumns: () => [],
-    checkAll: false,
-    isIndeterminate: false,
-    showColumns: undefined
+  import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
+  import { Refresh, Setting } from '@element-plus/icons-vue'
+  import type { ListColumnDef } from '@/composables/usePersistedListColumns'
+
+  defineOptions({ name: 'ListToolbarEndActions' })
+
+  const props = withDefaults(
+    defineProps<{
+      loading?: boolean
+      columnDefs?: ListColumnDef[]
+      visibleColumns?: string[]
+      checkAll?: boolean
+      isIndeterminate?: boolean
+      showColumns?: boolean
+      showExcelCopy?: boolean
+    }>(),
+    {
+      loading: false,
+      columnDefs: () => [],
+      visibleColumns: () => [],
+      checkAll: false,
+      isIndeterminate: false,
+      showColumns: undefined,
+      showExcelCopy: true
+    }
+  )
+
+  const emit = defineEmits<{
+    refresh: []
+    'update:visibleColumns': [value: string[]]
+    'update:checkAll': [value: boolean]
+    resetColumns: []
+  }>()
+
+  const showColumns = computed(() => {
+    if (props.showColumns !== undefined) return props.showColumns
+    return props.columnDefs.length > 0
+  })
+
+  const checkAllProxy = computed({
+    get: () => props.checkAll,
+    set: (value: boolean) => emit('update:checkAll', value)
+  })
+
+  const handleColumnToggle = (prop: string, visible: boolean) => {
+    const next = new Set(props.visibleColumns)
+    if (visible) {
+      next.add(prop)
+    } else {
+      next.delete(prop)
+    }
+    const ordered = props.columnDefs
+      .filter((col) => col.disabled || next.has(col.prop))
+      .map((col) => col.prop)
+    emit('update:visibleColumns', ordered)
+  }
+
+  const handleReset = () => {
+    emit('resetColumns')
+  }
+
+  const excelActive = ref(false)
+  const rootEl = ref<HTMLElement | null>(null)
+  let panelEl: HTMLElement | null = null
+
+  function controlledTable(): HTMLElement | null {
+    const panel = rootEl.value?.closest('.panel')
+    const table = panel?.querySelector('[data-list-excel-copy="on"]')
+    return table instanceof HTMLElement ? table : null
   }
-)
-
-const emit = defineEmits<{
-  refresh: []
-  'update:visibleColumns': [value: string[]]
-  'update:checkAll': [value: boolean]
-  resetColumns: []
-}>()
-
-const showColumns = computed(() => {
-  if (props.showColumns !== undefined) return props.showColumns
-  return props.columnDefs.length > 0
-})
-
-const checkAllProxy = computed({
-  get: () => props.checkAll,
-  set: (value: boolean) => emit('update:checkAll', value)
-})
-
-const handleColumnToggle = (prop: string, visible: boolean) => {
-  const next = new Set(props.visibleColumns)
-  if (visible) {
-    next.add(prop)
-  } else {
-    next.delete(prop)
+
+  function toggleExcelCopy() {
+    controlledTable()?.dispatchEvent(new CustomEvent('sx-list-excel-toggle'))
+  }
+
+  function onExcelState(event: Event) {
+    const custom = event as CustomEvent<{ active?: boolean }>
+    if (event.target !== controlledTable()) return
+    excelActive.value = !!custom.detail?.active
   }
-  const ordered = props.columnDefs
-    .filter((col) => col.disabled || next.has(col.prop))
-    .map((col) => col.prop)
-  emit('update:visibleColumns', ordered)
-}
-
-const handleReset = () => {
-  emit('resetColumns')
-}
+
+  function attachExcelStateListener() {
+    const panel = rootEl.value?.closest('.panel')
+    if (!(panel instanceof HTMLElement)) return false
+    panel.addEventListener('sx-list-excel-state', onExcelState, true)
+    panelEl = panel
+    return true
+  }
+
+  onMounted(() => {
+    if (!attachExcelStateListener()) {
+      nextTick(() => {
+        attachExcelStateListener()
+      })
+    }
+  })
+
+  onBeforeUnmount(() => {
+    panelEl?.removeEventListener('sx-list-excel-state', onExcelState, true)
+    panelEl = null
+  })
 </script>
 
 <style scoped lang="scss">
-.list-toolbar-end-btn.el-button {
-  --el-button-border-color: transparent;
-  --el-button-hover-border-color: transparent;
-  --el-button-active-border-color: transparent;
-  width: 36px;
-  min-width: 36px;
-  height: 36px;
-  padding: 0;
-  border-radius: 8px;
-  color: var(--mint-text-muted);
-  border: none !important;
-  box-shadow: none;
-  background: transparent;
-
-  .el-icon {
-    font-size: 16px;
+  .list-toolbar-end-btn.el-button {
+    --el-button-border-color: transparent;
+    --el-button-hover-border-color: transparent;
+    --el-button-active-border-color: transparent;
+    width: 36px;
+    min-width: 36px;
+    height: 36px;
+    padding: 0;
+    border-radius: 8px;
+    color: var(--mint-text-muted);
+    border: none !important;
+    box-shadow: none;
+    background: transparent;
+
+    .el-icon {
+      font-size: 16px;
+    }
+
+    &:hover,
+    &:focus-visible {
+      color: var(--mint-primary-dark);
+      border: none !important;
+      box-shadow: none;
+      background: var(--mint-primary-bg);
+    }
+
+    &:active {
+      border: none !important;
+      box-shadow: none;
+    }
+
+    &.is-excel-active {
+      color: var(--mint-primary-dark);
+      background: var(--mint-primary-bg);
+    }
   }
 
-  &:hover,
-  &:focus-visible {
+  .list-toolbar-end-btn.is-excel-active {
     color: var(--mint-primary-dark);
-    border: none !important;
-    box-shadow: none;
     background: var(--mint-primary-bg);
   }
 
-  &:active {
-    border: none !important;
-    box-shadow: none;
+  .list-excel-icon {
+    width: 16px;
+    height: 16px;
+    display: block;
   }
-}
 </style>