index.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  1. <template>
  2. <view class="home-page">
  3. <view class="banner">
  4. <text class="banner-icon">🚀</text>
  5. <text class="banner-title">爱伴·AI之旅</text>
  6. <view class="course-tag">
  7. <text class="course-tag-text">{{ courseLabel }}</text>
  8. </view>
  9. <button v-if="!isLoggedIn" class="login-entry-btn" @click="showLoginSheet">点击登录</button>
  10. </view>
  11. <view class="status-card">
  12. <view class="status-row">
  13. <text class="status-label">校友校验</text>
  14. <text :class="['status-value', verifyClass]">{{ verifyLabel }}</text>
  15. </view>
  16. <view class="status-row">
  17. <text class="status-label">班级</text>
  18. <text class="status-value">{{ classLabel }}</text>
  19. </view>
  20. <view class="status-row">
  21. <text class="status-label">小组</text>
  22. <text class="status-value">{{ groupLabel }}</text>
  23. </view>
  24. </view>
  25. <view class="section-title">日常服务</view>
  26. <view class="nav-grid">
  27. <view class="nav-item" @click="switchToTab('/pages/course/index')">
  28. <text class="nav-icon">📚</text>
  29. <text class="nav-text">课程</text>
  30. </view>
  31. <view class="nav-item" @click="goTo('/pages/enroll/list')">
  32. <text class="nav-icon">🎫</text>
  33. <text class="nav-text">报名中心</text>
  34. </view>
  35. <view class="nav-item" @click="goTo('/pages/survey/index')">
  36. <text class="nav-icon">📝</text>
  37. <text class="nav-text">兴趣调研</text>
  38. </view>
  39. <view class="nav-item" @click="goTo('/pages/verify/index')">
  40. <text class="nav-icon">🎓</text>
  41. <text class="nav-text">校友校验</text>
  42. </view>
  43. <view class="nav-item" @click="goTo('/pages/salon/list')">
  44. <text class="nav-icon">🎨</text>
  45. <text class="nav-text">周末沙龙</text>
  46. </view>
  47. </view>
  48. <view class="section-title">最近课程</view>
  49. <view class="course-mini-list">
  50. <view class="course-mini-card" v-for="(c, idx) in recentCourses" :key="idx" @click="goToCourse(c)">
  51. <view class="course-mini-head">
  52. <text class="course-mini-level">{{ c.level || 'L' }}</text>
  53. <text class="course-mini-name">{{ c.name }}</text>
  54. <text class="course-mini-status" :class="'st-' + c.effectiveStatus">{{ statusLabel(c.effectiveStatus) }}</text>
  55. </view>
  56. <text class="course-mini-desc" v-if="c.description">{{ c.description }}</text>
  57. </view>
  58. <view class="course-mini-empty" v-if="!recentCourses.length && isLoggedIn">
  59. <text class="course-mini-empty-text">暂无课程</text>
  60. </view>
  61. </view>
  62. <!-- Login Bottom Sheet -->
  63. <view class="login-sheet-mask" :class="{ 'sheet-open': loginSheetVisible }" @click="hideLoginSheet">
  64. <view class="login-sheet-panel" @click.stop>
  65. <view class="sheet-handle"></view>
  66. <view class="sheet-content">
  67. <view class="sheet-header">
  68. <text class="sheet-title">欢迎回来</text>
  69. <text class="sheet-sub">登录以解锁更多成长功能</text>
  70. </view>
  71. <button class="sheet-login-btn" @click="handleLogin" :loading="loginLoading" :disabled="loginLoading">
  72. 微信一键登录
  73. </button>
  74. <view class="sheet-footer">
  75. <text class="sheet-footer-text">登录即代表同意服务协议</text>
  76. </view>
  77. </view>
  78. </view>
  79. </view>
  80. </view>
  81. </template>
  82. <script>
  83. import { getMyGroup, getClassInfo, wechatLogin, getMyCourse, getCourseList } from '@/utils/api.js'
  84. export default {
  85. data() {
  86. return {
  87. groupInfo: null,
  88. classInfo: null,
  89. courseName: '',
  90. recentCourses: [],
  91. loginSheetVisible: false,
  92. loginLoading: false
  93. }
  94. },
  95. computed: {
  96. isLoggedIn() {
  97. return this.$store.getters.isLoggedIn
  98. },
  99. courseLabel() {
  100. return this.courseName || 'L0 家立方-AI管家成长营'
  101. },
  102. verifyLabel() {
  103. var v = this.$store.state.alumniVerify
  104. if (v === 'accepted') return '✅ 已通过'
  105. if (v === 'pending') return '⏳ 待审核'
  106. return '❌ 未校验'
  107. },
  108. verifyClass() {
  109. var v = this.$store.state.alumniVerify
  110. return v === 'accepted' ? 'val-pass' : (v === 'pending' ? 'val-pending' : 'val-fail')
  111. },
  112. classLabel() {
  113. return this.classInfo ? this.classInfo.name : '未进班'
  114. },
  115. groupLabel() {
  116. return this.groupInfo ? this.groupInfo.name : '未入组'
  117. }
  118. },
  119. onShow() {
  120. this.loadInfo()
  121. },
  122. onLoad() {
  123. // 注册全局登录浮层触发事件(uni-app 页面生命周期 onLoad 早于 created 时机更稳)
  124. var self = this
  125. try {
  126. uni.$on('showLoginSheet', function() {
  127. self.showLoginSheet()
  128. })
  129. } catch (e) {
  130. console.error('[home] 注册 showLoginSheet 监听失败', e)
  131. }
  132. },
  133. onUnload() {
  134. try {
  135. uni.$off('showLoginSheet')
  136. } catch (e) {}
  137. },
  138. methods: {
  139. loadInfo() {
  140. if (!this.isLoggedIn) return
  141. var self = this
  142. if (this.$store.state.classId) {
  143. getClassInfo().then(function(resp) {
  144. self.classInfo = resp.data
  145. }).catch(function() {})
  146. }
  147. getMyGroup().then(function(resp) {
  148. self.groupInfo = resp.data
  149. }).catch(function() {})
  150. getMyCourse().then(function(resp) {
  151. var d = resp.data || {}
  152. var c = d.course
  153. if (c && c.name) {
  154. self.courseName = (c.level ? c.level + ' ' : '') + c.name
  155. }
  156. }).catch(function() {})
  157. // 最近课程:优先取进行中/未开始,最多 3 门
  158. getCourseList().then(function(resp) {
  159. var list = resp.data || []
  160. var sorted = list.slice().sort(function(a, b) {
  161. return rankOf(a) - rankOf(b)
  162. })
  163. self.recentCourses = sorted.slice(0, 3)
  164. }).catch(function() {})
  165. },
  166. rankOf(c) {
  167. var st = c && c.effectiveStatus
  168. if (st === 'active') return 0
  169. if (st === 'upcoming') return 1
  170. if (st === 'finished') return 2
  171. return 3
  172. },
  173. statusLabel(st) {
  174. var map = { upcoming: '未开始', active: '进行中', finished: '已结束', draft: '未发布' }
  175. return map[st] || st || '-'
  176. },
  177. goToCourse(c) {
  178. if (!c || !c.id) return
  179. uni.navigateTo({ url: '/pages/course/detail?courseId=' + c.id })
  180. },
  181. showLoginSheet() {
  182. this.loginSheetVisible = true
  183. },
  184. hideLoginSheet() {
  185. this.loginSheetVisible = false
  186. },
  187. handleLogin() {
  188. if (this.loginLoading) return
  189. this.loginLoading = true
  190. var self = this
  191. wx.login({
  192. success: function(res) {
  193. if (res.code) {
  194. wechatLogin(res.code).then(function(resp) {
  195. var data = resp.data
  196. self.$store.dispatch('login', data)
  197. uni.showToast({ title: '登录成功', icon: 'success' })
  198. self.hideLoginSheet()
  199. self.loadInfo()
  200. }).catch(function(err) {
  201. console.error('登录失败', err)
  202. }).finally(function() {
  203. self.loginLoading = false
  204. })
  205. } else {
  206. uni.showToast({ title: '微信登录失败', icon: 'none' })
  207. self.loginLoading = false
  208. }
  209. },
  210. fail: function() {
  211. uni.showToast({ title: '微信登录失败', icon: 'none' })
  212. self.loginLoading = false
  213. }
  214. })
  215. },
  216. goTo(url) {
  217. uni.navigateTo({ url: url })
  218. },
  219. switchToTab(url) {
  220. uni.switchTab({ url: url })
  221. }
  222. }
  223. }
  224. </script>
  225. <style scoped>
  226. .home-page { min-height: 100vh; background: #F5F5F5; padding: 0 32rpx 32rpx; }
  227. .banner { background: linear-gradient(135deg, #F97316, #EA580C); border-radius: 0 0 32rpx 32rpx; padding: 60rpx 32rpx 48rpx; text-align: center; margin: 0 -32rpx 24rpx; position: relative; }
  228. .banner-icon { font-size: 72rpx; display: block; margin-bottom: 16rpx; }
  229. .banner-title { display: block; font-size: 40rpx; font-weight: 700; color: #FFF; margin-bottom: 16rpx; }
  230. .course-tag {
  231. display: inline-block;
  232. background: rgba(255,255,255,0.18);
  233. border: 2rpx solid rgba(255,255,255,0.4);
  234. border-radius: 100rpx;
  235. padding: 8rpx 24rpx;
  236. margin-bottom: 28rpx;
  237. }
  238. .course-tag-text {
  239. font-size: 28rpx;
  240. font-weight: 600;
  241. color: #FFF;
  242. line-height: 1.4;
  243. }
  244. .login-entry-btn {
  245. background: #FFF;
  246. color: #EA580C;
  247. font-size: 24rpx;
  248. font-weight: 600;
  249. border-radius: 100rpx;
  250. padding: 0 32rpx;
  251. height: 64rpx;
  252. line-height: 64rpx;
  253. margin: 0 auto;
  254. border: none;
  255. }
  256. .status-card { background: #FFF; border-radius: 16rpx; padding: 24rpx 32rpx; margin-bottom: 24rpx; }
  257. .status-row { display: flex; justify-content: space-between; padding: 14rpx 0; border-bottom: 1rpx solid #F1F5F9; }
  258. .status-row:last-child { border-bottom: none; }
  259. .status-label { font-size: 28rpx; color: #64748B; }
  260. .status-value { font-size: 28rpx; color: #1E293B; font-weight: 500; }
  261. .val-pass { color: #22C55E; }
  262. .val-pending { color: #FFB74D; }
  263. .val-fail { color: #EF5350; }
  264. .section-title { font-size: 30rpx; font-weight: 600; color: #1E293B; margin-bottom: 16rpx; border-left: 8rpx solid #F97316; padding-left: 16rpx; }
  265. .nav-grid { display: flex; flex-wrap: wrap; gap: 0; background: #FFF; border-radius: 16rpx; overflow: hidden; }
  266. .nav-item { width: 25%; display: flex; flex-direction: column; align-items: center; padding: 28rpx 0; }
  267. .nav-item:active { background: #F8FAFC; }
  268. .nav-icon { font-size: 44rpx; margin-bottom: 8rpx; }
  269. .nav-text { font-size: 24rpx; color: #1E293B; text-align: center; }
  270. .course-mini-list { display: flex; flex-direction: column; margin-top: 20rpx; }
  271. .course-mini-card { background: #FFF; border-radius: 16rpx; padding: 24rpx 28rpx; margin-bottom: 16rpx; }
  272. .course-mini-card:active { background: #F8FAFC; }
  273. .course-mini-head { display: flex; align-items: center; margin-bottom: 8rpx; }
  274. .course-mini-level { font-size: 22rpx; font-weight: 600; color: #F97316; background: #FFF7ED; border-radius: 8rpx; padding: 4rpx 14rpx; margin-right: 16rpx; }
  275. .course-mini-name { font-size: 28rpx; font-weight: 600; color: #1E293B; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  276. .course-mini-status { font-size: 22rpx; border-radius: 6rpx; padding: 4rpx 14rpx; }
  277. .course-mini-status.st-upcoming { color: #F97316; background: #FFF7ED; }
  278. .course-mini-status.st-active { color: #22C55E; background: #F0FDF4; }
  279. .course-mini-status.st-finished { color: #94A3B8; background: #F1F5F9; }
  280. .course-mini-desc { font-size: 24rpx; color: #64748B; line-height: 1.5; overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical; }
  281. .course-mini-empty { background: #FFF; border-radius: 16rpx; padding: 48rpx 0; text-align: center; }
  282. .course-mini-empty-text { font-size: 26rpx; color: #94A3B8; }
  283. /* Bottom Sheet Styles */
  284. .login-sheet-mask {
  285. position: fixed;
  286. top: 0;
  287. left: 0;
  288. right: 0;
  289. bottom: 0;
  290. background: rgba(0, 0, 0, 0.5);
  291. z-index: 100;
  292. display: flex;
  293. flex-direction: column;
  294. justify-content: flex-end;
  295. opacity: 0;
  296. visibility: hidden;
  297. pointer-events: none;
  298. transition: opacity 0.3s ease;
  299. }
  300. .login-sheet-mask.sheet-open {
  301. opacity: 1;
  302. visibility: visible;
  303. pointer-events: auto;
  304. }
  305. .login-sheet-panel {
  306. background: #FFF;
  307. border-radius: 32rpx 32rpx 0 0;
  308. padding-bottom: calc(constant(safe-area-inset-bottom) + 40rpx);
  309. width: 100%;
  310. transform: translateY(100%);
  311. transition: transform 0.3s cubic-bezier(0.25, 1, 0.5, 1);
  312. }
  313. .login-sheet-mask.sheet-open .login-sheet-panel {
  314. transform: translateY(0);
  315. }
  316. .sheet-handle {
  317. width: 80rpx;
  318. height: 8rpx;
  319. background: #E2E8F0;
  320. border-radius: 4rpx;
  321. margin: 24rpx auto;
  322. }
  323. .sheet-content {
  324. padding: 0 60rpx 40rpx;
  325. display: flex;
  326. flex-direction: column;
  327. align-items: center;
  328. }
  329. .sheet-header {
  330. text-align: center;
  331. margin-bottom: 48rpx;
  332. }
  333. .sheet-title {
  334. display: block;
  335. font-size: 36rpx;
  336. font-weight: 700;
  337. color: #1E293B;
  338. margin-bottom: 12rpx;
  339. }
  340. .sheet-sub {
  341. display: block;
  342. font-size: 26rpx;
  343. color: #64748B;
  344. }
  345. .sheet-login-btn {
  346. width: 100%;
  347. height: 96rpx;
  348. line-height: 96rpx;
  349. background: linear-gradient(135deg, #F97316, #EA580C);
  350. color: #FFF;
  351. font-size: 32rpx;
  352. font-weight: 600;
  353. border-radius: 48rpx;
  354. border: none;
  355. margin-bottom: 32rpx;
  356. }
  357. .sheet-login-btn:active {
  358. opacity: 0.9;
  359. }
  360. .sheet-footer {
  361. text-align: center;
  362. }
  363. .sheet-footer-text {
  364. font-size: 22rpx;
  365. color: #94A3B8;
  366. }
  367. </style>