Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.
 
 
 

348 rader
7.9 KiB

  1. <template>
  2. <view class="toolbox">
  3. <ai-nav-title title="AI在线-解放你的生产力" />
  4. <scroll-view
  5. class="page-scroll"
  6. scroll-y
  7. refresher-enabled
  8. :refresher-triggered="refreshing"
  9. @refresherrefresh="onRefresh"
  10. >
  11. <view class="toolbox-content">
  12. <swiper v-if="banners.length" class="hero" circular autoplay :interval="4000" @change="onBannerChange">
  13. <swiper-item v-for="(banner, i) in banners" :key="i" @click="onTool(banner)">
  14. <image class="hero-img" :src="banner.image" mode="aspectFill" />
  15. </swiper-item>
  16. </swiper>
  17. <view v-if="banners.length" class="hero-indicator">
  18. <view
  19. v-for="(banner, i) in banners"
  20. :key="banner.id || i"
  21. class="indicator-bar"
  22. :class="{ active: i === bannerIndex }"
  23. ></view>
  24. </view>
  25. <view v-if="recommends.length" class="recommend">
  26. <view v-for="item in recommends" :key="item.id || item.type" class="recommend-card" @click="onTool(item)">
  27. <image class="recommend-img" :src="item.image" mode="aspectFill" />
  28. </view>
  29. </view>
  30. <view class="tool-list">
  31. <view v-for="item in apps" :key="item.id || item.type || item.title" class="tool-row" @click="onTool(item)">
  32. <view class="tool-icon-wrap">
  33. <image class="tool-icon" :src="item.icon" mode="aspectFill" />
  34. </view>
  35. <view class="tool-main">
  36. <text class="tool-name">{{ item.name }}</text>
  37. <text class="tool-desc">{{ item.desc }}</text>
  38. </view>
  39. <text class="chevron">›</text>
  40. </view>
  41. </view>
  42. </view>
  43. </scroll-view>
  44. </view>
  45. </template>
  46. <script setup>
  47. import { ref } from 'vue'
  48. import { onLoad } from '@dcloudio/uni-app'
  49. import { commonApi } from '@/api/index.js'
  50. import { useApp } from '@/utils/useApp.js'
  51. const { toast } = useApp()
  52. const refreshing = ref(false)
  53. const bannerIndex = ref(0)
  54. const AI_CDN_BASE = 'https://cdn.aionline.cc/ai'
  55. const defaultBanners = []
  56. const defaultRecommends = [
  57. {
  58. name: 'AI动漫头像',
  59. title: 'AI动漫头像',
  60. image: `${AI_CDN_BASE}/toolbox/home_card_avatar.png`,
  61. link_type: 'app',
  62. type: 'anime-avatar'
  63. },
  64. {
  65. name: 'AI取名',
  66. title: 'AI取名',
  67. image: `${AI_CDN_BASE}/toolbox/home_card_name.png`,
  68. link_type: 'app',
  69. type: 'ai-name'
  70. }
  71. ]
  72. const defaultApps = [
  73. {
  74. icon: `${AI_CDN_BASE}/toolbox/tool_id_photo.png`,
  75. name: 'AI证件照/签证照',
  76. title: 'AI证件照/签证照',
  77. desc: '各类规格证件照、免冠照、换底色换正装',
  78. subtitle: '各类规格证件照、免冠照、换底色换正装',
  79. link_type: 'app',
  80. type: 'id-photo'
  81. },
  82. {
  83. icon: `${AI_CDN_BASE}/toolbox/tool_portrait.png`,
  84. name: 'AI形象照/职业照/写真',
  85. title: 'AI形象照/职业照/写真',
  86. desc: '上传自拍生成职业照、艺术写真、古风',
  87. subtitle: '上传自拍生成职业照、艺术写真、古风',
  88. link_type: 'app',
  89. type: 'portrait-photo'
  90. },
  91. {
  92. icon: `${AI_CDN_BASE}/toolbox/tool_id_photo.png`,
  93. name: '老照片上色',
  94. title: '老照片上色',
  95. desc: '模糊破损老照片修复、黑白照片自然上色',
  96. subtitle: '模糊破损老照片修复、黑白照片自然上色',
  97. link_type: 'app',
  98. type: 'old-photo'
  99. }
  100. ]
  101. const banners = ref(defaultBanners)
  102. const recommends = ref(defaultRecommends)
  103. const apps = ref(defaultApps)
  104. onLoad(() => {
  105. loadHomeConfig()
  106. })
  107. async function loadHomeConfig() {
  108. try {
  109. const res = await commonApi.getHomeConfig()
  110. const data = res.data || {}
  111. const nextBanners = normalizeBanners(data.banners)
  112. const nextRecommends = normalizeApps(data.recommends)
  113. const nextApps = normalizeApps(data.apps)
  114. if (nextBanners.length) banners.value = nextBanners
  115. if (nextRecommends.length) recommends.value = nextRecommends
  116. if (nextApps.length) apps.value = nextApps
  117. bannerIndex.value = 0
  118. } catch (e) {}
  119. }
  120. function normalizeBanners(list) {
  121. if (!Array.isArray(list)) return []
  122. return list
  123. .filter(item => item && item.image)
  124. .map(item => ({
  125. ...item,
  126. name: item.title,
  127. type: item.type || item.app_key || (item.link_type === 'app' ? item.link_url : '')
  128. }))
  129. }
  130. function normalizeApps(list) {
  131. if (!Array.isArray(list)) return []
  132. return list
  133. .filter(item => item && (item.image || item.icon))
  134. .map(item => ({
  135. ...item,
  136. name: item.name || item.title,
  137. desc: item.desc || item.subtitle,
  138. type: item.type || item.app_key || (item.link_type === 'app' ? item.link_url : '')
  139. }))
  140. }
  141. function onTool(item) {
  142. if (!item || item.link_type === 'none') return
  143. if (item.soon) {
  144. if (item && (item.name || item.title)) toast(`${item.name || item.title} 即将上线`)
  145. return
  146. }
  147. if (item.link_type === 'page' && item.link_url) {
  148. uni.navigateTo({ url: item.link_url })
  149. return
  150. }
  151. if (item.link_type === 'web' && item.link_url) {
  152. uni.navigateTo({ url: `/pages/webview/webview?url=${encodeURIComponent(item.link_url)}&title=${encodeURIComponent(item.name || item.title || '')}` })
  153. return
  154. }
  155. if (!item.type) {
  156. toast(`${item.name || item.title} 即将上线`)
  157. return
  158. }
  159. const routeMap = {
  160. 'anime-avatar': '/pages/tool/anime-avatar',
  161. 'ai-name': '/pages/tool/ai-name',
  162. 'id-photo': '/pages/tool/id-photo',
  163. 'portrait-photo': '/pages/tool/portrait-photo',
  164. 'old-photo': '/pages/tool/old-photo'
  165. }
  166. if (routeMap[item.type]) {
  167. uni.navigateTo({ url: routeMap[item.type] })
  168. return
  169. }
  170. uni.navigateTo({ url: `/pages/tool/detail?type=${item.type}&title=${encodeURIComponent(item.name || item.title)}` })
  171. }
  172. function onBannerChange(e) {
  173. bannerIndex.value = e.detail.current || 0
  174. }
  175. function onRefresh() {
  176. refreshing.value = true
  177. loadHomeConfig().finally(() => {
  178. refreshing.value = false
  179. })
  180. }
  181. </script>
  182. <style lang="scss" scoped>
  183. .toolbox {
  184. height: 100vh;
  185. display: flex;
  186. flex-direction: column;
  187. overflow: hidden;
  188. background: #f3f3f3;
  189. }
  190. .page-scroll {
  191. flex: 1;
  192. height: 0;
  193. }
  194. .toolbox-content {
  195. min-height: 100vh;
  196. padding: 30rpx 30rpx 96rpx;
  197. box-sizing: border-box;
  198. background: #f3f3f3;
  199. }
  200. .hero {
  201. width: 100%;
  202. height: 341rpx;
  203. border-radius: 21rpx;
  204. overflow: hidden;
  205. }
  206. .hero-img {
  207. display: block;
  208. width: 100%;
  209. height: 100%;
  210. }
  211. .hero-indicator {
  212. height: 8rpx;
  213. margin-top: -30rpx;
  214. margin-bottom: 22rpx;
  215. position: relative;
  216. z-index: 2;
  217. display: flex;
  218. justify-content: center;
  219. gap: 8rpx;
  220. pointer-events: none;
  221. }
  222. .indicator-bar {
  223. width: 22rpx;
  224. height: 6rpx;
  225. border-radius: 999rpx;
  226. background: rgba(255, 255, 255, 0.5);
  227. transition: width 0.2s ease, background 0.2s ease;
  228. }
  229. .indicator-bar.active {
  230. width: 50rpx;
  231. background: #ffffff;
  232. }
  233. .recommend {
  234. display: flex;
  235. gap: 16rpx;
  236. margin-top: 22rpx;
  237. }
  238. .recommend-card {
  239. flex: 1;
  240. min-width: 0;
  241. height: 195rpx;
  242. border-radius: 19rpx;
  243. overflow: visible;
  244. }
  245. .recommend-img {
  246. width: 100%;
  247. height: 195rpx;
  248. border-radius: 19rpx;
  249. display: block;
  250. }
  251. .tool-list {
  252. margin-top: 24rpx;
  253. }
  254. .tool-row {
  255. height: 169rpx;
  256. padding: 0 40rpx 0 44rpx;
  257. border-radius: 21rpx;
  258. background: #ffffff;
  259. display: flex;
  260. align-items: center;
  261. box-sizing: border-box;
  262. & + .tool-row {
  263. margin-top: 19rpx;
  264. }
  265. }
  266. .tool-icon-wrap {
  267. width: 91rpx;
  268. height: 91rpx;
  269. flex-shrink: 0;
  270. border-radius: 12rpx;
  271. overflow: hidden;
  272. background: #edf7ff;
  273. }
  274. .tool-icon {
  275. width: 100%;
  276. height: 100%;
  277. display: block;
  278. }
  279. .tool-main {
  280. min-width: 0;
  281. flex: 1;
  282. margin-left: 35rpx;
  283. display: flex;
  284. flex-direction: column;
  285. }
  286. .tool-name {
  287. color: #333333;
  288. font-size: 32rpx;
  289. font-weight: 400;
  290. line-height: 1.2;
  291. }
  292. .tool-desc {
  293. margin-top: 18rpx;
  294. color: #999999;
  295. font-size: 25rpx;
  296. line-height: 1.2;
  297. white-space: nowrap;
  298. overflow: hidden;
  299. text-overflow: ellipsis;
  300. }
  301. .chevron {
  302. margin-left: 16rpx;
  303. color: #c9c9c9;
  304. font-size: 54rpx;
  305. font-weight: 200;
  306. line-height: 1;
  307. }
  308. </style>