优化发布截图上传与押金提示
This commit is contained in:
@@ -28,7 +28,7 @@ export function buildPublishDraft(options: {
|
||||
form: PublishForm
|
||||
quantityValues: Record<QuantityKey, number>
|
||||
quantityModes: Record<QuantityKey, ChargeMode>
|
||||
screenshotFiles: Record<ScreenshotKey, string>
|
||||
screenshotFiles: Record<ScreenshotKey, string[]>
|
||||
selectedSkins: string[]
|
||||
}): PublishDraft {
|
||||
return {
|
||||
@@ -38,7 +38,7 @@ export function buildPublishDraft(options: {
|
||||
},
|
||||
quantityValues: { ...options.quantityValues },
|
||||
quantityModes: { ...options.quantityModes },
|
||||
screenshotFiles: { ...options.screenshotFiles },
|
||||
screenshotFiles: cloneScreenshotFiles(options.screenshotFiles),
|
||||
selectedSkins: [...options.selectedSkins],
|
||||
}
|
||||
}
|
||||
@@ -52,7 +52,7 @@ export function readPublishDraft(draftKey: string) {
|
||||
form: normalizeDraftForm(draft.form),
|
||||
quantityValues: normalizeNumberRecord(draft.quantityValues),
|
||||
quantityModes: normalizeQuantityModes(draft.quantityModes),
|
||||
screenshotFiles: normalizeStringRecord(draft.screenshotFiles),
|
||||
screenshotFiles: normalizeScreenshotFiles(draft.screenshotFiles),
|
||||
selectedSkins: Array.isArray(draft.selectedSkins)
|
||||
? draft.selectedSkins.filter((skin): skin is string => typeof skin === 'string')
|
||||
: [],
|
||||
@@ -105,15 +105,25 @@ function normalizeNumberRecord(value: unknown) {
|
||||
return record
|
||||
}
|
||||
|
||||
function normalizeStringRecord(value: unknown) {
|
||||
const record: Record<string, string> = {}
|
||||
function normalizeScreenshotFiles(value: unknown) {
|
||||
const record: Record<string, string[]> = {}
|
||||
if (!isRecord(value)) return record
|
||||
for (const [key, item] of Object.entries(value)) {
|
||||
if (typeof item === 'string') record[key] = item
|
||||
if (typeof item === 'string') {
|
||||
record[key] = item ? [item] : []
|
||||
continue
|
||||
}
|
||||
if (Array.isArray(item)) {
|
||||
record[key] = item.filter((url): url is string => typeof url === 'string' && Boolean(url))
|
||||
}
|
||||
}
|
||||
return record
|
||||
}
|
||||
|
||||
function cloneScreenshotFiles(value: Record<string, string[]>) {
|
||||
return Object.fromEntries(Object.entries(value).map(([key, urls]) => [key, [...urls]]))
|
||||
}
|
||||
|
||||
function normalizeQuantityModes(value: unknown) {
|
||||
const record: Record<string, ChargeMode> = {}
|
||||
if (!isRecord(value)) return record
|
||||
|
||||
@@ -29,6 +29,8 @@ import type { PublishForm } from '@/types/publish'
|
||||
import { commonOnlineTimes, dailyLossOptions, formatNumber, roundRatio } from '@/utils/pricing'
|
||||
|
||||
const draftSaveDelay = 400
|
||||
const multiScreenshotLimit = 3
|
||||
const multiScreenshotKeys = new Set(['tencentSecurity', 'skin'])
|
||||
|
||||
interface UsePublishFormOptions {
|
||||
draftKey: string
|
||||
@@ -56,8 +58,8 @@ export function usePublishForm(options: UsePublishFormOptions) {
|
||||
const form = reactive<PublishForm>(defaultPublishForm())
|
||||
const quantityValues = reactive<Record<string, number>>({})
|
||||
const quantityModes = reactive<Record<string, ChargeMode>>({})
|
||||
const screenshotFiles = reactive<Record<string, string>>({})
|
||||
const screenshotPreviews = reactive<Record<string, string>>({})
|
||||
const screenshotFiles = reactive<Record<string, string[]>>({})
|
||||
const screenshotPreviews = reactive<Record<string, string[]>>({})
|
||||
const selectedSkins = ref<string[]>([])
|
||||
let draftSaveTimer: number | undefined
|
||||
|
||||
@@ -70,7 +72,9 @@ export function usePublishForm(options: UsePublishFormOptions) {
|
||||
screenshotFiles,
|
||||
selectedSkins,
|
||||
})
|
||||
const uploadedScreenshotCount = computed(() => pricing.screenshotUrls.value.length)
|
||||
const uploadedScreenshotCount = computed(
|
||||
() => pricing.screenshotSlots.value.filter((item) => getScreenshotCount(item.key) > 0).length,
|
||||
)
|
||||
const requiredScreenshotCount = ref(0)
|
||||
const canPublishAfterAgreements = computed(
|
||||
() => virtualAssetSaleAgreementChecked.value && sellerAgreementChecked.value,
|
||||
@@ -263,24 +267,43 @@ export function usePublishForm(options: UsePublishFormOptions) {
|
||||
}
|
||||
|
||||
function triggerUpload(key: ScreenshotKey) {
|
||||
if (!canAddScreenshot(key)) {
|
||||
options.notifyWarning(`${getScreenshotLabel(key)}最多上传${getScreenshotLimit(key)}张`)
|
||||
return
|
||||
}
|
||||
activeUploadKey.value = key
|
||||
fileInput.value?.click()
|
||||
}
|
||||
|
||||
async function handleScreenshotUpload(event: Event) {
|
||||
const input = event.target as HTMLInputElement
|
||||
const file = input.files?.[0]
|
||||
if (!file) return
|
||||
const files = Array.from(input.files || [])
|
||||
if (!files.length) return
|
||||
const key = activeUploadKey.value
|
||||
const previewURL = URL.createObjectURL(file)
|
||||
setScreenshotPreview(key, previewURL)
|
||||
const availableCount = getScreenshotLimit(key) - getScreenshotCount(key)
|
||||
if (availableCount <= 0) {
|
||||
options.notifyWarning(`${getScreenshotLabel(key)}最多上传${getScreenshotLimit(key)}张`)
|
||||
input.value = ''
|
||||
return
|
||||
}
|
||||
const uploadFiles = files.slice(0, availableCount)
|
||||
if (files.length > availableCount) {
|
||||
options.notifyWarning(`${getScreenshotLabel(key)}最多上传${getScreenshotLimit(key)}张,本次仅上传${availableCount}张`)
|
||||
}
|
||||
uploading.value = true
|
||||
try {
|
||||
const uploaded = await uploadFile(file, 'listing')
|
||||
screenshotFiles[key] = uploaded.url
|
||||
options.notifySuccess('截图已上传')
|
||||
for (const file of uploadFiles) {
|
||||
const previewURL = URL.createObjectURL(file)
|
||||
try {
|
||||
const uploaded = await uploadFile(file, 'listing')
|
||||
appendScreenshot(key, uploaded.url, previewURL)
|
||||
} catch (error) {
|
||||
URL.revokeObjectURL(previewURL)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
options.notifySuccess(uploadFiles.length > 1 ? `已上传${uploadFiles.length}张截图` : '截图已上传')
|
||||
} catch (error) {
|
||||
revokeScreenshotPreview(key)
|
||||
options.notifyError(readError(error, '截图上传失败'))
|
||||
} finally {
|
||||
uploading.value = false
|
||||
@@ -288,38 +311,55 @@ export function usePublishForm(options: UsePublishFormOptions) {
|
||||
}
|
||||
}
|
||||
|
||||
function removeScreenshot(key: ScreenshotKey) {
|
||||
screenshotFiles[key] = ''
|
||||
revokeScreenshotPreview(key)
|
||||
function removeScreenshot(key: ScreenshotKey, index = 0) {
|
||||
revokeScreenshotPreview(key, index)
|
||||
screenshotFiles[key]?.splice(index, 1)
|
||||
if (!screenshotFiles[key]?.length) delete screenshotFiles[key]
|
||||
}
|
||||
|
||||
function getScreenshotPreviewURL(key: ScreenshotKey) {
|
||||
return screenshotPreviews[key] || screenshotFiles[key] || ''
|
||||
function getScreenshotPreviewURL(key: ScreenshotKey, index = 0) {
|
||||
return screenshotPreviews[key]?.[index] || screenshotFiles[key]?.[index] || ''
|
||||
}
|
||||
|
||||
function setScreenshotPreview(key: ScreenshotKey, previewURL: string) {
|
||||
revokeScreenshotPreview(key)
|
||||
screenshotPreviews[key] = previewURL
|
||||
function appendScreenshot(key: ScreenshotKey, fileURL: string, previewURL: string) {
|
||||
if (!screenshotFiles[key]) screenshotFiles[key] = []
|
||||
if (!screenshotPreviews[key]) screenshotPreviews[key] = []
|
||||
screenshotFiles[key].push(fileURL)
|
||||
screenshotPreviews[key].push(previewURL)
|
||||
}
|
||||
|
||||
function revokeScreenshotPreview(key: ScreenshotKey) {
|
||||
const previewURL = screenshotPreviews[key]
|
||||
function setScreenshotPreview(key: ScreenshotKey, index: number, previewURL: string) {
|
||||
revokeScreenshotPreview(key, index)
|
||||
if (!screenshotPreviews[key]) screenshotPreviews[key] = []
|
||||
screenshotPreviews[key][index] = previewURL
|
||||
}
|
||||
|
||||
function revokeScreenshotPreview(key: ScreenshotKey, index: number) {
|
||||
const previewURL = screenshotPreviews[key]?.[index]
|
||||
if (previewURL?.startsWith('blob:')) URL.revokeObjectURL(previewURL)
|
||||
delete screenshotPreviews[key]
|
||||
screenshotPreviews[key]?.splice(index, 1)
|
||||
if (!screenshotPreviews[key]?.length) delete screenshotPreviews[key]
|
||||
}
|
||||
|
||||
function revokeAllScreenshotPreviews() {
|
||||
for (const key of Object.keys(screenshotPreviews)) revokeScreenshotPreview(key)
|
||||
for (const [key, previews] of Object.entries(screenshotPreviews)) {
|
||||
for (const previewURL of previews) {
|
||||
if (previewURL?.startsWith('blob:')) URL.revokeObjectURL(previewURL)
|
||||
}
|
||||
delete screenshotPreviews[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。
|
||||
for (const [key, fileURLs] of Object.entries(screenshotFiles)) {
|
||||
for (const [index, fileURL] of fileURLs.entries()) {
|
||||
if (!fileURL || screenshotPreviews[key]?.[index] || !fileURL.startsWith('/api/files/object')) continue
|
||||
try {
|
||||
const blob = await fetchFileBlobByURL(fileURL)
|
||||
setScreenshotPreview(key, index, URL.createObjectURL(blob))
|
||||
} catch {
|
||||
// 草稿预览失败不影响已上传文件地址,提交时仍会带上原 URL。
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -463,7 +503,7 @@ export function usePublishForm(options: UsePublishFormOptions) {
|
||||
if (!Number.isFinite(pricing.calculatedFinalPrice.value)) return '发布价格计算异常,请检查填写内容'
|
||||
if (!canPublishAfterAgreements.value) return '请先阅读并勾选两份发布协议'
|
||||
for (const item of pricing.screenshotSlots.value) {
|
||||
if (isScreenshotRequired(item) && !screenshotFiles[item.key]) return `请上传${item.label}`
|
||||
if (isScreenshotRequired(item) && getScreenshotCount(item.key) === 0) return `请上传${item.label}`
|
||||
}
|
||||
for (const item of pricing.quantityItems.value) {
|
||||
if (!pricing.isQuantityItemDisabled(item)) {
|
||||
@@ -529,6 +569,7 @@ export function usePublishForm(options: UsePublishFormOptions) {
|
||||
groups[group.key] = group.options.filter((skin) => selectedSkins.value.includes(skin))
|
||||
return groups
|
||||
}, {}),
|
||||
screenshot_groups: buildScreenshotGroups(),
|
||||
online_time: {
|
||||
start: form.online_start,
|
||||
end: form.online_end,
|
||||
@@ -539,6 +580,43 @@ export function usePublishForm(options: UsePublishFormOptions) {
|
||||
}
|
||||
}
|
||||
|
||||
function buildScreenshotGroups() {
|
||||
return pricing.screenshotSlots.value.reduce<Record<string, string[]>>((groups, slot) => {
|
||||
groups[slot.key] = getScreenshotURLs(slot.key)
|
||||
return groups
|
||||
}, {})
|
||||
}
|
||||
|
||||
function getScreenshotURLs(key: ScreenshotKey) {
|
||||
return (screenshotFiles[key] || []).filter(Boolean)
|
||||
}
|
||||
|
||||
function getScreenshotCount(key: ScreenshotKey) {
|
||||
return getScreenshotURLs(key).length
|
||||
}
|
||||
|
||||
function getScreenshotLimit(key: ScreenshotKey) {
|
||||
return multiScreenshotKeys.has(key) ? multiScreenshotLimit : 1
|
||||
}
|
||||
|
||||
function isMultiScreenshotSlot(key: ScreenshotKey) {
|
||||
return getScreenshotLimit(key) > 1
|
||||
}
|
||||
|
||||
function canAddScreenshot(key: ScreenshotKey) {
|
||||
return !uploading.value && getScreenshotCount(key) < getScreenshotLimit(key)
|
||||
}
|
||||
|
||||
function getScreenshotLimitHint(key: ScreenshotKey) {
|
||||
const limit = getScreenshotLimit(key)
|
||||
if (limit <= 1) return ''
|
||||
return `最多${limit}张,已上传${getScreenshotCount(key)}张`
|
||||
}
|
||||
|
||||
function getScreenshotLabel(key: ScreenshotKey) {
|
||||
return pricing.screenshotSlots.value.find((item) => item.key === key)?.label || '该截图'
|
||||
}
|
||||
|
||||
return {
|
||||
...pricing,
|
||||
dailyLossOptions,
|
||||
@@ -571,7 +649,13 @@ export function usePublishForm(options: UsePublishFormOptions) {
|
||||
triggerUpload,
|
||||
handleScreenshotUpload,
|
||||
removeScreenshot,
|
||||
getScreenshotURLs,
|
||||
getScreenshotCount,
|
||||
getScreenshotPreviewURL,
|
||||
getScreenshotLimit,
|
||||
isMultiScreenshotSlot,
|
||||
canAddScreenshot,
|
||||
getScreenshotLimitHint,
|
||||
handleFireLevelInput,
|
||||
handleAcceleratedSaleRatioInput,
|
||||
syncRecommendedDeposit,
|
||||
|
||||
Reference in New Issue
Block a user