爱简收旧衣按件回收前端代码仓库
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

596 lines
14 KiB

2 weeks ago
  1. <template>
  2. <!-- 品牌索引弹窗 -->
  3. <view v-if="showBrandPopup" class="brand-popup-mask">
  4. <view class="brand-popup">
  5. <view class="brand-popup-header">
  6. <text class="brand-popup-close" @click="close">关闭</text>
  7. <text class="brand-popup-title">可回收的品牌</text>
  8. </view>
  9. <view class="brand-popup-search">
  10. <input class="brand-search-input" v-model="brandSearch" placeholder="请输入要查询的内容" @input="onBrandSearchInput" />
  11. </view>
  12. <scroll-view class="brand-popup-list" scroll-y :scroll-into-view="scrollToView">
  13. <!-- 热门品牌区域 -->
  14. <view v-if="hotBrandList.length > 0 && !brandSearch" class="hot-brands-section">
  15. <view class="hot-brands-title">热门品牌</view>
  16. <view class="hot-brands-grid">
  17. <view v-for="brand in hotBrandList" :key="brand.id" class="hot-brand-item" @click="openBrandConfirm(brand)">
  18. <image :src="brand.logo" class="hot-brand-logo" mode="aspectFit" />
  19. <text class="hot-brand-name">{{brand.name}}</text>
  20. </view>
  21. </view>
  22. </view>
  23. <view v-for="letter in brandIndexList" :key="letter" :id="'brand-letter-' + letter">
  24. <view class="brand-letter">{{letter}}</view>
  25. <view v-for="brand in filteredBrandList.filter(b => b.letter === letter)" :key="brand.name" class="brand-item" @click="openBrandConfirm(brand)">
  26. <image :src="brand.logo" class="brand-logo" mode="aspectFit" />
  27. <text class="brand-name">{{brand.name}}</text>
  28. </view>
  29. </view>
  30. </scroll-view>
  31. <view class="brand-index-bar">
  32. <text v-for="letter in brandIndexList" :key="letter" :class="{active: currentLetter === letter}" @click="scrollToLetter(letter)">{{letter}}</text>
  33. </view>
  34. </view>
  35. </view>
  36. <!-- 品牌确认弹窗 -->
  37. <view v-if="showBrandConfirm" class="brand-confirm-mask" @click.self="closeBrandConfirm">
  38. <view class="brand-confirm-popup">
  39. <view class="brand-confirm-title">品牌确认提示</view>
  40. <view class="brand-confirm-logo-wrap">
  41. <image :src="brandConfirmInfo.logo" class="brand-confirm-logo" mode="aspectFit" />
  42. </view>
  43. <view class="brand-confirm-name">{{ brandConfirmInfo.name }}</view>
  44. <view class="brand-confirm-desc">请确认所选品牌是否与实物品牌信息一致否则将无法进行回收</view>
  45. <view class="brand-confirm-btn-row">
  46. <button class="brand-confirm-btn retry" @click="closeBrandConfirm">重新选择</button>
  47. <button class="brand-confirm-btn confirm" @click="confirmBrand">确认一致</button>
  48. </view>
  49. </view>
  50. </view>
  51. <!-- 减少数量时的品牌选择弹窗 -->
  52. <view v-if="showBrandReducePopup" class="brand-reduce-popup-mask" @click.self="closeBrandReducePopup">
  53. <view class="brand-reduce-popup">
  54. <view class="brand-reduce-popup-header">
  55. <text class="brand-reduce-popup-close" @click="closeBrandReducePopup">关闭</text>
  56. <text class="brand-reduce-popup-title">选择要减少的品牌</text>
  57. </view>
  58. <scroll-view class="brand-reduce-popup-list" scroll-y>
  59. <view v-for="brand in reduceBrandList" :key="brand.brandId" class="brand-item" @click="selectReduceBrand(brand)">
  60. <image :src="brand.logo" class="brand-logo" mode="aspectFit" />
  61. <text class="brand-name">{{brand.name}}</text>
  62. </view>
  63. </scroll-view>
  64. </view>
  65. </view>
  66. <!-- 商品款式选择组件 -->
  67. <product-style-selector ref="styleSelector" @style-confirm="onStyleConfirm" @close="onStyleSelectorClose"></product-style-selector>
  68. </template>
  69. <script>
  70. import { pinyin } from '../../utils/pinyin.js'
  71. import ProductStyleSelector from './product-style-selector.vue'
  72. export default {
  73. name: 'BrandSelector',
  74. components: {
  75. ProductStyleSelector
  76. },
  77. data() {
  78. return {
  79. showBrandPopup: false,
  80. showBrandConfirm: false,
  81. showBrandReducePopup: false,
  82. brandConfirmInfo: {
  83. logo: '',
  84. name: '',
  85. id: ''
  86. },
  87. brandList: [],
  88. hotBrandList: [], // 热门品牌列表
  89. currentLetter: 'A',
  90. scrollToView: '',
  91. brandSearch: '',
  92. reduceBrandList: [],
  93. currentProductId: null,
  94. searchTimer: null
  95. }
  96. },
  97. computed: {
  98. filteredBrandList() {
  99. return this.brandList
  100. },
  101. // 动态生成品牌字母索引,只显示有品牌的字母
  102. brandIndexList() {
  103. const letters = new Set()
  104. let hasSharp = false
  105. this.brandList.forEach(b => {
  106. if (b.letter && /^[A-Z]$/.test(b.letter)) {
  107. letters.add(b.letter)
  108. } else {
  109. letters.add('#')
  110. hasSharp = true
  111. }
  112. })
  113. const arr = Array.from(letters).filter(l => l !== '#').sort()
  114. if (hasSharp) arr.push('#')
  115. return arr
  116. }
  117. },
  118. methods: {
  119. // 打开品牌选择弹窗
  120. open(productId) {
  121. if (!productId) {
  122. console.error('productId is required')
  123. return
  124. }
  125. this.currentProductId = productId
  126. this.getGoodsBrandList(productId)
  127. this.showBrandPopup = true
  128. },
  129. // 关闭品牌选择弹窗
  130. close() {
  131. this.showBrandPopup = false
  132. this.showBrandConfirm = false
  133. this.showBrandReducePopup = false
  134. // 清理搜索状态
  135. this.brandSearch = ''
  136. this.currentProductId = null
  137. // 清空品牌列表
  138. this.brandList = []
  139. this.hotBrandList = []
  140. if (this.searchTimer) {
  141. clearTimeout(this.searchTimer)
  142. this.searchTimer = null
  143. }
  144. this.$emit('close')
  145. },
  146. // 打开减少品牌选择弹窗
  147. openReducePopup(brandList) {
  148. this.reduceBrandList = brandList || []
  149. this.showBrandReducePopup = true
  150. },
  151. // 关闭减少品牌选择弹窗
  152. closeBrandReducePopup() {
  153. this.showBrandReducePopup = false
  154. this.reduceBrandList = []
  155. this.$emit('reduce-close')
  156. },
  157. // 选择要减少的品牌
  158. selectReduceBrand(brandInfo) {
  159. this.closeBrandReducePopup()
  160. this.$emit('reduce-select', brandInfo)
  161. },
  162. // 滚动到指定字母
  163. scrollToLetter(letter) {
  164. this.currentLetter = letter
  165. this.scrollToView = 'brand-letter-' + letter
  166. },
  167. // 打开品牌确认弹窗
  168. openBrandConfirm(brand) {
  169. this.brandConfirmInfo = {
  170. id: brand.id,
  171. logo: brand.logo,
  172. name: brand.name
  173. }
  174. this.showBrandConfirm = true
  175. },
  176. // 关闭品牌确认弹窗
  177. closeBrandConfirm() {
  178. this.showBrandConfirm = false
  179. },
  180. // 确认品牌
  181. confirmBrand() {
  182. this.showBrandConfirm = false
  183. // 打开商品款式选择弹窗,传递已有数量
  184. this.$emit('get-existing-quantities', this.brandConfirmInfo.id, (existingQuantities) => {
  185. this.$refs.styleSelector.open(this.brandConfirmInfo, this.currentProductId, existingQuantities)
  186. })
  187. },
  188. // 处理款式确认事件
  189. onStyleConfirm(data) {
  190. this.showBrandPopup = false
  191. this.$emit('brand-confirm', {
  192. brandInfo: data.brandInfo,
  193. selectedStyles: data.selectedStyles
  194. })
  195. },
  196. // 处理款式选择器关闭事件
  197. onStyleSelectorClose() {
  198. // 款式选择器关闭时的处理逻辑
  199. },
  200. // 获取商品品牌列表
  201. getGoodsBrandList(productId, searchName = '') {
  202. this.currentProductId = productId
  203. const params = { productId }
  204. if (searchName.trim()) {
  205. params.name = searchName.trim()
  206. }
  207. this.$api('getGoodsBrandList', params, res => {
  208. if (res && res.success && res.result && res.result.records) {
  209. const allBrands = res.result.records.map(item => {
  210. // 获取品牌名称的拼音首字母
  211. const firstChar = this.getPinyinFirstLetter(item.name)
  212. return {
  213. id: item.id,
  214. logo: item.image || '/static/brand/alexander.png',
  215. name: item.name,
  216. letter: firstChar,
  217. isPin: item.isPin,
  218. hot: item.hot || 0,
  219. ...item
  220. }
  221. })
  222. // 分离热门品牌和普通品牌
  223. this.hotBrandList = allBrands.filter(brand => brand.hot == 1)
  224. this.brandList = allBrands.filter(brand => brand.hot != 1)
  225. }
  226. })
  227. },
  228. // 获取中文拼音首字母
  229. getPinyinFirstLetter(str) {
  230. if (!str) return '#'
  231. const firstChar = str.charAt(0)
  232. // 遍历pinyin对象,查找包含该汉字的拼音
  233. for (let key in pinyin) {
  234. const chars = pinyin[key]
  235. if (chars && chars.indexOf(firstChar) !== -1) {
  236. return key.charAt(0).toUpperCase()
  237. }
  238. }
  239. // 英文首字母
  240. if (/^[A-Za-z]$/.test(firstChar)) {
  241. return firstChar.toUpperCase()
  242. }
  243. return '#'
  244. },
  245. // 品牌搜索输入事件处理
  246. onBrandSearchInput(e) {
  247. const searchValue = e.detail.value
  248. // 清除之前的定时器
  249. if (this.searchTimer) {
  250. clearTimeout(this.searchTimer)
  251. }
  252. // 设置防抖,500ms后执行搜索
  253. this.searchTimer = setTimeout(() => {
  254. if (this.currentProductId) {
  255. this.getGoodsBrandList(this.currentProductId, searchValue)
  256. }
  257. }, 500)
  258. }
  259. }
  260. }
  261. </script>
  262. <style lang="scss" scoped>
  263. .brand-popup-mask {
  264. position: fixed;
  265. left: 0;
  266. right: 0;
  267. top: 0;
  268. bottom: 0;
  269. background: rgba(0,0,0,0.35);
  270. z-index: 3000;
  271. display: flex;
  272. align-items: flex-end;
  273. justify-content: center;
  274. }
  275. .brand-popup {
  276. position: relative;
  277. width: 100%;
  278. max-width: 750px;
  279. background: #fff;
  280. border-radius: 32rpx 32rpx 0 0;
  281. box-shadow: 0 -4rpx 24rpx rgba(0,0,0,0.08);
  282. padding-bottom: 40rpx;
  283. height: 94vh;
  284. display: flex;
  285. flex-direction: column;
  286. overflow: hidden;
  287. }
  288. .brand-popup-header {
  289. display: flex;
  290. align-items: center;
  291. justify-content: center;
  292. padding: 32rpx 24rpx 0 24rpx;
  293. font-size: 32rpx;
  294. font-weight: bold;
  295. position: relative;
  296. }
  297. .brand-popup-close {
  298. position: absolute;
  299. left: 24rpx;
  300. font-size: 28rpx;
  301. color: #888;
  302. }
  303. .brand-popup-title {
  304. font-size: 32rpx;
  305. color: #222;
  306. font-weight: bold;
  307. }
  308. .brand-popup-search {
  309. padding: 20rpx 24rpx 0 24rpx;
  310. }
  311. .brand-search-input {
  312. height: 60rpx;
  313. border-radius: 30rpx;
  314. background: #f5f5f5;
  315. border: none;
  316. padding-left: 40rpx;
  317. font-size: 28rpx;
  318. color: #888;
  319. }
  320. .brand-popup-list {
  321. flex: 1;
  322. overflow-y: auto;
  323. max-height: calc(94vh - 160rpx);
  324. padding: 0 24rpx;
  325. scrollbar-width: none; /* Firefox */
  326. -ms-overflow-style: none; /* IE and Edge */
  327. &::-webkit-scrollbar {
  328. width: 0 !important;
  329. display: none; /* Chrome, Safari, Opera */
  330. }
  331. }
  332. .brand-letter {
  333. font-size: 28rpx;
  334. color: #888;
  335. background-color: #f8f8f8;
  336. margin: 24rpx 0 0 0;
  337. padding: 8rpx 8rpx;
  338. font-weight: bold;
  339. }
  340. .brand-item {
  341. display: flex;
  342. align-items: center;
  343. padding: 16rpx 0;
  344. border-bottom: 1px solid #f0f0f0;
  345. }
  346. .brand-logo {
  347. width: 60rpx;
  348. height: 60rpx;
  349. margin-right: 20rpx;
  350. border-radius: 8rpx;
  351. background: #f8f8f8;
  352. }
  353. .brand-name {
  354. font-size: 28rpx;
  355. color: #222;
  356. }
  357. .brand-index-bar {
  358. position: absolute;
  359. right: 12rpx;
  360. top: 120rpx;
  361. width: 32rpx;
  362. display: flex;
  363. flex-direction: column;
  364. align-items: center;
  365. z-index: 10;
  366. }
  367. .brand-index-bar text {
  368. font-size: 22rpx;
  369. color: #bbb;
  370. margin: 4rpx 0;
  371. font-weight: bold;
  372. &.active {
  373. color: #ff9c00;
  374. }
  375. }
  376. /* 热门品牌样式 */
  377. .hot-brands-section {
  378. padding: 20rpx 0;
  379. border-bottom: 1px solid #f0f0f0;
  380. }
  381. .hot-brands-title {
  382. font-size: 28rpx;
  383. color: #222;
  384. font-weight: bold;
  385. margin-bottom: 20rpx;
  386. }
  387. .hot-brands-grid {
  388. display: flex;
  389. flex-wrap: wrap;
  390. gap: 10rpx;
  391. }
  392. .hot-brand-item {
  393. width: calc((100% - 120rpx) / 3);
  394. display: flex;
  395. // flex-direction: column;
  396. align-items: center;
  397. padding: 2rpx 4rpx;
  398. border-radius: 40rpx;
  399. background: #f3f3f3;
  400. border: 1px solid #eee;
  401. &:active {
  402. background: #eee;
  403. }
  404. }
  405. .hot-brand-logo {
  406. width: 48rpx;
  407. height: 48rpx;
  408. border-radius: 8rpx;
  409. margin-bottom: 8rpx;
  410. background: #fff;
  411. flex-shrink: 0;
  412. }
  413. .hot-brand-name {
  414. font-size: 22rpx;
  415. color: #333;
  416. text-align: center;
  417. line-height: 1.2;
  418. max-width: 100%;
  419. overflow: hidden;
  420. text-overflow: ellipsis;
  421. white-space: nowrap;
  422. }
  423. .brand-confirm-mask {
  424. position: fixed;
  425. left: 0;
  426. right: 0;
  427. top: 0;
  428. bottom: 0;
  429. background: rgba(0,0,0,0.25);
  430. z-index: 5001;
  431. display: flex;
  432. align-items: center;
  433. justify-content: center;
  434. }
  435. .brand-confirm-popup {
  436. width: 70vw;
  437. max-width: 270px;
  438. background: #fff;
  439. border-radius: 32rpx;
  440. box-shadow: 0 8rpx 32rpx rgba(0,0,0,0.12);
  441. display: flex;
  442. flex-direction: column;
  443. align-items: center;
  444. padding: 48rpx 20rpx 36rpx 20rpx;
  445. position: relative;
  446. }
  447. .brand-confirm-title {
  448. font-size: 36rpx;
  449. color: #222;
  450. font-weight: bold;
  451. text-align: center;
  452. margin-bottom: 24rpx;
  453. }
  454. .brand-confirm-logo-wrap {
  455. width: 120rpx;
  456. height: 120rpx;
  457. background: #f8f8f8;
  458. border-radius: 50%;
  459. display: flex;
  460. align-items: center;
  461. justify-content: center;
  462. margin-bottom: 18rpx;
  463. }
  464. .brand-confirm-logo {
  465. width: 80rpx;
  466. height: 80rpx;
  467. border-radius: 50%;
  468. }
  469. .brand-confirm-name {
  470. font-size: 28rpx;
  471. color: #222;
  472. font-weight: bold;
  473. text-align: center;
  474. margin-bottom: 16rpx;
  475. }
  476. .brand-confirm-desc {
  477. font-size: 24rpx;
  478. color: #999;
  479. text-align: center;
  480. margin-bottom: 32rpx;
  481. line-height: 1.6;
  482. }
  483. .brand-confirm-btn-row {
  484. width: 100%;
  485. display: flex;
  486. justify-content: space-between;
  487. gap: 24rpx;
  488. }
  489. .brand-confirm-btn {
  490. flex: 1;
  491. height: 72rpx;
  492. border-radius: 36rpx;
  493. font-size: 28rpx;
  494. font-weight: bold;
  495. display: flex;
  496. align-items: center;
  497. justify-content: center;
  498. border: none;
  499. margin: 0 0;
  500. }
  501. .brand-confirm-btn.retry {
  502. background: #fff;
  503. color: #ff9c00;
  504. border: 2rpx solid #ff9c00;
  505. }
  506. .brand-confirm-btn.confirm {
  507. background: linear-gradient(to right, #ffd01e, #ff8917);
  508. color: #fff;
  509. border: none;
  510. }
  511. /* 减少数量时的品牌选择弹窗 */
  512. .brand-reduce-popup-mask {
  513. position: fixed;
  514. left: 0;
  515. right: 0;
  516. top: 0;
  517. bottom: 0;
  518. background: rgba(0,0,0,0.35);
  519. z-index: 5001;
  520. display: flex;
  521. align-items: center;
  522. justify-content: center;
  523. }
  524. .brand-reduce-popup {
  525. width: 70vw;
  526. max-width: 270px;
  527. background: #fff;
  528. border-radius: 32rpx;
  529. box-shadow: 0 8rpx 32rpx rgba(0,0,0,0.12);
  530. display: flex;
  531. flex-direction: column;
  532. align-items: center;
  533. padding: 48rpx 20rpx 36rpx 20rpx;
  534. position: relative;
  535. }
  536. .brand-reduce-popup-header {
  537. display: flex;
  538. align-items: center;
  539. justify-content: center;
  540. padding: 32rpx 24rpx 0 24rpx;
  541. font-size: 32rpx;
  542. font-weight: bold;
  543. position: relative;
  544. }
  545. .brand-reduce-popup-close {
  546. position: absolute;
  547. left: 24rpx;
  548. font-size: 28rpx;
  549. color: #888;
  550. }
  551. .brand-reduce-popup-title {
  552. font-size: 32rpx;
  553. color: #222;
  554. font-weight: bold;
  555. }
  556. .brand-reduce-popup-list {
  557. flex: 1;
  558. overflow-y: auto;
  559. max-height: 60vh;
  560. padding: 0 24rpx;
  561. scrollbar-width: none; /* Firefox */
  562. -ms-overflow-style: none; /* IE and Edge */
  563. &::-webkit-scrollbar {
  564. width: 0 !important;
  565. display: none; /* Chrome, Safari, Opera */
  566. }
  567. }
  568. </style>