待测试

This commit is contained in:
2026-07-28 07:58:26 +08:00
parent 1e9e25fe67
commit 6fbcf21024
22 changed files with 1294 additions and 264 deletions
+79 -4
View File
@@ -521,13 +521,20 @@ const normalizeProfileUpdatePayload = (payload) => {
throw new TypeError('个人资料请求包含未声明字段')
}
const normalized = {}
for (const [field, limit] of [['nickName', 30], ['realName', 30], ['sex', Infinity]]) {
for (const [field, limit] of [['nickName', 30], ['realName', 30]]) {
if (!Object.prototype.hasOwnProperty.call(payload, field)) continue
const value = normalizeOptionalAuthText(payload[field], field)
if (!value) continue
if (value.length > limit) throw new TypeError(`${field} 超出长度限制`)
normalized[field] = value
}
if (Object.prototype.hasOwnProperty.call(payload, 'sex')) {
const sex = normalizeOptionalAuthText(payload.sex, 'sex')
if (sex) {
if (!['0', '1', '2'].includes(sex)) throw new TypeError('sex 必须为 0、1 或 2')
normalized.sex = sex
}
}
if (Object.prototype.hasOwnProperty.call(payload, 'birthday')) {
const value = normalizeOptionalAuthText(payload.birthday, 'birthday')
if (value) {
@@ -705,7 +712,12 @@ const normalizeFeedbackPayload = (payload) => {
throw new TypeError(`反馈字段 ${optionalField} 必须是字符串`)
}
const value = payload[optionalField].trim()
if (value) normalized[optionalField] = value
if (value) {
if (optionalField === 'feedbackType' && !['advice', 'bug', 'complaint', 'other'].includes(value)) {
throw new TypeError('feedbackType 必须为 advice、bug、complaint 或 other')
}
normalized[optionalField] = value
}
}
return normalized
}
@@ -1586,17 +1598,62 @@ const normalizeAlbumPhotoCreatePayload = (payload) => {
return data
}
const normalizeVideoPayload = (payload) => {
const allowedFields = new Set(['videoTitle', 'videoDesc', 'coverOssId', 'videoOssId', 'durationSeconds', 'sortOrder', 'status'])
assertPlainPayload(payload, allowedFields, '视频请求')
const videoTitle = normalizeOptionalAuthText(payload.videoTitle, '视频标题')
if (!videoTitle) throw new TypeError('视频标题不能为空')
const videoOssId = normalizeOssIdString(payload.videoOssId, '视频文件 OSS ID')
const data = { videoTitle, videoOssId }
const videoDesc = normalizeOptionalAuthText(payload.videoDesc, '视频说明')
if (videoDesc) data.videoDesc = videoDesc
if (payload.coverOssId !== undefined && payload.coverOssId !== null && payload.coverOssId !== '') {
data.coverOssId = normalizeOssIdString(payload.coverOssId, '视频封面 OSS ID')
}
for (const field of ['durationSeconds', 'sortOrder']) {
const value = normalizeOptionalSafeInteger(payload[field], field)
if (value !== undefined) data[field] = value
}
if (Object.prototype.hasOwnProperty.call(payload, 'status')) {
const status = normalizeOptionalAuthText(payload.status, 'status')
if (status) {
if (!['0', '1'].includes(status)) throw new TypeError('status 必须为 0 或 1')
data.status = status
}
}
return data
}
const normalizeCeremonyPayload = (payload) => {
const allowedFields = new Set(['ceremonyType', 'ceremonyTitle', 'ceremonyDesc', 'ceremonyTime', 'location', 'coverOssId', 'sortOrder', 'status'])
const allowedFields = new Set(['ceremonyType', 'ceremonyTitle', 'ceremonyDesc', 'ceremonyTime', 'location', 'locationAddress', 'longitude', 'latitude', 'coverOssId', 'sortOrder', 'status'])
assertPlainPayload(payload, allowedFields, '礼仪活动请求')
const ceremonyType = normalizeOptionalAuthText(payload.ceremonyType, 'ceremonyType')
const ceremonyTitle = normalizeOptionalAuthText(payload.ceremonyTitle, 'ceremonyTitle')
if (!ceremonyType || !ceremonyTitle) throw new TypeError('请填写活动类型和活动标题')
const data = { ceremonyType, ceremonyTitle }
for (const field of ['ceremonyDesc', 'ceremonyTime', 'location', 'status']) {
for (const field of ['ceremonyDesc', 'ceremonyTime', 'location', 'locationAddress']) {
const value = normalizeOptionalAuthText(payload[field], field)
if (value) data[field] = value
}
if (Object.prototype.hasOwnProperty.call(payload, 'status')) {
const status = normalizeOptionalAuthText(payload.status, 'status')
if (status) {
if (!['0', '1'].includes(status)) throw new TypeError('status 必须为 0 或 1')
data.status = status
}
}
const hasLongitude = payload.longitude !== undefined && payload.longitude !== null && payload.longitude !== ''
const hasLatitude = payload.latitude !== undefined && payload.latitude !== null && payload.latitude !== ''
if (hasLongitude !== hasLatitude) throw new TypeError('longitude 和 latitude 必须同时提供')
if (hasLongitude) {
const longitude = typeof payload.longitude === 'number' ? payload.longitude : Number(payload.longitude)
const latitude = typeof payload.latitude === 'number' ? payload.latitude : Number(payload.latitude)
if (!Number.isFinite(longitude) || !Number.isFinite(latitude)) {
throw new TypeError('longitude 和 latitude 必须是有限数字')
}
data.longitude = longitude
data.latitude = latitude
}
if (payload.coverOssId !== undefined && payload.coverOssId !== null && payload.coverOssId !== '') {
data.coverOssId = normalizeOssIdString(payload.coverOssId, 'coverOssId')
}
@@ -3301,6 +3358,24 @@ export const appApi = {
async getFeedback(requestOptions = {}) {
return readRemoteList('/genealogy/app/feedback', '我的反馈', requestOptions)
},
async getVideos(genealogyId, requestOptions = {}) {
const id = normalizeGenealogyPathId(genealogyId)
return readRemoteList(`/genealogy/app/genealogies/${id}/videos`, '家族视频', requestOptions)
},
async getVideoDetail(genealogyId, videoId, requestOptions = {}) {
const id = normalizeGenealogyPathId(genealogyId)
const normalizedVideoId = normalizeResourcePathId(videoId, '视频标识')
return readRemoteObject(`/genealogy/app/genealogies/${id}/videos/${normalizedVideoId}`, '家族视频详情', requestOptions)
},
async createVideo(genealogyId, payload, requestOptions = {}) {
const id = normalizeGenealogyPathId(genealogyId)
return writeRemoteObject(`/genealogy/app/genealogies/${id}/videos`, 'POST', normalizeVideoPayload(payload), '家族视频', requestOptions)
},
async updateVideo(genealogyId, videoId, payload, requestOptions = {}) {
const id = normalizeGenealogyPathId(genealogyId)
const normalizedVideoId = normalizeResourcePathId(videoId, '视频标识')
return writeRemoteObject(`/genealogy/app/genealogies/${id}/videos/${normalizedVideoId}`, 'PUT', normalizeVideoPayload(payload), '家族视频', requestOptions)
},
async deleteVideo(genealogyId, videoId, requestOptions = {}) {
const id = normalizeGenealogyPathId(genealogyId)
const normalizedVideoId = normalizeResourcePathId(videoId, '视频标识')
+105 -2
View File
@@ -8,6 +8,7 @@ const createUploadError = (message, code) => {
};
export const isImagePickCancelled = (error) => error?.code === "IMAGE_PICK_CANCELLED";
export const isVideoPickCancelled = (error) => error?.code === "VIDEO_PICK_CANCELLED";
const createUploadId = () => {
if (typeof globalThis.crypto?.randomUUID === "function") return globalThis.crypto.randomUUID();
@@ -73,7 +74,66 @@ const pickBrowserImage = () => new Promise((resolve, reject) => {
input.click();
});
const readNativeImage = (filePath) => new Promise((resolve, reject) => {
const pickNativeVideo = () => new Promise((resolve, reject) => {
if (!globalThis.plus?.gallery?.pick) {
reject(createUploadError("当前运行环境不支持从相册选择视频", "VIDEO_PICK_UNAVAILABLE"));
return;
}
globalThis.plus.gallery.pick(
(filePath) => resolve(filePath),
() => reject(createUploadError("已取消选择视频", "VIDEO_PICK_CANCELLED")),
{ filter: "video", multiple: false },
);
});
const pickBrowserVideo = () => new Promise((resolve, reject) => {
if (typeof globalThis.document?.createElement !== "function" || typeof globalThis.FileReader !== "function") {
reject(createUploadError("当前运行环境不支持选择视频", "VIDEO_PICK_UNAVAILABLE"));
return;
}
const input = globalThis.document.createElement("input");
input.type = "file";
input.accept = "video/*";
input.style.display = "none";
const cleanup = () => input.remove();
input.addEventListener("cancel", () => {
cleanup();
reject(createUploadError("已取消选择视频", "VIDEO_PICK_CANCELLED"));
}, { once: true });
input.addEventListener("change", () => {
const browserFile = input.files?.[0];
if (!browserFile) {
cleanup();
reject(createUploadError("已取消选择视频", "VIDEO_PICK_CANCELLED"));
return;
}
const reader = new globalThis.FileReader();
reader.onload = () => {
const data = reader.result;
cleanup();
if (!(data instanceof ArrayBuffer)) {
reject(createUploadError("读取视频数据失败", "VIDEO_READ_INVALID"));
return;
}
resolve({
browserFile,
fileName: browserFile.name || "video",
size: browserFile.size,
contentType: browserFile.type || "video/*",
data,
});
};
reader.onerror = () => {
cleanup();
reject(createUploadError("读取视频数据失败", "VIDEO_READ_FAILED"));
};
reader.readAsArrayBuffer(browserFile);
}, { once: true });
globalThis.document.body?.append(input);
input.click();
});
const readNativeFile = (filePath) => new Promise((resolve, reject) => {
if (!globalThis.plus?.io?.resolveLocalFileSystemURL || !globalThis.plus?.io?.FileReader) {
reject(createUploadError("当前运行环境不支持读取图片", "IMAGE_READ_UNAVAILABLE"));
return;
@@ -137,7 +197,7 @@ const toUploadReceipt = ({ ossId, url = "", thumbnailUrl = "", fileName = "" })
export const pickAndUploadImage = async ({ requestController = null } = {}) => {
const file = globalThis.plus?.gallery?.pick
? await readNativeImage(await pickNativeImage())
? await readNativeFile(await pickNativeImage())
: await pickBrowserImage();
if (!Number.isSafeInteger(file.size) || file.size <= 0) {
throw createUploadError("所选图片大小无效", "IMAGE_SIZE_INVALID");
@@ -173,3 +233,46 @@ export const pickAndUploadImage = async ({ requestController = null } = {}) => {
}, { requestController });
return toUploadReceipt(completed);
};
export const pickAndUploadVideo = async ({ requestController = null } = {}) => {
const file = globalThis.plus?.gallery?.pick
? await readNativeFile(await pickNativeVideo())
: await pickBrowserVideo();
const fileName = String(file.fileName || "");
if (!String(file.contentType || "").startsWith("video/") && !/\.(mp4|mov|m4v|webm|avi|mkv)$/i.test(fileName)) {
throw createUploadError("请选择视频文件", "VIDEO_TYPE_INVALID");
}
if (!Number.isSafeInteger(file.size) || file.size <= 0) {
throw createUploadError("所选视频大小无效", "VIDEO_SIZE_INVALID");
}
const fileMd5 = calcMD5Bytes(file.data);
const initPayload = {
uploadId: createUploadId(),
fileName,
fileMd5,
totalSize: file.size,
totalChunks: 1,
chunkSize: file.size,
contentType: file.contentType || "video/*",
};
const initialized = await appApi.initializeResumableUpload(initPayload, { requestController });
if (initialized.instant) return toUploadReceipt(initialized);
const chunkPayload = {
uploadId: initialized.uploadId,
chunkIndex: 0,
chunkMd5: fileMd5,
};
if (file.browserFile) {
await appApi.uploadBrowserResumableChunk(chunkPayload, file.browserFile, { requestController });
} else {
await appApi.uploadResumableChunk({ ...chunkPayload, filePath: file.filePath }, { requestController });
}
const completed = await appApi.completeResumableUpload({
uploadId: initialized.uploadId,
fileName,
fileMd5,
totalSize: file.size,
totalChunks: 1,
}, { requestController });
return toUploadReceipt(completed);
};