index.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699
  1. <template>
  2. <view class="homepage">
  3. <!-- 轮播图区域 -->
  4. <view class="banner-section">
  5. <!-- 状态栏占位 -->
  6. <view class="status-bar-placeholder" :style="{ height: statusBarHeight + 'px' }"></view>
  7. <!-- 轮播图 - 自动播放,每张停留1秒,支持左右滑动切换 -->
  8. <swiper
  9. class="banner-swiper"
  10. :indicator-dots="true"
  11. :autoplay="true"
  12. :interval="1000"
  13. :duration="300"
  14. :circular="true"
  15. indicator-color="rgba(255,255,255,0.5)"
  16. indicator-active-color="#FFFFFF"
  17. @change="onBannerChange"
  18. >
  19. <swiper-item v-for="(banner, index) in bannerList" :key="index" @click="onBannerClick(banner)">
  20. <view class="banner-item">
  21. <image :src="banner.image" class="banner-image" mode="aspectFill"></image>
  22. <view class="banner-overlay">
  23. <text class="banner-title">{{ banner.title }}</text>
  24. </view>
  25. </view>
  26. </swiper-item>
  27. </swiper>
  28. <!-- 轮播图指示器 -->
  29. <view class="banner-indicator">{{ currentBanner + 1 }}/{{ bannerList.length }}</view>
  30. <!-- 透明搜索框 - 悬浮在轮播图上层 -->
  31. <view class="transparent-search-container">
  32. <view class="transparent-search-box" @click="goToSearch">
  33. <text class="search-icon">🔍</text>
  34. <text class="search-placeholder">搜索商品名称</text>
  35. </view>
  36. </view>
  37. </view>
  38. <!-- 商品分类区域 -->
  39. <view class="categories-section">
  40. <view v-for="category in categoryList" :key="category.id" class="category-block">
  41. <view class="category-header">
  42. <text class="category-title">{{ category.name }}</text>
  43. <view class="more-btn" @click="viewMoreProducts(category)">
  44. <text class="more-text">更多</text>
  45. <text class="more-arrow">></text>
  46. </view>
  47. </view>
  48. <view class="products-grid">
  49. <view
  50. v-for="product in category.products"
  51. :key="product.id"
  52. class="product-item"
  53. @click="goToProductDetail(product)"
  54. >
  55. <image :src="product.image" class="product-image" mode="aspectFill"></image>
  56. <view class="product-info">
  57. <text class="product-name">{{ product.name }}</text>
  58. <!-- 按需求不显示价格,只显示商品图和名称 -->
  59. </view>
  60. </view>
  61. </view>
  62. </view>
  63. </view>
  64. <!-- 加载状态 -->
  65. <view v-if="loading" class="loading-container">
  66. <view class="loading-content">
  67. <view class="loading-spinner"></view>
  68. <text class="loading-text">{{ loadingText }}</text>
  69. </view>
  70. </view>
  71. <!-- 底部间距,避免被tabBar遮挡 -->
  72. <view class="bottom-spacer"></view>
  73. <!-- 开发测试区域 (可选显示) -->
  74. <view v-if="showDebug" class="debug-section">
  75. <view class="debug-header" @click="toggleDebug">
  76. <text class="debug-title">🔧 开发测试</text>
  77. </view>
  78. <view v-if="debugExpanded" class="debug-content">
  79. <view class="user-status">
  80. <text class="status-text">{{ isLoggedIn ? '已登录: ' + userPhone : '游客模式' }}</text>
  81. <button v-if="!isLoggedIn" class="debug-btn login" @click="goToLogin">登录</button>
  82. <button v-else class="debug-btn logout" @click="logout">退出</button>
  83. </view>
  84. <button class="debug-btn clear" @click="clearAppData">清除数据</button>
  85. </view>
  86. </view>
  87. </view>
  88. </template>
  89. <script>
  90. import { isLoggedIn, getUserInfo, navigateToLogin, clearUserData } from '@/utils/auth.js'
  91. // TODO: API替换 - 首页数据接口
  92. import { getHomepageData, handleApiError } from '@/api/homepage.js'
  93. export default {
  94. data() {
  95. return {
  96. statusBarHeight: 0,
  97. isLoggedIn: false,
  98. userPhone: '',
  99. currentBanner: 0,
  100. showDebug: true, // 开发阶段显示调试面板
  101. debugExpanded: false,
  102. // 页面数据 - 从API获取
  103. bannerList: [], // TODO: 替换为 API 调用
  104. categoryList: [], // TODO: 替换为 API 调用
  105. // 加载状态
  106. loading: true,
  107. loadingText: '加载中...'
  108. }
  109. },
  110. onLoad() {
  111. // 获取系统信息
  112. const systemInfo = uni.getSystemInfoSync()
  113. this.statusBarHeight = systemInfo.statusBarHeight || 20
  114. // TODO: API替换 - 加载首页数据
  115. this.loadHomepageData()
  116. },
  117. onShow() {
  118. this.checkLoginStatus()
  119. // 更新自定义tabBar选中状态
  120. this.updateTabBar()
  121. },
  122. onPullDownRefresh() {
  123. // TODO: API替换 - 下拉刷新重新加载数据
  124. this.loadHomepageData().then(() => {
  125. uni.stopPullDownRefresh()
  126. })
  127. },
  128. methods: {
  129. // 更新tabBar选中状态
  130. updateTabBar() {
  131. try {
  132. if (typeof this.getTabBar === 'function' && this.getTabBar()) {
  133. this.getTabBar().updateSelected(0) // 首页索引为0
  134. }
  135. } catch (error) {
  136. console.log('更新tabBar状态失败:', error)
  137. }
  138. },
  139. // 检查登录状态
  140. checkLoginStatus() {
  141. this.isLoggedIn = isLoggedIn()
  142. const userInfo = getUserInfo()
  143. this.userPhone = userInfo ? userInfo.phone : ''
  144. },
  145. // 移除登录提示相关方法,游客可正常访问首页
  146. // === 数据加载方法 ===
  147. /**
  148. * 加载首页数据
  149. * TODO: API替换 - 将模拟数据替换为真实API调用
  150. */
  151. async loadHomepageData() {
  152. try {
  153. this.loading = true
  154. this.loadingText = '加载首页数据...'
  155. console.log('🔄 开始加载首页数据')
  156. // TODO: 替换为真实API调用
  157. const response = await getHomepageData()
  158. if (response.success) {
  159. // 处理轮播图数据 - 限制最多6张轮播图
  160. this.bannerList = response.data.banners
  161. .filter(banner => banner.status === 1) // 只显示启用的轮播图
  162. .sort((a, b) => a.sort - b.sort) // 按排序字段排序
  163. .slice(0, 6) // 限制最多6张
  164. .map(banner => ({
  165. id: banner.id,
  166. title: banner.title,
  167. subtitle: banner.subtitle,
  168. image: banner.imageUrl, // 使用素材库URL
  169. linkType: banner.linkType,
  170. linkTarget: banner.linkTarget
  171. }))
  172. // 处理商品分类数据
  173. this.categoryList = response.data.categories.map(category => ({
  174. id: category.id,
  175. name: category.name,
  176. products: category.products.map(product => ({
  177. id: product.id,
  178. name: product.name,
  179. image: product.imageUrl, // 使用素材库URL
  180. // 注意:不展示价格,按需求只显示图片和名称
  181. stock: product.stock,
  182. sales: product.sales
  183. }))
  184. }))
  185. console.log('✅ 首页数据加载成功', {
  186. banners: this.bannerList.length,
  187. categories: this.categoryList.length
  188. })
  189. } else {
  190. throw new Error(response.message || '数据加载失败')
  191. }
  192. } catch (error) {
  193. console.error('❌ 首页数据加载失败:', error)
  194. handleApiError(error)
  195. // 显示错误提示
  196. uni.showToast({
  197. title: '数据加载失败',
  198. icon: 'none',
  199. duration: 2000
  200. })
  201. } finally {
  202. this.loading = false
  203. }
  204. },
  205. // === 新的首页功能方法 ===
  206. // 跳转到搜索页
  207. goToSearch() {
  208. uni.navigateTo({
  209. url: '/pages/search/index'
  210. })
  211. },
  212. // 轮播图切换事件 - 支持左右滑动切换
  213. // 向左滑动:划到下一张图 (current 增加)
  214. // 向右滑动:返回上一张图 (current 减少)
  215. onBannerChange(e) {
  216. this.currentBanner = e.detail.current
  217. },
  218. // 轮播图点击事件
  219. onBannerClick(banner) {
  220. console.log('🖱️ 点击轮播图:', banner)
  221. // TODO: API替换 - 根据linkType和linkTarget进行不同跳转
  222. if (banner.linkType && banner.linkTarget) {
  223. switch (banner.linkType) {
  224. case 'product':
  225. // 跳转到商品详情
  226. uni.navigateTo({
  227. url: `/pages/product/detail?id=${banner.linkTarget}`
  228. })
  229. break
  230. case 'category':
  231. // 跳转到分类页面
  232. uni.navigateTo({
  233. url: `/pages/category/index?id=${banner.linkTarget}`
  234. })
  235. break
  236. case 'activity':
  237. // 跳转到活动页面
  238. uni.navigateTo({
  239. url: `/pages/activity/detail?id=${banner.linkTarget}`
  240. })
  241. break
  242. case 'external':
  243. // 外部链接
  244. uni.showModal({
  245. title: '提示',
  246. content: '即将跳转到外部链接',
  247. success: (res) => {
  248. if (res.confirm) {
  249. // TODO: 处理外部链接跳转
  250. console.log('跳转外部链接:', banner.linkTarget)
  251. }
  252. }
  253. })
  254. break
  255. default:
  256. console.log('未知的跳转类型:', banner.linkType)
  257. }
  258. }
  259. },
  260. // 查看更多商品
  261. viewMoreProducts(category) {
  262. console.log('查看更多商品:', category)
  263. uni.navigateTo({
  264. url: `/pages/category/index?id=${category.id}&name=${category.name}`
  265. })
  266. },
  267. // 跳转商品详情
  268. goToProductDetail(product) {
  269. console.log('查看商品详情:', product)
  270. uni.navigateTo({
  271. url: `/pages/product/detail?id=${product.id}`
  272. })
  273. },
  274. // === 开发调试功能 ===
  275. // 切换调试面板
  276. toggleDebug() {
  277. this.debugExpanded = !this.debugExpanded
  278. },
  279. // 跳转登录页
  280. goToLogin() {
  281. navigateToLogin()
  282. },
  283. // 退出登录
  284. logout() {
  285. uni.showModal({
  286. title: '提示',
  287. content: '确定要退出登录吗?',
  288. success: (res) => {
  289. if (res.confirm) {
  290. clearUserData()
  291. this.checkLoginStatus()
  292. uni.showToast({
  293. title: '已退出登录',
  294. icon: 'success'
  295. })
  296. }
  297. }
  298. })
  299. },
  300. // 清除所有数据
  301. clearAppData() {
  302. uni.showModal({
  303. title: '确认清除',
  304. content: '这将清除所有本地数据,包括登录状态和首次启动标记',
  305. confirmText: '确认清除',
  306. cancelText: '取消',
  307. success: (res) => {
  308. if (res.confirm) {
  309. // 清除所有相关数据
  310. uni.clearStorageSync()
  311. // 重新初始化全局数据
  312. try {
  313. const app = getApp()
  314. if (app && app.globalData) {
  315. app.globalData.isLoggedIn = false
  316. app.globalData.userInfo = null
  317. }
  318. } catch (error) {
  319. console.log('更新全局状态失败')
  320. }
  321. // 更新页面状态
  322. this.checkLoginStatus()
  323. uni.showToast({
  324. title: '数据已清除',
  325. icon: 'success'
  326. })
  327. // 延迟重启应用以模拟首次启动
  328. setTimeout(() => {
  329. uni.reLaunch({
  330. url: '/pages/index/index'
  331. })
  332. }, 1500)
  333. }
  334. }
  335. })
  336. }
  337. }
  338. }
  339. </script>
  340. <style lang="scss" scoped>
  341. .homepage {
  342. min-height: 100vh;
  343. background: #ffffff; /* 改为白色背景,符合UI设计 */
  344. }
  345. /* 轮播图区域 */
  346. .banner-section {
  347. position: relative;
  348. height: 550rpx; /* 恢复原来的高度 */
  349. overflow: hidden;
  350. background: #ffffff; /* 确保背景为白色 */
  351. /* 状态栏占位 */
  352. .status-bar-placeholder {
  353. position: absolute;
  354. top: 0;
  355. left: 0;
  356. right: 0;
  357. z-index: 1;
  358. }
  359. .banner-swiper {
  360. width: 100%;
  361. height: 100%;
  362. position: absolute;
  363. top: 0;
  364. left: 0;
  365. }
  366. .banner-item {
  367. width: 100%;
  368. height: 100%;
  369. position: relative;
  370. .banner-image {
  371. width: 100%;
  372. height: 100%;
  373. object-fit: cover;
  374. }
  375. .banner-overlay {
  376. position: absolute;
  377. bottom: 0;
  378. left: 0;
  379. right: 0;
  380. background: linear-gradient(transparent, rgba(0, 0, 0, 0.3)); /* 减少遮罩透明度 */
  381. padding: 30rpx;
  382. .banner-title {
  383. color: white;
  384. font-size: 32rpx;
  385. font-weight: bold;
  386. }
  387. }
  388. }
  389. .banner-indicator {
  390. position: absolute;
  391. bottom: 30rpx; /* 调整到底部右侧,符合UI设计 */
  392. right: 30rpx;
  393. background: rgba(255, 255, 255, 0.9); /* 改为白色半透明背景 */
  394. color: #333; /* 改为深色文字 */
  395. padding: 8rpx 16rpx;
  396. border-radius: 20rpx;
  397. font-size: 24rpx;
  398. z-index: 3;
  399. border: 1rpx solid rgba(0, 0, 0, 0.1); /* 添加细边框 */
  400. }
  401. /* 透明搜索框容器 - 调整位置到轮播图上方 */
  402. .transparent-search-container {
  403. position: absolute;
  404. left: 0;
  405. right: 0;
  406. z-index: 10;
  407. padding: 0 20rpx; /* 减少左右间距,更接近边缘对齐 */
  408. top: 80rpx; /* 固定位置,在状态栏下方 */
  409. }
  410. /* 透明搜索框 */
  411. .transparent-search-box {
  412. background: rgba(255, 255, 255, 0.6); /* 改为半透明背景 */
  413. backdrop-filter: blur(10rpx);
  414. border: 1rpx solid rgba(255, 255, 255, 0.3); /* 改为半透明白色边框 */
  415. border-radius: 50rpx;
  416. height: 80rpx; /* 增加高度 */
  417. display: flex;
  418. align-items: center;
  419. padding: 0 30rpx;
  420. box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1); /* 减少阴影 */
  421. transition: all 0.3s ease;
  422. .search-icon {
  423. font-size: 32rpx;
  424. color: #666; /* 改为深色图标 */
  425. margin-right: 20rpx;
  426. }
  427. .search-placeholder {
  428. font-size: 28rpx;
  429. color: #999; /* 改为深色占位符 */
  430. flex: 1;
  431. }
  432. /* 点击态效果 */
  433. &:active {
  434. background: rgba(255, 255, 255, 1);
  435. transform: scale(0.98);
  436. }
  437. }
  438. }
  439. /* 商品分类区域 */
  440. .categories-section {
  441. padding: 20rpx 20rpx 120rpx; /* 减少左右间距,与搜索框对齐,更接近边缘 */
  442. background: #ffffff;
  443. .category-block {
  444. background: #ffffff; /* 保持白色背景 */
  445. border-radius: 30rpx; /* 减少圆角 */
  446. margin-bottom: 20rpx; /* 减少间距 */
  447. overflow: hidden;
  448. border: 1rpx solid #f0f0f0; /* 添加细边框 */
  449. box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05); /* 减少阴影 */
  450. .category-header {
  451. display: flex;
  452. justify-content: space-between;
  453. align-items: center;
  454. padding: 25rpx 20rpx; /* 减少内边距,与外部容器对齐 */
  455. border-bottom: 1rpx solid #f5f5f5; /* 更淡的分割线 */
  456. .category-title {
  457. font-size: 32rpx; /* 减少字体大小 */
  458. font-weight: 600; /* 减少字重 */
  459. color: #333;
  460. }
  461. .more-btn {
  462. display: flex;
  463. align-items: center;
  464. color: #FF6600;
  465. .more-text {
  466. font-size: 26rpx; /* 减少字体大小 */
  467. margin-right: 8rpx;
  468. }
  469. .more-arrow {
  470. font-size: 22rpx; /* 减少字体大小 */
  471. }
  472. }
  473. }
  474. .products-grid {
  475. display: flex;
  476. padding: 20rpx 20rpx; /* 左右内边距与外部容器对齐 */
  477. gap: 15rpx; /* 使用gap替代margin */
  478. .product-item {
  479. flex: 1;
  480. background: #fafafa; /* 更淡的背景色 */
  481. border-radius: 12rpx; /* 减少圆角 */
  482. overflow: hidden;
  483. border: 1rpx solid #f0f0f0; /* 添加细边框 */
  484. .product-image {
  485. width: 100%;
  486. height: 180rpx; /* 调整图片高度 */
  487. background: #f5f5f5;
  488. object-fit: cover;
  489. }
  490. .product-info {
  491. padding: 15rpx 12rpx; /* 减少内边距 */
  492. .product-name {
  493. font-size: 24rpx; /* 减少字体大小 */
  494. color: #333;
  495. display: block;
  496. text-align: center;
  497. overflow: hidden;
  498. text-overflow: ellipsis;
  499. white-space: nowrap;
  500. line-height: 1.3;
  501. }
  502. }
  503. /* 点击态效果 */
  504. &:active {
  505. transform: scale(0.98);
  506. opacity: 0.8;
  507. }
  508. }
  509. }
  510. }
  511. }
  512. /* 加载状态 */
  513. .loading-container {
  514. position: fixed;
  515. top: 0;
  516. left: 0;
  517. right: 0;
  518. bottom: 0;
  519. background: rgba(255, 255, 255, 0.95);
  520. display: flex;
  521. align-items: center;
  522. justify-content: center;
  523. z-index: 9999;
  524. .loading-content {
  525. display: flex;
  526. flex-direction: column;
  527. align-items: center;
  528. .loading-spinner {
  529. width: 60rpx;
  530. height: 60rpx;
  531. border: 4rpx solid #f3f3f3;
  532. border-top: 4rpx solid #FF6600;
  533. border-radius: 50%;
  534. animation: loading-spin 1s linear infinite;
  535. margin-bottom: 20rpx;
  536. }
  537. .loading-text {
  538. font-size: 28rpx;
  539. color: #666;
  540. }
  541. }
  542. }
  543. @keyframes loading-spin {
  544. 0% { transform: rotate(0deg); }
  545. 100% { transform: rotate(360deg); }
  546. }
  547. /* 底部间距 */
  548. .bottom-spacer {
  549. height: 100rpx;
  550. }
  551. /* 调试区域 */
  552. .debug-section {
  553. position: fixed;
  554. bottom: 120rpx;
  555. right: 30rpx;
  556. background: rgba(0, 0, 0, 0.8);
  557. border-radius: 20rpx;
  558. overflow: hidden;
  559. z-index: 999;
  560. .debug-header {
  561. padding: 20rpx;
  562. .debug-title {
  563. color: white;
  564. font-size: 24rpx;
  565. }
  566. }
  567. .debug-content {
  568. border-top: 1rpx solid rgba(255, 255, 255, 0.2);
  569. padding: 20rpx;
  570. .user-status {
  571. display: flex;
  572. align-items: center;
  573. margin-bottom: 20rpx;
  574. .status-text {
  575. color: white;
  576. font-size: 22rpx;
  577. margin-right: 15rpx;
  578. flex: 1;
  579. }
  580. }
  581. .debug-btn {
  582. padding: 10rpx 20rpx;
  583. border-radius: 15rpx;
  584. border: none;
  585. font-size: 22rpx;
  586. margin-right: 10rpx;
  587. margin-bottom: 10rpx;
  588. &.login {
  589. background: #007aff;
  590. color: white;
  591. }
  592. &.logout {
  593. background: #ff9500;
  594. color: white;
  595. }
  596. &.clear {
  597. background: #ff3b30;
  598. color: white;
  599. width: 100%;
  600. margin-right: 0;
  601. }
  602. }
  603. }
  604. }
  605. /* 点击态效果 */
  606. .more-btn:active {
  607. opacity: 0.7;
  608. }
  609. .banner-item:active {
  610. opacity: 0.9;
  611. }
  612. </style>