useSettingsApi.ts 8.25 KB
import type { Member, MemberProfileForm, SettingsMutationResponse, SettingsNotifications, SettingsProfile } from '~/types'
import { createSharedComposable } from '@vueuse/core'

const DEFAULT_PROFILE: SettingsProfile = {
  name: '',
  email: '',
  username: '',
  avatar: undefined,
  bio: undefined
}

const DEFAULT_NOTIFICATIONS: SettingsNotifications = {
  email: true,
  desktop: false,
  product_updates: true,
  weekly_digest: false,
  important_updates: true
}

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === 'object' && value !== null
}

function pickMessage(payload: unknown, fallback: string) {
  if (!isRecord(payload)) {
    return fallback
  }

  if (typeof payload.message === 'string' && payload.message.trim().length > 0) {
    return payload.message
  }

  if (typeof payload.msg === 'string' && payload.msg.trim().length > 0) {
    return payload.msg
  }

  if (typeof payload.error === 'string' && payload.error.trim().length > 0) {
    return payload.error
  }

  return fallback
}

function pickErrorCode(payload: unknown): string | null {
  if (!isRecord(payload)) {
    return null
  }

  if (typeof payload.errorCode === 'string') {
    return payload.errorCode
  }

  if (typeof payload.code === 'string') {
    return payload.code
  }

  if (typeof payload.code === 'number') {
    return String(payload.code)
  }

  return null
}

function extractData<T>(payload: unknown): T | null {
  if (Array.isArray(payload)) {
    return payload as T
  }

  if (!isRecord(payload)) {
    return null
  }

  if ('data' in payload) {
    return payload.data as T
  }

  if ('result' in payload) {
    return payload.result as T
  }

  if ('payload' in payload) {
    return payload.payload as T
  }

  return payload as T
}

function normalizeMutationPayload(payload: unknown, defaultMessage: string): SettingsMutationResponse {
  if (isRecord(payload) && typeof payload.success === 'boolean') {
    return {
      success: payload.success,
      errorCode: pickErrorCode(payload),
      message: pickMessage(payload, defaultMessage)
    }
  }

  if (isRecord(payload) && typeof payload.code !== 'undefined') {
    const code = String(payload.code)
    const success = code === '0' || code === '200'

    return {
      success,
      errorCode: success ? null : code,
      message: pickMessage(payload, defaultMessage)
    }
  }

  return {
    success: true,
    errorCode: null,
    message: defaultMessage
  }
}

function normalizeMutationError(error: unknown): SettingsMutationResponse {
  if (isRecord(error) && 'data' in error) {
    const payload = normalizeMutationPayload(error.data, '请求失败,请稍后重试。')
    if (!payload.success) {
      return payload
    }
  }

  return {
    success: false,
    errorCode: 'REQUEST_FAILED',
    message: '请求失败,请稍后重试。'
  }
}

function normalizeProfile(payload: unknown): SettingsProfile {
  const extracted = extractData<SettingsProfile>(payload)

  if (!isRecord(extracted)) {
    return { ...DEFAULT_PROFILE }
  }

  return {
    name: typeof extracted.name === 'string' ? extracted.name : DEFAULT_PROFILE.name,
    email: typeof extracted.email === 'string' ? extracted.email : DEFAULT_PROFILE.email,
    username: typeof extracted.username === 'string' ? extracted.username : DEFAULT_PROFILE.username,
    avatar: typeof extracted.avatar === 'string' ? extracted.avatar : undefined,
    bio: typeof extracted.bio === 'string' ? extracted.bio : undefined
  }
}

function normalizeNotifications(payload: unknown): SettingsNotifications {
  const extracted = extractData<SettingsNotifications>(payload)

  if (!isRecord(extracted)) {
    return { ...DEFAULT_NOTIFICATIONS }
  }

  return {
    email: typeof extracted.email === 'boolean' ? extracted.email : DEFAULT_NOTIFICATIONS.email,
    desktop: typeof extracted.desktop === 'boolean' ? extracted.desktop : DEFAULT_NOTIFICATIONS.desktop,
    product_updates: typeof extracted.product_updates === 'boolean' ? extracted.product_updates : DEFAULT_NOTIFICATIONS.product_updates,
    weekly_digest: typeof extracted.weekly_digest === 'boolean' ? extracted.weekly_digest : DEFAULT_NOTIFICATIONS.weekly_digest,
    important_updates: typeof extracted.important_updates === 'boolean' ? extracted.important_updates : DEFAULT_NOTIFICATIONS.important_updates
  }
}

function normalizeRole(role: unknown): Member['role'] {
  if (role === 'member' || role === 'admin' || role === 'customer') {
    return role
  }

  if (role === 'owner') {
    return 'admin'
  }

  return 'member'
}

function normalizeMembers(payload: unknown): Member[] {
  const extracted = extractData<Member[]>(payload)

  if (!Array.isArray(extracted)) {
    return []
  }

  return extracted
    .filter(isRecord)
    .map((member) => {
      const avatar = isRecord(member.avatar) ? member.avatar : {}

      return {
        name: typeof member.name === 'string' ? member.name : '',
        email: typeof member.email === 'string' ? member.email : undefined,
        username: typeof member.username === 'string' ? member.username : '',
        role: normalizeRole(member.role),
        bio: typeof member.bio === 'string' ? member.bio : undefined,
        avatar: {
          src: typeof avatar.src === 'string' ? avatar.src : undefined,
          alt: typeof avatar.alt === 'string' ? avatar.alt : undefined
        }
      } satisfies Member
    })
    .filter(member => member.name.length > 0 && member.username.length > 0)
}

type ProfileUpdateResponse = SettingsMutationResponse & {
  profile?: SettingsProfile
}

const _useSettingsApi = () => {
  const api = useApiGateway()

  async function mutate(path: string, method: 'POST' | 'PUT' | 'PATCH' | 'DELETE', body?: object, successMessage = '操作成功。') {
    try {
      const result = await api.request<unknown>(`/api/settings${path}`, {
        method,
        body,
        remotePath: `/settings${path}`
      })

      return normalizeMutationPayload(result, successMessage)
    } catch (error) {
      return normalizeMutationError(error)
    }
  }

  const getProfile = () => {
    return api.useApiFetch<SettingsProfile>('/api/settings/profile', {
      remotePath: '/settings/profile',
      transform: normalizeProfile,
      default: () => ({ ...DEFAULT_PROFILE })
    })
  }

  const updateProfile = async (payload: SettingsProfile): Promise<ProfileUpdateResponse> => {
    const result = await mutate('/profile', 'PUT', payload, '个人资料已保存。')

    if (!result.success) {
      return result
    }

    return {
      ...result,
      profile: payload
    }
  }

  const getNotifications = () => {
    return api.useApiFetch<SettingsNotifications>('/api/settings/notifications', {
      remotePath: '/settings/notifications',
      transform: normalizeNotifications,
      default: () => ({ ...DEFAULT_NOTIFICATIONS })
    })
  }

  const updateNotifications = (payload: SettingsNotifications) => {
    return mutate('/notifications', 'PUT', payload, '通知设置已保存。')
  }

  const updatePassword = (payload: { current: string, new: string }) => {
    return mutate('/security/password', 'POST', payload, '密码已更新。')
  }

  const deleteAccount = () => {
    return mutate('/security/account', 'DELETE', undefined, '账号已删除。')
  }

  const getMembers = () => {
    return api.useApiFetch<Member[]>('/api/settings/members', {
      remotePath: '/settings/members',
      transform: normalizeMembers,
      default: () => []
    })
  }

  const inviteMember = (payload: MemberProfileForm) => {
    return mutate('/members', 'POST', payload, '成员邀请已发送。')
  }

  const updateMemberProfile = (username: string, payload: MemberProfileForm) => {
    return mutate(`/members/${username}/profile`, 'PUT', payload, '成员资料已更新。')
  }

  const updateMemberRole = (username: string, role: Member['role']) => {
    return mutate(`/members/${username}`, 'PATCH', { role }, '成员角色已更新。')
  }

  const removeMember = (username: string) => {
    return mutate(`/members/${username}`, 'DELETE', undefined, '成员已移除。')
  }

  return {
    getProfile,
    updateProfile,
    getNotifications,
    updateNotifications,
    updatePassword,
    deleteAccount,
    getMembers,
    inviteMember,
    updateMemberProfile,
    updateMemberRole,
    removeMember
  }
}

export const useSettingsApi = createSharedComposable(_useSettingsApi)