SelectionTab.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818
  1. <template>
  2. <div class="selection-tab">
  3. <div class="tab-header">
  4. <h2>🛒 选品工具</h2>
  5. <p>智能选品,帮您快速找到优质商品</p>
  6. <div class="status-bar">
  7. <div class="status-indicator" :class="{ 'running': isRunning, 'stopped': !isRunning }">
  8. <span class="status-dot"></span>
  9. <span class="status-text">{{ isRunning ? '运行中' : '已停止' }}</span>
  10. </div>
  11. <div v-if="isRunning" class="running-info">
  12. <span>已处理: {{ processedCount }} 个商品</span>
  13. <span>找到: {{ selectedCount }} 个目标商品</span>
  14. </div>
  15. </div>
  16. </div>
  17. <div class="content-grid">
  18. <!-- 左侧:配置面板 -->
  19. <div class="left-panel">
  20. <div class="card">
  21. <h3>筛选条件</h3>
  22. <form @submit.prevent="saveConfig">
  23. <div class="form-row">
  24. <div class="form-group">
  25. <label>商品类型</label>
  26. <select v-model="config.category">
  27. <option value="">全部类型</option>
  28. <option value="rifle">步枪</option>
  29. <option value="pistol">手枪</option>
  30. <option value="knife">刀具</option>
  31. <option value="gloves">手套</option>
  32. <option value="other">其他</option>
  33. </select>
  34. </div>
  35. <div class="form-group">
  36. <label>品质等级</label>
  37. <select v-model="config.quality">
  38. <option value="">全部品质</option>
  39. <option value="consumer">消费级</option>
  40. <option value="industrial">工业级</option>
  41. <option value="mil-spec">军规级</option>
  42. <option value="restricted">受限</option>
  43. <option value="classified">保密</option>
  44. <option value="covert">隐秘</option>
  45. </select>
  46. </div>
  47. </div>
  48. <div class="form-row">
  49. <div class="form-group">
  50. <label>最低价格 (¥)</label>
  51. <input
  52. v-model.number="config.minPrice"
  53. type="number"
  54. step="0.01"
  55. min="0"
  56. placeholder="0.00"
  57. />
  58. </div>
  59. <div class="form-group">
  60. <label>最高价格 (¥)</label>
  61. <input
  62. v-model.number="config.maxPrice"
  63. type="number"
  64. step="0.01"
  65. min="0"
  66. placeholder="不限制"
  67. />
  68. </div>
  69. </div>
  70. <div class="form-row">
  71. <div class="form-group">
  72. <label>最小交易量</label>
  73. <input
  74. v-model.number="config.minVolume"
  75. type="number"
  76. min="0"
  77. placeholder="0"
  78. />
  79. </div>
  80. <div class="form-group">
  81. <label>最大交易量</label>
  82. <input
  83. v-model.number="config.maxVolume"
  84. type="number"
  85. min="0"
  86. placeholder="不限制"
  87. />
  88. </div>
  89. </div>
  90. <div class="form-group">
  91. <label class="checkbox-label">
  92. <input
  93. v-model="config.enableFloatRange"
  94. type="checkbox"
  95. />
  96. 启用磨损筛选
  97. </label>
  98. </div>
  99. <div v-if="config.enableFloatRange" class="form-row">
  100. <div class="form-group">
  101. <label>最小磨损</label>
  102. <input
  103. v-model.number="config.minFloat"
  104. type="number"
  105. step="0.0001"
  106. min="0"
  107. max="1"
  108. placeholder="0.0000"
  109. />
  110. </div>
  111. <div class="form-group">
  112. <label>最大磨损</label>
  113. <input
  114. v-model.number="config.maxFloat"
  115. type="number"
  116. step="0.0001"
  117. min="0"
  118. max="1"
  119. placeholder="1.0000"
  120. />
  121. </div>
  122. </div>
  123. <div class="form-actions">
  124. <button type="submit" :disabled="savingConfig" class="primary-btn">
  125. {{ savingConfig ? '保存中...' : '保存配置' }}
  126. </button>
  127. <button type="button" @click="resetConfig" class="secondary-btn">
  128. 重置
  129. </button>
  130. </div>
  131. </form>
  132. </div>
  133. <div class="card">
  134. <h3>运行控制</h3>
  135. <div class="control-buttons">
  136. <button
  137. @click="startSelection"
  138. :disabled="isRunning || isSweepRunning"
  139. class="primary-btn large"
  140. >
  141. {{ isRunning ? '运行中...' : '开始选品' }}
  142. </button>
  143. <button
  144. @click="stopSelection"
  145. :disabled="!isRunning"
  146. class="danger-btn large"
  147. >
  148. 停止选品
  149. </button>
  150. </div>
  151. <div v-if="globalState.isSweepRunning" class="warning-notice">
  152. ⚠️ 扫货功能正在运行,请先停止扫货再开始选品
  153. </div>
  154. </div>
  155. </div>
  156. <!-- 右侧:结果展示 -->
  157. <div class="right-panel">
  158. <div class="card">
  159. <h3>选品结果</h3>
  160. <div v-if="results.length === 0" class="no-results">
  161. <div class="empty-icon">📦</div>
  162. <p>暂无选品结果</p>
  163. <small>配置筛选条件并开始选品</small>
  164. </div>
  165. <div v-else class="results-list">
  166. <div class="result-header">
  167. <span>共找到 {{ results.length }} 个商品</span>
  168. <button @click="exportResults" class="export-btn">
  169. 导出结果
  170. </button>
  171. </div>
  172. <div class="result-items">
  173. <div
  174. v-for="item in results.slice(0, 50)"
  175. :key="item.id"
  176. class="result-item"
  177. >
  178. <div class="item-info">
  179. <div class="item-name">{{ item.name }}</div>
  180. <div class="item-price">¥{{ item.price }}</div>
  181. </div>
  182. <div class="item-meta">
  183. <span class="item-volume">{{ item.volume }}笔交易</span>
  184. <span class="item-quality">{{ item.quality }}</span>
  185. </div>
  186. </div>
  187. </div>
  188. </div>
  189. </div>
  190. </div>
  191. </div>
  192. <!-- 消息提示 -->
  193. <div v-if="message.show" class="message-toast" :class="message.type">
  194. {{ message.text }}
  195. </div>
  196. </div>
  197. </template>
  198. <script setup lang="ts">
  199. import { ref, reactive, onMounted, onUnmounted } from 'vue'
  200. import { invoke } from '@tauri-apps/api/tauri'
  201. import { getBeijingDateString } from '../../utils/timezone'
  202. interface UserInfo {
  203. user_id: number
  204. username: string
  205. expire_time: string
  206. }
  207. interface SelectionConfig {
  208. category: string
  209. quality: string
  210. minPrice: number | null
  211. maxPrice: number | null
  212. minVolume: number | null
  213. maxVolume: number | null
  214. enableFloatRange: boolean
  215. minFloat: number | null
  216. maxFloat: number | null
  217. }
  218. interface SelectionResult {
  219. id: string
  220. name: string
  221. price: number
  222. volume: number
  223. quality: string
  224. }
  225. // Props
  226. const props = defineProps<{
  227. userInfo: UserInfo
  228. globalState: {
  229. isSelectionRunning: boolean
  230. isSweepRunning: boolean
  231. }
  232. }>()
  233. // 响应式数据
  234. const isRunning = ref(false)
  235. const isSweepRunning = ref(false)
  236. const processedCount = ref(0)
  237. const selectedCount = ref(0)
  238. const savingConfig = ref(false)
  239. const results = ref<SelectionResult[]>([])
  240. const config = reactive<SelectionConfig>({
  241. category: '',
  242. quality: '',
  243. minPrice: null,
  244. maxPrice: null,
  245. minVolume: null,
  246. maxVolume: null,
  247. enableFloatRange: false,
  248. minFloat: null,
  249. maxFloat: null
  250. })
  251. const message = reactive({
  252. show: false,
  253. text: '',
  254. type: 'info' as 'info' | 'success' | 'error'
  255. })
  256. let statusTimer: number | null = null
  257. // 保存配置
  258. const saveConfig = async () => {
  259. savingConfig.value = true
  260. try {
  261. // 这里应该调用后端API保存配置
  262. // 暂时保存到本地存储
  263. const configStr = JSON.stringify(config)
  264. await invoke('store_selection_config', { config: configStr })
  265. showMessage('配置保存成功', 'success')
  266. } catch (error) {
  267. console.error('保存配置失败:', error)
  268. showMessage('配置保存失败', 'error')
  269. } finally {
  270. savingConfig.value = false
  271. }
  272. }
  273. // 重置配置
  274. const resetConfig = () => {
  275. Object.assign(config, {
  276. category: '',
  277. quality: '',
  278. minPrice: null,
  279. maxPrice: null,
  280. minVolume: null,
  281. maxVolume: null,
  282. enableFloatRange: false,
  283. minFloat: null,
  284. maxFloat: null
  285. })
  286. }
  287. // 开始选品
  288. const startSelection = async () => {
  289. try {
  290. // 检查是否有正在运行的扫货任务
  291. if (props.globalState.isSweepRunning) {
  292. showMessage('扫货功能正在运行,请先停止扫货', 'error')
  293. return
  294. }
  295. isRunning.value = true
  296. props.globalState.isSelectionRunning = true
  297. processedCount.value = 0
  298. selectedCount.value = 0
  299. results.value = []
  300. // 启动选品服务
  301. await invoke('start_selection', {
  302. config: JSON.stringify(config)
  303. })
  304. showMessage('选品任务已启动', 'success')
  305. // 开始状态更新定时器
  306. startStatusUpdate()
  307. } catch (error) {
  308. console.error('启动选品失败:', error)
  309. isRunning.value = false
  310. props.globalState.isSelectionRunning = false
  311. showMessage('启动选品失败', 'error')
  312. }
  313. }
  314. // 停止选品
  315. const stopSelection = async () => {
  316. try {
  317. await invoke('stop_selection')
  318. isRunning.value = false
  319. props.globalState.isSelectionRunning = false
  320. stopStatusUpdate()
  321. showMessage('选品任务已停止', 'info')
  322. } catch (error) {
  323. console.error('停止选品失败:', error)
  324. showMessage('停止选品失败', 'error')
  325. }
  326. }
  327. // 导出结果
  328. const exportResults = () => {
  329. if (results.value.length === 0) {
  330. showMessage('没有可导出的结果', 'info')
  331. return
  332. }
  333. // 创建CSV内容
  334. const csvContent = [
  335. ['商品名称', '价格', '交易量', '品质'].join(','),
  336. ...results.value.map(item =>
  337. [item.name, item.price, item.volume, item.quality].join(',')
  338. )
  339. ].join('\n')
  340. // 创建下载链接
  341. const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' })
  342. const link = document.createElement('a')
  343. link.href = URL.createObjectURL(blob)
  344. link.download = `选品结果_${getBeijingDateString()}.csv`
  345. link.click()
  346. showMessage('结果导出成功', 'success')
  347. }
  348. // 状态更新定时器
  349. const startStatusUpdate = () => {
  350. if (statusTimer) return
  351. statusTimer = window.setInterval(async () => {
  352. try {
  353. const status = await invoke<any>('get_selection_status')
  354. processedCount.value = status.processed_count || 0
  355. selectedCount.value = status.selected_count || 0
  356. results.value = status.results || []
  357. // 如果任务已完成或停止
  358. if (!status.running) {
  359. isRunning.value = false
  360. stopStatusUpdate()
  361. showMessage('选品任务已完成', 'success')
  362. }
  363. } catch (error) {
  364. console.error('获取状态失败:', error)
  365. }
  366. }, 2000) // 每2秒更新一次
  367. }
  368. const stopStatusUpdate = () => {
  369. if (statusTimer) {
  370. clearInterval(statusTimer)
  371. statusTimer = null
  372. }
  373. }
  374. // 检查互斥状态
  375. const checkMutexStatus = () => {
  376. // 直接使用全局状态
  377. isSweepRunning.value = props.globalState.isSweepRunning
  378. }
  379. // 消息提示
  380. const showMessage = (text: string, type: 'info' | 'success' | 'error' = 'info') => {
  381. message.text = text
  382. message.type = type
  383. message.show = true
  384. setTimeout(() => {
  385. message.show = false
  386. }, 3000)
  387. }
  388. // 加载配置
  389. const loadConfig = async () => {
  390. try {
  391. const configStr = await invoke<string>('get_selection_config')
  392. if (configStr) {
  393. const loadedConfig = JSON.parse(configStr)
  394. Object.assign(config, loadedConfig)
  395. }
  396. } catch (error) {
  397. console.log('加载配置失败,使用默认配置')
  398. }
  399. }
  400. // 组件生命周期
  401. onMounted(async () => {
  402. await Promise.all([
  403. loadConfig(),
  404. checkMutexStatus()
  405. ])
  406. })
  407. onUnmounted(() => {
  408. stopStatusUpdate()
  409. })
  410. </script>
  411. <style scoped>
  412. .selection-tab {
  413. max-width: 1200px;
  414. margin: 0 auto;
  415. }
  416. .tab-header {
  417. text-align: center;
  418. margin-bottom: 30px;
  419. position: relative;
  420. }
  421. .tab-header h2 {
  422. color: #333;
  423. margin-bottom: 8px;
  424. }
  425. .tab-header p {
  426. color: #666;
  427. margin: 0;
  428. }
  429. .status-bar {
  430. display: flex;
  431. justify-content: center;
  432. align-items: center;
  433. gap: 30px;
  434. margin-top: 15px;
  435. padding: 15px;
  436. background: white;
  437. border-radius: 8px;
  438. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  439. }
  440. .status-indicator {
  441. display: flex;
  442. align-items: center;
  443. gap: 8px;
  444. font-weight: 500;
  445. }
  446. .status-indicator.running {
  447. color: #4caf50;
  448. }
  449. .status-indicator.stopped {
  450. color: #666;
  451. }
  452. .status-dot {
  453. width: 8px;
  454. height: 8px;
  455. border-radius: 50%;
  456. background: currentColor;
  457. animation: pulse 2s infinite;
  458. }
  459. .status-indicator.stopped .status-dot {
  460. animation: none;
  461. background: #ccc;
  462. }
  463. .running-info {
  464. display: flex;
  465. gap: 20px;
  466. color: #666;
  467. font-size: 14px;
  468. }
  469. .content-grid {
  470. display: grid;
  471. grid-template-columns: 1fr 1fr;
  472. gap: 30px;
  473. }
  474. .card {
  475. background: white;
  476. border-radius: 12px;
  477. padding: 24px;
  478. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  479. margin-bottom: 20px;
  480. }
  481. .card h3 {
  482. color: #333;
  483. margin-bottom: 20px;
  484. font-size: 18px;
  485. }
  486. .form-row {
  487. display: grid;
  488. grid-template-columns: 1fr 1fr;
  489. gap: 15px;
  490. margin-bottom: 20px;
  491. }
  492. .form-group {
  493. margin-bottom: 15px;
  494. }
  495. .form-group label {
  496. display: block;
  497. margin-bottom: 6px;
  498. font-weight: 500;
  499. color: #333;
  500. font-size: 14px;
  501. }
  502. .form-group input,
  503. .form-group select {
  504. width: 100%;
  505. padding: 10px 12px;
  506. border: 2px solid #e1e5e9;
  507. border-radius: 8px;
  508. font-size: 14px;
  509. transition: border-color 0.2s;
  510. }
  511. .form-group input:focus,
  512. .form-group select:focus {
  513. outline: none;
  514. border-color: #1976d2;
  515. }
  516. .checkbox-label {
  517. display: flex;
  518. align-items: center;
  519. gap: 8px;
  520. cursor: pointer;
  521. font-weight: normal !important;
  522. }
  523. .checkbox-label input[type="checkbox"] {
  524. width: auto !important;
  525. margin: 0;
  526. }
  527. .form-actions {
  528. display: flex;
  529. gap: 12px;
  530. margin-top: 20px;
  531. }
  532. .primary-btn {
  533. flex: 1;
  534. padding: 12px 24px;
  535. background: #1976d2;
  536. color: white;
  537. border: none;
  538. border-radius: 8px;
  539. font-size: 14px;
  540. font-weight: 500;
  541. cursor: pointer;
  542. transition: background-color 0.2s;
  543. }
  544. .primary-btn:hover:not(:disabled) {
  545. background: #1565c0;
  546. }
  547. .primary-btn:disabled {
  548. background: #ccc;
  549. cursor: not-allowed;
  550. }
  551. .secondary-btn {
  552. padding: 12px 24px;
  553. border: 1px solid #1976d2;
  554. background: white;
  555. color: #1976d2;
  556. border-radius: 8px;
  557. font-size: 14px;
  558. cursor: pointer;
  559. transition: all 0.2s;
  560. }
  561. .secondary-btn:hover {
  562. background: #1976d2;
  563. color: white;
  564. }
  565. .danger-btn {
  566. background: #f44336;
  567. color: white;
  568. border: none;
  569. }
  570. .danger-btn:hover:not(:disabled) {
  571. background: #d32f2f;
  572. }
  573. .large {
  574. font-size: 16px;
  575. padding: 14px 28px;
  576. }
  577. .control-buttons {
  578. display: flex;
  579. flex-direction: column;
  580. gap: 12px;
  581. }
  582. .warning-notice {
  583. margin-top: 15px;
  584. padding: 12px;
  585. background: #fff3e0;
  586. border: 1px solid #ffcc02;
  587. border-radius: 6px;
  588. color: #e65100;
  589. font-size: 14px;
  590. text-align: center;
  591. }
  592. .results-list {
  593. max-height: 500px;
  594. overflow-y: auto;
  595. }
  596. .result-header {
  597. display: flex;
  598. justify-content: space-between;
  599. align-items: center;
  600. margin-bottom: 15px;
  601. padding-bottom: 10px;
  602. border-bottom: 1px solid #e1e5e9;
  603. }
  604. .export-btn {
  605. padding: 6px 12px;
  606. background: #4caf50;
  607. color: white;
  608. border: none;
  609. border-radius: 4px;
  610. font-size: 12px;
  611. cursor: pointer;
  612. }
  613. .export-btn:hover {
  614. background: #388e3c;
  615. }
  616. .result-items {
  617. space-y: 10px;
  618. }
  619. .result-item {
  620. display: flex;
  621. justify-content: space-between;
  622. align-items: center;
  623. padding: 12px;
  624. border: 1px solid #e1e5e9;
  625. border-radius: 6px;
  626. margin-bottom: 8px;
  627. transition: border-color 0.2s;
  628. }
  629. .result-item:hover {
  630. border-color: #1976d2;
  631. }
  632. .item-info {
  633. flex: 1;
  634. }
  635. .item-name {
  636. font-weight: 500;
  637. color: #333;
  638. margin-bottom: 4px;
  639. }
  640. .item-price {
  641. color: #4caf50;
  642. font-weight: bold;
  643. font-size: 16px;
  644. }
  645. .item-meta {
  646. display: flex;
  647. gap: 12px;
  648. font-size: 12px;
  649. color: #666;
  650. }
  651. .no-results {
  652. text-align: center;
  653. padding: 60px 20px;
  654. color: #666;
  655. }
  656. .empty-icon {
  657. font-size: 48px;
  658. margin-bottom: 15px;
  659. }
  660. .no-results p {
  661. margin: 10px 0 5px 0;
  662. font-size: 16px;
  663. }
  664. .no-results small {
  665. color: #999;
  666. }
  667. /* 消息提示 */
  668. .message-toast {
  669. position: fixed;
  670. top: 20px;
  671. right: 20px;
  672. padding: 12px 20px;
  673. border-radius: 8px;
  674. color: white;
  675. font-weight: 500;
  676. z-index: 1001;
  677. animation: slideIn 0.3s ease-out;
  678. }
  679. .message-toast.success {
  680. background: #4caf50;
  681. }
  682. .message-toast.error {
  683. background: #f44336;
  684. }
  685. .message-toast.info {
  686. background: #2196f3;
  687. }
  688. @keyframes slideIn {
  689. from {
  690. transform: translateX(100%);
  691. opacity: 0;
  692. }
  693. to {
  694. transform: translateX(0);
  695. opacity: 1;
  696. }
  697. }
  698. @keyframes pulse {
  699. 0%, 100% {
  700. opacity: 1;
  701. }
  702. 50% {
  703. opacity: 0.5;
  704. }
  705. }
  706. /* 响应式布局 */
  707. @media (max-width: 768px) {
  708. .content-grid {
  709. grid-template-columns: 1fr;
  710. }
  711. .form-row {
  712. grid-template-columns: 1fr;
  713. }
  714. .running-info {
  715. flex-direction: column;
  716. gap: 8px;
  717. align-items: center;
  718. }
  719. }
  720. </style>