修复详情页账号截图加载
This commit is contained in:
@@ -10,6 +10,7 @@ import {
|
|||||||
type OrderAgreements,
|
type OrderAgreements,
|
||||||
} from '@/features/orders/api/orders'
|
} from '@/features/orders/api/orders'
|
||||||
import { useSessionStore } from '@/stores/session'
|
import { useSessionStore } from '@/stores/session'
|
||||||
|
import AuthImage from '@/shared/components/business/AuthImage.vue'
|
||||||
import { roundMoney, formatMoney } from '@/shared/utils/money'
|
import { roundMoney, formatMoney } from '@/shared/utils/money'
|
||||||
import {
|
import {
|
||||||
assetRegions,
|
assetRegions,
|
||||||
@@ -321,7 +322,13 @@ function listingPrice(item: Listing) {
|
|||||||
<div v-if="listing" class="pc-detail-layout">
|
<div v-if="listing" class="pc-detail-layout">
|
||||||
<section class="pc-detail-main">
|
<section class="pc-detail-main">
|
||||||
<div class="detail-hero-card">
|
<div class="detail-hero-card">
|
||||||
<img v-if="coverURL" :src="coverURL" :alt="getListingTitle(listing)" />
|
<AuthImage
|
||||||
|
v-if="coverURL"
|
||||||
|
:source="coverURL"
|
||||||
|
:alt="getListingTitle(listing)"
|
||||||
|
image-class="detail-hero-img"
|
||||||
|
loading="eager"
|
||||||
|
/>
|
||||||
<span v-else>HFB ACCOUNT</span>
|
<span v-else>HFB ACCOUNT</span>
|
||||||
<div class="detail-hero-overlay">
|
<div class="detail-hero-overlay">
|
||||||
<div class="detail-tags">
|
<div class="detail-tags">
|
||||||
@@ -417,7 +424,7 @@ function listingPrice(item: Listing) {
|
|||||||
</div>
|
</div>
|
||||||
<div class="detail-screenshot-grid">
|
<div class="detail-screenshot-grid">
|
||||||
<figure v-for="shot in detailScreenshots" :key="shot.url" class="detail-screenshot">
|
<figure v-for="shot in detailScreenshots" :key="shot.url" class="detail-screenshot">
|
||||||
<img :src="shot.url" :alt="shot.label" loading="lazy" decoding="async" />
|
<AuthImage :source="shot.url" :alt="shot.label" />
|
||||||
<figcaption>{{ shot.label }}</figcaption>
|
<figcaption>{{ shot.label }}</figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
type OrderAgreements,
|
type OrderAgreements,
|
||||||
} from '@/features/orders/api/orders'
|
} from '@/features/orders/api/orders'
|
||||||
import { useSessionStore } from '@/stores/session'
|
import { useSessionStore } from '@/stores/session'
|
||||||
|
import AuthImage from '@/shared/components/business/AuthImage.vue'
|
||||||
import { formatMoney } from '@/shared/utils/money'
|
import { formatMoney } from '@/shared/utils/money'
|
||||||
import {
|
import {
|
||||||
assetRegions,
|
assetRegions,
|
||||||
@@ -282,12 +283,12 @@ function readError(error: unknown, fallback: string) {
|
|||||||
|
|
||||||
<!-- 封面图 -->
|
<!-- 封面图 -->
|
||||||
<div class="cover-area">
|
<div class="cover-area">
|
||||||
<img
|
<AuthImage
|
||||||
v-if="detailScreenshots[0]?.url"
|
v-if="detailScreenshots[0]?.url"
|
||||||
:src="detailScreenshots[0].url"
|
:source="detailScreenshots[0].url"
|
||||||
:alt="getListingTitle(listing)"
|
:alt="getListingTitle(listing)"
|
||||||
class="cover-img"
|
image-class="cover-img"
|
||||||
decoding="async"
|
loading="eager"
|
||||||
/>
|
/>
|
||||||
<div v-else class="cover-placeholder">
|
<div v-else class="cover-placeholder">
|
||||||
<van-icon name="photo-o" :size="40" color="#ccc" />
|
<van-icon name="photo-o" :size="40" color="#ccc" />
|
||||||
@@ -382,7 +383,7 @@ function readError(error: unknown, fallback: string) {
|
|||||||
<h3 class="card-subtitle">账号截图</h3>
|
<h3 class="card-subtitle">账号截图</h3>
|
||||||
<div class="screenshot-grid">
|
<div class="screenshot-grid">
|
||||||
<figure v-for="shot in detailScreenshots" :key="shot.url" class="screenshot-item">
|
<figure v-for="shot in detailScreenshots" :key="shot.url" class="screenshot-item">
|
||||||
<img :src="shot.url" class="screenshot-thumb" loading="lazy" decoding="async" />
|
<AuthImage :source="shot.url" :alt="shot.label" image-class="screenshot-thumb" />
|
||||||
<figcaption>{{ shot.label }}</figcaption>
|
<figcaption>{{ shot.label }}</figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onBeforeUnmount, ref, watch } from 'vue'
|
||||||
|
import { fetchAdminFileBlob, fetchFileBlobByURL } from '@/shared/api/files'
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
source: string
|
||||||
|
alt?: string
|
||||||
|
admin?: boolean
|
||||||
|
imageClass?: string
|
||||||
|
fallbackClass?: string
|
||||||
|
loading?: 'eager' | 'lazy'
|
||||||
|
decoding?: 'async' | 'sync' | 'auto'
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
alt: '',
|
||||||
|
admin: false,
|
||||||
|
imageClass: '',
|
||||||
|
fallbackClass: '',
|
||||||
|
loading: 'lazy',
|
||||||
|
decoding: 'async',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const imageURL = ref('')
|
||||||
|
const failed = ref(false)
|
||||||
|
let createdObjectURL = ''
|
||||||
|
|
||||||
|
const fallbackText = computed(() => (failed.value ? '图片加载失败' : '图片加载中'))
|
||||||
|
|
||||||
|
function extractObjectKey(value: string) {
|
||||||
|
try {
|
||||||
|
const parsed = new URL(value, window.location.origin)
|
||||||
|
return parsed.searchParams.get('key') || ''
|
||||||
|
} catch {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldFetchWithAuth(value: string) {
|
||||||
|
return value.includes('/api/files/object') || value.includes('/api/admin/files/object')
|
||||||
|
}
|
||||||
|
|
||||||
|
function revokeCurrentURL() {
|
||||||
|
if (!createdObjectURL) return
|
||||||
|
URL.revokeObjectURL(createdObjectURL)
|
||||||
|
createdObjectURL = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadImage() {
|
||||||
|
revokeCurrentURL()
|
||||||
|
imageURL.value = ''
|
||||||
|
failed.value = false
|
||||||
|
|
||||||
|
if (!props.source) {
|
||||||
|
failed.value = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!shouldFetchWithAuth(props.source)) {
|
||||||
|
imageURL.value = props.source
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const key = extractObjectKey(props.source)
|
||||||
|
const blob =
|
||||||
|
props.admin && key ? await fetchAdminFileBlob(key) : await fetchFileBlobByURL(props.source)
|
||||||
|
createdObjectURL = URL.createObjectURL(blob)
|
||||||
|
imageURL.value = createdObjectURL
|
||||||
|
} catch {
|
||||||
|
failed.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => [props.source, props.admin] as const, loadImage, { immediate: true })
|
||||||
|
|
||||||
|
onBeforeUnmount(revokeCurrentURL)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<img
|
||||||
|
v-if="imageURL"
|
||||||
|
:class="imageClass"
|
||||||
|
:src="imageURL"
|
||||||
|
:alt="alt"
|
||||||
|
:loading="loading"
|
||||||
|
:decoding="decoding"
|
||||||
|
/>
|
||||||
|
<span v-else :class="['auth-image-fallback', fallbackClass]">{{ fallbackText }}</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.auth-image-fallback {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 120px;
|
||||||
|
border: 1px dashed #d8dee8;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #f8fafc;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
BASE_URL="http://127.0.0.1:8080/api"
|
|
||||||
|
|
||||||
echo "=== 支付配置功能测试 ==="
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# 1. 获取验证码
|
|
||||||
echo "1. 获取验证码..."
|
|
||||||
CAPTCHA_RESP=$(curl -s "${BASE_URL}/admin/auth/captcha")
|
|
||||||
CAPTCHA_ID=$(echo $CAPTCHA_RESP | jq -r '.data.captcha_id')
|
|
||||||
echo " 验证码 ID: $CAPTCHA_ID"
|
|
||||||
|
|
||||||
# 2. 登录(开发环境验证码可以使用任意值)
|
|
||||||
echo ""
|
|
||||||
echo "2. 管理员登录..."
|
|
||||||
LOGIN_RESP=$(curl -s "${BASE_URL}/admin/auth/login" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "{\"username\":\"admin\",\"password\":\"admin123456\",\"captcha_id\":\"${CAPTCHA_ID}\",\"captcha\":\"RAZK\"}")
|
|
||||||
|
|
||||||
TOKEN=$(echo $LOGIN_RESP | jq -r '.data.token')
|
|
||||||
if [ "$TOKEN" = "null" ] || [ -z "$TOKEN" ]; then
|
|
||||||
echo " ❌ 登录失败"
|
|
||||||
echo $LOGIN_RESP | jq .
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo " ✅ 登录成功"
|
|
||||||
echo " Token: ${TOKEN:0:50}..."
|
|
||||||
|
|
||||||
# 3. 查看支付配置列表
|
|
||||||
echo ""
|
|
||||||
echo "3. 查看支付配置列表..."
|
|
||||||
LIST_RESP=$(curl -s "${BASE_URL}/admin/payment-configs" \
|
|
||||||
-H "Authorization: Bearer ${TOKEN}")
|
|
||||||
echo $LIST_RESP | jq .
|
|
||||||
TOTAL=$(echo $LIST_RESP | jq -r '.data.total // 0')
|
|
||||||
echo " 当前配置数量: $TOTAL"
|
|
||||||
|
|
||||||
# 4. 创建测试支付配置
|
|
||||||
echo ""
|
|
||||||
echo "4. 创建测试支付配置..."
|
|
||||||
CREATE_RESP=$(curl -s "${BASE_URL}/admin/payment-configs" \
|
|
||||||
-H "Authorization: Bearer ${TOKEN}" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{
|
|
||||||
"name": "测试乐刷商户",
|
|
||||||
"provider": "leshua",
|
|
||||||
"merchant_id": "TEST123456789",
|
|
||||||
"gateway_url": "https://t-paygate.lepass.cn/cgi-bin/lepos_pay_gateway.cgi",
|
|
||||||
"sign_key": "test-sign-key-secret",
|
|
||||||
"notify_key": "test-notify-key-secret",
|
|
||||||
"notify_url": "http://localhost:8080/api/payments/leshua/notify",
|
|
||||||
"jump_url": "http://localhost:5173/payment/result",
|
|
||||||
"pay_way": "ZFBZF",
|
|
||||||
"jspay_flag": "2",
|
|
||||||
"sign_type": "MD5",
|
|
||||||
"is_default": true,
|
|
||||||
"status": "active",
|
|
||||||
"environment": "sandbox"
|
|
||||||
}')
|
|
||||||
|
|
||||||
CONFIG_ID=$(echo $CREATE_RESP | jq -r '.data.id // empty')
|
|
||||||
if [ -z "$CONFIG_ID" ]; then
|
|
||||||
echo " ❌ 创建失败"
|
|
||||||
echo $CREATE_RESP | jq .
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo " ✅ 创建成功,配置 ID: $CONFIG_ID"
|
|
||||||
|
|
||||||
# 5. 查看单个配置(不包含密钥)
|
|
||||||
echo ""
|
|
||||||
echo "5. 查看配置详情(不包含密钥)..."
|
|
||||||
GET_RESP=$(curl -s "${BASE_URL}/admin/payment-configs/${CONFIG_ID}" \
|
|
||||||
-H "Authorization: Bearer ${TOKEN}")
|
|
||||||
echo $GET_RESP | jq .
|
|
||||||
SIGN_KEY=$(echo $GET_RESP | jq -r '.data.sign_key')
|
|
||||||
echo " 密钥是否加密: $([ \"$SIGN_KEY\" = \"******\" ] && echo '✅ 是' || echo '❌ 否')"
|
|
||||||
|
|
||||||
# 6. 查看配置(包含密钥明文)
|
|
||||||
echo ""
|
|
||||||
echo "6. 查看配置详情(包含密钥明文)..."
|
|
||||||
GET_SECRET_RESP=$(curl -s "${BASE_URL}/admin/payment-configs/${CONFIG_ID}?include_secret=true" \
|
|
||||||
-H "Authorization: Bearer ${TOKEN}")
|
|
||||||
SIGN_KEY_PLAIN=$(echo $GET_SECRET_RESP | jq -r '.data.sign_key')
|
|
||||||
echo " 解密后的 sign_key: $SIGN_KEY_PLAIN"
|
|
||||||
echo " 密钥是否正确: $([ \"$SIGN_KEY_PLAIN\" = \"test-sign-key-secret\" ] && echo '✅ 是' || echo '❌ 否')"
|
|
||||||
|
|
||||||
# 7. 更新配置
|
|
||||||
echo ""
|
|
||||||
echo "7. 更新配置..."
|
|
||||||
UPDATE_RESP=$(curl -s -X PUT "${BASE_URL}/admin/payment-configs/${CONFIG_ID}" \
|
|
||||||
-H "Authorization: Bearer ${TOKEN}" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{
|
|
||||||
"name": "测试乐刷商户(已更新)",
|
|
||||||
"status": "testing"
|
|
||||||
}')
|
|
||||||
echo $UPDATE_RESP | jq .
|
|
||||||
NEW_NAME=$(echo $UPDATE_RESP | jq -r '.data.name')
|
|
||||||
echo " 更新后名称: $NEW_NAME"
|
|
||||||
|
|
||||||
# 8. 查看最终配置列表
|
|
||||||
echo ""
|
|
||||||
echo "8. 查看最终配置列表..."
|
|
||||||
FINAL_LIST=$(curl -s "${BASE_URL}/admin/payment-configs" \
|
|
||||||
-H "Authorization: Bearer ${TOKEN}")
|
|
||||||
echo $FINAL_LIST | jq .
|
|
||||||
|
|
||||||
# 9. 删除测试配置
|
|
||||||
echo ""
|
|
||||||
echo "9. 删除测试配置..."
|
|
||||||
DELETE_RESP=$(curl -s -X DELETE "${BASE_URL}/admin/payment-configs/${CONFIG_ID}" \
|
|
||||||
-H "Authorization: Bearer ${TOKEN}")
|
|
||||||
echo $DELETE_RESP | jq .
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "=== ✅ 测试完成 ==="
|
|
||||||
Reference in New Issue
Block a user