import { get } from '@/core/http/http-client' import { HttpError } from '@/core/http/http-error' import { parseApiResponse, snapshotEnvelope } from '@/core/http/response-evaluator' import { buildRestPath } from '@/core/config/api-paths' import appSettings from '@/core/config/app-settings' import { pullHomeCategories } from '@/features/travel/gateway' const uploadConfigPath = () => buildRestPath('common/uploadData') const publishPath = () => buildRestPath('find/find/addData') const videoUploadPath = () => '/common/localuploadvideo' function resolveUploadUrl(path) { if (!path) return '' if (/^https?:\/\//.test(path)) return path if (path.startsWith('//')) return `https:${path}` const base = (appSettings.appurl || '').replace(/\/$/, '') return `${base}${path.startsWith('/') ? path : `/${path}`}` } function unwrapUploadConfig(payload) { const body = payload && typeof payload === 'object' ? payload : {} if (body.uploadurl) return body if (body.data && typeof body.data === 'object') return body.data return body } function parseUploadResponse(raw) { if (!raw) return '' if (typeof raw === 'string') { try { const parsed = JSON.parse(raw) if (parsed.data && parsed.data.fullurl) return parsed.data.fullurl if (parsed.data && parsed.data.url) return parsed.data.url if (parsed.fullurl) return parsed.fullurl if (parsed.url) return parsed.url } catch (_) { return '' } } if (typeof raw === 'object') { if (raw.data && raw.data.fullurl) return raw.data.fullurl if (raw.data && raw.data.url) return raw.data.url if (raw.fullurl) return raw.fullurl if (raw.url) return raw.url } return '' } /** * 上传前解码并压缩图片,降低 App 端大图 / HEIC 导致的内存闪退。 * * @param {string} src * @returns {Promise} 可用于 uploadFile 的本地路径 */ export function prepareImageForUpload(src) { return new Promise((resolve) => { const fallback = () => resolve(src || '') if (!src) { fallback() return } const compress = (path, width = 0, height = 0) => { if (typeof uni.compressImage !== 'function') { resolve(path) return } const opts = { src: path, quality: 60, success: (res) => resolve((res && res.tempFilePath) || path), fail: () => resolve(path) } const w = Number(width) || 0 const h = Number(height) || 0 if (w > 1280 || h > 1280) { if (w >= h) opts.compressedWidth = 1280 else opts.compressedHeight = 1280 } try { uni.compressImage(opts) } catch (_) { resolve(path) } } uni.getImageInfo({ src, success: (info) => { const path = (info && info.path) || src compress(path, info && info.width, info && info.height) }, fail: () => { // 解码失败时仍尝试压缩原路径(部分机型 getImageInfo 对 HEIC 不稳定) compress(src) } }) }) } /** * 获取图片上传配置(wanlshop/common/uploadData) */ export async function fetchUploadConfig() { const { data, raw } = await get(uploadConfigPath()) const cfg = unwrapUploadConfig(data) if (cfg.uploadurl) return cfg return unwrapUploadConfig(raw?.res?.data ?? raw?.data) } /** * 上传单张图片,返回相对路径 url */ export function uploadImageFile(filePath, config) { return new Promise((resolve, reject) => { const uploadurl = resolveUploadUrl(config && config.uploadurl) if (!uploadurl) { reject(new Error('上传地址无效')) return } if (!filePath) { reject(new Error('图片文件无效')) return } const opts = { url: uploadurl, filePath, name: 'file', success: (res) => { const url = parseUploadResponse(res.data) if (url) resolve(url) else reject(new Error('上传失败')) }, fail: (err) => reject(err || new Error('上传失败')) } // local 不传 formData;非 local 才带 multipart(与参考工程一致) if (config && config.storage !== 'local' && config.multipart) { opts.formData = config.multipart } uni.uploadFile(opts) }) } function postFindData(body) { return new Promise((resolve, reject) => { let settled = false const finish = (fn, value) => { if (settled) return settled = true fn(value) } uni.request({ url: publishPath(), method: 'POST', data: body, success(res) { try { snapshotEnvelope(res) const parsed = parseApiResponse(res) if (parsed.kind === 'success' || parsed.kind === 'passthrough') { finish(resolve, parsed.data) return } finish(reject, HttpError.fromParsed(parsed)) } catch (err) { finish(reject, err) } }, fail(err) { finish(reject, HttpError.fromNetwork(err)) }, complete() { setTimeout(() => { if (!settled) { finish(reject, new HttpError('BUSINESS', '发布失败')) } }, 0) } }) }) } function normalizeRelativeVideoPath(path) { if (!path) return '' const raw = String(path).replace(/\\\//g, '/') if (/^https?:\/\//i.test(raw)) { const match = raw.match(/^https?:\/\/[^/]+(\/.*)$/i) return match ? match[1] : raw } return raw.startsWith('/') ? raw : `/${raw}` } function resolveVideoPreviewUrl(data) { if (!data || typeof data !== 'object') return '' const full = data.fullurl || data.full_url || '' if (full) return String(full).replace(/\\\//g, '/') const rel = normalizeRelativeVideoPath(data.url || '') if (!rel) return '' const base = (appSettings.cdnurl || appSettings.appurl || '') .replace(/\/api\/?$/i, '') .replace(/\/$/, '') return `${base}${rel}` } function parseVideoUploadEnvelope(raw) { if (!raw) return null let envelope = raw if (typeof raw === 'string') { try { envelope = JSON.parse(raw) } catch (_) { return null } } const body = envelope && envelope.data if (body && body.video_id != null && (envelope.code === 1 || envelope.code === '1')) { return { video_id: body.video_id, url: normalizeRelativeVideoPath(body.url || ''), previewUrl: resolveVideoPreviewUrl(body) } } return null } /** * 视频标签列表(/appvideolabel/list) */ export async function fetchVideoLabels() { try { return await pullHomeCategories() } catch (_) { return [] } } /** * 上传短视频(/common/localuploadvideo) */ export function uploadVideoFile(filePath, { fileName, onProgress } = {}) { return new Promise((resolve, reject) => { if (!filePath) { reject(new Error('视频文件无效')) return } const uploadTask = uni.uploadFile({ url: videoUploadPath(), filePath, name: 'file', formData: { name: fileName || `video-${Date.now()}.mp4` }, success: (res) => { const parsed = parseVideoUploadEnvelope(res.data) if (res.statusCode === 200 && parsed) { resolve(parsed) return } let message = '视频上传失败' try { const envelope = typeof res.data === 'string' ? JSON.parse(res.data) : res.data if (envelope && envelope.msg) message = String(envelope.msg) } catch (_) {} reject(new Error(message)) }, fail: (err) => reject(err || new Error('视频上传失败')) }) if (onProgress && uploadTask && typeof uploadTask.onProgressUpdate === 'function') { uploadTask.onProgressUpdate(onProgress) } }) } /** * 发布种草动态(type=want) * 使用 uni.request + complete,避免拦截器 return false 时 Promise 永不 settle */ export function publishGrassPost({ content, images, goodsIds = [] }) { const body = { type: 'want', content, images, goods_ids: goodsIds, video_id: null, label_id: null } return postFindData(body) } /** * 发布短视频(type=video) */ export function publishVideoPost({ content, videoId, videoUrl, labelId, goodsIds = [] }) { const body = { type: 'video', content, images: [], goods_ids: goodsIds, video_id: videoId, video: videoUrl || null, label_id: labelId } return postFindData(body) }