feat: 增加环境切换

This commit is contained in:
R524809
2026-10-19 17:49:19 +08:00
parent 997042789d
commit fe1074e3f0
21 changed files with 968 additions and 123 deletions
+73 -24
View File
@@ -2,11 +2,13 @@ import { fetchPageConfig } from './services/site-api';
import { normalizePreviewImages } from './config/assets';
import { defaultPrintConfig } from './config/config';
import { getUser } from './utils/auth';
import { apiContextKey, getApiConfig, isCurrentApiConfig } from './config/api';
import { generateUUID, getAppUUID, setAppUUID } from './utils/uuid';
let _refreshGeneration = 0;
let _loadGeneration = 0;
const STORAGE_KEY_PAGE_CONFIG = 'pageConfig';
const pageConfigStorageKey = () => `doodle_page_config_v2:${apiContextKey()}`;
// Pending resolvers for getPageConfig() callers waiting on config
let _configResolvers: Array<(config: PageConfigData | null) => void> = [];
@@ -65,6 +67,23 @@ App<IAppOption>({
onHide() {},
/** 切环境不重启 App:作废旧加载、释放等待者并重置共享业务状态。 */
resetApiState() {
_refreshGeneration++;
_loadGeneration++;
this.globalData.user = null;
this.globalData.pageConfig = undefined;
this.globalData.pageConfigReady = false;
this.globalData.pendingCategoryTabId = undefined;
for (const resolve of _configResolvers) resolve(null);
_configResolvers = [];
try {
wx.removeStorageSync('favorites_active_tab');
} catch {
/* 临时页签偏好清理失败不影响环境与用户隔离。 */
}
},
getPrintConfig(): PrintConfig {
return this.globalData.printConfig || defaultPrintConfig;
},
@@ -74,32 +93,54 @@ App<IAppOption>({
wx.setStorageSync('printConfig', config);
},
async loadPageConfig() {
async loadPageConfig(forceRefresh = false) {
let context: ReturnType<typeof getApiConfig>;
try {
context = getApiConfig();
} catch {
// 地址被移除时结束首页等待,开发者仍可进入 Debug 改选可用环境。
this.globalData.pageConfig = undefined;
this.globalData.pageConfigReady = true;
for (const resolve of _configResolvers) resolve(null);
_configResolvers = [];
return false;
}
const generation = ++_loadGeneration;
const current = () =>
generation === _loadGeneration && isCurrentApiConfig(context);
const storageKey = pageConfigStorageKey();
let config: PageConfigData | null = null;
// L1: 数据预拉取
try {
const res = await new Promise<{ fetchedData: string }>(
(resolve, reject) => {
wx.getBackgroundFetchData({
fetchType: 'pre',
success: resolve as any,
fail: reject,
});
},
);
if (res.fetchedData) {
const parsed = JSON.parse(res.fetchedData);
config = parsed?.data || parsed;
// 仅开发版跳过旧线上预拉取,保证环境切换后的内容来自目标服务。
// 体验/正式版继续沿用原预拉取模式、地址与旧云端任务。
if (context.version !== 'develop')
try {
const res = await new Promise<{ fetchedData: string }>(
(resolve, reject) => {
wx.getBackgroundFetchData({
fetchType: 'pre',
success: resolve as any,
fail: reject,
});
},
);
if (res.fetchedData) {
const parsed = JSON.parse(res.fetchedData);
config = parsed?.data || parsed;
}
} catch {
// 预拉取失败,继续降级
}
} catch {
// 预拉取失败,继续降级
}
if (!current()) return false;
// L2: storage 缓存
if (!config) {
if (!config && !forceRefresh) {
try {
const cached = wx.getStorageSync(STORAGE_KEY_PAGE_CONFIG);
const cached =
wx.getStorageSync(storageKey) ||
(context.version !== 'develop'
? wx.getStorageSync('pageConfig')
: null);
if (cached && typeof cached === 'object') {
config = cached;
}
@@ -117,6 +158,7 @@ App<IAppOption>({
/* 新后端不可用时继续展示空状态,本地生成仍可使用。 */
}
}
if (!current()) return false;
// 三种数据入口都先转换预览图,旧缓存会随成功加载写回新地址。
config = normalizePreviewImages(config);
@@ -126,7 +168,7 @@ App<IAppOption>({
this.globalData.pageConfig = config;
// 成功获取后写入 storage 缓存
try {
wx.setStorageSync(STORAGE_KEY_PAGE_CONFIG, config);
wx.setStorageSync(storageKey, config);
} catch {
// storage 写入失败不影响主流程
}
@@ -140,16 +182,23 @@ App<IAppOption>({
_configResolvers = [];
// 预拉取/缓存先渲染,再用新后端刷新,避免旧静态配置长期覆盖管理修改。
if (hadCachedConfig) void this.refreshPageConfig();
return !!config;
},
async refreshPageConfig() {
const context = getApiConfig();
const storageKey = pageConfigStorageKey();
const generation = ++_refreshGeneration;
try {
const config = normalizePreviewImages(await fetchPageConfig());
if (generation !== _refreshGeneration) return;
if (
generation !== _refreshGeneration ||
!isCurrentApiConfig(context)
)
return;
this.globalData.pageConfig = config;
try {
wx.setStorageSync(STORAGE_KEY_PAGE_CONFIG, config);
wx.setStorageSync(storageKey, config);
} catch {
/* 缓存失败不影响当前数据。 */
}
+20 -2
View File
@@ -66,13 +66,21 @@
margin-top: 4rpx;
}
.nav-bar__title-group {
display: flex;
align-items: center;
gap: 12rpx;
flex: 1;
min-width: 0;
}
.nav-bar__title {
font-size: @fs-nav-title;
font-weight: @fw-nav-title;
color: @color-nav-title;
letter-spacing: @ls-nav-title;
line-height: 1;
flex: 1;
flex: 0 1 auto;
min-width: 0;
}
@@ -86,4 +94,14 @@
.nav-bar__placeholder {
width: 100%;
flex-shrink: 0;
}
}
.nav-bar__environment {
margin-right: 16rpx;
padding: 6rpx 12rpx;
border-radius: 12rpx;
background: @brand;
color: @text-title;
font-size: 20rpx;
line-height: 1.2;
flex-shrink: 0;
}
@@ -1,4 +1,5 @@
import { getNavInnerPx } from '../../utils/navMetrics';
import { getApiEnvironment } from '../../config/api';
const HOME_TAB_URL = '/pages/home/home';
const HOME_ROUTE = 'pages/home/home';
@@ -24,6 +25,7 @@ Component({
leftActionType: 'none' as 'none' | 'back' | 'home' | 'brand',
/** 仅首页(Tab 根页)在 logo 旁展示「涂鸦丫」;其余主 Tab 只展示 logo */
showBrandName: false,
apiEnvironment: '',
},
lifetimes: {
attached() {
@@ -44,6 +46,12 @@ Component({
},
methods: {
syncLeftAction() {
this.setData({
apiEnvironment:
wx.getAccountInfoSync().miniProgram.envVersion === 'develop'
? getApiEnvironment()
: '',
});
const pages = getCurrentPages();
const currentRoute = pages[pages.length - 1]?.route || '';
const isTabBarPage = TAB_BAR_ROUTES.has(currentRoute);
@@ -22,7 +22,12 @@
wx:elif="{{leftActionType === 'home'}}"
class="toy-icon toy-icon-home-o nav-bar__back-icon"></text>
</view>
<text class="nav-bar__title">{{title}}</text>
<view class="nav-bar__title-group">
<text class="nav-bar__title">{{title}}</text>
<text wx:if="{{apiEnvironment}}" class="nav-bar__environment"
>{{apiEnvironment}}</text
>
</view>
<view class="nav-bar__right">
<slot name="right"></slot>
</view>
+112 -27
View File
@@ -1,37 +1,122 @@
export type ApiEnvironment = 'local' | 'online';
export type ApiEnvironment = 'local' | 'staging' | 'production';
type MiniProgramVersion = 'develop' | 'trial' | 'release';
/** 统一接口入口;本地真机调试时将 localBaseUrl 的 IP 改为电脑局域网 IP。 */
const LOCAL_BASE_URL = 'http://127.0.0.1:8800/api/v1';
const STORAGE_KEY = 'doodle_develop_api_environment_v1';
let selectedEnvironment: ApiEnvironment | undefined;
let revision = 0;
/** 暂时共用本地地址;部署后分别替换 staging/production,真机 local 使用电脑局域网 IP。 */
export const apiConfig = {
developmentEnvironment: 'local' as ApiEnvironment,
localBaseUrl: 'http://127.0.0.1:8800/api/v1',
// 线上域名尚未确定:填写完整 HTTPS 地址,例如 https://你的域名/api/v1。
onlineBaseUrl: '',
localBaseUrl: LOCAL_BASE_URL,
stagingBaseUrl: LOCAL_BASE_URL,
productionBaseUrl: LOCAL_BASE_URL,
timeout: 10000,
};
/** develop 可切换本地/线上;体验版、正式版和未知运行环境均只允许线上入口。 */
export function resolveApiConfig(version: MiniProgramVersion | string) {
const environment: ApiEnvironment =
version === 'develop' ? apiConfig.developmentEnvironment : 'online';
const baseUrl = (
environment === 'local'
? apiConfig.localBaseUrl
: apiConfig.onlineBaseUrl
)
.trim()
.replace(/\/+$/, '');
if (!baseUrl) throw new Error('请先配置线上接口地址');
if (
!/^https?:\/\/[^/?#\s]+\/api\/v1$/.test(baseUrl) ||
(environment === 'online' && !baseUrl.startsWith('https://'))
) {
throw new Error('接口地址配置不正确');
}
return { environment, baseUrl, timeout: apiConfig.timeout };
export const API_ENVIRONMENTS: ApiEnvironment[] = [
'local',
'staging',
'production',
];
export function isApiEnvironment(value: unknown): value is ApiEnvironment {
return API_ENVIRONMENTS.includes(value as ApiEnvironment);
}
/** 请求时读取微信包版本,避免依赖 App 启动顺序,也不接受本地缓存覆盖正式版环境。 */
export function getApiConfig() {
return resolveApiConfig(wx.getAccountInfoSync().miniProgram.envVersion);
/** 仅开发包读取本机选择;体验、正式及未知包始终连接 production。 */
export function getApiEnvironment(
version: string = wx.getAccountInfoSync().miniProgram.envVersion,
): ApiEnvironment {
if (version === 'develop' && selectedEnvironment === undefined) {
try {
const stored: unknown = wx.getStorageSync(STORAGE_KEY);
if (isApiEnvironment(stored)) selectedEnvironment = stored;
} catch {
/* 无缓存时使用开发默认值。 */
}
}
return version === 'develop'
? (selectedEnvironment ?? apiConfig.developmentEnvironment)
: 'production';
}
export function resolveApiConfig(version: MiniProgramVersion | string) {
return resolveEnvironment(getApiEnvironment(version), version);
}
function resolveEnvironment(environment: ApiEnvironment, version: string) {
const baseUrl = apiConfig[`${environment}BaseUrl`]
.trim()
.replace(/\/+$/, '');
if (!baseUrl) throw new Error(`请先配置 ${environment} 接口地址`);
// 用户指定的本机占位地址暂允许 HTTP;实际云端入口仍须使用 HTTPS。
if (
!/^https?:\/\/[^/?#\s]+\/api\/v1$/.test(baseUrl) ||
(environment !== 'local' &&
!baseUrl.startsWith('https://') &&
baseUrl !== LOCAL_BASE_URL)
)
throw new Error('接口地址配置不正确');
return {
environment,
baseUrl,
timeout: apiConfig.timeout,
revision,
version,
};
}
export function getApiConfig() {
const { appId = '', envVersion } = wx.getAccountInfoSync().miniProgram;
return { ...resolveApiConfig(envVersion), appId };
}
/** 即使三个地址相同也按环境隔离;develop 不与体验/正式包共享业务缓存。 */
export function apiContextKey(config = getApiConfig()): string {
const scope = config.version === 'develop' ? 'develop' : 'public';
return [config.appId, scope, config.environment, config.baseUrl]
.map(encodeURIComponent)
.join(':');
}
/** 版本号阻止 A→B→A 后旧 A 请求覆盖当前页面。 */
export function isCurrentApiConfig(config: ReturnType<typeof getApiConfig>) {
const current = getApiConfig();
return (
config.revision === current.revision &&
apiContextKey(config) === apiContextKey(current)
);
}
/** 写入成功后才提交内存选择;配置失败保留原环境,不自动回退。 */
export function selectApiEnvironment(environment: ApiEnvironment): boolean {
if (wx.getAccountInfoSync().miniProgram.envVersion !== 'develop')
throw new Error('仅开发版可切换环境');
if (!isApiEnvironment(environment)) throw new Error('未知接口环境');
resolveEnvironment(environment, 'develop');
if (getApiEnvironment() === environment) return false;
wx.setStorageSync(STORAGE_KEY, environment);
selectedEnvironment = environment;
revision++;
return true;
}
export function getApiEnvironmentOptions() {
return API_ENVIRONMENTS.map((environment) => {
try {
return {
environment,
baseUrl: resolveEnvironment(environment, 'develop').baseUrl,
available: true,
};
} catch {
return {
environment,
baseUrl: '尚未配置有效地址',
available: false,
};
}
});
}
+14 -3
View File
@@ -430,14 +430,17 @@ page {
}
.profile-editor__actions {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 24rpx;
display: flex;
padding-top: 20rpx;
}
.profile-editor__action {
flex: 1;
min-width: 0;
height: 92rpx;
margin: 0;
padding: 0;
border: 0;
border-radius: 24rpx;
display: flex;
align-items: center;
@@ -447,6 +450,14 @@ page {
font-weight: 700;
}
.profile-editor__action::after {
border: 0;
}
.profile-editor__action + .profile-editor__action {
margin-left: 24rpx;
}
.profile-editor__action--ghost {
background: @bg-header;
color: @text-title;
+5
View File
@@ -115,6 +115,11 @@ Page({
},
onOpenProfileEditor() {
console.log(
'onOpenProfileEditor',
this.data.userName,
this.data.avatarUrl,
);
this.setData({
showProfileEditor: true,
draftNickName:
+7 -4
View File
@@ -225,16 +225,19 @@
</view>
<view class="profile-editor__actions">
<view
<button
class="profile-editor__action profile-editor__action--ghost"
disabled="{{isSavingProfile}}"
bindtap="onCloseProfileEditor">
取消
</view>
<view
</button>
<button
class="profile-editor__action profile-editor__action--primary {{isSavingProfile ? 'profile-editor__action--disabled' : ''}}"
loading="{{isSavingProfile}}"
disabled="{{isSavingProfile}}"
bindtap="onSubmitProfile">
{{isSavingProfile ? '保存中...' : '保存'}}
</view>
</button>
</view>
</view>
</van-popup>
+13
View File
@@ -0,0 +1,13 @@
let pendingWrites = 0;
/** 覆盖登录等待、上传及重试全过程,防止写操作进行中切换服务端。 */
export function beginApiWrite() {
pendingWrites++;
return () => {
pendingWrites--;
};
}
export function hasPendingApiWrites() {
return pendingWrites > 0;
}
+16
View File
@@ -1,4 +1,5 @@
import { getApiConfig } from '../config/api';
import { beginApiWrite } from './api-activity';
import {
ApiRequestError,
requestApi,
@@ -30,6 +31,21 @@ const blocked = new Set([
export async function apiRequest<T>(
path: string,
options: RequestOptions & { auth?: 'required' | 'none' } = {},
): Promise<T> {
const finish =
options.upload || (options.method && options.method !== 'GET')
? beginApiWrite()
: undefined;
try {
return await performApiRequest<T>(path, options);
} finally {
finish?.();
}
}
async function performApiRequest<T>(
path: string,
options: RequestOptions & { auth?: 'required' | 'none' } = {},
): Promise<T> {
// Authorization 由会话层统一提供;公开请求不附带调用方误传的凭证。
const header = Object.fromEntries(
+16 -4
View File
@@ -1,4 +1,4 @@
import { getApiConfig } from '../config/api';
import { getApiConfig, isCurrentApiConfig } from '../config/api';
export class ApiRequestError extends Error {
constructor(
@@ -28,7 +28,15 @@ export function requestApi<T>(
if (!path.startsWith('/') || path.startsWith('//'))
throw new Error('接口路径不正确');
return new Promise((resolve, reject) => {
// 公开请求、上传和登录共用检查,旧环境的响应不进入上层业务回调。
const stale = () => {
if (isCurrentApiConfig(config)) return false;
reject(new ApiRequestError('接口环境已变化,请重新操作'));
return true;
};
if (stale()) return;
const success = (res: { statusCode: number; data: unknown }) => {
if (stale()) return;
const body = res.data as {
code?: number;
data?: T;
@@ -42,7 +50,7 @@ export function requestApi<T>(
: '接口请求失败,请稍后重试',
res.statusCode,
typeof body?.reason === 'string' &&
/^[A-Z_]{1,64}$/.test(body.reason)
/^[A-Z_]{1,64}$/.test(body.reason)
? body.reason
: '',
),
@@ -60,8 +68,12 @@ export function requestApi<T>(
);
} else resolve(body.data as T);
};
const fail = () =>
reject(new ApiRequestError('连接失败,请确认后端已启动及网络配置'));
const fail = () => {
if (!stale())
reject(
new ApiRequestError('连接失败,请确认后端已启动及网络配置'),
);
};
if (options.upload) {
// wx.uploadFile 由运行时生成 multipart boundary,不能自行设置 JSON Content-Type。
wx.uploadFile({
+32 -13
View File
@@ -1,4 +1,4 @@
import { getApiConfig } from '../config/api';
import { getApiConfig, apiContextKey, isCurrentApiConfig } from '../config/api';
import { ApiRequestError, requestApi } from './api-transport';
export interface ServerUser {
@@ -32,7 +32,7 @@ export function getAuthContext() {
if (!appId) throw new ApiRequestError('无法识别小程序身份');
return {
...config,
key: `doodle_auth_v1:${encodeURIComponent(appId)}:${encodeURIComponent(config.baseUrl)}`,
key: `doodle_auth_v2:${apiContextKey(config)}`,
};
}
@@ -70,7 +70,18 @@ function readState(context: Context): SessionState {
generation: 0,
};
try {
const stored = wx.getStorageSync(context.key);
// 旧版会话只迁入原默认环境,不能复制到同址的 staging/production 开发配置。
const legacyDefault =
context.version === 'develop'
? context.environment === 'local'
: context.environment === 'production';
const stored =
wx.getStorageSync(context.key) ||
(legacyDefault
? wx.getStorageSync(
`doodle_auth_v1:${encodeURIComponent(wx.getAccountInfoSync().miniProgram.appId)}:${encodeURIComponent(context.baseUrl)}`,
)
: null);
if (stored) {
state.history = 'used';
if (
@@ -145,7 +156,7 @@ export async function ensureSession(
force = false,
explicitStart = false,
): Promise<AuthSession> {
if (getAuthContext().key !== context.key)
if (!isCurrentApiConfig(context))
throw new ApiRequestError('接口环境已变化,请重新操作');
const state = readState(context);
if (state.history === 'blocked' && !explicitStart)
@@ -184,11 +195,13 @@ export async function ensureSession(
);
break;
} catch (error) {
if (!(
error instanceof ApiRequestError &&
error.reason === 'WECHAT_CODE_INVALID' &&
attempt === 0
))
if (
!(
error instanceof ApiRequestError &&
error.reason === 'WECHAT_CODE_INVALID' &&
attempt === 0
)
)
throw error;
}
}
@@ -210,10 +223,7 @@ export async function ensureSession(
user: parseServerUser(result.user),
};
// 清理、切换环境期间迟到的登录响应不得恢复会话或写入当前界面。
if (
state.generation !== generation ||
getAuthContext().key !== context.key
)
if (state.generation !== generation || !isCurrentApiConfig(context))
throw new ApiRequestError('登录状态已变化,请重新操作');
state.session = session;
state.history = 'used';
@@ -222,6 +232,7 @@ export async function ensureSession(
} catch (error) {
if (
state.generation === generation &&
isCurrentApiConfig(context) &&
error instanceof ApiRequestError &&
[
'ACCOUNT_NOT_FOUND',
@@ -244,6 +255,14 @@ export async function ensureSession(
}
}
/** 保留各环境已保存会话和账户历史,只作废尚未完成的登录任务。 */
export function invalidatePendingAuthentication() {
for (const state of states.values()) {
state.generation++;
state.flight = undefined;
}
}
/** 同一原请求只恢复一次;迟到的旧 401 直接复用较新凭证。 */
export async function recoverSession(context: Context, failed: AuthSession) {
const state = readState(context);
@@ -0,0 +1,46 @@
import {
getApiEnvironment,
selectApiEnvironment,
type ApiEnvironment,
} from '../config/api';
import { hasPendingApiWrites } from './api-activity';
import { invalidatePendingAuthentication } from './auth-session';
let switching = false;
/** 统一切换入口:写入中不切换,保留分环境会话,重建全部页面以丢弃旧业务状态。 */
export async function switchApiEnvironment(environment: ApiEnvironment) {
if (wx.getAccountInfoSync().miniProgram.envVersion !== 'develop')
throw new Error('仅开发版可切换环境');
if (switching) throw new Error('正在切换环境,请稍候');
if (getApiEnvironment() === environment) return false;
const busyPage = getCurrentPages().some(
(page) => page.data.debugPublishLoading || page.data.isSavingProfile,
);
if (hasPendingApiWrites() || busyPage)
throw new Error('请等待当前保存或上传完成后再切换');
switching = true;
try {
selectApiEnvironment(environment);
invalidatePendingAuthentication();
const app = getApp<IAppOption>();
app.resetApiState();
// reLaunch 只重建页面,不重新执行 App.onLaunch;目标数据需要主动初始化。
try {
await new Promise<void>((resolve, reject) =>
wx.reLaunch({
url: '/pages/home/home',
success: () => resolve(),
fail: () => reject(new Error('环境已切换,请重新进入首页')),
}),
);
} finally {
app.warmAuthentication();
if (!(await app.loadPageConfig(true)))
throw new Error('已切换,目标接口连接失败');
}
return true;
} finally {
switching = false;
}
}
+62 -22
View File
@@ -12,28 +12,6 @@ page {
box-sizing: border-box;
}
.debug-page__hero {
padding: 48rpx 40rpx;
background: @bg-white;
border-radius: @radius-xl;
box-shadow: @shadow;
}
.debug-page__hero-title {
display: block;
font-size: 40rpx;
font-weight: 800;
color: @text-title;
}
.debug-page__hero-desc {
display: block;
margin-top: 16rpx;
font-size: 26rpx;
line-height: 1.6;
color: @text-secondary;
}
.debug-page__section {
margin-top: 32rpx;
}
@@ -125,3 +103,65 @@ page {
font-size: 24rpx;
line-height: 1.6;
}
.debug-page__environment {
padding: 32rpx;
background: @bg-white;
border-radius: @radius-xl;
box-shadow: @shadow;
}
.debug-page__environment-url {
display: block;
word-break: break-all;
color: @text-secondary;
font-size: 24rpx;
}
.debug-page__environment-options {
display: flex;
flex-direction: column;
margin-top: 24rpx;
}
/* 明确按钮高度及整行点击区域,避免原生 button 被全局样式压缩。 */
.debug-page__environment-options .debug-environment-option {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 96rpx;
flex-shrink: 0;
box-sizing: border-box;
margin: 0;
padding: 0 24rpx;
border: 0;
font-size: 28rpx;
line-height: 1.4;
color: @text-title;
background: @bg-header;
border-radius: 20rpx;
}
.debug-page__environment-options .debug-environment-option::after {
border: 0;
}
.debug-page__environment-options
.debug-environment-option
+ .debug-environment-option {
margin-top: 16rpx;
}
.debug-page__environment-options .debug-environment-option--selected {
background: @brand;
font-weight: 700;
}
.debug-page__environment-note {
display: block;
margin-top: 20rpx;
color: @text-secondary;
font-size: 22rpx;
line-height: 1.6;
}
+46 -1
View File
@@ -1,3 +1,10 @@
import {
getApiEnvironment,
getApiEnvironmentOptions,
isApiEnvironment,
} from '../../config/api';
import { switchApiEnvironment } from '../../services/environment-switch';
type DebugEntry = {
id: string;
title: string;
@@ -47,7 +54,45 @@ const DEBUG_ENTRIES: DebugEntry[] = [
Page({
data: {
entries: DEBUG_ENTRIES,
isDevEnv: true,
isDevEnv: false,
currentEnvironment: '',
currentBaseUrl: '',
environments: getApiEnvironmentOptions(),
isSwitchingEnvironment: false,
},
onShow() {
const environment = getApiEnvironment();
const environments = getApiEnvironmentOptions();
this.setData({
currentEnvironment: environment,
currentBaseUrl:
environments.find((item) => item.environment === environment)
?.baseUrl || '',
environments,
});
},
async onSelectEnvironment(e: WechatMiniprogram.TouchEvent) {
if (!this.data.isDevEnv || this.data.isSwitchingEnvironment) return;
const environment: unknown = e.currentTarget.dataset.environment;
if (!isApiEnvironment(environment)) return;
this.setData({ isSwitchingEnvironment: true });
wx.showLoading({ title: '切换环境中', mask: true });
let errorMessage = '';
try {
await switchApiEnvironment(environment);
} catch (error) {
errorMessage = error instanceof Error ? error.message : '切换失败';
} finally {
wx.hideLoading();
// 成功时本页已被 reLaunch 销毁,失败时才需要恢复按钮。
if (getCurrentPages().includes(this)) {
this.setData({ isSwitchingEnvironment: false });
this.onShow();
}
}
if (errorMessage) wx.showToast({ title: errorMessage, icon: 'none' });
},
onLoad() {
+20 -5
View File
@@ -1,9 +1,24 @@
<view class="debug-page">
<view class="debug-page__hero">
<text class="debug-page__hero-title">Debug 工具页</text>
<text class="debug-page__hero-desc">
仅开发环境可见,用于发布前检查和同步调试数据。
</text>
<view wx:if="{{isDevEnv}}" class="debug-page__environment">
<text class="debug-page__section-title"
>接口环境 · {{currentEnvironment}}</text
>
<text class="debug-page__environment-url">{{currentBaseUrl}}</text>
<view class="debug-page__environment-options">
<button
wx:for="{{environments}}"
wx:key="environment"
class="debug-environment-option {{currentEnvironment === item.environment ? 'debug-environment-option--selected' : ''}}"
data-environment="{{item.environment}}"
disabled="{{!item.available || isSwitchingEnvironment}}"
bindtap="onSelectEnvironment">
{{item.environment}}
<text wx:if="{{!item.available}}">(未配置)</text>
</button>
</view>
<text class="debug-page__environment-note"
>切换后自动返回首页;下次打开开发版继续使用所选环境。</text
>
</view>
<view class="debug-page__section">