优化发布图片问题
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"mime/multipart"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -48,13 +49,13 @@ func (s *Service) Upload(req uploadRequest) (*UploadDTO, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
url := "/api/files/object?key=" + key
|
||||
fileURL := "/api/files/object?key=" + url.QueryEscape(key)
|
||||
if scene == "home-banner" {
|
||||
url = "/api/public/files/object?key=" + key
|
||||
fileURL = "/api/public/files/object?key=" + url.QueryEscape(key)
|
||||
}
|
||||
return &UploadDTO{
|
||||
ObjectKey: key,
|
||||
URL: url,
|
||||
URL: fileURL,
|
||||
Filename: req.Header.Filename,
|
||||
ContentType: contentType,
|
||||
Size: req.Header.Size,
|
||||
|
||||
@@ -36,15 +36,19 @@ func DefaultPublishOptions() PublishOptionsDTO {
|
||||
},
|
||||
},
|
||||
{
|
||||
Key: "operator",
|
||||
Title: "干员皮肤",
|
||||
Key: "operatorGold",
|
||||
Title: "干员金皮",
|
||||
Options: []string{
|
||||
"威龙-凌霄戍卫", "红狼-蚀金玫瑰", "露娜-黑天际线", "蛊-能天使午夜邮差",
|
||||
"骇爪-维什戴尔", "骇爪-水墨云图", "威龙-吴彦祖", "威龙-铁面判官",
|
||||
"威龙-壮志凌云", "威龙-蛟龙特战队", "蜂医-送葬人", "蜂医-危险物质",
|
||||
"蜂医-黑鹰坠落", "牧羊人-街头之星", "牧羊人-黑鹰坠落", "乌鲁鲁-黑鹰坠落",
|
||||
"红狼-黑鹰坠落", "无名-夜鹰", "深蓝-不破誓约", "红狼-电锯惊魂",
|
||||
"露娜-古墓丽影劳拉", "赛伊德",
|
||||
"露娜-古墓丽影联动", "蛊-不羁人生", "红狼-电锯惊魂", "露娜-金牌射手",
|
||||
"牧羊人-街头之星", "蜂医-危险物质", "蜂医-送葬人", "无名-夜鹰",
|
||||
"威龙-壮志凌云", "威龙-蛟龙特战队", "威龙-铁面判官", "威龙-吴彦祖",
|
||||
},
|
||||
},
|
||||
{
|
||||
Key: "operatorRed",
|
||||
Title: "干员红皮",
|
||||
Options: []string{
|
||||
"凌霄成卫", "蚀金玫瑰", "水墨云图", "午夜邮差", "天际线", "维什戴尔",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -84,7 +84,8 @@ VALUES
|
||||
),
|
||||
'skin_groups', JSON_OBJECT(
|
||||
'melee', JSON_ARRAY('龙牙', '信条'),
|
||||
'operator', JSON_ARRAY('威龙-凌霄戍卫', '红狼-蚀金玫瑰'),
|
||||
'operatorGold', JSON_ARRAY(),
|
||||
'operatorRed', JSON_ARRAY('凌霄成卫', '蚀金玫瑰'),
|
||||
'weapon', JSON_ARRAY('M4A1棱镜攻势', 'K416命运')
|
||||
),
|
||||
'remark', '部分仓库材料请勿使用,下单后会再次提醒。'
|
||||
@@ -120,7 +121,8 @@ VALUES
|
||||
),
|
||||
'skin_groups', JSON_OBJECT(
|
||||
'melee', JSON_ARRAY('怜悯'),
|
||||
'operator', JSON_ARRAY('露娜-黑天际线'),
|
||||
'operatorGold', JSON_ARRAY(),
|
||||
'operatorRed', JSON_ARRAY('天际线'),
|
||||
'weapon', JSON_ARRAY('SCAR-H电玩高手')
|
||||
),
|
||||
'remark', ''
|
||||
@@ -156,7 +158,8 @@ VALUES
|
||||
),
|
||||
'skin_groups', JSON_OBJECT(
|
||||
'melee', JSON_ARRAY('暗星'),
|
||||
'operator', JSON_ARRAY('赛伊德'),
|
||||
'operatorGold', JSON_ARRAY(),
|
||||
'operatorRed', JSON_ARRAY(),
|
||||
'weapon', JSON_ARRAY('Vector美杜莎')
|
||||
),
|
||||
'remark', ''
|
||||
@@ -193,7 +196,8 @@ VALUES
|
||||
),
|
||||
'skin_groups', JSON_OBJECT(
|
||||
'melee', JSON_ARRAY('坠星者', '处刑者'),
|
||||
'operator', JSON_ARRAY('威龙-铁面判官', '红狼-蚀金玫瑰'),
|
||||
'operatorGold', JSON_ARRAY('威龙-铁面判官'),
|
||||
'operatorRed', JSON_ARRAY('蚀金玫瑰'),
|
||||
'weapon', JSON_ARRAY('M7棱镜攻势S2', 'KC17-造物纪元')
|
||||
),
|
||||
'remark', '高价值账号,需按平台流程交接。'
|
||||
@@ -229,7 +233,8 @@ VALUES
|
||||
),
|
||||
'skin_groups', JSON_OBJECT(
|
||||
'melee', JSON_ARRAY('影锋'),
|
||||
'operator', JSON_ARRAY('骇爪-水墨云图'),
|
||||
'operatorGold', JSON_ARRAY(),
|
||||
'operatorRed', JSON_ARRAY('水墨云图'),
|
||||
'weapon', JSON_ARRAY('QBZ95王牌之剑', 'AUG气象感应')
|
||||
),
|
||||
'remark', 'AWM 子弹赠送,其他消耗品按规则扣费。'
|
||||
@@ -264,7 +269,8 @@ VALUES
|
||||
),
|
||||
'skin_groups', JSON_OBJECT(
|
||||
'melee', JSON_ARRAY('北极星'),
|
||||
'operator', JSON_ARRAY('蜂医-送葬人'),
|
||||
'operatorGold', JSON_ARRAY('蜂医-送葬人'),
|
||||
'operatorRed', JSON_ARRAY(),
|
||||
'weapon', JSON_ARRAY('MP7电玩高手S2')
|
||||
),
|
||||
'remark', ''
|
||||
@@ -300,7 +306,8 @@ VALUES
|
||||
),
|
||||
'skin_groups', JSON_OBJECT(
|
||||
'melee', JSON_ARRAY('赤枭', '黑海'),
|
||||
'operator', JSON_ARRAY('威龙-壮志凌云', '蜂医-危险物质'),
|
||||
'operatorGold', JSON_ARRAY('威龙-壮志凌云', '蜂医-危险物质'),
|
||||
'operatorRed', JSON_ARRAY(),
|
||||
'weapon', JSON_ARRAY('AS Val悬赏令', 'K416命运', 'AUG气象感应')
|
||||
),
|
||||
'remark', ''
|
||||
@@ -337,7 +344,8 @@ VALUES
|
||||
),
|
||||
'skin_groups', JSON_OBJECT(
|
||||
'melee', JSON_ARRAY('电锯惊魂', '处刑者'),
|
||||
'operator', JSON_ARRAY('露娜-古墓丽影劳拉', '红狼-电锯惊魂'),
|
||||
'operatorGold', JSON_ARRAY('露娜-古墓丽影联动', '红狼-电锯惊魂'),
|
||||
'operatorRed', JSON_ARRAY(),
|
||||
'weapon', JSON_ARRAY('M250电玩高手S2', '腾龙气象感应')
|
||||
),
|
||||
'remark', '六级弹赠送数量以下单截图为准。'
|
||||
|
||||
@@ -41,3 +41,11 @@ export async function fetchAdminFileBlob(key: string) {
|
||||
})
|
||||
return data
|
||||
}
|
||||
|
||||
export async function fetchFileBlobByURL(fileURL: string) {
|
||||
const apiPath = fileURL.startsWith('/api/') ? fileURL.slice(4) : fileURL
|
||||
const { data } = await apiClient.get<Blob>(apiPath, {
|
||||
responseType: 'blob',
|
||||
})
|
||||
return data
|
||||
}
|
||||
|
||||
@@ -81,6 +81,8 @@ const detailSkinGroups = computed(() => {
|
||||
const titles: Record<string, string> = {
|
||||
melee: "近战皮肤",
|
||||
operator: "干员皮肤",
|
||||
operatorGold: "干员金皮",
|
||||
operatorRed: "干员红皮",
|
||||
weapon: "武器皮肤",
|
||||
};
|
||||
return Object.entries(groups as Record<string, unknown>)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref, watch } from "vue";
|
||||
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from "vue";
|
||||
import { useRouter, useRoute } from "vue-router";
|
||||
import { showDialog, showToast } from "vant";
|
||||
|
||||
import { uploadFile } from "@/api/files";
|
||||
import { fetchFileBlobByURL, uploadFile } from "@/api/files";
|
||||
import {
|
||||
emptyListingPublishOptions,
|
||||
fetchListingPublishOptions,
|
||||
@@ -94,6 +94,7 @@ const quantityValues = reactive<Record<QuantityKey, number>>(defaultQuantityValu
|
||||
const quantityModes = reactive<Record<QuantityKey, ChargeMode>>(defaultQuantityModes());
|
||||
|
||||
const screenshotFiles = reactive<Record<ScreenshotKey, string>>(defaultScreenshotFiles());
|
||||
const screenshotPreviews = reactive<Record<ScreenshotKey, string>>({});
|
||||
|
||||
const selectedSkins = ref<string[]>([]);
|
||||
const fileInput = ref<HTMLInputElement | null>(null);
|
||||
@@ -118,8 +119,11 @@ const screenshotUrls = computed(() =>
|
||||
);
|
||||
const coinWanAmount = computed(() => Number(form.haf_coin_amount || 0));
|
||||
const calculatedRatio = computed(() => calculatePublishRatio());
|
||||
const calculatedConsumablePrice = computed(() => calculateConsumablePrice());
|
||||
const calculatedFinalPrice = computed(() =>
|
||||
calculatedRatio.value > 0 ? roundMoney(coinWanAmount.value / calculatedRatio.value) : 0
|
||||
calculatedRatio.value > 0
|
||||
? roundMoney(coinWanAmount.value / calculatedRatio.value + calculatedConsumablePrice.value)
|
||||
: 0
|
||||
);
|
||||
const calculatedRentDays = computed(() =>
|
||||
coinWanAmount.value > 0 ? calculateRentDays(coinWanAmount.value) : 0
|
||||
@@ -130,6 +134,10 @@ onMounted(() => {
|
||||
loadPublishOptions();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
revokeAllScreenshotPreviews();
|
||||
});
|
||||
|
||||
watch(
|
||||
[form, quantityValues, quantityModes, screenshotFiles, selectedSkins],
|
||||
() => {
|
||||
@@ -176,6 +184,7 @@ function restoreDraft() {
|
||||
selectedSkins.value = Array.isArray(draft.selectedSkins)
|
||||
? draft.selectedSkins.filter((skin): skin is string => typeof skin === "string")
|
||||
: [];
|
||||
hydrateScreenshotPreviews();
|
||||
} catch {
|
||||
localStorage.removeItem(draftKey);
|
||||
}
|
||||
@@ -207,6 +216,7 @@ function resetDraftState() {
|
||||
Object.assign(quantityValues, defaultQuantityValues());
|
||||
Object.assign(quantityModes, defaultQuantityModes());
|
||||
Object.assign(screenshotFiles, defaultScreenshotFiles());
|
||||
revokeAllScreenshotPreviews();
|
||||
selectedSkins.value = [];
|
||||
activeUploadKey.value = "coin";
|
||||
}
|
||||
@@ -257,12 +267,16 @@ async function handleScreenshotUpload(event: Event) {
|
||||
const input = event.target as HTMLInputElement;
|
||||
const file = input.files?.[0];
|
||||
if (!file) return;
|
||||
const key = activeUploadKey.value;
|
||||
const previewURL = URL.createObjectURL(file);
|
||||
setScreenshotPreview(key, previewURL);
|
||||
uploading.value = true;
|
||||
try {
|
||||
const uploaded = await uploadFile(file, "listing");
|
||||
screenshotFiles[activeUploadKey.value] = uploaded.url;
|
||||
screenshotFiles[key] = uploaded.url;
|
||||
showToast({ message: "截图已上传", icon: "passed" });
|
||||
} catch (error) {
|
||||
revokeScreenshotPreview(key);
|
||||
showToast({ message: readError(error, "截图上传失败"), icon: "cross" });
|
||||
} finally {
|
||||
uploading.value = false;
|
||||
@@ -272,6 +286,44 @@ async function handleScreenshotUpload(event: Event) {
|
||||
|
||||
function removeScreenshot(key: ScreenshotKey) {
|
||||
screenshotFiles[key] = "";
|
||||
revokeScreenshotPreview(key);
|
||||
}
|
||||
|
||||
function getScreenshotPreviewURL(key: ScreenshotKey) {
|
||||
return screenshotPreviews[key] || screenshotFiles[key] || "";
|
||||
}
|
||||
|
||||
function setScreenshotPreview(key: ScreenshotKey, previewURL: string) {
|
||||
revokeScreenshotPreview(key);
|
||||
screenshotPreviews[key] = previewURL;
|
||||
}
|
||||
|
||||
function revokeScreenshotPreview(key: ScreenshotKey) {
|
||||
const previewURL = screenshotPreviews[key];
|
||||
if (previewURL?.startsWith("blob:")) {
|
||||
URL.revokeObjectURL(previewURL);
|
||||
}
|
||||
delete screenshotPreviews[key];
|
||||
}
|
||||
|
||||
function revokeAllScreenshotPreviews() {
|
||||
for (const key of Object.keys(screenshotPreviews)) {
|
||||
revokeScreenshotPreview(key);
|
||||
}
|
||||
}
|
||||
|
||||
async function hydrateScreenshotPreviews() {
|
||||
for (const [key, fileURL] of Object.entries(screenshotFiles)) {
|
||||
if (!fileURL || screenshotPreviews[key] || !fileURL.startsWith("/api/files/object")) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const blob = await fetchFileBlobByURL(fileURL);
|
||||
setScreenshotPreview(key, URL.createObjectURL(blob));
|
||||
} catch {
|
||||
// 草稿预览失败不影响已上传文件地址,提交时仍会带上原 URL。
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleFireLevelInput(value: string | number) {
|
||||
@@ -386,6 +438,28 @@ function roundMoney(value: number) {
|
||||
return Math.round(value * 100) / 100;
|
||||
}
|
||||
|
||||
function calculateConsumablePrice() {
|
||||
const total = quantityItems.value.reduce((sum, item) => {
|
||||
const quantity = Number(quantityValues[item.key] || 0);
|
||||
const mode = quantityModes[item.key] || "收费";
|
||||
if (quantity <= 0 || mode !== "收费") return sum;
|
||||
return sum + quantity * readUnitPrice(item.price);
|
||||
}, 0);
|
||||
return roundMoney(total);
|
||||
}
|
||||
|
||||
function readUnitPrice(priceText: string) {
|
||||
const normalized = priceText.replace(/,/g, ",").trim();
|
||||
const fractionMatch = normalized.match(/(\d+(?:\.\d+)?)\s*元\s*\/\s*(\d+(?:\.\d+)?)/);
|
||||
if (fractionMatch) {
|
||||
const amount = Number(fractionMatch[1]);
|
||||
const count = Number(fractionMatch[2]);
|
||||
return count > 0 ? amount / count : 0;
|
||||
}
|
||||
const singleMatch = normalized.match(/(\d+(?:\.\d+)?)\s*元/);
|
||||
return singleMatch ? Number(singleMatch[1]) : 0;
|
||||
}
|
||||
|
||||
function calculatePublishRatio() {
|
||||
if (
|
||||
coinWanAmount.value <= 0 ||
|
||||
@@ -420,7 +494,7 @@ function getInsuranceSlots(insurance: string) {
|
||||
|
||||
function getConfigHitCount() {
|
||||
const checks = [
|
||||
hasSelectedSkinGroup("operator"),
|
||||
hasSelectedSkinGroup("operatorRed"),
|
||||
hasSelectedSkinGroup("melee"),
|
||||
isMaxLevel(form.stamina_level),
|
||||
isMaxLevel(form.load_level),
|
||||
@@ -756,7 +830,7 @@ function readError(error: unknown, fallback: string) {
|
||||
<small>{{ slot.hint }}</small>
|
||||
</div>
|
||||
<div v-if="screenshotFiles[slot.key]" class="upload-preview">
|
||||
<img :src="screenshotFiles[slot.key]" :alt="slot.label" />
|
||||
<img :src="getScreenshotPreviewURL(slot.key)" :alt="slot.label" />
|
||||
<button type="button" @click="removeScreenshot(slot.key)">移除</button>
|
||||
</div>
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user