Compare commits

..

5 Commits

Author SHA1 Message Date
wangchen14709853322
a5b7392b6e Update v2.0-beta.md 2026-06-26 18:01:56 +08:00
wangchen14709853322
3e71008192 v2.0-beta-20260626
v2框架公测版测试流程请看v2.0-beta.md
2026-06-26 17:56:38 +08:00
wangchen14709853322
67e7669694 Create readme.md 2026-06-06 19:15:55 +08:00
wangchen14709853322
9f683839e6 1.2.4
新增 云编译排队机制
优化 授权域名校验忽略站点域名
修复 会员注册事件存在异常会导致会员码未生成的问题
2026-06-06 11:18:46 +08:00
wangchen147
44805a4a1f 处理编译内存溢出 2026-05-09 16:38:27 +08:00
6873 changed files with 396097 additions and 2479 deletions

View File

@ -1,11 +1,9 @@
# api请求地址 # api请求地址
VITE_APP_BASE_URL='/adminapi/' VITE_APP_BASE_URL='/adminapi/'
# 图片服务器地址
VITE_IMG_DOMAIN='' VITE_IMG_DOMAIN=''
# 图片服务器地址VITE_IMG_DOMAIN='https://cs-home-service.site.niucloud.com/'
# 请求时header中token的参数名 # 请求时header中token的参数名
VITE_REQUEST_HEADER_TOKEN_KEY='token' VITE_REQUEST_HEADER_TOKEN_KEY='token'

View File

@ -1,11 +1,9 @@
# api请求地址 # api请求地址
VITE_APP_BASE_URL='/adminapi/' VITE_APP_BASE_URL='/adminapi/'
# 图片服务器地址
VITE_IMG_DOMAIN='' VITE_IMG_DOMAIN=''
# 图片服务器地址VITE_IMG_DOMAIN='https://cs-home-service.site.niucloud.com/'
# 请求时header中token的参数名 # 请求时header中token的参数名
VITE_REQUEST_HEADER_TOKEN_KEY='token' VITE_REQUEST_HEADER_TOKEN_KEY='token'

4
admin/.gitignore vendored
View File

@ -22,3 +22,7 @@ dist-ssr
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?
.build
build-report.json
auto-imports.d.ts
components.d.ts

View File

@ -1,6 +1,5 @@
// Generated by 'unplugin-auto-import' // Generated by 'unplugin-auto-import'
export {} export {}
declare global { declare global {
const ElMessageBox: typeof import('element-plus/es')['ElMessageBox']
const ElNotification: typeof import('element-plus/es')['ElNotification']
} }

47
admin/components.d.ts vendored
View File

@ -11,83 +11,36 @@ declare module '@vue/runtime-core' {
DiyLink: typeof import('./src/components/diy-link/index.vue')['default'] DiyLink: typeof import('./src/components/diy-link/index.vue')['default']
DiyPage: typeof import('./src/components/diy-page/index.vue')['default'] DiyPage: typeof import('./src/components/diy-page/index.vue')['default']
Editor: typeof import('./src/components/editor/index.vue')['default'] Editor: typeof import('./src/components/editor/index.vue')['default']
ElAlert: typeof import('element-plus/es')['ElAlert']
ElAside: typeof import('element-plus/es')['ElAside']
ElAvatar: typeof import('element-plus/es')['ElAvatar']
ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb']
ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem']
ElButton: typeof import('element-plus/es')['ElButton'] ElButton: typeof import('element-plus/es')['ElButton']
ElCalendar: typeof import('element-plus/es')['ElCalendar']
ElCard: typeof import('element-plus/es')['ElCard'] ElCard: typeof import('element-plus/es')['ElCard']
ElCarousel: typeof import('element-plus/es')['ElCarousel']
ElCarouselItem: typeof import('element-plus/es')['ElCarouselItem']
ElCascader: typeof import('element-plus/es')['ElCascader'] ElCascader: typeof import('element-plus/es')['ElCascader']
ElCheckbox: typeof import('element-plus/es')['ElCheckbox'] ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup'] ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
ElCheckTag: typeof import('element-plus/es')['ElCheckTag']
ElCol: typeof import('element-plus/es')['ElCol'] ElCol: typeof import('element-plus/es')['ElCol']
ElCollapse: typeof import('element-plus/es')['ElCollapse']
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
ElContainer: typeof import('element-plus/es')['ElContainer']
ElDatePicker: typeof import('element-plus/es')['ElDatePicker'] ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
ElDialog: typeof import('element-plus/es')['ElDialog'] ElDialog: typeof import('element-plus/es')['ElDialog']
ElDivider: typeof import('element-plus/es')['ElDivider']
ElDrawer: typeof import('element-plus/es')['ElDrawer']
ElDropdown: typeof import('element-plus/es')['ElDropdown'] ElDropdown: typeof import('element-plus/es')['ElDropdown']
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem'] ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu'] ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
ElEmpty: typeof import('element-plus/es')['ElEmpty']
ElForm: typeof import('element-plus/es')['ElForm'] ElForm: typeof import('element-plus/es')['ElForm']
ElFormItem: typeof import('element-plus/es')['ElFormItem'] ElFormItem: typeof import('element-plus/es')['ElFormItem']
ElHeader: typeof import('element-plus/es')['ElHeader']
ElIcon: typeof import('element-plus/es')['ElIcon']
ElImage: typeof import('element-plus/es')['ElImage'] ElImage: typeof import('element-plus/es')['ElImage']
ElImageViewer: typeof import('element-plus/es')['ElImageViewer']
ElInput: typeof import('element-plus/es')['ElInput'] ElInput: typeof import('element-plus/es')['ElInput']
ElInputNumber: typeof import('element-plus/es')['ElInputNumber'] ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
ElLink: typeof import('element-plus/es')['ElLink']
ElMain: typeof import('element-plus/es')['ElMain']
ElMenu: typeof import('element-plus/es')['ElMenu']
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
ElOption: typeof import('element-plus/es')['ElOption'] ElOption: typeof import('element-plus/es')['ElOption']
ElOptionGroup: typeof import('element-plus/es')['ElOptionGroup']
ElPageHeader: typeof import('element-plus/es')['ElPageHeader'] ElPageHeader: typeof import('element-plus/es')['ElPageHeader']
ElPagination: typeof import('element-plus/es')['ElPagination'] ElPagination: typeof import('element-plus/es')['ElPagination']
ElPopover: typeof import('element-plus/es')['ElPopover']
ElProgress: typeof import('element-plus/es')['ElProgress']
ElRadio: typeof import('element-plus/es')['ElRadio'] ElRadio: typeof import('element-plus/es')['ElRadio']
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'] ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
ElRate: typeof import('element-plus/es')['ElRate']
ElResult: typeof import('element-plus/es')['ElResult']
ElRow: typeof import('element-plus/es')['ElRow'] ElRow: typeof import('element-plus/es')['ElRow']
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
ElSelect: typeof import('element-plus/es')['ElSelect'] ElSelect: typeof import('element-plus/es')['ElSelect']
ElSkeleton: typeof import('element-plus/es')['ElSkeleton']
ElSkeletonItem: typeof import('element-plus/es')['ElSkeletonItem']
ElSlider: typeof import('element-plus/es')['ElSlider'] ElSlider: typeof import('element-plus/es')['ElSlider']
ElStatistic: typeof import('element-plus/es')['ElStatistic']
ElStep: typeof import('element-plus/es')['ElStep']
ElSteps: typeof import('element-plus/es')['ElSteps']
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
ElSwitch: typeof import('element-plus/es')['ElSwitch']
ElTable: typeof import('element-plus/es')['ElTable'] ElTable: typeof import('element-plus/es')['ElTable']
ElTableColumn: typeof import('element-plus/es')['ElTableColumn'] ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
ElTabPane: typeof import('element-plus/es')['ElTabPane'] ElTabPane: typeof import('element-plus/es')['ElTabPane']
ElTabs: typeof import('element-plus/es')['ElTabs'] ElTabs: typeof import('element-plus/es')['ElTabs']
ElTag: typeof import('element-plus/es')['ElTag']
ElTimeline: typeof import('element-plus/es')['ElTimeline']
ElTimelineItem: typeof import('element-plus/es')['ElTimelineItem']
ElTimePicker: typeof import('element-plus/es')['ElTimePicker']
ElTimeSelect: typeof import('element-plus/es')['ElTimeSelect'] ElTimeSelect: typeof import('element-plus/es')['ElTimeSelect']
ElTooltip: typeof import('element-plus/es')['ElTooltip']
ElTree: typeof import('element-plus/es')['ElTree'] ElTree: typeof import('element-plus/es')['ElTree']
ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect']
ElUpload: typeof import('element-plus/es')['ElUpload']
ExportSure: typeof import('./src/components/export-sure/index.vue')['default'] ExportSure: typeof import('./src/components/export-sure/index.vue')['default']
HeatMap: typeof import('./src/components/heat-map/index.vue')['default'] HeatMap: typeof import('./src/components/heat-map/index.vue')['default']
Icon: typeof import('./src/components/icon/index.vue')['default'] Icon: typeof import('./src/components/icon/index.vue')['default']

View File

@ -8,6 +8,16 @@
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
<script>
// 外挂配置 — 部署后可直接修改此处的值,无需重新编译
window.__ENV__ = {
VITE_APP_BASE_URL: "",
VITE_IMG_DOMAIN: "",
VITE_REQUEST_HEADER_TOKEN_KEY: "token",
VITE_REQUEST_HEADER_SITEID_KEY: "site-id",
VITE_DETAULT_TITLE: ""
};
</script>
<script type="module" src="/src/main.ts"></script> <script type="module" src="/src/main.ts"></script>
</body> </body>
</html> </html>

View File

@ -5,7 +5,11 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "cross-env NODE_OPTIONS=--max-old-space-size=4096 && vite build && node publish.cjs", "build": "node scripts/build-all.cjs",
"build:core": "node scripts/build-shared.cjs && node scripts/clean-core.cjs && node node_modules/vite/bin/vite.js build --config vite.config.core.ts --force && node scripts/verify-core-lang.cjs && node scripts/assemble-admin.cjs",
"build:shared": "node scripts/build-shared.cjs",
"build:addon": "node scripts/build-addon.cjs",
"build:assemble": "node scripts/assemble-admin.cjs",
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
@ -24,7 +28,8 @@
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"day": "^0.0.2", "day": "^0.0.2",
"echarts": "5.4.1", "echarts": "5.4.1",
"element-plus": "^2.7.4", "element-plus": "2.7.4",
"esbuild": "^0.16.17",
"highlight.js": "11.0.1", "highlight.js": "11.0.1",
"lodash-es": "4.17.21", "lodash-es": "4.17.21",
"nprogress": "0.2.0", "nprogress": "0.2.0",
@ -33,7 +38,7 @@
"sass": "1.58.0", "sass": "1.58.0",
"sortablejs": "1.15.0", "sortablejs": "1.15.0",
"vditor": "^3.10.9", "vditor": "^3.10.9",
"vue": "3.2.45", "vue": "3.3.7",
"vue-i18n": "9.2.2", "vue-i18n": "9.2.2",
"vue-jsonp": "2.0.0", "vue-jsonp": "2.0.0",
"vue-router": "4.1.6", "vue-router": "4.1.6",
@ -48,6 +53,7 @@
"@typescript-eslint/eslint-plugin": "5.53.0", "@typescript-eslint/eslint-plugin": "5.53.0",
"@vitejs/plugin-vue": "4.0.0", "@vitejs/plugin-vue": "4.0.0",
"autoprefixer": "10.4.13", "autoprefixer": "10.4.13",
"cross-env": "^7.0.3",
"eslint": "8.34.0", "eslint": "8.34.0",
"eslint-config-standard-with-typescript": "34.0.0", "eslint-config-standard-with-typescript": "34.0.0",
"eslint-plugin-import": "2.27.5", "eslint-plugin-import": "2.27.5",
@ -60,7 +66,6 @@
"unplugin-auto-import": "0.13.0", "unplugin-auto-import": "0.13.0",
"unplugin-vue-components": "0.23.0", "unplugin-vue-components": "0.23.0",
"vite": "4.1.0", "vite": "4.1.0",
"vue-tsc": "1.0.24", "vue-tsc": "1.0.24"
"cross-env": "^7.0.3"
} }
} }

View File

@ -1,40 +1,49 @@
const fs = require('fs') const fs = require('fs')
const path = require('path')
const publish = () => { const ROOT = path.resolve(__dirname)
const src = './dist' const src = path.join(ROOT, 'dist')
const dest = '../niucloud/public/admin' const dest = path.resolve(ROOT, process.env.PUBLISH_DEST || '../niucloud/public/admin')
solve() function fixIndexHtml() {
const fn = path.join(src, 'index.html')
// 目标目录不存在停止复制 if (!fs.existsSync(fn)) {
try { const core = path.join(src, '.core', 'index.html')
const dir = fs.readdirSync(dest) if (fs.existsSync(core)) {
} catch (e) { fs.copyFileSync(core, fn)
return } else {
} throw new Error('missing dist/index.html — run assemble or build:core first')
// 删除目标目录下文件
fs.rm(dest, { recursive: true }, err => {
if(err) {
console.log(err)
return
} }
}
fs.cp(src, dest, { recursive: true }, (err) => { let text = fs.readFileSync(fn, 'utf-8')
if (err) {
console.error(err)
}
})
})
}
const solve = () => {
const fn = './dist/index.html'
const fc = fs.readFileSync(fn, 'utf-8')
let text = new String(fc)
text = text.replaceAll('./assets/', '/admin/assets/') text = text.replaceAll('./assets/', '/admin/assets/')
text = text.replace('./niucloud.ico', '/admin/niucloud.ico') text = text.replace('./niucloud.ico', '/admin/niucloud.ico')
fs.writeFileSync(fn, text, 'utf8') fs.writeFileSync(fn, text, 'utf-8')
}
function publish() {
fixIndexHtml()
if (!fs.existsSync(dest)) {
console.error(`[publish] target not found: ${dest}`)
console.error('[publish] set PUBLISH_DEST to your web admin directory')
process.exit(1)
}
fs.rmSync(dest, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
fs.cpSync(src, dest, {
recursive: true,
force: true,
filter: (srcPath) => {
const relative = path.relative(src, srcPath)
const parts = relative.split(path.sep)
return !parts.includes('.core') && !parts.includes('.shared')
}
})
const shopEntry = path.join(dest, 'assets', 'addons', 'shop', 'index.js')
console.log(`[publish] ${src} -> ${dest}`)
console.log(`[publish] shop addon: ${fs.existsSync(shopEntry) ? 'OK' : 'MISSING — run node scripts/sync-addon.cjs shop'}`)
} }
publish() publish()

View File

@ -0,0 +1,69 @@
/**
* 插件构建公共工具
* - 扫描 src/addon 下目录视图语言包
* - generate-addon-entry / build-all / assemble 等脚本复用
*/
const fs = require('fs')
const path = require('path')
/** 项目根目录admin/ */
const ROOT = path.resolve(__dirname, '..')
/** 插件源码根目录 */
const ADDON_DIR = path.join(ROOT, 'src', 'addon')
/** 列出 src/addon 下所有插件 key目录名 */
function listAddonKeys() {
if (!fs.existsSync(ADDON_DIR)) return []
return fs.readdirSync(ADDON_DIR, { withFileTypes: true })
.filter((d) => d.isDirectory())
.map((d) => d.name)
.sort()
}
/** 递归遍历目录,收集满足 filter 的文件相对路径 */
function walkFiles(dir, filter, base = dir) {
const out = []
if (!fs.existsSync(dir)) return out
for (const name of fs.readdirSync(dir)) {
const full = path.join(dir, name)
const stat = fs.statSync(full)
if (stat.isDirectory()) {
out.push(...walkFiles(full, filter, base))
} else if (filter(full)) {
out.push(path.relative(base, full).replace(/\\/g, '/'))
}
}
return out
}
/**
* 扫描插件 views 目录返回视图路径列表不含 .vue
* order/listgoods/category
*/
function scanAddonViews(key) {
const viewsDir = path.join(ADDON_DIR, key, 'views')
return walkFiles(viewsDir, (f) => f.endsWith('.vue'), viewsDir)
.map((p) => p.replace(/\.vue$/, ''))
}
/**
* 扫描插件 lang 目录
* @returns {Record<string, string[]>} locale -> json 文件名不含 .json
* { 'zh-cn': ['common', 'order.list'], 'en': ['common'] }
*/
function scanAddonLang(key) {
const langDir = path.join(ADDON_DIR, key, 'lang')
const out = {}
if (!fs.existsSync(langDir)) return out
for (const locale of fs.readdirSync(langDir)) {
const localeDir = path.join(langDir, locale)
if (!fs.statSync(localeDir).isDirectory()) continue
out[locale] = fs.readdirSync(localeDir)
.filter((f) => f.endsWith('.json'))
.map((f) => f.replace(/\.json$/, ''))
.sort()
}
return out
}
module.exports = { ROOT, ADDON_DIR, listAddonKeys, walkFiles, scanAddonViews, scanAddonLang }

View File

@ -0,0 +1,67 @@
/**
* 修复构建产物中错误的 admin-lang import 路径
*
* Rollup external @/lang 时可能生成相对路径 /admin/assets/*.js 解析会变成
* ../admin/assets/shared/admin-lang.js /admin/admin/assets/...HTML 404
* ../shared/admin-lang.js /admin/shared/...HTML 404
*
* verify-core-langassemble-admincheck-deploy 引用
*/
const fs = require('fs')
const path = require('path')
const { ADMIN_LANG_URL } = require('./shared-external.cjs')
/** [错误路径, 正确绝对路径] */
const REPLACEMENTS = [
['../admin/assets/shared/admin-lang.js', ADMIN_LANG_URL],
['../shared/admin-lang.js', ADMIN_LANG_URL],
['./shared/admin-lang.js', ADMIN_LANG_URL]
]
/** 递归收集目录下所有 .js 文件 */
function walkJs(dir, out = []) {
if (!fs.existsSync(dir)) return out
const stat = fs.statSync(dir)
if (!stat.isDirectory()) return out
for (const name of fs.readdirSync(dir)) {
const p = path.join(dir, name)
if (fs.statSync(p).isDirectory()) walkJs(p, out)
else if (name.endsWith('.js')) out.push(p)
}
return out
}
function fixCode(code) {
let next = code
for (const [bad, good] of REPLACEMENTS) {
if (next.includes(bad)) next = next.split(bad).join(good)
}
return next
}
function hasBadImport(code) {
return REPLACEMENTS.some(([bad]) => code.includes(bad))
}
/**
* 扫描目录内 JS 并就地替换错误路径
* @returns 修改过的文件数量
*/
function fixDir(dir) {
if (!fs.existsSync(dir)) {
throw new Error(`[admin-lang-import] directory not found: ${dir}`)
}
if (!fs.statSync(dir).isDirectory()) {
throw new Error(`[admin-lang-import] not a directory: ${dir}`)
}
let fixed = 0
for (const file of walkJs(dir)) {
const code = fs.readFileSync(file, 'utf-8')
if (!hasBadImport(code)) continue
fs.writeFileSync(file, fixCode(code), 'utf-8')
fixed++
}
return fixed
}
module.exports = { fixCode, hasBadImport, REPLACEMENTS, ADMIN_LANG_URL, fixDir }

View File

@ -0,0 +1,304 @@
/**
* 组装最终可部署 dist/
*
* staging 目录合并为站点可访问结构
* dist/.addons/* dist/assets/addons/{key}/
* dist/.core/* dist/index.htmlassets 跳过 addons 子目录
* dist/.shared/* dist/assets/shared/
*
* 并注入 Import Map修正 /admin 路径修复 admin-lang import剥离多余 style import
*
* 用法npm run build:assemble需先 build-shared + build:core + build-addon
*/
const fs = require('fs')
const path = require('path')
const { ROOT, listAddonKeys } = require('./addon-utils.cjs')
const { IMPORT_MAP } = require('./shared-external.cjs')
const { stripBuiltAssets } = require('./strip-style-imports.cjs')
const CORE_DIR = path.join(ROOT, 'dist', '.core')
const ADDONS_STAGING = path.join(ROOT, 'dist', '.addons')
const OUT_DIR = path.join(ROOT, 'dist')
const SHARED_DIR = path.join(ROOT, 'dist', '.shared')
const REPORT_PATH = path.join(ROOT, 'build-report.json')
function toWinLongPath(dir) {
if (process.platform !== 'win32') return dir
if (dir.startsWith('\\\\?\\')) return dir
return `\\\\?\\${path.resolve(dir)}`
}
function rmDir(dir) {
if (!fs.existsSync(dir)) return
try {
fs.rmSync(toWinLongPath(dir), { recursive: true, force: true, maxRetries: 8, retryDelay: 300 })
} catch {
fs.rmSync(dir, { recursive: true, force: true, maxRetries: 8, retryDelay: 300 })
}
}
/** Windows 下 rename 容易因文件锁失败,加重试 + 降级为 copy+delete */
function safeRename(src, dest, maxRetries = 5) {
for (let i = 0; i < maxRetries; i++) {
try {
fs.renameSync(src, dest)
return
} catch (e) {
if (e.code === 'EPERM' || e.code === 'EBUSY') {
if (i < maxRetries - 1) {
console.log(`[assemble] rename busy, retry ${i + 1}/${maxRetries}...`)
const s = Date.now(); while (Date.now() - s < 500) {}
} else {
// 最终降级copy + delete
copyDir(src, dest)
rmDir(src)
console.log(`[assemble] rename failed, fallback to copy+delete`)
}
} else {
throw e
}
}
}
}
function copyDir(src, dest) {
fs.mkdirSync(dest, { recursive: true })
for (const name of fs.readdirSync(src)) {
const s = path.join(src, name)
const d = path.join(dest, name)
if (fs.statSync(s).isDirectory()) copyDir(s, d)
else fs.copyFileSync(s, d)
}
}
function copyTree(src, dest) {
fs.mkdirSync(path.dirname(dest), { recursive: true })
fs.cpSync(src, dest, { recursive: true, force: true })
}
/**
* 清理 dist/assets 下旧的 core 产物保留 addonsshared 随后从 .shared 重拷
* Vite 每次构建 chunk hash 会变不清理会留下上一版同名不同 hash 的孤儿文件
*/
function cleanCoreAssetsOutput() {
const outAssets = path.join(OUT_DIR, 'assets')
if (!fs.existsSync(outAssets)) {
fs.mkdirSync(outAssets, { recursive: true })
return
}
const addonsSrc = path.join(outAssets, 'addons')
const addonsTmp = path.join(OUT_DIR, '.addons_staging')
if (fs.existsSync(addonsTmp)) rmDir(addonsTmp)
if (fs.existsSync(addonsSrc)) {
safeRename(addonsSrc, addonsTmp)
}
if (fs.existsSync(outAssets)) {
const staleAssets = path.join(ROOT, '.build', '.assets_stale_' + Date.now())
try { safeRename(outAssets, staleAssets) } catch { rmDir(outAssets) }
}
fs.mkdirSync(outAssets, { recursive: true })
if (fs.existsSync(addonsTmp)) {
safeRename(addonsTmp, path.join(outAssets, 'addons'))
}
}
/**
* 合并 core 产物到 dist 根目录
* 注意跳过 core/assets/addons避免覆盖已同步的插件目录
*/
function mergeCoreOutput() {
for (const name of ['index.html', 'manifest.json', 'niucloud.ico']) {
const src = path.join(CORE_DIR, name)
if (fs.existsSync(src)) {
fs.copyFileSync(src, path.join(OUT_DIR, name))
console.log(`[assemble] core ${name}`)
}
}
const ueditor = path.join(CORE_DIR, 'ueditor')
if (fs.existsSync(ueditor)) {
copyTree(ueditor, path.join(OUT_DIR, 'ueditor'))
console.log('[assemble] core ueditor/')
}
const coreAssets = path.join(CORE_DIR, 'assets')
const outAssets = path.join(OUT_DIR, 'assets')
if (!fs.existsSync(coreAssets)) return
fs.mkdirSync(outAssets, { recursive: true })
for (const name of fs.readdirSync(coreAssets)) {
if (name === 'addons') continue
const src = path.join(coreAssets, name)
const dest = path.join(outAssets, name)
if (fs.statSync(src).isDirectory()) {
copyDir(src, dest)
} else {
fs.copyFileSync(src, dest)
}
console.log(`[assemble] core assets/${name}`)
}
}
function copyLang(key, destAddonDir) {
const langSrc = path.join(ROOT, 'src', 'addon', key, 'lang')
if (!fs.existsSync(langSrc)) return
const langDest = path.join(destAddonDir, 'lang')
copyDir(langSrc, langDest)
}
function writeAddonManifest(key, destAddonDir) {
const manifest = {
key,
version: '1.0.0',
sharedVersion: 'admin-core-1.0.0',
entry: './index.js',
langBase: './lang/'
}
fs.writeFileSync(path.join(destAddonDir, 'manifest.json'), JSON.stringify(manifest, null, 2) + '\n', 'utf-8')
}
/** Vite 默认相对路径 ./assets/ → 部署绝对路径 /admin/assets/ */
function fixIndexHtml() {
const fn = path.join(OUT_DIR, 'index.html')
if (!fs.existsSync(fn)) return
let text = fs.readFileSync(fn, 'utf-8')
text = text.replaceAll('./assets/', '/admin/assets/')
text = text.replace('./niucloud.ico', '/admin/niucloud.ico')
fs.writeFileSync(fn, text, 'utf-8')
}
/** 注入 Import Map使 vue / @/lang 等解析到 shared 单例 */
function injectImportMap() {
const fn = path.join(OUT_DIR, 'index.html')
if (!fs.existsSync(fn)) return
let text = fs.readFileSync(fn, 'utf-8')
const script = `<script type="importmap">\n${JSON.stringify({ imports: IMPORT_MAP }, null, 2)}\n</script>`
if (text.includes('type="importmap"')) {
text = text.replace(/<script type="importmap">[\s\S]*?<\/script>/, script)
} else {
text = text.replace('<head>', `<head>\n ${script}`)
}
fs.writeFileSync(fn, text, 'utf-8')
}
/**
* 清理 dist 根下旧文件保留 staging.core/.addons/.shared assets
* addon 子目录在 runAssemble 里按 key 单独 rm避免 Windows ENOTEMPTY
*/
function cleanAssembledOutput() {
if (!fs.existsSync(OUT_DIR)) {
fs.mkdirSync(OUT_DIR, { recursive: true })
return
}
const keep = new Set(['.core', '.addons', '.shared', '.addons_staging', 'assets'])
for (const name of fs.readdirSync(OUT_DIR)) {
if (keep.has(name)) continue
// 历史 trash 目录不阻塞构建,可手动删除 dist/assets.__trash_*
if (name.includes('.__trash_') || name.startsWith('assets.__')) continue
rmDir(path.join(OUT_DIR, name))
}
}
/** Element Plus 语言包importmap 中 locale 路径指向此处 */
function copySharedLocales() {
const srcBase = path.join(ROOT, 'node_modules', 'element-plus', 'dist', 'locale')
const destBase = path.join(OUT_DIR, 'assets', 'shared', 'locale')
fs.mkdirSync(destBase, { recursive: true })
for (const file of ['zh-cn.mjs', 'en.mjs']) {
const src = path.join(srcBase, file)
if (fs.existsSync(src)) {
fs.copyFileSync(src, path.join(destBase, file))
}
}
}
function main() {
try {
runAssemble()
} catch (err) {
console.error('[assemble] ERROR:', err)
process.exit(1)
}
}
function runAssemble() {
const report = fs.existsSync(REPORT_PATH)
? JSON.parse(fs.readFileSync(REPORT_PATH, 'utf-8'))
: { success: listAddonKeys(), failed: [] }
cleanAssembledOutput()
if (!fs.existsSync(CORE_DIR)) {
console.error('[assemble] missing dist/.core — run core build first')
process.exit(1)
}
const successKeys = report.success || []
const installedKeys = []
// 先同步 addon避免后续 core 合并耗时过长时 assets/addons 仍为空
for (const key of successKeys) {
const src = path.join(ADDONS_STAGING, key)
if (!fs.existsSync(src)) continue
const dest = path.join(OUT_DIR, 'assets', 'addons', key)
rmDir(dest)
copyTree(src, dest)
copyLang(key, dest)
writeAddonManifest(key, dest)
const entryFile = path.join(dest, 'index.js')
if (!fs.existsSync(entryFile)) {
console.error(`[assemble] missing ${key}/index.js — run: node scripts/build-addon.cjs ${key}`)
process.exit(1)
}
installedKeys.push(key)
console.log(`[assemble] addon "${key}" -> assets/addons/${key}/`)
}
const indexJson = { keys: installedKeys, sharedVersion: 'admin-core-1.0.0' }
fs.mkdirSync(path.join(OUT_DIR, 'assets', 'addons'), { recursive: true })
fs.writeFileSync(
path.join(OUT_DIR, 'assets', 'addons', 'index.json'),
JSON.stringify(indexJson, null, 2) + '\n',
'utf-8'
)
cleanCoreAssetsOutput()
mergeCoreOutput()
// 修复 Rollup 生成的错误 admin-lang 相对路径(否则会请求到 HTML 404
const { fixDir } = require('./admin-lang-import-utils.cjs')
fixDir(path.join(OUT_DIR, 'assets'))
if (fs.existsSync(SHARED_DIR)) {
const sharedDest = path.join(OUT_DIR, 'assets', 'shared')
fs.mkdirSync(sharedDest, { recursive: true })
for (const name of fs.readdirSync(SHARED_DIR)) {
copyTree(path.join(SHARED_DIR, name), path.join(sharedDest, name))
}
console.log('[assemble] shared -> assets/shared/')
} else {
console.warn('[assemble] missing dist/.shared — run build-shared first')
}
copySharedLocales()
// core 合并可能覆盖 addons/index.json需再次写入
fs.writeFileSync(
path.join(OUT_DIR, 'assets', 'addons', 'index.json'),
JSON.stringify(indexJson, null, 2) + '\n',
'utf-8'
)
for (const key of installedKeys) {
writeAddonManifest(key, path.join(OUT_DIR, 'assets', 'addons', key))
}
fixIndexHtml()
injectImportMap()
const cleaned = stripBuiltAssets(OUT_DIR)
if (cleaned.length) {
console.log(`[assemble] stripped element-plus style imports from ${cleaned.length} js files`)
}
console.log(`[assemble] done: core + ${installedKeys.length} addons`)
if (report.failed?.length) {
console.warn('[assemble] skipped failed addons:', report.failed.map((f) => f.key).join(', '))
}
}
main()

View File

@ -0,0 +1,56 @@
/**
* 编译单个 addon
*
* 流程generate-addon-entry vite lib build sync-addon
* 输出 stagingdist/.addons/{key}/
* 同步部署目录dist/assets/addons/{key}/
*
* 用法
* node scripts/build-addon.cjs shop
* ADDON_KEY=shop node scripts/build-addon.cjs
*/
const { spawnSync } = require('child_process')
const fs = require('fs')
const path = require('path')
const { ROOT } = require('./addon-utils.cjs')
const key = process.argv[2] || process.env.ADDON_KEY
if (!key) {
console.error('Usage: node build-addon.cjs <addon-key>')
process.exit(1)
}
// 生成 .build/addons/{key}/entry.ts
require('./generate-addon-entry.cjs')
const env = {
...process.env,
ADDON_KEY: key,
NODE_OPTIONS: process.env.NODE_OPTIONS || '--max-old-space-size=4096'
}
console.log(`[build-addon] building "${key}"...`)
const r = spawnSync('npx', ['vite', 'build', '--config', 'vite.config.addon.ts'], {
cwd: ROOT,
env,
stdio: 'inherit',
shell: process.platform === 'win32'
})
if (r.status !== 0) {
console.error(`[build-addon] FAILED: ${key}`)
process.exit(r.status ?? 1)
}
// 复制到 dist/assets/addons 并更新 build-report.json
const sync = spawnSync('node', ['scripts/sync-addon.cjs', key], {
cwd: ROOT,
stdio: 'inherit',
shell: process.platform === 'win32'
})
if (sync.status !== 0) {
process.exit(sync.status ?? 1)
}
console.log(`[build-addon] OK: ${key}`)
process.exit(0)

View File

@ -0,0 +1,86 @@
/**
* 全量生产构建npm run build
*
* 1. build-shared 共享 ESMvueadmin-lang
* 2. vite core 主应用 dist/.core
* 3. addon 并行编译 每个 src/addon/* dist/.addons/*
* 4. assemble 合并为可部署 dist/
* 5. publish 复制到 PUBLISH_DEST默认 ../niucloud/public/admin
*/
const { spawnSync } = require('child_process')
const fs = require('fs')
const path = require('path')
const { ROOT, listAddonKeys } = require('./addon-utils.cjs')
const REPORT_PATH = path.join(ROOT, 'build-report.json')
function run(cmd, args, env = {}) {
const r = spawnSync(cmd, args, {
cwd: ROOT,
env: { ...process.env, ...env },
stdio: 'inherit',
shell: process.platform === 'win32'
})
return r.status ?? 1
}
/** 多 worker 从队列取 key 构建 addon限制并发降低内存峰值 */
async function buildAddonsParallel(keys, concurrency = 2) {
const success = []
const failed = []
const queue = [...keys]
async function worker() {
while (queue.length) {
const key = queue.shift()
if (!key) break
const code = run('node', [path.join(__dirname, 'build-addon.cjs'), key])
if (code === 0) success.push(key)
else failed.push({ key, error: 'build failed' })
}
}
const workers = Array.from({ length: Math.min(concurrency, keys.length) }, () => worker())
await Promise.all(workers)
return { success, failed }
}
async function main() {
console.log('[build-all] step 1/5: shared deps')
const sharedCode = run('node', [path.join(__dirname, 'build-shared.cjs')])
if (sharedCode !== 0) {
console.error('[build-all] shared build failed')
process.exit(sharedCode)
}
console.log('[build-all] step 2/5: core build')
const coreCode = run('npx', ['vite', 'build', '--config', 'vite.config.core.ts'], {
NODE_OPTIONS: '--max-old-space-size=4096'
})
if (coreCode !== 0) {
console.error('[build-all] core build failed')
process.exit(coreCode)
}
const keys = listAddonKeys()
console.log(`[build-all] step 3/5: addon builds (${keys.length} addons, concurrency 2)`)
const { success, failed } = await buildAddonsParallel(keys, 2)
fs.writeFileSync(REPORT_PATH, JSON.stringify({ success, failed, time: new Date().toISOString() }, null, 2) + '\n', 'utf-8')
console.log(`[build-all] addons: ${success.length} ok, ${failed.length} failed`)
console.log('[build-all] step 4/5: assemble')
const asmCode = run('node', [path.join(__dirname, 'assemble-admin.cjs')])
if (asmCode !== 0) process.exit(asmCode)
console.log('[build-all] step 5/5: publish')
const pubCode = run('node', [path.join(ROOT, 'publish.cjs')])
if (pubCode !== 0) process.exit(pubCode)
console.log('[build-all] complete')
}
main().catch((e) => {
console.error(e)
process.exit(1)
})

View File

@ -0,0 +1,120 @@
/**
* 构建浏览器 ESM 共享依赖 dist/.shared/
*
* 产物经 assemble 复制到 dist/assets/shared/ Import Map 加载
* admin-lang src/lang 打成独立包 core addon 共用 t()
*
* 用法npm run build:shared
*/
const fs = require('fs')
const path = require('path')
const { spawnSync } = require('child_process')
const { ROOT } = require('./addon-utils.cjs')
const { SHARED_BUILD_ORDER } = require('./shared-external.cjs')
/** 临时入口目录,每次构建前重写 */
const SHARED_SRC = path.join(ROOT, '.build', 'shared')
/** Vite shared 构建 staging 输出 */
const OUT_DIR = path.join(ROOT, 'dist', '.shared')
/** 各 shared 包的极简 re-export 入口(由 vite.config.shared.ts 打包) */
const ENTRY_CONTENT = {
vue: "import * as Vue from 'vue'\nexport * from 'vue'\nexport default Vue\n",
'vue-router': "export * from 'vue-router'\n",
pinia: "export * from 'pinia'\n",
'element-plus': "import * as ElementPlus from 'element-plus'\nexport * from 'element-plus'\nexport default ElementPlus\n",
'icons-vue': "export * from '@element-plus/icons-vue'\n",
axios: "export { default } from 'axios'\nexport * from 'axios'\n",
'vue-i18n': "export * from 'vue-i18n/dist/vue-i18n.esm-bundler.js'\n",
'admin-lang': "export { language, t, default } from '../../src/lang/index.ts'\n",
'core-shared': '' // 动态生成,从 API 目录扫描
}
function writeEntries() {
fs.mkdirSync(SHARED_SRC, { recursive: true })
// 动态生成 core-shared全量重导出所有 @/app/api/* + @/utils/* 模块
const apiDir = path.join(ROOT, 'src', 'app', 'api')
const apiExports = []
if (fs.existsSync(apiDir)) {
for (const f of fs.readdirSync(apiDir)) {
if (f.endsWith('.ts') && f !== 'addon.ts' && f !== 'module.ts') {
const mod = f.replace('.ts', '')
apiExports.push(`export * from '../../src/app/api/${mod}'`)
}
}
}
const utilsDir = path.join(ROOT, 'src', 'utils')
const utilsExports = []
if (fs.existsSync(utilsDir)) {
for (const f of fs.readdirSync(utilsDir)) {
// common 和 request 单独处理,其余全量重导出
if (f.endsWith('.ts') && f !== 'common.ts' && f !== 'request.ts' && f !== 'addon-loader.ts' && f !== 'addon-lang.ts') {
const mod = f.replace('.ts', '')
utilsExports.push(`export * from '../../src/utils/${mod}'`)
}
}
}
ENTRY_CONTENT['core-shared'] = [
"export * from '../../src/utils/common'",
"import _request from '../../src/utils/request'",
"export { _request as request }",
"export default _request",
...utilsExports,
...apiExports,
"export { default as UploadImage } from '../../src/components/upload-image/index.vue'",
"export { default as DiyPage } from '../../src/components/diy-page/index.vue'",
"export { default as MapSelector } from '../../src/components/map-selector/index.vue'",
"export { default as Editor } from '../../src/components/editor/index.vue'"
].join('\n')
for (const [key, content] of Object.entries(ENTRY_CONTENT)) {
fs.writeFileSync(path.join(SHARED_SRC, `${key}.ts`), content, 'utf-8')
}
}
/** 构建单个 shared 包;仅第一个包 emptyOutDir 清空 .shared */
function runBuild(pkgKey, emptyOutDir) {
const r = spawnSync('npx', ['vite', 'build', '--config', 'vite.config.shared.ts'], {
cwd: ROOT,
env: {
...process.env,
SHARED_PKG: pkgKey,
SHARED_EMPTY: emptyOutDir ? '1' : '0',
NODE_OPTIONS: '--max-old-space-size=4096'
},
stdio: 'inherit',
shell: process.platform === 'win32'
})
if ((r.status ?? 1) !== 0) {
console.error(`[build-shared] failed: ${pkgKey}`)
process.exit(r.status ?? 1)
}
console.log(`[build-shared] ok: ${pkgKey}.js`)
}
function main() {
writeEntries()
fs.mkdirSync(OUT_DIR, { recursive: true })
SHARED_BUILD_ORDER.forEach((pkgKey, i) => {
runBuild(pkgKey, i === 0)
})
console.log(`[build-shared] done -> ${path.relative(ROOT, OUT_DIR)}/`)
// 若 dist/assets 已存在(曾 assemble 过),顺便同步一份 shared 便于单独调试
const distAssets = path.join(ROOT, 'dist', 'assets', 'shared')
if (fs.existsSync(path.join(ROOT, 'dist', 'assets'))) {
fs.mkdirSync(distAssets, { recursive: true })
for (const name of fs.readdirSync(OUT_DIR)) {
if (!name.endsWith('.js')) continue
fs.copyFileSync(path.join(OUT_DIR, name), path.join(distAssets, name))
}
console.log(`[build-shared] synced -> dist/assets/shared/`)
}
}
main()

View File

@ -0,0 +1,97 @@
/**
* 部署目录健康检查
*
* 校验 public/admin 是否具备 split 架构所需文件
* - index.html 入口sharedadmin-lang.js
* - addons/index.jsonshop 等插件
* - 无错误 admin-lang 相对路径无重复 createI18n
*
* 用法node scripts/check-deploy.cjs [deploy-dir]
*/
const fs = require('fs')
const path = require('path')
const DEPLOY = process.argv[2] || 'D:/AppData/phpstudy_pro/WWW/test/niucloud/public/admin'
const issues = []
const ok = []
function check(name, pass, detail) {
if (pass) ok.push(`[OK] ${name}${detail ? ': ' + detail : ''}`)
else issues.push(`[FAIL] ${name}${detail ? ': ' + detail : ''}`)
}
if (!fs.existsSync(DEPLOY)) {
console.error('deploy dir not found:', DEPLOY)
process.exit(1)
}
const html = fs.readFileSync(path.join(DEPLOY, 'index.html'), 'utf-8')
const entryMatch = html.match(/\/admin\/assets\/(index-[a-f0-9]+\.js)/)
const entry = entryMatch ? entryMatch[1] : null
check('index.html entry', !!entry, entry || 'no index-*.js in script src')
if (entry) {
const entryPath = path.join(DEPLOY, 'assets', entry)
check('entry file exists', fs.existsSync(entryPath))
if (fs.existsSync(entryPath)) {
const code = fs.readFileSync(entryPath, 'utf-8')
check('core not stale', !code.includes('loadAddonCommonLocales'), 'must not contain loadAddonCommonLocales')
check('core has addon loader', code.includes('assets/addons') || code.includes('/admin/assets/addons'))
const oldPreload = /async function \w+\(\w+,t=\["zh-cn","en"\]\)/.test(code)
check('core preload not old-only-common', !oldPreload)
}
}
// CSS hash 随构建变化;若 core 重建后此处失败,需同步更新此文件名
const css = 'index-c72b4d30.css'
check('entry css', fs.existsSync(path.join(DEPLOY, 'assets', css)), css)
for (const f of ['vue.js', 'element-plus.js', 'vue-i18n.js', 'admin-lang.js']) {
check('shared/' + f, fs.existsSync(path.join(DEPLOY, 'assets', 'shared', f)))
}
// 抽样检查 shop 列表页是否走共享 admin-lang
const shopList = fs.existsSync(path.join(DEPLOY, 'assets', 'addons', 'shop'))
? fs.readdirSync(path.join(DEPLOY, 'assets', 'addons', 'shop')).find((n) => n.startsWith('list-') && n.endsWith('.js'))
: null
if (shopList) {
const code = fs.readFileSync(path.join(DEPLOY, 'assets', 'addons', 'shop', shopList), 'utf-8')
check('shop list uses shared t()', code.includes('/admin/assets/shared/admin-lang.js'))
check('shop list not duplicate i18n', !code.includes('createI18n'))
}
const addonsIndex = path.join(DEPLOY, 'assets', 'addons', 'index.json')
check('addons/index.json', fs.existsSync(addonsIndex))
if (fs.existsSync(addonsIndex)) {
const data = JSON.parse(fs.readFileSync(addonsIndex, 'utf-8'))
check('addons keys', Array.isArray(data.keys) && data.keys.length > 0, data.keys?.join(', '))
}
const shopIndex = path.join(DEPLOY, 'assets', 'addons', 'shop', 'index.js')
check('shop/index.js', fs.existsSync(shopIndex))
if (fs.existsSync(shopIndex)) {
const shop = fs.readFileSync(shopIndex, 'utf-8')
check('shop exports langs', shop.includes('langs'))
}
const { hasBadImport } = require('./admin-lang-import-utils.cjs')
let badLangImports = 0
const assetsDir = path.join(DEPLOY, 'assets')
if (fs.existsSync(assetsDir)) {
for (const name of fs.readdirSync(assetsDir)) {
if (!name.endsWith('.js')) continue
if (hasBadImport(fs.readFileSync(path.join(assetsDir, name), 'utf-8'))) badLangImports++
}
}
check('no bad admin-lang import paths', badLangImports === 0, badLangImports ? `${badLangImports} core chunk(s)` : '')
const staleEntry = path.join(DEPLOY, 'assets', 'index-d2fee835.js')
if (fs.existsSync(staleEntry)) {
issues.push('[WARN] old bundle still present: index-d2fee835.js (harmless if index.html points to new entry)')
}
console.log('\n=== Deploy check:', DEPLOY, '===\n')
ok.forEach((l) => console.log(l))
issues.forEach((l) => console.log(l))
console.log('\nSummary:', ok.length, 'ok,', issues.length, 'issue(s)')
process.exit(issues.some((l) => l.startsWith('[FAIL]')) ? 1 : 0)

View File

@ -0,0 +1,25 @@
/**
* 删除 dist/.corecore 构建 staging
*
* build:core vite build 前执行避免 Windows 上旧 chunk 残留导致 hash 不一致
* 使用 rename 再删 的策略降低文件被占用时 rmSync 失败的概率
*/
const fs = require('fs')
const path = require('path')
const core = path.join(__dirname, '..', 'dist', '.core')
function rmDir(dir) {
if (!fs.existsSync(dir)) return
const trash = `${dir}.__trash_${Date.now()}`
try {
fs.renameSync(dir, trash)
} catch {
fs.rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
return
}
fs.rmSync(trash, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
}
rmDir(core)
console.log('[clean-core] removed dist/.core')

128
admin/scripts/deploy-to.cjs Normal file
View File

@ -0,0 +1,128 @@
/**
* 发布 dist 到目标 admin 目录保留已 drop-in 的插件
*
* 用法
* node scripts/deploy-to.cjs [deploy-dir]
* node scripts/deploy-to.cjs "D:/path/to/public/admin"
*/
const fs = require('fs')
const path = require('path')
const { ROOT } = require('./addon-utils.cjs')
const { fixDir } = require('./admin-lang-import-utils.cjs')
const src = path.join(ROOT, 'dist')
const dest = path.resolve(process.argv[2] || process.env.PUBLISH_DEST || path.join(ROOT, '../niucloud/public/admin'))
const backup = path.join(ROOT, '.deploy_addons_backup')
function rmDir(dir) {
if (!fs.existsSync(dir)) return
fs.rmSync(dir, { recursive: true, force: true, maxRetries: 8, retryDelay: 200 })
}
function copyTree(from, to) {
fs.mkdirSync(path.dirname(to), { recursive: true })
fs.cpSync(from, to, { recursive: true, force: true })
}
function readKeys(addonsDir) {
const p = path.join(addonsDir, 'index.json')
if (!fs.existsSync(p)) return []
try {
const data = JSON.parse(fs.readFileSync(p, 'utf-8'))
return Array.isArray(data.keys) ? data.keys : []
} catch {
return []
}
}
function writeKeys(addonsDir, keys) {
fs.mkdirSync(addonsDir, { recursive: true })
fs.writeFileSync(
path.join(addonsDir, 'index.json'),
JSON.stringify({ keys: [...new Set(keys)].sort(), sharedVersion: 'admin-core-1.0.0' }, null, 2) + '\n'
)
}
function listDropInKeys(addonsDir) {
if (!fs.existsSync(addonsDir)) return []
return fs.readdirSync(addonsDir).filter((name) => {
if (name === 'index.json' || name.startsWith('.')) return false
return fs.existsSync(path.join(addonsDir, name, 'index.js'))
})
}
function main() {
if (!fs.existsSync(path.join(src, 'index.html'))) {
console.error('[deploy] missing dist/index.html — run: npm run build:core')
process.exit(1)
}
if (!fs.existsSync(dest)) {
console.error(`[deploy] target not found: ${dest}`)
process.exit(1)
}
const destAddons = path.join(dest, 'assets', 'addons')
rmDir(backup)
if (fs.existsSync(destAddons)) {
copyTree(destAddons, backup)
console.log(`[deploy] backed up existing addons -> ${path.relative(ROOT, backup)}`)
}
const preserveKeys = [...new Set([...readKeys(backup), ...listDropInKeys(backup)])]
// 根目录静态文件
for (const name of ['index.html', 'manifest.json', 'niucloud.ico']) {
const f = path.join(src, name)
if (fs.existsSync(f)) fs.copyFileSync(f, path.join(dest, name))
}
const ueditor = path.join(src, 'ueditor')
if (fs.existsSync(ueditor)) {
rmDir(path.join(dest, 'ueditor'))
copyTree(ueditor, path.join(dest, 'ueditor'))
}
// core assets跳过 addons单独合并
const srcAssets = path.join(src, 'assets')
const destAssets = path.join(dest, 'assets')
fs.mkdirSync(destAssets, { recursive: true })
for (const name of fs.readdirSync(srcAssets)) {
if (name === 'addons') continue
const from = path.join(srcAssets, name)
const to = path.join(destAssets, name)
rmDir(to)
copyTree(from, to)
}
// 合并 addonsdist 构建产物 + 目标站 drop-in
const mergedAddons = path.join(destAssets, 'addons')
fs.mkdirSync(mergedAddons, { recursive: true })
const srcAddons = path.join(srcAssets, 'addons')
if (fs.existsSync(srcAddons)) {
copyTree(srcAddons, mergedAddons)
}
for (const key of preserveKeys) {
const from = path.join(backup, key)
const to = path.join(mergedAddons, key)
if (!fs.existsSync(path.join(from, 'index.js'))) continue
if (fs.existsSync(path.join(to, 'index.js'))) continue
copyTree(from, to)
console.log(`[deploy] preserved drop-in addon "${key}"`)
}
const keys = [...new Set([
...readKeys(mergedAddons),
...readKeys(backup),
...preserveKeys
])]
if (keys.length) writeKeys(mergedAddons, keys)
fixDir(destAssets)
rmDir(backup)
console.log(`[deploy] ${src} -> ${dest}`)
console.log(`[deploy] addons: ${keys.join(', ') || '(none)'}`)
const hasDropIn = fs.readFileSync(path.join(destAssets, 'shared', 'admin-lang.js'), 'utf-8').includes('drop-in entry')
console.log(`[deploy] drop-in runtime: ${hasDropIn ? 'OK' : 'MISSING — rebuild shared'}`)
}
main()

View File

@ -0,0 +1,185 @@
/**
* 构建前确保 esbuild 原生二进制可用
*
* Windows @esbuild/win32-x64/esbuild.exe 常被杀毒删除 npm postinstall
* 在二进制缺失时调用 install.js 形成死循环本脚本直接从 npm 拉取 tgz 解压
* 不依赖 esbuild/install.js
*
* npm install esbuild postinstall 失败请先
* npm install --ignore-scripts
* node scripts/ensure-esbuild.cjs
*/
const fs = require('fs')
const path = require('path')
const https = require('https')
const zlib = require('zlib')
const { spawnSync, execFileSync } = require('child_process')
const ROOT = path.resolve(__dirname, '..')
const DEFAULT_ESBUILD_VERSION = '0.16.17'
function platformPackage() {
const key = `${process.platform} ${process.arch} ${require('os').endianness()}`
const map = {
'win32 x64 LE': { pkg: '@esbuild/win32-x64', subpath: 'esbuild.exe' },
'win32 ia32 LE': { pkg: '@esbuild/win32-ia32', subpath: 'esbuild.exe' },
'win32 arm64 LE': { pkg: '@esbuild/win32-arm64', subpath: 'esbuild.exe' },
'darwin x64 LE': { pkg: '@esbuild/darwin-x64', subpath: 'bin/esbuild' },
'darwin arm64 LE': { pkg: '@esbuild/darwin-arm64', subpath: 'bin/esbuild' },
'linux x64 LE': { pkg: '@esbuild/linux-x64', subpath: 'bin/esbuild' }
}
const hit = map[key]
if (!hit) throw new Error(`Unsupported platform for esbuild: ${key}`)
return hit
}
function resolveEsbuildVersion() {
const candidates = [
path.join(ROOT, 'node_modules', 'esbuild', 'package.json'),
path.join(ROOT, 'node_modules', 'vite', 'node_modules', 'esbuild', 'package.json')
]
for (const file of candidates) {
try {
return require(file).version
} catch {
// continue
}
}
return DEFAULT_ESBUILD_VERSION
}
function platformBinaryPath() {
const { pkg, subpath } = platformPackage()
return path.join(ROOT, 'node_modules', ...pkg.split('/'), subpath)
}
function downloadedBinaryPath(version) {
const { pkg, subpath } = platformPackage()
const libDir = path.join(ROOT, 'node_modules', 'esbuild', 'lib')
const base = path.basename(subpath)
return path.join(libDir, `downloaded-${pkg.replace('/', '-')}-${base}`)
}
function binaryCandidates(version) {
return [platformBinaryPath(), downloadedBinaryPath(version)]
}
function fetchBuffer(url) {
return new Promise((resolve, reject) => {
https.get(url, (res) => {
if ((res.statusCode === 301 || res.statusCode === 302) && res.headers.location) {
return fetchBuffer(res.headers.location).then(resolve, reject)
}
if (res.statusCode !== 200) {
return reject(new Error(`HTTP ${res.statusCode} for ${url}`))
}
const chunks = []
res.on('data', (c) => chunks.push(c))
res.on('end', () => resolve(Buffer.concat(chunks)))
}).on('error', reject)
})
}
function extractFileFromTarGzip(buffer, subpath) {
try {
buffer = zlib.unzipSync(buffer)
} catch (err) {
throw new Error(`Invalid gzip: ${err.message}`)
}
const str = (i, n) => String.fromCharCode(...buffer.subarray(i, i + n)).replace(/\0.*$/, '')
let offset = 0
const target = `package/${subpath}`
while (offset < buffer.length) {
const name = str(offset, 100)
const size = parseInt(str(offset + 124, 12), 8)
offset += 512
if (!Number.isNaN(size)) {
if (name === target) return buffer.subarray(offset, offset + size)
offset += (size + 511) & ~511
}
}
throw new Error(`Not found in archive: ${target}`)
}
async function downloadPlatformBinary(version) {
const { pkg, subpath } = platformPackage()
const shortName = pkg.replace('@esbuild/', '')
const url = `https://registry.npmjs.org/${pkg}/-/${shortName}-${version}.tgz`
console.log(`[ensure-esbuild] downloading ${pkg}@${version} ...`)
const tgz = await fetchBuffer(url)
const data = extractFileFromTarGzip(tgz, subpath)
const dest = platformBinaryPath()
fs.mkdirSync(path.dirname(dest), { recursive: true })
fs.writeFileSync(dest, data)
if (process.platform !== 'win32') fs.chmodSync(dest, 0o755)
console.log(`[ensure-esbuild] wrote ${path.relative(ROOT, dest)} (${data.length} bytes)`)
return dest
}
function copyToDownloadedCache(version, src) {
const cache = downloadedBinaryPath(version)
const libDir = path.dirname(cache)
if (!fs.existsSync(path.join(ROOT, 'node_modules', 'esbuild'))) return
fs.mkdirSync(libDir, { recursive: true })
fs.copyFileSync(src, cache)
}
function verifyBinary(binPath, version) {
const out = execFileSync(binPath, ['--version'], { stdio: 'pipe' }).toString().trim()
if (out !== version) {
throw new Error(`Expected esbuild ${version}, got ${out}`)
}
}
function hasWorkingBinary(version) {
for (const p of binaryCandidates(version)) {
try {
if (fs.existsSync(p) && fs.statSync(p).size > 1024) {
verifyBinary(p, version)
return p
}
} catch {
// try next
}
}
return null
}
async function repair(version) {
const dest = await downloadPlatformBinary(version)
copyToDownloadedCache(version, dest)
verifyBinary(dest, version)
}
async function main() {
const version = resolveEsbuildVersion()
const existing = hasWorkingBinary(version)
if (existing) {
console.log(`[ensure-esbuild] ok (${path.relative(ROOT, existing)}, v${version})`)
return
}
console.log(`[ensure-esbuild] binary missing or invalid, repairing (esbuild@${version})...`)
try {
await repair(version)
} catch (err) {
console.error('[ensure-esbuild] repair failed:', err.message || err)
console.error('')
console.error('Try:')
console.error(' npm install --ignore-scripts')
console.error(' node scripts/ensure-esbuild.cjs')
process.exit(1)
}
const ok = hasWorkingBinary(version)
if (!ok) {
console.error('[ensure-esbuild] binary still not usable after repair.')
process.exit(1)
}
console.log('[ensure-esbuild] ok')
}
main().catch((err) => {
console.error('[ensure-esbuild]', err)
process.exit(1)
})

View File

@ -0,0 +1,39 @@
/**
* CLI修复构建产物中的 admin-lang import 路径
*
* 默认扫描 dist/.core/assets dist/assets
* 部署后可对 public/admin/assets 单独执行
*
* 用法
* node scripts/fix-admin-lang-imports.cjs
* node scripts/fix-admin-lang-imports.cjs "D:/path/to/public/admin/assets"
*/
const path = require('path')
const { fixDir, ADMIN_LANG_URL } = require('./admin-lang-import-utils.cjs')
const targets = process.argv.slice(2)
const dirs = targets.length
? targets.map((d) => path.resolve(d))
: [
path.join(__dirname, '..', 'dist', '.core', 'assets'),
path.join(__dirname, '..', 'dist', 'assets')
]
let total = 0
for (const dir of dirs) {
try {
const n = fixDir(dir)
if (n) console.log(`[fix-admin-lang-imports] ${path.relative(process.cwd(), dir)}: ${n} file(s)`)
total += n
} catch (err) {
console.error(err.message)
console.error('Usage: node scripts/fix-admin-lang-imports.cjs <assets-directory>')
process.exit(1)
}
}
if (total === 0) {
console.log('[fix-admin-lang-imports] no bad imports found')
} else {
console.log(`[fix-admin-lang-imports] fixed ${total} file(s) -> ${ADMIN_LANG_URL}`)
}

View File

@ -0,0 +1,115 @@
/**
* 为单个 addon 生成 Vite lib 构建入口.build/addons/{key}/entry.ts
*
* 入口导出
* - addonKey / sharedVersion
* - views视图路径 动态 import 函数运行时 loadAddonView 调用
* - langs locale 语言 json 静态 import打进 index.js启动时 preload
*
* build-addon.cjs vite build 前调用
* 用法node scripts/generate-addon-entry.cjs <addon-key>
*/
const fs = require('fs')
const path = require('path')
const { ROOT, ADDON_DIR, scanAddonViews, scanAddonLang } = require('./addon-utils.cjs')
const key = process.argv[2] || process.env.ADDON_KEY
if (!key) {
console.error('Usage: node generate-addon-entry.cjs <addon-key>')
process.exit(1)
}
const views = scanAddonViews(key)
const langMap = scanAddonLang(key)
const outDir = path.join(ROOT, '.build', 'addons', key)
fs.mkdirSync(outDir, { recursive: true })
// views 映射:'order/list' => () => import('.../order/list.vue')
const viewLines = views.map((v) =>
` '${v.replace(/'/g, "\\'")}': () => import('@/addon/${key}/views/${v}.vue'),`
).join('\n')
// components 映射diy/poster/printer/delivery 等子目录下的可复用组件
// 供 loadAddonComponent / diy 编辑页在生产环境使用
const componentSubdirs = ['diy/components', 'diy_form/components', 'poster/components', 'printer/components', 'delivery/components']
const componentLines = []
for (const sub of componentSubdirs) {
const dir = path.join(ADDON_DIR, key, 'views', sub)
if (!fs.existsSync(dir)) continue
const files = fs.readdirSync(dir).filter((f) => f.endsWith('.vue'))
for (const f of files) {
const name = f.replace('.vue', '')
componentLines.push(` '${sub}/${name}': () => import('@/addon/${key}/views/${sub}/${f}'),`)
}
}
const componentBlock = componentLines.length ? componentLines.join('\n') : ''
// 插件 router/index.ts 中的 ROUTE/NO_LOGIN_ROUTES 不做 re-export
// 原因:
// 1. 生产环境下插件路由由 core 通过后端菜单 API 下发src/router/index.ts:13 DEV only
// 2. router/index.ts 通常引用 @/layout/index.vue该文件又导入 @/utils/addon-loader
// 其中的 import.meta.glob 会匹配数百个核心 .vue 文件,全部打进插件包导致 404
let routerExport = ''
// 如需开发调试,设置 ADDON_ENTRY_DEV=1 环境变量
const isDev = process.env.ADDON_ENTRY_DEV === '1'
const routerPath = path.join(ADDON_DIR, key, 'router', 'index.ts')
if (fs.existsSync(routerPath)) {
const routerText = fs.readFileSync(routerPath, 'utf-8')
const routerExports = []
if (/export\s+(?:const|let|var)\s+ROUTE\b/.test(routerText) || /export\s*\{[^}]*\bROUTE\b/.test(routerText)) {
routerExports.push('ROUTE')
}
if (/export\s+(?:const|let|var)\s+NO_LOGIN_ROUTES\b/.test(routerText) || /export\s*\{[^}]*\bNO_LOGIN_ROUTES\b/.test(routerText)) {
routerExports.push('NO_LOGIN_ROUTES')
}
if (routerExports.length && isDev) {
routerExport = `export { ${routerExports.join(', ')} } from '@/addon/${key}/router/index.ts'\n`
}
if (routerExports.length && !isDev) {
console.log(`[addon-entry] ${key}: skipping ROUTE/NO_LOGIN_ROUTES re-export (prod: routes from backend API)`)
}
}
/** 语言 json 文件名含点号,变量名需消毒为合法标识符 */
function safeImportVar(locale, file) {
return 'lang_' + `${locale}_${file}`.replace(/[^a-zA-Z0-9_]/g, '_')
}
const langImports = []
const langByLocale = {}
for (const [locale, files] of Object.entries(langMap)) {
langByLocale[locale] = []
for (const file of files) {
const varName = safeImportVar(locale, file)
langImports.push(`import ${varName} from '@/addon/${key}/lang/${locale}/${file}.json'`)
langByLocale[locale].push(` '${file.replace(/'/g, "\\'")}': ${varName}`)
}
}
const langsBlock = Object.keys(langByLocale).length
? `export const langs: Record<string, Record<string, Record<string, string>>> = {\n${Object.entries(langByLocale).map(([locale, entries]) =>
` '${locale.replace(/'/g, "\\'")}': {\n${entries.join(',\n')}\n }`
).join(',\n')
}\n}\n`
: 'export const langs: Record<string, Record<string, Record<string, string>>> = {}\n'
const langCount = Object.values(langMap).reduce((n, files) => n + files.length, 0)
const content = `/* AUTO-GENERATED entry for addon "${key}" */
${langImports.join('\n')}
export const addonKey = '${key}'
export const sharedVersion = 'admin-core-1.0.0'
export const views: Record<string, () => Promise<any>> = {
${viewLines}
}
export const components: Record<string, () => Promise<any>> = {
${componentBlock}
}
${langsBlock}${routerExport}`
fs.writeFileSync(path.join(outDir, 'entry.ts'), content, 'utf-8')
console.log(`[addon-entry] ${key}: ${views.length} views, ${langCount} lang files -> ${path.relative(ROOT, outDir)}/entry.ts`)

View File

@ -0,0 +1,67 @@
/**
* 手动 drop-in 插件后登记到 assets/addons/index.json可选
*
* 场景从另一个 admin 项目拷贝 assets/addons/{key}/ 到目标站点
* 运行时也会在登录后根据后端已安装插件 + 菜单自动探测 index.js不强制依赖本脚本
*
* 用法
* node scripts/register-addon.cjs <addon-key> [deploy-dir]
* 示例
* node scripts/register-addon.cjs sd_minsu
* node scripts/register-addon.cjs sd_minsu "D:/path/to/public/admin"
*/
const fs = require('fs')
const path = require('path')
const { ROOT } = require('./addon-utils.cjs')
const key = process.argv[2]
const deployDir = process.argv[3] ? path.resolve(process.argv[3]) : path.join(ROOT, 'dist')
if (!key) {
console.error('Usage: node scripts/register-addon.cjs <addon-key> [deploy-dir]')
process.exit(1)
}
const addonDir = path.join(deployDir, 'assets', 'addons', key)
const indexPath = path.join(deployDir, 'assets', 'addons', 'index.json')
const entryPath = path.join(addonDir, 'index.js')
const manifestPath = path.join(addonDir, 'manifest.json')
if (!fs.existsSync(entryPath)) {
console.error(`[register-addon] missing ${entryPath}`)
console.error('[register-addon] copy the full addon folder (index.js + chunks + lang/) first')
process.exit(1)
}
if (!fs.existsSync(manifestPath)) {
const manifest = {
key,
version: '1.0.0',
sharedVersion: 'admin-core-1.0.0',
entry: './index.js',
langBase: './lang/'
}
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n', 'utf-8')
console.log(`[register-addon] wrote manifest.json`)
}
let data = { keys: [], sharedVersion: 'admin-core-1.0.0' }
if (fs.existsSync(indexPath)) {
try {
data = JSON.parse(fs.readFileSync(indexPath, 'utf-8'))
} catch {
console.warn('[register-addon] index.json parse failed, recreating')
}
}
const keys = new Set(Array.isArray(data.keys) ? data.keys : [])
const before = keys.size
keys.add(key)
data.keys = [...keys].sort()
fs.mkdirSync(path.dirname(indexPath), { recursive: true })
fs.writeFileSync(indexPath, JSON.stringify(data, null, 2) + '\n', 'utf-8')
console.log(`[register-addon] ${key} registered in assets/addons/index.json (${before} -> ${data.keys.length} keys)`)
console.log(`[register-addon] keys: ${data.keys.join(', ')}`)
console.log('[register-addon] reminder: backend must also install this addon (menu API returns addon=sd_minsu)')

View File

@ -0,0 +1,30 @@
/**
* 统一用 node 直接启动 vite避免 Windows npx + shell 触发 spawn EPERM
*
* 用法node scripts/run-vite.cjs build --config vite.config.shared.ts
*/
const path = require('path')
const { spawnSync } = require('child_process')
const ROOT = path.resolve(__dirname, '..')
const VITE_BIN = path.join(ROOT, 'node_modules', 'vite', 'bin', 'vite.js')
require('./ensure-esbuild.cjs')
const args = process.argv.slice(2)
if (!args.length) {
console.error('Usage: node scripts/run-vite.cjs <vite-args...>')
process.exit(1)
}
const r = spawnSync(process.execPath, [VITE_BIN, ...args], {
cwd: ROOT,
env: {
...process.env,
NODE_OPTIONS: process.env.NODE_OPTIONS || '--max-old-space-size=4096'
},
stdio: 'inherit',
shell: false
})
process.exit(r.status ?? 1)

View File

@ -0,0 +1,150 @@
/**
* Core / Addon 共用的运行时 external配置
*
* 生产构建时vue / element-plus / @/lang 等不打进 bundle
* index.html Import Map 指向 /admin/assets/shared/*.js
* 保证全应用只有一份 Vue 实例与一套 i18n
*/
const SHARED_PACKAGES = [
'vue',
'vue-router',
'pinia',
'element-plus',
'@element-plus/icons-vue',
'axios',
'vue-i18n',
'core-shared'
]
/** build-shared 按此顺序逐个 vite lib 构建core-shared/admin-lang 最后) */
const SHARED_BUILD_ORDER = [
'vue',
'vue-router',
'pinia',
'element-plus',
'icons-vue',
'axios',
'vue-i18n',
'core-shared',
'admin-lang'
]
/** build-shared 包名 -> npm 模块名 */
const PKG_TO_MODULE = {
vue: 'vue',
'vue-router': 'vue-router',
pinia: 'pinia',
'element-plus': 'element-plus',
'icons-vue': '@element-plus/icons-vue',
axios: 'axios',
'vue-i18n': 'vue-i18n'
}
/** 全局 t() / language 的浏览器绝对路径(须与部署前缀 /admin 一致) */
const ADMIN_LANG_URL = '/admin/assets/shared/admin-lang.js'
const CORE_SHARED_URL = '/admin/assets/shared/core-shared.js'
/** 写入 index.html 的 importmapassemble-admin 注入) */
const IMPORT_MAP = {
vue: '/admin/assets/shared/vue.js',
'vue-router': '/admin/assets/shared/vue-router.js',
pinia: '/admin/assets/shared/pinia.js',
'element-plus': '/admin/assets/shared/element-plus.js',
'element-plus/es': '/admin/assets/shared/element-plus.js',
'element-plus/dist/locale/zh-cn.mjs': '/admin/assets/shared/locale/zh-cn.mjs',
'element-plus/dist/locale/en.mjs': '/admin/assets/shared/locale/en.mjs',
'@element-plus/icons-vue': '/admin/assets/shared/icons-vue.js',
axios: '/admin/assets/shared/axios.js',
'vue-i18n': '/admin/assets/shared/vue-i18n.js',
'@/lang': ADMIN_LANG_URL,
'@/utils/common': CORE_SHARED_URL,
'@/utils/request': CORE_SHARED_URL,
'@/app/api/diy': CORE_SHARED_URL,
'@/app/api/sys': CORE_SHARED_URL,
'@/app/api/member': CORE_SHARED_URL,
'@/app/api/site': CORE_SHARED_URL,
'@/components/upload-image': CORE_SHARED_URL,
'@/components/diy-page': CORE_SHARED_URL,
'@/components/map-selector': CORE_SHARED_URL,
'@/components/editor': CORE_SHARED_URL
}
/** 所有需要 external 化并映射到 core-shared 的核心路径 */
const CORE_EXTERNAL_PATHS = Object.keys(IMPORT_MAP).filter(k => k.startsWith('@/'))
/** 是否将 @/lang 或 src/lang 标为 external避免 addon/core 内嵌第二套 i18n */
function isAdminLangExternal(id) {
const norm = id.replace(/\\/g, '/')
if (id === '@/lang' || id.startsWith('@/lang/')) return true
if (norm.includes('/src/lang/')) return true
return false
}
/** Rollup output.paths 中 @/lang 解析到的 URL */
function adminLangExternalPath() {
return ADMIN_LANG_URL
}
/** 核心应用模块路径是否应 external不打进 addon 包) */
function isCoreExternal(id) {
const norm = id.replace(/\\/g, '/')
// 直接匹配 @/ 别名形式Vite resolveId 前的 source
if (CORE_EXTERNAL_PATHS.some(p => norm === p || norm.startsWith(p + '/'))) return true
// 匹配已解析的文件路径(如 /path/src/utils/common.ts
if (norm.includes('/src/') && !norm.includes('/src/addon/')) {
return /\/src\/(utils|app|components)\//.test(norm)
}
return false
}
/** core external 路径统一映射到 core-shared.js */
function coreExternalPath() {
return CORE_SHARED_URL
}
/**
* 是否将依赖标为 external不打进当前 chunk
* element-plus locale / theme-chalk 除外仍由各自包处理
*/
function isSharedExternal(id) {
const norm = id.replace(/\\/g, '/')
if (norm.includes('/style/css') || norm.includes('/style/index')) return false
if (norm.includes('element-plus/dist/')) return false
if (norm.includes('element-plus/theme-chalk')) return false
if (SHARED_PACKAGES.includes(id)) return true
if (SHARED_PACKAGES.some((p) => id === p || id.startsWith(p + '/'))) return true
if (!norm.includes('node_modules/')) return false
return SHARED_PACKAGES.some((p) => {
return norm.includes(`/node_modules/${p}/`) || norm.endsWith(`/node_modules/${p}`)
})
}
/** 构建 shared 单包时,不把「自身」再 external 掉 */
function isSelfSharedPackage(id, pkgKey) {
const mod = PKG_TO_MODULE[pkgKey]
if (!mod) return false
if (id === mod || id.startsWith(mod + '/')) return true
const norm = id.replace(/\\/g, '/')
return norm.includes(`/node_modules/${mod}/`) || norm.endsWith(`/node_modules/${mod}`)
}
/** vite.config.shared.ts 用external 除当前正在打的包以外的 shared 依赖 */
function sharedExternalForBuild(pkgKey) {
return (id) => isSharedExternal(id) && !isSelfSharedPackage(id, pkgKey)
}
module.exports = {
SHARED_PACKAGES,
SHARED_BUILD_ORDER,
PKG_TO_MODULE,
IMPORT_MAP,
ADMIN_LANG_URL,
CORE_SHARED_URL,
isSharedExternal,
isAdminLangExternal,
adminLangExternalPath,
isCoreExternal,
coreExternalPath,
isSelfSharedPackage,
sharedExternalForBuild
}

View File

@ -0,0 +1,48 @@
/**
* 从已构建 JS 中移除 element-plus style/css side-effect import
*
* assemble 最后一步兜底vite 插件可能未覆盖到的 import 语句在此二次清理
* 也可单独运行node scripts/strip-style-imports.cjs [dist-root]
*/
const fs = require('fs')
const path = require('path')
const STYLE_IMPORT_RE = /import\s*["']element-plus[^"']*\/style\/css["'];?/g
function stripFile(filePath) {
const text = fs.readFileSync(filePath, 'utf-8')
if (!STYLE_IMPORT_RE.test(text)) return false
STYLE_IMPORT_RE.lastIndex = 0
const next = text.replace(STYLE_IMPORT_RE, '')
if (next === text) return false
fs.writeFileSync(filePath, next, 'utf-8')
return true
}
function walkJs(dir, changed) {
if (!fs.existsSync(dir)) return
for (const name of fs.readdirSync(dir)) {
const full = path.join(dir, name)
if (fs.statSync(full).isDirectory()) {
walkJs(full, changed)
} else if (name.endsWith('.js')) {
if (stripFile(full)) changed.push(full)
}
}
}
/** @param rootDir dist 根目录,递归处理 assets 下所有 .js */
function stripBuiltAssets(rootDir) {
const assetsDir = path.join(rootDir, 'assets')
const changed = []
walkJs(assetsDir, changed)
return changed
}
if (require.main === module) {
const root = process.argv[2] || path.join(__dirname, '..', 'dist')
const changed = stripBuiltAssets(root)
console.log(`[strip-style-imports] cleaned ${changed.length} files under ${root}/assets`)
}
module.exports = { stripBuiltAssets, stripFile }

View File

@ -0,0 +1,118 @@
/**
* dist/.addons/{key} 同步到 dist/assets/addons/{key}
*
* build-addon 成功后调用也可单独执行以刷新已构建插件到 assets 目录
* 同时写入 manifest.jsonlang/ 备份assets/addons/index.json并更新 build-report.json
*
* 用法node scripts/sync-addon.cjs <addon-key>
*/
const fs = require('fs')
const path = require('path')
const { ROOT } = require('./addon-utils.cjs')
/** Vite addon 构建 staging */
const ADDONS_STAGING = path.join(ROOT, 'dist', '.addons')
const OUT_DIR = path.join(ROOT, 'dist')
/** 记录各 addon 构建成功/失败assemble 据此决定复制哪些 key */
const REPORT_PATH = path.join(ROOT, 'build-report.json')
function rmDir(dir) {
if (!fs.existsSync(dir)) return
fs.rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
}
function copyDir(src, dest) {
fs.mkdirSync(dest, { recursive: true })
for (const name of fs.readdirSync(src)) {
const s = path.join(src, name)
const d = path.join(dest, name)
if (fs.statSync(s).isDirectory()) copyDir(s, d)
else fs.copyFileSync(s, d)
}
}
/** 复制源码 lang 目录,供运行时 fetch json 降级或运维查阅 */
function copyLang(key, destAddonDir) {
const langSrc = path.join(ROOT, 'src', 'addon', key, 'lang')
if (!fs.existsSync(langSrc)) return
copyDir(langSrc, path.join(destAddonDir, 'lang'))
}
/** 运行时 initAddonManifests 会 fetch 此文件 */
function writeAddonManifest(key, destAddonDir) {
const manifest = {
key,
version: '1.0.0',
sharedVersion: 'admin-core-1.0.0',
entry: './index.js',
langBase: './lang/'
}
fs.writeFileSync(
path.join(destAddonDir, 'manifest.json'),
JSON.stringify(manifest, null, 2) + '\n',
'utf-8'
)
}
function readReport() {
if (!fs.existsSync(REPORT_PATH)) return { success: [], failed: [] }
return JSON.parse(fs.readFileSync(REPORT_PATH, 'utf-8'))
}
function writeReport(report) {
fs.writeFileSync(REPORT_PATH, JSON.stringify(report, null, 2) + '\n', 'utf-8')
}
/** 将 key 记入 success 列表,并从 failed 中移除 */
function markSuccess(key) {
const report = readReport()
const success = new Set(report.success || [])
success.add(key)
report.success = [...success].sort()
report.failed = (report.failed || []).filter((f) => f.key !== key)
writeReport(report)
}
/** 浏览器启动时 fetch/admin/assets/addons/index.json */
function writeAddonsIndex(keys) {
const addonsDir = path.join(OUT_DIR, 'assets', 'addons')
fs.mkdirSync(addonsDir, { recursive: true })
fs.writeFileSync(
path.join(addonsDir, 'index.json'),
JSON.stringify({ keys, sharedVersion: 'admin-core-1.0.0' }, null, 2) + '\n',
'utf-8'
)
}
function syncAddon(key) {
const src = path.join(ADDONS_STAGING, key)
if (!fs.existsSync(src)) {
console.error(`[sync-addon] missing staging: ${src}`)
process.exit(1)
}
const entry = path.join(src, 'index.js')
if (!fs.existsSync(entry)) {
console.error(`[sync-addon] missing ${key}/index.js — rebuild addon first`)
process.exit(1)
}
const dest = path.join(OUT_DIR, 'assets', 'addons', key)
rmDir(dest)
fs.cpSync(src, dest, { recursive: true, force: true })
copyLang(key, dest)
writeAddonManifest(key, dest)
markSuccess(key)
const report = readReport()
writeAddonsIndex(report.success || [key])
console.log(`[sync-addon] ${key} -> dist/assets/addons/${key}/`)
}
const key = process.argv[2] || process.env.ADDON_KEY
if (!key) {
console.error('Usage: node scripts/sync-addon.cjs <addon-key>')
process.exit(1)
}
syncAddon(key)

View File

@ -0,0 +1,53 @@
/**
* Core 构建后校验build:core vite build 之后assemble 之前执行
*
* 检查项
* 1. dist/.core/manifest.json 存在
* 2. 入口 bundle 不含旧版 loadAddonCommonLocales stale 缓存
* 3. 含新版 preloadAllAddonLangs 逻辑
* 4. 修复并校验 admin-lang import 路径
*/
const fs = require('fs')
const path = require('path')
const core = path.join(__dirname, '..', 'dist', '.core')
const manifestPath = path.join(core, 'manifest.json')
if (!fs.existsSync(manifestPath)) {
console.error('[verify-core-lang] missing dist/.core/manifest.json — run build:core first')
process.exit(1)
}
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'))
const entry = manifest['index.html']?.file
const code = fs.readFileSync(path.join(core, entry), 'utf-8')
// 旧构建特征:仅加载 common.json 或旧函数名
const stale = code.includes('loadAddonCommonLocales') || code.includes('async function os(e,t=["zh-cn","en"])')
const hasPreload = code.includes('preloadAllAddonLangs') || code.includes('.langs')
if (stale) {
console.error('[verify-core-lang] STALE bundle:', entry)
console.error(' still contains old loadAddonCommonLocales — delete dist/.core and rebuild')
process.exit(1)
}
if (!hasPreload) {
console.warn('[verify-core-lang] warn: preloadAllAddonLangs string not found (may be minified)')
}
const assetsDir = path.join(core, 'assets')
const { fixDir, hasBadImport } = require('./admin-lang-import-utils.cjs')
fixDir(assetsDir)
if (fs.existsSync(assetsDir)) {
for (const name of fs.readdirSync(assetsDir)) {
if (!name.endsWith('.js')) continue
const code = fs.readFileSync(path.join(assetsDir, name), 'utf-8')
if (hasBadImport(code)) {
console.error('[verify-core-lang] bad admin-lang import still in', name)
process.exit(1)
}
}
}
console.log('[verify-core-lang] ok:', entry)

View File

@ -0,0 +1,47 @@
/**
* Vite 插件构建期剥离 element-plus 按需 style/css import
*
* 背景
* - 全局样式已在 src/styles/index.scss 引入 element-plus
* - Core/Addon external element-plus 残留的 style/css side-effect import
* 会进入 chunk 且浏览器无法通过 importmap 解析导致运行时报错
*
* 用于 vite.config.core.ts / vite.config.addon.ts
*/
const STYLE_IMPORT_RE = /import\s*["']element-plus[^"']*["'];?/g
function isElementPlusStyleImport(source) {
const norm = source.replace(/\\/g, '/')
if (!norm.includes('element-plus')) return false
// 匹配 style/css、style/index、theme-chalk、dist/{comp}/style 等
if (/\/style\/(css|index)(\.mjs|\.js)?$/i.test(norm)) return true
if (/\/theme-chalk\//i.test(norm)) return true
if (/\/dist\/[^/]+\/style\//i.test(norm)) return true
return false
}
function stripElementPlusStylePlugin() {
const EMPTY_ID = '\0element-plus-empty-style'
return {
name: 'strip-element-plus-style',
enforce: 'pre',
// resolve 阶段将 style import 指向空模块
resolveId(source) {
if (isElementPlusStyleImport(source)) return EMPTY_ID
return null
},
load(id) {
if (id === EMPTY_ID) return 'export {}'
return null
},
// generateBundle 再扫一遍 chunk 文本,删除遗漏的 import 语句
generateBundle(_, bundle) {
for (const item of Object.values(bundle)) {
if (item.type !== 'chunk') continue
item.code = item.code.replace(STYLE_IMPORT_RE, '')
}
}
}
}
module.exports = { stripElementPlusStylePlugin, isElementPlusStyleImport }

View File

@ -1,36 +1,90 @@
import axios from 'axios'
import request from '@/utils/request' import request from '@/utils/request'
import storage from '@/utils/storage'
/** export const CLOUD_COMPILE_BASE_URL = location.protocol + '//go.site.niucloud.com'
*
*/ function createCloudCompileRequest() {
export function cloudBuild(params: Record<string, any> = {}) { const instance = axios.create({
return request.post('niucloud/build', params) baseURL: CLOUD_COMPILE_BASE_URL,
timeout: 0,
headers: {
'lang': storage.get('lang') ?? 'zh-cn'
}
})
instance.interceptors.request.use(
(config) => {
const token = storage.get('token')
if (token) {
config.headers['token'] = token
}
return config
},
(err) => {
return Promise.reject(err)
}
)
instance.interceptors.response.use(
(response) => {
return response.data
},
(err) => {
return Promise.reject(err)
}
)
return instance
}
const cloudCompileRequest = createCloudCompileRequest()
export function cloudBuild() {
return request.post('niucloud/build')
} }
/**
*
*/
export function getCloudBuildTask() { export function getCloudBuildTask() {
return request.get('niucloud/build') return request.get('niucloud/build')
} }
/**
*
*/
export function getCloudBuildLog() { export function getCloudBuildLog() {
return request.get('niucloud/build/log') return request.get('niucloud/build/log')
} }
/**
*
*/
export function clearCloudBuildTask() { export function clearCloudBuildTask() {
return request.post('niucloud/build/clear') return request.post('niucloud/build/clear', {}, { showErrorMessage: false, showSuccessMessage: false })
} }
/**
*
*/
export function preBuildCheck() { export function preBuildCheck() {
return request.get('niucloud/build/check', { showErrorMessage: false }) return request.get('niucloud/build/check')
}
export function getCloudBuildQueuePosition(taskId: string) {
return cloudCompileRequest.get('/cloud/queue_position', { params: { task_id: taskId } })
}
export function getCloudBuildSseUrl(taskId: string): string {
return `${CLOUD_COMPILE_BASE_URL}/cloud/sse?task_id=${taskId}`
}
export function getCloudCompileLocalUrl() {
return request.get('niucloud/build/get_local_url')
}
export function setCloudCompileLocalUrl(url: string) {
return request.post('niucloud/build/set_local_url', { url })
}
export function startServerDownload(taskId: string, downloadUrl: string, authorizeCode: string, timestamp: string) {
return request.post('niucloud/build/start_server_download', {
task_id: taskId,
download_url: downloadUrl,
authorize_code: authorizeCode,
timestamp
})
}
export function getSseBuildLog(taskId: string) {
return request.get('niucloud/build/get_sse_build_log', { params: { task_id: taskId } })
} }

View File

@ -77,6 +77,15 @@ export function getWeappUploadLog(key: string) {
return request.get(`weapp/upload/${ key }`) return request.get(`weapp/upload/${ key }`)
} }
/**
*
* @param key
* @returns
*/
export function fetchWeappUploadLog(key: string) {
return request.get(`weapp/upload_log/${ key }`)
}
/***************************************************** 管理端 ****************************************************/ /***************************************************** 管理端 ****************************************************/
/** /**

View File

@ -167,7 +167,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref, watch, computed } from 'vue' import { ref, watch, computed } from 'vue'
import { t } from '@/lang' import { t } from '@/lang'
import { getCloudBuildLog, getCloudBuildTask, cloudBuild, clearCloudBuildTask, preBuildCheck } from '@/app/api/cloud' import { getCloudBuildLog, getCloudBuildTask, cloudBuild, clearCloudBuildTask, preBuildCheck, getCloudBuildQueuePosition, getCloudBuildSseUrl, CLOUD_COMPILE_BASE_URL, startServerDownload, getSseBuildLog } from '@/app/api/cloud'
import { getInstalledAddonList } from '@/app/api/addon' import { getInstalledAddonList } from '@/app/api/addon'
import { Terminal, TerminalFlash } from 'vue-web-terminal' import { Terminal, TerminalFlash } from 'vue-web-terminal'
import 'vue-web-terminal/lib/theme/dark.css' import 'vue-web-terminal/lib/theme/dark.css'
@ -196,8 +196,277 @@ const handleSelectionChange = (rows) => {
} }
let cloudBuildLog = [] let cloudBuildLog = []
let eventSource: EventSource | null = null
interface SSEMessage {
type: string
task_id?: string
percent?: number
action?: string
msg?: string
code?: string
time?: string
download_url?: string
download_percent?: number
downloaded_bytes?: number
total_bytes?: number
authorize_code?: string
timestamp?: string
}
const connectSSE = (taskId: string) => {
if (eventSource) {
eventSource.close()
}
const url = getCloudBuildSseUrl(taskId)
eventSource = new EventSource(url)
eventSource.onopen = () => {
console.log('SSE connected')
}
eventSource.onmessage = (event) => {
try {
const data: SSEMessage = JSON.parse(event.data)
handleSSEMessage(data)
} catch (e) {
console.error('SSE parse error:', e)
}
}
eventSource.addEventListener('progress', (event) => {
try {
const data: SSEMessage = JSON.parse((event as MessageEvent).data)
handleSSEMessage(data)
} catch (e) {
console.error('SSE progress parse error:', e)
}
})
eventSource.addEventListener('complete', (event) => {
try {
const data: SSEMessage = JSON.parse((event as MessageEvent).data)
handleSSEMessage(data)
} catch (e) {
console.error('SSE complete parse error:', e)
}
})
eventSource.addEventListener('failed', (event) => {
try {
const data: SSEMessage = JSON.parse((event as MessageEvent).data)
handleSSEMessage(data)
} catch (e) {
console.error('SSE failed parse error:', e)
}
})
eventSource.addEventListener('heartbeat', () => {
console.log('SSE heartbeat')
})
eventSource.addEventListener('download_start', (event) => {
try {
const data: SSEMessage = JSON.parse((event as MessageEvent).data)
handleSSEMessage(data)
} catch (e) {
console.error('SSE download_start parse error:', e)
}
})
eventSource.addEventListener('download_progress', (event) => {
try {
const data: SSEMessage = JSON.parse((event as MessageEvent).data)
handleSSEMessage(data)
} catch (e) {
console.error('SSE download_progress parse error:', e)
}
})
eventSource.addEventListener('download_complete', (event) => {
try {
const data: SSEMessage = JSON.parse((event as MessageEvent).data)
handleSSEMessage(data)
} catch (e) {
console.error('SSE download_complete parse error:', e)
}
})
eventSource.onerror = (error) => {
console.error('SSE error:', error)
setTimeout(() => {
if (showDialog.value && cloudBuildTask.value) {
getCloudBuildLogFn()
}
}, 5000)
}
}
const handleSSEMessage = (data: SSEMessage) => {
if (!showDialog.value) return
if (data.type === 'progress' && data.action) {
if (!cloudBuildLog.includes(data.action)) {
if (cloudBuildLog.length === 0) {
const storedTime = localStorage.getItem('cloud_build_start_time')
if (storedTime) {
buildStartTime.value = Number(storedTime)
} else {
const now = Date.now()
buildStartTime.value = now
localStorage.setItem('cloud_build_start_time', String(now))
}
buildDuration.value = Math.floor((Date.now() - buildStartTime.value) / 1000)
buildTimer && clearInterval(buildTimer)
buildTimer = setInterval(() => {
if (buildStartTime.value) {
buildDuration.value = Math.floor((Date.now() - buildStartTime.value) / 1000)
}
}, 1000)
terminalRef.value.execute('clear')
terminalRef.value.execute('开始编译')
}
if (data.action.indexOf('云编译任务正在排队') != -1) {
cloudQueue = data.action
cloudBuildLog.push(data.action)
return
} else {
cloudQueue = null
}
terminalRef.value.pushMessage({ content: `${data.action}` })
cloudBuildLog.push(data.action)
if (data.code === '0' && data.msg) {
errorAnalysis.value = {}
errorInfo.value = data.msg
terminalRef.value.pushMessage({ content: data.msg, class: 'error' })
timeloading.value = false
active.value = 'error'
terminalRef.value.execute('clear')
clearCloudBuildTask()
buildTimer && clearInterval(buildTimer)
buildTimer = null
localStorage.removeItem('cloud_build_start_time')
localStorage.removeItem('cloud_build_task')
closeSSE()
}
}
} else if (data.type === 'complete') {
terminalRef.value.pushMessage({ content: '编译完成' })
} else if (data.type === 'failed') {
errorInfo.value = data.msg || '编译失败'
active.value = 'error'
terminalRef.value.execute('clear')
clearCloudBuildTask()
buildTimer && clearInterval(buildTimer)
buildTimer = null
localStorage.removeItem('cloud_build_start_time')
localStorage.removeItem('cloud_build_task')
closeSSE()
} else if (data.type === 'download_start') {
if (data.action && !cloudBuildLog.includes(data.action)) {
terminalRef.value.pushMessage({ content: data.action })
cloudBuildLog.push(data.action)
}
} else if (data.type === 'download_progress') {
const percent = data.download_percent || 0
const downloaded = data.downloaded_bytes || 0
const total = data.total_bytes || 0
const downloadedMB = (downloaded / 1024 / 1024).toFixed(2)
const totalMB = (total / 1024 / 1024).toFixed(2)
terminalRef.value.pushMessage({ content: `下载进度: ${percent}% (${downloadedMB}MB / ${totalMB}MB)` })
} else if (data.type === 'download_complete') {
if (data.action && !cloudBuildLog.includes(data.action)) {
terminalRef.value.pushMessage({ content: data.action })
cloudBuildLog.push(data.action)
}
if (data.download_url && data.task_id) {
startServerDownloadFn(data.task_id, data.download_url, data.authorize_code, data.timestamp)
}
}
}
let downloadPollingTimer: number | null = null
const startServerDownloadFn = async (taskId: string, downloadUrl: string, authorizeCode: string, timestamp: string) => {
try {
terminalRef.value.pushMessage({ content: '正在启动后台下载...' })
const parseUrl = new URL(downloadUrl, CLOUD_COMPILE_BASE_URL)
const authorize_code = parseUrl.searchParams.get('authorize_code') || authorizeCode || ''
const ts = parseUrl.searchParams.get('timestamp') || timestamp || ''
await startServerDownload(taskId, downloadUrl, authorize_code, ts)
downloadPollingTimer = setTimeout(async () => {
if (!downloadPollingTimer) return
try {
const res = await getSseBuildLog(taskId)
if (res.code === 1) {
const { status, percent, msg } = res.data
if (status === 'downloading') {
terminalRef.value.pushMessage({ content: `下载进度: ${percent}% - ${msg}` })
} else if (status === 'unzipping') {
terminalRef.value.pushMessage({ content: msg })
} else if (status === 'completed') {
terminalRef.value.pushMessage({ content: '部署完成!' })
downloadPollingTimer && clearInterval(downloadPollingTimer)
downloadPollingTimer = null
active.value = 'complete'
timeloading.value = false
terminalRef.value.execute('clear')
clearCloudBuildTask()
buildTimer && clearInterval(buildTimer)
buildTimer = null
localStorage.removeItem('cloud_build_start_time')
localStorage.removeItem('cloud_build_task')
closeSSE()
ElMessage.success('编译部署完成')
} else if (status === 'error') {
terminalRef.value.pushMessage({ content: `错误: ${msg}`, class: 'error' })
downloadPollingTimer && clearInterval(downloadPollingTimer)
downloadPollingTimer = null
errorInfo.value = msg
active.value = 'error'
timeloading.value = false
clearCloudBuildTask()
closeSSE()
}
} else {
terminalRef.value.pushMessage({ content: '获取下载进度失败', class: 'error' })
downloadPollingTimer && clearInterval(downloadPollingTimer)
downloadPollingTimer = null
}
} catch (e) {
console.error('Polling error:', e)
}
}, 2000)
} catch (error) {
console.error('启动后台下载失败:', error)
terminalRef.value.pushMessage({ content: '启动后台下载失败', class: 'error' })
errorInfo.value = '启动后台下载失败'
active.value = 'error'
}
}
const closeSSE = () => {
if (eventSource) {
eventSource.close()
eventSource = null
}
if (downloadPollingTimer) {
clearInterval(downloadPollingTimer)
downloadPollingTimer = null
}
}
//
const buildStartTime = ref<number | null>(null) const buildStartTime = ref<number | null>(null)
const buildDuration = ref<number>(0) const buildDuration = ref<number>(0)
let buildTimer: number | null = null let buildTimer: number | null = null
@ -272,10 +541,17 @@ const getCloudBuildLogFn = () => {
data[0].forEach(item => { data[0].forEach(item => {
if (!cloudBuildLog.includes(item.action)) { if (!cloudBuildLog.includes(item.action)) {
if (item.action.indexOf('云编译任务正在排队') != -1) {
cloudQueue = item.action
cloudBuildLog.push(item.action)
return
} else {
cloudQueue = null
}
terminalRef.value.pushMessage({ content: `${item.action}` }) terminalRef.value.pushMessage({ content: `${item.action}` })
cloudBuildLog.push(item.action) cloudBuildLog.push(item.action)
if (item.code == 0) { if (item.code === '0') {
errorAnalysis.value = res.data.error_analysis || {} errorAnalysis.value = res.data.error_analysis || {}
error = item.msg error = item.msg
terminalRef.value.pushMessage({ content: item.msg, class: 'error' }) terminalRef.value.pushMessage({ content: item.msg, class: 'error' })
@ -305,7 +581,7 @@ const getCloudBuildLogFn = () => {
setTimeout(() => { setTimeout(() => {
getCloudBuildLogFn() getCloudBuildLogFn()
}, 2000) }, 5000)
}).catch() }).catch()
} }
@ -342,10 +618,17 @@ const open = async () => {
loading.value = true loading.value = true
active.value = 'build' active.value = 'build'
closeType.value = 'normal' closeType.value = 'normal'
cloudBuildLog = []
if (cloudBuildTask.value) { if (cloudBuildTask.value) {
showDialog.value = true showDialog.value = true
loading.value = false loading.value = false
getCloudBuildLogFn() const taskId = (cloudBuildTask.value as any).task_id
if (taskId) {
connectSSE(taskId)
} else {
getCloudBuildLogFn()
}
return return
} }
@ -355,7 +638,12 @@ const open = async () => {
loading.value = false loading.value = false
cloudBuildTask.value = data cloudBuildTask.value = data
showDialog.value = true showDialog.value = true
getCloudBuildLogFn() const taskId = data.task_id
if (taskId) {
connectSSE(taskId)
} else {
getCloudBuildLogFn()
}
}).catch(() => { }).catch(() => {
showDialog.value = false showDialog.value = false
loading.value = false loading.value = false
@ -399,15 +687,19 @@ const againBuild = () => {
return return
} }
loading.value = true loading.value = true
cloudBuild({ cloudBuildLog = []
addon: selectAddon.value cloudBuild().then(({ data }) => {
}).then(({ data }) => {
active.value = 'build' active.value = 'build'
loading.value = false loading.value = false
cloudBuildTask.value = data cloudBuildTask.value = data
showDialog.value = true showDialog.value = true
localStorage.removeItem('cloud_build_start_time') localStorage.removeItem('cloud_build_start_time')
getCloudBuildLogFn() const taskId = data.task_id
if (taskId) {
connectSSE(taskId)
} else {
getCloudBuildLogFn()
}
}).catch(() => { }).catch(() => {
showDialog.value = false showDialog.value = false
loading.value = false loading.value = false
@ -422,13 +714,19 @@ const selectable = (row: any) => {
* 升级进度动画 * 升级进度动画
*/ */
let flashInterval: null | number = null let flashInterval: null | number = null
const terminalFlash = new TerminalFlash() let terminalFlash = null
let cloudQueue = null
const onExecCmd = (key, command, success, failed, name) => { const onExecCmd = (key, command, success, failed, name) => {
if (command == '开始编译') { if (command == '开始编译') {
terminalFlash = new TerminalFlash()
success(terminalFlash) success(terminalFlash)
const frames = makeIterator(['/', '——', '\\', '|']) const frames = makeIterator(['/', '——', '\\', '|'])
flashInterval = setInterval(() => { flashInterval = setInterval(() => {
terminalFlash.flush('> ' + frames.next().value) if (cloudQueue) {
terminalFlash.flush(cloudQueue + '<br>> ' + frames.next().value)
} else {
terminalFlash.flush('> ' + frames.next().value)
}
}, 150) }, 150)
} }
} }
@ -446,6 +744,7 @@ const makeIterator = (array: string[]) => {
} }
const dialogClose = (done: () => {}) => { const dialogClose = (done: () => {}) => {
closeSSE()
if (active.value == 'build' && cloudBuildTask.value && closeType.value == 'normal') { if (active.value == 'build' && cloudBuildTask.value && closeType.value == 'normal') {
ElMessageBox.confirm( ElMessageBox.confirm(
t('cloudbuild.showDialogCloseTips'), t('cloudbuild.showDialogCloseTips'),
@ -471,6 +770,7 @@ const dialogClose = (done: () => {}) => {
} }
const dialogCancel = () => { const dialogCancel = () => {
closeSSE()
if (active.value == 'build' && cloudBuildTask.value && closeType.value == 'normal') { if (active.value == 'build' && cloudBuildTask.value && closeType.value == 'normal') {
ElMessageBox.confirm( ElMessageBox.confirm(
t('cloudbuild.showDialogCloseTips'), t('cloudbuild.showDialogCloseTips'),
@ -501,10 +801,12 @@ const cloudBuildCheckDirFn = () => {
watch(() => showDialog.value, () => { watch(() => showDialog.value, () => {
if (!showDialog.value) { if (!showDialog.value) {
closeSSE()
cloudBuildTask.value = null cloudBuildTask.value = null
active.value = 'build' active.value = 'build'
cloudBuildLog = [] cloudBuildLog = []
flashInterval && clearInterval(flashInterval) flashInterval && clearInterval(flashInterval)
terminalFlash && terminalFlash.finish()
buildTimer && clearInterval(buildTimer) buildTimer && clearInterval(buildTimer)
buildStartTime.value = null buildStartTime.value = null
buildDuration.value = 0 buildDuration.value = 0

View File

@ -114,12 +114,6 @@
<div class="flex flex-col" v-show="active == 'backup'"> <div class="flex flex-col" v-show="active == 'backup'">
<el-scrollbar> <el-scrollbar>
<div class="bg-[#fff] my-3"> <div class="bg-[#fff] my-3">
<div class="p-[20px] mt-[50px] mx-[10px] border-[1px] border-[#E6E6E6] rounded-[10px]">
<div class="flex justify-between items-center mt-[-9px]">
<el-checkbox v-model="upgradeOption.is_need_cloudbuild" :label="t('upgrade.isNeedCloudbuild')" :true-value="true" :false-value="false" size="large" ></el-checkbox>
</div>
<div class="text-[14px] text-[#374151] mb-[10px]">{{ t('upgrade.cloudbuildTips') }}</div>
</div>
<div class="p-[20px] mt-[20px] mx-[10px] border-[1px] border-[#E6E6E6] rounded-[10px]" v-if="upgradeContent.last_backup"> <div class="p-[20px] mt-[20px] mx-[10px] border-[1px] border-[#E6E6E6] rounded-[10px]" v-if="upgradeContent.last_backup">
<div class="flex justify-between items-center mt-[-9px]"> <div class="flex justify-between items-center mt-[-9px]">
<el-checkbox v-model="upgradeOption.is_need_backup" :label="t('upgrade.isNeedBackup')" :true-value="true" :false-value="false" size="large" ></el-checkbox> <el-checkbox v-model="upgradeOption.is_need_backup" :label="t('upgrade.isNeedBackup')" :true-value="true" :false-value="false" size="large" ></el-checkbox>
@ -198,8 +192,7 @@
<el-button v-if="step == 1 && upgradeContent.content.length && isAllowUpgrade" @click="step = 2" type="primary">{{ t("upgrade.upgradeButton") }}</el-button> <el-button v-if="step == 1 && upgradeContent.content.length && isAllowUpgrade" @click="step = 2" type="primary">{{ t("upgrade.upgradeButton") }}</el-button>
<el-button type="primary" v-show="step == 2 && showTerminal && upgradeTask && !errorDialog" :loading="timeloading" class="!w-[140px]">已用时 {{ formatUpgradeDuration }}</el-button> <el-button type="primary" v-show="step == 2 && showTerminal && upgradeTask && !errorDialog" :loading="timeloading" class="!w-[140px]">已用时 {{ formatUpgradeDuration }}</el-button>
<template v-if="step == 2 && active != 'complete'"> <template v-if="step == 2 && active != 'complete'">
<!-- <el-button v-if="active == 'content'" @click="showDialog = false">{{ t("return") }}</el-button>--> <el-button type="primary" :disabled="!is_pass" v-if="active == 'upgrade' && !upgradeTask" @click="showUpgradeOption">{{ t("nextStep") }}</el-button>
<el-button type="primary" :disabled="!is_pass" v-if="active == 'upgrade' && !upgradeTask" @click="() => { active = 'backup'; numberOfSteps = 1 }">{{ t("nextStep") }}</el-button>
<el-button v-if="active == 'backup'" @click="() => { active = 'upgrade'; numberOfSteps = 1 } ">{{ t("prev") }}</el-button> <el-button v-if="active == 'backup'" @click="() => { active = 'upgrade'; numberOfSteps = 1 } ">{{ t("prev") }}</el-button>
<el-button type="primary" v-if="active == 'backup'" :loading="loading" @click="() => { upgradeAddonFn() }">{{ t("nextStep") }}</el-button> <el-button type="primary" v-if="active == 'backup'" :loading="loading" @click="() => { upgradeAddonFn() }">{{ t("nextStep") }}</el-button>
<el-button v-if="active == 'complete'" @click="showDialog = false">{{ t("complete") }}</el-button> <el-button v-if="active == 'complete'" @click="showDialog = false">{{ t("complete") }}</el-button>
@ -283,7 +276,7 @@ const retrySecond = ref(30)
let retrySecondInterval: any = null let retrySecondInterval: any = null
const upgradeOption = ref({ const upgradeOption = ref({
is_need_backup: true, is_need_backup: true,
is_need_cloudbuild: true is_need_cloudbuild: false
}) })
// backupCode backupSql // backupCode backupSql
@ -356,8 +349,14 @@ const getUpgradeTaskFn = () => {
data.log.forEach((item) => { data.log.forEach((item) => {
if (!upgradeLog.includes(item)) { if (!upgradeLog.includes(item)) {
terminalRef.value.pushMessage({ content: `${item}` }) if (item.indexOf('云编译任务正在排队') != -1) {
upgradeLog.push(item) cloudQueue = item
upgradeLog.push(item)
} else {
cloudQueue = null
terminalRef.value.pushMessage({ content: `${item}` })
upgradeLog.push(item)
}
} }
}) })
// //
@ -537,6 +536,15 @@ const handleUpgrade = async () => {
}) })
} }
const showUpgradeOption = () => {
if (upgradeContent.value.last_backup) {
active.value = 'backup';
numberOfSteps.value = 1
} else {
upgradeAddonFn()
}
}
const upgradeAddonFn = () => { const upgradeAddonFn = () => {
if (!is_pass.value) return if (!is_pass.value) return
if (loading.value) return if (loading.value) return
@ -627,12 +635,18 @@ const open = (addonKey: string = '', callback = null, otherData: any = {}) => {
*/ */
let flashInterval: any = null let flashInterval: any = null
const terminalFlash = new TerminalFlash() const terminalFlash = new TerminalFlash()
let cloudQueue = null
const onExecCmd = (key, command, success, failed, name) => { const onExecCmd = (key, command, success, failed, name) => {
if (command == '开始升级') { if (command == '开始升级') {
success(terminalFlash) success(terminalFlash)
const frames = makeIterator(['/', '——', '\\', '|']) const frames = makeIterator(['/', '——', '\\', '|'])
flashInterval = setInterval(() => { flashInterval = setInterval(() => {
terminalFlash.flush('> ' + frames.next().value) if (cloudQueue) {
terminalFlash.flush(cloudQueue + '<br>> ' + frames.next().value)
} else {
terminalFlash.flush('> ' + frames.next().value)
}
}, 150) }, 150)
} }
} }

View File

@ -0,0 +1,441 @@
<template>
<el-dialog v-model="showDialog" title="小程序上传" width="850px" :close-on-click-modal="false" :close-on-press-escape="false" :before-close="dialogClose">
<div v-show="active == 'upload'" class="h-[50vh]" v-loading="loading">
<div class="h-[45vh]" v-show="cloudBuildTask">
<terminal ref="terminalRef" :name="`weappupload-${terminalId}`" context="" :init-log="null" :show-header="false" :show-log-time="true" @exec-cmd="onExecCmd"/>
</div>
<div class="flex justify-end mt-[20px]" v-show="cloudBuildTask">
<el-button @click="dialogCancel()">取消</el-button>
<el-button type="primary" :loading="timeloading" class="!w-[140px]" v-if="!errorInfo">已用时 {{ formattedDuration }}</el-button>
<el-button type="primary" @click="active = 'error'" v-if="errorInfo">下一步</el-button>
</div>
</div>
<div v-show="active == 'complete'">
<div class="h-[50vh] flex flex-col">
<div class="flex-1 h-0 flex justify-center items-center flex-col">
<el-result icon="success" title="上传成功">
<template #sub-title>
<p class="text-[16px]">小程序上传成功</p>
</template>
<template #extra>
<el-button type="primary" @click="handleComplete">完成</el-button>
</template>
</el-result>
</div>
</div>
</div>
<div v-show="active == 'error'">
<div class="h-[50vh] flex flex-col">
<div class="flex-1 h-0 flex justify-center items-center flex-col">
<el-result icon="error" title="上传失败">
<template #extra>
<p class="text-[14px] text-red-500 mb-[10px]" v-if="errorInfo">错误信息: {{ errorInfo }}</p>
<el-button type="primary" @click="handleErrorNextStep">下一步</el-button>
</template>
</el-result>
</div>
</div>
</div>
</el-dialog>
</template>
<script lang="ts" setup>
import { ref, nextTick, onUnmounted, computed } from 'vue'
import { t } from '@/lang'
import { getWeappUploadLog, fetchWeappUploadLog } from '@/app/api/weapp'
import { CLOUD_COMPILE_BASE_URL } from '@/app/api/cloud'
import { Terminal } from 'vue-web-terminal'
import 'vue-web-terminal/lib/theme/dark.css'
import { ElMessageBox, ElMessage } from 'element-plus'
const showDialog = ref(false)
const terminalId = ref(Date.now())
const cloudBuildTask = ref<any>(null)
const active = ref('upload')
const loading = ref(false)
const terminalRef = ref(null)
const errorInfo = ref('')
const timeloading = ref(false)
const errorAnalysis = ref({})
let uploadLog: any[] = []
let eventSource: EventSource | null = null
const uploadStartTime = ref<number | null>(null)
const uploadDuration = ref(0)
let durationTimer: number | null = null
interface TerminalMessage {
content: string
class?: string
}
let messageQueue: TerminalMessage[] = []
let messageQueueTimer: number | null = null
const MESSAGE_FLUSH_INTERVAL = 300
const flushMessageQueue = () => {
if (!terminalRef.value || messageQueue.length === 0) return
while (messageQueue.length > 0) {
const msg = messageQueue.shift()
if (msg) {
terminalRef.value.pushMessage(msg)
}
}
}
const queueMessage = (content: string, className?: string) => {
messageQueue.push({ content, class: className })
if (messageQueueTimer === null) {
messageQueueTimer = window.setTimeout(() => {
flushMessageQueue()
messageQueueTimer = null
}, MESSAGE_FLUSH_INTERVAL)
}
}
const clearMessageQueue = () => {
messageQueue = []
if (messageQueueTimer !== null) {
clearTimeout(messageQueueTimer)
messageQueueTimer = null
}
}
const formattedDuration = computed(() => {
const seconds = uploadDuration.value
const mins = Math.floor(seconds / 60)
const secs = seconds % 60
return mins > 0 ? `${mins}${secs}` : `${secs}`
})
const onExecCmd = (cmd: string) => {
}
const getWeappSseUrl = (taskId: string): string => {
return `${CLOUD_COMPILE_BASE_URL}/cloud/weapp_sse?task_id=${taskId}`
}
interface SSEMessage {
type: string
task_id?: string
percent?: number
action?: string
msg?: string
code?: string
time?: string
}
const connectSSE = (taskId: string) => {
if (eventSource) {
eventSource.close()
}
const url = getWeappSseUrl(taskId)
eventSource = new EventSource(url)
eventSource.onopen = () => {
console.log('Weapp SSE connected')
}
eventSource.onmessage = (event) => {
try {
const data: SSEMessage = JSON.parse(event.data)
handleSSEMessage(data)
} catch (e) {
console.error('Weapp SSE parse error:', e)
}
}
eventSource.addEventListener('progress', (event) => {
try {
const data: SSEMessage = JSON.parse((event as MessageEvent).data)
handleSSEMessage(data)
} catch (e) {
console.error('Weapp SSE progress parse error:', e)
}
})
eventSource.addEventListener('complete', (event) => {
try {
const data: SSEMessage = JSON.parse((event as MessageEvent).data)
handleSSEMessage(data)
} catch (e) {
console.error('Weapp SSE complete parse error:', e)
}
})
eventSource.addEventListener('failed', (event) => {
try {
const data: SSEMessage = JSON.parse((event as MessageEvent).data)
handleSSEMessage(data)
} catch (e) {
console.error('Weapp SSE failed parse error:', e)
}
})
eventSource.onerror = (error) => {
console.error('Weapp SSE error:', error)
closeSSE()
}
}
const closeSSE = () => {
if (eventSource) {
eventSource.close()
eventSource = null
}
clearMessageQueue()
}
const earlyLogCheck = (taskKey: string): Promise<boolean> => {
return new Promise((resolve) => {
fetchWeappUploadLog(taskKey).then(res => {
const data = (res.data && res.data.data) ? res.data.data : []
if (data[0] && data[0].length) {
const last = data[0][data[0].length - 1]
if (last.code == 1 && last.percent == 100) {
resolve(true)
return
}
if (last.code == 0) {
resolve(true)
return
}
}
resolve(false)
}).catch(() => {
resolve(false)
})
})
}
const handleSSEMessage = (data: SSEMessage) => {
if (!showDialog.value) return
if (data.type === 'progress' && data.action) {
if (!uploadLog.includes(data.action)) {
if (uploadLog.length === 0) {
const now = Date.now()
uploadStartTime.value = now
uploadDuration.value = 0
durationTimer && clearInterval(durationTimer)
durationTimer = setInterval(() => {
if (uploadStartTime.value) {
uploadDuration.value = Math.floor((Date.now() - uploadStartTime.value) / 1000)
}
}, 1000)
nextTick(() => {
if (terminalRef.value) {
terminalRef.value.execute('clear')
terminalRef.value.execute('开始上传')
}
})
}
queueMessage(`${data.action}`)
uploadLog.push(data.action)
if (data.code === '0') {
errorInfo.value = data.msg || '上传失败'
timeloading.value = false
active.value = 'error'
nextTick(() => {
if (terminalRef.value) {
terminalRef.value.execute('clear')
}
})
closeSSE()
uploadDurationTimerClear()
if (props.onError) {
props.onError()
}
return
}
if (data.code === '1' && data.percent === 100) {
nextTick(() => {
if (terminalRef.value) {
terminalRef.value.execute('clear')
}
})
queueMessage('上传完成')
timeloading.value = false
active.value = 'complete'
closeSSE()
uploadDurationTimerClear()
ElMessage.success('上传成功')
if (props.onSuccess) {
props.onSuccess()
}
return
}
}
} else if (data.type === 'complete') {
getWeappUploadLog(data.task_id)
queueMessage('上传完成')
timeloading.value = false
active.value = 'complete'
closeSSE()
uploadDurationTimerClear()
ElMessage.success('上传成功')
if (props.onSuccess) {
props.onSuccess()
}
} else if (data.type === 'failed') {
errorInfo.value = data.msg || '上传失败'
timeloading.value = false
active.value = 'error'
nextTick(() => {
if (terminalRef.value) {
terminalRef.value.execute('clear')
}
})
closeSSE()
uploadDurationTimerClear()
if (props.onError) {
props.onError()
}
}
}
const uploadDurationTimerClear = () => {
if (durationTimer) {
clearInterval(durationTimer)
durationTimer = null
}
uploadStartTime.value = null
}
const dialogClose = (done: () => void) => {
closeSSE()
uploadDurationTimerClear()
if (active.value == 'upload' && cloudBuildTask.value) {
ElMessageBox.confirm(
'确定要关闭上传窗口吗?关闭后将停止当前上传任务',
'提示',
{
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning'
}
).then(() => {
done()
}).catch(() => { })
} else {
done()
}
}
const dialogCancel = () => {
showDialog.value = false
}
const handleComplete = () => {
showDialog.value = false
if (props.onSuccess) {
props.onSuccess()
}
}
const handleErrorNextStep = () => {
showDialog.value = false
if (props.onError) {
props.onError()
}
}
const props = defineProps<{
onSuccess?: () => void
onError?: () => void
}>()
const open = async (taskKey: string, task?: any) => {
loading.value = true
active.value = 'upload'
uploadLog = []
clearMessageQueue()
errorInfo.value = ''
errorAnalysis.value = {}
timeloading.value = true
terminalId.value = Date.now()
const taskData = task || cloudBuildTask.value
if (taskData) {
cloudBuildTask.value = taskData
if (taskData.status == 0) {
loading.value = false
const isCompleted = await earlyLogCheck(taskData.task_key)
if (isCompleted) {
closeSSE()
getWeappUploadLog(taskData.task_key)
showDialog.value = true
active.value = 'complete'
loading.value = false
uploadDuration.value = taskData.update_time - taskData.create_time
nextTick(() => {
if (terminalRef.value) {
terminalRef.value.execute('clear')
terminalRef.value.execute('上传完成')
}
})
return
}
showDialog.value = true
connectSSE(taskData.task_key)
} else if (taskData.status == 1) {
active.value = 'complete'
loading.value = false
uploadDuration.value = taskData.update_time - taskData.create_time
showDialog.value = true
nextTick(() => {
if (terminalRef.value) {
terminalRef.value.execute('clear')
terminalRef.value.execute('上传完成')
}
})
if (props.onSuccess) {
props.onSuccess()
}
} else if (taskData.status == -1 || taskData.status == -2) {
active.value = 'error'
loading.value = false
errorInfo.value = taskData.fail_reason || '上传失败'
showDialog.value = true
nextTick(() => {
if (terminalRef.value) {
terminalRef.value.execute('clear')
terminalRef.value.execute('上传失败')
}
})
if (props.onError) {
props.onError()
}
} else {
loading.value = false
showDialog.value = true
connectSSE(taskData.task_key)
}
} else {
loading.value = false
}
}
const setTask = (task: any) => {
cloudBuildTask.value = task
}
onUnmounted(() => {
closeSSE()
uploadDurationTimerClear()
})
defineExpose({
open,
setTask,
cloudBuildTask,
loading,
showDialog
})
</script>

View File

@ -46,5 +46,28 @@
"uploadWeapp": "上传小程序", "uploadWeapp": "上传小程序",
"undoAudit" : "撤回审核", "undoAudit" : "撤回审核",
"undoAuditTips" : "撤回代码审核,单个账号每天审核撤回次数最多不超过 5 次每天的额度从0点开始生效一个月不超过 10 次。是否要继续撤回?", "undoAuditTips" : "撤回代码审核,单个账号每天审核撤回次数最多不超过 5 次每天的额度从0点开始生效一个月不超过 10 次。是否要继续撤回?",
"helpInfo": "查看帮助" "helpInfo": "查看帮助",
"weappUpload": {
"title": "小程序上传",
"noTask": "暂无上传任务",
"uploadFailed": "上传失败",
"uploadSuccess": "上传成功",
"uploadSuccessDesc": "小程序上传成功,耗时",
"usedTime": "已用时",
"startUpload": "开始上传",
"uploadComplete": "上传完成",
"dialogCloseTips": "确定要关闭上传窗口吗?关闭后将停止当前上传任务",
"errorInfo": "错误信息",
"errorAnalysis": "错误分析",
"complete": "完成",
"return": "返回",
"nextStep": "下一步",
"waitingUpload": "等待上传任务...",
"minute": "分",
"second": "秒",
"dot": ".",
"defaultDesc": "默认为列表版本号递增自定义则为手动输入版本号进行上传首位必须大于1"
},
"minute": "分",
"second": "秒"
} }

View File

@ -110,11 +110,11 @@
<el-form-item prop="code1"> <el-form-item prop="code1">
<el-input v-model.number="form.code1" class="!w-[70px]" :placeholder="t('codePlaceholder')" /> <el-input v-model.number="form.code1" class="!w-[70px]" :placeholder="t('codePlaceholder')" />
</el-form-item> </el-form-item>
<span class="mx-[10px]">.</span> <span class="mx-[10px]">{{ t('weappUpload.dot') }}</span>
<el-form-item prop="code2"> <el-form-item prop="code2">
<el-input v-model.number="form.code2" class="!w-[70px]" :placeholder="t('codePlaceholder')" /> <el-input v-model.number="form.code2" class="!w-[70px]" :placeholder="t('codePlaceholder')" />
</el-form-item> </el-form-item>
<span class="mx-[10px]">.</span> <span class="mx-[10px]">{{ t('weappUpload.dot') }}</span>
<el-form-item prop="code3"> <el-form-item prop="code3">
<el-input v-model.number="form.code3" class="!w-[70px]" :placeholder="t('codePlaceholder')" /> <el-input v-model.number="form.code3" class="!w-[70px]" :placeholder="t('codePlaceholder')" />
</el-form-item> </el-form-item>
@ -155,6 +155,8 @@
</template> </template>
</el-dialog> </el-dialog>
</div> </div>
<component :is="WeappUpload" ref="weappUploadRef" @success="getWeappVersionListFn" @error="getWeappVersionListFn" />
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -168,12 +170,17 @@ import { ElMessageBox } from 'element-plus'
import { AnyObject } from '@/types/global' import { AnyObject } from '@/types/global'
import Storage from '@/utils/storage' import Storage from '@/utils/storage'
import { siteWeappCommit, undoAudit } from "@/app/api/wxoplatform"; import { siteWeappCommit, undoAudit } from "@/app/api/wxoplatform";
import WeappUpload from '@/app/components/weappupload/index.vue'
// 使
const WeappUploadComponent = WeappUpload
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
const pageName = route.meta.title const pageName = route.meta.title
const dialogVisible = ref(false) const dialogVisible = ref(false)
const loading = ref(true) const loading = ref(true)
const weappUploadRef = ref<any>(null)
const weappTableData:{ const weappTableData:{
page: number, page: number,
limit: number, limit: number,
@ -286,19 +293,24 @@ const formRules = reactive({
/** /**
* 获取版本列表 * 获取版本列表
*/ */
const getWeappVersionListFn = (page: number = 1) => { const getWeappVersionListFn = (page?: number) => {
weappTableData.loading = true weappTableData.loading = true
weappTableData.page = page if (page) weappTableData.page = page
getWeappVersionList({ return getWeappVersionList({
page: weappTableData.page, page: weappTableData.page,
limit: weappTableData.limit limit: weappTableData.limit
}).then(res => { }).then(res => {
weappTableData.loading = false weappTableData.loading = false
weappTableData.data = res.data.data weappTableData.data = res.data.data
weappTableData.total = res.data.total weappTableData.total = res.data.total
if (page == 1 && weappTableData.data.length && weappTableData.data[0].status == 0) getWeappUploadLogFn(weappTableData.data[0].task_key)
weappTableData.version_info = res.data.version_info weappTableData.version_info = res.data.version_info
const uploadingTask = res.data.data.find((d: any) => d.status == 0)
if (uploadingTask && weappUploadRef.value) {
weappUploadRef.value.setTask(uploadingTask)
weappUploadRef.value.open(uploadingTask.task_key, uploadingTask)
}
}).catch(() => { }).catch(() => {
weappTableData.loading = false weappTableData.loading = false
}) })
@ -367,15 +379,35 @@ const insert = () => {
return return
} }
if (uploading.value) return if (uploading.value) {
const ref = weappUploadRef.value
if (ref && ref.cloudBuildTask && ref.cloudBuildTask.task_key) {
ref.open(ref.cloudBuildTask.task_key, ref.cloudBuildTask)
} else if (ref) {
ref.showDialog = true
}
return
}
uploading.value = true uploading.value = true
previewContent.value = '' previewContent.value = ''
setWeappVersion(form.value).then(res => { setWeappVersion(form.value).then(res => {
getWeappVersionListFn() const versionId = res.data
getWeappPreviewImage()
uploading.value = false getWeappVersionListFn().then(() => {
const item = weappTableData.data.find((d: any) => d.id === versionId)
if (item && item.task_key) {
const ref = weappUploadRef.value
if (ref) {
ref.open(item.task_key, item)
}
}
getWeappPreviewImage()
uploading.value = false
}).catch(() => {
uploading.value = false
})
}).catch(() => { }).catch(() => {
uploading.value = false uploading.value = false
}) })
@ -388,6 +420,7 @@ const localInsert = () => {
} }
const previewContent = ref('') const previewContent = ref('')
const getWeappPreviewImage = () => { const getWeappPreviewImage = () => {
if (!authCode.value) return if (!authCode.value) return
getWeappPreview().then(res => { getWeappPreview().then(res => {
@ -395,21 +428,55 @@ const getWeappPreviewImage = () => {
}).catch() }).catch()
} }
const currentUploadKey = ref<string | null>(null)
const uploadPollingCount = ref(0)
const maxPollingCount = 150
const getWeappUploadLogFn = (key: string) => { const getWeappUploadLogFn = (key: string) => {
if (!key) return
if (currentUploadKey.value !== key) {
currentUploadKey.value = key
uploadPollingCount.value = 0
}
uploadPollingCount.value++
if (uploadPollingCount.value > maxPollingCount) {
currentUploadKey.value = null
uploadPollingCount.value = 0
uploading.value = false
return
}
getWeappUploadLog(key).then(res => { getWeappUploadLog(key).then(res => {
const data = res.data.data ?? [] if (currentUploadKey.value !== key) return
const data = (res.data && res.data.data) ? res.data.data : []
if (data[0] && data[0].length) { if (data[0] && data[0].length) {
const last = data[0][data[0].length - 1] const last = data[0][data[0].length - 1]
if (last.code == 0) { if (last.code == 0) {
currentUploadKey.value = null
uploadPollingCount.value = 0
getWeappVersionListFn() getWeappVersionListFn()
return return
} }
if (last.code == 1 && last.percent == 100) { if (last.code == 1 && last.percent == 100) {
currentUploadKey.value = null
uploadPollingCount.value = 0
getWeappVersionListFn() getWeappVersionListFn()
getWeappPreviewImage() getWeappPreviewImage()
!Storage.get('weappUploadTipsLock') && (uploadSuccessShowDialog.value = true) !Storage.get('weappUploadTipsLock') && (uploadSuccessShowDialog.value = true)
return return
} }
}
if (currentUploadKey.value === key) {
setTimeout(() => {
getWeappUploadLogFn(key)
}, 2000)
}
}).catch(() => {
if (currentUploadKey.value === key) {
setTimeout(() => { setTimeout(() => {
getWeappUploadLogFn(key) getWeappUploadLogFn(key)
}, 2000) }, 2000)

View File

@ -9,6 +9,7 @@ import { computed, ref } from 'vue'
import { getToken } from '@/utils/common' import { getToken } from '@/utils/common'
import storage from '@/utils/storage' import storage from '@/utils/storage'
import { ElMessage, UploadFile, UploadFiles } from 'element-plus' import { ElMessage, UploadFile, UploadFiles } from 'element-plus'
import envConfig from '@/utils/config'
const prop = defineProps({ const prop = defineProps({
type: { type: {
@ -23,11 +24,11 @@ const uploadRef = ref<Record<string, any> | null>(null)
// //
const upload = computed(() => { const upload = computed(() => {
const headers: Record<string, any> = {} const headers: Record<string, any> = {}
headers[import.meta.env.VITE_REQUEST_HEADER_TOKEN_KEY] = getToken() headers[envConfig.VITE_REQUEST_HEADER_TOKEN_KEY] = getToken()
headers[import.meta.env.VITE_REQUEST_HEADER_SITEID_KEY] = storage.get('siteId') || 0 headers[envConfig.VITE_REQUEST_HEADER_SITEID_KEY] = storage.get('siteId') || 0
return { return {
action: `${import.meta.env.VITE_APP_BASE_URL}/wechat/media/${prop.type}`, action: `${envConfig.VITE_APP_BASE_URL}/wechat/media/${prop.type}`,
multiple: true, multiple: true,
headers, headers,
accept: prop.type == 'image' ? '.bmp,.png,.jpeg,.jpg,.gif' : '.mp4', accept: prop.type == 'image' ? '.bmp,.png,.jpeg,.jpg,.gif' : '.mp4',

View File

@ -213,7 +213,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, reactive, toRaw, watch, inject } from 'vue' import { ref, reactive, toRaw, watch, inject, defineAsyncComponent } from 'vue'
import { t } from '@/lang' import { t } from '@/lang'
import { ArrowLeft } from "@element-plus/icons-vue" import { ArrowLeft } from "@element-plus/icons-vue"
import { img } from '@/utils/common' import { img } from '@/utils/common'
@ -296,10 +296,14 @@ const goBack = () => {
// //
const modulesFiles = import.meta.glob('./components/*.vue', { eager: true }) const modulesFiles = import.meta.glob('./components/*.vue', { eager: true })
const addonModulesFiles = import.meta.glob('@/addon/**/views/diy/components/*.vue', { eager: true })
addonModulesFiles && Object.assign(modulesFiles, addonModulesFiles)
const modules = {} // glob
if (import.meta.env.DEV) {
const addonModulesFiles = import.meta.glob('@/addon/**/views/diy/components/*.vue', { eager: true })
Object.assign(modulesFiles, addonModulesFiles)
}
const modules: Record<string, any> = {}
for (const [key, value] of Object.entries(modulesFiles)) { for (const [key, value] of Object.entries(modulesFiles)) {
const moduleName = key.split('/').pop() const moduleName = key.split('/').pop()
const name = moduleName.split('.')[0] const name = moduleName.split('.')[0]
@ -455,6 +459,33 @@ initPage({
diyStore.components.push(com) diyStore.components.push(com)
} }
} }
// DIY
if (!import.meta.env.DEV && component.value) {
const { loadAddonComponent, getInstalledAddonKeys } = await import('@/utils/addon-loader')
const addonKeys = getInstalledAddonKeys()
for (const type of Object.values(component.value) as any[]) {
if (!type.list) continue
for (const rawKey of Object.keys(type.list)) {
const compDef = type.list[rawKey]
const componentPath = compDef.value?.path || compDef.path || rawKey
if (modules[componentPath]) continue
modules[componentPath] = defineAsyncComponent(() =>
(async () => {
for (const addon of addonKeys) {
const loader = loadAddonComponent(addon, 'diy/components', componentPath)
try {
const result = await loader()
if (result?.default) return result.default
} catch {}
}
throw new Error(`DIY component "${componentPath}" not found in any addon`)
})()
)
}
}
}
console.log( component.value ) console.log( component.value )
loadDiyTemplatePages(data.type) loadDiyTemplatePages(data.type)
@ -469,21 +500,18 @@ initPage({
if (import.meta.env.MODE == 'development') { if (import.meta.env.MODE == 'development') {
// envwap // envwap
// if (wapDomain.value) { if (wapDomain.value) {
// wapUrl.value = wapDomain.value + '/wap' wapUrl.value = wapDomain.value + '/wap'
// repeat = false repeat = false
// setDomain() setDomain()
// } }
// let wap_domain_storage = storage.get('wap_domain') let wap_domain_storage = storage.get('wap_domain')
// if (wap_domain_storage) { if (wap_domain_storage) {
// wapUrl.value = wap_domain_storage wapUrl.value = wap_domain_storage
// repeat = false repeat = false
// setDomain() setDomain()
// } }
wapUrl.value = 'http://localhost:5173/wap'
repeat = false
setDomain()
} }
if (repeat) { if (repeat) {

View File

@ -582,30 +582,9 @@
</div> </div>
<!-- </el-scrollbar> --> <!-- </el-scrollbar> -->
<div class="flex justify-end"> <div class="flex justify-end">
<el-tooltip effect="dark" placement="top"> <el-button type="primary" class="!w-[140px]" :disabled="!installCheckResult.is_pass || cloudInstalling" :loading="localInstalling" @click="handleInstall"
<template #content> >{{ t('install') }}
<div class="w-[400px]"> </el-button>
{{ t('installTips') }}
</div>
</template>
<el-button :disabled="!installCheckResult.is_pass || cloudInstalling" :loading="localInstalling" @click="handleInstall"
>{{ t('localInstall') }}
</el-button>
</el-tooltip>
<el-tooltip effect="dark" placement="top">
<template #content>
<div class="w-[400px]">
{{ t('cloudInstallTips') }}
</div>
</template>
<el-button
type="primary"
:disabled="!installCheckResult.is_pass || localInstalling"
:loading="cloudInstalling"
@click="handleCloudInstall"
>{{ t('cloudInstall') }}
</el-button>
</el-tooltip>
</div> </div>
</div> </div>
<div v-show="installStep == 1 && !errorDialog" class="h-[50vh] mt-[20px]"> <div v-show="installStep == 1 && !errorDialog" class="h-[50vh] mt-[20px]">
@ -1245,12 +1224,18 @@ const installCheckResult = ref({})
let flashInterval = null let flashInterval = null
const terminalFlash = new TerminalFlash() const terminalFlash = new TerminalFlash()
let cloudQueue = null
const onExecCmd = (key, command, success, failed, name) => { const onExecCmd = (key, command, success, failed, name) => {
if (command == '开始安装插件') { if (command == '开始安装插件') {
success(terminalFlash) success(terminalFlash)
const frames = makeIterator(['/', '——', '\\', '|']) const frames = makeIterator(['/', '——', '\\', '|'])
flashInterval = setInterval(() => { flashInterval = setInterval(() => {
terminalFlash.flush('> ' + frames.next().value) if (cloudQueue) {
terminalFlash.flush(cloudQueue + '<br>> ' + frames.next().value)
} else {
terminalFlash.flush('> ' + frames.next().value)
}
}, 150) }, 150)
} }
} }
@ -1342,7 +1327,7 @@ const getInstallTask = (first: boolean = true) => {
} }
setTimeout(() => { setTimeout(() => {
getInstallTask(false) getInstallTask(false)
}, 2000) }, 5000)
} else { } else {
if (!first) { if (!first) {
installStep.value = 2 installStep.value = 2
@ -1488,6 +1473,13 @@ const getCloudInstallLog = () => {
if (data[0] && data[0].length && installShowDialog.value == true) { if (data[0] && data[0].length && installShowDialog.value == true) {
data[0].forEach((item) => { data[0].forEach((item) => {
if (!installLog.includes(item.action)) { if (!installLog.includes(item.action)) {
if (item.action.indexOf('云编译任务正在排队') != -1) {
cloudQueue = item.action
installLog.push(item.action)
return
} else {
cloudQueue = null
}
terminalRef.value.pushMessage({ content: `${item.action}` }) terminalRef.value.pushMessage({ content: `${item.action}` })
installLog.push(item.action) installLog.push(item.action)

View File

@ -87,7 +87,7 @@
<el-table-column :label="t('siteInfo')" width="300" align="left"> <el-table-column :label="t('siteInfo')" width="300" align="left">
<template #default="{ row }"> <template #default="{ row }">
<div class="flex items-center"> <div class="flex items-center">
<img class="w-[54px] h-[54px] mr-[10px] rounded-[4px]" v-if="row.logo" :src="img(row.logo)" alt=""> <img class="w-[54px] h-[54px] mr-[10px] rounded-[4px]" v-if="row.icon" :src="img(row.icon)" alt="">
<img class="w-[54px] h-[54px] mr-[10px] rounded-[4px]" v-else src="@/app/assets/images/site_default.png" alt=""> <img class="w-[54px] h-[54px] mr-[10px] rounded-[4px]" v-else src="@/app/assets/images/site_default.png" alt="">
<div class="flex flex-col"> <div class="flex flex-col">
<span>{{ row.site_name || '' }}</span> <span>{{ row.site_name || '' }}</span>

View File

@ -14,67 +14,67 @@
<el-button class="w-[98px] !h-[36px]" type="primary" @click="handleCloudBuild" :loading="cloudBuildRef?.loading">云编译</el-button> <el-button class="w-[98px] !h-[36px]" type="primary" @click="handleCloudBuild" :loading="cloudBuildRef?.loading">云编译</el-button>
</div> </div>
</div> </div>
<!-- <div class="panel-title bg-[#F4F5F7] border-[#E6E6E6] border-solid border-b-[1px] h-[40px] flex items-center p-[10px]">--> <div class="panel-title bg-[#F4F5F7] border-[#E6E6E6] border-solid border-b-[1px] h-[40px] flex items-center p-[10px]">
<!-- <span class="text-[16px] font-500 text-[#1D1F3A]">云编译</span>--> <span class="text-[16px] font-500 text-[#1D1F3A]">云编译</span>
<!-- <span class="text-[12px] text-[#9699B6] ml-[10px]">云编译不需要本地安装node环境即可进行针对使用者方便快捷</span>--> <span class="text-[12px] text-[#9699B6] ml-[10px]">云编译不需要本地安装node环境即可进行针对使用者方便快捷</span>
<!-- </div>--> </div>
<!-- <div class="mt-[20px] flex mb-[14px] items-center">--> <div class="mt-[20px] flex mb-[14px] items-center">
<!-- <span class="flex ml-[20px] font-500 text-[16px] items-center text-[#1D1F3A]">--> <span class="flex ml-[20px] font-500 text-[16px] items-center text-[#1D1F3A]">
<!-- &lt;!&ndash; <i class="w-[3px] h-[12px] bg-primary mr-[6px] block"></i> &ndash;&gt;--> <!-- <i class="w-[3px] h-[12px] bg-primary mr-[6px] block"></i> -->
<!-- 温馨提示--> 温馨提示
<!-- </span>--> </span>
<!-- <span class="text-[12px] text-[#9699B6] ml-[10px]"> 以下情况可以进行云编译</span>--> <span class="text-[12px] text-[#9699B6] ml-[10px]"> 以下情况可以进行云编译</span>
<!-- </div>--> </div>
<!-- &lt;!&ndash; <div class="text-[14px] text-[#606266] ml-[13px] mb-[18px]">云编译不需要本地安装node环境即可进行针对使用者方便快捷</div> &ndash;&gt;--> <!-- <div class="text-[14px] text-[#606266] ml-[13px] mb-[18px]">云编译不需要本地安装node环境即可进行针对使用者方便快捷</div> -->
<!-- <div class="ml-[40px] text-[14px] text-[#4F516D] mb-[18px]">1系统或插件每次安装或升级完成后需要云编译</div>--> <div class="ml-[40px] text-[14px] text-[#4F516D] mb-[18px]">1系统或插件每次安装或升级完成后需要云编译</div>
<!-- <div class="ml-[40px] text-[14px] text-[#4F516D] mb-[18px]">2开发者编写完前端代码之后可以使用云编译进行源码编译</div>--> <div class="ml-[40px] text-[14px] text-[#4F516D] mb-[18px]">2开发者编写完前端代码之后可以使用云编译进行源码编译</div>
<!-- <div class="ml-[40px] text-[14px] text-[#4F516D] mb-[18px]">3由于云编译不是针对某个插件进行编译而是系统整体编译因此如果同时需要安装多个插件时往往需要安装到最后一个插件才整体进行云编译</div>--> <div class="ml-[40px] text-[14px] text-[#4F516D] mb-[18px]">3由于云编译不是针对某个插件进行编译而是系统整体编译因此如果同时需要安装多个插件时往往需要安装到最后一个插件才整体进行云编译</div>
<!-- <div class="mt-[21px] flex mb-[21px] text-[16px] text-[#1D1F3A] font-500 items-center">--> <div class="mt-[21px] flex mb-[21px] text-[16px] text-[#1D1F3A] font-500 items-center">
<!-- <span class="flex ml-[20px] items-center">--> <span class="flex ml-[20px] items-center">
<!-- &lt;!&ndash; <i class="w-[3px] h-[12px] bg-primary mr-[6px] block"></i> &ndash;&gt;--> <!-- <i class="w-[3px] h-[12px] bg-primary mr-[6px] block"></i> -->
<!-- 云编译流程--> 云编译流程
<!-- </span>--> </span>
<!-- </div>--> </div>
<!-- <div class="ml-[40px]">--> <div class="ml-[40px]">
<!-- <el-timeline>--> <el-timeline>
<!-- <el-timeline-item :hollow="true">--> <el-timeline-item :hollow="true">
<!-- &lt;!&ndash; <template #dot>--> <!-- <template #dot>
<!-- <div class="w-[15px] h-[15px] bg-primary rounded-[50%] text-[9px] text-[#fff] flex items-center justify-center">1</div>--> <div class="w-[15px] h-[15px] bg-primary rounded-[50%] text-[9px] text-[#fff] flex items-center justify-center">1</div>
<!-- </template> &ndash;&gt;--> </template> -->
<!-- <div class="text-[16px] text-[#1D1F3A]">编译admin代码</div>--> <div class="text-[16px] text-[#1D1F3A]">编译admin代码</div>
<!-- <div class="p-[10px] bg-[#F9F9FB] mt-[10px] text-[#4F516D] text-[14px] w-[1085px] border-[#F1F1F8] border-solid border-[1px] h-[40px] flex items-center rounded-[4px]">--> <div class="p-[10px] bg-[#F9F9FB] mt-[10px] text-[#4F516D] text-[14px] w-[1085px] border-[#F1F1F8] border-solid border-[1px] h-[40px] flex items-center rounded-[4px]">
<!-- <span>云编译会将admin端的vue代码编译为对应的html文件同时将生成的代码下载到系统 niucloud 下的</span>--> <span>云编译会将admin端的vue代码编译为对应的html文件同时将生成的代码下载到系统 niucloud 下的</span>
<!-- <span class="text-[#F09000] mx-[3px] font-bold">public/admin</span>--> <span class="text-[#F09000] mx-[3px] font-bold">public/admin</span>
<!-- <span>目录中后台的访问路径将变为</span>--> <span>目录中后台的访问路径将变为</span>
<!-- <span class="text-primary ml-[3px] font-500">https:///admin</span>--> <span class="text-primary ml-[3px] font-500">https:///admin</span>
<!-- </div>--> </div>
<!-- </el-timeline-item>--> </el-timeline-item>
<!-- <el-timeline-item :hollow="true">--> <el-timeline-item :hollow="true">
<!-- <div class="text-[16px] text-[#1D1F3A]">编译uniapp代码</div>--> <div class="text-[16px] text-[#1D1F3A]">编译uniapp代码</div>
<!-- <div class="p-[10px] bg-[#F9F9FB] mt-[10px] text-[#4F516D] text-[14px] w-[1085px] border-[#F1F1F8] border-solid border-[1px] h-[40px] flex items-center rounded-[4px]">--> <div class="p-[10px] bg-[#F9F9FB] mt-[10px] text-[#4F516D] text-[14px] w-[1085px] border-[#F1F1F8] border-solid border-[1px] h-[40px] flex items-center rounded-[4px]">
<!-- <span>云编译会将uniapp端的vue代码编译为对应的html文件同时将生成的代码下载到系统 niucloud下的</span>--> <span>云编译会将uniapp端的vue代码编译为对应的html文件同时将生成的代码下载到系统 niucloud下的</span>
<!-- <span class="text-[#F09000] mx-[3px] font-bold">public/wap</span>--> <span class="text-[#F09000] mx-[3px] font-bold">public/wap</span>
<!-- <span>目录中这样手机端网页的访问路径将变为</span>--> <span>目录中这样手机端网页的访问路径将变为</span>
<!-- <span class="text-primary ml-[3px] font-500"> https:///wap</span>--> <span class="text-primary ml-[3px] font-500"> https:///wap</span>
<!-- </div>--> </div>
<!-- </el-timeline-item>--> </el-timeline-item>
<!-- <el-timeline-item :hollow="true">--> <el-timeline-item :hollow="true">
<!-- <div class="text-[16px] text-[#1D1F3A]">编译web代码</div>--> <div class="text-[16px] text-[#1D1F3A]">编译web代码</div>
<!-- <div class="p-[10px] bg-[#F9F9FB] mt-[10px] text-[#4F516D] text-[14px] w-[1085px] border-[#F1F1F8] border-solid border-[1px] h-[40px] flex items-center rounded-[4px]">--> <div class="p-[10px] bg-[#F9F9FB] mt-[10px] text-[#4F516D] text-[14px] w-[1085px] border-[#F1F1F8] border-solid border-[1px] h-[40px] flex items-center rounded-[4px]">
<!-- <span>云编译会将web端的vue代码编译为对应的html文件同时将生成的代码下载到系统 niucloud下的</span>--> <span>云编译会将web端的vue代码编译为对应的html文件同时将生成的代码下载到系统 niucloud下的</span>
<!-- <span class="text-[#F09000] mx-[3px] font-bold">public/web</span>--> <span class="text-[#F09000] mx-[3px] font-bold">public/web</span>
<!-- <span>目录中这样电脑端网页的访问路径将变为</span>--> <span>目录中这样电脑端网页的访问路径将变为</span>
<!-- <span class="text-primary ml-[3px] font-500"> https:///web</span>--> <span class="text-primary ml-[3px] font-500"> https:///web</span>
<!-- </div>--> </div>
<!-- </el-timeline-item>--> </el-timeline-item>
<!-- </el-timeline>--> </el-timeline>
<!-- </div>--> </div>
</div> </div>
<div class="mt-[10px]"> <div class="mt-[10px]">
<div class="panel-title bg-[#F4F5F7] border-[#E6E6E6] border-solid border-b-[1px] h-[40px] flex items-center p-[10px]"> <div class="panel-title bg-[#F4F5F7] border-[#E6E6E6] border-solid border-b-[1px] h-[40px] flex items-center p-[10px]">
<span class="text-[16px] font-500 text-[#1D1F3A]">第三方云编译</span> <span class="text-[16px] font-500 text-[#1D1F3A]">第三方云编译</span>
<!-- <el-switch v-model="isCloudCompilation" :active-value="1" :inactive-value="0" class="ml-[10px]" @change="confirm" />--> <el-switch v-model="isCloudCompilation" :active-value="1" :inactive-value="0" class="ml-[10px]" @change="confirm" />
<span class="ml-[10px] text-[#9699B6] text-[12px]">自己搭建第三方云编译服务器无需等待</span> <span class="ml-[10px] text-[#9699B6] text-[12px]">自己搭建第三方云编译服务器无需等待</span>
</div> </div>
<div class="mt-[20px] flex mb-[14px] text-[16px] items-center text-[#1D1F3A]"> <div class="mt-[20px] flex mb-[14px] text-[16px] items-center text-[#1D1F3A]">

View File

@ -11,6 +11,7 @@ import { computed, ref } from 'vue'
import { getToken, img } from '@/utils/common' import { getToken, img } from '@/utils/common'
import { VueUeditorWrap } from 'vue-ueditor-wrap' import { VueUeditorWrap } from 'vue-ueditor-wrap'
import storage from '@/utils/storage' import storage from '@/utils/storage'
import envConfig from '@/utils/config'
const editorRef = ref() const editorRef = ref()
@ -46,9 +47,9 @@ const content = computed({
let editorEl = null let editorEl = null
const serverHeaders = {} const serverHeaders = {}
serverHeaders[import.meta.env.VITE_REQUEST_HEADER_TOKEN_KEY] = getToken() serverHeaders[envConfig.VITE_REQUEST_HEADER_TOKEN_KEY] = getToken()
serverHeaders[import.meta.env.VITE_REQUEST_HEADER_SITEID_KEY] = storage.get('siteId') || 0 serverHeaders[envConfig.VITE_REQUEST_HEADER_SITEID_KEY] = storage.get('siteId') || 0
const baseUrl = import.meta.env.VITE_APP_BASE_URL.substr(-1) == '/' ? import.meta.env.VITE_APP_BASE_URL : `${import.meta.env.VITE_APP_BASE_URL}/` const baseUrl = envConfig.VITE_APP_BASE_URL.substr(-1) == '/' ? envConfig.VITE_APP_BASE_URL : `${envConfig.VITE_APP_BASE_URL}/`
const editorConfig = ref({ const editorConfig = ref({
debug: false, debug: false,

View File

@ -199,6 +199,7 @@ import {
import { debounce, img, getToken } from '@/utils/common' import { debounce, img, getToken } from '@/utils/common'
import { ElMessage, UploadFile, UploadFiles, ElMessageBox, MessageParams } from 'element-plus' import { ElMessage, UploadFile, UploadFiles, ElMessageBox, MessageParams } from 'element-plus'
import storage from '@/utils/storage' import storage from '@/utils/storage'
import envConfig from '@/utils/config'
const attachmentCategoryName = ref('') const attachmentCategoryName = ref('')
const operate = ref(false) const operate = ref(false)
@ -446,9 +447,9 @@ const time = ref<any>(null)
// //
const upload = computed(() => { const upload = computed(() => {
const headers: Record<string, any> = {} const headers: Record<string, any> = {}
headers[import.meta.env.VITE_REQUEST_HEADER_TOKEN_KEY] = getToken() headers[envConfig.VITE_REQUEST_HEADER_TOKEN_KEY] = getToken()
headers[import.meta.env.VITE_REQUEST_HEADER_SITEID_KEY] = storage.get('siteId') || 0 headers[envConfig.VITE_REQUEST_HEADER_SITEID_KEY] = storage.get('siteId') || 0
const baseURL = import.meta.env.VITE_APP_BASE_URL.substr(-1) == '/' ? import.meta.env.VITE_APP_BASE_URL : `${import.meta.env.VITE_APP_BASE_URL}/` const baseURL = envConfig.VITE_APP_BASE_URL.substr(-1) == '/' ? envConfig.VITE_APP_BASE_URL : `${envConfig.VITE_APP_BASE_URL}/`
return { return {
action: `${baseURL}sys/${prop.type}`, action: `${baseURL}sys/${prop.type}`,

View File

@ -12,6 +12,7 @@ import { t } from '@/lang'
import { getToken } from '@/utils/common' import { getToken } from '@/utils/common'
import { UploadFile, ElMessage } from 'element-plus' import { UploadFile, ElMessage } from 'element-plus'
import storage from '@/utils/storage' import storage from '@/utils/storage'
import envConfig from '@/utils/config'
const prop = defineProps({ const prop = defineProps({
modelValue: { modelValue: {
@ -36,7 +37,7 @@ const value = computed({
}) })
const upload: Record<string, any> = { const upload: Record<string, any> = {
action: `${import.meta.env.VITE_APP_BASE_URL}/${prop.api}`, action: `${envConfig.VITE_APP_BASE_URL}/${prop.api}`,
showFileList: false, showFileList: false,
headers: {}, headers: {},
accept: 'audio/*,.mp3,.wav,.ogg,.m4a,.flac,.aac,.wma', accept: 'audio/*,.mp3,.wav,.ogg,.m4a,.flac,.aac,.wma',
@ -62,8 +63,8 @@ const upload: Record<string, any> = {
ElMessage({ message: t('upload.success'), type: 'success' }) ElMessage({ message: t('upload.success'), type: 'success' })
} }
} }
upload.headers[import.meta.env.VITE_REQUEST_HEADER_TOKEN_KEY] = getToken() upload.headers[envConfig.VITE_REQUEST_HEADER_TOKEN_KEY] = getToken()
upload.headers[import.meta.env.VITE_REQUEST_HEADER_SITEID_KEY] = storage.get('siteId') || 0 upload.headers[envConfig.VITE_REQUEST_HEADER_SITEID_KEY] = storage.get('siteId') || 0
</script> </script>

View File

@ -14,6 +14,7 @@ import { t } from '@/lang'
import { getToken } from '@/utils/common' import { getToken } from '@/utils/common'
import { UploadFile, ElMessage } from 'element-plus' import { UploadFile, ElMessage } from 'element-plus'
import storage from '@/utils/storage' import storage from '@/utils/storage'
import envConfig from '@/utils/config'
const prop = defineProps({ const prop = defineProps({
modelValue: { modelValue: {
@ -43,9 +44,9 @@ const value = computed({
const upload = computed(() => { const upload = computed(() => {
const headers: Record<string, any> = {} const headers: Record<string, any> = {}
headers[import.meta.env.VITE_REQUEST_HEADER_TOKEN_KEY] = getToken() headers[envConfig.VITE_REQUEST_HEADER_TOKEN_KEY] = getToken()
headers[import.meta.env.VITE_REQUEST_HEADER_SITEID_KEY] = storage.get('siteId') || 0 headers[envConfig.VITE_REQUEST_HEADER_SITEID_KEY] = storage.get('siteId') || 0
const baseURL = import.meta.env.VITE_APP_BASE_URL.substr(-1) == '/' ? import.meta.env.VITE_APP_BASE_URL : `${import.meta.env.VITE_APP_BASE_URL}/` const baseURL = envConfig.VITE_APP_BASE_URL.substr(-1) == '/' ? envConfig.VITE_APP_BASE_URL : `${envConfig.VITE_APP_BASE_URL}/`
return { return {
action: `${baseURL}${prop.api}`, action: `${baseURL}${prop.api}`,

View File

@ -1,6 +1,7 @@
import axios from 'axios' import axios from 'axios'
import envConfig from '@/utils/config'
axios.defaults.baseURL = import.meta.env.VITE_APP_BASE_URL axios.defaults.baseURL = envConfig.VITE_APP_BASE_URL
const service = axios.create({ const service = axios.create({
timeout: 40000, timeout: 40000,

View File

@ -4,18 +4,21 @@ import Language from "./language"
import zhCn from "./zh-cn/common.json"; import zhCn from "./zh-cn/common.json";
import en from "./en/common.json" import en from "./en/common.json"
const addonZhCnCommon = import.meta.globEager('@/addon/**/lang/zh-cn/common.json') if (import.meta.env.DEV) {
const addonEnCommon = import.meta.globEager('@/addon/**/lang/en/common.json') const addonZhCnCommon = import.meta.globEager('@/addon/**/lang/zh-cn/common.json')
const addonEnCommon = import.meta.globEager('@/addon/**/lang/en/common.json')
for (let key in addonZhCnCommon) { for (const key in addonZhCnCommon) {
Object.assign(zhCn, addonZhCnCommon[key].default) Object.assign(zhCn, addonZhCnCommon[key].default)
} }
for (let key in addonEnCommon) { for (const key in addonEnCommon) {
Object.assign(en, addonEnCommon[key].default) Object.assign(en, addonEnCommon[key].default)
}
} }
//创建实例 //创建实例
let i18n = createI18n({ let i18n = createI18n({
locale: 'zh-cn',
fallbackLocale: 'zh-cn',
datetimeFormats: {}, datetimeFormats: {},
numberFormats: {}, numberFormats: {},
globalInjection: true, //是否全局注入 globalInjection: true, //是否全局注入

View File

@ -1,10 +1,22 @@
import i18n, { language } from "./i18n" import i18n, { language } from "./i18n"
import useAppStore from '@/stores/modules/app' import useAppStore from '@/stores/modules/app'
import { resolveLangFile, resolveRouteAddon, resolveRouteView, inferAddonFromPath } from '@/utils/addon-lang'
import { getCurrentInstance } from 'vue'
import { useRoute } from 'vue-router'
function currentRoute() {
if (getCurrentInstance()) {
return useRoute()
}
return useAppStore().route as Parameters<typeof resolveRouteAddon>[0]
}
const t = (message: string) => { const t = (message: string) => {
const route = useAppStore().route const route = currentRoute()
const path = route.meta.view || route.path let app = resolveRouteAddon(route)
const file = path == '/' ? 'index' : path.replace(/^(\/admin\/|\/site\/|\/)/, '').replaceAll('/', '.') const path = resolveRouteView(route)
if (!app) app = inferAddonFromPath(path)
const file = resolveLangFile(app, path)
const key = `${file}.${message}` const key = `${file}.${message}`
return i18n.global.t(key) != key ? i18n.global.t(key) : i18n.global.t(message) return i18n.global.t(key) != key ? i18n.global.t(key) : i18n.global.t(message)
} }
@ -13,9 +25,6 @@ export { language, t }
export default { export default {
install(app: any) { install(app: any) {
//注册i18n
app.use(i18n); app.use(i18n);
} }
}; };

View File

@ -1,4 +1,5 @@
import { nextTick } from 'vue' import { nextTick } from 'vue'
import { loadAddonLang, preloadAllAddonLangs, resolveLangFile, inferAddonFromPath } from '@/utils/addon-lang'
class Language { class Language {
private i18n: any; private i18n: any;
@ -31,25 +32,42 @@ class Language {
*/ */
public async loadLocaleMessages(app: string, path: string, locale: string) { public async loadLocaleMessages(app: string, path: string, locale: string) {
try { try {
const file = path == '/' ? 'index' : path.replace(/^(\/admin\/|\/site\/|\/)/, '').replaceAll('/', '.') if (!app) app = inferAddonFromPath(path)
const file = resolveLangFile(app, path)
// 引入语言包文件 let pageMessages: Record<string, string> = {}
const messages = await import(app ? `@/addon/${app}/lang/${locale}/${file}.json` : `@/app/lang/${locale}/${file}.json`) if (app) {
if (import.meta.env.DEV) {
const messages = await import(/* @vite-ignore */ `@/addon/${app}/lang/${locale}/${file}.json`)
pageMessages = messages.default || {}
} else {
pageMessages = await loadAddonLang(app, locale, file)
}
} else {
const messages = await import(`@/app/lang/${locale}/${file}.json`)
pageMessages = messages.default || {}
}
let data: Record<string, string> = {} let data: Record<string, string> = {}
Object.keys(messages.default).forEach(key => { Object.keys(pageMessages).forEach(key => {
data[`${file}.${key}`] = messages.default[key] data[`${file}.${key}`] = pageMessages[key]
}) })
// 查询插件的公共语言包 // 查询插件的公共语言包(合并到根 key与 dev 启动时 globEager 行为一致)
if (app) { if (app) {
try { try {
const messagesCommon = await import( `@/${ app }/lang/${ locale }/common.json`); let commonMessages: Record<string, string> = {}
Object.keys(messagesCommon.default).forEach(key => { if (import.meta.env.DEV) {
data[`${file}.${key}`] = messagesCommon.default[key] const messagesCommon = await import(/* @vite-ignore */ `@/addon/${app}/lang/${locale}/common.json`)
commonMessages = messagesCommon.default || {}
} else {
commonMessages = await loadAddonLang(app, locale, 'common')
}
Object.keys(commonMessages).forEach(key => {
data[key] = commonMessages[key]
}) })
} catch (e) { } catch (e) {
// console.log('未找到插件公共语言包') // 未找到插件公共语言包
} }
} }
@ -61,6 +79,13 @@ class Language {
return nextTick() return nextTick()
} }
} }
/** 生产环境启动时预加载各插件全部语言包 */
public async preloadAddonLangs() {
await preloadAllAddonLangs((locale, messages) => {
this.i18n.global.mergeLocaleMessage(locale, messages)
})
}
} }
export default Language export default Language

View File

@ -138,7 +138,7 @@ getVersionsInfo()
</script> </script>
<style lang="scss"> <style lang="scss" scoped>
.one-menu{ .one-menu{
padding: 20px 10px 10px; padding: 20px 10px 10px;
width: 78px; width: 78px;
@ -198,10 +198,10 @@ getVersionsInfo()
&.expanded .menu-item .text-center { &.expanded .menu-item .text-center {
opacity: 1; opacity: 1;
} }
.el-menu{ :deep(.el-menu){
border: 0; border: 0;
} }
.el-scrollbar{ :deep(.el-scrollbar){
height: calc(100vh - 65px); height: calc(100vh - 65px);
} }
} }
@ -210,7 +210,7 @@ getVersionsInfo()
width: 185px; width: 185px;
border: 0; border: 0;
padding-top: 15px; padding-top: 15px;
.el-menu-item{ :deep(.el-menu-item){
height: 40px; height: 40px;
margin: 4px 15px; margin: 4px 15px;
padding: 0 8px !important; padding: 0 8px !important;
@ -232,11 +232,11 @@ getVersionsInfo()
// color: var(--el-color-primary); // color: var(--el-color-primary);
} }
} }
.el-sub-menu{ :deep(.el-sub-menu){
width: 185px; width: 185px;
margin: 4px 0; margin: 4px 0;
// margin-bottom: 8px; // margin-bottom: 8px;
.el-sub-menu__title{ :deep(.el-sub-menu__title){
margin: 0 15px; margin: 0 15px;
height: 40px; height: 40px;
padding-left: 8px; padding-left: 8px;
@ -253,11 +253,11 @@ getVersionsInfo()
// background-color: var(--el-color-primary-light-9) !important; // background-color: var(--el-color-primary-light-9) !important;
// color: var(--el-color-primary); // color: var(--el-color-primary);
} }
.el-icon.el-sub-menu__icon-arrow{ .el-icon:deep(.el-sub-menu__icon-arrow){
right: 5px; right: 5px;
} }
} }
.el-menu-item{ :deep(.el-menu-item){
padding-left: 25px !important; padding-left: 25px !important;
} }
} }
@ -286,7 +286,7 @@ getVersionsInfo()
// :deep(.el-scrollbar__bar){ // :deep(.el-scrollbar__bar){
// display: none !important; // display: none !important;
// } // }
// .layout-aside .el-scrollbar__wrap--hidden-default, .layout-aside .el-scrollbar{ // .layout-aside .el-scrollbar__wrap--hidden-default, .layout-aside :deep(.el-scrollbar){
// overflow: inherit !important; // overflow: inherit !important;
// } // }
// //

View File

@ -45,8 +45,8 @@ const props = defineProps({
const meta = computed(() => props.routes.meta) const meta = computed(() => props.routes.meta)
</script> </script>
<style lang="scss"> <style lang="scss" scoped>
.el-sub-menu{ :deep(.el-sub-menu){
.el-icon{ .el-icon{
width: auto; width: auto;
} }

View File

@ -59,7 +59,7 @@ userStore.routers = userStore.routers.filter((item, index) => {
// }) // })
</script> </script>
<style lang="scss"> <style lang="scss" scoped>
.logo-wrap { .logo-wrap {
padding: 0; padding: 0;
display: flex; display: flex;
@ -84,7 +84,7 @@ userStore.routers = userStore.routers.filter((item, index) => {
flex: 1 !important; flex: 1 !important;
padding: 0 !important; padding: 0 !important;
.el-menu { :deep(.el-menu) {
border-right: 0 !important; border-right: 0 !important;
} }
} }
@ -100,15 +100,15 @@ userStore.routers = userStore.routers.filter((item, index) => {
border-bottom: 2px solid #101117; border-bottom: 2px solid #101117;
} }
.el-menu { :deep(.el-menu) {
background-color: #191a23; background-color: #191a23;
.el-sub-menu { :deep(.el-sub-menu) {
background: transparent !important; background: transparent !important;
} }
.el-sub-menu__title, :deep(.el-sub-menu__title),
.el-menu-item { :deep(.el-menu-item) {
background: transparent !important; background: transparent !important;
color: #B7B7ba; color: #B7B7ba;
@ -118,7 +118,7 @@ userStore.routers = userStore.routers.filter((item, index) => {
} }
} }
.el-menu-item.is-active { :deep(.el-menu-item).is-active {
color: #fff !important; color: #fff !important;
background-color: var(--el-color-primary) !important; background-color: var(--el-color-primary) !important;
} }

View File

@ -25,7 +25,7 @@ watch(route, () => {
}) })
</script> </script>
<style lang="scss"> <style lang="scss" scoped>
.layout-aside { .layout-aside {
//--side-dark-color: #141414; //--side-dark-color: #141414;
//background-color: var(--side-dark-color, var(--el-bg-color)); //background-color: var(--side-dark-color, var(--el-bg-color));

View File

@ -85,22 +85,22 @@ watch(route, () => {
}, { immediate: true }) }, { immediate: true })
</script> </script>
<style lang="scss"> <style lang="scss" scoped>
.el-sub-menu{ :deep(.el-sub-menu){
.el-icon{ .el-icon{
// width: auto; // width: auto;
} }
} }
.el-menu { :deep(.el-menu) {
.el-sub-menu__title, :deep(.el-sub-menu__title),
.el-menu-item { :deep(.el-menu-item) {
&:hover { &:hover {
// background-color: #F1F5FF !important; // background-color: #F1F5FF !important;
color: var(--el-color-primary); color: var(--el-color-primary);
} }
} }
.el-icon.el-sub-menu__icon-arrow{ .el-icon:deep(.el-sub-menu__icon-arrow){
font-size: 15px; font-size: 15px;
top: 50%; top: 50%;
} }

View File

@ -75,7 +75,7 @@ if (siteInfo?.apps.length > 1) {
defaultOpeneds.value = menuData.value.map(item => item.name) defaultOpeneds.value = menuData.value.map(item => item.name)
</script> </script>
<style lang="scss"> <style lang="scss" scoped>
.logo-wrap { .logo-wrap {
// background: #1f2531; // background: #1f2531;
background: #fff; background: #fff;
@ -91,22 +91,22 @@ defaultOpeneds.value = menuData.value.map(item => item.name)
display: flex; display: flex;
flex-direction: column; flex-direction: column;
.el-menu { :deep(.el-menu) {
border-right: 0!important; border-right: 0!important;
&:not(.el-menu--collapse) { &:not(.el-menu--collapse) {
width: var(--aside-width); width: var(--aside-width);
} }
.el-menu-item, .el-sub-menu__title { :deep(.el-menu-item), :deep(.el-sub-menu__title) {
--el-menu-item-height: 40px; --el-menu-item-height: 40px;
} }
.el-sub-menu .el-menu-item { :deep(.el-sub-menu) :deep(.el-menu-item) {
--el-menu-sub-item-height: 40px; --el-menu-sub-item-height: 40px;
} }
.el-menu-item.is-active { :deep(.el-menu-item).is-active {
background: var(--el-color-primary) !important; background: var(--el-color-primary) !important;
color: #fff !important; color: #fff !important;
} }
@ -115,14 +115,14 @@ defaultOpeneds.value = menuData.value.map(item => item.name)
// background: #282e3a; // background: #282e3a;
// background: #fff; // background: #fff;
} }
.el-menu-item .el-menu-tooltip__trigger{ :deep(.el-menu-item) .el-menu-tooltip__trigger{
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
} }
&.el-menu--collapse { &.el-menu--collapse {
.el-menu-item, .el-sub-menu__title { :deep(.el-menu-item), :deep(.el-sub-menu__title) {
--el-menu-item-height: 60px; --el-menu-item-height: 60px;
justify-content: center; justify-content: center;
} }

View File

@ -20,7 +20,7 @@ watch(route, () => {
}) })
</script> </script>
<style lang="scss"> <style lang="scss" scoped>
.layout-aside { .layout-aside {
background-color: var(--side-dark-color, var(--el-bg-color)); background-color: var(--side-dark-color, var(--el-bg-color));
border-right: 1px solid var(--el-border-color-lighter); border-right: 1px solid var(--el-border-color-lighter);

View File

@ -83,8 +83,8 @@ const handleJump = (routeName: string) => {
</script> </script>
<style lang="scss"> <style lang="scss" scoped>
.el-sub-menu{ :deep(.el-sub-menu){
.el-icon{ .el-icon{
width: auto; width: auto;
} }

View File

@ -355,11 +355,11 @@ watch(route, () => {
}, { immediate: true }) }, { immediate: true })
</script> </script>
<style lang="scss"> <style lang="scss" scoped>
.one-menu{ .one-menu{
.aside-menu:not(.el-menu--collapse) { .aside-menu:not(.el-menu--collapse) {
background-color: transparent; background-color: transparent;
.el-menu-item{ :deep(.el-menu-item){
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
@ -395,10 +395,10 @@ watch(route, () => {
} }
} }
} }
.el-menu{ :deep(.el-menu){
border: 0; border: 0;
} }
.el-scrollbar{ :deep(.el-scrollbar){
height: calc(100vh - 65px); height: calc(100vh - 65px);
} }
} }
@ -407,7 +407,7 @@ watch(route, () => {
width: 190px; width: 190px;
border: 0; border: 0;
padding-top: 10px; padding-top: 10px;
.el-menu-item{ :deep(.el-menu-item){
height: 40px; height: 40px;
margin: 0 8px 2px; margin: 0 8px 2px;
padding: 0 10px !important; padding: 0 10px !important;
@ -425,8 +425,8 @@ watch(route, () => {
color: var(--el-color-primary); color: var(--el-color-primary);
} }
} }
.el-sub-menu{ :deep(.el-sub-menu){
.el-sub-menu__title{ :deep(.el-sub-menu__title){
margin: 0 8px 2px; margin: 0 8px 2px;
height: 40px; height: 40px;
padding-left: 8px; padding-left: 8px;
@ -442,11 +442,11 @@ watch(route, () => {
color: var(--el-color-primary); color: var(--el-color-primary);
} }
} }
.el-menu-item{ :deep(.el-menu-item){
padding-left: 20px !important; padding-left: 20px !important;
} }
.el-sub-menu{ :deep(.el-sub-menu){
.el-sub-menu__title{ :deep(.el-sub-menu__title){
margin: 0 8px 2px; margin: 0 8px 2px;
height: 40px; height: 40px;
padding-left: 18px; padding-left: 18px;
@ -462,7 +462,7 @@ watch(route, () => {
color: var(--el-color-primary); color: var(--el-color-primary);
} }
} }
.el-menu-item{ :deep(.el-menu-item){
padding-left: 30px !important; padding-left: 30px !important;
} }
} }

View File

@ -29,7 +29,7 @@ watch(route, () => {
}) })
</script> </script>
<style lang="scss"> <style lang="scss" scoped>
.layout-aside { .layout-aside {
--side-dark-color: #141414; --side-dark-color: #141414;
background-color: var(--side-dark-color, var(--el-bg-color)); background-color: var(--side-dark-color, var(--el-bg-color));

View File

@ -285,10 +285,21 @@ onMounted(() => {
}); });
</script> </script>
<style lang="scss"> <style lang="scss" scoped>
.el-sub-menu{ :deep(.el-sub-menu) {
.el-icon{ .el-icon {
width: auto; width: auto;
} }
} }
:deep(.el-menu-item) {
height: 40px!important;
line-height: 40px!important;
&.is-active {
color: var(--el-color-primary);
}
}
:deep(.el-sub-menu__title) {
height: 40px!important;
line-height: 40px!important;
}
</style> </style>

View File

@ -133,22 +133,22 @@ if (siteInfo?.apps.length > 1) {
} }
</script> </script>
<style lang="scss"> <style lang="scss" scoped>
.menu-wrap { .menu-wrap {
padding: 0!important; padding: 0!important;
.el-menu { :deep(.el-menu) {
border-right: 0!important; border-right: 0!important;
.el-menu-item, .el-sub-menu__title { :deep(.el-menu-item), :deep(.el-sub-menu__title) {
--el-menu-item-height: 40px; --el-menu-item-height: 40px;
} }
.el-sub-menu .el-menu-item { :deep(.el-sub-menu) :deep(.el-menu-item) {
--el-menu-sub-item-height: 40px; --el-menu-sub-item-height: 40px;
} }
.el-menu-item.is-active { :deep(.el-menu-item).is-active {
background-color: var(--el-color-primary) background-color: var(--el-color-primary)
} }
} }

View File

@ -29,7 +29,7 @@ watch(route, () => {
}) })
</script> </script>
<style lang="scss"> <style lang="scss" scoped>
.layout-aside { .layout-aside {
&.bright { &.bright {
background-color: #F5F7F9; background-color: #F5F7F9;

View File

@ -273,10 +273,18 @@ onMounted(() => {
}); });
</script> </script>
<style lang="scss"> <style lang="scss" scoped>
.el-sub-menu { :deep(.el-sub-menu) {
.el-icon { .el-icon {
width: auto; width: auto;
} }
} }
:deep(.el-menu-item) {
height: 40px!important;
line-height: 40px!important;
}
:deep(.el-sub-menu__title) {
height: 40px!important;
line-height: 40px!important;
}
</style> </style>

View File

@ -135,18 +135,18 @@ if (siteInfo?.apps.length > 1) {
} }
</script> </script>
<style lang="scss"> <style lang="scss" scoped>
.menu-wrap { .menu-wrap {
padding: 0!important; padding: 0!important;
.el-menu { :deep(.el-menu) {
border-right: 0!important; border-right: 0!important;
.el-menu-item, .el-sub-menu__title { :deep(.el-menu-item), :deep(.el-sub-menu__title) {
--el-menu-item-height: 40px; --el-menu-item-height: 40px;
} }
.el-sub-menu .el-menu-item { :deep(.el-sub-menu) :deep(.el-menu-item) {
--el-menu-sub-item-height: 40px; --el-menu-sub-item-height: 40px;
} }
} }

View File

@ -255,7 +255,7 @@ getVersionsInfo()
</script> </script>
<style lang="scss"> <style lang="scss" scoped>
.one-menu{ .one-menu{
padding: 20px 10px 10px; padding: 20px 10px 10px;
width: 78px; width: 78px;
@ -315,10 +315,10 @@ getVersionsInfo()
&.expanded .menu-item .text-center { &.expanded .menu-item .text-center {
opacity: 1; opacity: 1;
} }
.el-menu{ :deep(.el-menu){
border: 0; border: 0;
} }
.el-scrollbar{ :deep(.el-scrollbar){
height: calc(100vh - 65px); height: calc(100vh - 65px);
} }
} }
@ -327,7 +327,7 @@ getVersionsInfo()
width: 185px; width: 185px;
border: 0; border: 0;
padding-top: 15px; padding-top: 15px;
.el-menu-item{ :deep(.el-menu-item){
height: 40px; height: 40px;
margin: 4px 15px; margin: 4px 15px;
padding: 0 8px !important; padding: 0 8px !important;
@ -349,11 +349,11 @@ getVersionsInfo()
// color: var(--el-color-primary); // color: var(--el-color-primary);
} }
} }
.el-sub-menu{ :deep(.el-sub-menu){
width: 185px; width: 185px;
margin: 4px 0; margin: 4px 0;
// margin-bottom: 8px; // margin-bottom: 8px;
.el-sub-menu__title{ :deep(.el-sub-menu__title){
margin: 0 15px; margin: 0 15px;
height: 40px; height: 40px;
padding-left: 8px; padding-left: 8px;
@ -370,11 +370,11 @@ getVersionsInfo()
// background-color: var(--el-color-primary-light-9) !important; // background-color: var(--el-color-primary-light-9) !important;
// color: var(--el-color-primary); // color: var(--el-color-primary);
} }
.el-icon.el-sub-menu__icon-arrow{ .el-icon:deep(.el-sub-menu__icon-arrow){
right: 5px; right: 5px;
} }
} }
.el-menu-item{ :deep(.el-menu-item){
padding-left: 25px !important; padding-left: 25px !important;
} }
} }
@ -403,7 +403,7 @@ getVersionsInfo()
// :deep(.el-scrollbar__bar){ // :deep(.el-scrollbar__bar){
// display: none !important; // display: none !important;
// } // }
// .layout-aside .el-scrollbar__wrap--hidden-default, .layout-aside .el-scrollbar{ // .layout-aside .el-scrollbar__wrap--hidden-default, .layout-aside :deep(.el-scrollbar){
// overflow: inherit !important; // overflow: inherit !important;
// } // }
// //

View File

@ -45,8 +45,8 @@ const props = defineProps({
const meta = computed(() => props.routes.meta) const meta = computed(() => props.routes.meta)
</script> </script>
<style lang="scss"> <style lang="scss" scoped>
.el-sub-menu{ :deep(.el-sub-menu){
.el-icon{ .el-icon{
width: auto; width: auto;
} }

View File

@ -59,7 +59,7 @@ userStore.routers = userStore.routers.filter((item, index) => {
// }) // })
</script> </script>
<style lang="scss"> <style lang="scss" scoped>
.logo-wrap { .logo-wrap {
padding: 0; padding: 0;
display: flex; display: flex;
@ -84,7 +84,7 @@ userStore.routers = userStore.routers.filter((item, index) => {
flex: 1 !important; flex: 1 !important;
padding: 0 !important; padding: 0 !important;
.el-menu { :deep(.el-menu) {
border-right: 0 !important; border-right: 0 !important;
} }
} }
@ -100,15 +100,15 @@ userStore.routers = userStore.routers.filter((item, index) => {
border-bottom: 2px solid #101117; border-bottom: 2px solid #101117;
} }
.el-menu { :deep(.el-menu) {
background-color: #191a23; background-color: #191a23;
.el-sub-menu { :deep(.el-sub-menu) {
background: transparent !important; background: transparent !important;
} }
.el-sub-menu__title, :deep(.el-sub-menu__title),
.el-menu-item { :deep(.el-menu-item) {
background: transparent !important; background: transparent !important;
color: #B7B7ba; color: #B7B7ba;
@ -118,7 +118,7 @@ userStore.routers = userStore.routers.filter((item, index) => {
} }
} }
.el-menu-item.is-active { :deep(.el-menu-item).is-active {
color: #fff !important; color: #fff !important;
background-color: var(--el-color-primary) !important; background-color: var(--el-color-primary) !important;
} }

View File

@ -29,7 +29,7 @@ watch(route, () => {
}) })
</script> </script>
<style lang="scss"> <style lang="scss" scoped>
.layout-aside { .layout-aside {
border-right: 1px solid var(--el-border-color-lighter); border-right: 1px solid var(--el-border-color-lighter);
} }

View File

@ -80,8 +80,8 @@ const handleJump = (routeName: string) => {
} }
</script> </script>
<style lang="scss"> <style lang="scss" scoped>
.el-sub-menu { :deep(.el-sub-menu) {
.el-icon { .el-icon {
width: auto; width: auto;

View File

@ -377,13 +377,13 @@ watch(route, () => {
}, { immediate: true }) }, { immediate: true })
</script> </script>
<style lang="scss"> <style lang="scss" scoped>
.one-menu { .one-menu {
.aside-menu:not(.el-menu--collapse) { .aside-menu:not(.el-menu--collapse) {
background-color: transparent; background-color: transparent;
.el-menu-item { :deep(.el-menu-item) {
font-size: 14px; font-size: 14px;
height: 40px; height: 40px;
margin-bottom: 4px; margin-bottom: 4px;
@ -408,11 +408,11 @@ watch(route, () => {
} }
} }
.el-menu { :deep(.el-menu) {
border: 0; border: 0;
} }
.el-scrollbar { :deep(.el-scrollbar) {
height: calc(100vh - 65px); height: calc(100vh - 65px);
} }
} }
@ -424,7 +424,7 @@ watch(route, () => {
padding-top: 16px; padding-top: 16px;
border: 0; border: 0;
.el-menu-item { :deep(.el-menu-item) {
height: 36px; height: 36px;
margin: 0 12px 4px; margin: 0 12px 4px;
padding: 0 !important; padding: 0 !important;
@ -445,10 +445,10 @@ watch(route, () => {
} }
} }
.el-sub-menu { :deep(.el-sub-menu) {
margin-bottom: 8px; margin-bottom: 8px;
.el-sub-menu__title { :deep(.el-sub-menu__title) {
height: 36px; height: 36px;
margin: 0 8px 4px; margin: 0 8px 4px;
padding-left: 0; padding-left: 0;
@ -465,19 +465,19 @@ watch(route, () => {
color: var(--el-color-primary); color: var(--el-color-primary);
background-color: var(--el-color-primary-light-9) !important; background-color: var(--el-color-primary-light-9) !important;
} }
.el-icon.el-sub-menu__icon-arrow { .el-icon:deep(.el-sub-menu__icon-arrow) {
right: 5px; right: 5px;
} }
} }
.el-menu-item { :deep(.el-menu-item) {
padding-left: 20px !important; padding-left: 20px !important;
span{ span{
margin-left: 0 !important; margin-left: 0 !important;
} }
} }
.el-sub-menu{ :deep(.el-sub-menu){
.el-sub-menu__title{ :deep(.el-sub-menu__title){
margin: 0 8px 2px; margin: 0 8px 2px;
height: 40px; height: 40px;
padding-left: 18px; padding-left: 18px;
@ -493,7 +493,7 @@ watch(route, () => {
color: var(--el-color-primary); color: var(--el-color-primary);
} }
} }
.el-menu-item{ :deep(.el-menu-item){
padding-left: 40px !important; padding-left: 40px !important;
span{ span{
margin-left: 0 !important; margin-left: 0 !important;

View File

@ -20,7 +20,7 @@ watch(route, () => {
}) })
</script> </script>
<style lang="scss"> <style lang="scss" scoped>
.layout-aside { .layout-aside {
background-color: var(--side-dark-color, var(--el-bg-color)); background-color: var(--side-dark-color, var(--el-bg-color));
border-right: 1px solid var(--el-border-color-lighter); border-right: 1px solid var(--el-border-color-lighter);

View File

@ -82,5 +82,5 @@ const handleJump = (routeName: string) => {
</script> </script>
<style lang="scss"> <style lang="scss" scoped>
</style> </style>

View File

@ -202,7 +202,7 @@ const collectSpecialMenuNamesLevel1 = (menus: any[]) =>{
} }
</script> </script>
<style> <style scoped>
:root, :root,
body { body {
--layout-side-hover-bg: #f7f8fa; --layout-side-hover-bg: #f7f8fa;
@ -210,7 +210,7 @@ body {
--layout-side-active-text: var(--el-color-primary); --layout-side-active-text: var(--el-color-primary);
} }
</style> </style>
<style lang="scss"> <style lang="scss" scoped>
.two-menu { .two-menu {
.aside-menu:not(.el-menu--collapse) { .aside-menu:not(.el-menu--collapse) {
@ -218,7 +218,7 @@ body {
padding-top: 16px; padding-top: 16px;
border: 0; border: 0;
.el-menu-item { :deep(.el-menu-item) {
height: 36px; height: 36px;
margin: 0 8px 4px; margin: 0 8px 4px;
padding: 0 !important; padding: 0 !important;
@ -242,10 +242,10 @@ body {
} }
} }
.el-sub-menu { :deep(.el-sub-menu) {
margin-bottom: 8px; margin-bottom: 8px;
.el-sub-menu__title { :deep(.el-sub-menu__title) {
height: 36px; height: 36px;
margin: 0 8px 4px; margin: 0 8px 4px;
padding-left: 0; padding-left: 0;
@ -262,19 +262,19 @@ body {
color: var(--el-color-primary); color: var(--el-color-primary);
background-color: #fff !important; background-color: #fff !important;
} }
.el-icon.el-sub-menu__icon-arrow { .el-icon:deep(.el-sub-menu__icon-arrow) {
right: 5px; right: 5px;
} }
} }
.el-menu-item { :deep(.el-menu-item) {
padding-left: 25px !important; padding-left: 25px !important;
span{ span{
margin-left: 0 !important; margin-left: 0 !important;
} }
} }
.el-sub-menu{ :deep(.el-sub-menu){
.el-sub-menu__title{ :deep(.el-sub-menu__title){
margin: 0 8px 2px; margin: 0 8px 2px;
height: 40px; height: 40px;
padding-left: 18px; padding-left: 18px;
@ -290,7 +290,7 @@ body {
color: var(--el-color-primary); color: var(--el-color-primary);
} }
} }
.el-menu-item{ :deep(.el-menu-item){
padding-left: 40px !important; padding-left: 40px !important;
span{ span{
margin-left: 0 !important; margin-left: 0 !important;
@ -306,7 +306,7 @@ body {
padding: 8px; padding: 8px;
min-width: auto; min-width: auto;
} }
.el-menu-item, .el-sub-menu, .el-sub-menu{ :deep(.el-menu-item), :deep(.el-sub-menu), :deep(.el-sub-menu){
height: auto!important; height: auto!important;
padding: 10px; padding: 10px;
line-height: 1; line-height: 1;

View File

@ -397,7 +397,7 @@ const handleRouteSelect = (name:any) => {
} }
</script> </script>
<style> <style scoped>
:root, :root,
body { body {
--layout-header-bg: #fff; --layout-header-bg: #fff;

View File

@ -14,7 +14,14 @@ import VueUeditorWrap from 'vue-ueditor-wrap'
window.hl = hljs window.hl = hljs
import { initAddonManifests } from '@/utils/addon-lang'
import { language } from '@/lang'
async function run() { async function run() {
if (!import.meta.env.DEV) {
await initAddonManifests()
await language.preloadAddonLangs()
}
const app = createApp(App) const app = createApp(App)
app.use(pinia) app.use(pinia)
app.use(lang) app.use(lang)

View File

@ -6,14 +6,17 @@ import { language } from '@/lang'
import useSystemStore from '@/stores/modules/system' import useSystemStore from '@/stores/modules/system'
import useUserStore from '@/stores/modules/user' import useUserStore from '@/stores/modules/user'
import { setWindowTitle, getAppType, urlToRouteRaw } from '@/utils/common' import { setWindowTitle, getAppType, urlToRouteRaw } from '@/utils/common'
import { resolveRouteAddon, resolveRouteView } from '@/utils/addon-loader'
// 加载插件中定义的router // 加载插件中定义的router仅开发环境从源码加载
const ADDON_ROUTE = [] const ADDON_ROUTE: any[] = []
const addonRoutes = import.meta.globEager('@/addon/**/router/index.ts') if (import.meta.env.DEV) {
for (const key in addonRoutes) { const addonRoutes = import.meta.globEager('@/addon/**/router/index.ts')
const addon: any = addonRoutes[key] for (const key in addonRoutes) {
addon.ROUTE && ADDON_ROUTE.push(...addon.ROUTE) const addon: any = addonRoutes[key]
addon.NO_LOGIN_ROUTES && NO_LOGIN_ROUTES.push(...addon.NO_LOGIN_ROUTES) addon.ROUTE && ADDON_ROUTE.push(...addon.ROUTE)
addon.NO_LOGIN_ROUTES && NO_LOGIN_ROUTES.push(...addon.NO_LOGIN_ROUTES)
}
} }
const router = createRouter({ const router = createRouter({
@ -66,7 +69,11 @@ router.beforeEach(async (to: any, from, next) => {
} }
// 加载语言包 // 加载语言包
await language.loadLocaleMessages(to.meta.addon || '', (to.meta.view || to.path), systemStore.lang); await language.loadLocaleMessages(
resolveRouteAddon(to),
resolveRouteView(to),
systemStore.lang
);
let matched: any = to.matched; let matched: any = to.matched;
if (matched && matched.length && matched[0].path != '/:pathMatch(.*)*') { if (matched && matched.length && matched[0].path != '/:pathMatch(.*)*') {

View File

@ -1,6 +1,7 @@
import { RouteRecordRaw, RouterView } from 'vue-router' import { RouteRecordRaw, RouterView } from 'vue-router'
import Default from '@/layout/index.vue' import Default from '@/layout/index.vue'
import Decorate from '@/layout/decorate/index.vue' import Decorate from '@/layout/decorate/index.vue'
import { loadAddonView } from '@/utils/addon-loader'
// 静态路由 // 静态路由
export const STATIC_ROUTES: Array<RouteRecordRaw> = [ export const STATIC_ROUTES: Array<RouteRecordRaw> = [
@ -104,8 +105,9 @@ export const DECORATE_ROUTER: RouteRecordRaw = {
children: [] children: []
} }
const modules = import.meta.glob('@/app/views/**/*.vue') const modules = import.meta.glob('../app/views/**/*.vue')
const addonModules = import.meta.glob('@/addon/**/views/**/*.vue') // 开发环境:直接用 import.meta.glob 从源码加载所有插件视图(无需 manifest
const addonModules = import.meta.glob('../addon/**/views/**/*.vue')
interface Route { interface Route {
menu_name: string, menu_name: string,
@ -151,7 +153,26 @@ const createRoute = function (route: Route, parentRoute: RouteRecordRaw | null =
} }
} }
if (route.menu_type == 1) { if (route.menu_type == 1) {
record.component = route.addon ? addonModules[`/src/addon/${ route.addon }/views/${ route.view_path }.vue`] : modules[`/src/app/views/${ route.view_path }.vue`] if (route.addon) {
// view_path 优先,为空时 fallback 用 router_path
let viewPath = route.view_path
if (!viewPath && route.router_path) {
viewPath = route.router_path
}
if (viewPath) {
if (import.meta.env.DEV) {
// 开发环境:直接用 import.meta.glob 从源码加载(无需 addon-loader/manifest
record.component = addonModules[`../addon/${route.addon}/views/${viewPath}.vue`]
} else {
// 生产环境:通过编译产物的 addon-loader 加载
record.component = loadAddonView(route.addon, viewPath)
}
}
// 无 viewPath 且无 router_path → 不设 component作为容器路由
// 其 children由 formatRouters 递归创建)通过父级 <RouterView> 渲染
} else {
record.component = modules[`../app/views/${ route.view_path }.vue`]
}
} }
return record return record
} }

View File

@ -1,3 +1,4 @@
@import "addon/higohome/iconfont.css";
@import "addon/home_service/iconfont.css"; @import "addon/home_service/iconfont.css";
@import "addon/o2o/iconfont.css"; @import "addon/o2o/iconfont.css";
@import "addon/tourism/iconfont.css"; @import "addon/tourism/iconfont.css";

View File

@ -0,0 +1,643 @@
@font-face {
font-family: 'hi_iconfont'; /* Project id 4494655 */
src: url('//at.alicdn.com/t/c/font_4494655_tdxdqgcaf5r.woff2?t=1737198657789') format('woff2'),
url('//at.alicdn.com/t/c/font_4494655_tdxdqgcaf5r.woff?t=1737198657789') format('woff'),
url('//at.alicdn.com/t/c/font_4494655_tdxdqgcaf5r.ttf?t=1737198657789') format('truetype');
}
.hi_iconfont {
font-family: "hi_iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.hi_icon-biaodan:before {
content: "\e669";
}
.hi_icon-dianhua:before {
content: "\e665";
}
.hi_icon-shengao:before {
content: "\e662";
}
.hi_icon-edu-line:before {
content: "\e663";
}
.hi_icon-edu-s:before {
content: "\e664";
}
.hi_icon-sousuo:before {
content: "\e661";
}
.hi_icon-fenxiang:before {
content: "\e660";
}
.hi_icon-nv1:before {
content: "\e668";
}
.hi_icon-xuanzhong:before {
content: "\e65f";
}
.hi_icon-nan:before {
content: "\e65d";
}
.hi_icon-nv:before {
content: "\e8b3";
}
.hi_icon-shezhi1:before {
content: "\e65c";
}
.hi_icon-jifenshuoming:before {
content: "\e7ae";
}
.hi_icon-tuiguangpaihang:before {
content: "\e65b";
}
.hi_icon-dingdan1:before {
content: "\e6ad";
}
.hi_icon-dengjixunzhang:before {
content: "\1012e";
}
.hi_icon-yongjin:before {
content: "\e708";
}
.hi_icon-paihangbang:before {
content: "\e66e";
}
.hi_icon-icon:before {
content: "\e658";
}
.hi_icon-dingdan:before {
content: "\e655";
}
.hi_icon-top:before {
content: "\e659";
}
.hi_icon-tuandui:before {
content: "\e65a";
}
.hi_icon-shuoming:before {
content: "\f4f9";
}
.hi_icon-fenxiao-fenxiaojilu:before {
content: "\e653";
}
.hi_icon-RectangleCopy:before {
content: "\e6b1";
}
.hi_icon-pc-fenxiaoshangfenxiao:before {
content: "\e786";
}
.hi_icon-woyaofenxiao:before {
content: "\e66f";
}
.hi_icon-fenxiao-fenxiaoshuju:before {
content: "\e8d1";
}
.hi_icon-fenxiaoyeji:before {
content: "\e76c";
}
.hi_icon-jiantou1:before {
content: "\e64e";
}
.hi_icon-jiantoushang:before {
content: "\e652";
}
.hi_icon-erweima:before {
content: "\e64b";
}
.hi_icon-yanse:before {
content: "\e679";
}
.hi_icon-youhuiquan2:before {
content: "\e884";
}
.hi_icon-shouye:before {
content: "\e649";
}
.hi_icon-weixin:before {
content: "\e64a";
}
.hi_icon-dashiruzhu:before {
content: "\e645";
}
.hi_icon-hehuoren:before {
content: "\e646";
}
.hi_icon-daili:before {
content: "\e647";
}
.hi_icon-amazon-onestore:before {
content: "\e75f";
}
.hi_icon-a-Property1yewuyuanruzhu:before {
content: "\e648";
}
.hi_icon-mendianruzhu:before {
content: "\e66b";
}
.hi_icon-mendian1:before {
content: "\e682";
}
.hi_icon-shijian:before {
content: "\e63f";
}
.hi_icon-shezhi:before {
content: "\e656";
}
.hi_icon-jishiguanli:before {
content: "\e63e";
}
.hi_icon-jujue1:before {
content: "\e639";
}
.hi_icon-tuikuanshouhou:before {
content: "\e6bb";
}
.hi_icon-zhuandan:before {
content: "\e677";
}
.hi_icon-tongyi:before {
content: "\e636";
}
.hi_icon-jujue:before {
content: "\e638";
}
.hi_icon-wancheng1:before {
content: "\e633";
}
.hi_icon-daijiedan1:before {
content: "\e62b";
}
.hi_icon-chufa:before {
content: "\e62c";
}
.hi_icon-bangdingqudaoshang:before {
content: "\e629";
}
.hi_icon-xiangmu:before {
content: "\e614";
}
.hi_icon-kehuC:before {
content: "\e60d";
}
.hi_icon-huiyuan2:before {
content: "\e644";
}
.hi_icon-qianbao1:before {
content: "\e651";
}
.hi_icon-dapinglunxun:before {
content: "\e627";
}
.hi_icon-yingshoubaobiao:before {
content: "\e6b2";
}
.hi_icon-xuanzekuangmoren:before {
content: "\e6c1";
}
.hi_icon-xuanzekuangxuanzhong:before {
content: "\e678";
}
.hi_icon-icon-test1:before {
content: "\e666";
}
.hi_icon-zan1:before {
content: "\e872";
}
.hi_icon-tubiaozhizuo-:before {
content: "\e60f";
}
.hi_icon-jia:before {
content: "\e625";
}
.hi_icon-shipin1:before {
content: "\e622";
}
.hi_icon-tuwenzixun:before {
content: "\e683";
}
.hi_icon-shipin:before {
content: "\e642";
}
.hi_icon-zan:before {
content: "\e870";
}
.hi_icon-pinglun3:before {
content: "\e641";
}
.hi_icon-xiangji1fill:before {
content: "\e77e";
}
.hi_icon-xiangji:before {
content: "\e621";
}
.hi_icon-xuanze:before {
content: "\e62f";
}
.hi_icon-xuanze-weixuanze:before {
content: "\e615";
}
.hi_icon-gou:before {
content: "\e66c";
}
.hi_icon-dacha:before {
content: "\e711";
}
.hi_icon-chanpin1:before {
content: "\e61f";
}
.hi_icon-xiaoxi:before {
content: "\e635";
}
.hi_icon-chanpin:before {
content: "\e650";
}
.hi_icon-airudiantubiaohuizhi-zhuanqu_zixundongtai:before {
content: "\e69d";
}
.hi_icon-dongtai:before {
content: "\e64f";
}
.hi_icon-ziyuan:before {
content: "\e61d";
}
.hi_icon-youhuiquan1:before {
content: "\e61e";
}
.hi_icon-shengyin08-mianxing:before {
content: "\e6eb";
}
.hi_icon-pinglun2:before {
content: "\e63a";
}
.hi_icon-zanfill:before {
content: "\e6ce";
}
.hi_icon-fensiguanli:before {
content: "\e62a";
}
.hi_icon-basesalesupgradeSet:before {
content: "\e632";
}
.hi_icon-tixian:before {
content: "\e67c";
}
.hi_icon-tixian1:before {
content: "\e619";
}
.hi_icon-dingdanwuliaocaigouguanli:before {
content: "\e63b";
}
.hi_icon-chefeijilu:before {
content: "\e613";
}
.hi_icon-zhongchaping:before {
content: "\e634";
}
.hi_icon-addBlack:before {
content: "\e630";
}
.hi_icon-yidongfanyong:before {
content: "\eb68";
}
.hi_icon-fanyong:before {
content: "\e67d";
}
.hi_icon-fanyongjilu:before {
content: "\e605";
}
.hi_icon-daifuwu2:before {
content: "\e7e5";
}
.hi_icon-daijiedan:before {
content: "\e60c";
}
.hi_icon-jiantou:before {
content: "\e65e";
}
.hi_icon-icon-test:before {
content: "\e60b";
}
.hi_icon-show_more:before {
content: "\e637";
}
.hi_icon-jiantoukongxin_up:before {
content: "\e612";
}
.hi_icon-jiantou-zuoxia:before {
content: "\e643";
}
.hi_icon-zuojiantou:before {
content: "\e61b";
}
.hi_icon-xiajiantou:before {
content: "\e6b3";
}
.hi_icon-alarm-full:before {
content: "\e871";
}
.hi_icon-weizhigengxin:before {
content: "\e929";
}
.hi_icon-jihuatingjishijianshezhi_:before {
content: "\e60a";
}
.hi_icon-ditu:before {
content: "\e72d";
}
.hi_icon-jinjiqiuzhu-:before {
content: "\e6be";
}
.hi_icon-qianbao:before {
content: "\e829";
}
.hi_icon-zhifubaozhifu:before {
content: "\e654";
}
.hi_icon-weixinzhifu:before {
content: "\e607";
}
.hi_icon-dingwei:before {
content: "\e63c";
}
.hi_icon-dingwei1:before {
content: "\e603";
}
.hi_icon-dingwei2:before {
content: "\e604";
}
.hi_icon-dingwei3:before {
content: "\e717";
}
.hi_icon-dingwei4:before {
content: "\e8c4";
}
.hi_icon-dingwei5:before {
content: "\e62d";
}
.hi_icon-zaixianjiedan2mian:before {
content: "\e64d";
}
.hi_icon-wancheng:before {
content: "\e60e";
}
.hi_icon-chufadi:before {
content: "\e6ff";
}
.hi_icon-chufagangkou:before {
content: "\e626";
}
.hi_icon-daodamudedi:before {
content: "\e657";
}
.hi_icon-woyaofankui:before {
content: "\e631";
}
.hi_icon-pinglun1:before {
content: "\e8b4";
}
.hi_icon-tousutiwen:before {
content: "\e624";
}
.hi_icon-quanqudaofuwujiankong:before {
content: "\e6cf";
}
.hi_icon-mendian:before {
content: "\e61a";
}
.hi_icon-fenxianglaxin:before {
content: "\e617";
}
.hi_icon-dituibang1-09:before {
content: "\e618";
}
.hi_icon-laxinliebian:before {
content: "\e74c";
}
.hi_icon-quan:before {
content: "\e602";
}
.hi_icon-shoucang:before {
content: "\e623";
}
.hi_icon-bangzhu:before {
content: "\e8a3";
}
.hi_icon-youhuiquan:before {
content: "\e61c";
}
.hi_icon-daifuwu1:before {
content: "\e610";
}
.hi_icon-pinglun:before {
content: "\e609";
}
.hi_icon-daipingjia20:before {
content: "\e63d";
}
.hi_icon-yiwancheng:before {
content: "\e6a6";
}
.hi_icon-yiquxiao:before {
content: "\e69c";
}
.hi_icon-daizhifu:before {
content: "\e694";
}
.hi_icon-icon_xinyong_xianxing_jijin-:before {
content: "\e640";
}
.hi_icon-licai:before {
content: "\e600";
}
.hi_icon-diwup12:before {
content: "\e62e";
}
.hi_icon-shenqingdailishang:before {
content: "\e608";
}
.hi_icon-dailishang:before {
content: "\e628";
}
.hi_icon-yewuyuan:before {
content: "\e601";
}
.hi_icon-zu176:before {
content: "\e606";
}
.hi_icon-shenpitongguo:before {
content: "\e611";
}
.hi_icon-daifuwu:before {
content: "\e64c";
}
.hi_icon-jishi:before {
content: "\e616";
}
.hi_icon-yonghu:before {
content: "\e620";
}
.hi_icon-yonghu1:before {
content: "\e667";
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,255 @@
/**
* manifest core shared admin-lang addon-loader
*/
export interface AddonManifest {
key: string
version?: string
sharedVersion?: string
views?: Record<string, string>
components?: Record<string, string>
layout?: string
langBase?: string
}
let prodManifests: Record<string, AddonManifest> = {}
let installedAddonKeys: string[] = []
let prodReady = false
let prodInitPromise: Promise<void> | null = null
const addonModuleCache: Record<string, Promise<Record<string, unknown> | null>> = {}
const ADMIN_PREFIX = '/admin'
export function adminUrl(relative: string) {
if (!relative) return relative
if (relative.startsWith('http') || relative.startsWith('/admin/')) return relative
if (relative.startsWith('/assets/')) return `${ADMIN_PREFIX}${relative}`
if (relative.startsWith('./')) return `${ADMIN_PREFIX}/assets/addons/${relative.slice(2)}`
return `${ADMIN_PREFIX}/${relative.replace(/^\//, '')}`
}
function addonAssetUrl(addon: string, ...segments: string[]) {
const rel = ['assets', 'addons', addon, ...segments].join('/').replace(/\/+/g, '/')
return `${ADMIN_PREFIX}/${rel}`
}
export function resolveLangFile(app: string, path: string): string {
if (path === '/') return 'index'
let view = path.replace(/^(\/admin\/|\/site\/|\/)/, '').replace(/\.vue$/, '')
if (view.startsWith('views/')) view = view.slice('views/'.length)
if (app) {
if (view.startsWith(`${app}/`)) view = view.slice(app.length + 1)
else if (view === app) view = 'index'
}
return view.replaceAll('/', '.')
}
export function resolveRouteAddon(route: { meta: Record<string, unknown>; matched: Array<{ meta: Record<string, unknown> }> }): string {
if (route.meta.addon) return String(route.meta.addon)
for (let i = route.matched.length - 1; i >= 0; i--) {
const addon = route.matched[i].meta.addon
if (addon) return String(addon)
}
return ''
}
export function resolveRouteView(route: { meta: Record<string, unknown>; matched: Array<{ meta: Record<string, unknown> }>; path: string }): string {
if (route.meta.view) return String(route.meta.view)
for (let i = route.matched.length - 1; i >= 0; i--) {
const view = route.matched[i].meta.view
if (view) return String(view)
}
return route.path
}
function unwrapLangPack(pack: unknown): Record<string, string> {
if (!pack || typeof pack !== 'object') return {}
const obj = pack as Record<string, unknown>
if (obj.default && typeof obj.default === 'object') {
return unwrapLangPack(obj.default)
}
const result: Record<string, string> = {}
for (const [key, value] of Object.entries(obj)) {
if (key === 'default' || key === '__esModule') continue
if (typeof value === 'string') result[key] = value
}
return Object.keys(result).length ? result : (obj as Record<string, string>)
}
function flattenLangPack(file: string, pack: Record<string, string>): Record<string, string> {
const data: Record<string, string> = {}
for (const [key, value] of Object.entries(pack)) {
if (typeof value !== 'string') continue
data[`${file}.${key}`] = value
if (file === 'common') data[key] = value
}
return data
}
export function inferAddonFromPath(path: string, knownAddons?: string[]): string {
const view = path.replace(/^(\/admin\/|\/site\/|\/)/, '')
const first = view.split('/').filter(Boolean)[0]
if (!first) return ''
const keys = knownAddons?.length ? knownAddons : getInstalledAddonKeys()
return keys.includes(first) ? first : ''
}
async function isAddonEntryAvailable(key: string): Promise<boolean> {
try {
const res = await fetch(adminUrl(`/assets/addons/${key}/index.js`), { method: 'HEAD' })
if (!res.ok) return false
const ct = (res.headers.get('content-type') || '').toLowerCase()
return !ct.includes('text/html')
} catch {
return false
}
}
export function isAddonInstalled(addon: string): boolean {
return !!addon && getInstalledAddonKeys().includes(addon)
}
export async function loadAddonModule(addon: string): Promise<Record<string, unknown> | null> {
if (!(addon in addonModuleCache)) {
addonModuleCache[addon] = (async () => {
try {
const url = adminUrl(`/assets/addons/${addon}/index.js`)
return await import(/* @vite-ignore */ url) as Record<string, unknown>
} catch {
return null
}
})()
}
return addonModuleCache[addon]
}
/** 清除插件模块缓存(布局切换时调用,确保样式重新注入) */
export function clearAddonModuleCache() {
for (const key of Object.keys(addonModuleCache)) {
delete addonModuleCache[key]
}
}
export function isAddonProdReady() {
return prodReady
}
export async function ensureAddonProdReady() {
if (!prodReady) await initAddonManifests()
}
export async function initAddonManifests(keys?: string[]) {
if (import.meta.env.DEV) {
prodReady = true
return
}
if (prodInitPromise) return prodInitPromise
prodInitPromise = (async () => {
prodManifests = {}
installedAddonKeys = []
let addonKeys = keys
if (!addonKeys?.length) {
try {
const indexUrl = adminUrl('/assets/addons/index.json')
const res = await fetch(indexUrl)
if (res.ok) {
const data = await res.json()
addonKeys = Array.isArray(data.keys) ? data.keys : []
}
} catch {
addonKeys = []
}
}
const verifiedKeys: string[] = []
for (const key of addonKeys || []) {
if (!(await isAddonEntryAvailable(key))) continue
verifiedKeys.push(key)
try {
const url = adminUrl(`/assets/addons/${key}/manifest.json`)
const res = await fetch(url)
if (res.ok) {
prodManifests[key] = await res.json()
}
} catch {
// skip missing manifest
}
}
installedAddonKeys = verifiedKeys
prodReady = true
})()
return prodInitPromise
}
export function getInstalledAddonKeys(): string[] {
if (installedAddonKeys.length) return installedAddonKeys
return Object.keys(prodManifests)
}
export function getProdManifests(): Record<string, AddonManifest> {
return prodManifests
}
export async function preloadAllAddonLangs(
merge: (locale: string, messages: Record<string, string>) => void
) {
if (import.meta.env.DEV) return
if (!prodReady) await initAddonManifests()
for (const addon of getInstalledAddonKeys()) {
try {
const mod = await loadAddonModule(addon)
if (!mod) continue
const langs = mod.langs as Record<string, Record<string, unknown>> | undefined
if (!langs) continue
for (const [locale, files] of Object.entries(langs)) {
const merged: Record<string, string> = {}
for (const [file, pack] of Object.entries(files || {})) {
Object.assign(merged, flattenLangPack(file, unwrapLangPack(pack)))
}
if (Object.keys(merged).length) merge(locale, merged)
}
} catch {
// skip broken addon module
}
}
}
export function registerAddonManifest(manifest: AddonManifest) {
prodManifests[manifest.key] = manifest
prodReady = true
}
export async function loadAddonLang(addon: string, locale: string, file: string): Promise<Record<string, string>> {
if (import.meta.env.DEV) {
try {
const messages = await import(/* @vite-ignore */ `@/addon/${addon}/lang/${locale}/${file}.json`)
return messages.default || {}
} catch {
return {}
}
}
if (!prodReady) await initAddonManifests()
try {
const mod = await loadAddonModule(addon)
if (mod) {
const langs = mod.langs as Record<string, Record<string, Record<string, unknown>>> | undefined
const pack = langs?.[locale]?.[file]
const messages = unwrapLangPack(pack)
if (Object.keys(messages).length) return messages
}
} catch {
// fallback to static lang files
}
const url = addonAssetUrl(addon, 'lang', locale, `${file}.json`)
try {
const res = await fetch(url)
if (res.ok) return await res.json()
} catch {
// ignore
}
return {}
}

View File

@ -0,0 +1,204 @@
/**
* dev import.meta.glob prod drop-in
*/
import type { Component } from 'vue'
import { defineAsyncComponent } from 'vue'
import {
type AddonManifest,
adminUrl,
clearAddonModuleCache,
ensureAddonProdReady,
getProdManifests,
initAddonManifests,
loadAddonLang,
loadAddonModule,
preloadAllAddonLangs,
registerAddonManifest,
resolveLangFile,
resolveRouteAddon,
resolveRouteView,
inferAddonFromPath,
getInstalledAddonKeys,
isAddonInstalled
} from '@/utils/addon-lang'
export type { AddonManifest }
export {
adminUrl,
initAddonManifests,
loadAddonLang,
preloadAllAddonLangs,
registerAddonManifest,
resolveLangFile,
resolveRouteAddon,
resolveRouteView,
inferAddonFromPath,
getInstalledAddonKeys,
loadAddonModule
}
/** 清除插件样式 DOM 节点和模块缓存(布局切换时用) */
export function clearAddonStyleCache() {
document.querySelectorAll('style[data-addon-style]').forEach(el => el.remove())
clearAddonModuleCache()
}
type ViewLoader = () => Promise<{ default: Component }>
const load404 = (): Promise<{ default: Component }> =>
import('@/app/views/error/404.vue') as Promise<{ default: Component }>
// 开发环境:用 import.meta.glob 直读所有插件源码(无需 addon-manifest.dev.ts
const allAddonVueModules = import.meta.glob('../addon/**/views/**/*.vue')
const addonLayoutModules = import.meta.glob('../addon/*/layout/index.vue')
async function importProdModule(url: string): Promise<Component | null> {
try {
const full = adminUrl(url.startsWith('./') ? url : url)
const mod = await import(/* @vite-ignore */ full)
return mod.default ?? mod
} catch {
return null
}
}
export function loadAddonView(addon: string, viewPath: string): ViewLoader {
if (!addon) {
return () => load404()
}
// 开发环境glob 直读源码routers.ts 已优先走 glob此处为兜底
if (import.meta.env.DEV) {
return async () => {
const loader = allAddonVueModules[`../addon/${addon}/views/${viewPath}.vue`] as ViewLoader | undefined
if (loader) return loader()
return load404()
}
}
return async () => {
await ensureAddonProdReady()
if (!isAddonInstalled(addon)) {
console.error(`[loadAddonView] addon "${addon}" not installed. Installed:`, getInstalledAddonKeys())
return load404()
}
try {
const mod = await loadAddonModule(addon)
if (!mod) {
console.error(`[loadAddonView] addon "${addon}" module loaded as null/undefined`)
return load404()
}
const loader = mod.views?.[viewPath] as ViewLoader | undefined
if (!loader) {
const available = Object.keys(mod.views || {})
console.error(`[loadAddonView] addon="${addon}" viewPath="${viewPath}" NOT FOUND. Available (${available.length}):`, available.slice(0, 10), available.length > 10 ? `... and ${available.length - 10} more` : '')
return load404()
}
try {
return await loader()
} catch (e) {
console.error(`[loadAddonView] addon="${addon}" viewPath="${viewPath}" loader threw:`, e)
return load404()
}
} catch (e) {
console.error(`[loadAddonView] addon="${addon}" module import failed:`, e)
return load404()
}
}
}
export function loadAddonComponent(addon: string, subPath: string, name: string): ViewLoader {
const key = `${subPath}/${name}`.replace(/\/+/g, '/')
if (import.meta.env.DEV) {
return async () => {
const loader = allAddonVueModules[`../addon/${addon}/views/${subPath}/${name}.vue`] as ViewLoader | undefined
if (loader) return loader()
return load404()
}
}
return (async () => {
await ensureAddonProdReady()
if (!isAddonInstalled(addon)) return null as any
try {
const mod = await loadAddonModule(addon)
if (!mod) return null as any
const components = (mod as Record<string, unknown>).components as Record<string, ViewLoader> | undefined
if (!components) return null as any
const loader = components[key] as ViewLoader | undefined
if (!loader) return null as any
return await loader()
} catch {
return null as any
}
}) as unknown as ViewLoader
}
export function loadAddonLayout(addon: string): ViewLoader | null {
if (import.meta.env.DEV) {
return async () => {
const loader = addonLayoutModules[`../addon/${addon}/layout/index.vue`] as ViewLoader | undefined
if (loader) return loader()
return load404()
}
}
return async () => {
await ensureAddonProdReady()
if (!isAddonInstalled(addon)) return load404()
const rel = getProdManifests()[addon]?.layout
if (!rel) return load404()
const mod = await importProdModule(rel)
if (!mod) return load404()
return { default: mod as Component }
}
}
/** Core 内动态组件pay/diy-link 等),不含 addon 部分 */
export const coreVueModules = {
...import.meta.glob('../app/**/*.vue'),
...import.meta.glob('../components/**/*.vue'),
...import.meta.glob('../layout/**/*.vue')
}
function normalizeComponentKey(p: string): string {
return p
.replace(/^@\//, '')
.replace(/^\/src\//, '')
.replace(/^\.\.\//, '')
.replace(/\\/g, '/')
}
function findCoreModuleLoader(componentPath: string): ViewLoader | undefined {
const map = coreVueModules as Record<string, ViewLoader>
if (map[componentPath]) return map[componentPath]
const target = normalizeComponentKey(componentPath)
const key = Object.keys(map).find((k) => normalizeComponentKey(k) === target || normalizeComponentKey(k).endsWith(`/${target}`))
return key ? map[key] : undefined
}
export function resolveAsyncComponent(componentPath: string) {
return defineAsyncComponent(async () => {
const loader = findCoreModuleLoader(componentPath)
if (loader) return (await loader()).default as Component
const mod = await loadCoreVueModule(componentPath)
return mod.default as Component
})
}
export async function loadCoreVueModule(componentPath: string) {
const loader = findCoreModuleLoader(componentPath)
if (loader) return loader()
if (import.meta.env.DEV) {
if (componentPath.includes('/addon/')) {
const m = componentPath.match(/\/addon\/([^/]+)\/views\/(.+)\.vue$/)
if (m) {
const [, addon, rest] = m
const viewLoader = allAddonVueModules[`../addon/${addon}/views/${rest}.vue`] as ViewLoader | undefined
if (viewLoader) return viewLoader()
}
}
}
throw new Error(`Component not found: ${componentPath}`)
}

View File

@ -3,6 +3,7 @@ import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import { useCssVar, useTitle } from '@vueuse/core' import { useCssVar, useTitle } from '@vueuse/core'
import colorFunction from 'css-color-function' import colorFunction from 'css-color-function'
import storage from './storage' import storage from './storage'
import envConfig from '@/utils/config'
/** /**
* element-icon * element-icon
@ -65,7 +66,7 @@ export function getAppType() {
*/ */
export function setWindowTitle(value: string = ''): void { export function setWindowTitle(value: string = ''): void {
const title = useTitle() const title = useTitle()
title.value = value ? value : import.meta.env.VITE_DETAULT_TITLE title.value = value ? value : envConfig.VITE_DETAULT_TITLE
} }
/** /**
@ -129,7 +130,7 @@ export function isUrl(str: string): boolean {
export function img(path: string): string { export function img(path: string): string {
if (!path) return '' if (!path) return ''
let imgDomain = import.meta.env.VITE_IMG_DOMAIN || location.origin let imgDomain = envConfig.VITE_IMG_DOMAIN || location.origin
if (path.startsWith('/')) path = path.replace(/^\//, '') if (path.startsWith('/')) path = path.replace(/^\//, '')
if (imgDomain.endsWith('/')) imgDomain = imgDomain.slice(0, -1) if (imgDomain.endsWith('/')) imgDomain = imgDomain.slice(0, -1)

10
admin/src/utils/config.ts Normal file
View File

@ -0,0 +1,10 @@
// 优先读取外挂 window.__ENV__部署后可直接修改无需重编译
const env = (typeof window !== 'undefined' ? (window as any).__ENV__ : null) || {}
export default {
VITE_APP_BASE_URL: env.VITE_APP_BASE_URL || import.meta.env.VITE_APP_BASE_URL,
VITE_IMG_DOMAIN: env.VITE_IMG_DOMAIN || import.meta.env.VITE_IMG_DOMAIN,
VITE_REQUEST_HEADER_TOKEN_KEY: env.VITE_REQUEST_HEADER_TOKEN_KEY || import.meta.env.VITE_REQUEST_HEADER_TOKEN_KEY || 'token',
VITE_REQUEST_HEADER_SITEID_KEY: env.VITE_REQUEST_HEADER_SITEID_KEY || import.meta.env.VITE_REQUEST_HEADER_SITEID_KEY || 'site-id',
VITE_DETAULT_TITLE: env.VITE_DETAULT_TITLE || import.meta.env.VITE_DETAULT_TITLE || ''
}

View File

@ -0,0 +1,57 @@
/**
* Core pay/diy-link core 使 addon
*/
import type { Component } from 'vue'
import { defineAsyncComponent } from 'vue'
export const coreVueModules = {
...import.meta.glob('../app/**/*.vue'),
...import.meta.glob('../components/**/*.vue'),
...import.meta.glob('../layout/**/*.vue')
}
type ViewLoader = () => Promise<{ default: Component }>
function normalizeComponentKey(p: string): string {
return p
.replace(/^@\//, '')
.replace(/^\/src\//, '')
.replace(/^\.\.\//, '')
.replace(/\\/g, '/')
}
function findCoreModuleLoader(componentPath: string): ViewLoader | undefined {
const map = coreVueModules as Record<string, ViewLoader>
if (map[componentPath]) return map[componentPath]
const target = normalizeComponentKey(componentPath)
const key = Object.keys(map).find((k) => normalizeComponentKey(k) === target || normalizeComponentKey(k).endsWith(`/${target}`))
return key ? map[key] : undefined
}
export function resolveAsyncComponent(componentPath: string) {
return defineAsyncComponent(async () => {
const loader = findCoreModuleLoader(componentPath)
if (loader) return (await loader()).default as Component
const mod = await loadCoreVueModule(componentPath)
return mod.default as Component
})
}
// 开发环境glob 直读插件源码(无需 addon-manifest.dev.ts
const allAddonVueModules = import.meta.glob('../addon/**/views/**/*.vue')
export async function loadCoreVueModule(componentPath: string) {
const loader = findCoreModuleLoader(componentPath)
if (loader) return loader()
if (import.meta.env.DEV) {
if (componentPath.includes('/addon/')) {
const m = componentPath.match(/\/addon\/([^/]+)\/views\/(.+)\.vue$/)
if (m) {
const [, addon, rest] = m
const viewLoader = (allAddonVueModules as Record<string, ViewLoader>)[`../addon/${addon}/views/${rest}.vue`]
if (viewLoader) return viewLoader()
}
}
}
throw new Error(`Component not found: ${componentPath}`)
}

View File

@ -6,6 +6,7 @@ import type { MessageParams } from 'element-plus'
import { t } from '@/lang' import { t } from '@/lang'
import useUserStore from '@/stores/modules/user' import useUserStore from '@/stores/modules/user'
import storage from '@/utils/storage' import storage from '@/utils/storage'
import envConfig from '@/utils/config'
interface RequestConfig extends AxiosRequestConfig { interface RequestConfig extends AxiosRequestConfig {
showErrorMessage?: boolean showErrorMessage?: boolean
@ -47,7 +48,7 @@ class Request {
constructor() { constructor() {
this.instance = axios.create({ this.instance = axios.create({
baseURL: import.meta.env.VITE_APP_BASE_URL.substr(-1) == '/' ? import.meta.env.VITE_APP_BASE_URL : `${import.meta.env.VITE_APP_BASE_URL}/`, baseURL: envConfig.VITE_APP_BASE_URL.substr(-1) == '/' ? envConfig.VITE_APP_BASE_URL : `${envConfig.VITE_APP_BASE_URL}/`,
timeout: 0, timeout: 0,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -60,9 +61,9 @@ class Request {
(config: InternalRequestConfig) => { (config: InternalRequestConfig) => {
// 携带token site-id // 携带token site-id
if (getToken()) { if (getToken()) {
config.headers[import.meta.env.VITE_REQUEST_HEADER_TOKEN_KEY] = getToken() config.headers[envConfig.VITE_REQUEST_HEADER_TOKEN_KEY] = getToken()
} }
config.headers[import.meta.env.VITE_REQUEST_HEADER_SITEID_KEY] = storage.get('siteId') || 0 config.headers[envConfig.VITE_REQUEST_HEADER_SITEID_KEY] = storage.get('siteId') || 0
return config return config
}, },
(err: any) => { (err: any) => {

View File

@ -0,0 +1,81 @@
import { fileURLToPath, URL } from 'node:url'
import { createRequire } from 'node:module'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
const require = createRequire(import.meta.url)
const { isSharedExternal, isAdminLangExternal, adminLangExternalPath, isCoreExternal, coreExternalPath } = require('./scripts/shared-external.cjs')
const { stripElementPlusStylePlugin } = require('./scripts/vite-plugin-strip-element-plus-style.cjs')
const addonKey = process.env.ADDON_KEY || ''
if (!addonKey) {
throw new Error('ADDON_KEY is required for addon build')
}
const entry = fileURLToPath(new URL(`./.build/addons/${addonKey}/entry.ts`, import.meta.url))
const elementPlusResolver = ElementPlusResolver({ importStyle: false })
/** 单插件生产构建 */
export default defineConfig({
base: '',
publicDir: false,
define: {
'process.env.NODE_ENV': JSON.stringify('production'),
__VUE_OPTIONS_API__: true,
__VUE_PROD_DEVTOOLS__: false,
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: false
},
plugins: [
stripElementPlusStylePlugin(),
vue(),
AutoImport({ resolvers: [elementPlusResolver] }),
Components({ resolvers: [elementPlusResolver] }),
// 注入 CSSlib 模式 CSS 统一提取到 style.css运行时注入为 <style> 标签
{
name: 'addon-css-inject',
enforce: 'post',
generateBundle(_opts, bundle) {
const entry = bundle['index.js']
const cssAsset = bundle['style.css']
if (entry && entry.type === 'chunk' && cssAsset && cssAsset.type === 'asset') {
const css = JSON.stringify(cssAsset.source)
entry.code = `(function(){var s=document.createElement('style');s.textContent=${css};s.setAttribute('data-addon-style','${addonKey}');document.head.insertBefore(s,document.head.firstChild)})();\n${entry.code}`
delete bundle['style.css']
}
}
}
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
assets: fileURLToPath(new URL('./src/assets', import.meta.url)),
'vue-i18n': 'vue-i18n/dist/vue-i18n.cjs.js'
}
},
build: {
cssCodeSplit: false,
outDir: `dist/.addons/${addonKey}`,
emptyOutDir: true,
lib: {
entry,
formats: ['es'],
fileName: () => 'index.js'
},
rollupOptions: {
external: (id) => isAdminLangExternal(id) || isSharedExternal(id) || isCoreExternal(id),
output: {
inlineDynamicImports: false,
chunkFileNames: '[name]-[hash].js',
entryFileNames: 'index.js',
paths(id) {
if (isAdminLangExternal(id)) return adminLangExternalPath()
if (isCoreExternal(id)) return coreExternalPath()
}
}
}
}
})

77
admin/vite.config.core.ts Normal file
View File

@ -0,0 +1,77 @@
import { fileURLToPath, URL } from 'node:url'
import { createRequire } from 'node:module'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
const require = createRequire(import.meta.url)
const { isSharedExternal, isAdminLangExternal, adminLangExternalPath } = require('./scripts/shared-external.cjs')
const { stripElementPlusStylePlugin } = require('./scripts/vite-plugin-strip-element-plus-style.cjs')
const rootDir = fileURLToPath(new URL('.', import.meta.url))
const elementPlusResolver = ElementPlusResolver({ importStyle: false })
/** Core 生产构建:不包含 src/addon 源码(运行时通过 manifest 加载) */
export default defineConfig({
base: '',
define: {
'process.env.NODE_ENV': JSON.stringify('production'),
__VUE_OPTIONS_API__: true,
__VUE_PROD_DEVTOOLS__: false,
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: false
},
plugins: [
stripElementPlusStylePlugin(),
vue(),
AutoImport({ resolvers: [elementPlusResolver] }),
Components({ resolvers: [elementPlusResolver] }),
{
name: 'exclude-addon-source',
enforce: 'pre',
resolveId(source, importer) {
if (source.includes('*')) return null
const norm = source.replace(/\\/g, '/')
if (importer && importer.includes('/src/addon/')) return null
if (norm.startsWith('@/addon/') || norm.includes('/src/addon/')) {
// 虚拟 id 勿以 .json 结尾,否则 vite:json 会尝试解析
return '\0addon-external:' + norm.replace(/\.json$/i, '.langdata')
}
},
load(id) {
if (id.startsWith('\0addon-external:')) {
return 'export default {}'
}
}
}
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
assets: fileURLToPath(new URL('./src/assets', import.meta.url)),
'vue-i18n': 'vue-i18n/dist/vue-i18n.cjs.js'
}
},
build: {
outDir: 'dist/.core',
emptyOutDir: true,
manifest: true,
rollupOptions: {
external: (id) => {
const norm = id.replace(/\\/g, '/')
if (norm.includes('/style/css') || norm.includes('/style/index')) return false
if (norm.includes('element-plus/dist/')) return false
if (norm.includes('element-plus/theme-chalk')) return false
if (isAdminLangExternal(id)) return true
return isSharedExternal(id)
},
output: {
paths(id) {
if (isAdminLangExternal(id)) return adminLangExternalPath()
}
}
}
}
})

View File

@ -0,0 +1,67 @@
import { fileURLToPath, URL } from 'node:url'
import { createRequire } from 'node:module'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
const require = createRequire(import.meta.url)
const { sharedExternalForBuild } = require('./scripts/shared-external.cjs')
const pkgKey = process.env.SHARED_PKG || 'vue'
const emptyOutDir = process.env.SHARED_EMPTY === '1'
const entry = fileURLToPath(new URL(`./.build/shared/${pkgKey}.ts`, import.meta.url))
const outName = pkgKey === 'icons-vue' ? 'icons-vue' : pkgKey
const needsCore = pkgKey === 'admin-lang' || pkgKey === 'core-shared'
/** 单包 shared ESM 构建peer 依赖保持 external 由 import map 解析 */
export default defineConfig({
publicDir: false,
define: {
'process.env.NODE_ENV': JSON.stringify('production'),
__VUE_OPTIONS_API__: true,
__VUE_PROD_DEVTOOLS__: false,
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: false
},
resolve: needsCore
? {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
'vue-i18n': 'vue-i18n/dist/vue-i18n.cjs.js'
}
}
: undefined,
plugins: needsCore
? [
vue(),
{
name: 'shared-exclude-addon',
enforce: 'pre',
resolveId(source) {
if (source.includes('*')) return null
const norm = source.replace(/\\?vue.*$/, '').replace(/\\/g, '/')
if (norm.startsWith('@/addon/') || norm.includes('/src/addon/')) {
return { id: 'virtual:addon-empty', moduleSideEffects: false }
}
return null
},
load(id) {
if (id === 'virtual:addon-empty') return 'export default {}'
return null
}
}
]
: [],
build: {
outDir: 'dist/.shared',
emptyOutDir,
lib: {
entry,
formats: ['es'],
fileName: () => `${outName}.js`
},
rollupOptions: {
external: sharedExternalForBuild(pkgKey)
}
}
})

View File

@ -94,6 +94,7 @@ class Cloud extends BaseAdminController
public function setLocalCloudCompileConfig() public function setLocalCloudCompileConfig()
{ {
$data = $this->request->params([ $data = $this->request->params([
[ 'is_open', 0],
[ 'url', '' ], [ 'url', '' ],
]); ]);
return success('SUCCESS',(new NiucloudService())->setLocalCloudCompileConfig($data)); return success('SUCCESS',(new NiucloudService())->setLocalCloudCompileConfig($data));
@ -108,4 +109,36 @@ class Cloud extends BaseAdminController
{ {
return success('SUCCESS',(new NiucloudService())->getLocalCloudCompileConfig()); return success('SUCCESS',(new NiucloudService())->getLocalCloudCompileConfig());
} }
/**
* 启动后台下载SSE编译完成后调用
* @description 启动后台下载
* @return \think\Response
*/
public function startServerDownload()
{
$data = $this->request->params([
[ 'task_id', '' ],
[ 'download_url', '' ],
[ 'authorize_code', '' ],
[ 'timestamp', '' ],
]);
return success('操作成功', (new CoreCloudBuildService())->startServerDownload(
$data['task_id'],
$data['download_url'],
$data['authorize_code'],
$data['timestamp']
));
}
/**
* 获取后台下载进度
* @description 获取后台下载进度
* @return \think\Response
*/
public function getSseBuildLog()
{
$taskId = $this->request->param('task_id', '');
return success('操作成功', (new CoreCloudBuildService())->getSseBuildLog($taskId));
}
} }

View File

@ -89,4 +89,13 @@ class Version extends BaseAdminController
public function uploadLog(string $key) { public function uploadLog(string $key) {
return success(data: (new WeappVersionService())->getUploadLog($key)); return success(data: (new WeappVersionService())->getUploadLog($key));
} }
/**
* 直接获取小程序上传日志(不更新状态)
* @param string $key
* @return Response
*/
public function getUploadLogOnly(string $key) {
return success(data: (new WeappVersionService())->getUploadLogOnly($key));
}
} }

View File

@ -61,6 +61,10 @@ Route::group('niucloud', function() {
Route::post('build/set_local_url', 'niucloud.Cloud/setLocalCloudCompileConfig'); Route::post('build/set_local_url', 'niucloud.Cloud/setLocalCloudCompileConfig');
//获取本地服务器地址 //获取本地服务器地址
Route::get('build/get_local_url', 'niucloud.Cloud/getLocalCloudCompileConfig'); Route::get('build/get_local_url', 'niucloud.Cloud/getLocalCloudCompileConfig');
//启动后台下载SSE编译完成后
Route::post('build/start_server_download', 'niucloud.Cloud/startServerDownload');
//获取后台下载进度
Route::get('build/get_sse_build_log', 'niucloud.Cloud/getSseBuildLog');
})->middleware([ })->middleware([
AdminCheckToken::class, AdminCheckToken::class,
AdminCheckRole::class, AdminCheckRole::class,

View File

@ -44,8 +44,10 @@ Route::group('weapp', function() {
Route::get('version', 'weapp.Version/lists'); Route::get('version', 'weapp.Version/lists');
//获取预览码 //获取预览码
Route::get('preview', 'weapp.Version/preview'); Route::get('preview', 'weapp.Version/preview');
//获取小程序上传日志 //获取小程序上传日志(会更新状态)
Route::get('upload/:key', 'weapp.Version/uploadLog'); Route::get('upload/:key', 'weapp.Version/uploadLog');
//直接获取小程序上传日志(不更新状态)
Route::get('upload_log/:key', 'weapp.Version/getUploadLogOnly');

View File

@ -1020,20 +1020,6 @@ return [
] ]
] ]
], ],
[
'menu_name' => '云编译',
'menu_key' => 'cloud_compile',
'menu_short_name' => '云编译',
'menu_type' => '1',
'icon' => 'iconfont iconyunbianyi',
'api_url' => '',
'router_path' => 'tools/cloud_compile',
'view_path' => 'tools/cloud_compile',
'methods' => 'post',
'sort' => '0',
'status' => '1',
'is_show' => '1',
],
[ [
'menu_name' => '应用开发', 'menu_name' => '应用开发',
'menu_key' => 'tool', 'menu_key' => 'tool',
@ -1659,6 +1645,20 @@ return [
], ],
] ]
], ],
[
'menu_name' => '云编译',
'menu_key' => 'cloud_compile',
'menu_short_name' => '云编译',
'menu_type' => '1',
'icon' => 'iconfont iconyunbianyi',
'api_url' => '',
'router_path' => 'tools/cloud_compile',
'view_path' => 'tools/cloud_compile',
'methods' => 'post',
'sort' => '98',
'status' => '1',
'is_show' => '1',
],
[ [
'menu_name' => '授权信息', 'menu_name' => '授权信息',
'menu_key' => 'app_auth', 'menu_key' => 'app_auth',
@ -1669,7 +1669,7 @@ return [
'router_path' => 'tools/authorize', 'router_path' => 'tools/authorize',
'view_path' => 'app/authorize', 'view_path' => 'app/authorize',
'methods' => '', 'methods' => '',
'sort' => '98', 'sort' => '97',
'status' => '1', 'status' => '1',
'is_show' => '1', 'is_show' => '1',
'children' => [ 'children' => [

View File

@ -121,6 +121,11 @@ class AuthService extends BaseAdminService
$site_address = $authinfo['site_address'] ?? ''; $site_address = $authinfo['site_address'] ?? '';
$domain = request()->domain(); $domain = request()->domain();
// 如果是站点域名不进行验证
$site_id = (new CoreSiteService())->getSiteIdByDomain($domain);
if (!empty($site_id)) return;
if (!empty($site_address) && strpos($domain, $site_address) !== false) return; if (!empty($site_address) && strpos($domain, $site_address) !== false) return;
throw new CommonException("授权域名校验失败!请确保当前访问域名与授权码绑定的域名一致"); throw new CommonException("授权域名校验失败!请确保当前访问域名与授权码绑定的域名一致");

View File

@ -96,10 +96,9 @@ class NiucloudService extends BaseAdminService
* @return \app\model\sys\SysConfig|bool|\think\Model * @return \app\model\sys\SysConfig|bool|\think\Model
*/ */
public function setLocalCloudCompileConfig($data){ public function setLocalCloudCompileConfig($data){
$data = [ $data = [
'baseUri' => $data['url'], 'baseUri' => $data['url'],
'isOpen' => 1, 'isOpen' => $data['is_open'],
]; ];
return $this->core_config_service->setConfig(0,'LOCAL_CLOUD_COMPILE_CONFIG', $data); return $this->core_config_service->setConfig(0,'LOCAL_CLOUD_COMPILE_CONFIG', $data);
} }
@ -113,7 +112,7 @@ class NiucloudService extends BaseAdminService
$config = $this->core_config_service->getConfig(0,'LOCAL_CLOUD_COMPILE_CONFIG')['value'] ?? []; $config = $this->core_config_service->getConfig(0,'LOCAL_CLOUD_COMPILE_CONFIG')['value'] ?? [];
return [ return [
'baseUri' => $config['baseUri'] ?? '', 'baseUri' => $config['baseUri'] ?? '',
'isOpen' => 1, 'isOpen' => $config['isOpen'] ?? 0,
]; ];
} }

View File

@ -66,10 +66,9 @@ class UpgradeService extends BaseAdminService
'backupSql' => [ 'step' => 'backupSql', 'title' => '' ], 'backupSql' => [ 'step' => 'backupSql', 'title' => '' ],
'coverCode' => [ 'step' => 'coverCode', 'title' => '合并更新文件' ], 'coverCode' => [ 'step' => 'coverCode', 'title' => '合并更新文件' ],
'handleUniapp' => [ 'step' => 'handleUniapp', 'title' => '处理uniapp' ], 'handleUniapp' => [ 'step' => 'handleUniapp', 'title' => '处理uniapp' ],
'installDepend' => ['step' => 'installDepend', 'title' => '合并依赖'],
'refreshMenu' => [ 'step' => 'refreshMenu', 'title' => '刷新菜单' ], 'refreshMenu' => [ 'step' => 'refreshMenu', 'title' => '刷新菜单' ],
'installSchedule' => [ 'step' => 'installSchedule', 'title' => '安装计划任务' ], 'installSchedule' => [ 'step' => 'installSchedule', 'title' => '安装计划任务' ],
'cloudBuild' => [ 'step' => 'cloudBuild', 'title' => '开始云编译' ],
'gteCloudBuildLog' => [ 'step' => 'gteCloudBuildLog', 'title' => '' ],
'upgradeComplete' => [ 'step' => 'upgradeComplete', 'title' => '升级完成' ] 'upgradeComplete' => [ 'step' => 'upgradeComplete', 'title' => '升级完成' ]
]; ];
@ -211,21 +210,6 @@ class UpgradeService extends BaseAdminService
$upgrade[ 'app_key' ] = $upgrade_content['content'][0]['app']['app_key']; $upgrade[ 'app_key' ] = $upgrade_content['content'][0]['app']['app_key'];
$upgrade[ 'version' ] = $upgrade_content['content'][0]['version']; $upgrade[ 'version' ] = $upgrade_content['content'][0]['version'];
// if (!$addon) {
// $upgrade[ 'app_key' ] = AddonDict::FRAMEWORK_KEY;
// $upgrade[ 'version' ] = config('version.version');
// } else {
// $upgrade[ 'app_key' ] = $addon;
// $upgrade[ 'version' ] = ( new Addon() )->where([ [ 'key', '=', $addon ] ])->value('version');
// $upgrade_title = ( new Addon() )->where([ [ 'key', '=', $addon ] ])->value('title');
//
// // 判断框架版本是否低于插件支持版本
// $last_version = $upgrade_content[ 'version_list' ][ count($upgrade_content[ 'version_list' ]) - 1 ];
// if (str_replace('.', '', config('version.version')) < str_replace('.', '', $last_version[ 'niucloud_version' ][ 'version_no' ])) {
// throw new CommonException('BEFORE_UPGRADING_NEED_UPGRADE_FRAMEWORK');
// }
// }
$response = ( new CoreAddonCloudService() )->upgradeAddon($upgrade); $response = ( new CoreAddonCloudService() )->upgradeAddon($upgrade);
if (isset($response[ 'code' ]) && $response[ 'code' ] == 0) throw new CommonException($response[ 'msg' ]); if (isset($response[ 'code' ]) && $response[ 'code' ] == 0) throw new CommonException($response[ 'msg' ]);
@ -243,16 +227,6 @@ class UpgradeService extends BaseAdminService
unset($this->steps['backupSql']); unset($this->steps['backupSql']);
} }
// 是否需要云编译
$is_need_cloudbuild = $data['is_need_cloudbuild'] ?? true;
if (!$is_need_cloudbuild) {
unset($this->steps['cloudBuild']);
unset($this->steps['gteCloudBuildLog']);
} else {
// 校验云编译服务
(new CloudService())->checkLocal();
}
try { try {
$upgrade_task = [ $upgrade_task = [
'key' => $key, 'key' => $key,
@ -464,14 +438,12 @@ class UpgradeService extends BaseAdminService
@unlink($to_dir . $file); @unlink($to_dir . $file);
} }
} }
// 合并依赖
$this->installDepend($code_dir . $version_no, array_column($change, 2));
} }
// 覆盖文件 // 覆盖文件
if (is_dir($code_dir . $version_no)) { if (is_dir($code_dir . $version_no)) {
// 忽略环境变量文件 // 忽略环境变量文件
$exclude_files = [ '.env.development', '.env.production', '.env', '.env.dev', '.env.product', 'favicon.ico', 'niucloud.ico' ]; $exclude_files = [ '.env.development', '.env.production', '.env', '.env.dev', '.env.product', 'favicon.ico', 'niucloud.ico', 'index.json' ];
dir_copy($code_dir . $version_no, $to_dir, exclude_files: $exclude_files); dir_copy($code_dir . $version_no, $to_dir, exclude_files: $exclude_files);
if ($addon != AddonDict::FRAMEWORK_KEY) { if ($addon != AddonDict::FRAMEWORK_KEY) {
( new CoreAddonInstallService($addon) )->installDir(); ( new CoreAddonInstallService($addon) )->installDir();
@ -514,58 +486,54 @@ class UpgradeService extends BaseAdminService
* @param string $version_no * @param string $version_no
* @return void * @return void
*/ */
public function installDepend(string $dir, array $change_files) public function installDepend()
{ {
$addon = $this->upgrade_task[ 'upgrade' ][ 'app_key' ];
$depend_service = new CoreDependService(); $depend_service = new CoreDependService();
$addon_list = ( new CoreAddonService() )->getInstallAddonList();
if ($addon == AddonDict::FRAMEWORK_KEY) { $composer = 'niucloud'. DIRECTORY_SEPARATOR .'composer.json';
$composer = '/niucloud/composer.json'; $admin_package = 'admin'. DIRECTORY_SEPARATOR .'package.json';
$admin_package = '/admin/package.json'; $web_package = 'web'. DIRECTORY_SEPARATOR .'package.json';
$web_package = '/web/package.json'; $uniapp_package = 'uni-app'. DIRECTORY_SEPARATOR .'package.json';
$uniapp_package = '/uni-app/package.json';
} else {
$composer = "/niucloud/addon/{$addon}/package/composer.json";
$admin_package = "/niucloud/addon/{$addon}/package/admin-package.json";
$web_package = "/niucloud/addon/{$addon}/package/web-package.json";
$uniapp_package = "/niucloud/addon/{$addon}/package/uni-app-package.json";
}
if (in_array($composer, $change_files)) { $composer_original = $depend_service->getComposerContent();
$original = $depend_service->getComposerContent(); $admin_original = $depend_service->getNpmContent('admin');
$new = $depend_service->jsonFileToArray($dir . $composer); $web_original = $depend_service->getNpmContent('web');
foreach ($new as $name => $value) { $uni_app_original = $depend_service->getNpmContent('uni-app');
$original[ $name ] = isset($original[ $name ]) && is_array($original[ $name ]) ? array_map('unserialize', array_unique(array_map('serialize', array_merge($original[ $name ], $new[ $name ])))) : $new[ $name ];
foreach ($addon_list as $addon => $item) {
$new = $depend_service->jsonFileToArray($this->geAddonPackagePath($addon) . 'composer.json');
if (!empty($new)) {
foreach ($new as $name => $value) {
$composer_original[ $name ] = isset($composer_original[ $name ]) && is_array($composer_original[ $name ]) ? array_map('unserialize', array_unique(array_map('serialize', array_merge($composer_original[ $name ], $new[ $name ])))) : $new[ $name ];
}
} }
$depend_service->writeArrayToJsonFile($original, $dir . $composer);
}
if (in_array($admin_package, $change_files)) {
$original = $depend_service->getNpmContent('admin');
$new = $depend_service->jsonFileToArray($dir . $admin_package);
foreach ($new as $name => $value) { $new = $depend_service->jsonFileToArray($this->geAddonPackagePath($addon) . 'admin-package.json');
$original[ $name ] = isset($original[ $name ]) && is_array($original[ $name ]) ? array_merge($original[ $name ], $new[ $name ]) : $new[ $name ]; if (!empty($new)) {
foreach ($new as $name => $value) {
$admin_original[ $name ] = isset($admin_original[ $name ]) && is_array($admin_original[ $name ]) ? array_merge($admin_original[ $name ], $new[ $name ]) : $new[ $name ];
}
} }
$depend_service->writeArrayToJsonFile($original, $dir . $admin_package);
}
if (in_array($web_package, $change_files)) {
$original = $depend_service->getNpmContent('web');
$new = $depend_service->jsonFileToArray($dir . $web_package);
foreach ($new as $name => $value) { $new = $depend_service->jsonFileToArray($this->geAddonPackagePath($addon) . 'web-package.json');
$original[ $name ] = isset($original[ $name ]) && is_array($original[ $name ]) ? array_merge($original[ $name ], $new[ $name ]) : $new[ $name ]; if (!empty($new)) {
foreach ($new as $name => $value) {
$web_original[ $name ] = isset($web_original[ $name ]) && is_array($web_original[ $name ]) ? array_merge($web_original[ $name ], $new[ $name ]) : $new[ $name ];
}
} }
$depend_service->writeArrayToJsonFile($original, $dir . $web_package);
}
if (in_array($uniapp_package, $change_files)) {
$original = $depend_service->getNpmContent('uni-app');
$new = $depend_service->jsonFileToArray($dir . $uniapp_package);
foreach ($new as $name => $value) { $new = $depend_service->jsonFileToArray($this->geAddonPackagePath($addon) . 'uni-app-package.json');
$original[ $name ] = isset($original[ $name ]) && is_array($original[ $name ]) ? array_merge($original[ $name ], $new[ $name ]) : $new[ $name ]; if (!empty($new)) {
foreach ($new as $name => $value) {
$uni_app_original[ $name ] = isset($uni_app_original[ $name ]) && is_array($uni_app_original[ $name ]) ? array_merge($uni_app_original[ $name ], $new[ $name ]) : $new[ $name ];
}
} }
$depend_service->writeArrayToJsonFile($original, $dir . $uniapp_package);
} }
$depend_service->writeArrayToJsonFile($composer_original, $this->root_path . $composer);
$depend_service->writeArrayToJsonFile($admin_original, $this->root_path . $admin_package);
$depend_service->writeArrayToJsonFile($web_original, $this->root_path . $web_package);
$depend_service->writeArrayToJsonFile($uni_app_original, $this->root_path . $uniapp_package);
} }
/** /**
@ -581,36 +549,17 @@ class UpgradeService extends BaseAdminService
dir_copy($code_dir . 'uni-app', $this->root_path . 'uni-app', exclude_files: $exclude_files); dir_copy($code_dir . 'uni-app', $this->root_path . 'uni-app', exclude_files: $exclude_files);
$addon_list = ( new CoreAddonService() )->getInstallAddonList(); $addon_list = ( new CoreAddonService() )->getInstallAddonList();
$depend_service = new CoreDependService();
if (!empty($addon_list)) { if (!empty($addon_list)) {
foreach ($addon_list as $addon => $item) { foreach ($addon_list as $addon => $item) {
$this->addon = $addon; $this->addon = $addon;
// 编译 diy-group 自定义组件代码文件
$this->compileDiyComponentsCode($this->root_path . 'uni-app' . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR, $addon);
// 编译 pages.json 页面路由代码文件 // 编译 pages.json 页面路由代码文件
$this->installPageCode($this->root_path . 'uni-app' . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR); $this->installPageCode($this->root_path . 'uni-app' . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR);
// 编译 加载插件标题语言包 // 编译 加载插件标题语言包
$this->compileLocale($this->root_path . 'uni-app' . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR, $addon); $this->compileLocale($this->root_path . 'uni-app' . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR, $addon);
// 合并插件依赖
$addon_uniapp_package = str_replace('/', DIRECTORY_SEPARATOR, project_path() . "niucloud/addon/{$addon}/package/uni-app-package.json");
if (file_exists($addon_uniapp_package)) {
$original = $depend_service->getNpmContent('uni-app');
$new = $depend_service->jsonFileToArray($addon_uniapp_package);
foreach ($new as $name => $value) {
$original[ $name ] = isset($original[ $name ]) && is_array($original[ $name ]) ? array_merge($original[ $name ], $new[ $name ]) : $new[ $name ];
}
$uniapp_package = $this->root_path . 'uni-app' . DIRECTORY_SEPARATOR . 'package.json';
$depend_service->writeArrayToJsonFile($original, $uniapp_package);
}
} }
} }

View File

@ -141,17 +141,30 @@ class WeappVersionService extends BaseAdminService
if (isset($build_log['data']) && isset($build_log['data'][0]) && is_array($build_log['data'][0])) { if (isset($build_log['data']) && isset($build_log['data'][0]) && is_array($build_log['data'][0])) {
$last = end($build_log['data'][0]); $last = end($build_log['data'][0]);
file_put_contents(runtime_path() . 'debug_log.txt', date('Y-m-d H:i:s') . " | key: $key | last_code: {$last['code']} | last_percent: {$last['percent']}\n", FILE_APPEND);
if ($last['code'] == 0) { if ($last['code'] == 0) {
(new WeappVersion())->update(['status' => CloudDict::APPLET_UPLOAD_FAIL, 'fail_reason' => $last['msg'] ?? '', 'update_time' => time()], ['task_key' => $key]); $res = (new WeappVersion())->where(['task_key' => $key])->update(['status' => CloudDict::APPLET_UPLOAD_FAIL, 'fail_reason' => $last['msg'] ?? '', 'update_time' => time()]);
file_put_contents(runtime_path() . 'debug_log.txt', date('Y-m-d H:i:s') . " | fail update result: $res\n", FILE_APPEND);
return $build_log; return $build_log;
} }
if ($last['percent'] == 100) { if ($last['percent'] == 100) {
(new WeappVersion())->update(['status' => CloudDict::APPLET_UPLOAD_SUCCESS, 'update_time' => time()], ['task_key' => $key]); $res = (new WeappVersion())->where(['task_key' => $key])->update(['status' => CloudDict::APPLET_UPLOAD_SUCCESS, 'update_time' => time()]);
file_put_contents(runtime_path() . 'debug_log.txt', date('Y-m-d H:i:s') . " | success update result: $res\n", FILE_APPEND);
} }
} }
return $build_log; return $build_log;
} }
/**
* 直接获取小程序上传日志(不更新状态)
* @param string $key
* @return null
*/
public function getUploadLogOnly(string $key)
{
return (new CoreWeappCloudService())->getWeappCompileLog($key);
}
/** /**
* 获取小程序上传日志 * 获取小程序上传日志
* @param string $key * @param string $key

View File

@ -23,6 +23,7 @@ use core\exception\AuthException;
use think\db\exception\DataNotFoundException; use think\db\exception\DataNotFoundException;
use think\db\exception\DbException; use think\db\exception\DbException;
use think\db\exception\ModelNotFoundException; use think\db\exception\ModelNotFoundException;
use think\facade\Log;
/** /**
* 登录服务层 * 登录服务层
@ -78,8 +79,14 @@ class RegisterService extends BaseApiService
} }
$member_id = ( new MemberService() )->add($data); $member_id = ( new MemberService() )->add($data);
$data[ 'member_id' ] = $member_id; $data[ 'member_id' ] = $member_id;
event('MemberRegister', $data);
SetMemberNoJob::dispatch([ 'site_id' => $this->site_id, 'member_id' => $member_id ]); SetMemberNoJob::dispatch([ 'site_id' => $this->site_id, 'member_id' => $member_id ]);
try {
event('MemberRegister', $data);
} catch (\Exception $e) {
Log::write('MemberRegister event error');
Log::write($e->getTrace());
}
} }
$member_info = $member_service->findMemberInfo([ 'member_id' => $member_id, 'site_id' => $this->site_id ]); $member_info = $member_service->findMemberInfo([ 'member_id' => $member_id, 'site_id' => $this->site_id ]);
if ($member_info->isEmpty()) throw new AuthException('MEMBER_NOT_EXIST');//账号不存在 if ($member_info->isEmpty()) throw new AuthException('MEMBER_NOT_EXIST');//账号不存在

View File

@ -17,6 +17,7 @@ use core\exception\CommonException;
use core\util\niucloud\BaseNiucloudClient; use core\util\niucloud\BaseNiucloudClient;
use core\util\niucloud\CloudService; use core\util\niucloud\CloudService;
use think\facade\Cache; use think\facade\Cache;
use think\facade\Log;
/** /**
*/ */
@ -86,7 +87,7 @@ class CoreAddonCloudService extends CoreCloudBaseService
'authorize_code' => $this->auth_code, 'authorize_code' => $this->auth_code,
'timestamp' => $install_task['timestamp'] 'timestamp' => $install_task['timestamp']
]; ];
$response = (new CloudService())->httpPost('cloud/build?' . http_build_query($query), [ $response = (new CloudService(true))->httpPost('cloud/build?' . http_build_query($query), [
'multipart' => [ 'multipart' => [
[ [
'name' => 'file', 'name' => 'file',
@ -122,11 +123,11 @@ class CoreAddonCloudService extends CoreCloudBaseService
'authorize_code' => $this->auth_code, 'authorize_code' => $this->auth_code,
'timestamp' => $install_task['timestamp'] 'timestamp' => $install_task['timestamp']
]; ];
$build_log = (new CloudService())->httpGet('cloud/get_build_logs?' . http_build_query($query)); $build_log = (new CloudService(true))->httpGet('cloud/get_build_logs?' . http_build_query($query));
if (isset($build_log['data']) && isset($build_log['data'][0]) && is_array($build_log['data'][0])) { if (isset($build_log['data']) && isset($build_log['data'][0]) && is_array($build_log['data'][0])) {
$last = end($build_log['data'][0]); $last = end($build_log['data'][0]);
if ($last['percent'] == 100 && $last['code'] == 0) { if ((int) $last['code'] == 0) {
(new CoreAddonInstallService($addon))->installExceptionHandle(); (new CoreAddonInstallService($addon))->installExceptionHandle();
$install_task['error'] = 'ADDON_INSTALL_FAIL'; $install_task['error'] = 'ADDON_INSTALL_FAIL';
Cache::set('install_task', $install_task, 10); Cache::set('install_task', $install_task, 10);
@ -162,8 +163,8 @@ class CoreAddonCloudService extends CoreCloudBaseService
$cache = Cache::get('build_success_' . $addon); $cache = Cache::get('build_success_' . $addon);
if (is_null($cache)) { if (is_null($cache) || !isset($cache[ 'index' ])) {
$response = (new CloudService())->request('HEAD','cloud/build_download?' . http_build_query($query), [ $response = (new CloudService(true))->request('HEAD','cloud/build_download?' . http_build_query($query), [
'headers' => ['Range' => 'bytes=0-'] 'headers' => ['Range' => 'bytes=0-']
]); ]);
$length = $response->getHeader('Content-range'); $length = $response->getHeader('Content-range');
@ -187,7 +188,7 @@ class CoreAddonCloudService extends CoreCloudBaseService
$end = ($cache['index'] + 1) * $chunk_size; $end = ($cache['index'] + 1) * $chunk_size;
$end = min($end, $cache['length']); $end = min($end, $cache['length']);
$response = (new CloudService())->request('GET','cloud/build_download?' . http_build_query($query), [ $response = (new CloudService(true))->request('GET','cloud/build_download?' . http_build_query($query), [
'headers' => ['Range' => "bytes={$start}-{$end}"] 'headers' => ['Range' => "bytes={$start}-{$end}"]
]); ]);
fwrite($zip_resource, $response->getBody()); fwrite($zip_resource, $response->getBody());
@ -225,10 +226,18 @@ class CoreAddonCloudService extends CoreCloudBaseService
Cache::set('build_success_' . $addon, null); Cache::set('build_success_' . $addon, null);
} else { } else {
Cache::set('build_success_' . $addon, null); if (!isset($cache[ 'retry' ])) {
// 调用插件安装异常处理 unlink($zip_file);
(new CoreAddonInstallService($addon))->installExceptionHandle(); $cache['retry'] = 1;
throw new CommonException('Zip decompression failed'); unset($cache['index']);
Cache::set('build_success_' . $addon, $cache);
$log[] = [ 'code' => 1, 'msg' => '编译包解压失败,尝试重新下载', 'action' => '编译包解压失败,尝试重新下载', 'percent' => '100' ];
} else {
Cache::set('build_success_' . $addon, null);
// 调用插件安装异常处理
(new CoreAddonInstallService($addon))->installExceptionHandle();
throw new CommonException('Zip decompression failed');
}
} }
} }
} }
@ -251,7 +260,7 @@ class CoreAddonCloudService extends CoreCloudBaseService
'token' => $action_token['data']['token'] ?? '' 'token' => $action_token['data']['token'] ?? ''
]; ];
// 获取文件大小 // 获取文件大小
$response = (new CloudService())->request('HEAD','cloud/download?' . http_build_query($query), [ $response = (new CloudService(false, 'http://oss.niucloud.com/'))->request('HEAD','cloud/download?' . http_build_query($query), [
'headers' => ['Range' => 'bytes=0-'] 'headers' => ['Range' => 'bytes=0-']
]); ]);
$length = $response->getHeader('Content-range'); $length = $response->getHeader('Content-range');
@ -263,7 +272,7 @@ class CoreAddonCloudService extends CoreCloudBaseService
$zip_file = $temp_dir . $addon . '.zip'; $zip_file = $temp_dir . $addon . '.zip';
$zip_resource = fopen($zip_file, 'w'); $zip_resource = fopen($zip_file, 'w');
$response = (new CloudService())->request('GET','cloud/download?' . http_build_query($query), [ $response = (new CloudService(false, 'http://oss.niucloud.com/'))->request('GET','cloud/download?' . http_build_query($query), [
'headers' => ['Range' => "bytes=0-{$length}"] 'headers' => ['Range' => "bytes=0-{$length}"]
]); ]);
fwrite($zip_resource, $response->getBody()); fwrite($zip_resource, $response->getBody());
@ -286,7 +295,7 @@ class CoreAddonCloudService extends CoreCloudBaseService
'token' => $action_token['data']['token'] ?? '' 'token' => $action_token['data']['token'] ?? ''
]; ];
// 获取文件大小 // 获取文件大小
$response = (new CloudService())->httpGet('cloud/upgrade?' . http_build_query($query)); $response = (new CloudService(false, 'http://oss.niucloud.com/'))->httpGet('cloud/upgrade?' . http_build_query($query));
$response['token'] = $query['token']; $response['token'] = $query['token'];
return $response; return $response;
} }
@ -309,7 +318,7 @@ class CoreAddonCloudService extends CoreCloudBaseService
$chunk_size = 1 * 1024 * 1024; $chunk_size = 1 * 1024 * 1024;
if ($index == -1) { if ($index == -1) {
$response = (new CloudService())->request('HEAD','cloud/upgrade/download?' . http_build_query($query), [ $response = (new CloudService(false, 'http://oss.niucloud.com/'))->request('HEAD','cloud/upgrade/download?' . http_build_query($query), [
'headers' => ['Range' => 'bytes=0-'] 'headers' => ['Range' => 'bytes=0-']
]); ]);
$length = $response->getHeader('Content-range'); $length = $response->getHeader('Content-range');
@ -327,7 +336,7 @@ class CoreAddonCloudService extends CoreCloudBaseService
$end = ($index + 1) * $chunk_size; $end = ($index + 1) * $chunk_size;
$end = min($end, $length); $end = min($end, $length);
$response = (new CloudService())->request('GET','cloud/upgrade/download?' . http_build_query($query), [ $response = (new CloudService(false, 'http://oss.niucloud.com/'))->request('GET','cloud/upgrade/download?' . http_build_query($query), [
'headers' => ['Range' => "bytes={$start}-{$end}"] 'headers' => ['Range' => "bytes={$start}-{$end}"]
]); ]);
fwrite($zip_resource, $response->getBody()); fwrite($zip_resource, $response->getBody());

View File

@ -15,6 +15,7 @@ use app\dict\sys\MenuDict;
use app\model\sys\SysMenu; use app\model\sys\SysMenu;
use core\base\BaseCoreService; use core\base\BaseCoreService;
use core\exception\AddonException; use core\exception\AddonException;
use core\exception\CommonException;
/** /**
* 插件开发服务层 * 插件开发服务层
@ -48,6 +49,9 @@ class CoreAddonDevelopBuildService extends BaseCoreService
$this->admin(); $this->admin();
$this->uniapp(); $this->uniapp();
$this->adminDist();
$this->wapDist();
$this->mpWeixinDist();
$this->buildUniappPagesJson(); $this->buildUniappPagesJson();
$this->buildUniappLangJson(); $this->buildUniappLangJson();
$this->web(); $this->web();
@ -199,6 +203,17 @@ class CoreAddonDevelopBuildService extends BaseCoreService
return true; return true;
} }
public function adminDist() {
$admin_path = str_replace('/', DIRECTORY_SEPARATOR, $this->root_path . 'admin/dist/assets/addons/' . $this->addon . '/');
if (!is_dir($admin_path) || !is_readable($admin_path)) throw new CommonException("请先在admin目录下执行 npm run build:addon {$this->addon}命令编译插件admin端");
if (count(scandir($admin_path)) === 2) throw new CommonException("请先在admin目录下执行 npm run build:addon {$this->addon}命令编译插件admin端");
$addon_admin_path = $this->addon_path . 'dist' . DIRECTORY_SEPARATOR . 'admin' . DIRECTORY_SEPARATOR;
if (is_dir($addon_admin_path)) del_target_dir($addon_admin_path, true);
dir_copy($admin_path, $addon_admin_path);
}
/** /**
* wap打包 * wap打包
* @return true * @return true
@ -215,6 +230,34 @@ class CoreAddonDevelopBuildService extends BaseCoreService
return true; return true;
} }
public function wapDist() {
$uniapp_path = $this->root_path . 'uni-app' . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'addon' . DIRECTORY_SEPARATOR . $this->addon . DIRECTORY_SEPARATOR;
if (!is_dir($uniapp_path)) return true;
$uniapp_path = str_replace('/', DIRECTORY_SEPARATOR, $this->root_path . 'uni-app/dist/build/h5/assets/addons/' . $this->addon . '/');
if (!is_dir($uniapp_path) || !is_readable($uniapp_path)) throw new CommonException("请先在uni-app目录下执行 npm run build:h5:addon {$this->addon}命令编译插件wap端");
if (count(scandir($uniapp_path)) === 2) throw new CommonException("请先在uni-app目录下执行 npm run build:h5:addon {$this->addon}命令编译插件wap端");
$addon_uniapp_path = $this->addon_path . 'dist' . DIRECTORY_SEPARATOR . 'wap' . DIRECTORY_SEPARATOR;
if (is_dir($addon_uniapp_path)) del_target_dir($addon_uniapp_path, true);
dir_copy($uniapp_path, $addon_uniapp_path);
}
public function mpWeixinDist() {
$uniapp_path = $this->root_path . 'uni-app' . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'addon' . DIRECTORY_SEPARATOR . $this->addon . DIRECTORY_SEPARATOR;
if (!is_dir($uniapp_path)) return true;
$uniapp_path = str_replace('/', DIRECTORY_SEPARATOR, $this->root_path . 'uni-app/dist/mp-weixin-addons/' . $this->addon . '/');
if (!is_dir($uniapp_path) || !is_readable($uniapp_path)) throw new CommonException("请先在uni-app目录下执行 npm run build:mp-weixin:addon {$this->addon}命令编译插件微信小程序");
if (count(scandir($uniapp_path)) === 2) throw new CommonException("请先在uni-app目录下执行 npm run build:mp-weixin:addon {$this->addon}命令编译插件微信小程序");
$addon_uniapp_path = $this->addon_path . 'dist' . DIRECTORY_SEPARATOR . 'mp-weixin' . DIRECTORY_SEPARATOR;
if (is_dir($addon_uniapp_path)) del_target_dir($addon_uniapp_path, true);
dir_copy($uniapp_path, $addon_uniapp_path);
}
public function buildUniappPagesJson() { public function buildUniappPagesJson() {
$pages_json = file_get_contents($this->root_path . 'uni-app' . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'pages.json'); $pages_json = file_get_contents($this->root_path . 'uni-app' . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'pages.json');
$code_begin = strtoupper($this->addon) . '_PAGE_BEGIN' . PHP_EOL; $code_begin = strtoupper($this->addon) . '_PAGE_BEGIN' . PHP_EOL;

View File

@ -226,16 +226,6 @@ class CoreAddonInstallService extends CoreAddonBaseService
$this->backupFrontend(); $this->backupFrontend();
$tips = []; $tips = [];
if ($mode != 'cloud') {
$tips[] = get_lang('dict_addon.install_after_update');
} else {
try {
(new CloudService())->checkLocal();
} catch (\Exception $e) {
Cache::set('install_task', null);
throw new CommonException($e->getMessage(), $e->getCode());
}
}
foreach ($this->addon_list as $addon) { foreach ($this->addon_list as $addon) {
$this->install_task['addon'] = $addon; $this->install_task['addon'] = $addon;
@ -287,6 +277,8 @@ class CoreAddonInstallService extends CoreAddonBaseService
} }
$this->installWap(); $this->installWap();
$this->rebuildPublicAdminAddonIndexJson();
$this->rebuildPublicWapAddonIndexJson();
if ($mode == 'cloud') { if ($mode == 'cloud') {
$this->install_task['tips'] = $tips; $this->install_task['tips'] = $tips;
@ -343,15 +335,19 @@ class CoreAddonInstallService extends CoreAddonBaseService
$from_web_dir = $this->install_addon_path . 'web' . DIRECTORY_SEPARATOR; $from_web_dir = $this->install_addon_path . 'web' . DIRECTORY_SEPARATOR;
$from_wap_dir = $this->install_addon_path . 'uni-app' . DIRECTORY_SEPARATOR; $from_wap_dir = $this->install_addon_path . 'uni-app' . DIRECTORY_SEPARATOR;
$from_resource_dir = $this->install_addon_path . 'resource' . DIRECTORY_SEPARATOR; $from_resource_dir = $this->install_addon_path . 'resource' . DIRECTORY_SEPARATOR;
$from_wap_dist_dir = $this->install_addon_path . 'dist' . DIRECTORY_SEPARATOR . 'wap' . DIRECTORY_SEPARATOR;
$from_admin_dist_dir = $this->install_addon_path . 'dist' . DIRECTORY_SEPARATOR . 'admin' . DIRECTORY_SEPARATOR;
// 放入的文件 // 放入的文件
$to_admin_dir = $this->root_path . 'admin' . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'addon' . DIRECTORY_SEPARATOR . $this->addon . DIRECTORY_SEPARATOR; $to_admin_dir = $this->root_path . 'admin' . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'addon' . DIRECTORY_SEPARATOR . $this->addon . DIRECTORY_SEPARATOR;
$to_web_dir = $this->root_path . 'web' . DIRECTORY_SEPARATOR . 'addon' . DIRECTORY_SEPARATOR . $this->addon . DIRECTORY_SEPARATOR; $to_web_dir = $this->root_path . 'web' . DIRECTORY_SEPARATOR . 'addon' . DIRECTORY_SEPARATOR . $this->addon . DIRECTORY_SEPARATOR;
$to_wap_dir = $this->root_path . 'uni-app' . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'addon' . DIRECTORY_SEPARATOR . $this->addon . DIRECTORY_SEPARATOR; $to_wap_dir = $this->root_path . 'uni-app' . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'addon' . DIRECTORY_SEPARATOR . $this->addon . DIRECTORY_SEPARATOR;
$to_resource_dir = public_path() . 'addon' . DIRECTORY_SEPARATOR . $this->addon . DIRECTORY_SEPARATOR; $to_resource_dir = public_path() . 'addon' . DIRECTORY_SEPARATOR . $this->addon . DIRECTORY_SEPARATOR;
$to_wap_dist_dir = public_path() . 'wap' . DIRECTORY_SEPARATOR . 'assets' . DIRECTORY_SEPARATOR . 'addons' . DIRECTORY_SEPARATOR . $this->addon . DIRECTORY_SEPARATOR;
$to_admin_dist_dir = public_path() . 'admin' . DIRECTORY_SEPARATOR . 'assets' . DIRECTORY_SEPARATOR . 'addons' . DIRECTORY_SEPARATOR . $this->addon . DIRECTORY_SEPARATOR;
// 安装admin管理端 // 安装admin管理端
if (file_exists($from_admin_dir)) { if (is_dir($from_admin_dir)) {
dir_copy($from_admin_dir, $to_admin_dir, $this->files['admin'], exclude_dirs:['icon']); dir_copy($from_admin_dir, $to_admin_dir, $this->files['admin'], exclude_dirs:['icon']);
// 判断图标目录是否存在 // 判断图标目录是否存在
if (is_dir($from_admin_dir . 'icon')) { if (is_dir($from_admin_dir . 'icon')) {
@ -363,7 +359,7 @@ class CoreAddonInstallService extends CoreAddonBaseService
} }
// 安装电脑端 // 安装电脑端
if (file_exists($from_web_dir)) { if (is_dir($from_web_dir)) {
// 安装布局文件 // 安装布局文件
$layout = $from_web_dir . 'layouts'; $layout = $from_web_dir . 'layouts';
if (is_dir($layout)) { if (is_dir($layout)) {
@ -374,15 +370,23 @@ class CoreAddonInstallService extends CoreAddonBaseService
} }
// 安装手机端 // 安装手机端
if (file_exists($from_wap_dir)) { if (is_dir($from_wap_dir)) {
dir_copy($from_wap_dir, $to_wap_dir, $this->files['wap']); dir_copy($from_wap_dir, $to_wap_dir, $this->files['wap']);
} }
//安装资源文件 //安装资源文件
if (file_exists($from_resource_dir)) { if (is_dir($from_resource_dir)) {
dir_copy($from_resource_dir, $to_resource_dir, $this->files['resource']); dir_copy($from_resource_dir, $to_resource_dir, $this->files['resource']);
} }
if (is_dir($from_admin_dist_dir)) {
dir_copy($from_admin_dist_dir, $to_admin_dist_dir);
}
if (is_dir($from_wap_dist_dir)) {
dir_copy($from_wap_dist_dir, $to_wap_dist_dir);
}
return true; return true;
} }
@ -681,6 +685,8 @@ class CoreAddonInstallService extends CoreAddonBaseService
$to_web_layouts = $this->root_path . 'web' . DIRECTORY_SEPARATOR . 'layouts' . DIRECTORY_SEPARATOR . $this->addon . DIRECTORY_SEPARATOR; $to_web_layouts = $this->root_path . 'web' . DIRECTORY_SEPARATOR . 'layouts' . DIRECTORY_SEPARATOR . $this->addon . DIRECTORY_SEPARATOR;
$to_wap_dir = $this->root_path . 'uni-app' . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'addon' . DIRECTORY_SEPARATOR . $this->addon . DIRECTORY_SEPARATOR; $to_wap_dir = $this->root_path . 'uni-app' . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'addon' . DIRECTORY_SEPARATOR . $this->addon . DIRECTORY_SEPARATOR;
$to_resource_dir = public_path() . 'addon' . DIRECTORY_SEPARATOR . $this->addon . DIRECTORY_SEPARATOR; $to_resource_dir = public_path() . 'addon' . DIRECTORY_SEPARATOR . $this->addon . DIRECTORY_SEPARATOR;
$to_wap_dist_dir = public_path() . 'wap' . DIRECTORY_SEPARATOR . 'assets' . DIRECTORY_SEPARATOR . 'addons' . $this->addon . DIRECTORY_SEPARATOR;
$to_admin_dist_dir = public_path() . 'admin' . DIRECTORY_SEPARATOR . 'assets' . DIRECTORY_SEPARATOR . 'addons' . $this->addon . DIRECTORY_SEPARATOR;
// 卸载admin管理端 // 卸载admin管理端
if (is_dir($to_admin_dir)) del_target_dir($to_admin_dir, true); if (is_dir($to_admin_dir)) del_target_dir($to_admin_dir, true);
@ -701,6 +707,9 @@ class CoreAddonInstallService extends CoreAddonBaseService
//删除资源文件 //删除资源文件
if (is_dir($to_resource_dir)) del_target_dir($to_resource_dir, true); if (is_dir($to_resource_dir)) del_target_dir($to_resource_dir, true);
if (is_dir($to_wap_dist_dir)) del_target_dir($to_wap_dist_dir, true);
if (is_dir($to_admin_dist_dir)) del_target_dir($to_admin_dist_dir, true);
//todo 卸载插件目录涉及到的空文件 //todo 卸载插件目录涉及到的空文件
return true; return true;
} }
@ -734,9 +743,6 @@ class CoreAddonInstallService extends CoreAddonBaseService
*/ */
public function uninstallWap() public function uninstallWap()
{ {
// 编译 diy-group 自定义组件代码文件
$this->compileDiyComponentsCode($this->root_path . 'uni-app' . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR, $this->addon);
// 编译 pages.json 页面路由代码文件 // 编译 pages.json 页面路由代码文件
$this->uninstallPageCode($this->root_path . 'uni-app' . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR); $this->uninstallPageCode($this->root_path . 'uni-app' . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR);
@ -762,16 +768,65 @@ class CoreAddonInstallService extends CoreAddonBaseService
*/ */
public function installWap() public function installWap()
{ {
// 编译 diy-group 自定义组件代码文件
$this->compileDiyComponentsCode($this->root_path . 'uni-app' . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR, $this->addon_list);
// 编译 pages.json 页面路由代码文件 // 编译 pages.json 页面路由代码文件
$this->installPageCode($this->root_path . 'uni-app' . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR, $this->addon_list); $this->installPageCode($this->root_path . 'uni-app' . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR, $this->addon_list);
// 编译 加载插件标题语言包 // 编译 加载插件标题语言包
$this->compileLocale($this->root_path . 'uni-app' . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR, $this->addon_list); $this->compileLocale($this->root_path . 'uni-app' . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR, $this->addon_list);
}
/**
* 重新构建 public/admin/assets/addons/index.json
* 读取 addons 目录下的所有插件子目录,重新配置 keys
* @return true
*/
public function rebuildPublicAdminAddonIndexJson() {
$addons_dir = public_path() . 'admin' . DIRECTORY_SEPARATOR . 'assets' . DIRECTORY_SEPARATOR . 'addons';
$index_file = $addons_dir . DIRECTORY_SEPARATOR . 'index.json';
// 读取已有的 index.json,保留 sharedVersion 等字段
$index_data = $this->jsonFileToArray($index_file);
// 扫描 addons 目录下的所有子目录作为插件 keys
$keys = [];
if (is_dir($addons_dir)) {
$keys = get_files_by_dir($addons_dir);
}
// 更新 keys
$index_data['keys'] = $keys;
// 写入文件
$this->writeArrayToJsonFile($index_data, $index_file);
return true;
}
/**
* 重新构建 public/wap/assets/addons/index.json
* 读取 addons 目录下的所有插件子目录,重新配置 keys
* @return true
*/
public function rebuildPublicWapAddonIndexJson() {
$addons_dir = public_path() . 'wap' . DIRECTORY_SEPARATOR . 'assets' . DIRECTORY_SEPARATOR . 'addons';
$index_file = $addons_dir . DIRECTORY_SEPARATOR . 'index.json';
// 读取已有的 index.json,保留 sharedVersion 等字段
$index_data = $this->jsonFileToArray($index_file);
// 扫描 addons 目录下的所有子目录作为插件 keys
$keys = [];
if (is_dir($addons_dir)) {
$keys = get_files_by_dir($addons_dir);
}
// 更新 keys
$index_data['keys'] = $keys;
// 写入文件
$this->writeArrayToJsonFile($index_data, $index_file);
return true;
} }
public function download() public function download()

Some files were not shown because too many files have changed in this diff Show More