mirror of
https://github.com/crmeb/CRMEB.git
synced 2026-10-28 13:31:04 +00:00
feat(移动端): 新增 pages-modules 免注册分包机制
丢目录即自动注册页面路由:scripts/uni-auto-subpackages.mjs 对读取 src/pages.json 做路径级拦截,把 src/pages-modules/ 扫描出的分包条目在内存中 append-only 合并(磁盘 pages.json 字节不变,含 _template 复制模板)。vite.config.js 接入安装函数与 dev watch 插件;显式声明 jsonc-parser / chokidar 依赖。已验证 mp-weixin / h5 / mp-alipay 构建注入、dev watch 丢文件 15s 内自动注册、空目录回归与系统行为一致。
This commit is contained in:
parent
35c8c03db5
commit
9167aca15c
6
frontend/uni-app/package-lock.json
generated
6
frontend/uni-app/package-lock.json
generated
@ -33,6 +33,8 @@
|
||||
"@dcloudio/uni-stacktracey": "3.0.0-5020420260813003",
|
||||
"@dcloudio/vite-plugin-uni": "3.0.0-5020420260813003",
|
||||
"@vue/runtime-core": "^3.4.21",
|
||||
"chokidar": "3.6.0",
|
||||
"jsonc-parser": "3.3.1",
|
||||
"sass": "~1.78.0",
|
||||
"stylus": "^0.64.0",
|
||||
"vite": "5.2.8"
|
||||
@ -6140,6 +6142,8 @@
|
||||
},
|
||||
"node_modules/chokidar": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.6.0.tgz",
|
||||
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"anymatch": "~3.1.2",
|
||||
@ -9531,6 +9535,8 @@
|
||||
},
|
||||
"node_modules/jsonc-parser": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmmirror.com/jsonc-parser/-/jsonc-parser-3.3.1.tgz",
|
||||
"integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jsonfile": {
|
||||
|
||||
@ -43,6 +43,8 @@
|
||||
"@dcloudio/uni-stacktracey": "3.0.0-5020420260813003",
|
||||
"@dcloudio/vite-plugin-uni": "3.0.0-5020420260813003",
|
||||
"@vue/runtime-core": "^3.4.21",
|
||||
"chokidar": "3.6.0",
|
||||
"jsonc-parser": "3.3.1",
|
||||
"sass": "~1.78.0",
|
||||
"stylus": "^0.64.0",
|
||||
"vite": "5.2.8"
|
||||
|
||||
293
frontend/uni-app/scripts/uni-auto-subpackages.mjs
Normal file
293
frontend/uni-app/scripts/uni-auto-subpackages.mjs
Normal file
@ -0,0 +1,293 @@
|
||||
// CLI 调试:node scripts/uni-auto-subpackages.mjs [--print]
|
||||
// 注意:本文件被 vite.config.js import,由 esbuild 内联打包,不能带 shebang 行
|
||||
/**
|
||||
* pages-modules 自动分包注册(免改 src/pages.json)
|
||||
*
|
||||
* 机制:本版本 uni 编译器(@dcloudio 3.0.0-5020420260813003)没有官方"目录即路由"机制,
|
||||
* src/pages.json 文本是路由唯一输入;但 H5 / 各小程序 / APP 三端的全部消费点(路由生成、
|
||||
* easycom、分包优化等)都通过 fs.readFileSync 读取该文件。这里在 vite 配置加载期对
|
||||
* "读取 src/pages.json"做路径级拦截,把 src/pages-modules/ 扫描出的分包条目在内存中
|
||||
* append-only 合并进返回文本——磁盘上的 pages.json 保持字节不变,系统配置零改动。
|
||||
* uni 官方在 @dcloudio/vite-plugin-uni 内部用同样手法拦截 scss 读取实现条件编译
|
||||
* (rewriteScssReadFileSync);@dcloudio/* 整组锁版升级后需回归验证(见 AGENTS.md)。
|
||||
*
|
||||
* 约定(详见 frontend/uni-app/AGENTS.md「pages-modules 自动分包注册」):
|
||||
* - src/pages-modules/<snake_case 模块名>/ = 一个独立分包,root = pages-modules/<模块名>
|
||||
* - 模块内 .vue 自动注册为页面,路径 = /pages-modules/<模块名>/<相对路径去扩展名>;
|
||||
* 排除 components/、component/、static/ 目录及 `_` 前缀的文件/目录
|
||||
* - 模块根可放自己的 pages.json 片段({ "pages": [{ "path", "style" }] }),
|
||||
* 提供后该模块不再自动扫描,完全按片段注册
|
||||
* - 只追加 subPackages,绝不触碰 pages/tabBar/globalStyle/easycom 等系统字段
|
||||
*
|
||||
* CLI 调试:node scripts/uni-auto-subpackages.mjs [--print]
|
||||
*/
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { parse, parseTree, findNodeAtLocation, getNodeValue } from 'jsonc-parser';
|
||||
|
||||
const ROOT = path.resolve(fileURLToPath(new URL('..', import.meta.url)));
|
||||
const PAGES_JSON_PATH = path.join(ROOT, 'src', 'pages.json');
|
||||
const MODULES_DIR = path.join(ROOT, 'src', 'pages-modules');
|
||||
const MODULES_ROOT_PREFIX = 'pages-modules';
|
||||
// uni jsonJs 插件把虚拟模块 pages-json-js 解析为 <src>/pages-json-js,dev 下用于失效路由模块
|
||||
const PAGES_JSON_JS_IDS = [path.join(ROOT, 'src', 'pages-json-js')];
|
||||
// 模块内不参与页面注册的目录,与 scripts/check-naming.mjs 的 ROLE_DIRS 对齐
|
||||
const ROLE_DIRS = new Set(['components', 'component', 'static']);
|
||||
|
||||
const toPosix = (p) => p.split(path.sep).join('/');
|
||||
|
||||
/** 递归收集模块内可注册为页面的 .vue 相对路径(排除 `_` 前缀与角色目录) */
|
||||
function walkVueFiles(dir, base, out) {
|
||||
for (const name of fs.readdirSync(dir)) {
|
||||
if (name.startsWith('_') || name.startsWith('.')) continue;
|
||||
const full = path.join(dir, name);
|
||||
if (fs.statSync(full).isDirectory()) {
|
||||
if (ROLE_DIRS.has(name)) continue;
|
||||
walkVueFiles(full, base, out);
|
||||
} else if (name.endsWith('.vue')) {
|
||||
out.push(toPosix(path.relative(base, full)).replace(/\.vue$/, ''));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 读取模块自带的 pages.json 片段(JSONC),不存在返回 null */
|
||||
function readModuleFragment(moduleDir) {
|
||||
const fragPath = path.join(moduleDir, 'pages.json');
|
||||
if (!fs.existsSync(fragPath)) return null;
|
||||
const errors = [];
|
||||
const value = parse(fs.readFileSync(fragPath, 'utf8'), errors, { allowTrailingComma: true });
|
||||
if (errors.length) {
|
||||
throw new Error(`${toPosix(path.relative(ROOT, fragPath))} 解析失败(第 ${errors[0].offset} 字符附近)`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/** 扫描单个模块,生成分包条目;无页面时返回 null */
|
||||
function scanModule(name, moduleDir) {
|
||||
const root = `${MODULES_ROOT_PREFIX}/${name}`;
|
||||
const frag = readModuleFragment(moduleDir);
|
||||
let pages;
|
||||
if (frag) {
|
||||
if (!Array.isArray(frag.pages) || frag.pages.length === 0) {
|
||||
throw new Error(`${root}/pages.json 必须提供非空 pages 数组`);
|
||||
}
|
||||
pages = frag.pages.map((page, i) => {
|
||||
if (!page || typeof page.path !== 'string' || !page.path.trim()) {
|
||||
throw new Error(`${root}/pages.json 第 ${i + 1} 项缺少 path`);
|
||||
}
|
||||
return {
|
||||
path: page.path.trim().replace(/^\.?\//, '').replace(/\.vue$/, ''),
|
||||
style: page.style || {}
|
||||
};
|
||||
});
|
||||
} else {
|
||||
const found = [];
|
||||
walkVueFiles(moduleDir, moduleDir, found);
|
||||
pages = found.sort().map((p) => ({ path: p, style: {} }));
|
||||
if (pages.length === 0) return null;
|
||||
}
|
||||
return { root, pages };
|
||||
}
|
||||
|
||||
/** 扫描 src/pages-modules/ 一级目录,产出按模块名排序的分包条目列表 */
|
||||
export function collectSubpackageEntries() {
|
||||
if (!fs.existsSync(MODULES_DIR)) return [];
|
||||
return fs
|
||||
.readdirSync(MODULES_DIR)
|
||||
.filter((name) => {
|
||||
if (name.startsWith('_') || name.startsWith('.')) return false;
|
||||
return fs.statSync(path.join(MODULES_DIR, name)).isDirectory();
|
||||
})
|
||||
.sort()
|
||||
.map((name) => scanModule(name, path.join(MODULES_DIR, name)))
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
const stripComments = (s) => s.replace(/\/\/[^\n\r]*/g, '').replace(/\/\*[\s\S]*?\*\//g, '');
|
||||
|
||||
/** 把分包条目以文本方式注入 pages.json 原文(增量编辑,其余内容逐字节保留) */
|
||||
function injectSubPackages(text, entries) {
|
||||
const tree = parseTree(text);
|
||||
if (!tree || tree.type !== 'object') return text;
|
||||
const snippet = entries.map((e) => JSON.stringify(e, null, 2)).join(',\n');
|
||||
const prop = findNodeAtLocation(tree, ['subPackages']);
|
||||
if (!prop) {
|
||||
// 系统配置没有 subPackages 时在根对象末尾追加属性(当前工程不适用,仅兜底)
|
||||
const closeIdx = tree.offset + tree.length - 1;
|
||||
const children = tree.children || [];
|
||||
const lastEnd = children.length ? children[children.length - 1].offset + children[children.length - 1].length : tree.offset + 1;
|
||||
const needComma = children.length > 0 && !stripComments(text.slice(lastEnd, closeIdx)).includes(',');
|
||||
return (
|
||||
text.slice(0, closeIdx) +
|
||||
`${needComma ? ',' : ''}\n\t"subPackages": [\n\t\t${snippet}\n\t]\n` +
|
||||
text.slice(closeIdx)
|
||||
);
|
||||
}
|
||||
const arr = prop;
|
||||
if (arr.type !== 'array') return text;
|
||||
// 插入点选在闭括号 `]` 前:天然位于所有元素级 `// #ifdef ... // #endif` 块之后,
|
||||
// 不会被条件编译裁掉;逗号按“最后一个元素与 ] 之间(去注释后)是否已有逗号”判断
|
||||
const closeIdx = arr.offset + arr.length - 1;
|
||||
const children = arr.children || [];
|
||||
const lastEnd = children.length ? children[children.length - 1].offset + children[children.length - 1].length : arr.offset + 1;
|
||||
const needComma = children.length > 0 && !stripComments(text.slice(lastEnd, closeIdx)).includes(',');
|
||||
return text.slice(0, closeIdx) + `${needComma ? ',' : ''}\n\t\t${snippet}\n\t` + text.slice(closeIdx);
|
||||
}
|
||||
|
||||
/** 校验合并结果:仍是合法 JSONC 对象,且每个模块分包都在 subPackages 中完整存在 */
|
||||
function verifyMerged(mergedText, entries) {
|
||||
const errors = [];
|
||||
const value = parse(mergedText, errors, { allowTrailingComma: true, disallowComments: false });
|
||||
if (errors.length || !value || !Array.isArray(value.subPackages)) return false;
|
||||
return entries.every((e) => {
|
||||
const found = value.subPackages.find((v) => v && v.root === e.root);
|
||||
return !!found && Array.isArray(found.pages) && found.pages.length === e.pages.length;
|
||||
});
|
||||
}
|
||||
|
||||
/** 扫描模块并把分包条目合并进 pages.json 原文(内存操作,不写盘);异常时回退原文 */
|
||||
export function mergePagesJsonText(originalText) {
|
||||
const entries = collectSubpackageEntries();
|
||||
if (entries.length === 0) return originalText;
|
||||
const merged = injectSubPackages(originalText, entries);
|
||||
if (!verifyMerged(merged, entries)) {
|
||||
console.warn('[uni-auto-subpackages] 合并结果自检失败,本次读取回退原始 pages.json');
|
||||
return originalText;
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
let installed = false;
|
||||
|
||||
/**
|
||||
* 安装 fs.readFileSync 拦截:精确匹配 src/pages.json 的字符串读取,
|
||||
* 返回合并了 pages-modules 分包的文本;其余读取零开销透传。
|
||||
* 必须在 uni() 插件首次读取 pages.json 之前调用(vite.config.js 配置体内、return 之前)。
|
||||
*/
|
||||
export function installAutoSubpackages() {
|
||||
if (installed) return;
|
||||
installed = true;
|
||||
if (!fs.existsSync(PAGES_JSON_PATH)) return;
|
||||
const origReadFileSync = fs.readFileSync;
|
||||
const isPagesJson = (file) => {
|
||||
try {
|
||||
return typeof file === 'string' && path.resolve(file) === PAGES_JSON_PATH;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
fs.readFileSync = function readFileSyncWithPagesModules(file, options) {
|
||||
const content = origReadFileSync.call(fs, file, options);
|
||||
if (typeof content === 'string' && isPagesJson(file)) {
|
||||
try {
|
||||
return mergePagesJsonText(content);
|
||||
} catch (e) {
|
||||
console.warn(`[uni-auto-subpackages] 合并失败,回退原始 pages.json:${e && e.message}`);
|
||||
return content;
|
||||
}
|
||||
}
|
||||
return content;
|
||||
};
|
||||
const entries = collectSubpackageEntries();
|
||||
if (entries.length) {
|
||||
const summary = entries.map((e) => `${e.root}(${e.pages.length}页)`).join('、');
|
||||
console.log(`[uni-auto-subpackages] 已注入模块分包:${summary}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* dev 增量生效插件:监听 src/pages-modules/,文件增删改时 touch 一次 pages.json 的
|
||||
* mtime(内容不变),借 uni 自己的 addWatchFile(pagesJsonPath) 链路触发重建
|
||||
* (H5 整页刷新 / MP 重新产出 app.json)。
|
||||
* 两种 dev 形态分别接管:
|
||||
* - H5 dev(vite dev server):configureServer 挂 server.watcher,并失效 pages-json-js 虚拟模块
|
||||
* - MP dev(vite build --watch,无 dev server):buildStart 且 watchMode 时自建 chokidar 监听
|
||||
* 一次性 build(watchMode 为 false)不建监听,零开销。
|
||||
*/
|
||||
export function uniAutoSubpackagesPlugin() {
|
||||
let timer = null;
|
||||
let chokidarWatcher = null;
|
||||
let serverWatcherInstalled = false;
|
||||
|
||||
const handleModuleChange = (server) => {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => {
|
||||
try {
|
||||
const now = new Date();
|
||||
fs.utimesSync(PAGES_JSON_PATH, now, now);
|
||||
} catch {
|
||||
/* pages.json 必然存在,忽略 */
|
||||
}
|
||||
if (server) {
|
||||
for (const id of PAGES_JSON_JS_IDS) {
|
||||
const mod = server.moduleGraph.getModuleById(id);
|
||||
if (mod) server.moduleGraph.invalidateModule(mod);
|
||||
}
|
||||
server.ws.send({ type: 'full-reload' });
|
||||
}
|
||||
}, 200);
|
||||
};
|
||||
|
||||
const isUnderModulesDir = (fp) => {
|
||||
const p = path.resolve(String(fp));
|
||||
return p === MODULES_DIR || p.startsWith(MODULES_DIR + path.sep);
|
||||
};
|
||||
|
||||
return {
|
||||
name: 'uni-auto-subpackages',
|
||||
configureServer(server) {
|
||||
if (!fs.existsSync(MODULES_DIR)) return;
|
||||
serverWatcherInstalled = true;
|
||||
server.watcher.add(MODULES_DIR);
|
||||
const trigger = (fp) => {
|
||||
if (isUnderModulesDir(fp)) handleModuleChange(server);
|
||||
};
|
||||
server.watcher.on('add', trigger);
|
||||
server.watcher.on('unlink', trigger);
|
||||
server.watcher.on('change', trigger);
|
||||
},
|
||||
async buildStart() {
|
||||
if (!this.meta.watchMode || serverWatcherInstalled || chokidarWatcher) return;
|
||||
if (!fs.existsSync(MODULES_DIR)) return;
|
||||
let chokidar;
|
||||
try {
|
||||
chokidar = (await import('chokidar')).default;
|
||||
} catch {
|
||||
console.warn('[uni-auto-subpackages] 缺少 chokidar,dev watch 下新增模块页面需手动重启(build 不受影响);可 npm i -D chokidar');
|
||||
return;
|
||||
}
|
||||
chokidarWatcher = chokidar.watch(MODULES_DIR, { ignoreInitial: true });
|
||||
const trigger = (fp) => {
|
||||
if (isUnderModulesDir(fp)) handleModuleChange(null);
|
||||
};
|
||||
chokidarWatcher.on('add', trigger);
|
||||
chokidarWatcher.on('unlink', trigger);
|
||||
chokidarWatcher.on('change', trigger);
|
||||
},
|
||||
closeWatcher() {
|
||||
if (chokidarWatcher) {
|
||||
const w = chokidarWatcher;
|
||||
chokidarWatcher = null;
|
||||
w.close().catch(() => {});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// CLI 调试:node scripts/uni-auto-subpackages.mjs [--print]
|
||||
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||
const entries = collectSubpackageEntries();
|
||||
if (entries.length === 0) {
|
||||
console.log('未发现可注册模块(src/pages-modules/ 为空或仅含排除目录)');
|
||||
} else {
|
||||
for (const e of entries) {
|
||||
console.log(`▸ ${e.root}(${e.pages.length} 页)`);
|
||||
for (const p of e.pages) console.log(` /${e.root}/${p.path}`);
|
||||
}
|
||||
}
|
||||
const merged = mergePagesJsonText(fs.readFileSync(PAGES_JSON_PATH, 'utf8'));
|
||||
const subCount = getNodeValue(findNodeAtLocation(parseTree(merged), ['subPackages']) || {}).length;
|
||||
console.log(`合并自检:${verifyMerged(merged, entries) ? '通过' : '失败'}(subPackages 共 ${subCount} 个分包)`);
|
||||
if (process.argv.includes('--print')) console.log('\n' + merged);
|
||||
}
|
||||
56
frontend/uni-app/src/pages-modules/_template/index.vue
Normal file
56
frontend/uni-app/src/pages-modules/_template/index.vue
Normal file
@ -0,0 +1,56 @@
|
||||
<!--
|
||||
pages-modules 插件模块模板:把本目录复制为 src/pages-modules/<插件标识>/(snake_case,
|
||||
与后端模块标识对齐,如 seckill 秒杀、combination 拼团、bargain 砍价、presell 预售),
|
||||
即自动注册为分包页面(无需改 src/pages.json),本页路由 = /pages-modules/<插件标识>/index。
|
||||
页面 = snake_case 子目录 + index.vue(lint 强制,语义后缀如 *_details/*_list/*_record);
|
||||
局部组件放 components/(PascalCase),不会被注册为页面;
|
||||
需要自定义标题/导航样式时,在模块根新增 pages.json 片段。
|
||||
完整命名细则与营销插件对照表见 docs/命名规范.md §2.1。
|
||||
`_` 前缀目录会被扫描器排除,模板自身永不参与编译。
|
||||
-->
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import routes from './routes.js';
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="tpl-page">
|
||||
<view class="tpl-card">
|
||||
<text class="tpl-title">pages-modules 业务模块模板</text>
|
||||
<text class="tpl-path">路由:{{ routes.index }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tpl-page {
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.tpl-card {
|
||||
padding: 32rpx 24rpx;
|
||||
border-radius: 16rpx;
|
||||
background-color: #fff;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tpl-title {
|
||||
display: block;
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.tpl-path {
|
||||
display: block;
|
||||
margin-top: 16rpx;
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.tpl-counter {
|
||||
margin-top: 32rpx;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
</style>
|
||||
14
frontend/uni-app/src/pages-modules/_template/routes.js
Normal file
14
frontend/uni-app/src/pages-modules/_template/routes.js
Normal file
@ -0,0 +1,14 @@
|
||||
// 模块路由常量:模块内外跳转统一引用,禁止硬编码路径(命名细则见 docs/命名规范.md §2.1)。
|
||||
// 复制本目录后,把 MODULE_NAME 换成实际插件标识(与目录名一致,如 seckill),按页面追加条目。
|
||||
// 跳转用法:uni.navigateTo({ url: routes.goodsDetails + '?id=' + id });
|
||||
const MODULE_NAME = '_template';
|
||||
|
||||
export default {
|
||||
/** 模块首页 */
|
||||
index: `/pages-modules/${MODULE_NAME}/index`,
|
||||
// 按实际页面追加(示例):
|
||||
// /** 商品详情页 */
|
||||
// goodsDetails: `/pages-modules/${MODULE_NAME}/goods_details/index`,
|
||||
// /** 参与记录页 */
|
||||
// record: `/pages-modules/${MODULE_NAME}/record/index`,
|
||||
};
|
||||
@ -1,13 +1,18 @@
|
||||
import { defineConfig, loadEnv } from 'vite';
|
||||
import uni from '@dcloudio/vite-plugin-uni';
|
||||
import { installAutoSubpackages, uniAutoSubpackagesPlugin } from './scripts/uni-auto-subpackages.mjs';
|
||||
|
||||
export default defineConfig(({ mode }) => {
|
||||
// 读取根目录 .env / .env.[mode] 中自定义的 UNI_APP_* 变量
|
||||
// (vite 的 loadEnv 默认只取 VITE_ 前缀,第三个参数传 '' 表示取全部)
|
||||
const env = loadEnv(mode, process.cwd(), '');
|
||||
|
||||
// 把 src/pages-modules/ 模块分包在内存中合并进 pages.json 的读取(磁盘文件不变);
|
||||
// 必须在 uni() 首次读取 pages.json 之前执行
|
||||
installAutoSubpackages();
|
||||
|
||||
return {
|
||||
plugins: [uni()],
|
||||
plugins: [uniAutoSubpackagesPlugin(), uni()],
|
||||
define: {
|
||||
// 注入 src/config/app.js 中使用的接口地址变量;.env 未配置时置空串,代码内回退到各端默认值
|
||||
'process.env.UNI_APP_API_URL': JSON.stringify(env.UNI_APP_API_URL || '')
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user