Non puoi selezionare più di 25 argomenti Gli argomenti devono iniziare con una lettera o un numero, possono includere trattini ('-') e possono essere lunghi fino a 35 caratteri.
 
 
 

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