index.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486
  1. <template>
  2. <view class="location-display" :class="{ 'scroll ': isScrolled }" @click="handleLocationClick">
  3. <text v-show="!positioningType" class="iconfont icon-ic_location1"></text>
  4. <view class="location-text">
  5. <!-- 小程序使用CSS动画类 -->
  6. <!-- #ifdef MP -->
  7. <text class="text-content mp-text" :class="animationClass" :style="{transform: `translateX(${textOffset}px)`}">{{ displayText }}</text>
  8. <!-- #endif -->
  9. <!-- 其他平台使用JS动画 -->
  10. <!-- #ifndef MP -->
  11. <text class="text-content" :style="{transform: `translateX(${textOffset}px)`}">{{ displayText }}</text>
  12. <!-- #endif -->
  13. </view>
  14. <text v-show="positioningType" class="iconfont icon-ic_downarrow ml-4"></text>
  15. </view>
  16. </template>
  17. <script>
  18. export default {
  19. name: 'LocationDisplay',
  20. props: {
  21. // 显示的位置文字
  22. text: {
  23. type: String,
  24. default: '定位中...'
  25. },
  26. // 最大显示宽度
  27. maxWidth: {
  28. type: String,
  29. default: '118rpx'
  30. },
  31. // 最大高度
  32. maxHeight: {
  33. type: String,
  34. default: '42rpx'
  35. },
  36. // 是否处于滚动状态 外界用于时候更换颜色
  37. isScrolled: {
  38. type: Boolean,
  39. default: false
  40. },
  41. // 是否启用文字滚动功能
  42. enableScroll: {
  43. type: Boolean,
  44. default: true
  45. },
  46. positioningType:{
  47. type: Boolean,
  48. default: false
  49. }
  50. },
  51. data() {
  52. return {
  53. textOffset: 0, // 文字偏移量
  54. scrollTimer: null, // 滚动定时器
  55. isScrolling: false, // 是否正在滚动
  56. // 小程序CSS动画状态
  57. animationClass: '', // 动画类名
  58. maxDistance: 0, // 最大滚动距离
  59. currentLocationName: '', // 当前位置名称
  60. storageTimer: null, // 位置监听定时器
  61. isDestroyed: false // 组件销毁标记
  62. }
  63. },
  64. computed: {
  65. displayText() {
  66. // 如果外部传入了text,优先使用外部text
  67. if (this.text && this.text !== '定位中...') {
  68. return this.text;
  69. }
  70. if(this.currentLocationName){
  71. return this.currentLocationName
  72. } else {
  73. // 否则使用内部管理的位置数据
  74. return this.positioningType ? '选择圈层' : '选择位置';
  75. }
  76. }
  77. },
  78. mounted() {
  79. // 只有在没有外部传入text时,才启用内部位置管理
  80. if (!this.text || this.text === '定位中...') {
  81. this.initLocationData();
  82. this.setupStorageListener();
  83. }
  84. // 启动滚动
  85. this.resetAndStartScroll();
  86. },
  87. created() {
  88. // LocationDisplay 组件初始化
  89. },
  90. watch: {
  91. text() {
  92. this.resetAndStartScroll();
  93. },
  94. displayText() {
  95. this.resetAndStartScroll();
  96. },
  97. enableScroll() {
  98. if (!this.enableScroll) {
  99. this.stopScroll();
  100. this.textOffset = 0;
  101. } else {
  102. this.resetAndStartScroll();
  103. }
  104. }
  105. },
  106. beforeDestroy() {
  107. this.isDestroyed = true;
  108. this.stopScroll();
  109. this.stopLocationListener();
  110. },
  111. methods: {
  112. // 重置并开始滚动 - 临时停用
  113. resetAndStartScroll() {
  114. this.stopScroll();
  115. this.textOffset = 0;
  116. this.isScrolling = false;
  117. // 临时停用滚动功能
  118. return;
  119. if (!this.enableScroll) return;
  120. this.$nextTick(() => {
  121. this.startSimpleScroll();
  122. });
  123. },
  124. // 完整的双向滚动逻辑
  125. startSimpleScroll() {
  126. if (this.isScrolling) return;
  127. // 先计算需要滚动的距离
  128. this.$nextTick(() => {
  129. this.calculateScrollDistance();
  130. });
  131. },
  132. // 计算滚动距离
  133. calculateScrollDistance() {
  134. const query = uni.createSelectorQuery().in(this);
  135. query.select('.text-content').boundingClientRect();
  136. query.select('.location-text').boundingClientRect();
  137. query.exec((res) => {
  138. if (res[0] && res[1]) {
  139. const textWidth = res[0].width; // 文字总宽度
  140. const containerWidth = res[1].width; // 容器宽度
  141. let overflowWidth = textWidth - containerWidth; // 溢出宽度
  142. // 重新计算正确的滚动距离
  143. // 目标:让文字末尾刚好显示在容器右边缘,而不是让文字滚动消失
  144. // 正确公式:滚动距离 = 文字宽度 - 容器宽度 (这样文字末尾刚好到达容器右边)
  145. // 但实际上我们不需要滚动这么远,只需要让末尾文字可见即可
  146. // 小程序保守策略:确保文字末尾能显示,但保留开头部分
  147. // #ifdef MP
  148. // 策略:滚动距离 = 溢出宽度 - 10px,这样文字末尾显示,开头也保留10px可见
  149. overflowWidth = Math.max(0, overflowWidth - 10);
  150. // #endif
  151. if (overflowWidth > 0) {
  152. // 需要滚动,开始双向滚动
  153. this.startTwoWayScroll(overflowWidth);
  154. } else {
  155. // console.log('LocationDisplay无需滚动');
  156. }
  157. }
  158. });
  159. },
  160. // 双向滚动:先向左显示末尾,再向右回到开头
  161. startTwoWayScroll(maxDistance) {
  162. // 延迟1秒开始第一阶段滚动
  163. setTimeout(() => {
  164. if (this.isDestroyed || !this.enableScroll) return;
  165. this.isScrolling = true;
  166. this.textOffset = 0;
  167. // 第一阶段:向左滚动显示文字末尾
  168. this.scrollToEnd(maxDistance);
  169. }, 1000);
  170. },
  171. // 第一阶段:向左滚动到末尾
  172. scrollToEnd(maxDistance) {
  173. // 小程序优化:降低频率,增加步进
  174. // #ifdef MP
  175. const scrollStep = 1; // 小程序用更大步进
  176. const scrollInterval = 30; // 小程序用更长间隔
  177. // #endif
  178. // H5保持丝滑
  179. // #ifdef H5
  180. const scrollStep = 0.5;
  181. const scrollInterval = 20;
  182. // #endif
  183. // APP采用中等参数
  184. // #ifdef APP-PLUS
  185. const scrollStep = 0.8;
  186. const scrollInterval = 25;
  187. // #endif
  188. this.scrollTimer = setInterval(() => {
  189. // 小程序模式:每次更新前检查是否会超出边界
  190. // #ifdef MP
  191. const nextOffset = this.textOffset - scrollStep;
  192. if (nextOffset < -maxDistance) {
  193. // 直接定位到目标位置,不再继续滚动
  194. this.textOffset = -maxDistance;
  195. clearInterval(this.scrollTimer);
  196. setTimeout(() => {
  197. this.scrollToStart();
  198. }, 2000);
  199. return;
  200. }
  201. this.textOffset = nextOffset;
  202. // #endif
  203. // H5和APP正常滚动
  204. // #ifndef MP
  205. this.textOffset -= scrollStep;
  206. // 滚动到末尾位置(文字末尾刚好显示完整)
  207. if (this.textOffset <= -maxDistance) {
  208. this.textOffset = -maxDistance; // 精确定位
  209. clearInterval(this.scrollTimer);
  210. // 停留2秒后开始第二阶段
  211. setTimeout(() => {
  212. this.scrollToStart();
  213. }, 2000);
  214. }
  215. // #endif
  216. }, scrollInterval);
  217. },
  218. // 第二阶段:向右滚动回到开头
  219. scrollToStart() {
  220. // 小程序优化:使用相同的平台参数
  221. // #ifdef MP
  222. const scrollStep = 1;
  223. const scrollInterval = 30;
  224. // #endif
  225. // H5保持丝滑
  226. // #ifdef H5
  227. const scrollStep = 0.5;
  228. const scrollInterval = 20;
  229. // #endif
  230. // APP采用中等参数
  231. // #ifdef APP-PLUS
  232. const scrollStep = 0.8;
  233. const scrollInterval = 25;
  234. // #endif
  235. this.scrollTimer = setInterval(() => {
  236. // 小程序模式:每次更新前检查是否会超出边界
  237. // #ifdef MP
  238. const nextOffset = this.textOffset + scrollStep;
  239. if (nextOffset > 0) {
  240. // 直接定位到起始位置,不再继续滚动
  241. this.textOffset = 0;
  242. this.stopScroll();
  243. return;
  244. }
  245. this.textOffset = nextOffset;
  246. // #endif
  247. // H5和APP正常滚动
  248. // #ifndef MP
  249. this.textOffset += scrollStep;
  250. // 回到初始位置
  251. if (this.textOffset >= 0) {
  252. this.textOffset = 0; // 精确定位到起始位置
  253. this.stopScroll();
  254. }
  255. // #endif
  256. }, scrollInterval);
  257. },
  258. // 停止滚动
  259. stopScroll() {
  260. if (this.scrollTimer) {
  261. clearInterval(this.scrollTimer);
  262. this.scrollTimer = null;
  263. }
  264. this.isScrolling = false;
  265. },
  266. // 点击组件圈层
  267. handleLocationClick() {
  268. this.$emit('click');
  269. },
  270. // ============ 位置管理相关方法 ============
  271. // 从缓存获取位置数据 - 以location_info为准
  272. initLocationData() {
  273. if (this.isDestroyed) return;
  274. try {
  275. // 优先使用location_info(用户最新的主动定位选择)
  276. const locationInfo = uni.getStorageSync('location_info');
  277. if (locationInfo && (locationInfo.name || locationInfo.address) && !this.positioningType) {
  278. this.currentLocationName = locationInfo.name || locationInfo.address;
  279. return;
  280. }
  281. // 备选:使用areas_info(圈层选择的位置)
  282. const areasInfo = uni.getStorageSync('areas_info');
  283. if (areasInfo && Array.isArray(areasInfo) && areasInfo.length > 0 && this.positioningType) {
  284. // 显示最新选择的圈层
  285. const latestArea = areasInfo[0];
  286. this.currentLocationName = latestArea.name || latestArea.address || '选择圈层';
  287. return;
  288. }
  289. // 默认显示
  290. this.currentLocationName = this.positioningType ? '选择圈层' : '选择位置';
  291. } catch (error) {
  292. this.currentLocationName = this.positioningType ? '选择圈层' : '选择位置';
  293. }
  294. },
  295. // 设置存储监听
  296. setupStorageListener() {
  297. if (this.isDestroyed) return;
  298. // 使用定时器定期检查缓存变化
  299. this.storageTimer = setInterval(() => {
  300. if (!this.isDestroyed) {
  301. this.checkLocationUpdate();
  302. }
  303. }, 1000); // 每秒检查一次
  304. },
  305. // 检查位置更新 - 以location_info为准
  306. checkLocationUpdate() {
  307. if (this.isDestroyed) return;
  308. try {
  309. let newLocationName = this.positioningType ? '选择圈层' : '选择位置';
  310. // 优先使用location_info(用户最新的主动定位选择)
  311. const locationInfo = uni.getStorageSync('location_info');
  312. if (locationInfo && (locationInfo.name || locationInfo.address)&& !this.positioningType) {
  313. newLocationName = locationInfo.name || locationInfo.address;
  314. } else {
  315. // 备选:使用areas_info(圈层选择的位置)
  316. const areasInfo = uni.getStorageSync('areas_info');
  317. if (areasInfo && Array.isArray(areasInfo) && areasInfo.length > 0 && this.positioningType) {
  318. const latestArea = areasInfo[0];
  319. newLocationName = latestArea.name || latestArea.address || '选择圈层';
  320. }
  321. }
  322. // 如果地址发生变化,更新显示
  323. if (newLocationName !== this.currentLocationName) {
  324. this.currentLocationName = newLocationName;
  325. // displayText计算属性会自动更新,触发watch重新计算滚动
  326. }
  327. } catch (error) {
  328. // 位置更新失败,忽略
  329. }
  330. },
  331. // 停止位置监听
  332. stopLocationListener() {
  333. if (this.storageTimer) {
  334. clearInterval(this.storageTimer);
  335. this.storageTimer = null;
  336. }
  337. }
  338. }
  339. }
  340. </script>
  341. <style lang="scss" scoped>
  342. .scroll{
  343. .iconfont {
  344. color: #000 !important;
  345. }
  346. .text-content {
  347. color: #000 !important;
  348. }
  349. }
  350. .location-display {
  351. display: flex;
  352. align-items: center;
  353. cursor: pointer;
  354. height: 42rpx;
  355. max-height: 42rpx;
  356. overflow: hidden;
  357. box-sizing: border-box;
  358. position: relative;
  359. flex-shrink: 0;
  360. margin-right: 8rpx;
  361. width: 154rpx;
  362. .iconfont {
  363. font-size: 26rpx;
  364. color: #fff;
  365. margin-right: 2rpx;
  366. flex-shrink: 0;
  367. height: 24rpx;
  368. line-height: 24rpx;
  369. margin-right: 4rpx;
  370. /* #ifdef H5 */
  371. font-size: 24rpx;
  372. /* #endif */
  373. }
  374. .location-text {
  375. line-height: 32px;
  376. overflow: hidden;
  377. position: relative;
  378. }
  379. .text-content {
  380. /* 基础样式 */
  381. height: 18px;
  382. font-family: PingFang SC, PingFang SC;
  383. font-weight: 400;
  384. font-size: 13px;
  385. color: #FFFFFF;
  386. line-height: 18px;
  387. text-align: left;
  388. font-style: normal;
  389. text-transform: none;
  390. display: inline-block;
  391. /* H5平台特殊处理 */
  392. /* #ifdef H5 */
  393. min-width: 100%;
  394. font-size: 12px;
  395. line-height: 42rpx;
  396. height: 42rpx;
  397. display: flex;
  398. /* 禁用CSS transition,让JS动画完全控制 */
  399. transition: none !important;
  400. transform: translateZ(0); /* 启用硬件加速 */
  401. will-change: transform; /* 优化渲染性能 */
  402. /* #endif */
  403. /* 小程序/APP平台 */
  404. /* #ifdef MP || APP-PLUS */
  405. width: auto;
  406. min-width: 500rpx;
  407. /* 禁用transition,避免小程序抖动 */
  408. transition: none !important;
  409. /* 小程序强制GPU渲染优化 */
  410. transform: translateZ(0) translate3d(0,0,0);
  411. will-change: transform;
  412. backface-visibility: hidden;
  413. /* #endif */
  414. /* 小程序专用CSS动画 */
  415. /* #ifdef MP */
  416. &.mp-text {
  417. /* 基础优化 */
  418. transform: translateZ(0);
  419. will-change: transform;
  420. /* 动画效果暂时禁用,继续使用JS控制 */
  421. /* transition: transform 3s ease-in-out; */
  422. }
  423. &.mp-scroll-to-end {
  424. /* 滚动到末尾的动画 - 暂时不用 */
  425. }
  426. &.mp-scroll-to-start {
  427. /* 滚动回起始位置的动画 - 暂时不用 */
  428. }
  429. /* #endif */
  430. }
  431. }
  432. </style>