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