index.vue 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988
  1. <template>
  2. <view class="search-page">
  3. <!-- 自定义导航栏 -->
  4. <view class="custom-navbar" :style="{ paddingTop: statusBarHeight + 'px' }">
  5. <view class="navbar-content">
  6. <!-- 返回按钮 -->
  7. <view class="back-button" @click="goBack">
  8. <text class="back-icon">←</text>
  9. </view>
  10. <!-- 搜索输入框 -->
  11. <view class="search-input-container">
  12. <view class="search-icon">
  13. <text class="icon">🔍</text>
  14. </view>
  15. <input
  16. class="search-input"
  17. v-model="searchKeyword"
  18. placeholder="搜索商品名称"
  19. @input="onSearchInput"
  20. @confirm="performSearch"
  21. :focus="true"
  22. confirm-type="search"
  23. />
  24. </view>
  25. <!-- 搜索按钮 -->
  26. <view class="search-button" @click="performSearch">
  27. <text class="search-text">搜索</text>
  28. </view>
  29. </view>
  30. </view>
  31. <!-- 搜索历史 -->
  32. <view v-if="!searchKeyword && searchHistory.length > 0" class="search-history">
  33. <view class="history-header">
  34. <text class="history-title">历史记录</text>
  35. <view class="clear-history" @click="clearHistory">
  36. <text class="clear-icon">🗑</text>
  37. </view>
  38. </view>
  39. <view class="history-tags">
  40. <view
  41. v-for="(item, index) in searchHistory"
  42. :key="index"
  43. class="history-tag"
  44. @click="searchFromHistory(item)"
  45. >
  46. <text class="tag-text">{{ item }}</text>
  47. </view>
  48. </view>
  49. </view>
  50. <!-- 热门搜索 -->
  51. <view v-if="!searchKeyword" class="hot-search">
  52. <view class="hot-header">
  53. <text class="hot-title">热门搜索</text>
  54. </view>
  55. <!-- 如果没有数据,显示提示 -->
  56. <view v-if="hotSearchList.length === 0" class="no-data" style="padding: 60rpx; text-align: center; color: #999;">
  57. <text>暂无热门搜索数据</text>
  58. </view>
  59. <view v-else class="hot-list">
  60. <view
  61. v-for="(item, index) in hotSearchList"
  62. :key="index"
  63. class="hot-item"
  64. @click="searchFromHot(item)"
  65. >
  66. <view class="hot-rank" :class="[index === 0 ? 'rank-first' : index === 1 ? 'rank-second' : index === 2 ? 'rank-third' : 'rank-normal']">
  67. <text class="rank-number">{{ index + 1 }}</text>
  68. </view>
  69. <text class="hot-text">{{ item }}</text>
  70. </view>
  71. </view>
  72. </view>
  73. <!-- 搜索建议 -->
  74. <view v-if="searchKeyword && searchSuggestions.length > 0" class="search-suggestions">
  75. <view
  76. v-for="(suggestion, index) in searchSuggestions"
  77. :key="index"
  78. class="suggestion-item"
  79. @click="selectSuggestion(suggestion)"
  80. >
  81. <text class="suggestion-icon">🔍</text>
  82. <text class="suggestion-text">{{ suggestion }}</text>
  83. </view>
  84. </view>
  85. <!-- 搜索结果 -->
  86. <view v-if="hasSearched" class="search-results">
  87. <!-- 结果统计 -->
  88. <view class="result-stats">
  89. <text class="stats-text">
  90. 找到 <text class="stats-count">{{ searchResults.length }}</text> 个相关商品
  91. </text>
  92. </view>
  93. <!-- 排序筛选 -->
  94. <view class="filter-bar">
  95. <view
  96. v-for="(filter, index) in filterOptions"
  97. :key="index"
  98. class="filter-item"
  99. :class="{ active: currentFilter === filter.value }"
  100. @click="changeFilter(filter.value)"
  101. >
  102. <text class="filter-text">{{ filter.label }}</text>
  103. <text v-if="filter.sortable" class="sort-arrow">{{ getSortArrow(filter.value) }}</text>
  104. </view>
  105. </view>
  106. <!-- 商品列表 -->
  107. <view v-if="searchResults.length > 0" class="product-list">
  108. <view
  109. v-for="product in searchResults"
  110. :key="product.id"
  111. class="product-item"
  112. @click="goToProductDetail(product)"
  113. >
  114. <image :src="product.imageUrl" class="product-image" mode="aspectFill"></image>
  115. <view class="product-info">
  116. <text class="product-name">{{ highlightKeyword(product.name) }}</text>
  117. <view class="product-meta">
  118. <text class="product-category">{{ product.categoryName }}</text>
  119. <text class="product-sales">销量: {{ product.sales }}</text>
  120. </view>
  121. <view class="product-stock">
  122. <text class="stock-text" :class="{ 'low-stock': product.stock < 10 }">
  123. 库存: {{ product.stock }}
  124. </text>
  125. </view>
  126. </view>
  127. </view>
  128. </view>
  129. <!-- 无搜索结果 -->
  130. <view v-else class="no-results">
  131. <view class="no-results-icon">😔</view>
  132. <text class="no-results-text">未找到相关商品</text>
  133. <text class="no-results-tip">试试搜索其他关键词</text>
  134. </view>
  135. </view>
  136. <!-- 加载状态 -->
  137. <view v-if="loading" class="loading-container">
  138. <view class="loading-spinner"></view>
  139. <text class="loading-text">搜索中...</text>
  140. </view>
  141. </view>
  142. </template>
  143. <script>
  144. // TODO: API替换 - 搜索相关接口
  145. import { searchProducts, getHotSearch, handleApiError } from '@/api/search.js'
  146. export default {
  147. data() {
  148. return {
  149. statusBarHeight: 0,
  150. // 搜索相关
  151. searchKeyword: '', // 搜索关键词
  152. searchSuggestions: [], // 搜索建议
  153. searchResults: [], // 搜索结果
  154. searchHistory: [], // 搜索历史
  155. hotSearchList: [], // 热门搜索
  156. hasSearched: false, // 是否已执行搜索
  157. // 筛选排序
  158. currentFilter: 'default', // 当前筛选方式
  159. currentSort: 'asc', // 当前排序方向
  160. filterOptions: [
  161. { label: '综合', value: 'default', sortable: false },
  162. { label: '销量', value: 'sales', sortable: true },
  163. { label: '库存', value: 'stock', sortable: true },
  164. { label: '名称', value: 'name', sortable: true }
  165. ],
  166. // 状态
  167. loading: false,
  168. // 防抖定时器
  169. searchTimer: null
  170. }
  171. },
  172. onLoad() {
  173. console.log('🚀 搜索页面开始加载')
  174. // 获取系统信息
  175. const systemInfo = uni.getSystemInfoSync()
  176. this.statusBarHeight = systemInfo.statusBarHeight || 20
  177. // 加载搜索历史
  178. console.log('📚 开始加载搜索历史')
  179. this.loadSearchHistory()
  180. // 加载热门搜索
  181. console.log('🔥 开始加载热门搜索')
  182. this.loadHotSearch()
  183. console.log('✅ 搜索页面加载完成')
  184. },
  185. onUnload() {
  186. // 清除定时器
  187. if (this.searchTimer) {
  188. clearTimeout(this.searchTimer)
  189. }
  190. },
  191. methods: {
  192. /**
  193. * 加载搜索历史
  194. */
  195. loadSearchHistory() {
  196. try {
  197. const history = uni.getStorageSync('searchHistory') || []
  198. this.searchHistory = history.slice(0, 10) // 最多保存10条历史
  199. // 如果没有搜索历史,添加一些示例数据(仅用于演示)
  200. if (this.searchHistory.length === 0) {
  201. this.searchHistory = ['茅台', '酒谷特酿']
  202. console.log('🔄 使用示例搜索历史数据')
  203. }
  204. } catch (error) {
  205. console.error('加载搜索历史失败:', error)
  206. this.searchHistory = []
  207. }
  208. },
  209. /**
  210. * 加载热门搜索
  211. * TODO: API替换 - 从后台获取热门搜索词
  212. */
  213. async loadHotSearch() {
  214. try {
  215. const response = await getHotSearch()
  216. if (response.success) {
  217. // 确保数据是数组格式
  218. const searchList = response.data.hotSearchList || []
  219. // 清空现有数据并重新赋值,确保响应式更新
  220. this.hotSearchList.splice(0, this.hotSearchList.length, ...searchList)
  221. console.log('✅ 热门搜索数据加载成功:', this.hotSearchList)
  222. console.log('📊 数据长度:', this.hotSearchList.length)
  223. // 使用nextTick确保DOM更新
  224. this.$nextTick(() => {
  225. console.log('🔄 视图已更新')
  226. })
  227. } else {
  228. console.error('❌ 热门搜索API返回失败:', response)
  229. // 使用默认数据
  230. this.setDefaultHotSearch()
  231. }
  232. } catch (error) {
  233. console.error('❌ 加载热门搜索失败:', error)
  234. handleApiError(error)
  235. // 使用默认数据
  236. this.setDefaultHotSearch()
  237. }
  238. },
  239. /**
  240. * 设置默认热门搜索数据
  241. */
  242. setDefaultHotSearch() {
  243. const defaultData = [
  244. '酒谷',
  245. '茅台',
  246. '红酒',
  247. '陈酿',
  248. '特酿',
  249. '飞天',
  250. '波尔多',
  251. '珍藏'
  252. ]
  253. // 清空现有数据并重新赋值,确保响应式更新
  254. this.hotSearchList.splice(0, this.hotSearchList.length, ...defaultData)
  255. console.log('🔄 使用默认热门搜索数据:', this.hotSearchList)
  256. console.log('📊 默认数据长度:', this.hotSearchList.length)
  257. // 使用nextTick确保DOM更新
  258. this.$nextTick(() => {
  259. console.log('🔄 默认数据视图已更新')
  260. })
  261. },
  262. /**
  263. * 搜索输入事件 - 实时搜索建议
  264. */
  265. onSearchInput(e) {
  266. const keyword = e.detail.value.trim()
  267. this.searchKeyword = keyword
  268. // 清除之前的定时器
  269. if (this.searchTimer) {
  270. clearTimeout(this.searchTimer)
  271. }
  272. if (keyword) {
  273. // 防抖:500ms后触发搜索建议
  274. this.searchTimer = setTimeout(() => {
  275. this.getSearchSuggestions(keyword)
  276. }, 300)
  277. } else {
  278. this.searchSuggestions = []
  279. this.hasSearched = false
  280. }
  281. },
  282. /**
  283. * 获取搜索建议
  284. * TODO: API替换 - 从后台获取搜索建议
  285. */
  286. async getSearchSuggestions(keyword) {
  287. try {
  288. // 模拟搜索建议逻辑
  289. const allProducts = await this.getAllProducts()
  290. const suggestions = []
  291. // 根据商品名称生成建议
  292. allProducts.forEach(product => {
  293. if (product.name.includes(keyword) && !suggestions.includes(product.name)) {
  294. suggestions.push(product.name)
  295. }
  296. })
  297. // 根据分类名称生成建议
  298. const categories = ['酒谷酒', '茅台酒', '红酒', '白酒', '洋酒']
  299. categories.forEach(category => {
  300. if (category.includes(keyword) && !suggestions.includes(category)) {
  301. suggestions.push(category)
  302. }
  303. })
  304. this.searchSuggestions = suggestions.slice(0, 8) // 最多显示8个建议
  305. } catch (error) {
  306. console.error('获取搜索建议失败:', error)
  307. this.searchSuggestions = []
  308. }
  309. },
  310. /**
  311. * 执行搜索
  312. */
  313. async performSearch(keyword = null) {
  314. const searchKeyword = keyword || this.searchKeyword.trim()
  315. if (!searchKeyword) {
  316. uni.showToast({
  317. title: '请输入搜索关键词',
  318. icon: 'none'
  319. })
  320. return
  321. }
  322. try {
  323. this.loading = true
  324. this.hasSearched = true
  325. this.searchSuggestions = []
  326. console.log('🔍 开始搜索:', searchKeyword)
  327. // TODO: API替换 - 调用真实搜索接口
  328. const response = await searchProducts({
  329. keyword: searchKeyword,
  330. filter: this.currentFilter,
  331. sort: this.currentSort
  332. })
  333. if (response.success) {
  334. this.searchResults = response.data.products || []
  335. // 保存搜索历史
  336. this.saveSearchHistory(searchKeyword)
  337. console.log('✅ 搜索完成:', {
  338. keyword: searchKeyword,
  339. results: this.searchResults.length
  340. })
  341. } else {
  342. throw new Error(response.message || '搜索失败')
  343. }
  344. } catch (error) {
  345. console.error('❌ 搜索失败:', error)
  346. handleApiError(error)
  347. this.searchResults = []
  348. } finally {
  349. this.loading = false
  350. }
  351. },
  352. /**
  353. * 保存搜索历史
  354. */
  355. saveSearchHistory(keyword) {
  356. try {
  357. // 移除重复项
  358. let history = this.searchHistory.filter(item => item !== keyword)
  359. // 添加到开头
  360. history.unshift(keyword)
  361. // 限制数量
  362. history = history.slice(0, 10)
  363. this.searchHistory = history
  364. uni.setStorageSync('searchHistory', history)
  365. } catch (error) {
  366. console.error('保存搜索历史失败:', error)
  367. }
  368. },
  369. /**
  370. * 从历史记录搜索
  371. */
  372. searchFromHistory(keyword) {
  373. this.searchKeyword = keyword
  374. this.performSearch(keyword)
  375. },
  376. /**
  377. * 从热门搜索
  378. */
  379. searchFromHot(keyword) {
  380. this.searchKeyword = keyword
  381. this.performSearch(keyword)
  382. },
  383. /**
  384. * 选择搜索建议
  385. */
  386. selectSuggestion(suggestion) {
  387. this.searchKeyword = suggestion
  388. this.performSearch(suggestion)
  389. },
  390. /**
  391. * 改变筛选方式
  392. */
  393. changeFilter(filterValue) {
  394. if (this.currentFilter === filterValue) {
  395. // 如果是可排序的,切换排序方向
  396. const filter = this.filterOptions.find(f => f.value === filterValue)
  397. if (filter && filter.sortable) {
  398. this.currentSort = this.currentSort === 'asc' ? 'desc' : 'asc'
  399. }
  400. } else {
  401. this.currentFilter = filterValue
  402. this.currentSort = 'asc'
  403. }
  404. // 重新执行搜索
  405. if (this.hasSearched) {
  406. this.performSearch()
  407. }
  408. },
  409. /**
  410. * 获取排序箭头
  411. */
  412. getSortArrow(filterValue) {
  413. if (this.currentFilter !== filterValue) return ''
  414. return this.currentSort === 'asc' ? '↑' : '↓'
  415. },
  416. /**
  417. * 高亮关键词
  418. */
  419. highlightKeyword(text) {
  420. if (!this.searchKeyword) return text
  421. // 简单的高亮处理,实际项目中可以用更复杂的方案
  422. return text
  423. },
  424. /**
  425. * 清空搜索
  426. */
  427. clearSearch() {
  428. this.searchKeyword = ''
  429. this.searchSuggestions = []
  430. this.searchResults = []
  431. this.hasSearched = false
  432. },
  433. /**
  434. * 清空历史记录
  435. */
  436. clearHistory() {
  437. uni.showModal({
  438. title: '提示',
  439. content: '确定要清空搜索历史吗?',
  440. success: (res) => {
  441. if (res.confirm) {
  442. this.searchHistory = []
  443. uni.removeStorageSync('searchHistory')
  444. uni.showToast({
  445. title: '已清空历史',
  446. icon: 'success'
  447. })
  448. }
  449. }
  450. })
  451. },
  452. /**
  453. * 跳转商品详情
  454. */
  455. goToProductDetail(product) {
  456. console.log('查看商品详情:', product)
  457. uni.navigateTo({
  458. url: `/pages/product/detail?id=${product.id}`
  459. })
  460. },
  461. /**
  462. * 返回上一页或清除搜索结果
  463. */
  464. goBack() {
  465. // 如果当前有搜索结果,先清除搜索结果回到搜索页面
  466. if (this.hasSearched) {
  467. console.log('🔙 清除搜索结果,返回搜索页面')
  468. this.clearSearch()
  469. } else {
  470. // 如果没有搜索结果,直接返回上一页
  471. console.log('🔙 返回上一页')
  472. uni.navigateBack()
  473. }
  474. },
  475. /**
  476. * 获取所有商品数据(用于搜索建议)
  477. * TODO: API替换 - 这应该是一个专门的搜索建议接口
  478. */
  479. async getAllProducts() {
  480. try {
  481. // 临时使用模拟数据
  482. const { MOCK_PRODUCTS, MOCK_CATEGORIES, MOCK_ASSETS } = require('@/mock/mockData.js')
  483. return MOCK_PRODUCTS.data.products
  484. .filter(product => product.status === 1)
  485. .map(product => {
  486. const category = MOCK_CATEGORIES.data.categories.find(c => c.id === product.categoryId)
  487. return {
  488. ...product,
  489. categoryName: category ? category.name : '未分类',
  490. imageUrl: MOCK_ASSETS.products[product.assetId]
  491. }
  492. })
  493. } catch (error) {
  494. console.error('获取商品数据失败:', error)
  495. return []
  496. }
  497. }
  498. }
  499. }
  500. </script>
  501. <style lang="scss" scoped>
  502. .search-page {
  503. min-height: 100vh;
  504. background: #f8f8f8;
  505. }
  506. /* 自定义导航栏 */
  507. .custom-navbar {
  508. background: #FF6600;
  509. position: sticky;
  510. top: 0;
  511. z-index: 999;
  512. .navbar-content {
  513. display: flex;
  514. align-items: center;
  515. padding: 20rpx;
  516. gap: 20rpx;
  517. .back-button {
  518. width: 60rpx;
  519. height: 60rpx;
  520. display: flex;
  521. align-items: center;
  522. justify-content: center;
  523. .back-icon {
  524. font-size: 36rpx;
  525. color: #ffffff;
  526. font-weight: bold;
  527. }
  528. }
  529. .search-input-container {
  530. flex: 1;
  531. position: relative;
  532. background: #ffffff;
  533. border-radius: 50rpx;
  534. display: flex;
  535. align-items: center;
  536. padding: 0 30rpx;
  537. height: 80rpx;
  538. .search-icon {
  539. margin-right: 15rpx;
  540. .icon {
  541. font-size: 28rpx;
  542. color: #999;
  543. }
  544. }
  545. .search-input {
  546. flex: 1;
  547. height: 80rpx;
  548. font-size: 28rpx;
  549. color: #333;
  550. background: transparent;
  551. }
  552. }
  553. .search-button {
  554. padding: 0 30rpx;
  555. height: 80rpx;
  556. display: flex;
  557. align-items: center;
  558. justify-content: center;
  559. .search-text {
  560. font-size: 28rpx;
  561. color: #ffffff;
  562. font-weight: bold;
  563. }
  564. }
  565. }
  566. }
  567. /* 搜索历史 */
  568. .search-history {
  569. background: #ffffff;
  570. margin: 20rpx 0 0 0;
  571. padding: 30rpx 40rpx;
  572. .history-header {
  573. display: flex;
  574. justify-content: space-between;
  575. align-items: center;
  576. margin-bottom: 30rpx;
  577. .history-title {
  578. font-size: 32rpx;
  579. color: #333;
  580. }
  581. .clear-history {
  582. .clear-icon {
  583. font-size: 32rpx;
  584. color: #ccc;
  585. }
  586. }
  587. }
  588. .history-tags {
  589. display: flex;
  590. flex-wrap: wrap;
  591. gap: 20rpx;
  592. .history-tag {
  593. background: #f5f5f5;
  594. border-radius: 30rpx;
  595. padding: 15rpx 30rpx;
  596. .tag-text {
  597. font-size: 28rpx;
  598. color: #666;
  599. }
  600. &:active {
  601. background: #e0e0e0;
  602. }
  603. }
  604. }
  605. }
  606. /* 热门搜索 */
  607. .hot-search {
  608. background: #ffffff;
  609. margin: 20rpx 0 0 0;
  610. padding: 30rpx 40rpx;
  611. .hot-header {
  612. margin-bottom: 30rpx;
  613. .hot-title {
  614. font-size: 32rpx;
  615. color: #333;
  616. }
  617. }
  618. .hot-list {
  619. .hot-item {
  620. display: flex;
  621. align-items: center;
  622. padding: 25rpx 0;
  623. border-bottom: 1rpx solid #f5f5f5;
  624. .hot-rank {
  625. width: 50rpx;
  626. height: 50rpx;
  627. border-radius: 50%;
  628. display: flex;
  629. align-items: center;
  630. justify-content: center;
  631. margin-right: 30rpx;
  632. .rank-number {
  633. font-size: 26rpx;
  634. font-weight: bold;
  635. }
  636. &.rank-first {
  637. background: #FF6B6B;
  638. .rank-number {
  639. color: #ffffff;
  640. }
  641. }
  642. &.rank-second {
  643. background: #FFA500;
  644. .rank-number {
  645. color: #ffffff;
  646. }
  647. }
  648. &.rank-third {
  649. background: #FFD700;
  650. .rank-number {
  651. color: #ffffff;
  652. }
  653. }
  654. &.rank-normal {
  655. background: #f5f5f5;
  656. .rank-number {
  657. color: #999;
  658. }
  659. }
  660. }
  661. .hot-text {
  662. font-size: 28rpx;
  663. color: #333;
  664. flex: 1;
  665. }
  666. &:last-child {
  667. border-bottom: none;
  668. }
  669. &:active {
  670. background: #f9f9f9;
  671. }
  672. }
  673. }
  674. }
  675. /* 搜索建议 */
  676. .search-suggestions {
  677. background: #ffffff;
  678. margin: 0 20rpx;
  679. border-radius: 20rpx;
  680. overflow: hidden;
  681. .suggestion-item {
  682. display: flex;
  683. align-items: center;
  684. padding: 25rpx 30rpx;
  685. border-bottom: 1rpx solid #f0f0f0;
  686. .suggestion-icon {
  687. font-size: 24rpx;
  688. color: #999;
  689. margin-right: 20rpx;
  690. }
  691. .suggestion-text {
  692. font-size: 28rpx;
  693. color: #333;
  694. flex: 1;
  695. }
  696. &:last-child {
  697. border-bottom: none;
  698. }
  699. &:active {
  700. background: #f5f5f5;
  701. }
  702. }
  703. }
  704. /* 搜索结果 */
  705. .search-results {
  706. margin: 20rpx;
  707. .result-stats {
  708. background: #ffffff;
  709. border-radius: 20rpx;
  710. padding: 25rpx 30rpx;
  711. margin-bottom: 20rpx;
  712. .stats-text {
  713. font-size: 28rpx;
  714. color: #666;
  715. .stats-count {
  716. color: #FF6600;
  717. font-weight: bold;
  718. }
  719. }
  720. }
  721. .filter-bar {
  722. background: #ffffff;
  723. border-radius: 20rpx;
  724. padding: 20rpx 30rpx;
  725. margin-bottom: 20rpx;
  726. display: flex;
  727. gap: 30rpx;
  728. .filter-item {
  729. display: flex;
  730. align-items: center;
  731. gap: 8rpx;
  732. .filter-text {
  733. font-size: 28rpx;
  734. color: #666;
  735. }
  736. .sort-arrow {
  737. font-size: 24rpx;
  738. color: #666;
  739. }
  740. &.active {
  741. .filter-text,
  742. .sort-arrow {
  743. color: #FF6600;
  744. font-weight: bold;
  745. }
  746. }
  747. }
  748. }
  749. .product-list {
  750. .product-item {
  751. background: #ffffff;
  752. border-radius: 20rpx;
  753. margin-bottom: 20rpx;
  754. padding: 20rpx;
  755. display: flex;
  756. gap: 20rpx;
  757. .product-image {
  758. width: 160rpx;
  759. height: 160rpx;
  760. border-radius: 12rpx;
  761. background: #f5f5f5;
  762. }
  763. .product-info {
  764. flex: 1;
  765. display: flex;
  766. flex-direction: column;
  767. justify-content: space-between;
  768. .product-name {
  769. font-size: 30rpx;
  770. font-weight: bold;
  771. color: #333;
  772. line-height: 1.4;
  773. margin-bottom: 10rpx;
  774. }
  775. .product-meta {
  776. display: flex;
  777. gap: 20rpx;
  778. margin-bottom: 10rpx;
  779. .product-category {
  780. font-size: 24rpx;
  781. color: #FF6600;
  782. background: rgba(255, 102, 0, 0.1);
  783. padding: 4rpx 12rpx;
  784. border-radius: 12rpx;
  785. }
  786. .product-sales {
  787. font-size: 24rpx;
  788. color: #999;
  789. }
  790. }
  791. .product-stock {
  792. .stock-text {
  793. font-size: 24rpx;
  794. color: #666;
  795. &.low-stock {
  796. color: #ff4757;
  797. }
  798. }
  799. }
  800. }
  801. &:active {
  802. background: #f5f5f5;
  803. }
  804. }
  805. }
  806. }
  807. /* 无搜索结果 */
  808. .no-results {
  809. background: #ffffff;
  810. border-radius: 20rpx;
  811. padding: 80rpx 30rpx;
  812. text-align: center;
  813. .no-results-icon {
  814. font-size: 80rpx;
  815. margin-bottom: 20rpx;
  816. }
  817. .no-results-text {
  818. font-size: 32rpx;
  819. color: #333;
  820. display: block;
  821. margin-bottom: 10rpx;
  822. }
  823. .no-results-tip {
  824. font-size: 26rpx;
  825. color: #999;
  826. }
  827. }
  828. /* 加载状态 */
  829. .loading-container {
  830. display: flex;
  831. flex-direction: column;
  832. align-items: center;
  833. justify-content: center;
  834. padding: 60rpx;
  835. .loading-spinner {
  836. width: 60rpx;
  837. height: 60rpx;
  838. border: 4rpx solid #f3f3f3;
  839. border-top: 4rpx solid #FF6600;
  840. border-radius: 50%;
  841. animation: loading-spin 1s linear infinite;
  842. margin-bottom: 20rpx;
  843. }
  844. .loading-text {
  845. font-size: 28rpx;
  846. color: #666;
  847. }
  848. }
  849. @keyframes loading-spin {
  850. 0% { transform: rotate(0deg); }
  851. 100% { transform: rotate(360deg); }
  852. }
  853. </style>