chore: seed migration objects batch 1
This commit is contained in:
Vendored
+11
@@ -0,0 +1,11 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
import type { MessageApi } from 'naive-ui';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
$message: MessageApi;
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
@@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<main class="m-app" :class="{ 'm-app--with-nav': showNav }">
|
||||
<router-view />
|
||||
<nav v-if="showNav" class="m-bottom-nav" aria-label="主要导航">
|
||||
<router-link v-for="item in nav" :key="item.path" :to="item.path" :aria-label="item.label">
|
||||
<img :src="isActive(item.path) ? item.activeIcon : item.icon" alt="" />
|
||||
<span>{{ item.label }}</span>
|
||||
</router-link>
|
||||
</nav>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, watch } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { hasSession } from './api';
|
||||
import { mobileAssets } from './assets';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const showNav = computed(() => Boolean(route.meta.mobileNav));
|
||||
const nav = [
|
||||
{
|
||||
label: '首页',
|
||||
path: '/app/home',
|
||||
icon: mobileAssets.navHome,
|
||||
activeIcon: mobileAssets.navHomeActive,
|
||||
},
|
||||
{
|
||||
label: '记录',
|
||||
path: '/app/orders',
|
||||
icon: mobileAssets.navOrders,
|
||||
activeIcon: mobileAssets.navOrdersActive,
|
||||
},
|
||||
{
|
||||
label: '领取',
|
||||
path: '/app/claim',
|
||||
icon: mobileAssets.navClaimActive,
|
||||
activeIcon: mobileAssets.navClaimActive,
|
||||
},
|
||||
{
|
||||
label: '客服',
|
||||
path: '/app/support',
|
||||
icon: mobileAssets.navSupport,
|
||||
activeIcon: mobileAssets.navSupportActive,
|
||||
},
|
||||
{
|
||||
label: '我的',
|
||||
path: '/app/profile',
|
||||
icon: mobileAssets.navProfile,
|
||||
activeIcon: mobileAssets.navProfileActive,
|
||||
},
|
||||
];
|
||||
const isActive = (path: string) => route.path === path || route.path.startsWith(`${path}/`);
|
||||
|
||||
function ensureAccess() {
|
||||
const publicContent = [
|
||||
'/app/content/user_agreement',
|
||||
'/app/content/privacy_policy',
|
||||
'/app/content/platform_intro',
|
||||
];
|
||||
const publicPassword = ['/app/password/verify', '/app/password/reset'];
|
||||
if (
|
||||
!hasSession() &&
|
||||
!publicContent.includes(route.path) &&
|
||||
!publicPassword.includes(route.path)
|
||||
) {
|
||||
router.replace({ path: '/app/auth', query: { redirect: route.fullPath } });
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => route.path, ensureAccess, { immediate: true });
|
||||
</script>
|
||||
@@ -0,0 +1,188 @@
|
||||
import axios, { AxiosError, AxiosRequestConfig } from 'axios';
|
||||
|
||||
const TOKEN_KEY = 'olons_web_session';
|
||||
const DEVICE_KEY = 'olons_web_device';
|
||||
|
||||
const reasonMessages: Record<string, string> = {
|
||||
AUTH_REQUIRED: '登录已失效,请重新登录',
|
||||
FORBIDDEN: '当前账户无权执行此操作',
|
||||
INVALID_CREDENTIALS: '手机号或密码错误',
|
||||
ACCOUNT_ALREADY_REGISTERED: '该账户或手机号已经注册',
|
||||
RATE_LIMITED: '操作过于频繁,请稍后再试',
|
||||
TASK_DISABLED: '领取功能暂未开放',
|
||||
OUTSIDE_BUSINESS_HOURS: '当前不在营业时间内',
|
||||
INSUFFICIENT_BALANCE: '可用余额不足',
|
||||
DAILY_LIMIT_REACHED: '今日领取次数已用完',
|
||||
PENDING_ORDER_EXISTS: '存在未完成的任务,请先处理',
|
||||
NO_TASK_AVAILABLE: '当前暂无可领取任务',
|
||||
ORDER_EXPIRED: '该任务已超时',
|
||||
ORDER_STATE_INVALID: '当前任务状态不允许此操作',
|
||||
TRADE_PASSWORD_NOT_SET: '请先设置支付密码',
|
||||
TRADE_PASSWORD_INVALID: '支付密码不正确',
|
||||
PAYMENT_ACCOUNT_INVALID: '收款账户未核验或不可用',
|
||||
WITHDRAWAL_DISABLED: '取出功能暂未开放',
|
||||
WITHDRAWAL_AMOUNT_INVALID: '取出金额不符合当前规则',
|
||||
WITHDRAWAL_DAILY_LIMIT_REACHED: '今日取出次数已用完',
|
||||
WITHDRAWAL_CHANNEL_UNAVAILABLE: '当前没有可用的取出通道',
|
||||
RECHARGE_AMOUNT_INVALID: '储入金额不符合当前通道规则',
|
||||
RECHARGE_CHANNEL_UNAVAILABLE: '当前没有可用的储入通道',
|
||||
RECHARGE_VOUCHER_INVALID: '储入凭证无效',
|
||||
SERVICE_UNAVAILABLE: '服务暂不可用,请稍后重试',
|
||||
INTERNAL: '服务异常,请稍后重试',
|
||||
};
|
||||
|
||||
export interface ApiEnvelope<T> {
|
||||
code: number;
|
||||
data: T;
|
||||
message?: string;
|
||||
reason?: string;
|
||||
request_id?: string;
|
||||
server_time?: string;
|
||||
}
|
||||
|
||||
export class MobileApiError extends Error {
|
||||
reason: string;
|
||||
status: number;
|
||||
|
||||
constructor(message: string, reason = '', status = 0) {
|
||||
super(message);
|
||||
this.reason = reason;
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
|
||||
const client = axios.create({
|
||||
baseURL: '',
|
||||
timeout: 15000,
|
||||
headers: { Accept: 'application/json' },
|
||||
});
|
||||
|
||||
client.interceptors.request.use((config) => {
|
||||
const token = sessionStorage.getItem(TOKEN_KEY);
|
||||
if (token) config.headers.Authorization = `Bearer ${token}`;
|
||||
config.headers['X-Request-ID'] = crypto.randomUUID?.() || `${Date.now()}-${Math.random()}`;
|
||||
config.headers['X-Device-ID'] = getDeviceID();
|
||||
return config;
|
||||
});
|
||||
|
||||
client.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error: AxiosError<ApiEnvelope<unknown>>) => {
|
||||
const status = error.response?.status || 0;
|
||||
const payload = error.response?.data;
|
||||
if (status === 401) {
|
||||
clearSession();
|
||||
const currentPath = window.location.hash.replace(/^#/, '') || '/app/home';
|
||||
if (!currentPath.startsWith('/app/auth')) {
|
||||
window.location.hash = `/app/auth?redirect=${encodeURIComponent(currentPath)}`;
|
||||
}
|
||||
}
|
||||
const reason = payload?.reason || '';
|
||||
return Promise.reject(
|
||||
new MobileApiError(
|
||||
reasonMessages[reason] ||
|
||||
payload?.message ||
|
||||
(status ? '请求失败,请稍后重试' : '网络连接失败'),
|
||||
reason,
|
||||
status
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export async function request<T>(config: AxiosRequestConfig): Promise<T> {
|
||||
const method = String(config.method || 'GET').toUpperCase();
|
||||
const attempts = method === 'GET' ? 3 : 1;
|
||||
let lastError: unknown;
|
||||
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
||||
try {
|
||||
const response = await client.request<ApiEnvelope<T>>(config);
|
||||
if (response.data.code !== 0) {
|
||||
const reason = response.data.reason || '';
|
||||
throw new MobileApiError(
|
||||
reasonMessages[reason] || response.data.message || '请求失败',
|
||||
reason,
|
||||
response.status
|
||||
);
|
||||
}
|
||||
return response.data.data;
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
if (!shouldRetry(error, attempt, attempts)) throw error;
|
||||
await new Promise((resolve) => window.setTimeout(resolve, attempt === 0 ? 250 : 600));
|
||||
}
|
||||
}
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
function shouldRetry(error: unknown, attempt: number, attempts: number) {
|
||||
if (attempt >= attempts - 1) return false;
|
||||
if (axios.isCancel(error)) return false;
|
||||
return error instanceof MobileApiError && (error.status === 0 || error.status === 503);
|
||||
}
|
||||
|
||||
function getDeviceID() {
|
||||
let value = localStorage.getItem(DEVICE_KEY);
|
||||
if (!value) {
|
||||
value = crypto.randomUUID?.() || `web-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
||||
localStorage.setItem(DEVICE_KEY, value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function saveSession(token: string) {
|
||||
sessionStorage.setItem(TOKEN_KEY, token);
|
||||
}
|
||||
|
||||
export function clearSession() {
|
||||
sessionStorage.removeItem(TOKEN_KEY);
|
||||
}
|
||||
|
||||
export function hasSession() {
|
||||
return Boolean(sessionStorage.getItem(TOKEN_KEY));
|
||||
}
|
||||
|
||||
export function idempotencyKey() {
|
||||
return crypto.randomUUID?.() || `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
||||
}
|
||||
|
||||
export function money(value?: number, currency = 'CNY') {
|
||||
const amount = Number.isFinite(value) ? Number(value) / 100 : 0;
|
||||
return new Intl.NumberFormat('zh-CN', {
|
||||
style: 'currency',
|
||||
currency,
|
||||
minimumFractionDigits: 2,
|
||||
}).format(amount);
|
||||
}
|
||||
|
||||
export function dateTime(value?: string) {
|
||||
if (!value) return '--';
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return '--';
|
||||
return new Intl.DateTimeFormat('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false,
|
||||
})
|
||||
.format(date)
|
||||
.replaceAll('/', '-');
|
||||
}
|
||||
|
||||
export function safeImage(url?: string) {
|
||||
if (!url) return '';
|
||||
try {
|
||||
const parsed = new URL(url, window.location.origin);
|
||||
return parsed.protocol === 'https:' || parsed.origin === window.location.origin
|
||||
? parsed.href
|
||||
: '';
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
export function errorText(error: unknown) {
|
||||
return error instanceof Error ? error.message : '操作失败,请稍后重试';
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
import homeHero from './assets/home-hero.png';
|
||||
import homeBalance from './assets/home-balance.png';
|
||||
import shortcutClaim from './assets/shortcut-claim.png';
|
||||
import shortcutRecharge from './assets/shortcut-recharge.png';
|
||||
import shortcutWithdraw from './assets/shortcut-withdraw.png';
|
||||
import shortcutMall from './assets/shortcut-mall.png';
|
||||
import navHome from './assets/nav-home.png';
|
||||
import navHomeActive from './assets/nav-home-active.png';
|
||||
import navOrders from './assets/nav-orders.png';
|
||||
import navOrdersActive from './assets/nav-orders-active.png';
|
||||
import navClaimActive from './assets/nav-claim-active.png';
|
||||
import navSupport from './assets/nav-support.png';
|
||||
import navSupportActive from './assets/nav-support-active.png';
|
||||
import navProfile from './assets/nav-profile.png';
|
||||
import navProfileActive from './assets/nav-profile-active.png';
|
||||
import profileBackground from './assets/profile-bg.png';
|
||||
import profileWallet from './assets/profile-wallet.png';
|
||||
import menuProfile from './assets/menu-profile.png';
|
||||
import menuRecharge from './assets/menu-recharge.png';
|
||||
import menuWithdraw from './assets/menu-withdraw.png';
|
||||
import menuLedger from './assets/menu-ledger.png';
|
||||
import menuNotices from './assets/menu-notices.png';
|
||||
import menuLogout from './assets/menu-logout.png';
|
||||
import copy from './assets/copy.png';
|
||||
import supportHero from './assets/support-hero.png';
|
||||
import back from './assets/back.png';
|
||||
import claimOrbit from './assets/claim-orbit.png';
|
||||
import claimGift from './assets/claim-gift.png';
|
||||
import claimButton from './assets/claim-button.png';
|
||||
import processSimple from './assets/process-simple.png';
|
||||
import processMaterial from './assets/process-material.png';
|
||||
import processTime from './assets/process-time.png';
|
||||
import processReturn from './assets/process-return.png';
|
||||
import chatVoice from './assets/chat-voice.png';
|
||||
import chatMore from './assets/chat-more.png';
|
||||
import companyPlatform from './assets/company-platform.png';
|
||||
import companyRules from './assets/company-rules.png';
|
||||
import companyQualification from './assets/company-qualification.png';
|
||||
import companyAgency from './assets/company-agency.png';
|
||||
import avatarEdit from './assets/avatar-edit.png';
|
||||
import rechargeRecord from './assets/recharge-record.png';
|
||||
import withdrawalRecord from './assets/withdrawal-record.png';
|
||||
import loadingOrbit from './assets/loading-orbit.png';
|
||||
import agencyHero from './assets/agency-hero.png';
|
||||
import agencyBackground from './assets/agency-bg.png';
|
||||
import agencyPersonPrimary from './assets/agency-person-primary.png';
|
||||
import agencyPersonSecondary from './assets/agency-person-secondary.png';
|
||||
import agencyPersonTertiary from './assets/agency-person-tertiary.png';
|
||||
import rulesBackground from './assets/rules-bg.png';
|
||||
import rulesBanner from './assets/rules-banner.png';
|
||||
import levelSilver from './assets/level-silver.png';
|
||||
import levelGold from './assets/level-gold.png';
|
||||
import levelPlatinum from './assets/level-platinum.png';
|
||||
import levelDiamond from './assets/level-diamond.png';
|
||||
import levelStar from './assets/level-star.png';
|
||||
import levelCrown from './assets/level-crown.png';
|
||||
import claimMachine from './assets/claim-machine.png';
|
||||
import claimAutoButton from './assets/claim-auto-button.png';
|
||||
import claimTotal from './assets/claim-total.png';
|
||||
import claimYesterday from './assets/claim-yesterday.png';
|
||||
import claimToday from './assets/claim-today.png';
|
||||
import claimCount from './assets/claim-count.png';
|
||||
import claimSpinner from './assets/claim-spinner.png';
|
||||
import claimMatchingGift from './assets/claim-matching-gift.png';
|
||||
import claimSuccess from './assets/claim-success.png';
|
||||
import claimFailed from './assets/claim-failed.png';
|
||||
import claimCancelled from './assets/claim-cancelled.png';
|
||||
import mallBanner from './assets/mall-banner.png';
|
||||
import mallBackground from './assets/mall-bg.png';
|
||||
import rankFirst from './assets/rank-first.png';
|
||||
import rankSecond from './assets/rank-second.png';
|
||||
import rankThird from './assets/rank-third.png';
|
||||
import rankBackground from './assets/rank-bg.png';
|
||||
import qualificationOperation from './assets/qualification-operation.png';
|
||||
import qualificationDevelopment from './assets/qualification-development.png';
|
||||
|
||||
export const mobileAssets = {
|
||||
homeHero,
|
||||
homeBalance,
|
||||
shortcutClaim,
|
||||
shortcutRecharge,
|
||||
shortcutWithdraw,
|
||||
shortcutMall,
|
||||
navHome,
|
||||
navHomeActive,
|
||||
navOrders,
|
||||
navOrdersActive,
|
||||
navClaimActive,
|
||||
navSupport,
|
||||
navSupportActive,
|
||||
navProfile,
|
||||
navProfileActive,
|
||||
profileBackground,
|
||||
profileWallet,
|
||||
menuProfile,
|
||||
menuRecharge,
|
||||
menuWithdraw,
|
||||
menuLedger,
|
||||
menuNotices,
|
||||
menuLogout,
|
||||
copy,
|
||||
supportHero,
|
||||
back,
|
||||
claimOrbit,
|
||||
claimGift,
|
||||
claimButton,
|
||||
processSimple,
|
||||
processMaterial,
|
||||
processTime,
|
||||
processReturn,
|
||||
chatVoice,
|
||||
chatMore,
|
||||
companyPlatform,
|
||||
companyRules,
|
||||
companyQualification,
|
||||
companyAgency,
|
||||
avatarEdit,
|
||||
rechargeRecord,
|
||||
withdrawalRecord,
|
||||
loadingOrbit,
|
||||
agencyHero,
|
||||
agencyBackground,
|
||||
agencyPersonPrimary,
|
||||
agencyPersonSecondary,
|
||||
agencyPersonTertiary,
|
||||
rulesBackground,
|
||||
rulesBanner,
|
||||
levelSilver,
|
||||
levelGold,
|
||||
levelPlatinum,
|
||||
levelDiamond,
|
||||
levelStar,
|
||||
levelCrown,
|
||||
claimMachine,
|
||||
claimAutoButton,
|
||||
claimTotal,
|
||||
claimYesterday,
|
||||
claimToday,
|
||||
claimCount,
|
||||
claimSpinner,
|
||||
claimMatchingGift,
|
||||
claimSuccess,
|
||||
claimFailed,
|
||||
claimCancelled,
|
||||
mallBanner,
|
||||
mallBackground,
|
||||
rankFirst,
|
||||
rankSecond,
|
||||
rankThird,
|
||||
rankBackground,
|
||||
qualificationOperation,
|
||||
qualificationDevelopment,
|
||||
};
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 939 KiB |
Reference in New Issue
Block a user