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.
 
 
 

423 rader
11 KiB

  1. <template>
  2. <view class="page">
  3. <scroll-view class="scroll" scroll-y>
  4. <view class="wrap">
  5. <view class="hero-card">
  6. <view class="hero-copy">
  7. <text class="eyebrow">OLD PHOTO RESTORE</text>
  8. <text class="hero-title">老照片修复 / 上色</text>
  9. <text class="hero-desc">适合黑白、泛黄、模糊、划痕或破损照片,先搭建 UI,生成逻辑后续接入。</text>
  10. </view>
  11. <view class="hero-preview">
  12. <view class="photo before"></view>
  13. <view class="photo after"></view>
  14. </view>
  15. </view>
  16. <view class="upload-card">
  17. <view class="card-head">
  18. <view>
  19. <text class="card-title"><text class="required">*</text>上传照片</text>
  20. <text class="card-desc">支持黑白、泛黄、模糊、划痕或破损照片</text>
  21. </view>
  22. <text class="card-tip">0/1</text>
  23. </view>
  24. <view class="upload-strip" @click="showTodo('选择老照片')">
  25. <view class="upload-thumb"><text class="upload-plus">+</text></view>
  26. <view class="upload-copy">
  27. <text class="upload-title">上传需要修复的老照片</text>
  28. <text class="upload-sub">建议选择主体清晰、人物不被严重遮挡的照片</text>
  29. </view>
  30. <view class="upload-action">上传</view>
  31. </view>
  32. </view>
  33. <view class="section">
  34. <view class="section-head">
  35. <text class="tool-section-title">照片风格</text>
  36. <text class="section-tip">输出方式</text>
  37. </view>
  38. <view class="style-grid">
  39. <view
  40. v-for="item in styleOptions"
  41. :key="item"
  42. class="style-item"
  43. :class="{ active: activeStyle === item }"
  44. @click="activeStyle = item"
  45. >
  46. {{ item }}
  47. </view>
  48. </view>
  49. </view>
  50. <view class="section">
  51. <view class="section-head">
  52. <text class="tool-section-title">描述关键词</text>
  53. <text class="section-tip">点击添加</text>
  54. </view>
  55. <textarea class="textarea" placeholder="补充照片修复要求,例如保留原始五官、增强清晰度、自然上色"></textarea>
  56. <view class="keyword-row">
  57. <view
  58. v-for="item in keywords"
  59. :key="item"
  60. class="keyword-chip"
  61. :class="{ active: selectedKeywords.includes(item) }"
  62. @click="toggleKeyword(item)"
  63. >
  64. {{ item }}
  65. </view>
  66. </view>
  67. </view>
  68. </view>
  69. </scroll-view>
  70. <view class="bottom-bar">
  71. <view class="balance">
  72. <text class="balance-copy">{{ balanceText }}</text>
  73. <view v-if="isBalanceInsufficient" class="recharge-link" @click="goRecharge">去充值</view>
  74. </view>
  75. <view class="buttons">
  76. <view class="history-btn" @click="showTodo('历史记录')">历史记录</view>
  77. <view class="submit-btn" @click="showTodo('老照片修复')">
  78. <image class="submit-icon" src="/static/icons/bolt.svg" mode="aspectFit" />
  79. <text>{{ submitText }}</text>
  80. </view>
  81. </view>
  82. </view>
  83. </view>
  84. </template>
  85. <script setup>
  86. import { computed, ref } from 'vue'
  87. import { onLoad, onShow } from '@dcloudio/uni-app'
  88. import { billingApi, userApi } from '@/api/index.js'
  89. import { useUserStore } from '@/store/user.js'
  90. import { useApp } from '@/utils/useApp.js'
  91. const userStore = useUserStore()
  92. const { toast } = useApp()
  93. const activeStyle = ref('彩色')
  94. const selectedKeywords = ref(['4K', '自然上色'])
  95. const balance = ref({ points: 0 })
  96. const quote = ref({ points: 0 })
  97. const appKey = 'old-photo'
  98. const styleOptions = ['彩色', '黑白']
  99. const keywords = [
  100. '4K',
  101. '自然上色',
  102. '去掉划痕',
  103. '去褶皱',
  104. '去噪点',
  105. '去除斑驳破损',
  106. '衣服修复',
  107. '面部还原真实人像',
  108. '还原人脸五官细节',
  109. '眼神神态自然',
  110. '修补缺失破损区域',
  111. '画质超清增强',
  112. '锐化细节',
  113. '保留年代质感',
  114. '人物容貌不变',
  115. '不扭曲五官'
  116. ]
  117. const currentPoints = computed(() => Number(balance.value.points || balance.value.point_balance || 0))
  118. const currentCost = computed(() => Number(quote.value.points || 0))
  119. const balanceText = computed(() => {
  120. const cost = currentCost.value
  121. if (!userStore.isLogin) return cost ? `老照片修复 · ${cost}点/次` : '老照片修复'
  122. return cost ? `余额${currentPoints.value}点 · 预计消耗${cost}点` : `余额${currentPoints.value}点`
  123. })
  124. const isBalanceInsufficient = computed(() => userStore.isLogin && currentCost.value > 0 && currentPoints.value < currentCost.value)
  125. const submitText = computed(() => currentCost.value ? `立即修复 · ${currentCost.value}点` : '立即修复')
  126. onLoad(() => {
  127. loadBillingConfig()
  128. })
  129. onShow(() => {
  130. if (userStore.isLogin) loadBalance()
  131. loadBillingConfig()
  132. })
  133. async function loadBalance() {
  134. try {
  135. const res = await userApi.getBalance()
  136. balance.value = res.data || {}
  137. } catch (e) {}
  138. }
  139. async function loadBillingConfig() {
  140. try {
  141. const res = await billingApi.getConfig({ app_key: appKey, image_count: 1 })
  142. const data = res.data || {}
  143. quote.value = data.quote || {}
  144. if (data.balance) balance.value = data.balance
  145. } catch (e) {}
  146. }
  147. function goRecharge() {
  148. uni.navigateTo({ url: '/pages/recharge/recharge' })
  149. }
  150. function toggleKeyword(item) {
  151. const index = selectedKeywords.value.indexOf(item)
  152. if (index >= 0) {
  153. selectedKeywords.value.splice(index, 1)
  154. } else {
  155. selectedKeywords.value.push(item)
  156. }
  157. }
  158. function showTodo(name) {
  159. toast(`${name}逻辑待接入`)
  160. }
  161. </script>
  162. <style lang="scss" scoped>
  163. .page {
  164. height: 100vh;
  165. background: #f3f7fb;
  166. display: flex;
  167. flex-direction: column;
  168. overflow: hidden;
  169. }
  170. .scroll { flex: 1; height: 0; }
  171. .wrap { padding: 24rpx 28rpx 300rpx; }
  172. .hero-card {
  173. position: relative;
  174. min-height: 260rpx;
  175. padding: 30rpx;
  176. border-radius: 28rpx;
  177. color: #fff;
  178. background: linear-gradient(135deg, #172033 0%, #2d5d7d 56%, #5cb7c4 100%);
  179. overflow: hidden;
  180. box-sizing: border-box;
  181. }
  182. .hero-card::before {
  183. content: '';
  184. position: absolute;
  185. right: -90rpx;
  186. top: -90rpx;
  187. width: 280rpx;
  188. height: 280rpx;
  189. border-radius: 50%;
  190. background: rgba(255, 255, 255, 0.12);
  191. }
  192. .hero-copy,
  193. .hero-preview {
  194. position: relative;
  195. z-index: 1;
  196. }
  197. .eyebrow {
  198. display: block;
  199. color: rgba(255, 255, 255, 0.68);
  200. font-size: 21rpx;
  201. font-weight: 800;
  202. }
  203. .hero-title {
  204. display: block;
  205. margin-top: 16rpx;
  206. color: #fff;
  207. font-size: 40rpx;
  208. font-weight: 900;
  209. }
  210. .hero-desc {
  211. display: block;
  212. margin-top: 14rpx;
  213. width: 430rpx;
  214. color: rgba(255, 255, 255, 0.82);
  215. font-size: 24rpx;
  216. line-height: 1.5;
  217. }
  218. .hero-preview {
  219. position: absolute;
  220. right: 30rpx;
  221. bottom: 28rpx;
  222. width: 170rpx;
  223. height: 116rpx;
  224. }
  225. .photo {
  226. position: absolute;
  227. width: 96rpx;
  228. height: 116rpx;
  229. border-radius: 16rpx;
  230. box-shadow: 0 16rpx 28rpx rgba(0, 0, 0, 0.18);
  231. }
  232. .photo.before {
  233. left: 0;
  234. top: 0;
  235. background: linear-gradient(135deg, #d7d0c6, #6c6259);
  236. }
  237. .photo.after {
  238. right: 0;
  239. bottom: 0;
  240. background: linear-gradient(135deg, #f4d4a0, #5db2d9 50%, #254f6d);
  241. }
  242. .upload-card,
  243. .section {
  244. margin-top: 22rpx;
  245. padding: 28rpx;
  246. border-radius: 24rpx;
  247. background: #fff;
  248. box-shadow: 0 8rpx 28rpx rgba(34, 68, 112, 0.05);
  249. }
  250. .card-head,
  251. .section-head {
  252. display: flex;
  253. align-items: baseline;
  254. }
  255. .card-title,
  256. .tool-section-title {
  257. display: block;
  258. color: #172033;
  259. font-size: 31rpx;
  260. font-weight: 800;
  261. }
  262. .required { margin-right: 4rpx; color: #ef4444; }
  263. .card-desc {
  264. display: block;
  265. margin-top: 8rpx;
  266. color: #8a95a6;
  267. font-size: 23rpx;
  268. line-height: 1.35;
  269. }
  270. .card-tip,
  271. .section-tip {
  272. margin-left: auto;
  273. color: #8a95a6;
  274. font-size: 23rpx;
  275. }
  276. .upload-strip {
  277. margin-top: 22rpx;
  278. min-height: 142rpx;
  279. padding: 18rpx;
  280. border: 2rpx dashed rgba(11, 140, 255, 0.28);
  281. border-radius: 20rpx;
  282. background: #f8fbff;
  283. display: flex;
  284. align-items: center;
  285. box-sizing: border-box;
  286. }
  287. .upload-thumb {
  288. width: 104rpx;
  289. height: 104rpx;
  290. border-radius: 18rpx;
  291. background: #eef6ff;
  292. display: flex;
  293. align-items: center;
  294. justify-content: center;
  295. flex-shrink: 0;
  296. }
  297. .upload-plus { color: #0b8cff; font-size: 48rpx; line-height: 1; }
  298. .upload-copy { min-width: 0; flex: 1; margin-left: 20rpx; }
  299. .upload-title { display: block; color: #172033; font-size: 29rpx; font-weight: 800; }
  300. .upload-sub { display: block; margin-top: 8rpx; color: #7f8896; font-size: 23rpx; line-height: 1.4; }
  301. .upload-action { margin-left: 18rpx; color: #0b8cff; font-size: 25rpx; font-weight: 800; }
  302. .style-grid {
  303. margin-top: 20rpx;
  304. display: grid;
  305. grid-template-columns: repeat(2, minmax(0, 1fr));
  306. gap: 14rpx;
  307. }
  308. .style-item {
  309. height: 76rpx;
  310. border-radius: 18rpx;
  311. color: #596579;
  312. font-size: 26rpx;
  313. font-weight: 800;
  314. line-height: 76rpx;
  315. text-align: center;
  316. background: #f7f9fd;
  317. border: 2rpx solid transparent;
  318. }
  319. .style-item.active {
  320. color: #0b8cff;
  321. background: #eff8ff;
  322. border-color: #0b8cff;
  323. }
  324. .textarea {
  325. margin-top: 20rpx;
  326. width: 100%;
  327. min-height: 180rpx;
  328. padding: 22rpx;
  329. border-radius: 20rpx;
  330. color: #172033;
  331. font-size: 25rpx;
  332. line-height: 1.5;
  333. background: #f7f9fd;
  334. box-sizing: border-box;
  335. }
  336. .keyword-row {
  337. margin-top: 18rpx;
  338. display: flex;
  339. flex-wrap: wrap;
  340. gap: 14rpx;
  341. }
  342. .keyword-chip {
  343. min-height: 58rpx;
  344. padding: 0 22rpx;
  345. border-radius: 30rpx;
  346. color: #0b8cff;
  347. font-size: 24rpx;
  348. font-weight: 700;
  349. line-height: 58rpx;
  350. background: #eef7ff;
  351. border: 2rpx solid transparent;
  352. box-sizing: border-box;
  353. }
  354. .keyword-chip.active {
  355. color: #fff;
  356. background: #0b8cff;
  357. }
  358. .bottom-bar {
  359. position: fixed;
  360. left: 0;
  361. right: 0;
  362. bottom: 0;
  363. z-index: 80;
  364. padding: 18rpx 28rpx calc(18rpx + env(safe-area-inset-bottom));
  365. background: rgba(255, 255, 255, 0.96);
  366. box-shadow: 0 -10rpx 30rpx rgba(32, 56, 92, 0.1);
  367. }
  368. .balance {
  369. margin-bottom: 14rpx;
  370. display: flex;
  371. align-items: center;
  372. justify-content: space-between;
  373. gap: 20rpx;
  374. color: #7b8797;
  375. font-size: 24rpx;
  376. line-height: 1.35;
  377. }
  378. .balance-copy { min-width: 0; flex: 1; }
  379. .recharge-link { color: #0b8cff; font-size: 24rpx; font-weight: 800; flex-shrink: 0; }
  380. .buttons { display: flex; gap: 22rpx; }
  381. .history-btn,
  382. .submit-btn {
  383. height: 86rpx;
  384. border-radius: 999rpx;
  385. font-size: 30rpx;
  386. font-weight: 800;
  387. display: flex;
  388. align-items: center;
  389. justify-content: center;
  390. }
  391. .history-btn { width: 210rpx; color: #1f8cff; background: #eef7ff; }
  392. .submit-btn {
  393. flex: 1;
  394. color: #fff;
  395. background: linear-gradient(135deg, #1f8cff, #41b9ff);
  396. box-shadow: 0 12rpx 24rpx rgba(31, 140, 255, 0.28);
  397. }
  398. .submit-icon {
  399. width: 24rpx;
  400. height: 24rpx;
  401. margin-right: 8rpx;
  402. }
  403. </style>