diff --git a/frontend/src/views/ImagesView.vue b/frontend/src/views/ImagesView.vue
index 2ba0ade..2c31f2e 100644
--- a/frontend/src/views/ImagesView.vue
+++ b/frontend/src/views/ImagesView.vue
@@ -144,8 +144,9 @@ onUnmounted(() => window.removeEventListener('keydown', onKey))
@mouseenter="$event.target.play && $event.target.play()"
@mouseleave="$event.target.pause && $event.target.pause()" />
-
![]()
+
+
diff --git a/frontend/src/views/OrdersView.vue b/frontend/src/views/OrdersView.vue
new file mode 100644
index 0000000..e4c124f
--- /dev/null
+++ b/frontend/src/views/OrdersView.vue
@@ -0,0 +1,191 @@
+
+
+
+
+
+
+
订单
+
{{ total }} 笔充值订单 · 未支付可继续支付
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 加载中…
+
+
+ 还没有充值订单
+
+
+
+
+
+
+ | 订单号 |
+ 下单时间 |
+ 支付时间 |
+ 金额 |
+ 充值积分 |
+ 状态 |
+ 操作 |
+
+
+
+
+ | {{ o.id }} |
+ {{ fmt(o.created_at) }} |
+ {{ fmt(o.paid_at) }} |
+ ¥{{ o.amount }} |
+ {{ o.points }} |
+
+
+ {{ STATUS[o.status] }}
+ · {{ METHOD[o.pay_type] || o.pay_type }}
+
+ |
+
+
+ —
+ |
+
+
+
+
+
+
{{ pageStart }}–{{ pageEnd }}/ {{ total }} 笔
+
+
+ …
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/views/PlaygroundView.vue b/frontend/src/views/PlaygroundView.vue
index 39f4233..8884d71 100644
--- a/frontend/src/views/PlaygroundView.vue
+++ b/frontend/src/views/PlaygroundView.vue
@@ -360,7 +360,7 @@ let prevPending = 0
async function loadHistory() {
// Server-side filter: status IN (pending, success), newest 12 — exactly the
// rows the grid shows, in one query (no client over-fetch).
- const r = await api('/logs?limit=10&statuses=pending,success')
+ const r = await api('/logs?limit=10&statuses=pending,success&source=user')
if (!r.ok) return
history.value = (r.data?.data || [])
.filter((e) => e.status === 'pending' || e.file)
@@ -432,20 +432,16 @@ function lastFrameDataUrl(url) {
})
}
-// Click a generated VIDEO. For a 首尾帧 (frame) model, set the video's LAST frame
-// as the 首帧 (first reference) — to continue the scene. Otherwise just zoom.
-async function onVideoClick(item) {
- if (refMode.value === 'frame' && maxRefs.value > 0 && item.url) {
- const dataUrl = await lastFrameDataUrl(item.url)
- if (dataUrl) {
- const ref = { name: 'frame', dataUrl }
- if (refImages.value.length === 0) refImages.value = [ref]
- else refImages.value.splice(0, 1, ref) // replace the 首帧 slot
- flash('已把视频末帧设为首帧')
- return
- }
- }
- lightbox.value = item
+// Use a generated VIDEO's LAST frame as the 首帧 (first reference) — 首尾帧
+// (frame) models only. Triggered by the small button; clicking the video zooms.
+async function useVideoFrame(item) {
+ if (!item || !item.url) return
+ const dataUrl = await lastFrameDataUrl(item.url)
+ if (!dataUrl) { flash('截取末帧失败'); return }
+ const ref = { name: 'frame', dataUrl }
+ if (refImages.value.length === 0) refImages.value = [ref]
+ else refImages.value.splice(0, 1, ref) // replace the 首帧 slot
+ flash('已把视频末帧设为首帧')
}
function onKey(e) { if (e.key === 'Escape') lightbox.value = null }
@@ -639,26 +635,28 @@ onUnmounted(() => {
-
+
+
-
+
-
-
{{ item.prompt }}
-
{{ item.model }} · {{ (item.elapsed_ms / 1000).toFixed(1) }}s
+
{{ item.prompt }}
+
{{ item.model }} · {{ (item.elapsed_ms / 1000).toFixed(1) }}s
@@ -705,4 +703,10 @@ onUnmounted(() => {
diff --git a/frontend/src/views/SettingsView.vue b/frontend/src/views/SettingsView.vue
index b8df185..f74955b 100644
--- a/frontend/src/views/SettingsView.vue
+++ b/frontend/src/views/SettingsView.vue
@@ -3,6 +3,7 @@ import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { auth, refreshMe, logout as authLogout } from '../auth'
import { api, jsonBody } from '../api'
+import { openPayment } from '../payment'
import Icon from '../components/Icon.vue'
import { points, pointsLabel } from '../credits'
import { site } from '../site'
@@ -174,6 +175,41 @@ function toast(m) {
clearTimeout(toastTimer)
toastTimer = setTimeout(() => (toastMsg.value = ''), 2200)
}
+
+// ---- Recharge (易支付) ----
+const payCfg = ref({ enabled: false, methods: [], min_amount: 0, points_ratio: 100 })
+const AMOUNTS = [10, 20, 50, 100]
+const picked = ref(10) // a preset number, or 'custom'
+const customAmount = ref('')
+const payMethod = ref('')
+const rechargeTotal = computed(() => Number(auth.user?.recharge_total || 0))
+const methodName = (m) => ({ wxpay: '微信', alipay: '支付宝' }[m] || m)
+const finalAmount = computed(() => Number(picked.value === 'custom' ? customAmount.value : picked.value) || 0)
+const pointsPreview = computed(() => Math.round(finalAmount.value * (payCfg.value.points_ratio || 0)))
+async function loadPayCfg() {
+ const r = await api('/pay/config')
+ if (r.ok && r.data) {
+ payCfg.value = r.data
+ if (r.data.methods?.length && !payMethod.value) payMethod.value = r.data.methods[0]
+ }
+}
+onMounted(loadPayCfg)
+const recharging = ref(false)
+async function recharge() {
+ if (recharging.value) return
+ const amt = finalAmount.value
+ if (!amt || amt <= 0) { toast('请输入有效金额'); return }
+ if (amt < payCfg.value.min_amount) { toast(`最低充值 ${payCfg.value.min_amount} 元`); return }
+ if (!payMethod.value) { toast('请选择支付方式'); return }
+ recharging.value = true
+ try {
+ const r = await api('/pay/recharge', jsonBody('POST', { amount: amt, method: payMethod.value }))
+ if (!r.ok) { toast(r.data?.detail || '下单失败'); return }
+ openPayment(r.data, { onPaid: refreshMe })
+ } finally {
+ recharging.value = false
+ }
+}
@@ -214,6 +250,10 @@ function toast(m) {
积分余额
{{ pointsLabel(balance) }}