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.

404 lines
14 KiB

  1. <template>
  2. <view class="breed-select-container">
  3. <!-- 搜索栏 -->
  4. <view class="search-container">
  5. <u-search
  6. v-model="searchValue"
  7. placeholder="搜索宠物品种"
  8. :show-action="false"
  9. @change="onSearchChange"
  10. shape="round"
  11. bg-color="#fff"
  12. ></u-search>
  13. </view>
  14. <!-- 品种列表 -->
  15. <view class="breed-list-container">
  16. <scroll-view
  17. scroll-y="true"
  18. class="breed-list"
  19. :scroll-into-view="scrollIntoView"
  20. @scroll="onScroll"
  21. scroll-with-animation="true"
  22. >
  23. <view
  24. v-for="letter in alphabetList"
  25. :key="letter"
  26. v-if="groupedBreeds && groupedBreeds[letter] && Array.isArray(groupedBreeds[letter]) && groupedBreeds[letter].length > 0"
  27. :id="`section-${letter}`"
  28. class="breed-section"
  29. >
  30. <view class="section-header">{{ letter }}</view>
  31. <view
  32. v-for="(breed, index) in groupedBreeds[letter]"
  33. :key="`${letter}-${index}`"
  34. class="breed-item"
  35. @click="() => selectBreed(breed)"
  36. >
  37. <text class="breed-name">{{ breed }}</text>
  38. <view class="breed-divider" v-if="index < groupedBreeds[letter].length - 1"></view>
  39. </view>
  40. </view>
  41. </scroll-view>
  42. </view>
  43. <!-- 字母索引 -->
  44. <view class="letter-index">
  45. <view
  46. v-for="letter in alphabetList"
  47. :key="letter"
  48. class="letter-item"
  49. :class="{ 'active': currentLetter === letter, 'has-content': groupedBreeds && groupedBreeds[letter] && Array.isArray(groupedBreeds[letter]) && groupedBreeds[letter].length > 0 }"
  50. @click="scrollToLetter(letter)"
  51. >
  52. {{ letter }}
  53. </view>
  54. </view>
  55. </view>
  56. </template>
  57. <script>
  58. import { getDictList } from "@/api/system/user"
  59. export default {
  60. data() {
  61. return {
  62. petType: 'dog',
  63. searchValue: '',
  64. breedData: [],
  65. filteredBreeds: [],
  66. groupedBreeds: {},
  67. alphabetList: ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'],
  68. currentLetter: 'A',
  69. scrollIntoView: '',
  70. selectedBreed: '',
  71. scrollTimer: null
  72. }
  73. },
  74. onLoad(options) {
  75. this.petType = options.petType || 'dog'
  76. this.selectedBreed = options.selectedBreed || ''
  77. this.getPetBreeds()
  78. },
  79. methods: {
  80. // 获取宠物品种数据
  81. async getPetBreeds() {
  82. try {
  83. const petBreedType = this.petType === 'cat' ? 'pet_brand_cat' : 'pet_brand_dog'
  84. const res = await getDictList(petBreedType)
  85. if (res && res.code === 200 && res.data && Array.isArray(res.data)) {
  86. // 过滤掉空值并去重
  87. const validBreeds = res.data
  88. .filter(e => e && e.dictLabel && typeof e.dictLabel === 'string')
  89. .map(e => e.dictLabel.trim())
  90. .filter(label => label.length > 0)
  91. this.breedData = Array.from(new Set(validBreeds)).sort((a, b) => a.localeCompare(b, 'zh-CN'))
  92. this.filteredBreeds = [...this.breedData]
  93. this.groupBreedsByLetter()
  94. console.log('品种数据加载成功,共', this.breedData.length, '个品种')
  95. } else {
  96. console.error('API返回数据格式异常:', res)
  97. uni.showToast({
  98. title: '获取品种数据失败',
  99. icon: 'none'
  100. })
  101. }
  102. } catch (error) {
  103. console.error('获取品种数据失败:', error)
  104. uni.showToast({
  105. title: '获取品种数据失败',
  106. icon: 'none'
  107. })
  108. }
  109. },
  110. // 按字母分组品种
  111. groupBreedsByLetter() {
  112. // 初始化所有字母组
  113. this.groupedBreeds = {}
  114. this.alphabetList.forEach(letter => {
  115. this.groupedBreeds[letter] = []
  116. })
  117. // 确保filteredBreeds是数组且不为空
  118. if (!this.filteredBreeds || !Array.isArray(this.filteredBreeds)) {
  119. console.warn('filteredBreeds不是有效数组')
  120. this.filteredBreeds = []
  121. return
  122. }
  123. // 按字母分组
  124. this.filteredBreeds.forEach(breed => {
  125. if (breed && typeof breed === 'string' && breed.trim()) {
  126. const firstChar = this.getFirstChar(breed)
  127. if (this.groupedBreeds && this.groupedBreeds[firstChar] && Array.isArray(this.groupedBreeds[firstChar])) {
  128. this.groupedBreeds[firstChar].push(breed)
  129. }
  130. }
  131. })
  132. // 对每个字母组内的品种进行排序
  133. if (this.groupedBreeds) {
  134. Object.keys(this.groupedBreeds).forEach(letter => {
  135. if (this.groupedBreeds[letter] && Array.isArray(this.groupedBreeds[letter])) {
  136. this.groupedBreeds[letter].sort((a, b) => a.localeCompare(b, 'zh-CN'))
  137. }
  138. })
  139. }
  140. },
  141. // 获取品种名称的首字母
  142. getFirstChar(breed) {
  143. if (!breed || typeof breed !== 'string' || breed.trim() === '') {
  144. return 'A'
  145. }
  146. const firstChar = breed.charAt(0)
  147. // 如果是中文字符,返回拼音首字母
  148. if (/[\u4e00-\u9fa5]/.test(firstChar)) {
  149. return this.getPinyinFirstChar(firstChar)
  150. }
  151. return firstChar.toUpperCase()
  152. },
  153. // 获取中文字符的拼音首字母
  154. getPinyinFirstChar(char) {
  155. const pinyinMap = {
  156. '阿': 'A', '艾': 'A', '澳': 'A', '爱': 'A', '安': 'A', '奥': 'A',
  157. '巴': 'B', '比': 'B', '博': 'B', '边': 'B', '布': 'B', '伯': 'B',
  158. '藏': 'C', '柴': 'C', '长': 'C', '成': 'C', '查': 'C', '春': 'C',
  159. '大': 'D', '德': 'D', '杜': 'D', '斗': 'D', '丹': 'D', '道': 'D',
  160. '俄': 'E', '恩': 'E', '尔': 'E',
  161. '法': 'F', '芬': 'F', '佛': 'F', '费': 'F',
  162. '高': 'G', '贵': 'G', '古': 'G', '格': 'G', '哥': 'G', '国': 'G',
  163. '哈': 'H', '惠': 'H', '黑': 'H', '红': 'H', '华': 'H', '虎': 'H',
  164. '吉': 'J', '金': 'J', '加': 'J', '杰': 'J', '京': 'J', '基': 'J',
  165. '可': 'K', '卡': 'K', '克': 'K', '科': 'K',
  166. '拉': 'L', '罗': 'L', '兰': 'L', '莱': 'L', '利': 'L', '路': 'L',
  167. '马': 'M', '美': 'M', '牧': 'M', '摩': 'M', '曼': 'M', '米': 'M', '缅': 'M',
  168. '牛': 'N', '纽': 'N', '南': 'N', '尼': 'N', '纳': 'N',
  169. '平': 'P', '帕': 'P', '普': 'P', '皮': 'P',
  170. '秋': 'Q', '奇': 'Q', '丘': 'Q',
  171. '日': 'R', '瑞': 'R', '若': 'R', '热': 'R',
  172. '松': 'S', '萨': 'S', '圣': 'S', '苏': 'S', '斯': 'S', '山': 'S',
  173. '泰': 'T', '土': 'T', '特': 'T', '托': 'T',
  174. '威': 'W', '魏': 'W', '温': 'W', '沃': 'W',
  175. '西': 'X', '喜': 'X', '雪': 'X', '新': 'X',
  176. '约': 'Y', '英': 'Y', '意': 'Y', '伊': 'Y',
  177. '中': 'Z', '藏': 'Z', '芝': 'Z', '泽': 'Z'
  178. }
  179. return pinyinMap[char] || 'A'
  180. },
  181. // 搜索处理
  182. onSearchChange(value) {
  183. this.searchValue = value || ''
  184. if (!this.breedData || !Array.isArray(this.breedData)) {
  185. console.warn('breedData不是有效数组')
  186. this.filteredBreeds = []
  187. this.groupBreedsByLetter()
  188. return
  189. }
  190. if (value && value.trim()) {
  191. this.filteredBreeds = this.breedData.filter(breed =>
  192. breed && typeof breed === 'string' && breed.toLowerCase().includes(value.toLowerCase())
  193. )
  194. } else {
  195. this.filteredBreeds = [...this.breedData]
  196. }
  197. this.groupBreedsByLetter()
  198. },
  199. // 选择品种
  200. selectBreed(breed) {
  201. try {
  202. console.log('选择品种:', breed)
  203. // 使用全局事件总线传递数据,这是最可靠的方式
  204. uni.$emit('breedSelected', {
  205. breed: breed,
  206. petType: this.petType
  207. })
  208. console.log('触发全局事件 breedSelected:', breed)
  209. // 返回上一页
  210. uni.navigateBack()
  211. } catch (error) {
  212. console.error('选择品种时出错:', error)
  213. uni.showToast({
  214. title: '选择失败,请重试',
  215. icon: 'none'
  216. })
  217. // 即使出错也要返回上一页
  218. uni.navigateBack()
  219. }
  220. },
  221. // 滚动到指定字母
  222. scrollToLetter(letter) {
  223. this.currentLetter = letter
  224. this.scrollIntoView = `section-${letter}`
  225. },
  226. // 滚动事件处理
  227. onScroll(e) {
  228. // 暂时禁用滚动检测,避免错误
  229. // const scrollTop = e.detail.scrollTop
  230. // 使用节流来优化性能
  231. // if (this.scrollTimer) {
  232. // clearTimeout(this.scrollTimer)
  233. // }
  234. // this.scrollTimer = setTimeout(() => {
  235. // this.updateCurrentLetter(scrollTop)
  236. // }, 100)
  237. },
  238. // 更新当前字母
  239. updateCurrentLetter(scrollTop) {
  240. // 获取所有字母区域的位置信息
  241. const query = uni.createSelectorQuery().in(this)
  242. query.selectAll('.breed-section').boundingClientRect()
  243. query.exec((res) => {
  244. if (res && res[0] && Array.isArray(res[0])) {
  245. const sections = res[0]
  246. let currentLetter = 'A'
  247. // 找到当前滚动位置对应的字母
  248. for (let i = 0; i < sections.length; i++) {
  249. const section = sections[i]
  250. if (section && typeof section.top === 'number' && typeof section.height === 'number') {
  251. const sectionTop = section.top
  252. const sectionHeight = section.height
  253. // 只有当字母标题被粘性定位悬挂时才激活
  254. // 检查字母标题是否在顶部位置(粘性定位生效)
  255. // 考虑搜索栏的高度,粘性定位是相对于搜索栏的
  256. const searchBarHeight = 120 // 搜索栏高度约120rpx
  257. const stickyTop = searchBarHeight
  258. // sectionTop <= stickyTop 表示字母标题已经到达粘性定位位置
  259. // sectionTop > stickyTop - sectionHeight 表示字母区域还没有完全滚动过去
  260. if (sectionTop <= stickyTop && sectionTop > stickyTop - sectionHeight) {
  261. currentLetter = section.id.replace('section-', '')
  262. break
  263. }
  264. }
  265. }
  266. if (this.currentLetter !== currentLetter) {
  267. this.currentLetter = currentLetter
  268. console.log('当前激活字母:', currentLetter, '滚动位置:', scrollTop)
  269. }
  270. }
  271. })
  272. },
  273. }
  274. }
  275. </script>
  276. <style lang="scss" scoped>
  277. .breed-select-container {
  278. height: 100vh;
  279. background-color: #f5f5f5;
  280. position: relative;
  281. }
  282. .search-container {
  283. background-color: #fff;
  284. padding: 20rpx 30rpx;
  285. position: relative;
  286. z-index: 99;
  287. }
  288. .breed-list-container {
  289. flex: 1;
  290. position: relative;
  291. height: calc(100vh - 120rpx);
  292. background-color: #fff;
  293. }
  294. .breed-list {
  295. height: 100%;
  296. background-color: #fff;
  297. padding: 0 30rpx;
  298. }
  299. .breed-section {
  300. .section-header {
  301. font-size: 36rpx;
  302. font-weight: bold;
  303. color: #333;
  304. padding: 20rpx 0 10rpx 0;
  305. background-color: #fff;
  306. position: sticky;
  307. top: 0;
  308. z-index: 10;
  309. }
  310. .breed-item {
  311. padding: 20rpx 0;
  312. cursor: pointer;
  313. transition: background-color 0.2s;
  314. .breed-name {
  315. font-size: 28rpx;
  316. color: #333;
  317. line-height: 1.5;
  318. font-weight: 400;
  319. }
  320. .breed-divider {
  321. height: 1rpx;
  322. background-color: #f0f0f0;
  323. margin-top: 20rpx;
  324. }
  325. }
  326. }
  327. .letter-index {
  328. position: fixed;
  329. right: 10rpx;
  330. top: 50%;
  331. transform: translateY(-50%);
  332. display: flex;
  333. flex-direction: column;
  334. align-items: center;
  335. z-index: 1000;
  336. .letter-item {
  337. width: 40rpx;
  338. height: 40rpx;
  339. display: flex;
  340. align-items: center;
  341. justify-content: center;
  342. font-size: 24rpx;
  343. color: #666;
  344. margin: 2rpx 0;
  345. transition: all 0.3s;
  346. font-weight: 400;
  347. border-radius: 50%;
  348. &.active {
  349. color: #fff !important;
  350. font-weight: bold;
  351. background-color: #FFBF60;
  352. }
  353. &.has-content {
  354. color: #333;
  355. }
  356. }
  357. }
  358. </style>