feat: 完成前端业务闭环与后端联调
This commit is contained in:
@@ -3,10 +3,10 @@ const CURRENT_GENEALOGY_INVALIDATED_KEY = 'jiapu_current_genealogy_invalidated'
|
||||
|
||||
const normalizeGenealogyId = (genealogyId) => {
|
||||
if (typeof genealogyId !== 'string') {
|
||||
throw new TypeError('当前家谱 ID 必须是词法字符串')
|
||||
throw new TypeError('当前家谱 ID 必须是字符串')
|
||||
}
|
||||
if (!genealogyId || genealogyId.trim() !== genealogyId || /[\u0000-\u001f\u007f]/.test(genealogyId)) {
|
||||
throw new TypeError('当前家谱 ID 必须是无边界空白的非空字符串')
|
||||
if (!/^[1-9]\d*$/.test(genealogyId)) {
|
||||
throw new TypeError('当前家谱 ID 必须是正整数标识')
|
||||
}
|
||||
return genealogyId
|
||||
}
|
||||
|
||||
+15
-11
@@ -133,7 +133,7 @@ const pickBrowserVideo = () => new Promise((resolve, reject) => {
|
||||
input.click();
|
||||
});
|
||||
|
||||
const readNativeFile = (filePath) => new Promise((resolve, reject) => {
|
||||
const readNativeFile = (filePath, mediaLabel) => new Promise((resolve, reject) => {
|
||||
if (typeof globalThis.uni?.getFileInfo === 'function') {
|
||||
const normalizedPath = String(filePath || '')
|
||||
const fileName = normalizedPath.split(/[\\/]/).pop() || 'file'
|
||||
@@ -148,7 +148,7 @@ const readNativeFile = (filePath) => new Promise((resolve, reject) => {
|
||||
contentType: 'application/octet-stream',
|
||||
md5: String(file.digest || '').toLowerCase()
|
||||
}),
|
||||
fail: () => reject(createUploadError('读取图片信息失败', 'IMAGE_READ_FAILED'))
|
||||
fail: () => reject(createUploadError(`读取${mediaLabel}信息失败`, 'MEDIA_READ_FAILED'))
|
||||
})
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
@@ -156,7 +156,7 @@ const readNativeFile = (filePath) => new Promise((resolve, reject) => {
|
||||
return
|
||||
}
|
||||
if (!globalThis.plus?.io?.resolveLocalFileSystemURL || !globalThis.plus?.io?.FileReader) {
|
||||
reject(createUploadError("当前运行环境不支持读取图片", "IMAGE_READ_UNAVAILABLE"));
|
||||
reject(createUploadError(`当前运行环境不支持读取${mediaLabel}`, "MEDIA_READ_UNAVAILABLE"));
|
||||
return;
|
||||
}
|
||||
globalThis.plus.io.resolveLocalFileSystemURL(
|
||||
@@ -167,7 +167,7 @@ const readNativeFile = (filePath) => new Promise((resolve, reject) => {
|
||||
reader.onloadend = (event) => {
|
||||
const fileData = event?.target?.result ?? reader.result;
|
||||
if (!(fileData instanceof ArrayBuffer)) {
|
||||
reject(createUploadError("读取图片数据失败", "IMAGE_READ_INVALID"));
|
||||
reject(createUploadError(`读取${mediaLabel}数据失败`, "MEDIA_READ_INVALID"));
|
||||
return;
|
||||
}
|
||||
resolve({
|
||||
@@ -178,12 +178,12 @@ const readNativeFile = (filePath) => new Promise((resolve, reject) => {
|
||||
data: fileData,
|
||||
});
|
||||
};
|
||||
reader.onerror = () => reject(createUploadError("读取图片数据失败", "IMAGE_READ_FAILED"));
|
||||
reader.onerror = () => reject(createUploadError(`读取${mediaLabel}数据失败`, "MEDIA_READ_FAILED"));
|
||||
reader.readAsArrayBuffer(file);
|
||||
},
|
||||
() => reject(createUploadError("读取图片信息失败", "IMAGE_READ_FAILED")),
|
||||
() => reject(createUploadError(`读取${mediaLabel}信息失败`, "MEDIA_READ_FAILED")),
|
||||
),
|
||||
() => reject(createUploadError("无法读取所选图片", "IMAGE_READ_FAILED")),
|
||||
() => reject(createUploadError(`无法读取所选${mediaLabel}`, "MEDIA_READ_FAILED")),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -204,15 +204,19 @@ const toUploadReceipt = ({ ossId, url = "", thumbnailUrl = "", fileName = "" })
|
||||
|
||||
export const pickAndUploadImage = async ({ requestController = null } = {}) => {
|
||||
const file = globalThis.plus?.gallery?.pick
|
||||
? await readNativeFile(await pickNativeImage())
|
||||
? await readNativeFile(await pickNativeImage(), "图片")
|
||||
: await pickBrowserImage();
|
||||
const fileName = String(file.fileName || "");
|
||||
if (!String(file.contentType || "").startsWith("image/") && !/\.(jpe?g|png|gif|webp|heic|heif)$/i.test(fileName)) {
|
||||
throw createUploadError("请选择图片文件", "IMAGE_TYPE_INVALID");
|
||||
}
|
||||
if (!Number.isSafeInteger(file.size) || file.size <= 0) {
|
||||
throw createUploadError("所选图片大小无效", "IMAGE_SIZE_INVALID");
|
||||
}
|
||||
const fileMd5 = file.md5 || calcMD5Bytes(file.data);
|
||||
const initPayload = {
|
||||
uploadId: createUploadId(),
|
||||
fileName: file.fileName,
|
||||
fileName,
|
||||
fileMd5,
|
||||
totalSize: file.size,
|
||||
totalChunks: 1,
|
||||
@@ -233,7 +237,7 @@ export const pickAndUploadImage = async ({ requestController = null } = {}) => {
|
||||
}
|
||||
const completed = await fileUploadApi.completeResumableUpload({
|
||||
uploadId: initialized.uploadId,
|
||||
fileName: file.fileName,
|
||||
fileName,
|
||||
fileMd5,
|
||||
totalSize: file.size,
|
||||
totalChunks: 1,
|
||||
@@ -243,7 +247,7 @@ export const pickAndUploadImage = async ({ requestController = null } = {}) => {
|
||||
|
||||
export const pickAndUploadVideo = async ({ requestController = null } = {}) => {
|
||||
const file = globalThis.plus?.gallery?.pick
|
||||
? await readNativeFile(await pickNativeVideo())
|
||||
? 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)) {
|
||||
|
||||
@@ -233,6 +233,17 @@ const runUniNavigation = (
|
||||
return promise;
|
||||
};
|
||||
|
||||
const runUniNavigationAfterCurrent = (key, invoke) => {
|
||||
if (!navigationInFlight) return runUniNavigation(key, invoke);
|
||||
if (navigationInFlight.key === key) return navigationInFlight.promise;
|
||||
|
||||
const currentPromise = navigationInFlight.promise;
|
||||
return currentPromise.then(
|
||||
() => runUniNavigationAfterCurrent(key, invoke),
|
||||
() => runUniNavigationAfterCurrent(key, invoke),
|
||||
);
|
||||
};
|
||||
|
||||
const asNavigationPromise = (execute) => {
|
||||
try {
|
||||
return execute();
|
||||
@@ -494,6 +505,18 @@ export const goRoot = (routeKey, params = {}) =>
|
||||
uni.reLaunch({ url, ...callbacks }));
|
||||
});
|
||||
|
||||
export const recoverToAuthRoot = () =>
|
||||
asNavigationPromise(() => {
|
||||
const route = getRoute("A01");
|
||||
const url = buildValidatedRouteUrl("A01", route, Object.freeze({}));
|
||||
if (isCurrentTarget("A01", {})) return Promise.resolve(false);
|
||||
|
||||
// 401 可能在上一场页面转场仍持锁时返回。普通点击可以被去重,登录恢复不能
|
||||
// 被静默丢弃,因此等待当前转场收尾后再重置页面栈。
|
||||
return runUniNavigationAfterCurrent(`recovery-root:${url}`, (callbacks) =>
|
||||
uni.reLaunch({ url, ...callbacks }));
|
||||
});
|
||||
|
||||
export const openNoticeTarget = (targetType, params, sourceKey = "N02") =>
|
||||
asNavigationPromise(() => {
|
||||
assertScalarString("通知目标类型", targetType);
|
||||
|
||||
@@ -72,7 +72,7 @@ export const ROUTES = Object.freeze({
|
||||
kind: "flow",
|
||||
parent: "G06",
|
||||
requiredParams: ["genealogyId"],
|
||||
optionalParams: ["source"],
|
||||
optionalParams: ["source", "genealogyName"],
|
||||
allowedSources: ["G01", "G05", "G06", "G09"],
|
||||
}),
|
||||
G09: defineRoute({
|
||||
@@ -108,6 +108,13 @@ export const ROUTES = Object.freeze({
|
||||
kind: "page",
|
||||
parent: "G05",
|
||||
requiredParams: ["genealogyId"],
|
||||
allowedSources: ["G01", "G05"],
|
||||
}),
|
||||
G14: defineRoute({
|
||||
path: "/pages/genealogy/capability-center",
|
||||
kind: "page",
|
||||
parent: "G05",
|
||||
requiredParams: ["genealogyId"],
|
||||
allowedSources: ["G05"],
|
||||
}),
|
||||
T01: defineRoute({
|
||||
@@ -316,7 +323,7 @@ export const ROUTES = Object.freeze({
|
||||
kind: "page",
|
||||
parent: "F01",
|
||||
requiredParams: ["genealogyId"],
|
||||
optionalParams: ["memoId"],
|
||||
optionalParams: ["memoId", "memoType"],
|
||||
allowedSources: ["F01", "N02"],
|
||||
}),
|
||||
R11: defineRoute({
|
||||
@@ -326,6 +333,13 @@ export const ROUTES = Object.freeze({
|
||||
requiredParams: ["genealogyId"],
|
||||
allowedSources: ["F01"],
|
||||
}),
|
||||
R12: defineRoute({
|
||||
path: "/pages/records/person-documents",
|
||||
kind: "page",
|
||||
parent: "G05",
|
||||
requiredParams: ["genealogyId"],
|
||||
allowedSources: ["G05"],
|
||||
}),
|
||||
N01: defineRoute({
|
||||
path: "/pages/notification/message-center",
|
||||
kind: "page",
|
||||
@@ -399,6 +413,13 @@ export const ROUTES = Object.freeze({
|
||||
parent: "M01",
|
||||
allowedSources: ["M01"],
|
||||
}),
|
||||
F11: defineRoute({
|
||||
path: "/pages/family/platform-videos",
|
||||
kind: "page",
|
||||
parent: "F01",
|
||||
optionalParams: ["placement", "videoId"],
|
||||
allowedSources: ["F10", "G01"],
|
||||
}),
|
||||
M11: defineRoute({
|
||||
path: "/pages/profile/ceremony-invitations",
|
||||
kind: "page",
|
||||
@@ -416,7 +437,7 @@ export const ROUTES = Object.freeze({
|
||||
kind: "page",
|
||||
parent: "M10",
|
||||
requiredParams: ["documentKey"],
|
||||
allowedSources: ["M10"],
|
||||
allowedSources: ["A01", "A04", "M10"],
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -20,9 +20,6 @@ const normalizeFeedback = (form) => ({
|
||||
const snapshotOf = (form) => JSON.stringify(normalizeFeedback(form));
|
||||
|
||||
const errorMessage = (error) => {
|
||||
if (error?.code === "WRITE_UNAVAILABLE") {
|
||||
return "当前为本地预览,反馈没有发出。";
|
||||
}
|
||||
if (error?.httpStatus === 401) {
|
||||
return "登录状态已失效,请重新登录后再提交。";
|
||||
}
|
||||
|
||||
+8
-21
@@ -1,13 +1,9 @@
|
||||
const configuredMode = import.meta.env.VITE_JIAPU_RUNTIME_MODE || 'remote'
|
||||
const configuredBaseUrl = import.meta.env.VITE_JIAPU_API_BASE_URL || 'https://backend-api.ddxcjp.cn'
|
||||
const configuredClientId = import.meta.env.VITE_JIAPU_CLIENT_ID || '428a8310cd442757ae699df5d894f051'
|
||||
const configuredTenantId = import.meta.env.VITE_JIAPU_TENANT_ID || '000000'
|
||||
const configuredBaseUrl = 'https://backend-api.ddxcjp.cn'
|
||||
const configuredClientId = '428a8310cd442757ae699df5d894f051'
|
||||
const configuredTenantId = '000000'
|
||||
|
||||
if (!['mock', 'remote'].includes(configuredMode)) {
|
||||
throw new Error('VITE_JIAPU_RUNTIME_MODE 只允许 mock 或 remote')
|
||||
}
|
||||
if (configuredMode === 'remote' && !/^https:\/\/[^\s/]+(?:\/[^\s]*)?$/.test(configuredBaseUrl)) {
|
||||
throw new Error('VITE_JIAPU_API_BASE_URL 必须是 HTTPS 地址')
|
||||
if (!/^https:\/\/[^\s/]+(?:\/[^\s]*)?$/.test(configuredBaseUrl)) {
|
||||
throw new Error('正式服务地址必须使用 HTTPS')
|
||||
}
|
||||
if (configuredBaseUrl.endsWith('/')) {
|
||||
throw new Error('VITE_JIAPU_API_BASE_URL 不能以斜杠结尾')
|
||||
@@ -16,19 +12,10 @@ if (!configuredClientId || !configuredTenantId) {
|
||||
throw new Error('远程服务的 clientId 和 tenantId 不能为空')
|
||||
}
|
||||
|
||||
// 业务模块只消费这一个不可变配置;不同构建环境通过 VITE_JIAPU_* 覆盖。
|
||||
// 业务模块只消费这一份不可变正式配置;测试替身在 API 服务边界注入,
|
||||
// 不通过构建机环境变量改变正式包的服务端点。
|
||||
export const runtimeConfig = Object.freeze({
|
||||
mode: configuredMode,
|
||||
baseUrl: configuredMode === 'remote' ? configuredBaseUrl : '',
|
||||
baseUrl: configuredBaseUrl,
|
||||
clientId: configuredClientId,
|
||||
tenantId: configuredTenantId
|
||||
})
|
||||
|
||||
export const isMockMode = () => runtimeConfig.mode === 'mock'
|
||||
export const hasRemoteConfig = () => runtimeConfig.mode === 'remote' && Boolean(runtimeConfig.baseUrl && runtimeConfig.clientId)
|
||||
|
||||
export const resolveRuntimeMode = () => {
|
||||
if (isMockMode()) return 'mock'
|
||||
if (hasRemoteConfig()) return 'remote'
|
||||
throw new Error('运行模式配置无效:只允许 mock 或配置完整的 remote')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user