Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.
 
 
 

313 рядки
7.0 KiB

  1. <template>
  2. <view class="page">
  3. <scroll-view class="scroll" scroll-y>
  4. <view class="wrap">
  5. <view v-if="isLoadingState" class="state-card">
  6. <view v-if="detail.is_suc === 0" class="spinner"></view>
  7. <text class="state-title">{{ detail.is_suc === 0 ? '创作中,请耐心等待' : '生成失败' }}</text>
  8. <text v-if="detail.is_suc === 0 && countDown > 0" class="count">{{ countDown }} S</text>
  9. <text v-if="detail.is_suc < 0" class="error">{{ detail.content }}</text>
  10. </view>
  11. <view v-else class="image-card" @click="preview">
  12. <image class="work-img" :src="detail.picUrl" mode="widthFix" />
  13. <view v-if="detail.picList && detail.picList.length > 1" class="num">{{ detail.picList.length }}</view>
  14. </view>
  15. <view class="info-card">
  16. <view class="title">作品信息</view>
  17. <view v-if="detail.prompts" class="cell">
  18. <text class="cell-label">画面描述:</text>
  19. <text class="cell-content" user-select>{{ detail.prompts }}</text>
  20. </view>
  21. <view class="cell">
  22. <text class="cell-label">作者:</text>
  23. <text class="cell-content">{{ detail.author_name || '-' }}</text>
  24. </view>
  25. <view class="cell">
  26. <text class="cell-label">作品来源:</text>
  27. <text class="link" @click="createAgain">{{ detail.ai_type_name || aiTypeName }}</text>
  28. </view>
  29. </view>
  30. </view>
  31. </scroll-view>
  32. <view v-if="!isLoadingState" class="bottom-bar">
  33. <view class="bar-btn" @click="download">下载</view>
  34. <button class="bar-btn" open-type="share">分享</button>
  35. <view class="main-btn" @click="createAgain">再画一张</view>
  36. </view>
  37. </view>
  38. </template>
  39. <script setup>
  40. import { computed, ref } from 'vue'
  41. import { onLoad, onShareAppMessage, onUnload } from '@dcloudio/uni-app'
  42. import { toolApi } from '@/api/index.js'
  43. import { useApp } from '@/utils/useApp.js'
  44. const { toast } = useApp()
  45. const id = ref('')
  46. const imgIndex = ref(0)
  47. const from = ref('')
  48. const detail = ref({ picList: [], is_suc: 0 })
  49. const countDown = ref(0)
  50. let timer = 0
  51. let countTimer = 0
  52. const aiTypeName = computed(() => {
  53. const map = { 3: '动漫头像', 7: '动漫头像', 8: '动漫头像', 2: 'MJ绘画', 6: 'AI绘画' }
  54. return map[detail.value.ai_type] || 'AI绘画'
  55. })
  56. const isLoadingState = computed(() => [0, -1, -2].includes(Number(detail.value.is_suc)))
  57. onLoad((options) => {
  58. const parts = String(options.id || '').split('_')
  59. id.value = parts[0] || ''
  60. imgIndex.value = Number(parts[1] || 0)
  61. from.value = options.from || ''
  62. loadDetail()
  63. })
  64. onUnload(() => {
  65. clearTimeout(timer)
  66. clearInterval(countTimer)
  67. })
  68. onShareAppMessage(() => ({
  69. title: '推荐给你看一个 AI 绘画作品',
  70. path: `/pages/tool/work-detail?id=${id.value}_0`,
  71. imageUrl: detail.value.picUrl || ''
  72. }))
  73. async function loadDetail() {
  74. if (!id.value) return
  75. try {
  76. const api = from.value === 'anime' ? toolApi.getAnimeDetail : toolApi.getWorkDetail
  77. const res = await api({ chat_open_id: id.value })
  78. const data = res.data || {}
  79. const pics = buildPicList(data)
  80. detail.value = {
  81. ...data,
  82. picList: pics,
  83. picUrl: pics[imgIndex.value] || pics[0] || data.content || '',
  84. ai_type_name: data.ai_type_name || aiTypeName.value
  85. }
  86. if (Number(detail.value.is_suc) === 0) waitNext()
  87. } catch (e) {
  88. toast(e.msg || '作品详情加载失败')
  89. }
  90. }
  91. function buildPicList(data) {
  92. if (Array.isArray(data.picList) && data.picList.length) return data.picList
  93. if (data.imgs) return String(data.imgs).split(',').filter(Boolean)
  94. if (data.content && String(data.content).includes(',')) return String(data.content).split(',').filter(Boolean)
  95. return data.content ? [data.content] : []
  96. }
  97. function waitNext() {
  98. clearTimeout(timer)
  99. clearInterval(countTimer)
  100. countDown.value = 3
  101. countTimer = setInterval(() => {
  102. countDown.value -= 1
  103. if (countDown.value <= 0) clearInterval(countTimer)
  104. }, 1000)
  105. timer = setTimeout(loadDetail, 3000)
  106. }
  107. function preview() {
  108. const urls = detail.value.picList || []
  109. if (!urls.length) return
  110. uni.previewImage({ urls, current: detail.value.picUrl })
  111. }
  112. function download() {
  113. if (!detail.value.picUrl) return
  114. uni.previewImage({ urls: [detail.value.picUrl] })
  115. }
  116. function createAgain() {
  117. const delta = getPageStackDelta('pages/tool/anime-avatar')
  118. if (delta > 0) {
  119. if (id.value) uni.setStorageSync('anime_copy_id', id.value)
  120. uni.navigateBack({ delta })
  121. return
  122. }
  123. uni.navigateTo({ url: `/pages/tool/anime-avatar?copy_id=${id.value}` })
  124. }
  125. function getPageStackDelta(routeName) {
  126. const pages = getCurrentPages()
  127. const index = pages.findIndex((page) => page.route === routeName)
  128. return index >= 0 ? pages.length - 1 - index : -1
  129. }
  130. </script>
  131. <style lang="scss" scoped>
  132. .page {
  133. height: 100vh;
  134. background: #f4f7fb;
  135. display: flex;
  136. flex-direction: column;
  137. overflow: hidden;
  138. }
  139. .scroll {
  140. flex: 1;
  141. height: 0;
  142. }
  143. /* 底栏是 fixed 的,留够 300rpx 才不会盖住内容 */
  144. .wrap {
  145. padding: 28rpx 28rpx 300rpx;
  146. }
  147. .state-card,
  148. .image-card,
  149. .info-card {
  150. background: #fff;
  151. border-radius: 18rpx;
  152. overflow: hidden;
  153. }
  154. .state-card {
  155. min-height: 540rpx;
  156. display: flex;
  157. flex-direction: column;
  158. align-items: center;
  159. justify-content: center;
  160. color: #172033;
  161. font-size: 30rpx;
  162. }
  163. .spinner {
  164. width: 72rpx;
  165. height: 72rpx;
  166. border: 8rpx solid #e9edf5;
  167. border-top-color: #0b8cff;
  168. border-radius: 50%;
  169. animation: spin 0.8s linear infinite;
  170. margin-bottom: 28rpx;
  171. }
  172. .count,
  173. .error {
  174. margin-top: 18rpx;
  175. color: #7f8896;
  176. font-size: 26rpx;
  177. }
  178. .error {
  179. color: #f04438;
  180. padding: 0 40rpx;
  181. text-align: center;
  182. }
  183. .work-img {
  184. width: 100%;
  185. display: block;
  186. }
  187. .image-card {
  188. position: relative;
  189. }
  190. .num {
  191. position: absolute;
  192. right: 18rpx;
  193. bottom: 18rpx;
  194. min-width: 44rpx;
  195. height: 44rpx;
  196. border-radius: 22rpx;
  197. color: #fff;
  198. font-size: 24rpx;
  199. line-height: 44rpx;
  200. text-align: center;
  201. background: rgba(0, 0, 0, 0.45);
  202. }
  203. .info-card {
  204. margin-top: 24rpx;
  205. padding: 28rpx;
  206. }
  207. .title {
  208. color: #172033;
  209. font-size: 32rpx;
  210. font-weight: 700;
  211. margin-bottom: 16rpx;
  212. }
  213. .cell {
  214. display: flex;
  215. color: #7f8896;
  216. font-size: 27rpx;
  217. line-height: 1.6;
  218. padding: 10rpx 0;
  219. }
  220. .cell-label {
  221. flex-shrink: 0;
  222. }
  223. .cell-content {
  224. flex: 1;
  225. min-width: 0;
  226. color: #172033;
  227. }
  228. .link {
  229. color: #0b8cff;
  230. }
  231. .bottom-bar {
  232. position: fixed;
  233. left: 0;
  234. right: 0;
  235. bottom: 0;
  236. padding: 18rpx 28rpx calc(18rpx + env(safe-area-inset-bottom));
  237. background: #fff;
  238. display: flex;
  239. gap: 18rpx;
  240. box-shadow: 0 -8rpx 24rpx rgba(35, 55, 90, 0.08);
  241. }
  242. .bar-btn,
  243. .main-btn {
  244. height: 82rpx;
  245. border-radius: 42rpx;
  246. font-size: 29rpx;
  247. margin: 0;
  248. padding: 0;
  249. border: 0;
  250. display: flex;
  251. align-items: center;
  252. justify-content: center;
  253. }
  254. .bar-btn::after {
  255. border: 0;
  256. }
  257. .bar-btn {
  258. width: 150rpx;
  259. color: #0b8cff;
  260. background: #eef6ff;
  261. }
  262. .main-btn {
  263. flex: 1;
  264. color: #fff;
  265. background: #0b8cff;
  266. }
  267. @keyframes spin {
  268. to {
  269. transform: rotate(360deg);
  270. }
  271. }
  272. </style>