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

feat: 扫货成功数据列表增加搜索和导出功能

Made-with: Cursor
wuzilong 6 месяцев назад
Родитель
Сommit
fade882e95

+ 105 - 1
client-tauri/package-lock.json

@@ -15,7 +15,8 @@
         "element-plus": "^2.3.0",
         "pinia": "^2.1.0",
         "vue": "^3.3.0",
-        "vue-router": "^4.2.0"
+        "vue-router": "^4.2.0",
+        "xlsx": "0.18.5"
       },
       "devDependencies": {
         "@tauri-apps/cli": "^1.5.0",
@@ -977,6 +978,15 @@
         "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
       }
     },
+    "node_modules/adler-32": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmmirror.com/adler-32/-/adler-32-1.3.1.tgz",
+      "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
     "node_modules/ajv": {
       "version": "6.12.6",
       "dev": true,
@@ -1114,6 +1124,19 @@
         "node": ">=6"
       }
     },
+    "node_modules/cfb": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmmirror.com/cfb/-/cfb-1.2.2.tgz",
+      "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "adler-32": "~1.3.0",
+        "crc-32": "~1.2.0"
+      },
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
     "node_modules/chalk": {
       "version": "4.1.2",
       "dev": true,
@@ -1129,6 +1152,15 @@
         "url": "https://github.com/chalk/chalk?sponsor=1"
       }
     },
+    "node_modules/codepage": {
+      "version": "1.15.0",
+      "resolved": "https://registry.npmmirror.com/codepage/-/codepage-1.15.0.tgz",
+      "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
     "node_modules/color-convert": {
       "version": "2.0.1",
       "dev": true,
@@ -1183,6 +1215,18 @@
         "url": "https://opencollective.com/core-js"
       }
     },
+    "node_modules/crc-32": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmmirror.com/crc-32/-/crc-32-1.2.2.tgz",
+      "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
+      "license": "Apache-2.0",
+      "bin": {
+        "crc32": "bin/crc32.njs"
+      },
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
     "node_modules/cross-spawn": {
       "version": "7.0.6",
       "dev": true,
@@ -1987,6 +2031,15 @@
       "dev": true,
       "license": "ISC"
     },
+    "node_modules/frac": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmmirror.com/frac/-/frac-1.1.2.tgz",
+      "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
     "node_modules/fs.realpath": {
       "version": "1.0.0",
       "dev": true,
@@ -2904,6 +2957,18 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/ssf": {
+      "version": "0.11.2",
+      "resolved": "https://registry.npmmirror.com/ssf/-/ssf-0.11.2.tgz",
+      "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "frac": "~1.1.2"
+      },
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
     "node_modules/strip-ansi": {
       "version": "6.0.1",
       "dev": true,
@@ -3236,6 +3301,24 @@
         "node": ">= 8"
       }
     },
+    "node_modules/wmf": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/wmf/-/wmf-1.0.2.tgz",
+      "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
+    "node_modules/word": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmmirror.com/word/-/word-0.3.0.tgz",
+      "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
     "node_modules/word-wrap": {
       "version": "1.2.5",
       "dev": true,
@@ -3249,6 +3332,27 @@
       "dev": true,
       "license": "ISC"
     },
+    "node_modules/xlsx": {
+      "version": "0.18.5",
+      "resolved": "https://registry.npmmirror.com/xlsx/-/xlsx-0.18.5.tgz",
+      "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "adler-32": "~1.3.0",
+        "cfb": "~1.2.1",
+        "codepage": "~1.15.0",
+        "crc-32": "~1.2.1",
+        "ssf": "~0.11.2",
+        "wmf": "~1.0.1",
+        "word": "~0.3.0"
+      },
+      "bin": {
+        "xlsx": "bin/xlsx.njs"
+      },
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
     "node_modules/xml-name-validator": {
       "version": "4.0.0",
       "dev": true,

+ 3 - 2
client-tauri/package.json

@@ -13,7 +13,7 @@
     "tauri:build": "tauri build",
     "tauri:build:all": "tauri build --target all",
     "tauri:build:windows": "tauri build",
-    "tauri:build:prod": "set DEFAULT_BACKEND_API_URL=http://125.122.32.114:9000&& tauri build",
+    "tauri:build:prod": "set DEFAULT_BACKEND_API_URL=http://192.168.2.54:9000&& tauri build",
     "build:windows:with-name": "powershell -ExecutionPolicy Bypass -File build-windows.ps1 -RunBuild",
     "postembed": "powershell -ExecutionPolicy Bypass -File src-tauri/post_embed_icon.ps1",
     "tauri:build:windows:with-icon": "npm run tauri:build:windows && npm run postembed && npm run nsis:pack",
@@ -26,12 +26,13 @@
   "dependencies": {
     "@element-plus/icons-vue": "^2.3.0",
     "@tauri-apps/api": "^1.5.0",
+    "ant-design-vue": "^4.0.0",
     "dayjs": "1.11.19",
     "element-plus": "^2.3.0",
     "pinia": "^2.1.0",
     "vue": "^3.3.0",
     "vue-router": "^4.2.0",
-    "ant-design-vue": "^4.0.0"
+    "xlsx": "0.18.5"
   },
   "devDependencies": {
     "@tauri-apps/cli": "^1.5.0",

+ 126 - 6
client-tauri/src/views/BuffSelectionView.vue

@@ -391,17 +391,35 @@
           <div class="section-header-left">
             <span class="section-icon">🧾</span>
             <span class="section-title">扫货成功数据</span>
+            <div class="selection-search-bar">
+              <a-input
+                v-model:value="sweepSearchKeyword"
+                placeholder="按饰品ID或名称搜索"
+                class="selection-search-input"
+                allow-clear
+              />
+              <span v-if="sweepSearchKeyword && filteredSweepList.length !== sweepList.length" class="selection-search-hint">
+                匹配 {{ filteredSweepList.length }} / {{ sweepList.length }}
+              </span>
+            </div>
           </div>
           <div class="section-actions">
+            <a-button type="link" size="small" class="btn-export" @click="exportSweepTxt">
+              导出TXT
+            </a-button>
+            <a-button type="link" size="small" class="btn-export" :loading="exportingSweepExcel" @click="exportSweepExcel">
+              导出Excel
+            </a-button>
+            <a-divider type="vertical" class="btn-divider" />
             <a-button type="link" size="small" class="btn-log" @click="openSweepLogModal">
-              扫货日志查看
+              查看扫货日志
             </a-button>
           </div>
         </div>
         <div class="section-body table-wrap">
           <a-table
-            :columns="sweepColumns"
-            :data-source="sweepList"
+            :columns="filteredSweepColumns"
+            :data-source="paginatedSweepList"
             :pagination="sweepPagination"
             :loading="sweepTableLoading"
             :row-key="sweepRowKey"
@@ -1063,6 +1081,107 @@ const switchingStatus = computed(() => buffSweepStore.switchingStatus)
 const buffPrepareFor = ref<'selection' | 'sweep' | null>(null)
 const sweepTableLoading = ref(false)
 const sweepList = ref<any[]>([])
+const sweepSearchKeyword = ref('')
+const exportingSweepExcel = ref(false)
+
+const filteredSweepList = computed(() => {
+  if (!sweepSearchKeyword.value.trim()) {
+    return sweepList.value
+  }
+  const keyword = sweepSearchKeyword.value.trim().toLowerCase()
+  return sweepList.value.filter((item) => {
+    const idMatch = String(item.goods_id || item.id || '').toLowerCase().includes(keyword)
+    const nameMatch = (item.goods_name || item.name || '').toLowerCase().includes(keyword)
+    return idMatch || nameMatch
+  })
+})
+
+const paginatedSweepList = computed(() => {
+  const start = (sweepPagination.current - 1) * sweepPagination.pageSize
+  const end = start + sweepPagination.pageSize
+  return filteredSweepList.value.slice(start, end)
+})
+
+const filteredSweepColumns = computed(() => {
+  return sweepColumns.filter(col => col.key !== 'listing_id')
+})
+
+const formatSweepTime = (timestamp: number) => {
+  if (!timestamp) return '—'
+  const date = new Date(timestamp * 1000)
+  return date.toLocaleString('zh-CN', {
+    year: 'numeric',
+    month: '2-digit',
+    day: '2-digit',
+    hour: '2-digit',
+    minute: '2-digit',
+    second: '2-digit',
+    hour12: false
+  })
+}
+
+const exportSweepTxt = () => {
+  const data = sweepList.value
+  if (data.length === 0) {
+    message.warning('暂无数据可导出')
+    return
+  }
+  const exportData = data.map((item, index) => ({
+    '序号': index + 1,
+    '饰品ID': item.goods_id || '',
+    '饰品名称': item.goods_name || '',
+    '区间': item.wear_range || '',
+    '已购磨损值': item.purchase_wear || '',
+    '目标价格(美元)': item.target_price_usd || '',
+    '成交价格(美元)': item.purchase_price_usd || '',
+    '成交价格(人民币)': item.purchase_price_cny || '',
+    '购买账号': item.steam_account || '',
+    '扫货时间': formatSweepTime(item.created_at)
+  }))
+  const txtContent = JSON.stringify(exportData, null, 2)
+  const blob = new Blob(['\ufeff' + txtContent], { type: 'text/plain;charset=utf-8' })
+  const url = URL.createObjectURL(blob)
+  const link = document.createElement('a')
+  link.href = url
+  link.download = `扫货成功数据_${new Date().toISOString().slice(0, 10)}.txt`
+  link.click()
+  URL.revokeObjectURL(url)
+  message.success(`已导出 ${data.length} 条数据`)
+}
+
+const exportSweepExcel = async () => {
+  const data = sweepList.value
+  if (data.length === 0) {
+    message.warning('暂无数据可导出')
+    return
+  }
+  exportingSweepExcel.value = true
+  try {
+    const XLSX = await import('xlsx')
+    const exportData = data.map((item, index) => ({
+      '序号': index + 1,
+      '饰品ID': item.goods_id || '',
+      '饰品名称': item.goods_name || '',
+      '区间': item.wear_range || '',
+      '已购磨损值': item.purchase_wear || '',
+      '目标价格(美元)': item.target_price_usd || '',
+      '成交价格(美元)': item.purchase_price_usd || '',
+      '成交价格(人民币)': item.purchase_price_cny || '',
+      '购买账号': item.steam_account || '',
+      '扫货时间': formatSweepTime(item.created_at)
+    }))
+    const ws = XLSX.utils.json_to_sheet(exportData)
+    const wb = XLSX.utils.book_new()
+    XLSX.utils.book_append_sheet(wb, ws, '扫货成功数据')
+    XLSX.writeFile(wb, `扫货成功数据_${new Date().toISOString().slice(0, 10)}.xlsx`)
+    message.success(`已导出 ${data.length} 条数据`)
+  } catch (err) {
+    console.error('导出Excel失败:', err)
+    message.error('导出Excel失败')
+  } finally {
+    exportingSweepExcel.value = false
+  }
+}
 type BoundSteamApiAccount = {
   id: number
   steam_username: string
@@ -1385,9 +1504,11 @@ const tablePagination = reactive({
 const sweepPagination = reactive({
   current: 1,
   pageSize: 10,
-  total: 0,
+  total: computed(() => filteredSweepList.value.length),
   showSizeChanger: true,
-  showTotal: (total: number) => `共 ${total} 条`
+  showTotal: (total: number) => `共 ${total} 条`,
+  simple: false,
+  showQuickJumper: true
 })
 
 const columns = [
@@ -2779,7 +2900,6 @@ function startSweepPoll(showFinishMessage = false) {
 function onSweepTableChange(pagination: any) {
   sweepPagination.current = pagination?.current || 1
   sweepPagination.pageSize = pagination?.pageSize || 10
-  fetchSweepResult(true)
 }
 
 /** 判断是否为网络相关错误(需要重试) */