Du kannst nicht mehr als 25 Themen auswählen Themen müssen entweder mit einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.
 
 
 

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