mirror of
https://gitee.com/niucloud-team/niucloud.git
synced 2026-06-26 15:52:06 +00:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5b7392b6e | ||
|
|
3e71008192 | ||
|
|
67e7669694 | ||
|
|
9f683839e6 | ||
|
|
44805a4a1f | ||
|
|
0864a7a502 | ||
|
|
396eb8406d | ||
|
|
20db81d87e | ||
|
|
4309d7c458 | ||
|
|
9ed68ca110 | ||
|
|
b4ebe684b2 | ||
|
|
c721282ffa | ||
|
|
da84a1087c | ||
|
|
65d9a38693 | ||
|
|
330ffa3efc |
4
admin/.gitignore
vendored
4
admin/.gitignore
vendored
@ -22,3 +22,7 @@ dist-ssr
|
|||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
.build
|
||||||
|
build-report.json
|
||||||
|
auto-imports.d.ts
|
||||||
|
components.d.ts
|
||||||
|
|||||||
@ -1,143 +0,0 @@
|
|||||||
# NiuCloud Admin 开发规范
|
|
||||||
|
|
||||||
## 技术栈区分
|
|
||||||
|
|
||||||
本项目采用前后端分离架构,包含两个主要前端部分:
|
|
||||||
|
|
||||||
1. **PC端后台管理系统**
|
|
||||||
- 框架: Vue 3 + TypeScript + Vite
|
|
||||||
- UI组件库: Element Plus
|
|
||||||
- 状态管理: Pinia
|
|
||||||
- 样式处理: SCSS + Tailwind CSS
|
|
||||||
|
|
||||||
2. **移动端应用**
|
|
||||||
- 框架: uni-app + Vue 3 + TypeScript
|
|
||||||
- UI组件库: uview-plus
|
|
||||||
- 状态管理: Pinia
|
|
||||||
- 样式处理: SCSS + Windi CSS
|
|
||||||
|
|
||||||
## 关键组件使用规范
|
|
||||||
|
|
||||||
### 消息提示组件
|
|
||||||
|
|
||||||
**重要注意事项:请根据开发平台选择正确的消息提示组件!**
|
|
||||||
|
|
||||||
#### PC端 (admin目录)
|
|
||||||
- **必须使用Element Plus的消息提示组件**,而不是uni-app的方法
|
|
||||||
- 主要组件包括:`ElMessage`、`ElMessageBox`、`ElNotification`等
|
|
||||||
- 导入方式:`import { ElMessage, ElMessageBox } from 'element-plus'`
|
|
||||||
- 使用示例:
|
|
||||||
```typescript
|
|
||||||
import { ElMessage } from 'element-plus'
|
|
||||||
|
|
||||||
// 成功消息
|
|
||||||
ElMessage.success('操作成功')
|
|
||||||
|
|
||||||
// 错误消息
|
|
||||||
ElMessage.error('操作失败')
|
|
||||||
|
|
||||||
// 确认对话框
|
|
||||||
ElMessageBox.confirm('确定要执行此操作吗?', '提示', {
|
|
||||||
confirmButtonText: '确定',
|
|
||||||
cancelButtonText: '取消',
|
|
||||||
type: 'warning'
|
|
||||||
}).then(() => {
|
|
||||||
// 用户点击确认后的逻辑
|
|
||||||
}).catch(() => {
|
|
||||||
// 用户点击取消后的逻辑
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 移动端 (uni-app目录)
|
|
||||||
- **使用uni-app提供的API**进行消息提示
|
|
||||||
- 主要方法包括:`uni.showToast`、`uni.showModal`、`uni.showLoading`等
|
|
||||||
- 使用示例:
|
|
||||||
```typescript
|
|
||||||
// 成功提示
|
|
||||||
uni.showToast({
|
|
||||||
title: '操作成功',
|
|
||||||
icon: 'success',
|
|
||||||
duration: 2000
|
|
||||||
})
|
|
||||||
|
|
||||||
// 模态对话框
|
|
||||||
uni.showModal({
|
|
||||||
title: '提示',
|
|
||||||
content: '确定要执行此操作吗?',
|
|
||||||
success: (res) => {
|
|
||||||
if (res.confirm) {
|
|
||||||
// 用户点击确认后的逻辑
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
## API请求规范
|
|
||||||
|
|
||||||
### PC端API请求
|
|
||||||
- 使用`@/utils/request.ts`封装的请求工具
|
|
||||||
- 支持`showSuccessMessage`和`showErrorMessage`选项控制消息显示
|
|
||||||
- 示例:
|
|
||||||
```typescript
|
|
||||||
import request from '@/utils/request'
|
|
||||||
|
|
||||||
// GET请求
|
|
||||||
export function getOrderList(params: Record<string, any>) {
|
|
||||||
return request.get('order/list', params)
|
|
||||||
}
|
|
||||||
|
|
||||||
// POST请求(带成功消息)
|
|
||||||
export function createOrder(params: Record<string, any>) {
|
|
||||||
return request.post('order/create', params, { showSuccessMessage: true })
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 移动端API请求
|
|
||||||
- 使用uni-app的`uni.request`或封装的请求工具
|
|
||||||
- 示例:
|
|
||||||
```typescript
|
|
||||||
// 发送请求
|
|
||||||
uni.request({
|
|
||||||
url: 'https://example.com/api/order/list',
|
|
||||||
method: 'GET',
|
|
||||||
data: {
|
|
||||||
page: 1,
|
|
||||||
limit: 10
|
|
||||||
},
|
|
||||||
success: (res) => {
|
|
||||||
// 处理成功响应
|
|
||||||
},
|
|
||||||
fail: (err) => {
|
|
||||||
// 处理请求失败
|
|
||||||
}
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
## 代码风格规范
|
|
||||||
|
|
||||||
1. **文件命名**
|
|
||||||
- 组件文件:PascalCase,如 `OrderList.vue`
|
|
||||||
- 普通文件:kebab-case 或 camelCase,如 `api-service.ts` 或 `commonUtils.ts`
|
|
||||||
|
|
||||||
2. **TypeScript规范**
|
|
||||||
- 为函数参数、返回值和重要变量添加明确的类型注解
|
|
||||||
- 使用接口 (interface) 定义复杂数据结构
|
|
||||||
- 避免 `any` 类型的滥用
|
|
||||||
|
|
||||||
3. **Vue组件规范**
|
|
||||||
- 使用 Vue 3 Composition API 和 `<script setup lang="ts">` 语法
|
|
||||||
- 组件样式建议使用 scoped 属性或 CSS Modules
|
|
||||||
|
|
||||||
## 国际化规范
|
|
||||||
|
|
||||||
- PC端使用Vue I18n进行国际化,语言文件位于`src/lang`目录
|
|
||||||
- 移动端同样使用Vue I18n,语言文件位于`src/app/locale`目录
|
|
||||||
- 使用`t('key')`函数获取翻译文本
|
|
||||||
|
|
||||||
## 其他重要规范
|
|
||||||
|
|
||||||
- 严格遵循RESTful API设计规范
|
|
||||||
- 统一处理API响应数据和错误情况
|
|
||||||
- 代码提交前确保通过TypeScript类型检查
|
|
||||||
- 组件开发遵循高内聚低耦合原则
|
|
||||||
- 优先复用现有组件和工具函数
|
|
||||||
4
admin/auto-imports.d.ts
vendored
4
admin/auto-imports.d.ts
vendored
@ -1,7 +1,5 @@
|
|||||||
// Generated by 'unplugin-auto-import'
|
// Generated by 'unplugin-auto-import'
|
||||||
export {}
|
export {}
|
||||||
declare global {
|
declare global {
|
||||||
const ElMessage: typeof import('element-plus/es')['ElMessage']
|
|
||||||
const ElMessageBox: typeof import('element-plus/es')['ElMessageBox']
|
|
||||||
const ElNotification: typeof import('element-plus/es')['ElNotification']
|
|
||||||
}
|
}
|
||||||
|
|||||||
46
admin/components.d.ts
vendored
46
admin/components.d.ts
vendored
@ -11,80 +11,40 @@ 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']
|
||||||
ElCard: typeof import('element-plus/es')['ElCard']
|
ElCard: typeof import('element-plus/es')['ElCard']
|
||||||
ElCarousel: typeof import('element-plus/es')['ElCarousel']
|
ElCascader: typeof import('element-plus/es')['ElCascader']
|
||||||
ElCarouselItem: typeof import('element-plus/es')['ElCarouselItem']
|
|
||||||
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']
|
||||||
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']
|
||||||
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']
|
||||||
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']
|
ElTimeSelect: typeof import('element-plus/es')['ElTimeSelect']
|
||||||
ElTimeline: typeof import('element-plus/es')['ElTimeline']
|
|
||||||
ElTimelineItem: typeof import('element-plus/es')['ElTimelineItem']
|
|
||||||
ElTimePicker: typeof import('element-plus/es')['ElTimePicker']
|
|
||||||
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']
|
||||||
|
MapSelector: typeof import('./src/components/map-selector/index.vue')['default']
|
||||||
Markdown: typeof import('./src/components/markdown/index.vue')['default']
|
Markdown: typeof import('./src/components/markdown/index.vue')['default']
|
||||||
PopoverInput: typeof import('./src/components/popover-input/index.vue')['default']
|
PopoverInput: typeof import('./src/components/popover-input/index.vue')['default']
|
||||||
RangeInput: typeof import('./src/components/range-input/index.vue')['default']
|
RangeInput: typeof import('./src/components/range-input/index.vue')['default']
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
69
admin/scripts/addon-utils.cjs
Normal file
69
admin/scripts/addon-utils.cjs
Normal 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/list、goods/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 }
|
||||||
67
admin/scripts/admin-lang-import-utils.cjs
Normal file
67
admin/scripts/admin-lang-import-utils.cjs
Normal 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-lang、assemble-admin、check-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 }
|
||||||
304
admin/scripts/assemble-admin.cjs
Normal file
304
admin/scripts/assemble-admin.cjs
Normal file
@ -0,0 +1,304 @@
|
|||||||
|
/**
|
||||||
|
* 组装最终可部署 dist/
|
||||||
|
*
|
||||||
|
* 将 staging 目录合并为站点可访问结构:
|
||||||
|
* dist/.addons/* → dist/assets/addons/{key}/
|
||||||
|
* dist/.core/* → dist/(index.html、assets 等,跳过 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 产物,保留 addons(shared 随后从 .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()
|
||||||
56
admin/scripts/build-addon.cjs
Normal file
56
admin/scripts/build-addon.cjs
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
/**
|
||||||
|
* 编译单个 addon
|
||||||
|
*
|
||||||
|
* 流程:generate-addon-entry → vite lib build → sync-addon
|
||||||
|
* 输出 staging:dist/.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)
|
||||||
86
admin/scripts/build-all.cjs
Normal file
86
admin/scripts/build-all.cjs
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
/**
|
||||||
|
* 全量生产构建(npm run build)
|
||||||
|
*
|
||||||
|
* 1. build-shared 共享 ESM(vue、admin-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)
|
||||||
|
})
|
||||||
120
admin/scripts/build-shared.cjs
Normal file
120
admin/scripts/build-shared.cjs
Normal 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()
|
||||||
97
admin/scripts/check-deploy.cjs
Normal file
97
admin/scripts/check-deploy.cjs
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
/**
|
||||||
|
* 部署目录健康检查
|
||||||
|
*
|
||||||
|
* 校验 public/admin 是否具备 split 架构所需文件:
|
||||||
|
* - index.html 入口、shared、admin-lang.js
|
||||||
|
* - addons/index.json、shop 等插件
|
||||||
|
* - 无错误 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)
|
||||||
25
admin/scripts/clean-core.cjs
Normal file
25
admin/scripts/clean-core.cjs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
/**
|
||||||
|
* 删除 dist/.core(core 构建 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
128
admin/scripts/deploy-to.cjs
Normal 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 合并 addons:dist 构建产物 + 目标站 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()
|
||||||
185
admin/scripts/ensure-esbuild.cjs
Normal file
185
admin/scripts/ensure-esbuild.cjs
Normal 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)
|
||||||
|
})
|
||||||
39
admin/scripts/fix-admin-lang-imports.cjs
Normal file
39
admin/scripts/fix-admin-lang-imports.cjs
Normal 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}`)
|
||||||
|
}
|
||||||
115
admin/scripts/generate-addon-entry.cjs
Normal file
115
admin/scripts/generate-addon-entry.cjs
Normal 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`)
|
||||||
67
admin/scripts/register-addon.cjs
Normal file
67
admin/scripts/register-addon.cjs
Normal 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)')
|
||||||
30
admin/scripts/run-vite.cjs
Normal file
30
admin/scripts/run-vite.cjs
Normal 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)
|
||||||
150
admin/scripts/shared-external.cjs
Normal file
150
admin/scripts/shared-external.cjs
Normal 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 的 importmap(assemble-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
|
||||||
|
}
|
||||||
48
admin/scripts/strip-style-imports.cjs
Normal file
48
admin/scripts/strip-style-imports.cjs
Normal 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 }
|
||||||
118
admin/scripts/sync-addon.cjs
Normal file
118
admin/scripts/sync-addon.cjs
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
/**
|
||||||
|
* 将 dist/.addons/{key} 同步到 dist/assets/addons/{key}
|
||||||
|
*
|
||||||
|
* build-addon 成功后调用;也可单独执行以刷新已构建插件到 assets 目录。
|
||||||
|
* 同时写入 manifest.json、lang/ 备份、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)
|
||||||
53
admin/scripts/verify-core-lang.cjs
Normal file
53
admin/scripts/verify-core-lang.cjs
Normal 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)
|
||||||
47
admin/scripts/vite-plugin-strip-element-plus-style.cjs
Normal file
47
admin/scripts/vite-plugin-strip-element-plus-style.cjs
Normal 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 }
|
||||||
@ -31,7 +31,7 @@ export function installAddon(params: Record<string, any>) {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function cloudInstallAddon(params: Record<string, any>) {
|
export function cloudInstallAddon(params: Record<string, any>) {
|
||||||
return request.post(`addon/cloudinstall/${ params.addon }`, params)
|
return request.post(`addon/cloudinstall/${ params.addon }`, params, { showErrorMessage: false })
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -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() {
|
const instance = axios.create({
|
||||||
return request.post('niucloud/build', {})
|
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')
|
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 } })
|
||||||
|
}
|
||||||
|
|||||||
@ -629,8 +629,8 @@ export function setMap(params: Record<string, any>) {
|
|||||||
/**
|
/**
|
||||||
* 获取地图配置
|
* 获取地图配置
|
||||||
*/
|
*/
|
||||||
export function getMap() {
|
export function getMap(params: Record<string, any>) {
|
||||||
return request.get(`sys/config/map`)
|
return request.get(`sys/config/map`, { params })
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -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 }`)
|
||||||
|
}
|
||||||
|
|
||||||
/***************************************************** 管理端 ****************************************************/
|
/***************************************************** 管理端 ****************************************************/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -70,7 +70,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex justify-end mt-[20px]" v-show="cloudBuildTask">
|
<div class="flex justify-end mt-[20px]" v-show="cloudBuildTask">
|
||||||
<el-button @click="dialogCancel()" class="!w-[90px]">取消</el-button>
|
<el-button @click="dialogCancel()" class="!w-[90px]">取消</el-button>
|
||||||
<el-button type="primary" :loading="timeloading" class="!w-[140px]">已用时 {{ formattedDuration }}</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>
|
</div>
|
||||||
<div v-show="active == 'error'">
|
<div v-show="active == 'error'">
|
||||||
@ -81,10 +82,18 @@
|
|||||||
<img src="@/app/assets/images/error_icon.png" alt="">
|
<img src="@/app/assets/images/error_icon.png" alt="">
|
||||||
</template>
|
</template>
|
||||||
<template #extra>
|
<template #extra>
|
||||||
<el-scrollbar class="max-h-[150px] !overflow-auto text-[15px] text-[#4F516D] mb-[15px] mt-[-15px]">
|
<el-scrollbar class="max-h-[150px] !overflow-auto text-[15px] text-[#4F516D] mb-[15px] mt-[-15px]" v-if="errorInfo">
|
||||||
{{errorInfo}}
|
{{errorInfo}}
|
||||||
</el-scrollbar>
|
</el-scrollbar>
|
||||||
|
<el-alert :closable="false" class="!mb-[15px] !w-full" v-if="errorAnalysis.analysis" type="warning">
|
||||||
|
<template #default>
|
||||||
|
<div class="text-left">
|
||||||
|
错误分析:{{ errorAnalysis.analysis }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-alert>
|
||||||
<el-button @click="handleReturn" class="!w-[90px]">错误信息</el-button>
|
<el-button @click="handleReturn" class="!w-[90px]">错误信息</el-button>
|
||||||
|
<el-button @click="againBuild" type="primary" plain class="!w-[90px]" v-if="errorAnalysis.error_addon">重新编译</el-button>
|
||||||
<el-button @click="showDialog=false" type="primary" class="!w-[90px]">完成</el-button>
|
<el-button @click="showDialog=false" type="primary" class="!w-[90px]">完成</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-result>
|
</el-result>
|
||||||
@ -106,29 +115,358 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-show="active == 'again_build'">
|
||||||
|
<div class="h-[50vh] flex flex-col">
|
||||||
|
<div class="flex-1 h-0 flex items-center flex-col">
|
||||||
|
<el-table
|
||||||
|
ref="tableRef"
|
||||||
|
:data="installedAddonList"
|
||||||
|
row-key="key"
|
||||||
|
size="large"
|
||||||
|
@selection-change="handleSelectionChange"
|
||||||
|
>
|
||||||
|
<el-table-column type="selection" width="55" />
|
||||||
|
<el-table-column label="应用信息" align="left" width="300">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="flex items-center cursor-pointer relative left-[-10px]">
|
||||||
|
<el-image class="w-[54px] h-[54px] rounded-[5px]" :src="row.icon" fit="contain">
|
||||||
|
<template #error>
|
||||||
|
<div class="flex items-center w-full h-full rounded-[5px]">
|
||||||
|
<img class="max-w-full max-h-full" src="@/app/assets/images/icon-addon-one.png" alt="" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-image>
|
||||||
|
<div class="flex-1 w-0 flex flex-col justify-center pl-[20px] font-500 text-[13px]">
|
||||||
|
<div class="w-[236px] truncate leading-[18px]">{{ row.title }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="编译结果" align="left">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="flex items-center" v-if="errorAnalysis.error_addon && errorAnalysis.error_addon != row.key">
|
||||||
|
<el-icon class="text-success mr-1"><SuccessFilled /></el-icon> 编译成功
|
||||||
|
</div>
|
||||||
|
<div v-else class="flex items-center">
|
||||||
|
<el-icon class="text-error mr-1"><WarningFilled /></el-icon> 编译失败,请排除该插件后重新进行编译
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end mt-[20px]">
|
||||||
|
<el-button @click="dialogCancel()" class="!w-[90px]">取消</el-button>
|
||||||
|
<el-button @click="active = 'error'" plain type="primary" class="!w-[90px]">上一步</el-button>
|
||||||
|
<el-button type="primary" @click="againBuild" :loading="loading" class="!w-[100px]">开始编译</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, h, 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 { 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'
|
||||||
import { AnyObject } from '@/types/global'
|
import { AnyObject } from '@/types/global'
|
||||||
import { ElNotification, ElMessageBox } from 'element-plus'
|
import {ElNotification, ElMessageBox, ElMessage} from 'element-plus'
|
||||||
|
|
||||||
const showDialog = ref<boolean>(false)
|
const showDialog = ref<boolean>(false)
|
||||||
const terminalId = ref(Date.now());
|
const terminalId = ref(Date.now())
|
||||||
const cloudBuildTask = ref<null | AnyObject>(null)
|
const cloudBuildTask = ref<null | AnyObject>(null)
|
||||||
const active = ref('build')
|
const active = ref('build')
|
||||||
const cloudBuildCheck = ref<null | AnyObject>(null)
|
const cloudBuildCheck = ref<null | AnyObject>(null)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const terminalRef = ref(null)
|
const terminalRef = ref(null)
|
||||||
|
const selectAddon = ref([])
|
||||||
|
const installedAddonList = ref([])
|
||||||
|
const tableRef = ref(null)
|
||||||
|
|
||||||
|
getInstalledAddonList().then(({ data }) => {
|
||||||
|
installedAddonList.value = Object.values(data)
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleSelectionChange = (rows) => {
|
||||||
|
selectAddon.value = rows.map(row => {
|
||||||
|
return row.key
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
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
|
||||||
@ -157,6 +495,7 @@ const getCloudBuildTaskFn = () => {
|
|||||||
getCloudBuildTaskFn()
|
getCloudBuildTaskFn()
|
||||||
const errorInfo = ref('')
|
const errorInfo = ref('')
|
||||||
const timeloading = ref(false)
|
const timeloading = ref(false)
|
||||||
|
const errorAnalysis = ref({})
|
||||||
const getCloudBuildLogFn = () => {
|
const getCloudBuildLogFn = () => {
|
||||||
timeloading.value = true
|
timeloading.value = true
|
||||||
getCloudBuildLog().then(res => {
|
getCloudBuildLog().then(res => {
|
||||||
@ -202,10 +541,18 @@ 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 || {}
|
||||||
error = item.msg
|
error = item.msg
|
||||||
terminalRef.value.pushMessage({ content: item.msg, class: 'error' })
|
terminalRef.value.pushMessage({ content: item.msg, class: 'error' })
|
||||||
timeloading.value = false
|
timeloading.value = false
|
||||||
@ -234,7 +581,7 @@ const getCloudBuildLogFn = () => {
|
|||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
getCloudBuildLogFn()
|
getCloudBuildLogFn()
|
||||||
}, 2000)
|
}, 5000)
|
||||||
}).catch()
|
}).catch()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -271,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -284,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
|
||||||
@ -294,22 +653,80 @@ const open = async () => {
|
|||||||
cloudBuildCheck.value = data
|
cloudBuildCheck.value = data
|
||||||
showDialog.value = true
|
showDialog.value = true
|
||||||
}
|
}
|
||||||
|
}).catch((e) => {
|
||||||
|
loading.value = false
|
||||||
|
showDialog.value = false
|
||||||
|
if (e.code && e.code == 601) {
|
||||||
|
ElMessageBox.confirm(
|
||||||
|
'云编译服务未启动,必须在启动后进行云编译!',
|
||||||
|
'提示',
|
||||||
|
{
|
||||||
|
distinguishCancelAndClose: true,
|
||||||
|
confirmButtonText: '重新检测',
|
||||||
|
cancelButtonText: '查看操作手册',
|
||||||
|
type: 'warning'
|
||||||
|
}
|
||||||
|
).then(() => {
|
||||||
|
open()
|
||||||
|
}).catch((action) => {
|
||||||
|
action == 'cancel' && window.open('https://doc.press.niucloud.com/php/saas-framework/use/other/third-party-cloud-compilation.html', '_blank')
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
ElMessage({ message: e.msg, type: 'error' })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const againBuild = () => {
|
||||||
|
if (active.value != 'again_build') {
|
||||||
|
active.value = 'again_build'
|
||||||
|
selectAddon.value = installedAddonList.value.map((item) => {
|
||||||
|
tableRef.value.toggleRowSelection(item, true)
|
||||||
|
return item.key
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
loading.value = true
|
||||||
|
cloudBuildLog = []
|
||||||
|
cloudBuild().then(({ data }) => {
|
||||||
|
active.value = 'build'
|
||||||
|
loading.value = false
|
||||||
|
cloudBuildTask.value = data
|
||||||
|
showDialog.value = true
|
||||||
|
localStorage.removeItem('cloud_build_start_time')
|
||||||
|
const taskId = data.task_id
|
||||||
|
if (taskId) {
|
||||||
|
connectSSE(taskId)
|
||||||
|
} else {
|
||||||
|
getCloudBuildLogFn()
|
||||||
|
}
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
showDialog.value = false
|
showDialog.value = false
|
||||||
|
loading.value = false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const selectable = (row: any) => {
|
||||||
|
return selectAddon.value.includes(row.key)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 升级进度动画
|
* 升级进度动画
|
||||||
*/
|
*/
|
||||||
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -327,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'),
|
||||||
@ -352,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'),
|
||||||
@ -382,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
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
441
admin/src/app/components/weappupload/index.vue
Normal file
441
admin/src/app/components/weappupload/index.vue
Normal 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>
|
||||||
@ -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": "秒"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
@ -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)
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -7,15 +7,50 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-form class="page-form mt-[20px]" :model="formData" :rules="formRules" label-width="150px" ref="formRef" v-loading="loading">
|
<el-form class="page-form mt-[20px]" :model="formData" :rules="formRules" label-width="150px" ref="formRef" v-loading="loading">
|
||||||
<el-form-item :label="t('mapKey')" prop="key">
|
<el-form-item label="地图类型" prop="map_type">
|
||||||
<el-input v-model.trim="formData.key" class="input-width" clearable />
|
<div>
|
||||||
<span class="ml-2 cursor-pointer tutorial-btn" @click="tutorialFn">{{ t('clickTutorial') }}</span>
|
<el-radio-group v-model="formData.map_type">
|
||||||
<span class="ml-2 cursor-pointer secret-btn" @click="secretFn('https://lbs.qq.com/dev/console/key/manage')">{{ t('clickSecretKey') }}</span>
|
<el-radio label="tianditu">天地图</el-radio>
|
||||||
</el-form-item>
|
<el-radio label="tencent">腾讯地图</el-radio>
|
||||||
<el-form-item :label="t('aMapKey')" prop="key">
|
</el-radio-group>
|
||||||
<el-input v-model.trim="formData.amap_key" class="input-width" clearable />
|
<div class="text-sm text-gray-400 mt-[10px] leading-none">选择地图服务提供商</div>
|
||||||
<span class="ml-2 cursor-pointer secret-btn" @click="secretFn('https://lbs.amap.com/')">{{ t('clickSecretKey') }}</span>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
|
<template v-if="formData.map_type === 'tencent'">
|
||||||
|
<el-form-item :label="t('mapKey')" prop="key">
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<el-input v-model.trim="formData.key" class="input-width" clearable />
|
||||||
|
<span class="ml-2 cursor-pointer tutorial-btn" @click="tutorialFn">{{ t('clickTutorial') }}</span>
|
||||||
|
<span class="ml-2 cursor-pointer secret-btn" @click="secretFn('https://lbs.qq.com/dev/console/key/manage')">{{ t('clickSecretKey') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-if="formData.map_type === 'tianditu'">
|
||||||
|
<el-form-item label="天地图服务端KEY" prop="tianditu_map_key">
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<el-input v-model.trim="formData.tianditu_map_key" class="input-width" clearable />
|
||||||
|
<span class="ml-2 cursor-pointer secret-btn" @click="secretFn('https://cloudcenter.tianditu.gov.cn/center/development/myApp')">获取密钥</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-400 mt-[10px] leading-none">天地图服务器端API密钥,用于后端地理编码等服务,请求从业务服务器发起,不支持前端地图瓦片加载与 JS API 渲染。</div>
|
||||||
|
<div class="text-sm text-gray-400 mt-[10px] leading-none">使用场景:如地理编码、逆地理编码、坐标转换、批量 POI 查询等。</div>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="天地图浏览器端KEY" prop="tianditu_map_web_key">
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<el-input v-model.trim="formData.tianditu_map_web_key" class="input-width" clearable />
|
||||||
|
<span class="ml-2 cursor-pointer secret-btn" @click="secretFn('https://cloudcenter.tianditu.gov.cn/center/development/myApp')">获取密钥</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-400 mt-[10px] leading-none">天地图浏览器端API密钥,用于前端加载地图,不可用于后端接口鉴权。</div>
|
||||||
|
<div class="text-sm text-gray-400 mt-[10px] leading-none">使用场景:展示地图瓦片、实现地图交互(缩放、拖拽、图层切换、标注、可视化展示、地图选位置等)。</div>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
</template>
|
||||||
<el-form-item :label="t('isOpen')" prop="is_open">
|
<el-form-item :label="t('isOpen')" prop="is_open">
|
||||||
<el-switch v-model="formData.is_open" :active-value="1" :inactive-value="0" />
|
<el-switch v-model="formData.is_open" :active-value="1" :inactive-value="0" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@ -45,8 +80,10 @@ import { FormInstance } from 'element-plus'
|
|||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const formRef = ref<FormInstance>()
|
const formRef = ref<FormInstance>()
|
||||||
const formData = reactive({
|
const formData = reactive({
|
||||||
|
map_type: 'tencent',
|
||||||
key: '',
|
key: '',
|
||||||
amap_key: '',
|
tianditu_map_key: '',
|
||||||
|
tianditu_map_web_key: '',
|
||||||
is_open: 0,
|
is_open: 0,
|
||||||
valid_time: 0
|
valid_time: 0
|
||||||
})
|
})
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1,79 +1,79 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="main-container">
|
<div class="main-container">
|
||||||
<el-form class="page-form loading-box" :model="formData" label-width="150px" ref="formRef" :rules="formRules" v-loading="loading">
|
<el-form class="page-form loading-box" :model="formData" label-width="150px" ref="formRef" :rules="formRules" v-loading="loading">
|
||||||
<el-card class="box-card !border-none" shadow="never">
|
<el-card class="box-card !border-none" shadow="never">
|
||||||
<h3 class="text-[16px] text-[#1D1F3A] font-bold mb-4">{{ pageName }}</h3>
|
<h3 class="text-[16px] text-[#1D1F3A] font-bold mb-4">{{ pageName }}</h3>
|
||||||
<h3 class="panel-title !text-[14px] bg-[#F4F5F7] p-3 border-[#E6E6E6] border-solid border-b-[1px]">{{ t('websiteInfo') }}</h3>
|
<h3 class="panel-title !text-[14px] bg-[#F4F5F7] p-3 border-[#E6E6E6] border-solid border-b-[1px]">{{ t('websiteInfo') }}</h3>
|
||||||
|
|
||||||
<el-form-item :label="t('siteName')" prop="site_name">
|
<el-form-item :label="t('siteName')" prop="site_name">
|
||||||
<el-input v-model.trim="formData.site_name" :placeholder="t('siteNamePlaceholder')" class="input-width" clearable maxlength="20" show-word-limit />
|
<el-input v-model.trim="formData.site_name" :placeholder="t('siteNamePlaceholder')" class="input-width" clearable maxlength="20" show-word-limit />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item :label="t('logo')" prop="logo">
|
<el-form-item :label="t('logo')" prop="logo">
|
||||||
<div>
|
<div>
|
||||||
<upload-image v-model="formData.logo" />
|
<upload-image v-model="formData.logo" />
|
||||||
<p class="text-[12px] text-[#a9a9a9]">{{ t('logoPlaceholder') }}</p>
|
<p class="text-[12px] text-[#a9a9a9]">{{ t('logoPlaceholder') }}</p>
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item :label="t('icon')" prop="icon">
|
<el-form-item :label="t('icon')" prop="icon">
|
||||||
<div>
|
<div>
|
||||||
<upload-image v-model="formData.icon" />
|
<upload-image v-model="formData.icon" />
|
||||||
<p class="text-[12px] text-[#a9a9a9]">{{ t('iconPlaceholder') }}</p>
|
<p class="text-[12px] text-[#a9a9a9]">{{ t('iconPlaceholder') }}</p>
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item :label="t('keywords')">
|
<el-form-item :label="t('keywords')">
|
||||||
<el-input v-model.trim="formData.keywords" :placeholder="t('keywordsPlaceholder')" class="input-width" clearable maxlength="20" show-word-limit />
|
<el-input v-model.trim="formData.keywords" :placeholder="t('keywordsPlaceholder')" class="input-width" clearable maxlength="20" show-word-limit />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item :label="t('desc')">
|
<el-form-item :label="t('desc')">
|
||||||
<el-input v-model.trim="formData.desc" type="textarea" :rows="4" clearable :placeholder="t('descPlaceholder')" class="input-width" maxlength="100" show-word-limit />
|
<el-input v-model.trim="formData.desc" type="textarea" :rows="4" clearable :placeholder="t('descPlaceholder')" class="input-width" maxlength="100" show-word-limit />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<div class="mt-[20px]" v-show="appType == 'site'">
|
<div class="mt-[20px]" v-show="appType == 'site'">
|
||||||
<h3 class="panel-title !text-[14px] bg-[#F4F5F7] p-3 border-[#E6E6E6] border-solid border-b-[1px]">{{ t('frontEndInfo') }}</h3>
|
<h3 class="panel-title !text-[14px] bg-[#F4F5F7] p-3 border-[#E6E6E6] border-solid border-b-[1px]">{{ t('frontEndInfo') }}</h3>
|
||||||
<el-form-item :label="t('frontEndName')">
|
<el-form-item :label="t('frontEndName')">
|
||||||
<el-input v-model.trim="formData.front_end_name" :placeholder="t('frontEndNamePlaceholder')" class="input-width" clearable maxlength="20" show-word-limit />
|
<el-input v-model.trim="formData.front_end_name" :placeholder="t('frontEndNamePlaceholder')" class="input-width" clearable maxlength="20" show-word-limit />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item :label="t('phone')">
|
<el-form-item :label="t('phone')">
|
||||||
<el-input v-model.trim="formData.phone" :placeholder="t('phonePlaceholder')" class="input-width" clearable maxlength="20" show-word-limit />
|
<el-input v-model.trim="formData.phone" :placeholder="t('phonePlaceholder')" class="input-width" clearable maxlength="20" show-word-limit />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item :label="t('logo')">
|
<el-form-item :label="t('logo')">
|
||||||
<upload-image v-model="formData.front_end_logo" />
|
<upload-image v-model="formData.front_end_logo" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item :label="t('icon')">
|
<el-form-item :label="t('icon')">
|
||||||
<upload-image v-model="formData.front_end_icon" />
|
<upload-image v-model="formData.front_end_icon" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item :label="t('metaTitle')">
|
<el-form-item :label="t('metaTitle')">
|
||||||
<el-input v-model.trim="formData.meta_title" :placeholder="t('MetaPlaceholder')" class="input-width" clearable maxlength="40" show-word-limit />
|
<el-input v-model.trim="formData.meta_title" :placeholder="t('MetaPlaceholder')" class="input-width" clearable maxlength="40" show-word-limit />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item :label="t('metaDescription')">
|
<el-form-item :label="t('metaDescription')">
|
||||||
<el-input v-model.trim="formData.meta_desc" :placeholder="t('metaDescriptionPlaceholder')" class="input-width" clearable maxlength="200" show-word-limit />
|
<el-input v-model.trim="formData.meta_desc" :placeholder="t('metaDescriptionPlaceholder')" class="input-width" clearable maxlength="200" show-word-limit />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item :label="t('metaKeywords')">
|
<el-form-item :label="t('metaKeywords')">
|
||||||
<el-input v-model.trim="formData.meta_keyword" :placeholder="t('metaKeywordsPlaceholder')" class="input-width" clearable maxlength="200" show-word-limit />
|
<el-input v-model.trim="formData.meta_keyword" :placeholder="t('metaKeywordsPlaceholder')" class="input-width" clearable maxlength="200" show-word-limit />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-[20px]" v-if="appType == 'admin'">
|
||||||
|
<h3 class="panel-title !text-[14px] bg-[#F4F5F7] p-3 border-[#E6E6E6] border-solid border-b-[1px]">{{ t('serviceInformation') }}</h3>
|
||||||
|
<el-form-item :label="t('contactsTel')">
|
||||||
|
<el-input v-model.trim="formData.tel" :placeholder="t('contactsTelPlaceholder')" class="input-width" clearable maxlength="20" show-word-limit />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item :label="t('wechatCode')">
|
||||||
|
<upload-image v-model="formData.wechat_code" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item :label="t('customerServiceCode')">
|
||||||
|
<upload-image v-model="formData.enterprise_wechat" />
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<div class="fixed-footer-wrap">
|
||||||
|
<div class="fixed-footer">
|
||||||
|
<el-button type="primary" :loading="loading" @click="save(formRef)">{{ t('save') }}</el-button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-[20px]" v-if="appType == 'admin'">
|
|
||||||
<h3 class="panel-title !text-[14px] bg-[#F4F5F7] p-3 border-[#E6E6E6] border-solid border-b-[1px]">{{ t('serviceInformation') }}</h3>
|
|
||||||
<el-form-item :label="t('contactsTel')">
|
|
||||||
<el-input v-model.trim="formData.tel" :placeholder="t('contactsTelPlaceholder')" class="input-width" clearable maxlength="20" show-word-limit />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item :label="t('wechatCode')">
|
|
||||||
<upload-image v-model="formData.wechat_code" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item :label="t('customerServiceCode')">
|
|
||||||
<upload-image v-model="formData.enterprise_wechat" />
|
|
||||||
</el-form-item>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
</el-form>
|
|
||||||
|
|
||||||
<div class="fixed-footer-wrap">
|
|
||||||
<div class="fixed-footer">
|
|
||||||
<el-button type="primary" :loading="loading" @click="save(formRef)">{{ t('save') }}</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
@ -91,46 +91,46 @@ const pageName = route.meta.title
|
|||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const appType = ref(getAppType())
|
const appType = ref(getAppType())
|
||||||
const formData: any = reactive<Record<string, string>>({
|
const formData: any = reactive<Record<string, string>>({
|
||||||
site_name: '',
|
site_name: '',
|
||||||
logo: '',
|
logo: '',
|
||||||
desc: '',
|
desc: '',
|
||||||
latitude: '',
|
latitude: '',
|
||||||
keywords: '',
|
keywords: '',
|
||||||
longitude: '',
|
longitude: '',
|
||||||
province_id: '',
|
province_id: '',
|
||||||
city_id: '',
|
city_id: '',
|
||||||
district_id: '',
|
district_id: '',
|
||||||
address: '',
|
address: '',
|
||||||
full_address: '',
|
full_address: '',
|
||||||
business_hours: '',
|
business_hours: '',
|
||||||
phone: '',
|
phone: '',
|
||||||
front_end_name: '',
|
front_end_name: '',
|
||||||
front_end_logo: '',
|
front_end_logo: '',
|
||||||
front_end_icon: '',
|
front_end_icon: '',
|
||||||
icon: '',
|
icon: '',
|
||||||
wechat_code: '',
|
wechat_code: '',
|
||||||
enterprise_wechat: '',
|
enterprise_wechat: '',
|
||||||
tel: '',
|
tel: '',
|
||||||
site_login_logo: '',
|
site_login_logo: '',
|
||||||
site_login_bg_img: '',
|
site_login_bg_img: '',
|
||||||
meta_title: '',
|
meta_title: '',
|
||||||
meta_desc: '',
|
meta_desc: '',
|
||||||
meta_keyword: ''
|
meta_keyword: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
const setFormData = async () => {
|
const setFormData = async () => {
|
||||||
const data = await (await getWebsite()).data
|
const data = await (await getWebsite()).data
|
||||||
Object.keys(formData).forEach((key: string) => {
|
Object.keys(formData).forEach((key: string) => {
|
||||||
if (data[key] != undefined) formData[key] = data[key]
|
if (data[key] != undefined) formData[key] = data[key]
|
||||||
})
|
})
|
||||||
|
|
||||||
const service_data: any = await (await getService()).data
|
const service_data: any = await (await getService()).data
|
||||||
formData.wechat_code = service_data.wechat_code
|
formData.wechat_code = service_data.wechat_code
|
||||||
formData.enterprise_wechat = service_data.enterprise_wechat
|
formData.enterprise_wechat = service_data.enterprise_wechat
|
||||||
formData.tel = service_data.tel
|
formData.tel = service_data.tel
|
||||||
formData.site_login_logo = service_data.site_login_logo
|
formData.site_login_logo = service_data.site_login_logo
|
||||||
formData.site_login_bg_img = service_data.site_login_bg_img
|
formData.site_login_bg_img = service_data.site_login_bg_img
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
setFormData()
|
setFormData()
|
||||||
|
|
||||||
@ -138,42 +138,42 @@ const formRef = ref<FormInstance>()
|
|||||||
|
|
||||||
// 表单验证规则
|
// 表单验证规则
|
||||||
const formRules = reactive<FormRules>({
|
const formRules = reactive<FormRules>({
|
||||||
site_name: [
|
site_name: [
|
||||||
{ required: true, message: t('siteNamePlaceholder'), trigger: 'blur' }
|
{ required: true, message: t('siteNamePlaceholder'), trigger: 'blur' }
|
||||||
],
|
],
|
||||||
logo: [
|
logo: [
|
||||||
{ required: true, message: t('请选择长方形Logo'), trigger: 'blur' }
|
{ required: true, message: t('请选择长方形Logo'), trigger: 'blur' }
|
||||||
],
|
],
|
||||||
icon: [
|
icon: [
|
||||||
{ required: true, message: t('请选择正方形Logo'), trigger: 'blur' }
|
{ required: true, message: t('请选择正方形Logo'), trigger: 'blur' }
|
||||||
],
|
],
|
||||||
front_end_name: [
|
front_end_name: [
|
||||||
{ required: true, message: t('frontEndNamePlaceholder'), trigger: 'blur' }
|
{ required: true, message: t('frontEndNamePlaceholder'), trigger: 'blur' }
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 保存
|
* 保存
|
||||||
*/
|
*/
|
||||||
const save = async (formEl: FormInstance | undefined) => {
|
const save = async (formEl: FormInstance | undefined) => {
|
||||||
if (loading.value || !formEl) return
|
if (loading.value || !formEl) return
|
||||||
|
|
||||||
await formEl.validate(async (valid) => {
|
await formEl.validate(async (valid) => {
|
||||||
if (valid) {
|
if (valid) {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
setWebsite(formData).then(() => {
|
setWebsite(formData).then(() => {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
appType.value == 'admin' ? useSystemStore().getWebsiteInfo() : useUserStore().getSiteInfo()
|
appType.value == 'admin' ? useSystemStore().getWebsiteInfo() : useUserStore().getSiteInfo()
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
:deep(.loading-box .el-loading-spinner){
|
:deep(.loading-box .el-loading-spinner){
|
||||||
top: 33%;
|
top: 33%;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,149 +1,200 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="main-container">
|
<div class="main-container">
|
||||||
<el-card class="box-card mt-[15px] !border-none" shadow="never" v-for="(item, key) in screne" :key="key">
|
<el-card class="box-card mt-[15px] !border-none" shadow="never" v-for="(item, key) in screne" :key="key">
|
||||||
<div class="flex items-center mb-[20px]">
|
<div class="flex items-center mb-[20px]">
|
||||||
<h3 class="text-[14px] mr-[20px]">{{ item.name }}</h3>
|
<h3 class="text-[14px] mr-[20px]">{{ item.name }}</h3>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<span class="text-[14px] mr-[10px]">{{ t('transferSceneId') }}:</span>
|
<span class="text-[14px] mr-[10px]">{{ t('transferSceneId') }}:</span>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<el-input v-model.trim="item.scene_id" maxlength="5" class="!w-[60px]" :disabled="item.disabled" @blur="handleInput($event,key,item)" :ref="(el: any) =>{ if(el) inputRefs[key] = el }" v-show="!item.disabled"/>
|
<el-input
|
||||||
<div v-show="item.disabled">{{item.scene_id ? item.scene_id : '--'}}</div>
|
v-model.trim="item.scene_id"
|
||||||
<div @click="handleDisabled(item, key)" class="w-[40xp] flex items-center ml-[8px]"><el-icon size="20" color="var(--el-color-primary)"><Edit /></el-icon></div>
|
maxlength="5"
|
||||||
</div>
|
minlength="4"
|
||||||
</div>
|
class="!w-[60px]"
|
||||||
</div>
|
:class="item.error ? '!border-red-500' : ''"
|
||||||
<div>
|
:disabled="item.disabled"
|
||||||
<div class="flex items-center justify-between p-[10px] table-item-border bg">
|
@blur="handleInput($event,key,item)"
|
||||||
<span class="text-base w-[230px]">{{ t('transferType') }}</span>
|
:ref="(el: any) =>{ if(el) inputRefs[key] = el }"
|
||||||
<span class="text-base w-[230px]">{{ t('recvPerception') }}</span>
|
v-show="!item.disabled"
|
||||||
<span class="text-base w-[230px]">{{ t('reportInfos') }}</span>
|
/>
|
||||||
<span class="text-base w-[80px] text-center">{{ t('operation') }}</span>
|
<div v-show="item.disabled">{{ item.scene_id ? item.scene_id : '--' }}</div>
|
||||||
</div>
|
<div @click="handleDisabled(item, key)" class="w-[40px] flex items-center ml-[8px]">
|
||||||
<div v-if="Object.values(item.trade_scene_data).length">
|
<el-icon size="20" color="var(--el-color-primary)">
|
||||||
<div class="flex items-center justify-between p-[10px] table-item-border" v-for="(subItem, subKey) in item.trade_scene_data" :key="subKey">
|
<Edit/>
|
||||||
<div class="flex w-[230px] flex-shrink-0 text-base">{{ subItem.name }}</div>
|
</el-icon>
|
||||||
<div class="flex w-[230px] flex-shrink-0 text-base">{{ subItem.perception }}</div>
|
</div>
|
||||||
<div class="w-[230px] flex-shrink-0 text-base">
|
<span v-if="item.error" class="text-red-500 text-xs ml-2">场景值最少为4位</span>
|
||||||
<div v-for="(childItem,childKey) in subItem.infos" :key="childKey">{{ childKey }}:{{ childItem }}</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-center w-[80px] select-none">
|
</div>
|
||||||
<button class="text-base text-primary" @click="configFn(item,subItem,subKey)">{{ t('deploy') }}</button>
|
<div>
|
||||||
</div>
|
<div class="flex items-center justify-between p-[10px] table-item-border bg">
|
||||||
</div>
|
<span class="text-base w-[230px]">{{ t('transferType') }}</span>
|
||||||
</div>
|
<span class="text-base w-[230px]">{{ t('recvPerception') }}</span>
|
||||||
<div v-else class="min-h-[80px] flex items-center justify-center text-base">
|
<span class="text-base w-[230px]">{{ t('reportInfos') }}</span>
|
||||||
{{ t('noData') }}
|
<span class="text-base w-[80px] text-center">{{ t('operation') }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div v-if="Object.values(item.trade_scene_data).length">
|
||||||
</el-card>
|
<div class="flex items-center justify-between p-[10px] table-item-border"
|
||||||
<el-dialog v-model="showDialog" :title="curData.name" width="550px" :destroy-on-close="true">
|
v-for="(subItem, subKey) in item.trade_scene_data" :key="subKey">
|
||||||
<el-form :model="formData" label-width="110px" ref="formRef" class="page-form">
|
<div class="flex w-[230px] flex-shrink-0 text-base">{{ subItem.name }}</div>
|
||||||
<el-form-item :label="t('recvPerception')" prop="perception" :rules="[{ required: true, message: t('recvPerceptionTips'), trigger: 'blur' }]">
|
<div class="flex w-[230px] flex-shrink-0 text-base">{{ subItem.perception }}</div>
|
||||||
<el-select v-model="formData.perception" :placeholder="t('recvPerceptionTips')" clearable class="!w-[300px]">
|
<div class="w-[230px] flex-shrink-0 text-base">
|
||||||
<el-option v-for="(item,index) in curData.user_recv_perception" :key="index" :label="item" :value="item" />
|
<div v-for="(childItem,childKey) in subItem.infos" :key="childKey">{{ childKey }}:{{ childItem }}</div>
|
||||||
</el-select>
|
</div>
|
||||||
</el-form-item>
|
<div class="flex items-center justify-center w-[80px] select-none">
|
||||||
<template v-for="(item, index) in curData.transfer_scene_report_infos" :key="index">
|
<button class="text-base text-primary" @click="configFn(item,subItem,subKey)">{{ t('deploy') }}</button>
|
||||||
<el-form-item :label="item" :prop="`infos[${item}]`" :rules="[{ required: true, message: `请输入${item}`, trigger: 'blur' }]">
|
</div>
|
||||||
<el-input v-model.trim="formData.infos[item]" maxlength="40" class="!w-[300px]"/>
|
</div>
|
||||||
</el-form-item>
|
</div>
|
||||||
</template>
|
<div v-else class="min-h-[80px] flex items-center justify-center text-base">
|
||||||
</el-form>
|
{{ t('noData') }}
|
||||||
<template #footer>
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
<el-dialog v-model="showDialog" :title="curData.name" width="550px" :destroy-on-close="true">
|
||||||
|
<el-form :model="formData" label-width="110px" ref="formRef" class="page-form">
|
||||||
|
<el-form-item :label="t('recvPerception')" prop="perception"
|
||||||
|
:rules="[{ required: true, message: t('recvPerceptionTips'), trigger: 'blur' }]">
|
||||||
|
<el-select v-model="formData.perception" :placeholder="t('recvPerceptionTips')" clearable class="!w-[300px]">
|
||||||
|
<el-option v-for="(item,index) in curData.user_recv_perception" :key="index" :label="item" :value="item"/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<template v-for="(item, index) in curData.transfer_scene_report_infos" :key="index">
|
||||||
|
<el-form-item :label="item" :prop="`infos[${item}]`"
|
||||||
|
:rules="[{ required: true, message: `请输入${item}`, trigger: 'blur' }]">
|
||||||
|
<el-input v-model.trim="formData.infos[item]" maxlength="40" class="!w-[300px]"/>
|
||||||
|
</el-form-item>
|
||||||
|
</template>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
<span class="dialog-footer">
|
<span class="dialog-footer">
|
||||||
<el-button @click="cancel">{{ t('cancel') }}</el-button>
|
<el-button @click="cancel">{{ t('cancel') }}</el-button>
|
||||||
<el-button type="primary" :loading="loading" @click="confirm(formRef)">{{ t('confirm') }}</el-button>
|
<el-button type="primary" :loading="loading" @click="confirm(formRef)">{{
|
||||||
|
t('confirm')
|
||||||
|
}}</el-button>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { nextTick, ref } from 'vue'
|
import {nextTick, ref} from 'vue'
|
||||||
import { t } from '@/lang'
|
import {t} from '@/lang'
|
||||||
import { getTransferScene, setSceneId, setTradeScene } from '@/app/api/pay'
|
import {getTransferScene, setSceneId, setTradeScene} from '@/app/api/pay'
|
||||||
import { cloneDeep } from 'lodash-es'
|
import {cloneDeep} from 'lodash-es'
|
||||||
import { FormInstance } from 'element-plus'
|
import {FormInstance} from 'element-plus'
|
||||||
|
|
||||||
const screne = ref<any>({})
|
const screne = ref<any>({})
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const getTransferSceneFn = () => {
|
const getTransferSceneFn = () => {
|
||||||
getTransferScene().then(res => {
|
getTransferScene().then(res => {
|
||||||
screne.value = res.data
|
screne.value = res.data
|
||||||
for (const key in screne.value) {
|
for (const key in screne.value) {
|
||||||
screne.value[key].disabled = true
|
screne.value[key].disabled = true
|
||||||
}
|
screne.value[key].error = false
|
||||||
})
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
getTransferSceneFn()
|
getTransferSceneFn()
|
||||||
|
|
||||||
// 更改场景值
|
// 更改场景值(最终修复版)
|
||||||
const handleInput = (e: any, key: any, data: any) => {
|
const handleInput = (e: any, key: any, data: any) => {
|
||||||
if (e.target.value) {
|
const val = e.target.value?.trim() || ''
|
||||||
|
const originalVal = originalValues.value[key] || ''
|
||||||
|
|
||||||
|
// 有值 → 清空:必须提交接口置空
|
||||||
|
if (val === '' && originalVal !== '') {
|
||||||
|
data.error = false
|
||||||
setSceneId({
|
setSceneId({
|
||||||
scene: key,
|
scene: key,
|
||||||
scene_id: e.target.value
|
scene_id: ''
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
data.disabled = true
|
data.disabled = true
|
||||||
getTransferSceneFn()
|
getTransferSceneFn()
|
||||||
})
|
})
|
||||||
} else {
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 本来就空:不提交
|
||||||
|
if (val === '' && originalVal === '') {
|
||||||
|
data.error = false
|
||||||
data.disabled = true
|
data.disabled = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 有值但长度不够:提示,不提交
|
||||||
|
if (val.length < 4) {
|
||||||
|
data.error = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 合法 4-5 位:提交
|
||||||
|
if (val.length >= 4 && val.length <= 5) {
|
||||||
|
data.error = false
|
||||||
|
setSceneId({
|
||||||
|
scene: key,
|
||||||
|
scene_id: val
|
||||||
|
}).then(() => {
|
||||||
|
data.disabled = true
|
||||||
|
getTransferSceneFn()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const inputRefs = ref<any>({})
|
const inputRefs = ref<any>({})
|
||||||
|
const originalValues = ref<any>({})
|
||||||
const handleDisabled = (data: any, key: any) => {
|
const handleDisabled = (data: any, key: any) => {
|
||||||
data.disabled = false
|
originalValues.value[key] = data.scene_id
|
||||||
nextTick(() => {
|
data.disabled = false
|
||||||
inputRefs.value[key].focus()
|
data.error = false
|
||||||
})
|
nextTick(() => {
|
||||||
|
inputRefs.value[key]?.focus()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const showDialog = ref(false)
|
const showDialog = ref(false)
|
||||||
const curData = ref<any>({})
|
const curData = ref<any>({})
|
||||||
const formData = ref({
|
const formData = ref({
|
||||||
type: '',
|
type: '',
|
||||||
scene: '',
|
scene: '',
|
||||||
perception: '',
|
perception: '',
|
||||||
infos: {}
|
infos: {}
|
||||||
})
|
})
|
||||||
const configFn = (data: any, subData: any, type: any) => {
|
const configFn = (data: any, subData: any, type: any) => {
|
||||||
curData.value = cloneDeep(data)
|
curData.value = cloneDeep(data)
|
||||||
formData.value.type = type
|
formData.value.type = type
|
||||||
formData.value.scene = subData.scene
|
formData.value.scene = subData.scene
|
||||||
formData.value.perception = subData.perception
|
formData.value.perception = subData.perception
|
||||||
formData.value.infos = cloneDeep(subData.infos)
|
formData.value.infos = cloneDeep(subData.infos)
|
||||||
showDialog.value = true
|
showDialog.value = true
|
||||||
}
|
}
|
||||||
const formRef = ref<FormInstance>()
|
const formRef = ref<FormInstance>()
|
||||||
|
|
||||||
/**
|
|
||||||
* 确认
|
|
||||||
* @param formEl
|
|
||||||
*/
|
|
||||||
const confirm = async (formEl: FormInstance | undefined) => {
|
const confirm = async (formEl: FormInstance | undefined) => {
|
||||||
if (loading.value || !formEl) return
|
if (loading.value || !formEl) return
|
||||||
await formEl.validate(async (valid) => {
|
await formEl.validate(async (valid) => {
|
||||||
if (valid) {
|
if (valid) {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
setTradeScene(formData.value).then(() => {
|
setTradeScene(formData.value).then(() => {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
showDialog.value = false
|
showDialog.value = false
|
||||||
getTransferSceneFn()
|
getTransferSceneFn()
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const cancel = () => {
|
const cancel = () => {
|
||||||
showDialog.value = false
|
showDialog.value = false
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.table-item-border {
|
.table-item-border {
|
||||||
@apply border-b border-[var(--el-border-color)];
|
@apply border-b border-[var(--el-border-color)];
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@ -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>
|
||||||
|
|||||||
@ -1,18 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="main-container attachment-container">
|
<div class="main-container attachment-container">
|
||||||
<el-card class="box-card !border-none full-container" shadow="never">
|
<el-card class="box-card !border-none full-container" shadow="never">
|
||||||
|
|
||||||
<div class="flex justify-between items-center mb-[20px]">
|
<div class="flex justify-between items-center mb-[20px]">
|
||||||
<span class="text-page-title">{{ pageName }}</span>
|
<span class="text-page-title">{{ pageName }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-tabs v-model="type" tab-position="top">
|
<el-tabs v-model="type" tab-position="top">
|
||||||
<el-tab-pane :label="t(tab)" v-for="(tab, index) in attachmentType" :name="tab" :key="index">
|
<el-tab-pane :label="t(tab)" v-for="(tab, index) in attachmentType" :name="tab" :key="index">
|
||||||
<attachment scene="attachment" :type="tab" />
|
<attachment scene="attachment" :type="tab" />
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
</el-tabs>
|
</el-tabs>
|
||||||
</el-card>
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
@ -30,46 +30,46 @@ const type = ref(attachmentType[0])
|
|||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.attachment-container {
|
.attachment-container {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
min-height: calc(100vh - 94px);
|
min-height: calc(100vh - 94px);
|
||||||
background-color: var(--el-bg-color-overlay);
|
background-color: var(--el-bg-color-overlay);
|
||||||
|
|
||||||
.full-container {
|
.full-container {
|
||||||
height: calc(100vh - 100px);
|
height: calc(100vh - 100px);
|
||||||
}
|
|
||||||
|
|
||||||
.el-card__body {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-tabs {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: calc(100% - 40px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-tabs__content {
|
|
||||||
flex: 1;
|
|
||||||
|
|
||||||
.el-tab-pane {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-tabs__nav-wrap::after {
|
|
||||||
height: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-wrap {
|
|
||||||
border: none;
|
|
||||||
|
|
||||||
.group-wrap {
|
|
||||||
padding: 0 15px 0 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.attachment-list-wrap {
|
.el-card__body {
|
||||||
padding: 0 0 0 15px;
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-tabs {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: calc(100% - 40px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-tabs__content {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.el-tab-pane {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-tabs__nav-wrap::after {
|
||||||
|
height: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-wrap {
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
.group-wrap {
|
||||||
|
padding: 0 15px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-list-wrap {
|
||||||
|
padding: 0 0 0 15px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
512
admin/src/components/map-selector/index.vue
Normal file
512
admin/src/components/map-selector/index.vue
Normal file
@ -0,0 +1,512 @@
|
|||||||
|
<template>
|
||||||
|
<div :id="containerId" :class="containerClass" :style="containerStyle" v-loading="loading"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, onMounted, watch, nextTick, onBeforeUnmount } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { t } from '@/lang'
|
||||||
|
import { getMap } from '@/app/api/sys'
|
||||||
|
|
||||||
|
import {
|
||||||
|
createMarker,
|
||||||
|
createCircle,
|
||||||
|
createPolygon,
|
||||||
|
deleteGeometry,
|
||||||
|
selectGeometry,
|
||||||
|
latLngToAddress
|
||||||
|
} from '@/utils/qqmap'
|
||||||
|
|
||||||
|
// 天地图工具
|
||||||
|
import {
|
||||||
|
createCircle as tdCreateCircle,
|
||||||
|
createPolygon as tdCreatePolygon,
|
||||||
|
deleteGeometry as tdDeleteGeometry,
|
||||||
|
selectGeometry as tdSelectGeometry,
|
||||||
|
clearAllGeometry,
|
||||||
|
latLngToAddress as tiandituLatLngToAddress
|
||||||
|
} from '@/utils/tianditu'
|
||||||
|
|
||||||
|
const MAP_INIT_DELAY = 500
|
||||||
|
const MAP_SEARCH_TIMEOUT = 5000
|
||||||
|
const MAP_SEARCH_INTERVAL = 100
|
||||||
|
const MAP_SEARCH_MAX_ATTEMPTS = 50
|
||||||
|
|
||||||
|
interface MapInstance {
|
||||||
|
destroy?: () => void
|
||||||
|
clearOverLays?: () => void
|
||||||
|
on?: (event: string, callback: Function) => void
|
||||||
|
addControl?: (control: any) => void
|
||||||
|
addOverLay?: (overlay: any) => void
|
||||||
|
removeOverLay?: (overlay: any) => void
|
||||||
|
centerAndZoom?: (center: any, zoom: number) => void
|
||||||
|
setZoom?: (zoom: number) => void
|
||||||
|
setCenter?: (center: any) => void
|
||||||
|
addEventListener?: (event: string, callback: Function) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
containerId?: string
|
||||||
|
containerClass?: string
|
||||||
|
containerStyle?: string
|
||||||
|
longitude?: string | number
|
||||||
|
latitude?: string | number
|
||||||
|
zoom?: number
|
||||||
|
selectedKey?: string
|
||||||
|
disabledClickMarker?: any // 是否禁止点击事件添加标注
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Emits {
|
||||||
|
(e: 'update:longitude', value: string): void
|
||||||
|
(e: 'update:latitude', value: string): void
|
||||||
|
(e: 'locationChange', data: { lat: number, lng: number, address: any }): void
|
||||||
|
(e: 'areaChange', data: { key: string, type: 'circle' | 'polygon', path: any }): void
|
||||||
|
(e: 'selectChange', key: string): void
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
containerId: 'map-container',
|
||||||
|
containerClass: 'w-[800px] h-[500px] relative border border-gray-300',
|
||||||
|
containerStyle: '',
|
||||||
|
longitude: 116.397463,
|
||||||
|
latitude: 39.909187,
|
||||||
|
zoom: 14,
|
||||||
|
selectedKey: '',
|
||||||
|
disabledClickMarker: false
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<Emits>()
|
||||||
|
|
||||||
|
const loading = ref(true)
|
||||||
|
const mapKey = ref('')
|
||||||
|
const mapType = ref<'tencent' | 'tianditu'>('tianditu')
|
||||||
|
let map: MapInstance | null = null
|
||||||
|
let marker: any = null
|
||||||
|
const overlays = ref<any[]>([])
|
||||||
|
|
||||||
|
// 获取地图配置
|
||||||
|
const getMapConfigData = () => {
|
||||||
|
return getMap({
|
||||||
|
need_encrypt: false
|
||||||
|
}).then((res: any) => {
|
||||||
|
mapType.value = res.data.map_type || 'tianditu'
|
||||||
|
mapKey.value = mapType.value === 'tianditu' ? res.data.tianditu_map_web_key : res.data.key
|
||||||
|
}).catch((error) => {
|
||||||
|
loading.value = false
|
||||||
|
ElMessage.error(t('获取地图配置失败'))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化腾讯地图
|
||||||
|
const initTencentMap = () => {
|
||||||
|
const mapScript = document.createElement('script')
|
||||||
|
mapScript.src = 'https://map.qq.com/api/gljs?libraries=tools,service&v=1.exp&key=' + mapKey.value
|
||||||
|
document.body.appendChild(mapScript)
|
||||||
|
|
||||||
|
mapScript.onload = () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
const TMap = (window as any).TMap
|
||||||
|
if (TMap) {
|
||||||
|
const lng = props.longitude ? Number(props.longitude) : 116.397463
|
||||||
|
const lat = props.latitude ? Number(props.latitude) : 39.909187
|
||||||
|
const center = new TMap.LatLng(lat, lng)
|
||||||
|
map = new TMap.Map(props.containerId, { center, zoom: props.zoom })
|
||||||
|
map.on('tilesloaded', () => loading.value = false)
|
||||||
|
|
||||||
|
marker = createMarker(map)
|
||||||
|
if(!props.disabledClickMarker) {
|
||||||
|
map.on('click', (evt: any) => {
|
||||||
|
const ll = evt?.latLng
|
||||||
|
if (!ll) return
|
||||||
|
const lat = typeof ll.getLat === 'function' ? ll.getLat() : ll.lat
|
||||||
|
const lng = typeof ll.getLng === 'function' ? ll.getLng() : ll.lng
|
||||||
|
if (lat == null || lng == null || Number.isNaN(lat) || Number.isNaN(lng)) return
|
||||||
|
|
||||||
|
const newCenter = new TMap.LatLng(lat, lng)
|
||||||
|
map.setZoom(16)
|
||||||
|
map.setCenter(newCenter)
|
||||||
|
marker.updateGeometries({ id: 'center', position: newCenter })
|
||||||
|
nextTick(() => {
|
||||||
|
handleLocationChange(lat, lng)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.latitude && props.longitude) {
|
||||||
|
handleLocationChange(lat, lng)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
loading.value = false
|
||||||
|
ElMessage.error(t('腾讯地图加载失败'))
|
||||||
|
}
|
||||||
|
}, MAP_INIT_DELAY)
|
||||||
|
}
|
||||||
|
|
||||||
|
mapScript.onerror = () => {
|
||||||
|
loading.value = false
|
||||||
|
ElMessage.error(t('腾讯地图脚本加载失败'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化天地图
|
||||||
|
const initTiandituMap = () => {
|
||||||
|
const mapScript = document.createElement('script')
|
||||||
|
mapScript.src = 'https://api.tianditu.gov.cn/api?v=4.0&tk=' + mapKey.value
|
||||||
|
document.body.appendChild(mapScript)
|
||||||
|
|
||||||
|
mapScript.onload = () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
const T = (window as any).T
|
||||||
|
if (T) {
|
||||||
|
const lng = props.longitude ? Number(props.longitude) : 116.397463
|
||||||
|
const lat = props.latitude ? Number(props.latitude) : 39.909187
|
||||||
|
map = new T.Map(props.containerId)
|
||||||
|
map.centerAndZoom(new T.LngLat(lng, lat), props.zoom)
|
||||||
|
|
||||||
|
const ctrl = new T.Control.Zoom()
|
||||||
|
map.addControl(ctrl)
|
||||||
|
|
||||||
|
marker = new T.Marker(new T.LngLat(lng, lat))
|
||||||
|
map.addOverLay(marker)
|
||||||
|
|
||||||
|
loading.value = false
|
||||||
|
|
||||||
|
if(!props.disabledClickMarker) {
|
||||||
|
map.addEventListener('click', (evt: any) => {
|
||||||
|
const lngLat = evt.lnglat
|
||||||
|
if (!lngLat) return
|
||||||
|
const lat = lngLat.lat
|
||||||
|
const lng = lngLat.lng
|
||||||
|
if (lat == null || lng == null || Number.isNaN(lat) || Number.isNaN(lng)) return
|
||||||
|
|
||||||
|
map.removeOverLay(marker)
|
||||||
|
|
||||||
|
marker = new T.Marker(new T.LngLat(lng, lat))
|
||||||
|
map.addOverLay(marker)
|
||||||
|
|
||||||
|
map.centerAndZoom(new T.LngLat(lng, lat), 16)
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
handleLocationChange(lat, lng)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.latitude && props.longitude) {
|
||||||
|
handleLocationChange(lat, lng)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
loading.value = false
|
||||||
|
ElMessage.error(t('天地图加载失败'))
|
||||||
|
}
|
||||||
|
}, MAP_INIT_DELAY)
|
||||||
|
}
|
||||||
|
|
||||||
|
mapScript.onerror = () => {
|
||||||
|
loading.value = false
|
||||||
|
ElMessage.error(t('天地图加载失败,请检查密钥配置'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化地图
|
||||||
|
const initMap = () => {
|
||||||
|
if (!mapKey.value) {
|
||||||
|
loading.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 防止重复初始化
|
||||||
|
if (map) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mapType.value === 'tencent') {
|
||||||
|
initTencentMap()
|
||||||
|
} else if (mapType.value === 'tianditu') {
|
||||||
|
initTiandituMap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理位置变化
|
||||||
|
const handleLocationChange = (lat: number, lng: number) => {
|
||||||
|
emit('update:longitude', String(lng))
|
||||||
|
emit('update:latitude', String(lat))
|
||||||
|
|
||||||
|
if (mapType.value === 'tencent') {
|
||||||
|
latLngToAddress({ mapKey: mapKey.value, lat, lng }).then(({ message, result }) => {
|
||||||
|
if (message == 'query ok' || message == 'Success') {
|
||||||
|
emit('locationChange', { lat, lng, address: result })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else if (mapType.value === 'tianditu') {
|
||||||
|
tiandituLatLngToAddress({ mapKey: mapKey.value, lat, lng }).then((addressData: any) => {
|
||||||
|
emit('locationChange', { lat, lng, address: addressData })
|
||||||
|
}).catch((error: any) => {
|
||||||
|
ElMessage.error(error.message || t('地址解析失败'))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新地图位置(供外部调用)
|
||||||
|
const updateLocation = (lat: number, lng: number) => {
|
||||||
|
if (!map || !marker) return
|
||||||
|
|
||||||
|
if (mapType.value === 'tencent') {
|
||||||
|
const TMap = (window as any).TMap
|
||||||
|
if (!TMap) return
|
||||||
|
const newCenter = new TMap.LatLng(lat, lng)
|
||||||
|
map.setZoom(16)
|
||||||
|
map.setCenter(newCenter)
|
||||||
|
marker.updateGeometries({ id: 'center', position: newCenter })
|
||||||
|
} else if (mapType.value === 'tianditu') {
|
||||||
|
const T = (window as any).T
|
||||||
|
if (!T) return
|
||||||
|
const newCenter = new T.LngLat(lng, lat)
|
||||||
|
|
||||||
|
// 移除旧标记
|
||||||
|
map.removeOverLay(marker)
|
||||||
|
|
||||||
|
// 创建新标记
|
||||||
|
marker = new T.Marker(newCenter)
|
||||||
|
map.addOverLay(marker)
|
||||||
|
|
||||||
|
map.centerAndZoom(newCenter, 16)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 地址搜索(供外部调用)
|
||||||
|
const searchAddress = (address: string): Promise<{ lat: number, lng: number }> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!mapKey.value) {
|
||||||
|
reject(new Error(t('地图配置未加载')))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const waitForMap = () => {
|
||||||
|
return new Promise<void>((resolveWait, rejectWait) => {
|
||||||
|
if (map) {
|
||||||
|
resolveWait()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let attempts = 0
|
||||||
|
const checkInterval = setInterval(() => {
|
||||||
|
attempts++
|
||||||
|
if (map) {
|
||||||
|
clearInterval(checkInterval)
|
||||||
|
resolveWait()
|
||||||
|
} else if (attempts > MAP_SEARCH_MAX_ATTEMPTS) {
|
||||||
|
clearInterval(checkInterval)
|
||||||
|
rejectWait(new Error(t('地图初始化超时')))
|
||||||
|
}
|
||||||
|
}, MAP_SEARCH_INTERVAL)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
waitForMap().then(() => {
|
||||||
|
if (mapType.value === 'tencent') {
|
||||||
|
const TMap = (window as any).TMap
|
||||||
|
if (!TMap?.service) {
|
||||||
|
reject(new Error(t('腾讯地图服务未加载')))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const geocoder = new TMap.service.Geocoder({ key: mapKey.value })
|
||||||
|
geocoder.getLocation({ address }).then((result: any) => {
|
||||||
|
if (result.status === 0 && result.result?.location) {
|
||||||
|
const location = result.result.location
|
||||||
|
const lat = Number(location.lat)
|
||||||
|
const lng = Number(location.lng)
|
||||||
|
updateLocation(lat, lng)
|
||||||
|
resolve({ lat, lng })
|
||||||
|
} else {
|
||||||
|
reject(new Error(result.message || t('未找到该地址')))
|
||||||
|
}
|
||||||
|
}).catch((error) => {
|
||||||
|
reject(error)
|
||||||
|
})
|
||||||
|
} else if (mapType.value === 'tianditu') {
|
||||||
|
const T = (window as any).T
|
||||||
|
if (!T) {
|
||||||
|
reject(new Error(t('天地图服务未加载')))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const geocoder = new T.Geocoder()
|
||||||
|
geocoder.getPoint(address, (result: any) => {
|
||||||
|
if (result && result.status === '0' && result.location) {
|
||||||
|
const lat = result.location.lat
|
||||||
|
const lng = result.location.lon || result.location.lng
|
||||||
|
if (lat && lng) {
|
||||||
|
updateLocation(lat, lng)
|
||||||
|
resolve({ lat, lng })
|
||||||
|
} else {
|
||||||
|
reject(new Error(t('坐标解析失败')))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
reject(new Error(result?.msg || t('未找到该地址')))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}).catch(reject)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// 添加圆
|
||||||
|
const addCircle = (center?: { lat: number; lng: number }, radius = 1000, options?: any) => {
|
||||||
|
if (!map) return null
|
||||||
|
const key = options?.key || `circle_${Date.now()}`
|
||||||
|
let circle = null
|
||||||
|
|
||||||
|
// 创建geometriesData对象
|
||||||
|
const geometriesData = { key, center, radius, ...options }
|
||||||
|
|
||||||
|
if (mapType.value === 'tencent') {
|
||||||
|
createCircle(map, geometriesData, (selectedKey) => {
|
||||||
|
// 触发selectChange事件
|
||||||
|
emit('selectChange', selectedKey)
|
||||||
|
})
|
||||||
|
// 腾讯地图的createCircle不返回值,创建一个包含key的对象添加到overlays
|
||||||
|
circle = { key, geometriesData }
|
||||||
|
} else {
|
||||||
|
circle = tdCreateCircle(map, geometriesData, (data) => {
|
||||||
|
// 触发areaChange事件,确保path数据干净,不包含循环引用
|
||||||
|
emit('areaChange', {
|
||||||
|
key: data.key,
|
||||||
|
type: 'circle',
|
||||||
|
path: {
|
||||||
|
key: data.key,
|
||||||
|
center: data.center,
|
||||||
|
radius: data.radius
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, (selectedKey) => {
|
||||||
|
// 触发selectChange事件
|
||||||
|
emit('selectChange', selectedKey)
|
||||||
|
})
|
||||||
|
// 为天地图添加geometriesData引用
|
||||||
|
if (circle) {
|
||||||
|
circle.geometriesData = geometriesData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (circle) overlays.value.push(circle)
|
||||||
|
return circle
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加多边形
|
||||||
|
const addPolygon = (paths: any[], options?: any) => {
|
||||||
|
if (!map) return null
|
||||||
|
const key = options?.key || `poly_${Date.now()}`
|
||||||
|
let poly = null
|
||||||
|
|
||||||
|
// 创建geometriesData对象
|
||||||
|
const geometriesData = { key, paths, ...options }
|
||||||
|
|
||||||
|
if (mapType.value === 'tencent') {
|
||||||
|
createPolygon(map, geometriesData, (selectedKey) => {
|
||||||
|
// 触发selectChange事件
|
||||||
|
emit('selectChange', selectedKey)
|
||||||
|
})
|
||||||
|
// 腾讯地图的createPolygon不返回值,创建一个包含key的对象添加到overlays
|
||||||
|
poly = { key, geometriesData }
|
||||||
|
} else {
|
||||||
|
poly = tdCreatePolygon(map, geometriesData, (data) => {
|
||||||
|
// 触发areaChange事件,确保path数据干净,不包含循环引用
|
||||||
|
emit('areaChange', {
|
||||||
|
key: data.key,
|
||||||
|
type: 'polygon',
|
||||||
|
path: {
|
||||||
|
key: data.key,
|
||||||
|
paths: data.paths.map((point: any) => ({
|
||||||
|
lat: point.lat,
|
||||||
|
lng: point.lng
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, (selectedKey) => {
|
||||||
|
// 触发selectChange事件
|
||||||
|
emit('selectChange', selectedKey)
|
||||||
|
})
|
||||||
|
// 为天地图添加geometriesData引用
|
||||||
|
if (poly) {
|
||||||
|
poly.geometriesData = geometriesData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (poly) overlays.value.push(poly)
|
||||||
|
return poly
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除覆盖物
|
||||||
|
const removeOverlay = (overlay: any) => {
|
||||||
|
if (!map || !overlay) return
|
||||||
|
const key = overlay.key || ''
|
||||||
|
mapType.value === 'tencent' ? deleteGeometry(key) : tdDeleteGeometry(map, key)
|
||||||
|
|
||||||
|
// 通过key查找并删除覆盖物
|
||||||
|
const idx = overlays.value.findIndex(item => item.key === key)
|
||||||
|
if (idx > -1) overlays.value.splice(idx, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空所有覆盖物
|
||||||
|
const clearOverlays = () => {
|
||||||
|
if (!map) return
|
||||||
|
mapType.value === 'tencent'
|
||||||
|
? overlays.value.forEach(o => deleteGeometry(o.key))
|
||||||
|
: clearAllGeometry(map)
|
||||||
|
overlays.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectGeo = (key: string) => {
|
||||||
|
mapType.value === 'tencent' ? selectGeometry(key) : tdSelectGeometry(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听选中key变化
|
||||||
|
watch(() => props.selectedKey, (newKey) => {
|
||||||
|
if (newKey) {
|
||||||
|
selectGeo(newKey)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听地图key和类型变化
|
||||||
|
watch([mapKey, mapType], ([newKey, newType]) => {
|
||||||
|
if (newKey && newType) {
|
||||||
|
initMap()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getMapConfigData()
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (map) {
|
||||||
|
try {
|
||||||
|
if (mapType.value === 'tencent') {
|
||||||
|
map.destroy && map.destroy()
|
||||||
|
} else if (mapType.value === 'tianditu') {
|
||||||
|
clearOverlays()
|
||||||
|
map.clearOverLays && map.clearOverLays()
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
}
|
||||||
|
map = null
|
||||||
|
marker = null
|
||||||
|
overlays.value = []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
updateLocation,
|
||||||
|
searchAddress,
|
||||||
|
addCircle,
|
||||||
|
addPolygon,
|
||||||
|
removeOverlay,
|
||||||
|
clearOverlays,
|
||||||
|
selectGeometry: selectGeo,
|
||||||
|
getMap: () => map,
|
||||||
|
getMarker: () => marker,
|
||||||
|
handleLocationChange
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
</style>
|
||||||
@ -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}`,
|
||||||
|
|||||||
@ -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>
|
||||||
|
|
||||||
|
|||||||
@ -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}`,
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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, //是否全局注入
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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;
|
||||||
// }
|
// }
|
||||||
// 隐藏滚动条
|
// 隐藏滚动条
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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));
|
||||||
|
|||||||
@ -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%;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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));
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,35 +1,35 @@
|
|||||||
<template>
|
<template>
|
||||||
<div :class="['layout-aside ease-in duration-200 flex box-border', { 'bright': !dark}]">
|
<div :class="['layout-aside ease-in duration-200 flex box-border', { 'bright': !dark}]">
|
||||||
<div class="flex flex-col border-0 border-r-[1px] border-solid border-[var(--el-color-info-light-8)] box-border overflow-hidden">
|
<div class="flex flex-col border-0 border-r-[1px] border-solid border-[var(--el-color-info-light-8)] box-border overflow-hidden">
|
||||||
<div :class="['w-[150px] one-menu hide-scrollbar', { 'expanded': systemStore.menuIsCollapse }]" >
|
<div :class="['w-[150px] one-menu hide-scrollbar', { 'expanded': systemStore.menuIsCollapse }]" >
|
||||||
<div class="flex flex-col items-center">
|
<div class="flex flex-col items-center">
|
||||||
<template v-for="(item, index) in oneMenuData">
|
<template v-for="(item, index) in oneMenuData">
|
||||||
<div v-if="item.meta.show" :title="systemStore.menuIsCollapse ? item.meta.title : item.meta.short_title" class="menu-item my-[2px] p-2 flex w-full box-border cursor-pointer relative" :class="{'is-active':oneMenuActive===item.original_name,'hover-left': systemStore.menuIsCollapse, 'vertical': !systemStore.menuIsCollapse , 'horizontal': systemStore.menuIsCollapse }" :style="{ height: (systemStore.menuIsCollapse ) ? '40px' : '55px' }" @click="router.push({ name: item.name })">
|
<div v-if="item.meta.show" :title="systemStore.menuIsCollapse ? item.meta.title : item.meta.short_title" class="menu-item my-[2px] p-2 flex w-full box-border cursor-pointer relative" :class="{'is-active':oneMenuActive===item.original_name,'hover-left': systemStore.menuIsCollapse, 'vertical': !systemStore.menuIsCollapse , 'horizontal': systemStore.menuIsCollapse }" :style="{ height: (systemStore.menuIsCollapse ) ? '40px' : '55px' }" @click="router.push({ name: item.name })">
|
||||||
<div class="w-[20px] h-[20px] flex items-center justify-center menu-icon" :class="{'is-active':oneMenuActive===item.original_name}">
|
<div class="w-[20px] h-[20px] flex items-center justify-center menu-icon" :class="{'is-active':oneMenuActive===item.original_name}">
|
||||||
<template v-if="item.meta.icon">
|
<template v-if="item.meta.icon">
|
||||||
<el-image class="w-[20px] h-[20px] overflow-hidden" :src="item.meta.icon" fit="fill" v-if="isUrl(item.meta.icon)"/>
|
<el-image class="w-[20px] h-[20px] overflow-hidden" :src="item.meta.icon" fit="fill" v-if="isUrl(item.meta.icon)"/>
|
||||||
<icon :name="item.meta.icon" size="20px" color="#1D1F3A" v-else />
|
<icon :name="item.meta.icon" size="20px" color="#1D1F3A" v-else />
|
||||||
</template>
|
</template>
|
||||||
<icon v-else :name="'iconfont iconshezhi1'" color="#1D1F3A" />
|
<icon v-else :name="'iconfont iconshezhi1'" color="#1D1F3A" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="systemStore.menuIsCollapse" class="text-left text-[14px] mt-[3px] w-[75px] using-hidden ml-[10px]">{{ item.meta.title || item.meta.short_title }}</div>
|
<div v-if="systemStore.menuIsCollapse" class="text-left text-[14px] mt-[3px] w-[75px] using-hidden ml-[10px]">{{ item.meta.title || item.meta.short_title }}</div>
|
||||||
<div v-else class="text-center text-[12px] using-hidden mt-1">{{ item.meta.short_title || item.meta.title }}</div>
|
<div v-else class="text-center text-[12px] using-hidden mt-1">{{ item.meta.short_title || item.meta.title }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col two-menu w-[185px] " v-if="twoMenuData.length">
|
||||||
|
<el-scrollbar class="flex-1" >
|
||||||
|
<el-menu :default-active="route.name" :router="true" class="aside-menu">
|
||||||
|
<menu-item v-for="(route, index) in twoMenuData" :routes="route" :key="index" :isNewVersion="isNewVersion" />
|
||||||
|
</el-menu>
|
||||||
|
<div class="h-[48px]"></div>
|
||||||
|
</el-scrollbar>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col two-menu w-[185px] " v-if="twoMenuData.length">
|
|
||||||
<el-scrollbar class="flex-1" >
|
|
||||||
<el-menu :default-active="route.name" :router="true" class="aside-menu">
|
|
||||||
<menu-item v-for="(route, index) in twoMenuData" :routes="route" :key="index" :isNewVersion="isNewVersion" />
|
|
||||||
</el-menu>
|
|
||||||
<div class="h-[48px]"></div>
|
|
||||||
</el-scrollbar>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
@ -53,370 +53,370 @@ const router = useRouter()
|
|||||||
const addonRouters: Record<string, any> = {}
|
const addonRouters: Record<string, any> = {}
|
||||||
const addonIndexRoute = userStore.addonIndexRoute
|
const addonIndexRoute = userStore.addonIndexRoute
|
||||||
const dark = computed(() => {
|
const dark = computed(() => {
|
||||||
return systemStore.dark
|
return systemStore.dark
|
||||||
})
|
})
|
||||||
|
|
||||||
const twoMenuData = ref<Record<string, any>[]>([])
|
const twoMenuData = ref<Record<string, any>[]>([])
|
||||||
const oneMenuData = ref<Record<string, any>[]>([])
|
const oneMenuData = ref<Record<string, any>[]>([])
|
||||||
routers.forEach(item => {
|
routers.forEach(item => {
|
||||||
item.original_name = item.name
|
item.original_name = item.name
|
||||||
if (item.meta.addon == '') {
|
if (item.meta.addon == '') {
|
||||||
if (item.meta.attr == '') {
|
if (item.meta.attr == '') {
|
||||||
if (item.children && item.children.length) {
|
if (item.children && item.children.length) {
|
||||||
item.name = findFirstValidRoute(item.children)
|
item.name = findFirstValidRoute(item.children)
|
||||||
}
|
}
|
||||||
oneMenuData.value.push(item)
|
oneMenuData.value.push(item)
|
||||||
}
|
}
|
||||||
} else if (item.meta.addon != '' && siteInfo?.apps.length <= 1 && siteInfo?.apps[0].key == item.meta.addon && item.meta.show) {
|
} else if (item.meta.addon != '' && siteInfo?.apps.length <= 1 && siteInfo?.apps[0].key == item.meta.addon && item.meta.show) {
|
||||||
if (item.children) {
|
if (item.children) {
|
||||||
item.children.forEach((citem: Record<string, any>) => {
|
item.children.forEach((citem: Record<string, any>) => {
|
||||||
citem.original_name = citem.name
|
citem.original_name = citem.name
|
||||||
if (citem.children && citem.children.length) {
|
if (citem.children && citem.children.length) {
|
||||||
citem.name = findFirstValidRoute(citem.children)
|
citem.name = findFirstValidRoute(citem.children)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
oneMenuData.value.unshift(...item.children)
|
||||||
|
} else {
|
||||||
|
oneMenuData.value.unshift(item)
|
||||||
}
|
}
|
||||||
})
|
|
||||||
oneMenuData.value.unshift(...item.children)
|
|
||||||
} else {
|
} else {
|
||||||
oneMenuData.value.unshift(item)
|
addonRouters[item.meta.addon] = item
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
addonRouters[item.meta.addon] = item
|
|
||||||
}
|
|
||||||
|
|
||||||
// 排序, 功能正确,改了排序后需要把菜单排序的默认值重新调整一下【多应用一级菜单,单应用二级菜单】
|
// 排序, 功能正确,改了排序后需要把菜单排序的默认值重新调整一下【多应用一级菜单,单应用二级菜单】
|
||||||
oneMenuData.value.sort((a, b) => {
|
oneMenuData.value.sort((a, b) => {
|
||||||
if (a.meta.sort && b.meta.sort) {
|
if (a.meta.sort && b.meta.sort) {
|
||||||
return b.meta.sort - a.meta.sort
|
return b.meta.sort - a.meta.sort
|
||||||
} else if (a.meta.sort) {
|
} else if (a.meta.sort) {
|
||||||
return -1
|
return -1
|
||||||
} else if (b.meta.sort) {
|
} else if (b.meta.sort) {
|
||||||
return 1
|
return 1
|
||||||
} else {
|
} else {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// 多应用时将应用插入菜单
|
// 多应用时将应用插入菜单
|
||||||
if (siteInfo?.apps.length > 1) {
|
if (siteInfo?.apps.length > 1) {
|
||||||
const routers:Record<string, any>[] = []
|
const routers:Record<string, any>[] = []
|
||||||
siteInfo?.apps.forEach((item: Record<string, any>) => {
|
siteInfo?.apps.forEach((item: Record<string, any>) => {
|
||||||
if (addonRouters[item.key]) {
|
if (addonRouters[item.key]) {
|
||||||
addonRouters[item.key].name = addonIndexRoute[item.key]
|
addonRouters[item.key].name = addonIndexRoute[item.key]
|
||||||
routers.push(addonRouters[item.key])
|
routers.push(addonRouters[item.key])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
oneMenuData.value.unshift(...routers)
|
oneMenuData.value.unshift(...routers)
|
||||||
|
|
||||||
// 排序, 功能正确,改了排序后需要把菜单排序的默认值重新调整一下【多应用一级菜单,单应用二级菜单】
|
// 排序, 功能正确,改了排序后需要把菜单排序的默认值重新调整一下【多应用一级菜单,单应用二级菜单】
|
||||||
oneMenuData.value.sort((a, b) => {
|
oneMenuData.value.sort((a, b) => {
|
||||||
if (a.meta.sort && b.meta.sort) {
|
if (a.meta.sort && b.meta.sort) {
|
||||||
return b.meta.sort - a.meta.sort
|
return b.meta.sort - a.meta.sort
|
||||||
} else if (a.meta.sort) {
|
} else if (a.meta.sort) {
|
||||||
return -1
|
return -1
|
||||||
} else if (b.meta.sort) {
|
} else if (b.meta.sort) {
|
||||||
return 1
|
return 1
|
||||||
} else {
|
} else {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const oneMenuActive = ref(route.matched[1].name)
|
const oneMenuActive = ref(route.matched[1].name)
|
||||||
// 从 addonKeys 中提取所有需要匹配的 key
|
// 从 addonKeys 中提取所有需要匹配的 key
|
||||||
const getAddonAllKeys = (addonData) => {
|
const getAddonAllKeys = (addonData) => {
|
||||||
if (!addonData || typeof addonData !== 'object') return [];
|
if (!addonData || typeof addonData !== 'object') return [];
|
||||||
const allKeys = [];
|
const allKeys = [];
|
||||||
Object.values(addonData).forEach(category => {
|
Object.values(addonData).forEach(category => {
|
||||||
if (Array.isArray(category.list)) {
|
if (Array.isArray(category.list)) {
|
||||||
category.list.forEach(item => {
|
category.list.forEach(item => {
|
||||||
if (item.key) allKeys.push(item.key);
|
if (item.key) allKeys.push(item.key);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return allKeys;
|
return allKeys;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理 specialMenusKeys 子菜单 show 的方法
|
// 处理 specialMenusKeys 子菜单 show 的方法
|
||||||
const handleSpecialMenus = () => {
|
const handleSpecialMenus = () => {
|
||||||
const specialMenusKeys = storage.get('specialAppList')
|
const specialMenusKeys = storage.get('specialAppList')
|
||||||
if (Array.isArray(specialMenusKeys) && specialMenusKeys.length) {
|
if (Array.isArray(specialMenusKeys) && specialMenusKeys.length) {
|
||||||
const processedSpecialMenus = JSON.parse(JSON.stringify(specialMenusKeys));
|
const processedSpecialMenus = JSON.parse(JSON.stringify(specialMenusKeys));
|
||||||
const activeAppKey = storage.get('activeAppKey');
|
const activeAppKey = storage.get('activeAppKey');
|
||||||
|
|
||||||
// 收集所有特殊菜单的name
|
// 收集所有特殊菜单的name
|
||||||
processedSpecialMenus.forEach(menu => {
|
processedSpecialMenus.forEach(menu => {
|
||||||
if (menu.children && Array.isArray(menu.children)) {
|
if (menu.children && Array.isArray(menu.children)) {
|
||||||
const traverseChildren = (children) => {
|
const traverseChildren = (children) => {
|
||||||
children.forEach(child => {
|
children.forEach(child => {
|
||||||
if (child && child.is_show !== undefined) {
|
if (child && child.is_show !== undefined) {
|
||||||
child.is_show = (child.menu_key === activeAppKey) ? 1 : 0;
|
child.is_show = (child.menu_key === activeAppKey) ? 1 : 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
traverseChildren(menu.children);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
// 过滤掉 children 为空的特殊菜单
|
||||||
traverseChildren(menu.children);
|
const filteredSpecialMenus = processedSpecialMenus.filter(menu => {
|
||||||
}
|
return menu.children && menu.children.length > 0;
|
||||||
});
|
});
|
||||||
// 过滤掉 children 为空的特殊菜单
|
return formatRouters(filteredSpecialMenus);
|
||||||
const filteredSpecialMenus = processedSpecialMenus.filter(menu => {
|
}
|
||||||
return menu.children && menu.children.length > 0;
|
return [];
|
||||||
});
|
|
||||||
return formatRouters(filteredSpecialMenus);
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
};
|
};
|
||||||
watch(route, () => {
|
watch(route, () => {
|
||||||
if (route.meta.attr != '') {
|
if (route.meta.attr != '') {
|
||||||
oneMenuActive.value = route.matched[1].name
|
oneMenuActive.value = route.matched[1].name
|
||||||
twoMenuData.value = route.matched[1].children ?? []
|
twoMenuData.value = route.matched[1].children ?? []
|
||||||
} else {
|
|
||||||
// 多应用
|
|
||||||
if (siteInfo?.apps.length > 1) {
|
|
||||||
twoMenuData.value = route.matched[2].children
|
|
||||||
oneMenuActive.value = route.matched[2].name
|
|
||||||
} else {
|
} else {
|
||||||
// 单应用
|
// 多应用
|
||||||
const oneMenu = route.matched[2]
|
if (siteInfo?.apps.length > 1) {
|
||||||
if (oneMenu.meta.addon == '') {
|
twoMenuData.value = route.matched[2].children
|
||||||
oneMenuActive.value = route.matched[2].name
|
oneMenuActive.value = route.matched[2].name
|
||||||
twoMenuData.value = route.matched[2].children ?? []
|
|
||||||
} else {
|
|
||||||
if (oneMenu.meta.addon == siteInfo?.apps[0].key) {
|
|
||||||
oneMenuActive.value = route.matched[3].name
|
|
||||||
twoMenuData.value = route.matched[3].children ?? []
|
|
||||||
} else {
|
} else {
|
||||||
oneMenuActive.value = route.matched[2].name
|
// 单应用
|
||||||
twoMenuData.value = route.matched[2].children ?? []
|
const oneMenu = route.matched[2]
|
||||||
|
if (oneMenu.meta.addon == '') {
|
||||||
|
oneMenuActive.value = route.matched[2].name
|
||||||
|
twoMenuData.value = route.matched[2].children ?? []
|
||||||
|
} else {
|
||||||
|
if (oneMenu.meta.addon == siteInfo?.apps[0].key) {
|
||||||
|
oneMenuActive.value = route.matched[3].name
|
||||||
|
twoMenuData.value = route.matched[3].children ?? []
|
||||||
|
} else {
|
||||||
|
oneMenuActive.value = route.matched[2].name
|
||||||
|
twoMenuData.value = route.matched[2].children ?? []
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
// const addonKeys = storage.get('defaultAppList')
|
||||||
// const addonKeys = storage.get('defaultAppList')
|
// const addonAllKeys = getAddonAllKeys(addonKeys)
|
||||||
// const addonAllKeys = getAddonAllKeys(addonKeys)
|
// twoMenuData.value = twoMenuData.value.filter((child) =>{
|
||||||
// twoMenuData.value = twoMenuData.value.filter((child) =>{
|
// return !child.name || !addonAllKeys.includes(child.name);
|
||||||
// return !child.name || !addonAllKeys.includes(child.name);
|
// })
|
||||||
// })
|
// if(oneMenuActive.value == 'addon'){
|
||||||
// if(oneMenuActive.value == 'addon'){
|
// // 处理特殊菜单并插入到 twoMenuData 中(与 addon_list 同级)
|
||||||
// // 处理特殊菜单并插入到 twoMenuData 中(与 addon_list 同级)
|
// const processedSpecialMenus = handleSpecialMenus();
|
||||||
// const processedSpecialMenus = handleSpecialMenus();
|
// if (processedSpecialMenus.length) {
|
||||||
// if (processedSpecialMenus.length) {
|
// // 先找到 addon_list 在 twoMenuData 中的索引
|
||||||
// // 先找到 addon_list 在 twoMenuData 中的索引
|
// const addonListIndex = twoMenuData.value.findIndex(
|
||||||
// const addonListIndex = twoMenuData.value.findIndex(
|
// (item) => item.name === 'addon_list'
|
||||||
// (item) => item.name === 'addon_list'
|
// );
|
||||||
// );
|
// if (addonListIndex !== -1) {
|
||||||
// if (addonListIndex !== -1) {
|
// // 将特殊菜单插入到 addon_list 后面(同级)
|
||||||
// // 将特殊菜单插入到 addon_list 后面(同级)
|
// twoMenuData.value.splice(
|
||||||
// twoMenuData.value.splice(
|
// addonListIndex + 1,
|
||||||
// addonListIndex + 1,
|
// 0,
|
||||||
// 0,
|
// ...processedSpecialMenus
|
||||||
// ...processedSpecialMenus
|
// );
|
||||||
// );
|
// } else {
|
||||||
// } else {
|
// // 如果没有 addon_list,直接将特殊菜单添加到 twoMenuData 中
|
||||||
// // 如果没有 addon_list,直接将特殊菜单添加到 twoMenuData 中
|
// twoMenuData.value.push(...processedSpecialMenus);
|
||||||
// twoMenuData.value.push(...processedSpecialMenus);
|
// }
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
// }
|
|
||||||
}, { immediate: true })
|
}, { immediate: true })
|
||||||
|
|
||||||
const frameworkVersionList = ref([])
|
const frameworkVersionList = ref([])
|
||||||
const isNewVersion = computed(() => {
|
const isNewVersion = computed(() => {
|
||||||
if (!newVersion.value || newVersion.value.version_no === version.value) {
|
if (!newVersion.value || newVersion.value.version_no === version.value) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 将版本号转为字符串再处理
|
// 将版本号转为字符串再处理
|
||||||
const currentVersionStr = String(version.value);
|
const currentVersionStr = String(version.value);
|
||||||
const latestVersionStr = String(newVersion.value.version_no);
|
const latestVersionStr = String(newVersion.value.version_no);
|
||||||
// 移除点号并转为数字比较
|
// 移除点号并转为数字比较
|
||||||
const currentVersionNum = parseInt(currentVersionStr.replace(/\./g, ''), 10);
|
const currentVersionNum = parseInt(currentVersionStr.replace(/\./g, ''), 10);
|
||||||
const latestVersionNum = parseInt(latestVersionStr.replace(/\./g, ''), 10);
|
const latestVersionNum = parseInt(latestVersionStr.replace(/\./g, ''), 10);
|
||||||
return latestVersionNum > currentVersionNum;
|
return latestVersionNum > currentVersionNum;
|
||||||
})
|
})
|
||||||
|
|
||||||
const getFrameworkVersionListFn = () => {
|
const getFrameworkVersionListFn = () => {
|
||||||
getFrameworkVersionList().then(({ data }) => {
|
getFrameworkVersionList().then(({ data }) => {
|
||||||
frameworkVersionList.value = data
|
frameworkVersionList.value = data
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
getFrameworkVersionListFn()
|
getFrameworkVersionListFn()
|
||||||
|
|
||||||
const newVersion: any = computed(() => {
|
const newVersion: any = computed(() => {
|
||||||
return frameworkVersionList.value.length ? frameworkVersionList.value[0] : null
|
return frameworkVersionList.value.length ? frameworkVersionList.value[0] : null
|
||||||
})
|
})
|
||||||
const version = ref('')
|
const version = ref('')
|
||||||
const getVersionsInfo = () => {
|
const getVersionsInfo = () => {
|
||||||
getVersions().then((res) => {
|
getVersions().then((res) => {
|
||||||
version.value = res.data.version.version
|
version.value = res.data.version.version
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
getVersionsInfo()
|
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;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
// transition: width 0.1s ease-out;
|
// transition: width 0.1s ease-out;
|
||||||
&.expanded {
|
&.expanded {
|
||||||
width: 185px;
|
width: 185px;
|
||||||
padding: 18px 15px 15px;
|
padding: 18px 15px 15px;
|
||||||
}
|
|
||||||
.menu-item{
|
|
||||||
border-radius: 2px;
|
|
||||||
justify-content: center;
|
|
||||||
&.vertical {
|
|
||||||
width: 55px;
|
|
||||||
height: 55px;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
}
|
}
|
||||||
|
.menu-item{
|
||||||
|
border-radius: 2px;
|
||||||
|
justify-content: center;
|
||||||
|
&.vertical {
|
||||||
|
width: 55px;
|
||||||
|
height: 55px;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
&.horizontal {
|
&.horizontal {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
.menu-icon {
|
.menu-icon {
|
||||||
// background-color: transparent; /* 默认无背景色 */
|
// background-color: transparent; /* 默认无背景色 */
|
||||||
color: #1D1F3A;
|
color: #1D1F3A;
|
||||||
}
|
}
|
||||||
|
|
||||||
// .menu-icon.is-active {
|
// .menu-icon.is-active {
|
||||||
// background-color: var(--el-color-primary); /* 选中时背景色 */
|
// background-color: var(--el-color-primary); /* 选中时背景色 */
|
||||||
// color: white; /* 选中时图标颜色变白 */
|
// color: white; /* 选中时图标颜色变白 */
|
||||||
// border-radius: 4px; /* 可选:使图标背景为圆形 */
|
// border-radius: 4px; /* 可选:使图标背景为圆形 */
|
||||||
// }
|
// }
|
||||||
|
|
||||||
&:hover{
|
&:hover{
|
||||||
background-color: #EAEBF0 !important;
|
background-color: #EAEBF0 !important;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
// 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);
|
||||||
|
}
|
||||||
|
&.is-active{
|
||||||
|
background-color: #EAEBF0 !important;
|
||||||
|
border-radius: 6px;
|
||||||
|
// background-color: var(--el-color-primary-light-9) !important;
|
||||||
|
// border: none;
|
||||||
|
// color:var(--el-color-primary);
|
||||||
|
}
|
||||||
|
span{
|
||||||
|
font-size: 14px;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
&.is-active{
|
.menu-item.hover-left {
|
||||||
background-color: #EAEBF0 !important;
|
justify-content: flex-start;
|
||||||
border-radius: 6px;
|
padding-left: 5px;
|
||||||
// background-color: var(--el-color-primary-light-9) !important;
|
|
||||||
// border: none;
|
|
||||||
// color:var(--el-color-primary);
|
|
||||||
}
|
}
|
||||||
span{
|
&.expanded .menu-item .text-center {
|
||||||
font-size: 14px;
|
opacity: 1;
|
||||||
margin-left: 8px;
|
}
|
||||||
|
:deep(.el-menu){
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
:deep(.el-scrollbar){
|
||||||
|
height: calc(100vh - 65px);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
.menu-item.hover-left {
|
|
||||||
justify-content: flex-start;
|
|
||||||
padding-left: 5px;
|
|
||||||
}
|
|
||||||
&.expanded .menu-item .text-center {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
.el-menu{
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
.el-scrollbar{
|
|
||||||
height: calc(100vh - 65px);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.two-menu{
|
.two-menu{
|
||||||
.aside-menu:not(.el-menu--collapse) {
|
.aside-menu:not(.el-menu--collapse) {
|
||||||
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;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
span{
|
span{
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
&.is-active{
|
&.is-active{
|
||||||
background-color: #EAEBF0 !important;
|
background-color: #EAEBF0 !important;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
// background-color: var(--el-color-primary-light-9) !important;
|
// background-color: var(--el-color-primary-light-9) !important;
|
||||||
}
|
}
|
||||||
&:hover{
|
&:hover{
|
||||||
background-color: #EAEBF0 !important;
|
background-color: #EAEBF0 !important;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
// 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);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
:deep(.el-sub-menu){
|
||||||
|
width: 185px;
|
||||||
|
margin: 4px 0;
|
||||||
|
// margin-bottom: 8px;
|
||||||
|
:deep(.el-sub-menu__title){
|
||||||
|
margin: 0 15px;
|
||||||
|
height: 40px;
|
||||||
|
padding-left: 8px;
|
||||||
|
border-radius: 2px;
|
||||||
|
span{
|
||||||
|
height: 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
&:hover{
|
||||||
|
background-color:#EAEBF0 !important;
|
||||||
|
border-radius: 6px;
|
||||||
|
// background-color: var(--el-color-primary-light-9) !important;
|
||||||
|
// color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
.el-icon:deep(.el-sub-menu__icon-arrow){
|
||||||
|
right: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
:deep(.el-menu-item){
|
||||||
|
padding-left: 25px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.el-sub-menu{
|
|
||||||
width: 185px;
|
|
||||||
margin: 4px 0;
|
|
||||||
// margin-bottom: 8px;
|
|
||||||
.el-sub-menu__title{
|
|
||||||
margin: 0 15px;
|
|
||||||
height: 40px;
|
|
||||||
padding-left: 8px;
|
|
||||||
border-radius: 2px;
|
|
||||||
span{
|
|
||||||
height: 40px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
&:hover{
|
|
||||||
background-color:#EAEBF0 !important;
|
|
||||||
border-radius: 6px;
|
|
||||||
// background-color: var(--el-color-primary-light-9) !important;
|
|
||||||
// color: var(--el-color-primary);
|
|
||||||
}
|
|
||||||
.el-icon.el-sub-menu__icon-arrow{
|
|
||||||
right: 5px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.el-menu-item{
|
|
||||||
padding-left: 25px !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo-wrap {
|
.logo-wrap {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo-title {
|
.logo-title {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
width: 0;
|
width: 0;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
font-size: var(--el-font-size-base);
|
font-size: var(--el-font-size-base);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// :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;
|
||||||
// }
|
// }
|
||||||
// 隐藏滚动条
|
// 隐藏滚动条
|
||||||
.hide-scrollbar::-webkit-scrollbar {
|
.hide-scrollbar::-webkit-scrollbar {
|
||||||
display: none;
|
display: none;
|
||||||
/* Chrome/Safari/Edge */
|
/* Chrome/Safari/Edge */
|
||||||
}
|
}
|
||||||
|
|
||||||
.hide-scrollbar {
|
.hide-scrollbar {
|
||||||
-ms-overflow-style: none;
|
-ms-overflow-style: none;
|
||||||
/* IE/Edge */
|
/* IE/Edge */
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
/* Firefox */
|
/* Firefox */
|
||||||
}
|
}
|
||||||
// .layout-aside .menu-item.is-active{
|
// .layout-aside .menu-item.is-active{
|
||||||
// position: relative;
|
// position: relative;
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -82,5 +82,5 @@ const handleJump = (routeName: string) => {
|
|||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss" scoped>
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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(.*)*') {
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1 +1,4 @@
|
|||||||
/* addon-iconfont.css */
|
@import "addon/higohome/iconfont.css";
|
||||||
|
@import "addon/home_service/iconfont.css";
|
||||||
|
@import "addon/o2o/iconfont.css";
|
||||||
|
@import "addon/tourism/iconfont.css";
|
||||||
|
|||||||
643
admin/src/styles/icon/addon/higohome/iconfont.css
Normal file
643
admin/src/styles/icon/addon/higohome/iconfont.css
Normal 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";
|
||||||
|
}
|
||||||
|
|
||||||
1101
admin/src/styles/icon/addon/higohome/iconfont.json
Normal file
1101
admin/src/styles/icon/addon/higohome/iconfont.json
Normal file
File diff suppressed because it is too large
Load Diff
38
admin/src/styles/icon/addon/home_service/iconfont.css
Normal file
38
admin/src/styles/icon/addon/home_service/iconfont.css
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: "o2o"; /* Project id 4412516 */
|
||||||
|
src: url('//at.alicdn.com/t/c/font_4412516_cacqsbew46.woff2?t=1705720131974') format('woff2'),
|
||||||
|
url('//at.alicdn.com/t/c/font_4412516_cacqsbew46.woff?t=1705720131974') format('woff'),
|
||||||
|
url('//at.alicdn.com/t/c/font_4412516_cacqsbew46.ttf?t=1705720131974') format('truetype');
|
||||||
|
}
|
||||||
|
|
||||||
|
.o2o {
|
||||||
|
font-family: "o2o" !important;
|
||||||
|
font-size: 16px;
|
||||||
|
font-style: normal;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o2o-icon-danhanghuadong:before {
|
||||||
|
content: "\e66f";
|
||||||
|
}
|
||||||
|
|
||||||
|
.o2o-icon-yuanjiao:before {
|
||||||
|
content: "\e6c0";
|
||||||
|
}
|
||||||
|
|
||||||
|
.o2o-icon-gl-square:before {
|
||||||
|
content: "\ea92";
|
||||||
|
}
|
||||||
|
|
||||||
|
.o2o-icon-sousuo12:before {
|
||||||
|
content: "\e699";
|
||||||
|
}
|
||||||
|
|
||||||
|
.o2o-icon-sousuo11:before {
|
||||||
|
content: "\e6d4";
|
||||||
|
}
|
||||||
|
|
||||||
|
.o2o-icon-jishi:before {
|
||||||
|
content: "\e600";
|
||||||
|
}
|
||||||
51
admin/src/styles/icon/addon/home_service/iconfont.json
Normal file
51
admin/src/styles/icon/addon/home_service/iconfont.json
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
"id": "4412516",
|
||||||
|
"name": "上门服务",
|
||||||
|
"font_family": "o2o",
|
||||||
|
"css_prefix_text": "o2o-icon-",
|
||||||
|
"description": "",
|
||||||
|
"glyphs": [
|
||||||
|
{
|
||||||
|
"icon_id": "30621139",
|
||||||
|
"name": "单行滑动",
|
||||||
|
"font_class": "danhanghuadong",
|
||||||
|
"unicode": "e66f",
|
||||||
|
"unicode_decimal": 58991
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon_id": "7149037",
|
||||||
|
"name": "圆角",
|
||||||
|
"font_class": "yuanjiao",
|
||||||
|
"unicode": "e6c0",
|
||||||
|
"unicode_decimal": 59072
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon_id": "7594344",
|
||||||
|
"name": "20gl-square",
|
||||||
|
"font_class": "gl-square",
|
||||||
|
"unicode": "ea92",
|
||||||
|
"unicode_decimal": 60050
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon_id": "10133070",
|
||||||
|
"name": "搜索",
|
||||||
|
"font_class": "sousuo12",
|
||||||
|
"unicode": "e699",
|
||||||
|
"unicode_decimal": 59033
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon_id": "14652583",
|
||||||
|
"name": "搜索",
|
||||||
|
"font_class": "sousuo11",
|
||||||
|
"unicode": "e6d4",
|
||||||
|
"unicode_decimal": 59092
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon_id": "6818781",
|
||||||
|
"name": "技师",
|
||||||
|
"font_class": "jishi",
|
||||||
|
"unicode": "e600",
|
||||||
|
"unicode_decimal": 58880
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
38
admin/src/styles/icon/addon/o2o/iconfont.css
Normal file
38
admin/src/styles/icon/addon/o2o/iconfont.css
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: "o2o"; /* Project id 4412516 */
|
||||||
|
src: url('//at.alicdn.com/t/c/font_4412516_cacqsbew46.woff2?t=1705720131974') format('woff2'),
|
||||||
|
url('//at.alicdn.com/t/c/font_4412516_cacqsbew46.woff?t=1705720131974') format('woff'),
|
||||||
|
url('//at.alicdn.com/t/c/font_4412516_cacqsbew46.ttf?t=1705720131974') format('truetype');
|
||||||
|
}
|
||||||
|
|
||||||
|
.o2o {
|
||||||
|
font-family: "o2o" !important;
|
||||||
|
font-size: 16px;
|
||||||
|
font-style: normal;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o2o-icon-danhanghuadong:before {
|
||||||
|
content: "\e66f";
|
||||||
|
}
|
||||||
|
|
||||||
|
.o2o-icon-yuanjiao:before {
|
||||||
|
content: "\e6c0";
|
||||||
|
}
|
||||||
|
|
||||||
|
.o2o-icon-gl-square:before {
|
||||||
|
content: "\ea92";
|
||||||
|
}
|
||||||
|
|
||||||
|
.o2o-icon-sousuo12:before {
|
||||||
|
content: "\e699";
|
||||||
|
}
|
||||||
|
|
||||||
|
.o2o-icon-sousuo11:before {
|
||||||
|
content: "\e6d4";
|
||||||
|
}
|
||||||
|
|
||||||
|
.o2o-icon-jishi:before {
|
||||||
|
content: "\e600";
|
||||||
|
}
|
||||||
51
admin/src/styles/icon/addon/o2o/iconfont.json
Normal file
51
admin/src/styles/icon/addon/o2o/iconfont.json
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
"id": "4412516",
|
||||||
|
"name": "上门服务",
|
||||||
|
"font_family": "o2o",
|
||||||
|
"css_prefix_text": "o2o-icon-",
|
||||||
|
"description": "",
|
||||||
|
"glyphs": [
|
||||||
|
{
|
||||||
|
"icon_id": "30621139",
|
||||||
|
"name": "单行滑动",
|
||||||
|
"font_class": "danhanghuadong",
|
||||||
|
"unicode": "e66f",
|
||||||
|
"unicode_decimal": 58991
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon_id": "7149037",
|
||||||
|
"name": "圆角",
|
||||||
|
"font_class": "yuanjiao",
|
||||||
|
"unicode": "e6c0",
|
||||||
|
"unicode_decimal": 59072
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon_id": "7594344",
|
||||||
|
"name": "20gl-square",
|
||||||
|
"font_class": "gl-square",
|
||||||
|
"unicode": "ea92",
|
||||||
|
"unicode_decimal": 60050
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon_id": "10133070",
|
||||||
|
"name": "搜索",
|
||||||
|
"font_class": "sousuo12",
|
||||||
|
"unicode": "e699",
|
||||||
|
"unicode_decimal": 59033
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon_id": "14652583",
|
||||||
|
"name": "搜索",
|
||||||
|
"font_class": "sousuo11",
|
||||||
|
"unicode": "e6d4",
|
||||||
|
"unicode_decimal": 59092
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon_id": "6818781",
|
||||||
|
"name": "技师",
|
||||||
|
"font_class": "jishi",
|
||||||
|
"unicode": "e600",
|
||||||
|
"unicode_decimal": 58880
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
58
admin/src/styles/icon/addon/tourism/iconfont.css
Normal file
58
admin/src/styles/icon/addon/tourism/iconfont.css
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: "tourism"; /* Project id 4137250 */
|
||||||
|
src: url('//at.alicdn.com/t/c/font_4137250_st1ha9l0k1e.woff2?t=1687685028672') format('woff2'),
|
||||||
|
url('//at.alicdn.com/t/c/font_4137250_st1ha9l0k1e.woff?t=1687685028672') format('woff'),
|
||||||
|
url('//at.alicdn.com/t/c/font_4137250_st1ha9l0k1e.ttf?t=1687685028672') format('truetype');
|
||||||
|
}
|
||||||
|
|
||||||
|
.tourism {
|
||||||
|
font-family: "tourism" !important;
|
||||||
|
font-size: 16px;
|
||||||
|
font-style: normal;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tourism-icon-feiji:before {
|
||||||
|
content: "\e600";
|
||||||
|
}
|
||||||
|
|
||||||
|
.tourism-icon-lvyou:before {
|
||||||
|
content: "\e6a9";
|
||||||
|
}
|
||||||
|
|
||||||
|
.tourism-icon-lvyouchanpin:before {
|
||||||
|
content: "\e63b";
|
||||||
|
}
|
||||||
|
|
||||||
|
.tourism-icon-lvyou1:before {
|
||||||
|
content: "\e623";
|
||||||
|
}
|
||||||
|
|
||||||
|
.tourism-icon-lvyou2:before {
|
||||||
|
content: "\e601";
|
||||||
|
}
|
||||||
|
|
||||||
|
.tourism-icon-lvyou3:before {
|
||||||
|
content: "\e60c";
|
||||||
|
}
|
||||||
|
|
||||||
|
.tourism-icon-lvyoubaochedingdan:before {
|
||||||
|
content: "\e612";
|
||||||
|
}
|
||||||
|
|
||||||
|
.tourism-icon-lvyou4:before {
|
||||||
|
content: "\e653";
|
||||||
|
}
|
||||||
|
|
||||||
|
.tourism-icon-lvyou5:before {
|
||||||
|
content: "\e610";
|
||||||
|
}
|
||||||
|
|
||||||
|
.tourism-icon-lvyouguanguang:before {
|
||||||
|
content: "\e87e";
|
||||||
|
}
|
||||||
|
|
||||||
|
.tourism-icon-lvyou6:before {
|
||||||
|
content: "\e642";
|
||||||
|
}
|
||||||
86
admin/src/styles/icon/addon/tourism/iconfont.json
Normal file
86
admin/src/styles/icon/addon/tourism/iconfont.json
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
{
|
||||||
|
"id": "4137250",
|
||||||
|
"name": "旅游业",
|
||||||
|
"font_family": "tourism",
|
||||||
|
"css_prefix_text": "tourism-icon-",
|
||||||
|
"description": "",
|
||||||
|
"glyphs": [
|
||||||
|
{
|
||||||
|
"icon_id": "1443",
|
||||||
|
"name": "飞机",
|
||||||
|
"font_class": "feiji",
|
||||||
|
"unicode": "e600",
|
||||||
|
"unicode_decimal": 58880
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon_id": "446824",
|
||||||
|
"name": "旅游",
|
||||||
|
"font_class": "lvyou",
|
||||||
|
"unicode": "e6a9",
|
||||||
|
"unicode_decimal": 59049
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon_id": "1167173",
|
||||||
|
"name": "旅游产品",
|
||||||
|
"font_class": "lvyouchanpin",
|
||||||
|
"unicode": "e63b",
|
||||||
|
"unicode_decimal": 58939
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon_id": "1354920",
|
||||||
|
"name": "旅游",
|
||||||
|
"font_class": "lvyou1",
|
||||||
|
"unicode": "e623",
|
||||||
|
"unicode_decimal": 58915
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon_id": "1505555",
|
||||||
|
"name": "旅游",
|
||||||
|
"font_class": "lvyou2",
|
||||||
|
"unicode": "e601",
|
||||||
|
"unicode_decimal": 58881
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon_id": "2121726",
|
||||||
|
"name": "旅游",
|
||||||
|
"font_class": "lvyou3",
|
||||||
|
"unicode": "e60c",
|
||||||
|
"unicode_decimal": 58892
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon_id": "2357494",
|
||||||
|
"name": "旅游包车订单",
|
||||||
|
"font_class": "lvyoubaochedingdan",
|
||||||
|
"unicode": "e612",
|
||||||
|
"unicode_decimal": 58898
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon_id": "3944019",
|
||||||
|
"name": "旅游",
|
||||||
|
"font_class": "lvyou4",
|
||||||
|
"unicode": "e653",
|
||||||
|
"unicode_decimal": 58963
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon_id": "4838220",
|
||||||
|
"name": "旅游",
|
||||||
|
"font_class": "lvyou5",
|
||||||
|
"unicode": "e610",
|
||||||
|
"unicode_decimal": 58896
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon_id": "7444178",
|
||||||
|
"name": "旅游观光",
|
||||||
|
"font_class": "lvyouguanguang",
|
||||||
|
"unicode": "e87e",
|
||||||
|
"unicode_decimal": 59518
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon_id": "9748082",
|
||||||
|
"name": "旅游",
|
||||||
|
"font_class": "lvyou6",
|
||||||
|
"unicode": "e642",
|
||||||
|
"unicode_decimal": 58946
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
255
admin/src/utils/addon-lang.ts
Normal file
255
admin/src/utils/addon-lang.ts
Normal 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 {}
|
||||||
|
}
|
||||||
204
admin/src/utils/addon-loader.ts
Normal file
204
admin/src/utils/addon-loader.ts
Normal 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}`)
|
||||||
|
}
|
||||||
@ -3,60 +3,61 @@ 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
|
||||||
* @param app
|
* @param app
|
||||||
*/
|
*/
|
||||||
export function useElementIcon(app: App): void {
|
export function useElementIcon(app: App): void {
|
||||||
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||||
app.component(key, component)
|
app.component(key, component)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设置主题色
|
* 设置主题色
|
||||||
*/
|
*/
|
||||||
export function setThemeColor(color: string, mode: string = 'light'): void {
|
export function setThemeColor(color: string, mode: string = 'light'): void {
|
||||||
useCssVar('--el-color-primary', null).value = color
|
useCssVar('--el-color-primary', null).value = color
|
||||||
|
|
||||||
const colors: any = {
|
const colors: any = {
|
||||||
dark: {
|
dark: {
|
||||||
'light-3': 'shade(20%)',
|
'light-3': 'shade(20%)',
|
||||||
'light-5': 'shade(30%)',
|
'light-5': 'shade(30%)',
|
||||||
'light-7': 'shade(50%)',
|
'light-7': 'shade(50%)',
|
||||||
'light-8': 'shade(60%)',
|
'light-8': 'shade(60%)',
|
||||||
'light-9': 'shade(70%)',
|
'light-9': 'shade(70%)',
|
||||||
'dark-2': 'tint(20%)'
|
'dark-2': 'tint(20%)'
|
||||||
},
|
},
|
||||||
light: {
|
light: {
|
||||||
'dark-2': 'shade(20%)',
|
'dark-2': 'shade(20%)',
|
||||||
'light-3': 'tint(30%)',
|
'light-3': 'tint(30%)',
|
||||||
'light-5': 'tint(50%)',
|
'light-5': 'tint(50%)',
|
||||||
'light-7': 'tint(70%)',
|
'light-7': 'tint(70%)',
|
||||||
'light-8': 'tint(80%)',
|
'light-8': 'tint(80%)',
|
||||||
'light-9': 'tint(90%)'
|
'light-9': 'tint(90%)'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.keys(colors[mode]).forEach((key) => {
|
Object.keys(colors[mode]).forEach((key) => {
|
||||||
useCssVar('--el-color-primary' + '-' + key, null).value = colorFunction.convert(`color(${ color } ${ colors[mode][key] })`)
|
useCssVar('--el-color-primary' + '-' + key, null).value = colorFunction.convert(`color(${ color } ${ colors[mode][key] })`)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取当前访问应用类型
|
* 获取当前访问应用类型
|
||||||
*/
|
*/
|
||||||
export function getAppType() {
|
export function getAppType() {
|
||||||
const path = location.pathname.split('/').filter((val) => {
|
const path = location.pathname.split('/').filter((val) => {
|
||||||
return val
|
return val
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!path.length) {
|
if (!path.length) {
|
||||||
return 'admin'
|
return 'admin'
|
||||||
} else {
|
} else {
|
||||||
return path[0]
|
return path[0]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -64,8 +65,8 @@ export function getAppType() {
|
|||||||
* @param value
|
* @param value
|
||||||
*/
|
*/
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -73,7 +74,7 @@ export function setWindowTitle(value: string = ''): void {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function getToken(): null | string {
|
export function getToken(): null | string {
|
||||||
return storage.get('token')
|
return storage.get('token')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -82,7 +83,7 @@ export function getToken(): null | string {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function setToken(token: string): void {
|
export function setToken(token: string): void {
|
||||||
storage.set({ key: 'token', data: token })
|
storage.set({ key: 'token', data: token })
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -90,7 +91,7 @@ export function setToken(token: string): void {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function removeToken(): void {
|
export function removeToken(): void {
|
||||||
storage.remove('token')
|
storage.remove('token')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -100,16 +101,16 @@ export function removeToken(): void {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function debounce(fn: (args?: any) => any, delay: number = 300) {
|
export function debounce(fn: (args?: any) => any, delay: number = 300) {
|
||||||
let timer: null | number = null
|
let timer: null | number = null
|
||||||
return function (...args) {
|
return function (...args) {
|
||||||
if (timer != null) {
|
if (timer != null) {
|
||||||
clearTimeout(timer)
|
clearTimeout(timer)
|
||||||
timer = null
|
timer = null
|
||||||
}
|
}
|
||||||
timer = setTimeout(() => {
|
timer = setTimeout(() => {
|
||||||
fn.call(this, ...args)
|
fn.call(this, ...args)
|
||||||
}, delay);
|
}, delay);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -118,7 +119,7 @@ export function debounce(fn: (args?: any) => any, delay: number = 300) {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function isUrl(str: string): boolean {
|
export function isUrl(str: string): boolean {
|
||||||
return str.indexOf('http://') != -1 || str.indexOf('https://') != -1
|
return str.indexOf('http://') != -1 || str.indexOf('https://') != -1
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -127,14 +128,14 @@ export function isUrl(str: string): boolean {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
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)
|
||||||
|
|
||||||
return isUrl(path) ? path : `${imgDomain}/${path}`
|
return isUrl(path) ? path : `${imgDomain}/${path}`
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -143,7 +144,7 @@ export function img(path: string): string {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function assetImg(path: string) {
|
export function assetImg(path: string) {
|
||||||
return new URL('@/', import.meta.url) + path
|
return new URL('@/', import.meta.url) + path
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -152,15 +153,15 @@ export function assetImg(path: string) {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function strByteLength(str: string = ''): number {
|
export function strByteLength(str: string = ''): number {
|
||||||
let len = 0;
|
let len = 0;
|
||||||
for (let i = 0; i < str.length; i++) {
|
for (let i = 0; i < str.length; i++) {
|
||||||
if (str.charCodeAt(i) > 127 || str.charCodeAt(i) == 94) {
|
if (str.charCodeAt(i) > 127 || str.charCodeAt(i) == 94) {
|
||||||
len += 2;
|
len += 2;
|
||||||
} else {
|
} else {
|
||||||
len++;
|
len++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return len;
|
return len;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -168,22 +169,22 @@ export function strByteLength(str: string = ''): number {
|
|||||||
* @param url
|
* @param url
|
||||||
*/
|
*/
|
||||||
export function urlToRouteRaw(url: string) {
|
export function urlToRouteRaw(url: string) {
|
||||||
const query: any = {}
|
const query: any = {}
|
||||||
const [path, param] = url.split('?')
|
const [path, param] = url.split('?')
|
||||||
|
|
||||||
param && param.split('&').forEach((str: string) => {
|
param && param.split('&').forEach((str: string) => {
|
||||||
let [name, value] = str.split('=')
|
let [name, value] = str.split('=')
|
||||||
query[name] = value
|
query[name] = value
|
||||||
})
|
})
|
||||||
|
|
||||||
return { path, query }
|
return { path, query }
|
||||||
}
|
}
|
||||||
|
|
||||||
const isArray = (value: any) => {
|
const isArray = (value: any) => {
|
||||||
if (typeof Array.isArray === 'function') {
|
if (typeof Array.isArray === 'function') {
|
||||||
return Array.isArray(value)
|
return Array.isArray(value)
|
||||||
}
|
}
|
||||||
return Object.prototype.toString.call(value) === '[object Array]'
|
return Object.prototype.toString.call(value) === '[object Array]'
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -192,19 +193,19 @@ const isArray = (value: any) => {
|
|||||||
* @returns {*} 克隆后的对象或者原值(不是对象)
|
* @returns {*} 克隆后的对象或者原值(不是对象)
|
||||||
*/
|
*/
|
||||||
export function deepClone(obj: object) {
|
export function deepClone(obj: object) {
|
||||||
// 对常见的“非”值,直接返回原来值
|
// 对常见的“非”值,直接返回原来值
|
||||||
if ([null, undefined, NaN, false].includes(obj)) return obj
|
if ([null, undefined, NaN, false].includes(obj)) return obj
|
||||||
if (typeof obj !== 'object' && typeof obj !== 'function') {
|
if (typeof obj !== 'object' && typeof obj !== 'function') {
|
||||||
// 原始类型直接返回
|
// 原始类型直接返回
|
||||||
return obj
|
return obj
|
||||||
}
|
}
|
||||||
const o = isArray(obj) ? [] : {}
|
const o = isArray(obj) ? [] : {}
|
||||||
for (const i in obj) {
|
for (const i in obj) {
|
||||||
if (obj.hasOwnProperty(i)) {
|
if (obj.hasOwnProperty(i)) {
|
||||||
o[i] = typeof obj[i] === 'object' ? deepClone(obj[i]) : obj[i]
|
o[i] = typeof obj[i] === 'object' ? deepClone(obj[i]) : obj[i]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return o
|
return o
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -214,91 +215,91 @@ export function deepClone(obj: object) {
|
|||||||
* @param {Number} radix
|
* @param {Number} radix
|
||||||
*/
|
*/
|
||||||
export function guid(len = 10, firstU = true, radix: any = null) {
|
export function guid(len = 10, firstU = true, radix: any = null) {
|
||||||
const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('')
|
const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('')
|
||||||
const uuid = []
|
const uuid = []
|
||||||
radix = radix || chars.length
|
radix = radix || chars.length
|
||||||
|
|
||||||
if (len) {
|
if (len) {
|
||||||
// 如果指定uuid长度,只是取随机的字符,0|x为位运算,能去掉x的小数位,返回整数位
|
// 如果指定uuid长度,只是取随机的字符,0|x为位运算,能去掉x的小数位,返回整数位
|
||||||
for (let i = 0; i < len; i++) uuid[i] = chars[0 | Math.random() * radix]
|
for (let i = 0; i < len; i++) uuid[i] = chars[0 | Math.random() * radix]
|
||||||
} else {
|
} else {
|
||||||
let r
|
let r
|
||||||
// rfc4122标准要求返回的uuid中,某些位为固定的字符
|
// rfc4122标准要求返回的uuid中,某些位为固定的字符
|
||||||
uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-'
|
uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-'
|
||||||
uuid[14] = '4'
|
uuid[14] = '4'
|
||||||
|
|
||||||
for (let i = 0; i < 36; i++) {
|
for (let i = 0; i < 36; i++) {
|
||||||
if (!uuid[i]) {
|
if (!uuid[i]) {
|
||||||
r = 0 | Math.random() * 16
|
r = 0 | Math.random() * 16
|
||||||
uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r]
|
uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 移除第一个字符,并用u替代,因为第一个字符为数值时,该guuid不能用作id或者class
|
// 移除第一个字符,并用u替代,因为第一个字符为数值时,该guuid不能用作id或者class
|
||||||
if (firstU) {
|
if (firstU) {
|
||||||
uuid.shift()
|
uuid.shift()
|
||||||
return `u${ uuid.join('') }`
|
return `u${ uuid.join('') }`
|
||||||
}
|
}
|
||||||
return uuid.join('')
|
return uuid.join('')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 金额格式化
|
* 金额格式化
|
||||||
*/
|
*/
|
||||||
export function moneyFormat(money: string): string {
|
export function moneyFormat(money: string): string {
|
||||||
return isNaN(parseFloat(money)) ? money : parseFloat(money).toFixed(2)
|
return isNaN(parseFloat(money)) ? money : parseFloat(money).toFixed(2)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 时间戳转日期格式
|
* 时间戳转日期格式
|
||||||
*/
|
*/
|
||||||
export function timeStampTurnTime(timeStamp: any, type = "") {
|
export function timeStampTurnTime(timeStamp: any, type = "") {
|
||||||
if (timeStamp != undefined && timeStamp != "" && timeStamp > 0) {
|
if (timeStamp != undefined && timeStamp != "" && timeStamp > 0) {
|
||||||
const date = new Date();
|
const date = new Date();
|
||||||
date.setTime(timeStamp * 1000);
|
date.setTime(timeStamp * 1000);
|
||||||
const y: any = date.getFullYear();
|
const y: any = date.getFullYear();
|
||||||
let m: any = date.getMonth() + 1;
|
let m: any = date.getMonth() + 1;
|
||||||
m = m < 10 ? ('0' + m) : m;
|
m = m < 10 ? ('0' + m) : m;
|
||||||
let d: any = date.getDate();
|
let d: any = date.getDate();
|
||||||
d = d < 10 ? ('0' + d) : d;
|
d = d < 10 ? ('0' + d) : d;
|
||||||
let h: any = date.getHours();
|
let h: any = date.getHours();
|
||||||
h = h < 10 ? ('0' + h) : h;
|
h = h < 10 ? ('0' + h) : h;
|
||||||
let minute: any = date.getMinutes();
|
let minute: any = date.getMinutes();
|
||||||
let second: any = date.getSeconds();
|
let second: any = date.getSeconds();
|
||||||
minute = minute < 10 ? ('0' + minute) : minute;
|
minute = minute < 10 ? ('0' + minute) : minute;
|
||||||
second = second < 10 ? ('0' + second) : second;
|
second = second < 10 ? ('0' + second) : second;
|
||||||
if (type) {
|
if (type) {
|
||||||
if (type == 'yearMonthDay') {
|
if (type == 'yearMonthDay') {
|
||||||
return y + '年' + m + '月' + d + '日';
|
return y + '年' + m + '月' + d + '日';
|
||||||
}
|
}
|
||||||
return y + '-' + m + '-' + d;
|
return y + '-' + m + '-' + d;
|
||||||
} else {
|
} else {
|
||||||
return y + '-' + m + '-' + d + ' ' + h + ':' + minute + ':' + second;
|
return y + '-' + m + '-' + d + ' ' + h + ':' + minute + ':' + second;
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取当前日期时间
|
* 获取当前日期时间
|
||||||
*/
|
*/
|
||||||
export function getCurrentDataTime(timeStamp: any) {
|
export function getCurrentDataTime(timeStamp: any) {
|
||||||
const addZero = (t) => {
|
const addZero = (t) => {
|
||||||
return t < 10 ? '0' + t : t;
|
return t < 10 ? '0' + t : t;
|
||||||
}
|
}
|
||||||
const time = new Date(timeStamp);
|
const time = new Date(timeStamp);
|
||||||
let Y = time.getFullYear(), // 年
|
let Y = time.getFullYear(), // 年
|
||||||
M = time.getMonth() + 1, // 月
|
M = time.getMonth() + 1, // 月
|
||||||
D = time.getDate(), // 日
|
D = time.getDate(), // 日
|
||||||
h = time.getHours(), // 时
|
h = time.getHours(), // 时
|
||||||
m = time.getMinutes(), // 分
|
m = time.getMinutes(), // 分
|
||||||
s = time.getSeconds(); // 秒
|
s = time.getSeconds(); // 秒
|
||||||
if (M > 12) {
|
if (M > 12) {
|
||||||
M = M - 12;
|
M = M - 12;
|
||||||
}
|
}
|
||||||
return `${Y}-${addZero(M)}-${addZero(D)} ${addZero(h)}:${addZero(m)}:${addZero(s)}`
|
return `${Y}-${addZero(M)}-${addZero(D)} ${addZero(h)}:${addZero(m)}:${addZero(s)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -306,17 +307,17 @@ export function getCurrentDataTime(timeStamp: any) {
|
|||||||
* @param {Object} date
|
* @param {Object} date
|
||||||
*/
|
*/
|
||||||
export function timeTurnTimeStamp(date: string) {
|
export function timeTurnTimeStamp(date: string) {
|
||||||
const f = date.split(' ', 2);
|
const f = date.split(' ', 2);
|
||||||
const d = (f[0] ? f[0] : '').split('-', 3);
|
const d = (f[0] ? f[0] : '').split('-', 3);
|
||||||
const t = (f[1] ? f[1] : '').split(':', 3);
|
const t = (f[1] ? f[1] : '').split(':', 3);
|
||||||
return (new Date(
|
return (new Date(
|
||||||
parseInt(d[0], 10) || null,
|
parseInt(d[0], 10) || null,
|
||||||
(parseInt(d[1], 10) || 1) - 1,
|
(parseInt(d[1], 10) || 1) - 1,
|
||||||
parseInt(d[2], 10) || null,
|
parseInt(d[2], 10) || null,
|
||||||
parseInt(t[0], 10) || null,
|
parseInt(t[0], 10) || null,
|
||||||
parseInt(t[1], 10) || null,
|
parseInt(t[1], 10) || null,
|
||||||
parseInt(t[2], 10) || null
|
parseInt(t[2], 10) || null
|
||||||
)).getTime() / 1000;
|
)).getTime() / 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -324,15 +325,15 @@ export function timeTurnTimeStamp(date: string) {
|
|||||||
* @param event
|
* @param event
|
||||||
*/
|
*/
|
||||||
export function filterDigit(event: any) {
|
export function filterDigit(event: any) {
|
||||||
event.target.value = event.target.value.replace(/[^\d\.]/g, '');
|
event.target.value = event.target.value.replace(/[^\d\.]/g, '');
|
||||||
event.target.value = event.target.value.replace(/^\./g, '');
|
event.target.value = event.target.value.replace(/^\./g, '');
|
||||||
event.target.value = event.target.value.replace(/\.{2,}/g, '.');
|
event.target.value = event.target.value.replace(/\.{2,}/g, '.');
|
||||||
// 限制最多两位小数
|
// 限制最多两位小数
|
||||||
const decimalParts = event.target.value.split('.');
|
const decimalParts = event.target.value.split('.');
|
||||||
if (decimalParts.length > 1 && decimalParts[1].length > 2) {
|
if (decimalParts.length > 1 && decimalParts[1].length > 2) {
|
||||||
// 如果有小数部分且超过两位,则截取前两位
|
// 如果有小数部分且超过两位,则截取前两位
|
||||||
event.target.value = `${ decimalParts[0] }.${ decimalParts[1].slice(0, 2) }`;
|
event.target.value = `${ decimalParts[0] }.${ decimalParts[1].slice(0, 2) }`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -340,7 +341,7 @@ export function filterDigit(event: any) {
|
|||||||
* @param event
|
* @param event
|
||||||
*/
|
*/
|
||||||
export function filterNumber(event: any) {
|
export function filterNumber(event: any) {
|
||||||
event.target.value = event.target.value.replace(/[^\d]/g, '');
|
event.target.value = event.target.value.replace(/[^\d]/g, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -348,8 +349,8 @@ export function filterNumber(event: any) {
|
|||||||
* @param event
|
* @param event
|
||||||
*/
|
*/
|
||||||
export function filterSpecial(event: any) {
|
export function filterSpecial(event: any) {
|
||||||
event.target.value = event.target.value.replace(/[^\u4e00-\u9fa5a-zA-Z0-9]/g, '')
|
event.target.value = event.target.value.replace(/[^\u4e00-\u9fa5a-zA-Z0-9]/g, '')
|
||||||
event.target.value = event.target.value.replace(/[`~!@#$%^&*()_\-+=<>?:"{}|,.\/;'\\[\]·~!@#¥%……&*()——\-+={}|《》?:“”【】、;‘’,。、]/g, '')
|
event.target.value = event.target.value.replace(/[`~!@#$%^&*()_\-+=<>?:"{}|,.\/;'\\[\]·~!@#¥%……&*()——\-+={}|《》?:“”【】、;‘’,。、]/g, '')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -357,7 +358,7 @@ export function filterSpecial(event: any) {
|
|||||||
* @param event
|
* @param event
|
||||||
*/
|
*/
|
||||||
export function filterBlank(event: any) {
|
export function filterBlank(event: any) {
|
||||||
event.target.value = event.target.value.replace(/\s/g, '');
|
event.target.value = event.target.value.replace(/\s/g, '');
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* 设置表格分页数据的本地存储
|
* 设置表格分页数据的本地存储
|
||||||
@ -366,23 +367,23 @@ export function filterBlank(event: any) {
|
|||||||
* @param where
|
* @param where
|
||||||
*/
|
*/
|
||||||
export function setTablePageStorage(page: any = 1, limit: any = 10, where: any = {}) {
|
export function setTablePageStorage(page: any = 1, limit: any = 10, where: any = {}) {
|
||||||
let data = storage.get('tablePageStorage');
|
let data = storage.get('tablePageStorage');
|
||||||
if (!data) {
|
if (!data) {
|
||||||
data = {};
|
data = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
const key = location.pathname + JSON.stringify(where);
|
const key = location.pathname + JSON.stringify(where);
|
||||||
data[key] = {
|
data[key] = {
|
||||||
page,
|
page,
|
||||||
limit
|
limit
|
||||||
};
|
};
|
||||||
|
|
||||||
const MAX_COUNT = 5; // 最多存储 5 个页面的分页缓存,超出则删除最开始的第一个页面
|
const MAX_COUNT = 5; // 最多存储 5 个页面的分页缓存,超出则删除最开始的第一个页面
|
||||||
if (Object.keys(data).length > MAX_COUNT) {
|
if (Object.keys(data).length > MAX_COUNT) {
|
||||||
delete data[Object.keys(data)[0]];
|
delete data[Object.keys(data)[0]];
|
||||||
}
|
}
|
||||||
|
|
||||||
storage.set({ key: 'tablePageStorage', data });
|
storage.set({ key: 'tablePageStorage', data });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -390,49 +391,49 @@ export function setTablePageStorage(page: any = 1, limit: any = 10, where: any =
|
|||||||
* @param where
|
* @param where
|
||||||
*/
|
*/
|
||||||
export function getTablePageStorage(where: any = {}) {
|
export function getTablePageStorage(where: any = {}) {
|
||||||
let data = storage.get('tablePageStorage');
|
let data = storage.get('tablePageStorage');
|
||||||
const key = location.pathname + JSON.stringify(where);
|
const key = location.pathname + JSON.stringify(where);
|
||||||
if (!data || !data[key]) {
|
if (!data || !data[key]) {
|
||||||
data = {
|
data = {
|
||||||
page: 1,
|
page: 1,
|
||||||
limit: 10
|
limit: 10
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
data = data[key];
|
data = data[key];
|
||||||
}
|
}
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 距离显示
|
// 距离显示
|
||||||
|
|
||||||
export function distance(distance: string | number): string {
|
export function distance(distance: string | number): string {
|
||||||
const dist = typeof distance === 'string' ? parseFloat(distance) : distance;
|
const dist = typeof distance === 'string' ? parseFloat(distance) : distance;
|
||||||
if (isNaN(dist)) return distance.toString();
|
if (isNaN(dist)) return distance.toString();
|
||||||
return dist < 1 ? parseInt((dist * 1000).toString()) + 'm' : dist.toFixed(1) + 'km'
|
return dist < 1 ? parseInt((dist * 1000).toString()) + 'm' : dist.toFixed(1) + 'km'
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取图片尺寸的函数
|
// 获取图片尺寸的函数
|
||||||
export function getImageDimensions (url: string) {
|
export function getImageDimensions (url: string) {
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const imgObj = new Image()
|
const imgObj = new Image()
|
||||||
imgObj.onload = () => {
|
imgObj.onload = () => {
|
||||||
// 成功加载
|
// 成功加载
|
||||||
const size = {
|
const size = {
|
||||||
width: imgObj.naturalWidth,
|
width: imgObj.naturalWidth,
|
||||||
height: imgObj.naturalHeight
|
height: imgObj.naturalHeight
|
||||||
}
|
}
|
||||||
resolve(size)
|
resolve(size)
|
||||||
}
|
}
|
||||||
|
|
||||||
imgObj.onerror = (err) => {
|
imgObj.onerror = (err) => {
|
||||||
// 加载失败
|
// 加载失败
|
||||||
resolve(null)
|
resolve(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置图片源,开始加载
|
// 设置图片源,开始加载
|
||||||
// 注意:如果图片跨域且服务器未设置CORS,可能会触发onerror
|
// 注意:如果图片跨域且服务器未设置CORS,可能会触发onerror
|
||||||
imgObj.src = img(url)
|
imgObj.src = img(url)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
10
admin/src/utils/config.ts
Normal file
10
admin/src/utils/config.ts
Normal 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 || ''
|
||||||
|
}
|
||||||
57
admin/src/utils/core-component-resolver.ts
Normal file
57
admin/src/utils/core-component-resolver.ts
Normal 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}`)
|
||||||
|
}
|
||||||
@ -4,8 +4,11 @@ const geometry: any = {}
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 在地图上创建一个圆形
|
* 在地图上创建一个圆形
|
||||||
|
* @param map 地图实例
|
||||||
|
* @param geometriesData 圆形数据
|
||||||
|
* @param onSelect 图形被选中时的回调函数
|
||||||
*/
|
*/
|
||||||
export const createCircle = (map: any, geometriesData: any) => {
|
export const createCircle = (map: any, geometriesData: any, onSelect?: (key: string) => void) => {
|
||||||
const TMap = (window as any).TMap
|
const TMap = (window as any).TMap
|
||||||
const LatLng = TMap.LatLng
|
const LatLng = TMap.LatLng
|
||||||
|
|
||||||
@ -27,7 +30,11 @@ export const createCircle = (map: any, geometriesData: any) => {
|
|||||||
showBorder: true,
|
showBorder: true,
|
||||||
borderColor: `rgb(${color.toString()})`,
|
borderColor: `rgb(${color.toString()})`,
|
||||||
borderWidth: 2
|
borderWidth: 2
|
||||||
})
|
}),
|
||||||
|
highlight: new TMap.PolygonStyle({
|
||||||
|
color: 'rgba(255, 255, 0, 0.6)'
|
||||||
|
})
|
||||||
|
|
||||||
},
|
},
|
||||||
geometries: [
|
geometries: [
|
||||||
{
|
{
|
||||||
@ -38,25 +45,84 @@ export const createCircle = (map: any, geometriesData: any) => {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
geometry[geometriesData.key] = { graphical: multiCircle }
|
|
||||||
|
|
||||||
// 创建图形编辑器
|
// 如果已存在该图形,先删除
|
||||||
const editor = new TMap.tools.GeometryEditor({
|
if (geometry[geometriesData.key]) {
|
||||||
map: map,
|
try {
|
||||||
overlayList: [
|
geometry[geometriesData.key].graphical.remove(geometriesData.key)
|
||||||
{
|
geometry[geometriesData.key].editor?.delete()
|
||||||
overlay: multiCircle,
|
} catch (e) {
|
||||||
id: geometriesData.key,
|
// 删除旧图形失败
|
||||||
}
|
// console.warn('删除旧图形失败:', e)
|
||||||
],
|
}
|
||||||
actionMode: TMap.tools.constants.EDITOR_ACTION.INTERACT,
|
}
|
||||||
activeOverlayId: geometriesData.key, // 激活图层
|
|
||||||
selectable: true // 开启点选功能
|
|
||||||
})
|
|
||||||
|
|
||||||
editor.on('adjust_complete', (data: any) => {
|
let editor = null
|
||||||
geometriesData.center = { lat: data.center.lat, lng: data.center.lng }
|
// 检查 TMap.tools 是否存在
|
||||||
geometriesData.radius = parseInt(data.radius)
|
if (TMap.tools) {
|
||||||
|
// 创建图形编辑器 - 支持拖动中心点和调整半径
|
||||||
|
try {
|
||||||
|
editor = new TMap.tools.GeometryEditor({
|
||||||
|
map: map,
|
||||||
|
overlayList: [
|
||||||
|
{
|
||||||
|
overlay: multiCircle,
|
||||||
|
id: geometriesData.key,
|
||||||
|
selectedStyleId: 'highlight'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
actionMode: TMap.tools.constants?.EDITOR_ACTION?.INTERACT || 0, // 交互模式,支持拖动中心点和调整半径
|
||||||
|
activeOverlayId: null, // 初始不激活,等待用户选择
|
||||||
|
selectable: true ,// 开启点选功能
|
||||||
|
snappable: true // 开启吸附
|
||||||
|
})
|
||||||
|
editor.setKeyboardDeleteEnable(false);
|
||||||
|
|
||||||
|
// 监听调整开始事件 - 禁用地图拖拽
|
||||||
|
editor.on('adjust_start', () => {
|
||||||
|
currentMap?.setDraggable(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听调整完成事件
|
||||||
|
editor.on('adjust_complete', (data: any) => {
|
||||||
|
if (data?.center && data.radius !== undefined) {
|
||||||
|
geometriesData.center = { lat: data.center.lat, lng: data.center.lng }
|
||||||
|
geometriesData.radius = parseInt(data.radius)
|
||||||
|
}
|
||||||
|
setTimeout(() => currentMap?.setDraggable(true), 100)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听调整中事件(实时更新)
|
||||||
|
editor.on('adjust', (data: any) => {
|
||||||
|
if (data?.center && data.radius !== undefined) {
|
||||||
|
geometriesData.center = { lat: data.center.lat, lng: data.center.lng }
|
||||||
|
geometriesData.radius = parseInt(data.radius)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听选中事件
|
||||||
|
editor.on('select', () => {
|
||||||
|
// 只有在用户点击地图上的图形时才触发,而不是在切换页面时
|
||||||
|
if (onSelect) {
|
||||||
|
onSelect(geometriesData.key)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('创建编辑器失败:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 为圆形添加点击事件监听器
|
||||||
|
multiCircle.on('click', () => {
|
||||||
|
// 先选中当前图形,使其高亮显示
|
||||||
|
if (editor) {
|
||||||
|
editor.select([geometriesData.key])
|
||||||
|
editor.setActiveOverlay?.(geometriesData.key)
|
||||||
|
}
|
||||||
|
// 然后触发选中回调
|
||||||
|
if (onSelect) {
|
||||||
|
onSelect(geometriesData.key)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
geometry[geometriesData.key] = { graphical: multiCircle, editor }
|
geometry[geometriesData.key] = { graphical: multiCircle, editor }
|
||||||
@ -64,10 +130,11 @@ export const createCircle = (map: any, geometriesData: any) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 在地图上创建一个多边形
|
* 在地图上创建一个多边形
|
||||||
* @param map
|
* @param map 地图实例
|
||||||
* @param geometriesData
|
* @param geometriesData 多边形数据
|
||||||
|
* @param onSelect 图形被选中时的回调函数
|
||||||
*/
|
*/
|
||||||
export const createPolygon = (map: any, geometriesData: any) => {
|
export const createPolygon = (map: any, geometriesData: any, onSelect?: (key: string) => void) => {
|
||||||
const TMap = (window as any).TMap
|
const TMap = (window as any).TMap
|
||||||
const LatLng = TMap.LatLng
|
const LatLng = TMap.LatLng
|
||||||
|
|
||||||
@ -86,6 +153,17 @@ export const createPolygon = (map: any, geometriesData: any) => {
|
|||||||
Math.floor(Math.random() * 255)
|
Math.floor(Math.random() * 255)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// 如果已存在该图形,先删除
|
||||||
|
if (geometry[geometriesData.key]) {
|
||||||
|
try {
|
||||||
|
geometry[geometriesData.key].graphical.remove(geometriesData.key)
|
||||||
|
geometry[geometriesData.key].editor?.delete()
|
||||||
|
} catch (e) {
|
||||||
|
// 删除旧多边形失败
|
||||||
|
// console.warn('删除旧多边形失败:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const multiPolygon = new TMap.MultiPolygon({
|
const multiPolygon = new TMap.MultiPolygon({
|
||||||
map: map,
|
map: map,
|
||||||
styles: {
|
styles: {
|
||||||
@ -94,7 +172,10 @@ export const createPolygon = (map: any, geometriesData: any) => {
|
|||||||
showBorder: true,
|
showBorder: true,
|
||||||
borderColor: `rgb(${color.toString()})`,
|
borderColor: `rgb(${color.toString()})`,
|
||||||
borderWidth: 2
|
borderWidth: 2
|
||||||
})
|
}),
|
||||||
|
highlight: new TMap.PolygonStyle({
|
||||||
|
color: 'rgba(255, 255, 0, 0.6)'
|
||||||
|
})
|
||||||
},
|
},
|
||||||
geometries: [
|
geometries: [
|
||||||
{
|
{
|
||||||
@ -107,23 +188,70 @@ export const createPolygon = (map: any, geometriesData: any) => {
|
|||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
const editor = new TMap.tools.GeometryEditor({
|
let editor = null
|
||||||
map: map,
|
// 检查 TMap.tools 是否存在
|
||||||
overlayList: [
|
if (TMap.tools) {
|
||||||
{
|
// 创建图形编辑器 - 支持拖动边框和调整形状
|
||||||
overlay: multiPolygon,
|
try {
|
||||||
id: geometriesData.key,
|
editor = new TMap.tools.GeometryEditor({
|
||||||
}
|
map: map,
|
||||||
],
|
overlayList: [
|
||||||
actionMode: TMap.tools.constants.EDITOR_ACTION.INTERACT,
|
{
|
||||||
activeOverlayId: geometriesData.key, // 激活图层
|
overlay: multiPolygon,
|
||||||
selectable: true, // 开启点选功能
|
id: geometriesData.key,
|
||||||
})
|
selectedStyleId: 'highlight'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
actionMode: TMap.tools.constants?.EDITOR_ACTION?.INTERACT || 0, // 交互模式,支持拖动边框和调整形状
|
||||||
|
activeOverlayId: null, // 初始不激活,等待用户选择
|
||||||
|
selectable: true, // 开启点选功能
|
||||||
|
snappable: true // 开启吸附
|
||||||
|
})
|
||||||
|
|
||||||
editor.on('adjust_complete', (data: any) => {
|
editor.setKeyboardDeleteEnable(false);
|
||||||
geometriesData.paths = data.paths.map(item => {
|
// 监听调整开始事件 - 禁用地图拖拽
|
||||||
return { lat: item.lat, lng: item.lng}
|
editor.on('adjust_start', () => {
|
||||||
})
|
currentMap?.setDraggable(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听调整完成事件
|
||||||
|
editor.on('adjust_complete', (data: any) => {
|
||||||
|
if (data?.paths) {
|
||||||
|
geometriesData.paths = data.paths.map((item: any) => ({ lat: item.lat, lng: item.lng }))
|
||||||
|
}
|
||||||
|
setTimeout(() => currentMap?.setDraggable(true), 100)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听调整中事件(实时更新)
|
||||||
|
editor.on('adjust', (data: any) => {
|
||||||
|
if (data?.paths) {
|
||||||
|
geometriesData.paths = data.paths.map((item: any) => ({ lat: item.lat, lng: item.lng }))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听选中事件
|
||||||
|
editor.on('select', () => {
|
||||||
|
// 只有在用户点击地图上的图形时才触发,而不是在切换页面时
|
||||||
|
if (onSelect) {
|
||||||
|
onSelect(geometriesData.key)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('创建编辑器失败:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 为多边形添加点击事件监听器
|
||||||
|
multiPolygon.on('click', () => {
|
||||||
|
// 先选中当前图形,使其高亮显示
|
||||||
|
if (editor) {
|
||||||
|
editor.select([geometriesData.key])
|
||||||
|
editor.setActiveOverlay?.(geometriesData.key)
|
||||||
|
}
|
||||||
|
// 然后触发选中回调
|
||||||
|
if (onSelect) {
|
||||||
|
onSelect(geometriesData.key)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
geometry[geometriesData.key] = { graphical: multiPolygon, editor }
|
geometry[geometriesData.key] = { graphical: multiPolygon, editor }
|
||||||
@ -131,25 +259,68 @@ export const createPolygon = (map: any, geometriesData: any) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除图形
|
* 删除图形
|
||||||
* @param key
|
|
||||||
*/
|
*/
|
||||||
export const deleteGeometry = (key: string) => {
|
export const deleteGeometry = (key: string) => {
|
||||||
if (!geometry[key]) {
|
if (!geometry[key]) return
|
||||||
return
|
try {
|
||||||
|
geometry[key].graphical?.remove(key)
|
||||||
|
geometry[key].editor?.delete()
|
||||||
|
delete geometry[key]
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('删除图形失败:', e)
|
||||||
}
|
}
|
||||||
geometry[key].graphical.remove(key)
|
}
|
||||||
geometry[key].editor.delete()
|
|
||||||
|
/**
|
||||||
|
* 清空所有图形
|
||||||
|
*/
|
||||||
|
export const clearAllGeometry = () => {
|
||||||
|
Object.keys(geometry).forEach(deleteGeometry)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存地图引用,用于控制地图拖拽
|
||||||
|
let currentMap: any = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置地图引用
|
||||||
|
* @param map 地图实例
|
||||||
|
*/
|
||||||
|
export const setMapInstance = (map: any) => {
|
||||||
|
currentMap = map
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 选中图形
|
* 选中图形
|
||||||
* @param key
|
|
||||||
*/
|
*/
|
||||||
export const selectGeometry = (key: string) => {
|
export const selectGeometry = (key: string) => {
|
||||||
if (!geometry[key] || !geometry[key].editor) {
|
if (!geometry[key]?.editor) return
|
||||||
return
|
|
||||||
|
try {
|
||||||
|
// 取消其他图形的选中状态
|
||||||
|
Object.keys(geometry).forEach(k => {
|
||||||
|
if (k !== key && geometry[k]?.editor) {
|
||||||
|
geometry[k].editor.deselect?.()
|
||||||
|
geometry[k].editor.setActiveOverlay?.(null)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// 选中当前图形
|
||||||
|
geometry[key].editor.select?.([key])
|
||||||
|
geometry[key].editor.setActiveOverlay?.(key)
|
||||||
|
} catch (e) {
|
||||||
|
// 删除图形失败
|
||||||
|
// console.error('选中图形失败:', e)
|
||||||
}
|
}
|
||||||
geometry[key].editor.select([key])
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消选中所有图形
|
||||||
|
*/
|
||||||
|
export const deselectAllGeometry = () => {
|
||||||
|
Object.keys(geometry).forEach(k => {
|
||||||
|
geometry[k]?.editor?.deselect?.()
|
||||||
|
geometry[k]?.editor?.setActiveOverlay?.(null)
|
||||||
|
})
|
||||||
|
currentMap?.setDraggable(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -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
|
||||||
@ -21,12 +22,33 @@ interface requestResponse extends AxiosResponse {
|
|||||||
config: InternalRequestConfig
|
config: InternalRequestConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ErrorResponse {
|
||||||
|
msg: string = '';
|
||||||
|
code: number = 0;
|
||||||
|
response: any = null;
|
||||||
|
|
||||||
|
constructor(msg: string);
|
||||||
|
constructor(code: number, msg: string, response: any);
|
||||||
|
|
||||||
|
constructor(arg1?: string | number, arg2?: string, arg3?: any) {
|
||||||
|
if (typeof arg1 === 'number') {
|
||||||
|
this.code = arg1;
|
||||||
|
this.msg = arg2 || '';
|
||||||
|
this.response = arg3; // 修正点3:补上漏掉的赋值
|
||||||
|
} else {
|
||||||
|
this.msg = (arg1 as string) || '';
|
||||||
|
this.code = 0;
|
||||||
|
this.response = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class Request {
|
class Request {
|
||||||
private instance: AxiosInstance;
|
private instance: AxiosInstance;
|
||||||
|
|
||||||
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',
|
||||||
@ -39,13 +61,13 @@ 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) => {
|
||||||
return Promise.reject(err)
|
return Promise.reject(new ErrorResponse(0, err.message, err.response))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -57,7 +79,7 @@ class Request {
|
|||||||
if (res.code != 1) {
|
if (res.code != 1) {
|
||||||
this.handleAuthError(res.code)
|
this.handleAuthError(res.code)
|
||||||
if (res.code != 401 && response.config.showErrorMessage !== false) this.showElMessage({ message: res.msg, type: 'error', dangerouslyUseHTMLString: true, duration: 5000 })
|
if (res.code != 401 && response.config.showErrorMessage !== false) this.showElMessage({ message: res.msg, type: 'error', dangerouslyUseHTMLString: true, duration: 5000 })
|
||||||
return Promise.reject(new Error(res.msg || 'Error'))
|
return Promise.reject(res)
|
||||||
} else {
|
} else {
|
||||||
if (response.config.showSuccessMessage) ElMessage({ message: res.msg, type: 'success' })
|
if (response.config.showSuccessMessage) ElMessage({ message: res.msg, type: 'success' })
|
||||||
return res
|
return res
|
||||||
@ -67,7 +89,7 @@ class Request {
|
|||||||
},
|
},
|
||||||
(err: any) => {
|
(err: any) => {
|
||||||
this.handleNetworkError(err)
|
this.handleNetworkError(err)
|
||||||
return Promise.reject(err)
|
return Promise.reject(new ErrorResponse(0, err.message, err.response))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
511
admin/src/utils/tianditu.ts
Normal file
511
admin/src/utils/tianditu.ts
Normal file
@ -0,0 +1,511 @@
|
|||||||
|
const geometry: any = {}
|
||||||
|
|
||||||
|
// 保存地图引用,用于控制地图拖拽
|
||||||
|
let currentMap: any = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置地图引用
|
||||||
|
* @param map 地图实例
|
||||||
|
*/
|
||||||
|
export const setMapInstance = (map: any) => {
|
||||||
|
currentMap = map
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在天地图上创建一个圆形
|
||||||
|
* @param map 天地图实例
|
||||||
|
* @param geometriesData 圆形数据
|
||||||
|
* @param onChange 图形变化时的回调函数
|
||||||
|
* @param onSelect 图形被选中时的回调函数
|
||||||
|
*/
|
||||||
|
export const createCircle = (map: any, geometriesData: any, onChange?: (data: any) => void, onSelect?: (key: string) => void) => {
|
||||||
|
const T = (window as any).T
|
||||||
|
if (!T) return null
|
||||||
|
|
||||||
|
geometriesData.radius = geometriesData.radius ?? 1000
|
||||||
|
geometriesData.center = geometriesData.center ?? { lat: map.getCenter().lat, lng: map.getCenter().lng }
|
||||||
|
|
||||||
|
const color = [
|
||||||
|
Math.floor(Math.random() * 255),
|
||||||
|
Math.floor(Math.random() * 255),
|
||||||
|
Math.floor(Math.random() * 255)
|
||||||
|
]
|
||||||
|
// 保存原始样式
|
||||||
|
const originalStyle = {
|
||||||
|
color: geometriesData.color || `rgba(${color.toString()}, .4)`,
|
||||||
|
weight: geometriesData.weight || 3,
|
||||||
|
opacity: geometriesData.opacity || 0.8,
|
||||||
|
fillColor: geometriesData.fillColor || `rgba(${color.toString()}, .4)`,
|
||||||
|
fillOpacity: geometriesData.fillOpacity || 0.3
|
||||||
|
}
|
||||||
|
const circle = new T.Circle(new T.LngLat(geometriesData.center.lng, geometriesData.center.lat), geometriesData.radius, originalStyle)
|
||||||
|
|
||||||
|
// 如果已存在该图形,先删除
|
||||||
|
if (geometry[geometriesData.key]) {
|
||||||
|
try {
|
||||||
|
if (geometry[geometriesData.key].editTimer) {
|
||||||
|
clearTimeout(geometry[geometriesData.key].editTimer)
|
||||||
|
}
|
||||||
|
map.removeOverLay(geometry[geometriesData.key].graphical)
|
||||||
|
} catch (e) {
|
||||||
|
// 删除旧图形失败
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
map.addOverLay(circle)
|
||||||
|
|
||||||
|
// 保存几何数据的引用,确保在事件监听器中可用
|
||||||
|
const circleData = { ...geometriesData }
|
||||||
|
|
||||||
|
// 保存上一次的中心点和半径
|
||||||
|
let preCenter = { ...circleData.center }
|
||||||
|
let preRadius = circleData.radius
|
||||||
|
|
||||||
|
// 监听地图触摸结束事件,检测圆形变化
|
||||||
|
const handleTouchEnd = () => {
|
||||||
|
if (!circle.isEditable()) return
|
||||||
|
|
||||||
|
if (geometry[circleData.key]?.editTimer) {
|
||||||
|
clearTimeout(geometry[circleData.key].editTimer)
|
||||||
|
}
|
||||||
|
|
||||||
|
geometry[circleData.key].editTimer = setTimeout(() => {
|
||||||
|
const curCenter = circle.getCenter()
|
||||||
|
const curRadius = circle.getRadius()
|
||||||
|
|
||||||
|
// 中心点改变了
|
||||||
|
if (curCenter.lng !== preCenter.lng || curCenter.lat !== preCenter.lat) {
|
||||||
|
circleData.center = { lat: curCenter.lat, lng: curCenter.lng }
|
||||||
|
preCenter = { ...circleData.center }
|
||||||
|
// console.log('圆形中心点改变了', circleData.center)
|
||||||
|
// 调用回调函数
|
||||||
|
if (onChange) {
|
||||||
|
onChange(circleData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 半径改变了
|
||||||
|
if (curRadius !== preRadius) {
|
||||||
|
circleData.radius = curRadius
|
||||||
|
preRadius = curRadius
|
||||||
|
// console.log('圆形半径改变了', curRadius)
|
||||||
|
// 调用回调函数
|
||||||
|
if (onChange) {
|
||||||
|
onChange(circleData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 200)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听圆形点击事件
|
||||||
|
const handleClick = () => {
|
||||||
|
// 调用选择回调函数
|
||||||
|
if (onSelect) {
|
||||||
|
onSelect(circleData.key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始时不启用编辑功能,只有在选中时才启用
|
||||||
|
// circle.enableEdit()
|
||||||
|
|
||||||
|
// 添加事件监听
|
||||||
|
map.addEventListener('touchend', handleTouchEnd)
|
||||||
|
map.addEventListener('mouseup', handleTouchEnd)
|
||||||
|
circle.addEventListener('click', handleClick)
|
||||||
|
|
||||||
|
geometry[circleData.key] = { graphical: circle, handleTouchEnd, handleClick, originalStyle }
|
||||||
|
return circle
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在天地图上创建一个矩形
|
||||||
|
* @param map 天地图实例
|
||||||
|
* @param geometriesData 矩形数据
|
||||||
|
*/
|
||||||
|
export const createRectangle = (map: any, geometriesData: any) => {
|
||||||
|
const T = (window as any).T
|
||||||
|
if (!T) return null
|
||||||
|
|
||||||
|
const { lat, lng } = map.getCenter()
|
||||||
|
|
||||||
|
geometriesData.bounds = geometriesData.bounds ?? {
|
||||||
|
southwest: { lat: lat - 0.005, lng: lng - 0.005 },
|
||||||
|
northeast: { lat: lat + 0.005, lng: lng + 0.005 }
|
||||||
|
}
|
||||||
|
|
||||||
|
const lngLats = [
|
||||||
|
new T.LngLat(geometriesData.bounds.southwest.lng, geometriesData.bounds.southwest.lat),
|
||||||
|
new T.LngLat(geometriesData.bounds.northeast.lng, geometriesData.bounds.southwest.lat),
|
||||||
|
new T.LngLat(geometriesData.bounds.northeast.lng, geometriesData.bounds.northeast.lat),
|
||||||
|
new T.LngLat(geometriesData.bounds.southwest.lng, geometriesData.bounds.northeast.lat)
|
||||||
|
]
|
||||||
|
const color = [
|
||||||
|
Math.floor(Math.random() * 255),
|
||||||
|
Math.floor(Math.random() * 255),
|
||||||
|
Math.floor(Math.random() * 255)
|
||||||
|
]
|
||||||
|
|
||||||
|
const rectangle = new T.Polygon(lngLats, {
|
||||||
|
color: geometriesData.color || `rgba(${color.toString()}, .4)`,
|
||||||
|
weight: geometriesData.weight || 3,
|
||||||
|
opacity: geometriesData.opacity || 0.8,
|
||||||
|
fillColor: geometriesData.fillColor || `rgba(${color.toString()}, .4)`,
|
||||||
|
fillOpacity: geometriesData.fillOpacity || 0.3
|
||||||
|
})
|
||||||
|
|
||||||
|
map.addOverLay(rectangle)
|
||||||
|
geometry[geometriesData.key] = { graphical: rectangle }
|
||||||
|
return rectangle
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在天地图上创建一个多边形
|
||||||
|
* @param map 天地图实例
|
||||||
|
* @param geometriesData 多边形数据
|
||||||
|
* @param onChange 图形变化时的回调函数
|
||||||
|
* @param onSelect 图形被选中时的回调函数
|
||||||
|
*/
|
||||||
|
export const createPolygon = (map: any, geometriesData: any, onChange?: (data: any) => void, onSelect?: (key: string) => void) => {
|
||||||
|
const T = (window as any).T
|
||||||
|
if (!T) return null
|
||||||
|
|
||||||
|
const { lat, lng } = map.getCenter()
|
||||||
|
|
||||||
|
geometriesData.paths = geometriesData.paths ?? [
|
||||||
|
{ lat: lat + 0.01, lng: lng + 0.01 },
|
||||||
|
{ lat: lat - 0.01, lng: lng + 0.01 },
|
||||||
|
{ lat: lat - 0.01, lng: lng - 0.01 },
|
||||||
|
{ lat: lat + 0.01, lng: lng - 0.01 }
|
||||||
|
]
|
||||||
|
|
||||||
|
const lngLats = geometriesData.paths.map((item: any) => new T.LngLat(item.lng, item.lat))
|
||||||
|
const color = [
|
||||||
|
Math.floor(Math.random() * 255),
|
||||||
|
Math.floor(Math.random() * 255),
|
||||||
|
Math.floor(Math.random() * 255)
|
||||||
|
]
|
||||||
|
// 保存原始样式
|
||||||
|
const originalStyle = {
|
||||||
|
color: geometriesData.color || `rgba(${color.toString()}, .4)`,
|
||||||
|
weight: geometriesData.weight || 3,
|
||||||
|
opacity: geometriesData.opacity || 0.8,
|
||||||
|
fillColor: geometriesData.fillColor || `rgba(${color.toString()}, .4)`,
|
||||||
|
fillOpacity: geometriesData.fillOpacity || 0.3
|
||||||
|
}
|
||||||
|
const polygon = new T.Polygon(lngLats, originalStyle)
|
||||||
|
|
||||||
|
// 如果已存在该图形,先删除
|
||||||
|
if (geometry[geometriesData.key]) {
|
||||||
|
try {
|
||||||
|
if (geometry[geometriesData.key].editTimer) {
|
||||||
|
clearTimeout(geometry[geometriesData.key].editTimer)
|
||||||
|
}
|
||||||
|
map.removeOverLay(geometry[geometriesData.key].graphical)
|
||||||
|
} catch (e) {
|
||||||
|
// 删除旧图形失败
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
map.addOverLay(polygon)
|
||||||
|
|
||||||
|
// 保存上一次的路径
|
||||||
|
let prePaths = JSON.parse(JSON.stringify(geometriesData.paths))
|
||||||
|
|
||||||
|
// 保存几何数据的引用,确保在事件监听器中可用
|
||||||
|
const polygonData = { ...geometriesData }
|
||||||
|
|
||||||
|
// 监听地图触摸结束事件,检测多边形变化
|
||||||
|
const handleTouchEnd = () => {
|
||||||
|
if (!polygon.isEditable()) return
|
||||||
|
|
||||||
|
if (geometry[polygonData.key]?.editTimer) {
|
||||||
|
clearTimeout(geometry[polygonData.key].editTimer)
|
||||||
|
}
|
||||||
|
|
||||||
|
geometry[polygonData.key].editTimer = setTimeout(() => {
|
||||||
|
try {
|
||||||
|
const curLngLats = polygon.getLngLats()
|
||||||
|
|
||||||
|
// 确保 curLngLats 是数组
|
||||||
|
if (Array.isArray(curLngLats)) {
|
||||||
|
// 检查 curLngLats[0] 是否是数组(多边形可能返回嵌套数组)
|
||||||
|
let pathsArray = curLngLats
|
||||||
|
if (Array.isArray(curLngLats[0])) {
|
||||||
|
pathsArray = curLngLats[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
const curPaths = pathsArray.map((lngLat: any) => {
|
||||||
|
// 检查 lngLat 是否是数组 [lng, lat]
|
||||||
|
if (Array.isArray(lngLat)) {
|
||||||
|
return {
|
||||||
|
lat: lngLat[1],
|
||||||
|
lng: lngLat[0]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 否则,假设是对象 { lat, lng }
|
||||||
|
return {
|
||||||
|
lat: lngLat.lat,
|
||||||
|
lng: lngLat.lng
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 检查路径是否改变
|
||||||
|
const pathsChanged = JSON.stringify(curPaths) !== JSON.stringify(prePaths)
|
||||||
|
|
||||||
|
if (pathsChanged) {
|
||||||
|
polygonData.paths = curPaths
|
||||||
|
prePaths = JSON.parse(JSON.stringify(polygonData.paths))
|
||||||
|
// 调用回调函数
|
||||||
|
if (onChange) {
|
||||||
|
onChange(polygonData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('curLngLats 不是数组:', curLngLats)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取多边形路径失败:', error)
|
||||||
|
}
|
||||||
|
}, 200)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听多边形点击事件
|
||||||
|
const handleClick = () => {
|
||||||
|
// 调用选择回调函数
|
||||||
|
if (onSelect) {
|
||||||
|
onSelect(polygonData.key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始时不启用编辑功能,只有在选中时才启用
|
||||||
|
// polygon.enableEdit()
|
||||||
|
|
||||||
|
// 添加事件监听
|
||||||
|
map.addEventListener('touchend', handleTouchEnd)
|
||||||
|
map.addEventListener('mouseup', handleTouchEnd)
|
||||||
|
polygon.addEventListener('click', handleClick)
|
||||||
|
|
||||||
|
geometry[polygonData.key] = { graphical: polygon, handleTouchEnd, handleClick, originalStyle }
|
||||||
|
return polygon
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除图形
|
||||||
|
* @param map 地图实例
|
||||||
|
* @param key 图形key
|
||||||
|
*/
|
||||||
|
export const deleteGeometry = (map: any, key: string) => {
|
||||||
|
if (!geometry[key]) return
|
||||||
|
try {
|
||||||
|
const T = (window as any).T
|
||||||
|
if (T && geometry[key].graphical) {
|
||||||
|
// 清除定时器
|
||||||
|
if (geometry[key].editTimer) {
|
||||||
|
clearTimeout(geometry[key].editTimer)
|
||||||
|
}
|
||||||
|
// 移除事件监听器
|
||||||
|
if (geometry[key].handleTouchEnd) {
|
||||||
|
map.removeEventListener('touchend', geometry[key].handleTouchEnd)
|
||||||
|
map.removeEventListener('mouseup', geometry[key].handleTouchEnd)
|
||||||
|
}
|
||||||
|
// 移除点击事件监听器
|
||||||
|
if (geometry[key].handleClick) {
|
||||||
|
geometry[key].graphical.removeEventListener('click', geometry[key].handleClick)
|
||||||
|
}
|
||||||
|
// 移除覆盖物
|
||||||
|
map.removeOverLay(geometry[key].graphical)
|
||||||
|
}
|
||||||
|
delete geometry[key]
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('删除图形失败:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空所有图形
|
||||||
|
* @param map 地图实例
|
||||||
|
*/
|
||||||
|
export const clearAllGeometry = (map: any) => {
|
||||||
|
const T = (window as any).T
|
||||||
|
if (T) {
|
||||||
|
// 先清除所有图形的事件监听器和定时器
|
||||||
|
Object.keys(geometry).forEach(key => {
|
||||||
|
if (geometry[key].editTimer) {
|
||||||
|
clearTimeout(geometry[key].editTimer)
|
||||||
|
}
|
||||||
|
if (geometry[key].handleTouchEnd) {
|
||||||
|
map.removeEventListener('touchend', geometry[key].handleTouchEnd)
|
||||||
|
map.removeEventListener('mouseup', geometry[key].handleTouchEnd)
|
||||||
|
}
|
||||||
|
if (geometry[key].handleClick) {
|
||||||
|
geometry[key].graphical.removeEventListener('click', geometry[key].handleClick)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// 一次性清空地图上的所有覆盖物
|
||||||
|
map.clearOverLays()
|
||||||
|
}
|
||||||
|
// 清空几何对象存储
|
||||||
|
Object.keys(geometry).forEach(key => delete geometry[key])
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选中图形
|
||||||
|
* @param key 图形key
|
||||||
|
*/
|
||||||
|
export const selectGeometry = (key: string) => {
|
||||||
|
if (!geometry[key]?.graphical) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 先取消所有图形的选中状态
|
||||||
|
deselectAllGeometry()
|
||||||
|
|
||||||
|
// 设置选中状态
|
||||||
|
geometry[key].graphical.setSelected?.(true)
|
||||||
|
|
||||||
|
// 启用当前图形的编辑功能
|
||||||
|
geometry[key].graphical.enableEdit()
|
||||||
|
|
||||||
|
const color = [
|
||||||
|
Math.floor(Math.random() * 255),
|
||||||
|
Math.floor(Math.random() * 255),
|
||||||
|
Math.floor(Math.random() * 255)
|
||||||
|
]
|
||||||
|
|
||||||
|
// 修改样式,使其显示为选中状态
|
||||||
|
geometry[key].graphical.setStyle({
|
||||||
|
color: '#1890ff', // 选中时的边框颜色
|
||||||
|
weight: 2, // 选中时的边框宽度
|
||||||
|
opacity: 1, // 选中时的边框透明度
|
||||||
|
fillColor: `rgba(${color.toString()}, .6)`, // 选中时的填充颜色
|
||||||
|
fillOpacity: 1// 选中时的填充透明度
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
console.error('选中图形失败:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消选中所有图形
|
||||||
|
*/
|
||||||
|
export const deselectAllGeometry = () => {
|
||||||
|
Object.keys(geometry).forEach(key => {
|
||||||
|
try {
|
||||||
|
// 取消选中状态
|
||||||
|
geometry[key]?.graphical?.setSelected?.(false)
|
||||||
|
|
||||||
|
// 禁用编辑功能
|
||||||
|
geometry[key]?.graphical?.disableEdit?.()
|
||||||
|
|
||||||
|
// 恢复原始样式
|
||||||
|
if (geometry[key]?.originalStyle) {
|
||||||
|
geometry[key].graphical.setStyle(geometry[key].originalStyle)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('取消选中图形失败:', e)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建点标记
|
||||||
|
* @param map 天地图实例
|
||||||
|
* @returns 标记实例
|
||||||
|
*/
|
||||||
|
export const createMarker = (map: any) => {
|
||||||
|
const T = (window as any).T
|
||||||
|
if (!T) return null
|
||||||
|
|
||||||
|
const { lat, lng } = map.getCenter()
|
||||||
|
const marker = new T.Marker(new T.LngLat(lng, lat))
|
||||||
|
map.addOverLay(marker)
|
||||||
|
return marker
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 逆地址解析
|
||||||
|
* @param params 参数 { mapKey, lat, lng }
|
||||||
|
* @returns Promise
|
||||||
|
*/
|
||||||
|
export const latLngToAddress = (params: any) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const T = (window as any).T
|
||||||
|
if (!T) {
|
||||||
|
reject(new Error('天地图服务未加载'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const geocoder = new T.Geocoder()
|
||||||
|
const lngLat = new T.LngLat(params.lng, params.lat)
|
||||||
|
|
||||||
|
geocoder.getLocation(lngLat, (result: any) => {
|
||||||
|
if (result && result.status === '0') {
|
||||||
|
const addrComp = result.addressComponent || {}
|
||||||
|
let detailAddress = ''
|
||||||
|
|
||||||
|
if (result.formattedAddress) {
|
||||||
|
detailAddress = result.formattedAddress
|
||||||
|
} else if (result.address) {
|
||||||
|
detailAddress = result.address
|
||||||
|
} else {
|
||||||
|
const parts = []
|
||||||
|
if (addrComp.road) parts.push(addrComp.road)
|
||||||
|
if (addrComp.address) parts.push(addrComp.address)
|
||||||
|
if (addrComp.poi) parts.push(addrComp.poi)
|
||||||
|
detailAddress = parts.join('')
|
||||||
|
}
|
||||||
|
|
||||||
|
const addressData = {
|
||||||
|
location: { lat: params.lat, lng: params.lng },
|
||||||
|
address: addrComp ?
|
||||||
|
`${ addrComp.province || '' }${ addrComp.city || '' }${ addrComp.county || '' }` : '',
|
||||||
|
formatted_addresses: {
|
||||||
|
recommend: detailAddress
|
||||||
|
},
|
||||||
|
address_component: {
|
||||||
|
province: addrComp.province || '',
|
||||||
|
city: addrComp.city || '',
|
||||||
|
district: addrComp.county || '',
|
||||||
|
street: addrComp.road || '',
|
||||||
|
street_number: addrComp.address || ''
|
||||||
|
},
|
||||||
|
addressComponent: addrComp,
|
||||||
|
formatted_address: detailAddress
|
||||||
|
}
|
||||||
|
resolve(addressData)
|
||||||
|
} else {
|
||||||
|
reject(new Error(result?.msg || '地址解析失败'))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 地址解析
|
||||||
|
* @param params 参数 { mapKey, address }
|
||||||
|
* @returns Promise
|
||||||
|
*/
|
||||||
|
export const addressToLatLng = (params: any) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const T = (window as any).T
|
||||||
|
if (!T) {
|
||||||
|
reject(new Error('天地图服务未加载'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const geocoder = new T.Geocoder()
|
||||||
|
geocoder.getPoint(params.address, (result: any) => {
|
||||||
|
if (result && result.status === '0' && result.location) {
|
||||||
|
const lat = result.location.lat
|
||||||
|
const lng = result.location.lon || result.location.lng
|
||||||
|
if (lat && lng) {
|
||||||
|
resolve({ lat, lng })
|
||||||
|
} else {
|
||||||
|
reject(new Error('坐标解析失败'))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
reject(new Error(result?.msg || '未找到该地址'))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
81
admin/vite.config.addon.ts
Normal file
81
admin/vite.config.addon.ts
Normal 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] }),
|
||||||
|
// 注入 CSS:lib 模式 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
77
admin/vite.config.core.ts
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user