完成50%
This commit is contained in:
+345
-65
@@ -1,21 +1,107 @@
|
||||
import { currentUser, genealogies, treeMembers, familyFeeds, familyContent, notifications, joinApplications } from '@/data/mock.js'
|
||||
import { hasRemoteConfig, isMockMode, runtimeConfig } from '@/utils/config.js'
|
||||
import {
|
||||
currentUser,
|
||||
genealogies,
|
||||
publicGenealogies,
|
||||
treeMembers,
|
||||
notifications,
|
||||
joinApplications,
|
||||
listFamilyFeedFixtures,
|
||||
listFamilyArticleFixtures,
|
||||
listFamilyAlbumFixtures,
|
||||
listCeremonyFixtures,
|
||||
listGrowthRecordFixtures,
|
||||
listNotificationFixtures
|
||||
} from '@/data/mock.js'
|
||||
import { hasRemoteConfig, resolveRuntimeMode, runtimeConfig } from '@/utils/config.js'
|
||||
import { AUTH_TAC_SCENE, assertSmsCode } from '@/utils/auth-verification.js'
|
||||
import { GENEALOGY_ACCESS_PRESET } from '@/utils/genealogy-contracts.js'
|
||||
import { session } from '@/utils/session.js'
|
||||
|
||||
const successCodes = [0, 200]
|
||||
const REQUEST_TIMEOUT_MS = 15000
|
||||
|
||||
const createRequestError = (message, code, details = {}) => {
|
||||
const error = new Error(message)
|
||||
error.code = code
|
||||
Object.assign(error, details)
|
||||
return error
|
||||
}
|
||||
|
||||
const createRequestCancelledError = () => createRequestError('请求已取消', 'REQUEST_CANCELLED')
|
||||
|
||||
// 页面只持有这个窄控制器,不直接依赖各端 RequestTask 的实现差异。
|
||||
// request() 是唯一绑定/释放 owner;离页时 abort() 会同时拒绝等待中的 Promise,
|
||||
// 因而页面不会在已销毁后继续处理成功回调。
|
||||
export const createRequestController = () => {
|
||||
let abortCurrent = null
|
||||
return {
|
||||
bind(abortRequest) {
|
||||
if (typeof abortRequest !== 'function') throw new TypeError('请求中止器必须是函数')
|
||||
if (abortCurrent) abortCurrent()
|
||||
abortCurrent = abortRequest
|
||||
},
|
||||
release(abortRequest) {
|
||||
if (abortCurrent === abortRequest) abortCurrent = null
|
||||
},
|
||||
abort() {
|
||||
const abortRequest = abortCurrent
|
||||
abortCurrent = null
|
||||
if (abortRequest) abortRequest()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const isRequestCancelled = (error) => error?.code === 'REQUEST_CANCELLED'
|
||||
|
||||
export const unwrapResponse = (response) => {
|
||||
if (!response || typeof response !== 'object' || Array.isArray(response)) return response
|
||||
if (!Object.prototype.hasOwnProperty.call(response, 'code')) return response
|
||||
if (!successCodes.includes(Number(response.code))) {
|
||||
throw new Error(response.msg || '请求未成功')
|
||||
const numericCode = Number(response.code)
|
||||
throw createRequestError(response.msg || '请求未成功', 'BUSINESS_ERROR', {
|
||||
businessCode: Number.isNaN(numericCode) ? response.code : numericCode
|
||||
})
|
||||
}
|
||||
return Object.prototype.hasOwnProperty.call(response, 'data') ? response.data : response
|
||||
}
|
||||
|
||||
const request = (options, { authenticated = true } = {}) => new Promise((resolve, reject) => {
|
||||
const request = (options, {
|
||||
authenticated = true,
|
||||
strictEnvelope = false,
|
||||
requireEnvelopeData = false,
|
||||
expectedStatus = null,
|
||||
requestController = null
|
||||
} = {}) => new Promise((resolve, reject) => {
|
||||
if (requestController !== null && (
|
||||
typeof requestController.bind !== 'function' ||
|
||||
typeof requestController.release !== 'function'
|
||||
)) {
|
||||
reject(new TypeError('请求控制器格式无效'))
|
||||
return
|
||||
}
|
||||
let settled = false
|
||||
let requestTask = null
|
||||
const release = () => requestController?.release(abortRequest)
|
||||
const resolveOnce = (value) => {
|
||||
if (settled) return
|
||||
settled = true
|
||||
release()
|
||||
resolve(value)
|
||||
}
|
||||
const rejectOnce = (error) => {
|
||||
if (settled) return
|
||||
settled = true
|
||||
release()
|
||||
reject(error)
|
||||
}
|
||||
const abortRequest = () => {
|
||||
if (settled) return
|
||||
const task = requestTask
|
||||
rejectOnce(createRequestCancelledError())
|
||||
task?.abort?.()
|
||||
}
|
||||
const token = session.getToken()
|
||||
uni.request({
|
||||
requestTask = uni.request({
|
||||
...options,
|
||||
url: `${runtimeConfig.baseUrl}${options.url}`,
|
||||
header: {
|
||||
@@ -24,18 +110,49 @@ const request = (options, { authenticated = true } = {}) => new Promise((resolve
|
||||
...(options.header || {})
|
||||
},
|
||||
success: ({ data, statusCode }) => {
|
||||
if (expectedStatus !== null && statusCode !== expectedStatus) {
|
||||
rejectOnce(createRequestError(
|
||||
`服务只接受 HTTP ${expectedStatus} 响应,实际为 ${statusCode}`,
|
||||
'HTTP_ERROR',
|
||||
{ httpStatus: statusCode }
|
||||
))
|
||||
return
|
||||
}
|
||||
if (statusCode < 200 || statusCode >= 300) {
|
||||
reject(new Error(data?.msg || `请求失败(${statusCode})`))
|
||||
rejectOnce(createRequestError(
|
||||
data?.msg || `请求失败(${statusCode})`,
|
||||
'HTTP_ERROR',
|
||||
{ httpStatus: statusCode }
|
||||
))
|
||||
return
|
||||
}
|
||||
if (
|
||||
strictEnvelope &&
|
||||
(!data ||
|
||||
typeof data !== 'object' ||
|
||||
Array.isArray(data) ||
|
||||
!Object.prototype.hasOwnProperty.call(data, 'code') ||
|
||||
!Number.isInteger(data.code) ||
|
||||
(requireEnvelopeData && !Object.prototype.hasOwnProperty.call(data, 'data')))
|
||||
) {
|
||||
rejectOnce(createRequestError('服务响应格式无效', 'RESPONSE_INVALID'))
|
||||
return
|
||||
}
|
||||
try {
|
||||
resolve(unwrapResponse(data))
|
||||
resolveOnce(unwrapResponse(data))
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
rejectOnce(error)
|
||||
}
|
||||
},
|
||||
fail: (error) => reject(new Error(error.errMsg || '网络连接失败'))
|
||||
fail: (error) => {
|
||||
const message = error?.errMsg || '网络连接失败'
|
||||
rejectOnce(createRequestError(
|
||||
message,
|
||||
/timeout/i.test(message) ? 'REQUEST_TIMEOUT' : 'NETWORK_ERROR'
|
||||
))
|
||||
}
|
||||
})
|
||||
if (!settled && requestController) requestController.bind(abortRequest)
|
||||
})
|
||||
|
||||
const authPayload = (payload) => ({
|
||||
@@ -44,8 +161,98 @@ const authPayload = (payload) => ({
|
||||
...payload
|
||||
})
|
||||
|
||||
// 已核对接口使用这一严格边界:只接受 HTTP 200 与完整 JSON envelope,
|
||||
// 并统一限制弱网等待时间。尚未逐页治理的旧业务读取仍沿用上方通用 request。
|
||||
const requestStrict = (options, {
|
||||
authenticated = true,
|
||||
requireData = true,
|
||||
requestController = null
|
||||
} = {}) => request({
|
||||
...options,
|
||||
timeout: REQUEST_TIMEOUT_MS
|
||||
}, {
|
||||
authenticated,
|
||||
strictEnvelope: true,
|
||||
requireEnvelopeData: requireData,
|
||||
expectedStatus: 200,
|
||||
requestController
|
||||
})
|
||||
|
||||
const requestAuth = (options, { requestController = null } = {}) => requestStrict(options, {
|
||||
authenticated: false,
|
||||
requestController
|
||||
})
|
||||
|
||||
// RVoid 的 OpenAPI schema 没有把 data 声明为必填。空结果接口仍要求合法的
|
||||
// HTTP 200 与整数 code,但统一向页面返回 null,避免把 envelope 本身泄漏成业务数据。
|
||||
const requestAuthVoid = async (options, { requestController = null } = {}) => {
|
||||
await requestStrict(options, {
|
||||
authenticated: false,
|
||||
requireData: false,
|
||||
requestController
|
||||
})
|
||||
return null
|
||||
}
|
||||
|
||||
const requireRemoteAuth = () => {
|
||||
if (resolveRuntimeMode() !== 'remote') {
|
||||
const error = new Error('当前为本地预览模式,真实认证服务未启用')
|
||||
error.code = 'AUTH_REMOTE_REQUIRED'
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const assertAuthScene = (sceneCode) => {
|
||||
if (!Object.values(AUTH_TAC_SCENE).includes(sceneCode)) {
|
||||
throw new TypeError('短信场景不属于当前认证合同')
|
||||
}
|
||||
return sceneCode
|
||||
}
|
||||
|
||||
const assertValidToken = (validToken) => {
|
||||
if (typeof validToken !== 'string' || !validToken.trim()) {
|
||||
throw new TypeError('发送短信前必须取得有效的行为验证票据')
|
||||
}
|
||||
return validToken.trim()
|
||||
}
|
||||
|
||||
const assertPasswordHash = (passwordHash) => {
|
||||
if (typeof passwordHash !== 'string' || !/^[a-f0-9]{32}$/.test(passwordHash)) {
|
||||
throw new TypeError('密码摘要必须是 32 位小写 MD5')
|
||||
}
|
||||
return passwordHash
|
||||
}
|
||||
|
||||
const normalizeFeedbackPayload = (payload) => {
|
||||
if (!payload || typeof payload !== 'object' || Array.isArray(payload) || Object.getPrototypeOf(payload) !== Object.prototype) {
|
||||
throw new TypeError('反馈请求必须是普通对象')
|
||||
}
|
||||
const allowedFields = new Set(['feedbackType', 'feedbackContent', 'contactInfo'])
|
||||
const fieldNames = Object.getOwnPropertyNames(payload)
|
||||
if (Object.getOwnPropertySymbols(payload).length > 0 || fieldNames.some((field) => !allowedFields.has(field))) {
|
||||
throw new TypeError('反馈请求包含未声明字段')
|
||||
}
|
||||
const descriptors = Object.getOwnPropertyDescriptors(payload)
|
||||
if (fieldNames.some((field) => !Object.prototype.hasOwnProperty.call(descriptors[field], 'value'))) {
|
||||
throw new TypeError('反馈字段必须是普通数据属性')
|
||||
}
|
||||
if (typeof payload.feedbackContent !== 'string' || !payload.feedbackContent.trim()) {
|
||||
throw new TypeError('反馈内容必须是非空字符串')
|
||||
}
|
||||
const normalized = { feedbackContent: payload.feedbackContent.trim() }
|
||||
for (const optionalField of ['feedbackType', 'contactInfo']) {
|
||||
if (!Object.prototype.hasOwnProperty.call(payload, optionalField)) continue
|
||||
if (typeof payload[optionalField] !== 'string') {
|
||||
throw new TypeError(`反馈字段 ${optionalField} 必须是字符串`)
|
||||
}
|
||||
const value = payload[optionalField].trim()
|
||||
if (value) normalized[optionalField] = value
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
const saveLogin = (loginResult) => {
|
||||
const token = loginResult?.token || loginResult?.accessToken || loginResult?.tokenValue
|
||||
const token = loginResult?.access_token
|
||||
if (!token) throw new Error('登录响应未包含会话令牌')
|
||||
session.saveToken(token)
|
||||
return loginResult
|
||||
@@ -55,6 +262,9 @@ const toTreeNode = (person, index = 0) => {
|
||||
const generation = Number(person.generationNo || person.generation || 1)
|
||||
return {
|
||||
...person,
|
||||
relatives: Array.isArray(person.relatives)
|
||||
? person.relatives.map((relative) => ({ ...relative }))
|
||||
: [],
|
||||
id: person.id || person.personId,
|
||||
name: person.personName || person.name || '未命名族人',
|
||||
relation: person.relation || (generation === 1 ? '始祖' : '族人'),
|
||||
@@ -67,38 +277,99 @@ const toTreeNode = (person, index = 0) => {
|
||||
}
|
||||
|
||||
export const appApi = {
|
||||
async sendSmsCode({ phone, validToken = '' }) {
|
||||
if (isMockMode()) return { success: true }
|
||||
return request({
|
||||
async getCaptchaRequirement({ sceneCode, subject }, requestOptions = {}) {
|
||||
requireRemoteAuth()
|
||||
return requestAuth({
|
||||
url: '/captcha/require',
|
||||
method: 'GET',
|
||||
data: authPayload({ sceneCode: assertAuthScene(sceneCode), subject })
|
||||
}, requestOptions)
|
||||
},
|
||||
async sendSmsCode({ sceneCode, phone, validToken }, requestOptions = {}) {
|
||||
requireRemoteAuth()
|
||||
return requestAuthVoid({
|
||||
url: '/genealogy/app/auth/sms/code',
|
||||
method: 'POST',
|
||||
data: authPayload({ grantType: 'sms', sceneCode: 'APP_LOGIN', phone, validToken })
|
||||
}, { authenticated: false })
|
||||
data: authPayload({
|
||||
grantType: 'sms',
|
||||
sceneCode: assertAuthScene(sceneCode),
|
||||
phone,
|
||||
validToken: assertValidToken(validToken)
|
||||
})
|
||||
}, requestOptions)
|
||||
},
|
||||
async loginWithPassword({ phone, passwordHash }) {
|
||||
if (isMockMode()) {
|
||||
session.saveToken('mock-session-token')
|
||||
return { token: 'mock-session-token', userId: currentUser.id }
|
||||
}
|
||||
const result = await request({
|
||||
async loginWithPassword({ phone, passwordHash }, requestOptions = {}) {
|
||||
requireRemoteAuth()
|
||||
const result = await requestAuth({
|
||||
url: '/genealogy/app/auth/login',
|
||||
method: 'POST',
|
||||
data: authPayload({ grantType: 'password', phone, password: passwordHash })
|
||||
}, { authenticated: false })
|
||||
data: authPayload({ grantType: 'password', phone, password: assertPasswordHash(passwordHash) })
|
||||
}, requestOptions)
|
||||
return saveLogin(result)
|
||||
},
|
||||
async loginWithSms({ phone, smsCode }) {
|
||||
if (isMockMode()) {
|
||||
session.saveToken('mock-session-token')
|
||||
return { token: 'mock-session-token', userId: currentUser.id }
|
||||
}
|
||||
const result = await request({
|
||||
async loginWithSms({ phone, smsCode }, requestOptions = {}) {
|
||||
requireRemoteAuth()
|
||||
const result = await requestAuth({
|
||||
url: '/genealogy/app/auth/login/sms',
|
||||
method: 'POST',
|
||||
data: authPayload({ grantType: 'sms', phone, smsCode })
|
||||
}, { authenticated: false })
|
||||
data: authPayload({ grantType: 'sms', phone, smsCode: assertSmsCode(smsCode) })
|
||||
}, requestOptions)
|
||||
return saveLogin(result)
|
||||
},
|
||||
async registerWithPassword({ phone, passwordHash, smsCode }, requestOptions = {}) {
|
||||
requireRemoteAuth()
|
||||
const result = await requestAuth({
|
||||
url: '/genealogy/app/auth/register',
|
||||
method: 'POST',
|
||||
data: authPayload({
|
||||
grantType: 'password',
|
||||
phone,
|
||||
password: assertPasswordHash(passwordHash),
|
||||
smsCode: assertSmsCode(smsCode)
|
||||
})
|
||||
}, requestOptions)
|
||||
return saveLogin(result)
|
||||
},
|
||||
async resetPassword({ phone, passwordHash, smsCode }, requestOptions = {}) {
|
||||
requireRemoteAuth()
|
||||
return requestAuthVoid({
|
||||
url: '/genealogy/app/auth/password/reset',
|
||||
method: 'PUT',
|
||||
data: authPayload({
|
||||
grantType: 'password',
|
||||
phone,
|
||||
newPassword: assertPasswordHash(passwordHash),
|
||||
smsCode: assertSmsCode(smsCode)
|
||||
})
|
||||
}, requestOptions)
|
||||
},
|
||||
async submitFeedback(payload, requestOptions = {}) {
|
||||
const data = normalizeFeedbackPayload(payload)
|
||||
if (!requestOptions || typeof requestOptions !== 'object' || Array.isArray(requestOptions) || Object.getPrototypeOf(requestOptions) !== Object.prototype) {
|
||||
throw new TypeError('反馈请求选项必须是普通对象')
|
||||
}
|
||||
const optionNames = Object.getOwnPropertyNames(requestOptions)
|
||||
const optionDescriptors = Object.getOwnPropertyDescriptors(requestOptions)
|
||||
if (
|
||||
Object.getOwnPropertySymbols(requestOptions).length > 0 ||
|
||||
optionNames.some((field) => field !== 'requestController') ||
|
||||
optionNames.some((field) => !Object.prototype.hasOwnProperty.call(optionDescriptors[field], 'value'))
|
||||
) {
|
||||
throw new TypeError('反馈请求选项包含未声明字段')
|
||||
}
|
||||
const runtimeMode = resolveRuntimeMode()
|
||||
if (runtimeMode !== 'remote') {
|
||||
throw createRequestError('当前为本地预览模式,反馈未提交服务器', 'WRITE_UNAVAILABLE')
|
||||
}
|
||||
return requestStrict({
|
||||
url: '/genealogy/app/feedback',
|
||||
method: 'POST',
|
||||
data
|
||||
}, {
|
||||
requireData: false,
|
||||
requestController: requestOptions.requestController ?? null
|
||||
})
|
||||
},
|
||||
async getProfile() {
|
||||
return hasRemoteConfig() ? request({ url: '/genealogy/app/auth/profile' }) : currentUser
|
||||
},
|
||||
@@ -107,7 +378,10 @@ export const appApi = {
|
||||
},
|
||||
async createGenealogy(payload) {
|
||||
if (hasRemoteConfig()) return request({ url: '/genealogy/app/genealogies', method: 'POST', data: payload })
|
||||
const created = { ...payload, id: Date.now(), memberCount: 0, activeCount: 0, motto: '敦亲睦族,敬祖传家。', visibility: '仅成员可见' }
|
||||
const accessPreset = Object.values(GENEALOGY_ACCESS_PRESET).includes(payload.accessPreset)
|
||||
? payload.accessPreset
|
||||
: GENEALOGY_ACCESS_PRESET.MEMBER_ONLY
|
||||
const created = { ...payload, id: String(Date.now()), memberCount: 0, activeCount: 0, motto: '敦亲睦族,敬祖传家。', accessPreset, membership: 'created' }
|
||||
genealogies.unshift(created)
|
||||
return created
|
||||
},
|
||||
@@ -122,14 +396,19 @@ export const appApi = {
|
||||
const tree = await request({ url: `/genealogy/app/genealogies/${genealogyId}/lineage/tree` })
|
||||
return (tree || []).map(toTreeNode)
|
||||
}
|
||||
return treeMembers.filter((item) => String(item.genealogyId || 1001) === String(genealogyId)).map(toTreeNode)
|
||||
return treeMembers
|
||||
.filter((item) => String(item.genealogyId) === String(genealogyId))
|
||||
.map(toTreeNode)
|
||||
},
|
||||
async getPerson(genealogyId, personId) {
|
||||
if (hasRemoteConfig()) {
|
||||
const person = await request({ url: `/genealogy/app/genealogies/${genealogyId}/lineage/persons/${personId}` })
|
||||
return toTreeNode(person)
|
||||
}
|
||||
const person = treeMembers.find((item) => String(item.id) === String(personId) && String(item.genealogyId || 1001) === String(genealogyId))
|
||||
const person = treeMembers.find(
|
||||
(item) => String(item.id) === String(personId) &&
|
||||
String(item.genealogyId) === String(genealogyId)
|
||||
)
|
||||
return person ? toTreeNode(person) : null
|
||||
},
|
||||
async createPerson(genealogyId, payload) {
|
||||
@@ -137,42 +416,36 @@ export const appApi = {
|
||||
const person = await request({ url: `/genealogy/app/genealogies/${genealogyId}/lineage/persons`, method: 'POST', data: payload })
|
||||
return toTreeNode(person)
|
||||
}
|
||||
const person = toTreeNode({
|
||||
...payload,
|
||||
id: Date.now(),
|
||||
genealogyId,
|
||||
relation: Number(payload.generationNo) === 1 ? '始祖' : '族人',
|
||||
x: 50,
|
||||
y: 7
|
||||
})
|
||||
treeMembers.push(person)
|
||||
const genealogy = genealogies.find((item) => String(item.id) === String(genealogyId))
|
||||
if (genealogy) {
|
||||
genealogy.memberCount = (genealogy.memberCount || 0) + 1
|
||||
genealogy.activeCount = (genealogy.activeCount || 0) + 1
|
||||
}
|
||||
return person
|
||||
const error = new Error('人物创建接口在本地预览模式不可用,当前内容不会保存')
|
||||
error.code = 'WRITE_UNAVAILABLE'
|
||||
throw error
|
||||
},
|
||||
async getFeeds(genealogyId) {
|
||||
return hasRemoteConfig() ? request({ url: `/genealogy/app/genealogies/${genealogyId}/feeds` }) : familyFeeds
|
||||
return hasRemoteConfig()
|
||||
? request({ url: `/genealogy/app/genealogies/${genealogyId}/feeds` })
|
||||
: listFamilyFeedFixtures(genealogyId)
|
||||
},
|
||||
async createFeed(genealogyId, payload) {
|
||||
if (hasRemoteConfig()) return request({ url: `/genealogy/app/genealogies/${genealogyId}/feeds`, method: 'POST', data: payload })
|
||||
const feed = { id: Date.now(), genealogyId, author: currentUser.name, title: '家族动态', content: payload.feedContent, date: '刚刚', type: '族务' }
|
||||
familyFeeds.unshift(feed)
|
||||
return feed
|
||||
const error = new Error('动态发布接口尚未接入,当前内容不会保存')
|
||||
error.code = 'WRITE_UNAVAILABLE'
|
||||
throw error
|
||||
},
|
||||
async getArticles(genealogyId) {
|
||||
return hasRemoteConfig() ? request({ url: `/genealogy/app/genealogies/${genealogyId}/articles` }) : familyContent.article
|
||||
return hasRemoteConfig()
|
||||
? request({ url: `/genealogy/app/genealogies/${genealogyId}/articles` })
|
||||
: listFamilyArticleFixtures(genealogyId)
|
||||
},
|
||||
async getAlbums(genealogyId) {
|
||||
return hasRemoteConfig() ? request({ url: `/genealogy/app/genealogies/${genealogyId}/albums` }) : familyContent.album
|
||||
return hasRemoteConfig()
|
||||
? request({ url: `/genealogy/app/genealogies/${genealogyId}/albums` })
|
||||
: listFamilyAlbumFixtures(genealogyId)
|
||||
},
|
||||
async getCeremonies(genealogyId) {
|
||||
return hasRemoteConfig() ? request({ url: `/genealogy/app/genealogies/${genealogyId}/ceremonies` }) : familyContent.ceremony
|
||||
return hasRemoteConfig() ? request({ url: `/genealogy/app/genealogies/${genealogyId}/ceremonies` }) : listCeremonyFixtures(genealogyId)
|
||||
},
|
||||
async getGrowthRecords(genealogyId) {
|
||||
return hasRemoteConfig() ? request({ url: `/genealogy/app/genealogies/${genealogyId}/growth-records` }) : familyContent.record
|
||||
return hasRemoteConfig() ? request({ url: `/genealogy/app/genealogies/${genealogyId}/growth-records` }) : listGrowthRecordFixtures(genealogyId)
|
||||
},
|
||||
async getContent(type, genealogyId) {
|
||||
const loaders = { article: this.getArticles, album: this.getAlbums, ceremony: this.getCeremonies, record: this.getGrowthRecords }
|
||||
@@ -180,7 +453,7 @@ export const appApi = {
|
||||
return loaders[type].call(this, genealogyId)
|
||||
},
|
||||
async getNotifications() {
|
||||
return hasRemoteConfig() ? request({ url: '/genealogy/app/notifications' }) : notifications
|
||||
return hasRemoteConfig() ? request({ url: '/genealogy/app/notifications' }) : listNotificationFixtures()
|
||||
},
|
||||
async markNotificationRead(notificationId) {
|
||||
if (hasRemoteConfig()) return request({ url: `/genealogy/app/notifications/${notificationId}/read`, method: 'POST' })
|
||||
@@ -194,16 +467,17 @@ export const appApi = {
|
||||
return { success: true }
|
||||
},
|
||||
async getPublicGenealogies() {
|
||||
return hasRemoteConfig() ? request({ url: '/genealogy/app/genealogies/public' }) : genealogies.filter((item) => item.visibility === '公开可申请')
|
||||
return hasRemoteConfig() ? request({ url: '/genealogy/app/genealogies/public' }) : publicGenealogies
|
||||
},
|
||||
async applyToJoin(genealogyId, payload) {
|
||||
if (hasRemoteConfig()) return request({ url: `/genealogy/app/genealogies/${genealogyId}/join-applies`, method: 'POST', data: payload })
|
||||
joinApplications.unshift({
|
||||
id: Date.now(),
|
||||
id: String(Date.now()),
|
||||
genealogyId,
|
||||
name: currentUser.name,
|
||||
phone: currentUser.phone,
|
||||
relation: payload.message || '申请加入家谱',
|
||||
name: payload.applicantName || currentUser.name,
|
||||
phone: payload.phone || currentUser.phone,
|
||||
relation: payload.relationDesc || '关系待补充',
|
||||
reason: payload.applyReason || '',
|
||||
appliedAt: '刚刚',
|
||||
status: 'PENDING'
|
||||
})
|
||||
@@ -212,7 +486,13 @@ export const appApi = {
|
||||
async getPendingApplications(genealogyId) {
|
||||
return hasRemoteConfig() ? request({ url: `/genealogy/app/genealogies/${genealogyId}/join-applies/pending` }) : joinApplications.filter((item) => String(item.genealogyId || 1001) === String(genealogyId))
|
||||
},
|
||||
async auditApplication(genealogyId, applicationId, approved) {
|
||||
return hasRemoteConfig() ? request({ url: `/genealogy/app/genealogies/${genealogyId}/join-applies/${applicationId}/audit`, method: 'PUT', data: { approved } }) : { success: true }
|
||||
async auditApplication(genealogyId, applicationId, { status, auditRemark = '' }) {
|
||||
if (typeof status !== 'string' || !/^[12]$/.test(status)) throw new TypeError('审核状态只能是 1 或 2')
|
||||
if (typeof auditRemark !== 'string') throw new TypeError('审核备注必须是字符串')
|
||||
if (Array.from(auditRemark).length > 500) throw new RangeError('审核备注不能超过 500 个字符')
|
||||
if (hasRemoteConfig()) return request({ url: `/genealogy/app/genealogies/${genealogyId}/join-applies/${applicationId}/audit`, method: 'PUT', data: { status, auditRemark } })
|
||||
const application = joinApplications.find((item) => String(item.id) === String(applicationId))
|
||||
if (application) Object.assign(application, { status, auditRemark })
|
||||
return { success: true }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user