feat: 新增管理端demo代码

feat: 补充遗漏的文件

fix: 移除license
This commit is contained in:
parisma 2022-03-11 15:21:32 +08:00 committed by jia000
parent 66eb52f8da
commit 2bfb85bdbf
109 changed files with 36582 additions and 0 deletions

View File

@ -34,6 +34,14 @@ const sidebar = {
]
}
],
admin: [
{
text: '管理端Demo',
children: [
'/admin/introduction',
]
}
],
component: [
{
text: '组件开发',
@ -88,6 +96,10 @@ export default defineUserConfig<DefaultThemeOptions>({
text: '页面发布',
link: '/page/introduction'
},
{
text: '管理端Demo',
link: '/admin/introduction'
},
]
},
{

View File

@ -0,0 +1,227 @@
# 介绍
我们提供了与编辑器表单配套的管理端供开发者直接使用。管理端magic-admin代码存放于[开源仓库](https://github.com/Tencent/tmagic-editor)"magic-admin"目录下,可作为一个独立项目运行。我们提供这个管理端一方面期望开发者可以更清晰的了解一个活动从编辑到生成的整个流程,另一方面,开发者也可以
在 magic-admin 的基础上快速搭建适合自己业务的管理平台。
管理端提供了如下能力:
- 活动列表展示,查询
- 活动创建,复制
- 活动编辑以及 AB TEST 配置能力
- 活动发布以及发布状态查看和管理
<img src="https://vfiles.gtimg.cn/vupload/20211129/81d34a1638168945248.png">
## 快速开始
::: tip
前提条件node 环境>=14.15
:::
1、首先 clone 开源仓库代码到本地
2、执行如下命令运行管理端
```bash
$ cd magic
$ npm run admin:run
```
3、访问 http://localhost:80
## 开发调试
magic-admin 管理端分为 web 端和 server 端,目录结构如下:
**web 目录结构**
```
.
├── babel.config.js
├── jest.config.js
├── package.json
├── package-lock.json
├── public
│   ├── favicon.ico
│   └── index.html
├── README.md
├── src
│   ├── apiweb 端接口文件)
│   ├── App.vue
│   ├── assets
│   ├── components组件文件
│   ├── config表单和状态配置文件
│   ├── main.ts入口文件
│   ├── plugins插件
│   ├── router路由
│   ├── shims-vue.d.ts
│   ├── store全局变量的封装
│   ├── typings
│   ├── use核心逻辑
│   ├── util公共方法
│   └── views
├── tests
│   ├── unit测试用例文件
│   └── utils.ts
├── tsconfig.json
├── types声明文件
│   ├── axios-jsonp
│   ├── index.d.ts
│   └── shims-vue.d.ts
└── vue.config.js
```
**server 目录结构**
```
.
├── jest.config.ts
├── package.json
├── package-lock.json
├── pm2.config.js
├── src
│   ├── config配置文件
│   ├── controller控制器
│   ├── database数据库初始化 sql 文件)
│   ├── index.ts入口文件
│   ├── models数据库模型定义使用`sequelize`
│   ├── routers路由文件
│   ├── sequelize数据库实例初始化文件
│   ├── serviceservice 文件)
│   ├── template发布所需模板文件
│   ├── typings声明文件
│   └── utils公共方法文件
├── tests
│   └── unit测试用例
└── tsconfig.json
```
**开发者本地调试 magic-admin 请按照如下步骤:**
- 数据库:
我们在 magic-admin/server/src/database/init.sql 中准备了库表初始化文件,开发者首先需要创建所需数据表
- 表名magic_act_info
活动基础信息表,包含活动 ID活动名称活动负责人活动时间等活动基础信息。
- 表名magic_ui_config
页面配置表magic-admin 支持了一个活动中包含多个活动页面的能力,因此每个页面的组件配置信息将分别存储。
- 启动 web 端:
```bash
$ cd magic-admin/web
$ npm i
$ npm run serve
```
- 启动 server 端
```bash
$ cd magic-admin/server
$ npm i
$ npm run dev
```
server 文件夹下面这些敏感文件,需要开发者参考示例进行替换:
```
.
├── src
│   ├── config
│   │   ├── databaseExample.ts数据库配置文件
│   │   ├── keyExample.ts加密秘钥配置
```
- 关于登陆态:
magic-admin 在库表中为开发者预留了用户信息字段(活动负责人),开发者可以根据自身业务需要,实现用户登陆态
```js
// web/src/App.vue
watchEffect(async () => {
// 登陆态获取交由开发者实现
const userName = process.env.VUE_APP_USER_NAME || "defaultName";
Cookies.set("userName", userName);
});
```
## 管理端能力
- **活动状态**
我们将活动的状态分为三种:修改中,部分页面已发布,全部页面已发布。在活动列表页面,可以展开查看每个活动页面的状态。
修改中:活动所有页面均在编辑状态
部分已发布:活动的一些页面在编辑状态,一些页面已发布
已发布:活动所有页面均已发布
- **在管理端引入 runtime**
在管理端中我们提供了一个可视化的模拟画布,他需要依赖 runtime 核心库,因此我们需要先在 magic 根目录下运行
```js
cd magic
npm run build
```
将 /playground/dist/runtime 文件夹复制到 /magic-admin/web/public 和 /magic-admin/server/assets 目录下。web 下的 runtime 提供给模拟画布使用server 下的 runtime 提供给发布后的页面来使用。
上面的操作我们提供了/magic-admin/setup.sh 脚本文件来实现,开发者可以参考该脚本文件来搭建流水线。
[runtime 详细介绍](https://tencent.github.io/tmagic-editor/docs/page/introduction.html#runtime)
- **AB TEST**
当活动开发者需要对页面进行 AB TEST 测试时,可以在活动中创建多个活动页面,并且在活动配置中进行配置
<img src="https://vfiles.gtimg.cn/vupload/20211129/c11fa81638173475771.png">
这里仅为管理端的配置,通过这里我们将在活动配置文件中得到类似如下结构的 abtest 信息,开发者可以在页面加载时根据 uiconfig 的 abtest 字段进行判断。
```js
abTest: [
{
name: "abtest1",
type: ["pgv_pvid"],
pageList: [
{
pageName: "page_1",
proportion: "50",
},
{
pageName: "page_2",
proportion: "50",
},
],
},
];
```
- **活动保存**
活动创建之后的配置信息分为两部份:活动基础信息,页面组件配置信息。
活动基础信息是整个活动共用的配置项,对应 magic_act_info 数据表
页面组件配置信息是一个活动中单个页面的配置项,对应 magic_ui_config 数据表
活动基础信息不在 magic-editor 支持范围以内,需要开发者自行结合 magic-form 按需开发。管理端示例中这部分内容在页面右上角【活动配置】抽屉页
页面组件配置信息是指 magic-editor 中的 modelValue他是一份 js schema 包含了页面内组件的配置内容,也是页面渲染的关键依赖文件。[uiconfig 概念参考](https://tencent.github.io/tmagic-editor/docs/page/introduction.html#%E7%BC%96%E8%BE%91%E5%99%A8%E4%BA%A7%E7%89%A9-uiconfig)
magic-admin 支持一个活动中创建多个活动页面的能力,因此,在活动保存的时候,我们的做法是将每个页面单独作为一条记录保存,比如活动 A 中包含页面 1 和页面 2
保存之后我们将得到
magic_act_info 表
| act_id | act_name | operator | act_status | abtest_raw | ... |
| --- | --- | --- | --- |--- |--- |
| 123 |活动 A | username | 修改中| []|... |
magic_ui_config 表
| id | act_id | c_dist_code | page_title | page_publish_status |... |
| --- | --- | --- | --- |--- | --- |
| 1 |123 | 页面 1 的 uiconfig 配置 |页面 1 |修改中|...|
| 2 |123 | 页面 2 的 uiconfig 配置 |页面 2|修改中|...|
- **活动发布**
管理端的活动发布是对[页面发布](https://tencent.github.io/tmagic-editor/docs/page/introduction.html#%E9%A1%B5%E9%9D%A2%E5%8F%91%E5%B8%83) 的实践。
原始的页面框架 page.html 需要通过 runtime 打包生成,注入的 uiconfig 保存在 magic_ui_config 表 c_dist_code 字段中。
发布时将 uiconfig 文件注入到 page.html 中,写入 server/assets/publish 目录下,访问路径: http://localhost/pubish/${page_name}.html

227
magic-admin/README.md Normal file
View File

@ -0,0 +1,227 @@
# 介绍
我们提供了与编辑器表单配套的管理端供开发者直接使用。管理端magic-admin代码存放于[开源仓库](https://github.com/Tencent/tmagic-editor)"magic-admin"目录下,可作为一个独立项目运行。我们提供这个管理端一方面期望开发者可以更清晰的了解一个活动从编辑到生成的整个流程,另一方面,开发者也可以
在 magic-admin 的基础上快速搭建适合自己业务的管理平台。
管理端提供了如下能力:
- 活动列表展示,查询
- 活动创建,复制
- 活动编辑以及 AB TEST 配置能力
- 活动发布以及发布状态查看和管理
<img src="https://vfiles.gtimg.cn/vupload/20211129/81d34a1638168945248.png">
## 快速开始
::: tip
前提条件node 环境>=14.15
:::
1、首先 clone 开源仓库代码到本地
2、执行如下命令运行管理端
```bash
$ cd magic
$ npm run admin:run
```
3、访问 http://localhost:80
## 开发调试
magic-admin 管理端分为 web 端和 server 端,目录结构如下:
**web 目录结构**
```
.
├── babel.config.js
├── jest.config.js
├── package.json
├── package-lock.json
├── public
│   ├── favicon.ico
│   └── index.html
├── README.md
├── src
│   ├── apiweb 端接口文件)
│   ├── App.vue
│   ├── assets
│   ├── components组件文件
│   ├── config表单和状态配置文件
│   ├── main.ts入口文件
│   ├── plugins插件
│   ├── router路由
│   ├── shims-vue.d.ts
│   ├── store全局变量的封装
│   ├── typings
│   ├── use核心逻辑
│   ├── util公共方法
│   └── views
├── tests
│   ├── unit测试用例文件
│   └── utils.ts
├── tsconfig.json
├── types声明文件
│   ├── axios-jsonp
│   ├── index.d.ts
│   └── shims-vue.d.ts
└── vue.config.js
```
**server 目录结构**
```
.
├── jest.config.ts
├── package.json
├── package-lock.json
├── pm2.config.js
├── src
│   ├── config配置文件
│   ├── controller控制器
│   ├── database数据库初始化 sql 文件)
│   ├── index.ts入口文件
│   ├── models数据库模型定义使用`sequelize`
│   ├── routers路由文件
│   ├── sequelize数据库实例初始化文件
│   ├── serviceservice 文件)
│   ├── template发布所需模板文件
│   ├── typings声明文件
│   └── utils公共方法文件
├── tests
│   └── unit测试用例
└── tsconfig.json
```
**开发者本地调试 magic-admin 请按照如下步骤:**
- 数据库:
我们在 magic-admin/server/src/database/init.sql 中准备了库表初始化文件,开发者首先需要创建所需数据表
- 表名magic_act_info
活动基础信息表,包含活动 ID活动名称活动负责人活动时间等活动基础信息。
- 表名magic_ui_config
页面配置表magic-admin 支持了一个活动中包含多个活动页面的能力,因此每个页面的组件配置信息将分别存储。
- 启动 web 端:
```bash
$ cd magic-admin/web
$ npm i
$ npm run serve
```
- 启动 server 端
```bash
$ cd magic-admin/server
$ npm i
$ npm run dev
```
server 文件夹下面这些敏感文件,需要开发者参考示例进行替换:
```
.
├── src
│   ├── config
│   │   ├── databaseExample.ts数据库配置文件
│   │   ├── keyExample.ts加密秘钥配置
```
- 关于登陆态:
magic-admin 在库表中为开发者预留了用户信息字段(活动负责人),开发者可以根据自身业务需要,实现用户登陆态
```js
// web/src/App.vue
watchEffect(async () => {
// 登陆态获取交由开发者实现
const userName = process.env.VUE_APP_USER_NAME || "defaultName";
Cookies.set("userName", userName);
});
```
## 管理端能力
- **活动状态**
我们将活动的状态分为三种:修改中,部分页面已发布,全部页面已发布。在活动列表页面,可以展开查看每个活动页面的状态。
修改中:活动所有页面均在编辑状态
部分已发布:活动的一些页面在编辑状态,一些页面已发布
已发布:活动所有页面均已发布
- **在管理端引入 runtime**
在管理端中我们提供了一个可视化的模拟画布,他需要依赖 runtime 核心库,因此我们需要先在 magic 根目录下运行
```js
cd magic
npm run build
```
将 /playground/dist/runtime 文件夹复制到 /magic-admin/web/public 和 /magic-admin/server/assets 目录下。web 下的 runtime 提供给模拟画布使用server 下的 runtime 提供给发布后的页面来使用。
上面的操作我们提供了/magic-admin/setup.sh 脚本文件来实现,开发者可以参考该脚本文件来搭建流水线。
[runtime 详细介绍](https://tencent.github.io/tmagic-editor/docs/page/introduction.html#runtime)
- **AB TEST**
当活动开发者需要对页面进行 AB TEST 测试时,可以在活动中创建多个活动页面,并且在活动配置中进行配置
<img src="https://vfiles.gtimg.cn/vupload/20211129/c11fa81638173475771.png">
这里仅为管理端的配置,通过这里我们将在活动配置文件中得到类似如下结构的 abtest 信息,开发者可以在页面加载时根据 uiconfig 的 abtest 字段进行判断。
```js
abTest: [
{
name: "abtest1",
type: ["pgv_pvid"],
pageList: [
{
pageName: "page_1",
proportion: "50",
},
{
pageName: "page_2",
proportion: "50",
},
],
},
];
```
- **活动保存**
活动创建之后的配置信息分为两部份:活动基础信息,页面组件配置信息。
活动基础信息是整个活动共用的配置项,对应 magic_act_info 数据表
页面组件配置信息是一个活动中单个页面的配置项,对应 magic_ui_config 数据表
活动基础信息不在 magic-editor 支持范围以内,需要开发者自行结合 magic-form 按需开发。管理端示例中这部分内容在页面右上角【活动配置】抽屉页
页面组件配置信息是指 magic-editor 中的 modelValue他是一份 js schema 包含了页面内组件的配置内容,也是页面渲染的关键依赖文件。[uiconfig 概念参考](https://tencent.github.io/tmagic-editor/docs/page/introduction.html#%E7%BC%96%E8%BE%91%E5%99%A8%E4%BA%A7%E7%89%A9-uiconfig)
magic-admin 支持一个活动中创建多个活动页面的能力,因此,在活动保存的时候,我们的做法是将每个页面单独作为一条记录保存,比如活动 A 中包含页面 1 和页面 2
保存之后我们将得到
magic_act_info 表
| act_id | act_name | operator | act_status | abtest_raw | ... |
| --- | --- | --- | --- |--- |--- |
| 123 |活动 A | username | 修改中| []|... |
magic_ui_config 表
| id | act_id | c_dist_code | page_title | page_publish_status |... |
| --- | --- | --- | --- |--- | --- |
| 1 |123 | 页面 1 的 uiconfig 配置 |页面 1 |修改中|...|
| 2 |123 | 页面 2 的 uiconfig 配置 |页面 2|修改中|...|
- **活动发布**
管理端的活动发布是对[页面发布](https://tencent.github.io/tmagic-editor/docs/page/introduction.html#%E9%A1%B5%E9%9D%A2%E5%8F%91%E5%B8%83) 的实践。
原始的页面框架 page.html 需要通过 runtime 打包生成,注入的 uiconfig 保存在 magic_ui_config 表 c_dist_code 字段中。
发布时将 uiconfig 文件注入到 page.html 中,写入 server/assets/publish 目录下,访问路径: http://localhost/pubish/${page_name}.html

12
magic-admin/package.json Normal file
View File

@ -0,0 +1,12 @@
{
"name": "magic-admin",
"version": "1.0.0",
"description": "Magic Admin 可视化搭建平台管理端。magic-admin 为独立项目目录,分为 web 端和 server 端,请按照下面指引操作",
"main": "index.js",
"scripts": {
"init": "npm run web:install && npm run server:install",
"web:install": "cd web && npm install",
"server:install": "cd server && npm install"
},
"author": ""
}

View File

@ -0,0 +1,10 @@
{
"presets": [
[
"@babel/preset-env"
],
[
"@babel/preset-typescript"
]
]
}

View File

@ -0,0 +1,3 @@
dist
node_modules
pm2.config.js

View File

@ -0,0 +1,51 @@
module.exports = {
env: {
node: true,
browser: true,
},
globals: {
describe: true,
it: true,
expect: true,
jest: true,
beforeEach: true,
},
parser: '@typescript-eslint/parser',
extends: [
'eslint-config-tencent',
'eslint-config-tencent/ts',
'eslint-config-tencent/prettier',
],
plugins: [
'@typescript-eslint',
'simple-import-sort',
],
ignorePatterns: ['.eslintrc.js','/assets/*','/tests/*','/coverage/*'],
rules: {
'no-param-reassign': 'off',
'simple-import-sort/imports': [
'error', {
groups: [
// Node.js builtins. You could also generate this regex if you use a `.js` config.
// For example: `^(${require('module').builtinModules.join('|')})(/|$)`
[
'^(assert|buffer|child_process|cluster|console|constants|crypto|dgram|dns|domain|events|fs|http|https|module|net|os|path|punycode|querystring|readline|repl|stream|string_decoder|sys|timers|tls|tty|url|util|vm|zlib|freelist|v8|process|async_hooks|http2|perf_hooks)(/.*|$)',
],
// Packages. `react|vue` related packages come first.
['^(react|vue|vite)', '^@?\\w'],
['^(@tmagic)(/.*|$)'],
// Internal packages.
['^(@|@src|@tests)(/.*|$)'],
// Side effect imports.
['^\\u0000'],
// Parent imports. Put `..` last.
['^\\.\\.(?!/?$)', '^\\.\\./?$'],
// Other relative imports. Put same-folder imports and `.` last.
['^\\./(?=.*/)(?!/?$)', '^\\.(?!/?$)', '^\\./?$'],
// Style imports.
['^.+\\.s?css$'],
],
},
],
},
};

9
magic-admin/server/.gitignore vendored Normal file
View File

@ -0,0 +1,9 @@
node_modules
dist
yarn.lock
package.json.lock
coverage
.vscode
assets
src/config/database.ts
src/config/key.ts

View File

@ -0,0 +1,7 @@
export default {
collectCoverage: true,
coverageProvider: 'v8',
moduleNameMapper: {
'^@src/(.*)$': '<rootDir>/src/$1',
},
};

9886
magic-admin/server/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,75 @@
{
"name": "magic-admin",
"version": "1.0.0",
"description": "",
"tags": [
"orm",
"typescript",
"koa"
],
"scripts": {
"dev": "nodemon",
"lint": "eslint . --ext .js,.ts --fix",
"test": "jest"
},
"_moduleAliases": {
"@": "dist/src"
},
"nodemonConfig": {
"watch": [
"src"
],
"ext": "ts",
"env": {
"NODE_ENV": "development",
"PORT": 3001
},
"exec": "ts-node -r tsconfig-paths/register src/index.ts --files"
},
"dependencies": {
"koa": "^2.7.0",
"koa-bodyparser": "^4.2.1",
"koa-router": "^7.4.0",
"koa-send": "^5.0.1",
"log4js": "^6.3.0",
"mysql2": "^2.3.3",
"reflect-metadata": "^0.1.13",
"sequelize": "^6.6.2",
"sequelize-typescript": "^2.1.0"
},
"devDependencies": {
"@babel/core": "^7.15.0",
"@babel/preset-env": "^7.15.0",
"@babel/preset-typescript": "^7.15.0",
"@types/axios": "^0.14.0",
"@types/babel-core": "^6.25.7",
"@types/fs-extra": "^9.0.13",
"@types/jest": "^26.0.24",
"@types/koa": "^2.0.48",
"@types/koa-bodyparser": "^4.2.2",
"@types/koa-router": "^7.0.40",
"@types/koa-send": "^4.1.3",
"@types/node": "^12.0.0",
"@typescript-eslint/eslint-plugin": "^4.28.1",
"@typescript-eslint/parser": "^4.28.1",
"axios": "^0.24.0",
"babel-jest": "^27.0.6",
"eslint": "^7.29.0",
"eslint-config-tencent": "^1.0.2",
"eslint-plugin-prettier": "^3.4.0",
"eslint-plugin-simple-import-sort": "^7.0.0",
"fs-extra": "^10.0.0",
"jest": "^27.0.6",
"lodash": "^4.17.21",
"module-alias": "^2.2.2",
"moment": "^2.29.1",
"moment-timezone": "^0.5.33",
"nodemon": "^1.19.0",
"prettier": "^2.3.2",
"serialize-javascript": "^6.0.0",
"ts-node": "^8.1.0",
"tsconfig-paths": "^3.8.0",
"typescript": "^3.4.5",
"uglify-js": "^3.14.1"
}
}

View File

@ -0,0 +1,41 @@
/*
* Tencent is pleased to support the open source community by making MagicEditor available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import path from 'path';
// 活动状态
export enum ActStatus {
ALL = -1, // 查询传参使用:全部状态占位
MODIFYING, // 修改中
PART_PUBLISHED, // 部分页面已发布
PUBLISHED, // 全部页面已发布
}
// 页面状态
export enum PageStatus {
MODIFYING = 0, // 修改中
PUBLISHED, // 已预发布
}
// 静态资源根目录
export const StaticPath = {
ASSETS: path.resolve(__dirname, '../../assets'),
TEMPLATE: path.resolve(__dirname, '../template'),
PUBLISH: path.resolve(__dirname, '../../assets/publish'),
};
export const UiRuntimeJS = '<script src="https://unpkg.com/vue@next/dist/vue.runtime.global.js"></script>';

View File

@ -0,0 +1,27 @@
/*
* Tencent is pleased to support the open source community by making MagicEditor available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// 数据库配置文件需开发者自行替换并更名为database.ts
export default {
connectionLimit: 10,
host: '1.2.3.4',
port: 36000,
user: 'database_username',
password: 'database_password',
database: 'database_name',
};

View File

@ -0,0 +1,22 @@
/*
* Tencent is pleased to support the open source community by making MagicEditor available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// crypto加密key请开发者自行替换
export default {
key: 'crypto_algorithm_aes-256-cbc',
};

View File

@ -0,0 +1,120 @@
/*
* Tencent is pleased to support the open source community by making MagicEditor available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// 处理活动请求
import Koa from 'koa';
import ActService, { ActInfoDetail, ActListQuery, CopyInfo } from '@src/service/act';
class ActController {
private service: ActService = new ActService();
// 获取活动列表
getList = async (ctx: Koa.Context) => {
try {
const query: ActListQuery = JSON.parse(ctx.request.body.data);
const [actList, count] = await Promise.all([this.service.getActList(query), this.service.getCount(query)]);
ctx.body = {
data: actList,
total: count,
fetch: true,
errorMsg: '',
};
} catch (e) {
ctx.logger.error(e);
ctx.body = {
data: [],
total: 0,
fetch: false,
errorMsg: (e as Error).message,
};
}
};
// 新建活动
create = async (ctx: Koa.Context) => {
try {
const actInfo: ActInfoDetail = JSON.parse(ctx.request.body.data);
const actId = await this.service.create(actInfo);
ctx.body = {
ret: 0,
msg: '新建活动成功',
data: { actId },
};
} catch (e) {
ctx.body = {
ret: -1,
msg: (e as Error).message,
};
}
};
// 复制活动
copy = async (ctx: Koa.Context) => {
try {
const copyInfo: CopyInfo = JSON.parse(ctx.request.body.data);
await this.service.copy(copyInfo);
ctx.body = {
ret: 0,
msg: '复制成功',
};
} catch (e) {
ctx.body = {
ret: -1,
msg: (e as Error).message,
};
}
};
// 根据id查询活动详情
getInfo = async (ctx: Koa.Context) => {
try {
const id = Number(ctx.query.id);
const act = await this.service.getActInfo(id);
ctx.body = {
ret: 0,
msg: '获取活动信息成功',
data: act,
};
} catch (e) {
ctx.body = {
ret: -1,
msg: (e as Error).message,
};
}
};
// 根据页面id删除活动页面
removePage = async (ctx: Koa.Context) => {
try {
const { pageId } = JSON.parse(ctx.request.body.data);
await this.service.removePage(pageId);
ctx.body = {
ret: 0,
msg: '删除活动页面成功',
};
} catch (e) {
ctx.body = {
ret: -1,
msg: (e as Error).message,
};
}
};
}
export default new ActController();

View File

@ -0,0 +1,41 @@
/*
* Tencent is pleased to support the open source community by making MagicEditor available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// 处理编辑器请求
import Koa from 'koa';
import EditorService from '@src/service/editor';
class EditorController {
private service: EditorService = new EditorService();
// 拉取编辑器左侧展示的组件列表
getComponentList = async (ctx: Koa.Context) => {
ctx.body = {
ret: 0,
msg: '获取组件列表成功',
data: await this.service.getComponentList(),
};
};
// 拉取编辑器右边活动配置的web插件
getWebPlugins = async (ctx: Koa.Context) => {
ctx.body = await this.service.getWebPlugins();
};
}
export default new EditorController();

View File

@ -0,0 +1,65 @@
/*
* Tencent is pleased to support the open source community by making MagicEditor available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// 处理保存发布请求
import Koa from 'koa';
import PublishService from '@src/service/publish';
class PublishController {
private service: PublishService = new PublishService();
// 保存活动基础信息
saveActInfo = async (ctx: Koa.Context) => {
try {
// data不是真正的json对象可能包含组件自定义code代码
/* eslint-disable-next-line */
const { actInfo,rootInfo } = eval(`(${ctx.request.body.data})`);
const res = await this.service.saveActInfo({ actInfo, rootInfo });
ctx.body = {
ret: res.ret,
msg: res.msg,
};
} catch (e) {
ctx.body = {
ret: -1,
msg: (e as Error).message,
};
}
};
// 发布
publish = async (ctx: Koa.Context) => {
try {
// data不是真正的json对象可能包含组件自定义code代码
/* eslint-disable-next-line */
const { actId, publishPages,rootInfo } = eval(`(${ctx.request.body.data})`);
const operator = ctx.cookies.get('userName');
const res = await this.service.publish({ actId, publishPages, rootInfo, operator });
ctx.body = {
ret: res.ret,
msg: res.msg,
};
} catch (e) {
ctx.body = {
ret: -1,
msg: (e as Error).message,
};
}
};
}
export default new PublishController();

View File

@ -0,0 +1,34 @@
-- 活动基础信息表
CREATE TABLE `magic_act_info` (
`act_id` int(20) NOT NULL AUTO_INCREMENT COMMENT '活动id',
`act_crypto_id` varchar(128) NOT NULL COMMENT '活动加密ID',
`act_name` varchar(128) NOT NULL COMMENT '活动名称',
`act_begin_time` varchar(128) NOT NULL COMMENT '活动开始时间',
`act_end_time` varchar(128) NOT NULL COMMENT '活动结束时间',
`act_modify_time` varchar(128) DEFAULT NULL COMMENT '活动修改时间',
`act_create_time` varchar(128) NOT NULL COMMENT '活动创建时间',
`operator` varchar(512) DEFAULT NULL COMMENT '负责人',
`locker` varchar(128) DEFAULT NULL COMMENT '当前正在编辑的人',
`lock_time` datetime DEFAULT '0000-00-00 00:00:00' COMMENT '锁定时间',
`act_status` int(11) DEFAULT NULL COMMENT '活动状态0-修改中1-部分已发布2-已发布',
`abtest_raw` mediumtext COMMENT 'serialize后的abtest',
PRIMARY KEY (`act_id`),
KEY `act_name` (`act_name`)
) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8mb4 COMMENT = '魔方开源-活动信息表'
-- 页面配置表
CREATE TABLE `magic_ui_config` (
`id` int(8) unsigned NOT NULL AUTO_INCREMENT COMMENT '页面id',
`act_id` int(8) NOT NULL COMMENT '活动id',
`c_dist_code` mediumblob COMMENT 'babel编译后的config',
`c_src_code` mediumblob COMMENT 'config 源码',
`c_c_time` varchar(128) DEFAULT NULL COMMENT 'config创建时间',
`c_m_time` varchar(128) DEFAULT NULL COMMENT 'config修改时间',
`c_ui_version` varchar(64) DEFAULT NULL COMMENT 'magic-ui 版本',
`page_title` varchar(128) NOT NULL COMMENT '活动页面标题H5顶部展示',
`page_publish_time` varchar(128) DEFAULT NULL COMMENT '页面发布时间',
`page_publish_status` int(11) NOT NULL DEFAULT '0' COMMENT '页面发布状态修改中0已发布1',
`publish_operator` varchar(128) DEFAULT NULL COMMENT '发布人',
`web_plugin` varchar(255) DEFAULT NULL COMMENT 'web插件',
`page_name` varchar(128) DEFAULT NULL COMMENT '页面名称(编辑器页面唯一标识)',
PRIMARY KEY (`id`)
) ENGINE = InnoDB AUTO_INCREMENT = 5 DEFAULT CHARSET = utf8mb4 COMMENT = '魔方开源-uiconfig表'

View File

@ -0,0 +1,48 @@
/*
* Tencent is pleased to support the open source community by making MagicEditor available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Koa from 'koa';
import bodyParser from 'koa-bodyparser';
import { getLogger } from 'log4js';
import routers from '@src/routers';
import staticRouters from '@src/routers/static';
const app = new Koa();
const { PORT } = process.env;
app.use(
bodyParser({
formLimit: '10mb',
jsonLimit: '10mb',
}),
);
app.use(async (ctx, next) => {
ctx.logger = getLogger();
ctx.logger.level = 'debug';
await next();
});
app.use(async (ctx, next) => {
ctx.logger.debug(ctx.url);
await next();
});
// 初始化路由中间件
app.use(routers.routes()).use(routers.allowedMethods());
app.use(staticRouters.routes()).use(staticRouters.allowedMethods());
app.listen(PORT);
console.log(`server启动成功 端口:${PORT}`);

View File

@ -0,0 +1,73 @@
/*
* Tencent is pleased to support the open source community by making MagicEditor available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { AllowNull, Column, HasMany, Model, Table } from 'sequelize-typescript';
import { Page } from '@src/models/page';
// 活动基础信息表
@Table({
tableName: 'magic_act_info',
})
export class ActInfo extends Model<ActInfo> {
@Column({
primaryKey: true,
autoIncrement: true,
field: 'act_id',
})
actId: number;
@Column({ field: 'act_crypto_id' })
actCryptoId: string;
@Column({ field: 'act_name' })
actName: string;
@Column({ field: 'act_begin_time' })
actBeginTime: string;
@Column({ field: 'act_end_time' })
actEndTime: string;
@Column({ field: 'act_modify_time' })
actModifyTime: string;
@Column({ field: 'act_create_time' })
actCreateTime: string;
@AllowNull
@Column
operator?: string;
@AllowNull
@Column
locker?: string;
@Column({ field: 'lock_time' })
lockTime: string;
@AllowNull
@Column({ field: 'act_status' })
actStatus: number; // 0:修改中 1部分已发布 2已发布
@HasMany(() => Page)
pages: Page[];
@AllowNull
@Column({ field: 'abtest_raw' })
abTestRaw?: string;
}

View File

@ -0,0 +1,22 @@
/*
* Tencent is pleased to support the open source community by making MagicEditor available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// 模型汇总
import { ActInfo } from '@src/models/act';
import { Page } from '@src/models/page';
export default [ActInfo, Page];

View File

@ -0,0 +1,76 @@
/*
* Tencent is pleased to support the open source community by making MagicEditor available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { AllowNull, BelongsTo, Column, ForeignKey, Model, Table } from 'sequelize-typescript';
import { ActInfo } from '@src/models/act';
// 页面信息表
@Table({
tableName: 'magic_ui_config',
})
export class Page extends Model<Page> {
@Column({
primaryKey: true,
autoIncrement: true,
})
id: string;
@ForeignKey(() => ActInfo)
@Column({ field: 'act_id' })
actId: number;
@BelongsTo(() => ActInfo)
act: ActInfo;
@AllowNull
@Column({ field: 'c_dist_code' })
distCode: string;
@AllowNull
@Column({ field: 'c_src_code' })
srcCode: string;
@AllowNull
@Column({ field: 'c_c_time' })
pageCreateTime: string;
@AllowNull
@Column({ field: 'c_m_time' })
pageModifyTime: string;
@AllowNull
@Column({ field: 'c_ui_version' })
pagePublishUiVersion: string;
@Column({ field: 'page_title' })
pageTitle: string;
@Column({ field: 'page_name' })
pageName: string;
@AllowNull
@Column({ field: 'page_publish_time' })
pagePublishTime: string;
@Column({ field: 'page_publish_status' })
pagePublishStatus: number; // 0:修改中 1已发布
@AllowNull
@Column({ field: 'publish_operator' })
pagePublishOperator: string;
}

View File

@ -0,0 +1,40 @@
/*
* Tencent is pleased to support the open source community by making MagicEditor available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// 活动列表相关路由
import Router from 'koa-router';
import actController from '@src/controller/act';
const router = new Router();
// 拉取编辑器左侧展示的组件列表
router.post('/getList', actController.getList);
// 创建活动
router.post('/create', actController.create);
// 复制活动
router.post('/copy', actController.copy);
// 根据id获取活动信息
router.get('/get', actController.getInfo);
// 删除活动页面
router.post('/removePage', actController.removePage);
export default router;

View File

@ -0,0 +1,31 @@
/*
* Tencent is pleased to support the open source community by making MagicEditor available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// 编辑器相关路由
import Router from 'koa-router';
import editorController from '@src/controller/editor';
const router = new Router();
// 拉取编辑器左侧展示的组件列表
router.get('/getComponentList', editorController.getComponentList);
// 拉取活动配置的web插件
router.get('/getWebPlugins', editorController.getWebPlugins);
export default router;

View File

@ -0,0 +1,38 @@
/*
* Tencent is pleased to support the open source community by making MagicEditor available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// 整合所有子路由
import Router from 'koa-router';
import act from '@src/routers/act';
import editor from '@src/routers/editor';
import publish from '@src/routers/publish';
const router = new Router({
prefix: '/api',
});
// 编辑器相关路由
router.use('/editor', editor.routes(), editor.allowedMethods());
// 活动列表相关路由
router.use('/act', act.routes(), act.allowedMethods());
// 保存发布相关路由
router.use('/publish', publish.routes(), publish.allowedMethods());
export default router;

View File

@ -0,0 +1,31 @@
/*
* Tencent is pleased to support the open source community by making MagicEditor available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// 保存发布的相关路由
import Router from 'koa-router';
import PublishController from '@src/controller/publish';
const router = new Router();
// 保存活动基础信息
router.post('/saveActInfo', PublishController.saveActInfo);
// 发布
router.post('/publish', PublishController.publish);
export default router;

View File

@ -0,0 +1,41 @@
/*
* Tencent is pleased to support the open source community by making MagicEditor available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// web静态资源相关路由
import { pathExistsSync } from 'fs-extra';
import Router from 'koa-router';
import send from 'koa-send';
import { StaticPath } from '@src/config/config';
const router = new Router();
const options = { root: '/', gzip: true, maxage: 36000 };
router.get('/', async (ctx) => {
await send(ctx, `${StaticPath.ASSETS}/index.html`, options);
});
router.get('/*', async (ctx) => {
const file = `${StaticPath.ASSETS}/${ctx.params[0]}`;
if (pathExistsSync(file)) {
await send(ctx, file, options);
} else {
await send(ctx, `${StaticPath.ASSETS}/index.html`, options);
}
});
export default router;

View File

@ -0,0 +1,41 @@
/*
* Tencent is pleased to support the open source community by making MagicEditor available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Sequelize } from 'sequelize-typescript';
import sqlConf from '@src/config/database';
import models from '@src/models/index';
// 数据库初始化
export default class SequelizeHelper {
private static instance;
public static getInstance() {
if (!SequelizeHelper.instance) {
const sequelize = new Sequelize(sqlConf.database, sqlConf.user, sqlConf.password, {
host: sqlConf.host,
port: sqlConf.port,
dialect: 'mysql',
define: {
timestamps: false,
},
});
sequelize.addModels(models);
SequelizeHelper.instance = sequelize;
}
return SequelizeHelper.instance;
}
}

View File

@ -0,0 +1,380 @@
/*
* Tencent is pleased to support the open source community by making MagicEditor available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { cloneDeep } from 'lodash';
import { Op } from 'sequelize';
import { ActStatus } from '@src/config/config';
import { ActInfo } from '@src/models/act';
import { Page } from '@src/models/page';
import SequelizeHelper from '@src/sequelize/index';
import PageService from '@src/service/page';
import type { ABTest, ActBaseInfo, ActInfoIncludePage, PageInfo } from '@src/typings';
import Crypto from '@src/utils/crypto/crypto';
import { getFormatTime } from '@src/utils/index';
import logger from '@src/utils/logger';
export interface OrderItem {
columnName: string;
direction: string;
}
export interface ActListQuery {
where: {
onlySelf: boolean;
search: string;
pageTitle: string;
actStatus: number;
};
orderBy: OrderItem[];
pgIndex: number;
pgSize: number;
userName: string;
}
// 新建活动参数
export interface ActInfoDetail {
actName: string;
actBeginTime: string;
actEndTime: string;
operator: string;
}
// 复制活动参数
export interface CopyInfo {
actId: number;
userName: string;
}
export type FormatType = 'search' | 'actId' | 'status' | 'order' | 'title';
// 活动列表查询体的构建方法
const getActListQueryConstructor = ({ attributes = [], query }: { attributes?: string[]; query: ActListQuery }) => ({
attributes: [...attributes],
where: {
operator: { [Op.substring]: query.where.onlySelf ? query.userName : '' },
actStatus: formatQuery(query.where.actStatus, 'status'),
[Op.or]: [
{ actName: { [Op.substring]: formatQuery(query.where.search, 'search') } },
{ operator: { [Op.substring]: formatQuery(query.where.search, 'search') } },
{ actCryptoId: query.where.search },
formatQuery(query.where.search, 'actId'),
],
},
});
export default class ActService {
private pageService: PageService = new PageService();
/**
*
* @param {ActListQuery} query
* @returns {ActInfoIncludePage[]}
*/
getActList = async (query: ActListQuery) => {
SequelizeHelper.getInstance();
// 构建查询体
const attributes = ['actId', 'actName', 'actBeginTime', 'actEndTime', 'actStatus', 'operator', 'actCryptoId'];
const queryCond = getActListQueryConstructor({ attributes, query });
const actList = await ActInfo.findAll({
...queryCond,
order: formatQuery(query.orderBy[0], 'order'),
limit: query.pgSize,
offset: query.pgIndex * query.pgSize,
include: [
{
model: Page,
attributes: ['pageTitle', 'pagePublishTime', 'pagePublishStatus', 'pagePublishOperator'],
where: formatQuery(query.where.pageTitle, 'title') as {
pageTitle: {
[Op.substring]: string;
};
} | null,
},
],
});
const formatActList = actList.map((act) => this.formatPages(act));
return formatActList;
};
/**
*
* @param {ActListQuery} query
* @returns {number}
*/
getCount = async (query: ActListQuery) => {
SequelizeHelper.getInstance();
// 构建查询体
const queryCond = getActListQueryConstructor({ query });
const actList = await ActInfo.findAll({
...queryCond,
include: [
{
model: Page,
attributes: [],
where: formatQuery(query.where.pageTitle, 'title'),
},
],
});
return actList.length;
};
/**
* id查询活动详情
* @param {number} actId id
* @returns {ActInfoIncludePage}
*/
getActInfo = async (actId: number) => {
const act = await getActInfoHandler(actId);
const formatAct = this.formatPages(act);
let abTestArray: ABTest[] = [];
try {
if (formatAct.abTestRaw) {
abTestArray = JSON.parse(formatAct.abTestRaw);
}
formatAct.abTest = abTestArray;
delete formatAct.abTestRaw;
return formatAct;
} catch (error) {
logger.error(error);
throw new Error('根据id查询活动详情失败');
}
};
/**
*
* @param {ActInfoDetail} actInfo
* @returns {number} id
*/
create = async (actInfo: ActInfoDetail) => {
SequelizeHelper.getInstance();
// 新增活动
const newAct = {
actModifyTime: getFormatTime(),
actCreateTime: getFormatTime(),
actStatus: ActStatus.MODIFYING,
...actInfo,
};
const act = await ActInfo.create<ActInfo>(newAct as ActInfo);
// 更新加密id
const cryptoId = Crypto.encode(act.actId.toString());
await ActInfo.update(
{ actCryptoId: cryptoId },
{
where: { actId: act.actId },
},
);
// 添加默认活动页
const defaultPage = await this.pageService.create();
await act.$add<Page>('Pages', defaultPage);
return act.actId;
};
/**
*
* @param {CopyInfo} copyInfo
* @returns void
*/
copy = async (copyInfo: CopyInfo) => {
SequelizeHelper.getInstance();
const targetAct = await ActInfo.findOne({
where: { actId: copyInfo.actId },
include: Page,
});
if (!targetAct) throw new Error('源活动不存在');
const { actName, actBeginTime, actEndTime } = targetAct;
const newAct = {
actName: `【复制】${actName}`,
actBeginTime,
actEndTime,
actModifyTime: getFormatTime(),
actCreateTime: getFormatTime(),
operator: copyInfo.userName,
actStatus: ActStatus.MODIFYING,
};
const act = await ActInfo.create<ActInfo>(newAct as ActInfo);
// 更新加密id
const cryptoId = Crypto.encode(act.actId.toString());
await ActInfo.update(
{ actCryptoId: cryptoId },
{
where: { actId: act.actId },
},
);
// 复制page
if (targetAct.pages) {
await Promise.all(
targetAct.pages.map(async (page) => {
const copyPage = await this.pageService.create(page);
await act.$add<Page>('Pages', copyPage);
}),
);
}
};
/**
*
* @param {ActBaseInfo} actInfo
* @returns {Res}
*/
update = async (actInfo: ActBaseInfo) => {
try {
if (actInfo.abTest?.length) {
actInfo.abTestRaw = JSON.stringify(actInfo.abTest);
}
SequelizeHelper.getInstance();
const updateActInfo = Object.assign(actInfo, {
actModifyTime: getFormatTime(),
actStatus: ActStatus.MODIFYING,
});
await ActInfo.update(
{
...updateActInfo,
},
{
where: { actId: actInfo.actId },
},
);
} catch (error) {
logger.error(`活动基础信息保存失败:${error}`);
throw new Error('活动基础信息保存失败');
}
return { ret: 0, msg: '活动基础信息保存成功' };
};
/**
*
* @param {ActBaseInfo} actInfo
* @param {number} actStatus
* @returns {Res}
*/
publish = async (actInfo: ActBaseInfo, actStatus: number) => {
try {
const updateActInfo = Object.assign(actInfo, {
actModifyTime: getFormatTime(),
actStatus,
});
await ActInfo.update(
{
...updateActInfo,
},
{
where: { actId: actInfo.actId },
},
);
} catch (error) {
logger.error(`活动基础信息发布失败:${error}`);
throw new Error('活动基础信息发布失败');
}
return { ret: 0, msg: '活动基础信息发布成功' };
};
/**
* ID删除活动页面
* @param {number} pageId ID
* @returns void
*/
removePage = async (pageId: number) => {
try {
await Page.destroy({
where: {
id: pageId,
},
});
} catch (error) {
logger.error(error);
throw new Error('删除活动页面失败');
}
};
/**
* pages的code
* @param {ActInfo} act
* @returns {ActInfo}
*/
formatPages = (act: ActInfoIncludePage): ActInfoIncludePage => {
const newAct: ActInfoIncludePage = cloneDeep(act);
if (act.pages) {
newAct.pages = act.pages.map((page: PageInfo) => this.pageService.formatCode(page));
}
return newAct;
};
}
/**
* page数据
* @param {number} actId id
* @returns {ActInfoIncludePage}
*/
const getActInfoHandler = async (actId: number): Promise<ActInfoIncludePage> => {
SequelizeHelper.getInstance();
const act = await ActInfo.findOne({
where: { actId },
include: Page,
});
if (!act) throw new Error('活动不存在');
return act.toJSON() as ActInfoIncludePage;
};
// 将原始查询参数转化为构建查询体的参数
const formatSearch = (search: string) => search || '';
const formatActIdQuery = (search: string) => {
const isNumber = search && !isNaN(parseInt(search, 10));
return isNumber ? { actId: parseInt(search, 10) } : {};
};
const formatActStatusQuery = (status: number) =>
status === ActStatus.ALL ? { [Op.in]: [ActStatus.MODIFYING, ActStatus.PART_PUBLISHED, ActStatus.PUBLISHED] } : status;
const formatOrder = (order: OrderItem) =>
order.columnName ? [[order.columnName, order.direction === 'descending' ? 'DESC' : 'ASC']] : [];
const formatPageTitle = (title: string) =>
title
? {
pageTitle: { [Op.substring]: title },
}
: null;
// 根据查询参数类型返回对应的转化结果
const formatQuery = (query: string | OrderItem | number, type: FormatType) => {
const formats = {
search: formatSearch,
actId: formatActIdQuery,
status: formatActStatusQuery,
order: formatOrder,
title: formatPageTitle,
};
return formats[type]?.call(this, query);
};

View File

@ -0,0 +1,27 @@
/*
* Tencent is pleased to support the open source community by making MagicEditor available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import componetList from '@src/template/editor/get-component-list';
import webPlugins from '@src/template/editor/get-web-plugins';
export default class EditorService {
// 获取组件列表
getComponentList = () => componetList;
// 获取插件列表
getWebPlugins = () => webPlugins;
}

View File

@ -0,0 +1,196 @@
/*
* Tencent is pleased to support the open source community by making MagicEditor available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { cloneDeep } from 'lodash';
import { PageStatus } from '@src/config/config';
import { Page } from '@src/models/page';
import SequelizeHelper from '@src/sequelize/index';
import type { PageInfo } from '@src/typings';
import { configTransformDist, getFormatTime, serializeConfig } from '@src/utils/index';
import logger from '@src/utils/logger';
export default class PageService {
/**
*
* @param {Page} page
* @returns {Page}
*/
create = (page?: Page) => {
const oldPage = page;
const newPage = {
pageCreateTime: getFormatTime(),
pageModifyTime: getFormatTime(),
pagePublishStatus: PageStatus.MODIFYING,
distCode: oldPage?.distCode,
srcCode: oldPage?.srcCode,
pagePublishUiVersion: oldPage?.pagePublishUiVersion,
pageTitle: oldPage?.pageTitle || 'index',
pageName: oldPage?.pageName || 'index',
};
return Page.create(newPage as Page);
};
/**
* buffer类型
* @param {any} val
* @returns {Boolean}
*/
isBuffer = (val: any) => val && typeof val === 'object' && Buffer.isBuffer(val);
/**
* codesrcCode和distCode转化为string
* @param {PageInfo} page
* @returns {PageInfo}
*/
formatCode = (page?: PageInfo): PageInfo => {
const newPage: PageInfo = cloneDeep(page);
if (this.isBuffer(page.distCode)) newPage.distCode = page.distCode.toString();
if (this.isBuffer(page.srcCode)) newPage.srcCode = page.srcCode.toString();
if (page.pageCreateTime) newPage.pageCreateTime = getFormatTime(page.pageCreateTime);
if (page.pageModifyTime) newPage.pageModifyTime = getFormatTime(page.pageModifyTime);
if (page.pagePublishTime) newPage.pagePublishTime = getFormatTime(page.pagePublishTime);
return newPage;
};
/**
*
* @param {PageInfo} page
* @param {Boolean} isPublish
* @param {String} operator
* @returns {Res}
*/
update = async (pages: PageInfo[], isPublish = false, operator = '') => {
const sequelize = SequelizeHelper.getInstance();
const pageColUpdate: PageInfo = {};
if (!isPublish) {
pageColUpdate.pagePublishStatus = PageStatus.MODIFYING;
pageColUpdate.pageModifyTime = getFormatTime();
} else {
pageColUpdate.pagePublishStatus = PageStatus.PUBLISHED;
pageColUpdate.pagePublishTime = getFormatTime();
pageColUpdate.pagePublishOperator = operator;
}
try {
await sequelize.transaction(() => {
Promise.all(
pages.map(async (page: PageInfo) => {
// 如果page.id不是纯数字说明是新建的页面需要补入一些字段
const isNewPage = !/^\d+$/.test(page.id);
if (isNewPage) {
pageColUpdate.pageCreateTime = getFormatTime();
pageColUpdate.pageTitle = page.pageName;
}
const upsertPage = Object.assign(page, pageColUpdate) as Page;
// page更新到数据库
await Page.upsert(upsertPage);
if (isNewPage) {
// 将新分配的Pageid回写至srcCode,distCode
const newPageIdRes = await Page.findOne({
where: {
actId: page.actId,
pageTitle: page.pageTitle,
},
attributes: ['id'],
});
const newPageId = newPageIdRes.toJSON();
await this.updateCode(newPageId.id, page);
}
}),
);
});
return { ret: 0, msg: '页面配置更新成功' };
} catch (e) {
logger.error(`页面配置更新失败:${e}`);
throw new Error('页面配置更新失败');
}
};
/**
*
* @param {number} actId Id
* @param {string[]} publishPages
* @returns {PageInfo[]}
*/
getPages = async (actId: number, publishPages: string[]): Promise<PageInfo[]> => {
SequelizeHelper.getInstance();
const pages = await Page.findAll(
publishPages.length > 0
? {
where: {
actId,
pageName: publishPages,
},
raw: true,
}
: {
where: {
actId,
},
raw: true,
},
);
return pages.map((page) => this.formatCode(page));
};
/**
*
* @param {number} actId Id
* @returns {number}
*/
getPagesCount = async (actId: number): Promise<number> => {
SequelizeHelper.getInstance();
const pages = await Page.findAll({
where: {
actId,
},
raw: true,
});
return pages.length;
};
/**
* Pageid回写至srcCode,distCode
* @param {string} pageId id
* @param {PageInfo} page
* @returns void
*/
updateCode = async (pageId: string, page: PageInfo) => {
try {
// 不是真正的json对象可能包含组件自定义code代码
// eslint-disable-next-line no-eval
const srcCode = eval(`(${page.srcCode})`);
srcCode.items[0].id = pageId;
const newSrcCode = serializeConfig(srcCode);
const newDistCode = configTransformDist(newSrcCode);
await Page.update(
{
srcCode: newSrcCode,
distCode: newDistCode,
},
{
where: { id: pageId },
},
);
} catch (error) {
logger.error(error);
throw new Error('保存新添加的页面失败');
}
};
}

View File

@ -0,0 +1,273 @@
/*
* Tencent is pleased to support the open source community by making MagicEditor available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { copySync, emptyDir, outputFileSync, readFileSync } from 'fs-extra';
import { cloneDeep } from 'lodash';
import { ActStatus, StaticPath, UiRuntimeJS } from '@src/config/config';
import ActService from '@src/service/act';
import PageService from '@src/service/page';
import type { ActBaseInfo, PageInfo, UiConfig } from '@src/typings';
import { configTransformDist, processTransConfig, serializeConfig } from '@src/utils/index';
import logger from '@src/utils/logger';
const actService = new ActService();
const pageService = new PageService();
type DivideConfig = {
pageName: string;
pageConfig: UiConfig;
};
type DivideConfigTrans = {
pageName: string;
pageConfigStr: string;
};
export default class PublishService {
/**
*
* @param {ActBaseInfo} actInfo
* @param {string} rootInfo uiconfig页面配置信息
* @returns {Res}
*/
saveActInfo = async ({ actInfo, rootInfo }: { actInfo: ActBaseInfo; rootInfo: string }) => {
// 保存活动基础信息
await actService.update(actInfo);
try {
// 不是真正的json对象可能包含组件自定义code代码
// eslint-disable-next-line no-eval
const uiConfig = eval(`(${rootInfo})`);
// 按页面拆分 abtest等发布时处理
const divideConfigs = dividePageProcessor(uiConfig);
// 处理为保存所需格式
const pagesForSave = pageSaveProcessor(divideConfigs, actInfo);
// 保存到magic_ui_config表
await pageService.update(pagesForSave);
return { ret: 0, msg: '保存成功' };
} catch (error) {
logger.error(error);
throw new Error('保存活动失败');
}
};
/**
*
* @param {number} actId Id
* @param {string[]} publishPages
* @param {string} operator
* @returns {Res}
*/
publish = async ({
actId,
publishPages,
operator,
}: {
actId: number;
publishPages: string[];
rootInfo: string;
operator: string;
}) => {
try {
// 查询活动基础信息
const actInfo = await actService.getActInfo(actId);
// 查询需要发布的页面
const pagesToPublish = await pageService.getPages(actId, publishPages);
let publishPagesArr: DivideConfigTrans[] = pagesToPublish.map((pageItem) => ({
pageName: pageItem.pageName,
pageConfigStr: pageItem.distCode,
}));
// 处理abtest页面
const abTestConfig: DivideConfigTrans[] = await getAbTestConfig(publishPages, actInfo.abTest, actId);
publishPagesArr = publishPagesArr.concat(abTestConfig);
// 发布
await publishToFiles(publishPagesArr);
// 更新页面状态
await pageService.update(pagesToPublish, true, operator);
// 确认页面是否全部发布
let actStatus = ActStatus.PART_PUBLISHED;
const singlePageCount = await pageService.getPagesCount(actId);
const abTestCount = actInfo.abTest.length;
const allPagesCount = singlePageCount + abTestCount;
if (allPagesCount === publishPages.length) {
actStatus = ActStatus.PUBLISHED;
}
await actService.publish(actInfo, actStatus);
return { ret: 0, msg: '发布成功' };
} catch (error) {
logger.error(error);
throw new Error('发布失败');
}
};
}
/**
*
* @param {UiConfig} uiConfig uiconfig
* @returns {DivideConfig[]}
*/
const dividePageProcessor = (uiConfig: UiConfig): DivideConfig[] => {
const deployPages: DivideConfig[] = [];
// 按页面拆分uiConfig
for (let i = 0; i < uiConfig.items.length; i++) {
// 深复制一份
const pageConfig = cloneDeep(uiConfig);
pageConfig.items = [pageConfig.items[i]];
deployPages.push({
pageName: uiConfig.items[i].name,
pageConfig,
});
}
return deployPages;
};
/**
* abtest包含的页面配置
* @param {ActBaseInfo.abTest} abTest abtest配置
* @param {string[]} publishPages
* @param {number} actId ID
* @returns {DivideConfig[]} abtest拆分后的页面数组
*/
const getAbTestConfig = async (
publishPages: string[],
abTest: ActBaseInfo['abTest'],
actId: number,
): Promise<DivideConfigTrans[]> => {
const filterArr = filterIncludeAbTest(publishPages, abTest);
if (filterArr.length === 0) {
return [];
}
return await Promise.all(
filterArr.map(async (test) => {
let pagesToPublish: UiConfig;
await Promise.all(
test.pageList.map(async (testItem) => {
const [pageRecord] = await pageService.getPages(actId, [testItem.pageName]);
// 不是真正的json对象可能包含组件自定义code代码
// eslint-disable-next-line no-eval
const pageConfig = eval(`(${pageRecord.distCode})`);
if (!pagesToPublish) {
pagesToPublish = pageConfig;
} else {
pagesToPublish.items.push(pageConfig?.items[0]);
}
}),
);
// 将config对象序列化并转译
const srcCode = serializeConfig(pagesToPublish);
const distCode = configTransformDist(srcCode);
return {
pageName: test.name,
pageConfigStr: distCode,
};
}),
);
};
/**
* abtest页面
* @param {ActBaseInfo.abTest} abTest abtest配置
* @param {string[]} publishPages
* @returns {ActBaseInfo['abTest']}
*/
const filterIncludeAbTest = (publishPages: string[], abTest: ActBaseInfo['abTest']) => {
if (abTest.length === 0) {
return [];
}
const filterArr = [];
abTest.forEach((test) => {
if (publishPages.includes(test.name)) {
filterArr.push(test);
}
});
return filterArr;
};
/**
*
* @param {DivideConfig[]} divideConfigs uiconfig
* @param {ActBaseInfo} actInfo
* @returns {PageInfo[]} PageInfo数组
*/
const pageSaveProcessor = (divideConfigs: DivideConfig[], actInfo: ActBaseInfo): PageInfo[] =>
divideConfigs.map((divideItem) => {
const srcCode = serializeConfig(divideItem.pageConfig);
const distCode = configTransformDist(srcCode);
return {
pageTitle: divideItem.pageConfig.items[0].title || divideItem.pageConfig.items[0].name,
pageName: divideItem.pageName,
srcCode,
distCode,
actId: actInfo.actId,
id: divideItem.pageConfig.items[0].id,
};
});
/**
*
* @param {DivideConfigTrans} pageConfig UiConfig活动配置
* @returns void
*/
const publishUiconfig = (pageConfig: DivideConfigTrans) => {
try {
const { pageName, pageConfigStr } = pageConfig;
const distJs = `${StaticPath.PUBLISH}/uiconfig_${pageName}.js`;
const code = processTransConfig(pageConfigStr);
outputFileSync(distJs, code);
logger.debug(`create ${distJs} success!`);
} catch (error) {
logger.error(error);
throw new Error('发布活动配置文件失败');
}
};
/**
* html文件
* @param {DivideConfigTrans} pageConfig UiConfig活动配置
* @returns void
*/
const publishHtml = (pageConfig: DivideConfigTrans) => {
const { pageName } = pageConfig;
try {
// 复制html模板
const distHtml = `${StaticPath.PUBLISH}/${pageName}.html`;
const tmpHtml = `${StaticPath.TEMPLATE}/publish/page-tmp.html`;
copySync(tmpHtml, distHtml);
// 注入活动配置文件
const configScript = `<script type='module' src='./uiconfig_${pageName}.js'></script>\n\t${UiRuntimeJS}`;
const data = readFileSync(distHtml, 'utf-8');
const newData = data.replace(UiRuntimeJS, configScript);
outputFileSync(distHtml, newData, 'utf-8');
logger.debug(`create ${distHtml} success!`);
} catch (error) {
throw new Error('发布活动html文件失败');
}
};
/**
*
* @param {DivideConfigTrans[]} publishPagesArr UiConfig
* @returns void
*/
const publishToFiles = async (publishPagesArr: DivideConfigTrans[]) => {
// 1. 文件夹清空并重新创建
await emptyDir(StaticPath.PUBLISH);
publishPagesArr.forEach((divideConfig) => {
// 2、发布uiconfig.js
publishUiconfig(divideConfig);
// 3、发布html模板
publishHtml(divideConfig);
});
};

View File

@ -0,0 +1,45 @@
/*
* Tencent is pleased to support the open source community by making MagicEditor available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export default [
{
title: '容器',
items: [
{
icon: 'folder-opened',
text: '组',
type: 'container',
},
],
},
{
title: '示例组件',
items: [
{
icon: 'iconfont icon-wenben',
text: '文本',
type: 'text',
},
{
icon: 'iconfont icon-anniu',
text: '按钮',
type: 'button',
},
],
},
];

View File

@ -0,0 +1,26 @@
/*
* Tencent is pleased to support the open source community by making MagicEditor available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export default {
options: [
{
text: '会员登录认证',
value: '会员登录认证',
},
],
};

View File

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Magic App</title>
<script src="https://unpkg.com/vue@next/dist/vue.runtime.global.js"></script>
<script type="module" crossorigin src="/runtime/vue3/assets/page.js"></script>
<link rel="modulepreload" href="/runtime/vue3/assets/vendor.3f0d9265.js">
<link rel="modulepreload" href="/runtime/vue3/assets/resetcss.b08d57c8.js">
<link rel="modulepreload" href="/runtime/vue3/assets/plugin-vue_export-helper.3811b543.js">
<link rel="modulepreload" href="/runtime/vue3/assets/components.js">
<link rel="stylesheet" href="/runtime/vue3/assets/resetcss.52e41e6b.css">
<link rel="stylesheet" href="/runtime/vue3/assets/page.6c73043b.css">
</head>
<body>
<div id="app"></div>
</body>
</html>

View File

@ -0,0 +1,98 @@
/*
* Tencent is pleased to support the open source community by making MagicEditor available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// 活动信息定义
export interface ActBaseInfo {
actId: number;
actCryptoId: string;
actName: string;
operator?: string;
actStatus: number;
actBeginTime?: string;
actEndTime?: string;
actModifyTime?: string;
actCreateTime?: string;
locker?: string;
lockTime?: string;
abTest?: ABTest[];
abTestRaw?: string;
plugins?: string[];
id?: string;
name?: string;
}
// 查询处理后的活动信息,含页面配置
export interface ActInfoIncludePage extends ActBaseInfo {
pages: PageInfo[];
}
// 编辑器组件配置定义
export interface UiConfig {
actId: number;
type?: string;
id?: string;
name?: string;
operator?: string;
items?: PageInfo[];
abTest?: ABTest[];
useLastPackage?: string;
}
// 活动页面abtest定义
export interface ABTest {
name: string;
type: string;
pageList?: AbTestPageList[];
}
// 活动页面abtest pagelist定义
export interface AbTestPageList {
pageName: string;
proportion: string;
}
// 活动页面信息定义
export interface PageInfo {
id?: string;
pageTitle?: string;
title?: string;
name?: string;
pageName?: string;
actId?: number;
pageCreateTime?: string;
pageModifyTime?: string;
pagePublishStatus?: number;
pagePublishTime?: string;
pagePublishOperator?: string;
pagePublishUiVersion?: string;
srcCode?: string;
distCode?: string;
plugins?: string[];
}
// 从editor拿到的活动页面信息
export interface EditorInfo {
type: string;
items?: PageInfo[];
}
// 接口返回
export interface Res<T = any> {
ret: number;
msg: string;
data?: T;
}

View File

@ -0,0 +1,52 @@
/*
* Tencent is pleased to support the open source community by making MagicEditor available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// 活动加密
import myCrypto from 'crypto';
import config from '@src/config/key';
const algorithm = 'aes-256-cbc';
const keyStr = config.key;
const ivByte = Buffer.from(keyStr.substr(0, 16));
function encrypt(text: string) {
const cipher = myCrypto.createCipheriv(algorithm, Buffer.from(keyStr), ivByte);
let encrypted = cipher.update(text);
encrypted = Buffer.concat([encrypted, cipher.final()]);
return encrypted.toString('hex');
}
function decrypt(text: string) {
const encryptedData = text;
const encryptedText = Buffer.from(encryptedData, 'hex');
const decipher = myCrypto.createDecipheriv(algorithm, Buffer.from(keyStr), ivByte);
let decrypted = decipher.update(encryptedText);
decrypted = Buffer.concat([decrypted, decipher.final()]);
return decrypted.toString();
}
export default {
encode(text: string): string {
return encrypt(text);
},
decode(text: string): string {
return decrypt(text);
},
};

View File

@ -0,0 +1,96 @@
/*
* Tencent is pleased to support the open source community by making MagicEditor available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import path from 'path';
import axios from 'axios';
import { createWriteStream, emptyDir } from 'fs-extra';
import momentTimezone from 'moment-timezone';
import serialize from 'serialize-javascript';
import uglifyJS from 'uglify-js';
import type { UiConfig } from '@src/typings';
import { babelTransform } from '@src/utils/transform';
/**
*
* @param {string} value
* @returns {string}
*/
const serializeConfig = function (value: UiConfig): string {
return serialize(value, {
space: 2,
unsafe: true,
}).replace(/"(\w+)":\s/g, '$1: ');
};
/**
* srccode转换为distcode
* @param {string} srcCode srcCode
* @returns {string} distcode
*/
const configTransformDist = (srcCode: string): string => {
let babelCode: string = babelTransform(`window.uiConfig=[${srcCode}]`);
babelCode = babelCode.replace('window.uiConfig = [', '');
return babelCode.substring(0, babelCode.length - 2);
};
/**
* uglifyJS处理distcode
* @param {string} transConfig transConfig
* @returns {string}
*/
const processTransConfig = (transConfig) => {
const code = `window.magicUiconfig = [${transConfig}]`;
return uglifyJS.minify(`${code}`).code;
};
/**
*
* @param {string} url
* @param {string} filePath
* @param {string} fileName
* @returns {Promise}
*/
const getFileFromUrl = async ({ url, filePath, fileName }) => {
// 1. 文件夹清空并重新创建
await emptyDir(filePath);
const distPath = path.resolve(filePath, fileName);
const writer = createWriteStream(distPath);
const response = await axios({
url,
method: 'GET',
responseType: 'stream',
});
response.data.pipe(writer);
return new Promise((resolve, reject) => {
writer.on('finish', resolve);
writer.on('error', reject);
});
};
/**
*
* @param {string} [time]
* @param {string} [formatTmp] 使'YYYY-MM-DD HH:mm:ss'
* @returns {string}
*/
const getFormatTime = (time: string | number = Date.now(), formatTmp = 'YYYY-MM-DD HH:mm:ss') =>
momentTimezone.tz(time, 'Asia/Shanghai').format(formatTmp);
export { serializeConfig, configTransformDist, processTransConfig, getFileFromUrl, getFormatTime };

View File

@ -0,0 +1,39 @@
/*
* Tencent is pleased to support the open source community by making MagicEditor available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { getLogger } from 'log4js';
class Logger {
public logger;
constructor() {
this.logger = getLogger();
}
info = (message: string) => {
this.logger.info(message);
};
debug = (message: string) => {
this.logger.debug(message);
};
warn = (message: string) => {
this.logger.warn(message);
};
error = (message: string) => {
this.logger.error(message);
};
}
export default new Logger();

View File

@ -0,0 +1,43 @@
/*
* Tencent is pleased to support the open source community by making MagicEditor available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { transformSync } from '@babel/core';
import logger from '@src/utils/logger';
export function babelTransform(value) {
const options = {
compact: false,
presets: [
[
'@babel/preset-env',
{
modules: false,
targets: {
browsers: ['> 1%', 'last 2 versions', 'not ie <= 8'],
},
},
],
],
};
try {
return transformSync(value, options)?.code || '';
} catch (e) {
logger.error(e);
throw new Error('babel 编译失败');
}
}

View File

@ -0,0 +1,129 @@
/*
* Tencent is pleased to support the open source community by making MagicEditor available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import 'regenerator-runtime/runtime';
import actController from '@src/controller/act';
jest.mock('@src/service/act', () =>
jest.fn().mockImplementation(() => ({
getActList: jest.fn(() => []),
getCount: jest.fn(() => 0),
create: jest.fn(() => 1),
copy: jest.fn(),
getActInfo: jest.fn((id) => ({ actId: id })),
})),
);
const mockCtx = {
request: {
body: {
data: '{}',
},
},
body: {
data: [],
total: 0,
fetch: false,
errorMsg: '',
},
};
describe('act controller测试', () => {
it('获取活动列表', async () => {
await actController.getList(mockCtx);
expect(mockCtx.body).toEqual({
data: [],
total: 0,
fetch: true,
errorMsg: '',
});
});
it('获取活动列表异常', async () => {
const mockErroCtx = { body: {}, logger:{error:jest.fn()} };
await actController.getList(mockErroCtx);
expect(mockErroCtx.body).toEqual({
data: [],
total: 0,
fetch: false,
errorMsg: "Cannot read property 'body' of undefined",
});
});
it('新建活动', async () => {
await actController.create(mockCtx);
expect(mockCtx.body).toEqual({
ret: 0,
msg: '新建活动成功',
data: { actId: 1 },
});
});
it('新建活动异常', async () => {
const mockErroCtx = { body: {}};
await actController.create(mockErroCtx);
expect(mockErroCtx.body).toEqual({
ret: -1,
msg: "Cannot read property 'body' of undefined",
});
});
it('复制活动', async () => {
await actController.copy(mockCtx);
expect(mockCtx.body).toEqual({
ret: 0,
msg: '复制成功',
});
});
it('复制活动异常', async () => {
const mockErroCtx = { body: {} };
await actController.copy(mockErroCtx);
expect(mockErroCtx.body).toEqual({
ret: -1,
msg: "Cannot read property 'body' of undefined",
});
});
it('查询活动详情', async () => {
const getInfoCtx = {
query: {
id: '1',
},
body: {},
};
await actController.getInfo(getInfoCtx);
expect(getInfoCtx.body).toEqual({
ret: 0,
msg: '获取活动信息成功',
data: { actId: 1 },
});
});
it('查询活动详情异常', async () => {
const getInfoErrorCtx = {
body: {},
};
await actController.getInfo(getInfoErrorCtx);
expect(getInfoErrorCtx.body).toEqual({
ret: -1,
msg: "Cannot read property 'id' of undefined",
});
});
});

View File

@ -0,0 +1,56 @@
/*
* Tencent is pleased to support the open source community by making MagicEditor available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import 'regenerator-runtime/runtime';
import editorController from '@src/controller/editor';
jest.mock('@src/service/editor', () =>
jest.fn().mockImplementation(() => ({
getComponentList: jest.fn(() => []),
getWebPlugins: jest.fn(() => []),
})),
);
const mockCtx = {
request: {
body: {
data: '{}',
},
},
body: {
data: [],
total: 0,
fetch: false,
errorMsg: '',
},
};
describe('editor controller测试', () => {
it('获取组件列表', async () => {
await editorController.getComponentList(mockCtx);
expect(mockCtx.body).toEqual({
ret: 0,
msg: '获取组件列表成功',
data: [],
});
});
it('获取web插件', async () => {
await editorController.getWebPlugins(mockCtx);
expect(mockCtx.body).toEqual([]);
});
});

View File

@ -0,0 +1,138 @@
/*
* Tencent is pleased to support the open source community by making MagicEditor available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import 'regenerator-runtime/runtime';
import publishController from '@src/controller/publish';
import serialize from 'serialize-javascript';
jest.mock('@src/service/publish', () =>
jest.fn().mockImplementation(() => ({
publish: jest.fn((res) => {
res.ret = 0;
res.msg = '发布成功';
return res;
}),
saveActInfo: jest.fn((res) => {
res.ret = 0;
res.msg = '保存成功';
return res;
}),
'ctx.cookie.get': jest.fn(() => {}),
})),
);
const serializeConfig = (value): string =>
serialize(value, {
space: 2,
unsafe: true,
}).replace(/"(\w+)":\s/g, '$1: ');
const mockSaveData = {
actId: 1,
type: 'app',
items: [],
};
const mockPublishData = {
actId: '123',
publishPages: ['index'],
rootInfo: '',
};
const uiConfigStr = serializeConfig(mockSaveData);
const publishDataStr = serializeConfig(mockPublishData);
describe('保存与发布', () => {
it('保存成功', async () => {
const ctx = {
request: {
body: {
data: uiConfigStr,
},
},
body: {
data: [],
total: 0,
fetch: false,
errorMsg: '',
},
};
await publishController.saveActInfo(ctx);
expect(ctx.body).toEqual({
ret: 0,
msg: '保存成功',
});
});
it('保存失败', async () => {
const ctx = {
request: {},
body: {},
};
await publishController.saveActInfo(ctx);
expect(ctx.body).toEqual({
ret: -1,
msg: "Cannot read property 'data' of undefined",
});
});
it('发布成功', async () => {
const ctx = {
request: {
body: {
data: publishDataStr,
},
},
body: {
data: [],
total: 0,
fetch: false,
errorMsg: '',
},
cookies: {
get(key) {
if (key === 'user') return 'default';
},
},
};
await publishController.publish(ctx);
expect(ctx.body).toEqual({
ret: 0,
msg: '发布成功',
});
});
it('发布失败', async () => {
const ctx = {
request: {
body: {
data: '',
},
},
body: {
data: [],
total: 0,
fetch: false,
errorMsg: '',
},
cookies: {
get(key) {
if (key === 'user') return 'default';
},
},
};
await publishController.publish(ctx);
expect(ctx.body).toEqual({
ret: -1,
msg: "Unexpected token ')'",
});
});
});

View File

@ -0,0 +1,35 @@
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"jsx": "preserve",
"importHelpers": true,
"moduleResolution": "node",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"skipLibCheck": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true,
"sourceMap": true,
"baseUrl": ".",
"outDir": "./dist",
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
],
"paths": {
"@src/*": [
"src/*"
],
"@tests/*": [
"tests/*"
]
},
"noEmitOnError": true
},
"includes": ["src/**/*.ts"],
"exclude": ["node_modules","tests","coverage"],
}

39
magic-admin/setup.sh Normal file
View File

@ -0,0 +1,39 @@
#!/bin/bash
# 项目根目录
WORKSPACE=$(dirname "$PWD")
echo ${WORKSPACE}
# 全局安装lerna
tnpm i lerna -g
# magic依赖安装和构建
cd ${WORKSPACE}
tnpm run reinstall
tnpm run build
echo "magic依赖安装完毕 & 打包完毕"
# 移动runtime打包产物到web
mv -f ${WORKSPACE}/playground/dist/runtime/ ${WORKSPACE}/magic-admin/web/public
echo "移动runtime打包产物到web完毕"
# web构建
cd ${WORKSPACE}/magic-admin/web
tnpm run build
echo "web依赖安装完毕"
# 移动web文件到server
mkdir -p ${WORKSPACE}/magic-admin/server/assets
cp -rf ${WORKSPACE}/magic-admin/web/dist/* ${WORKSPACE}/magic-admin/server/assets
echo "移动web文件到server完毕"
# 运行server
cd ${WORKSPACE}/magic-admin/server
tnpm i pm2 -g
pm2-runtime start pm2.config.js --env production

View File

@ -0,0 +1,3 @@
> 1%
last 2 versions
not dead

View File

@ -0,0 +1,6 @@
dist/
node_modules/
babel.config.js
vue.config.js
jest.config.js
tscofnig.json

View File

@ -0,0 +1,62 @@
module.exports = {
env: {
node: true,
browser: true,
es2021: true,
},
globals: {
describe: true,
it: true,
expect: true,
jest: true,
beforeEach: true,
},
extends: [
'eslint-config-tencent',
'eslint-config-tencent/ts',
'plugin:vue/vue3-essential',
'eslint-config-tencent/prettier',
],
parser: 'vue-eslint-parser',
parserOptions: {
ecmaVersion: 12,
parser: '@typescript-eslint/parser',
extraFileExtensions: ['.vue', '.tsx'],
sourceType: 'module',
},
plugins: [
'vue',
'@typescript-eslint',
'simple-import-sort',
],
ignorePatterns: ['.eslintrc.js'],
rules: {
'vue/no-mutating-props': 'off',
'no-param-reassign': 'off',
'@typescript-eslint/no-require-imports': 'off',
'simple-import-sort/imports': [
'error', {
groups: [
// Node.js builtins. You could also generate this regex if you use a `.js` config.
// For example: `^(${require('module').builtinModules.join('|')})(/|$)`
[
'^(assert|buffer|child_process|cluster|console|constants|crypto|dgram|dns|domain|events|fs|http|https|module|net|os|path|punycode|querystring|readline|repl|stream|string_decoder|sys|timers|tls|tty|url|util|vm|zlib|freelist|v8|process|async_hooks|http2|perf_hooks)(/.*|$)',
],
// Packages. `react|vue` related packages come first.
['^(react|vue|vite)', '^@?\\w'],
['^(@tmagic)(/.*|$)'],
// Internal packages.
['^(@|@src|@tests)(/.*|$)'],
// Side effect imports.
['^\\u0000'],
// Parent imports. Put `..` last.
['^\\.\\.(?!/?$)', '^\\.\\./?$'],
// Other relative imports. Put same-folder imports and `.` last.
['^\\./(?=.*/)(?!/?$)', '^\\.(?!/?$)', '^\\./?$'],
// Style imports.
['^.+\\.s?css$'],
],
},
],
},
};

27
magic-admin/web/.gitignore vendored Normal file
View File

@ -0,0 +1,27 @@
.DS_Store
node_modules
/dist
/public/runtime
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
.vscode
# test coverage
coverage

View File

@ -0,0 +1,4 @@
module.exports = {
presets: ['@vue/cli-plugin-babel/preset'],
plugins: ['@babel/plugin-proposal-optional-chaining', '@babel/plugin-proposal-nullish-coalescing-operator'],
};

View File

@ -0,0 +1,29 @@
# magic-admin
## Project setup
```
npm install
```
### Compiles and hot-reloads for development
```
npm run serve
```
### Compiles and minifies for production
```
npm run build
```
### Run your unit tests
```
npm run test:unit
```
### Lints and fixes files
```
npm run lint
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).

View File

@ -0,0 +1,13 @@
module.exports = {
preset: '@vue/cli-plugin-unit-jest/presets/typescript-and-babel',
transform: {
'^.+\\.vue$': 'vue-jest',
},
moduleNameMapper: {
'^@src/(.*)$': '<rootDir>/src/$1',
'^@tests/(.*)$': '<rootDir>/tests/$1',
},
collectCoverage: true,
transformIgnorePatterns: ['/node_modules/(?!lodash-es/.*)'],
collectCoverageFrom: ['src/views/*.{ts,vue}', 'src/components/*.{ts,vue}'],
};

18785
magic-admin/web/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,58 @@
{
"name": "magic-admin-new",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"test": "vue-cli-service test:unit --maxWorkers=8 --coverage",
"lint": "vue-cli-service lint"
},
"dependencies": {
"@tmagic/editor": "^1.0.0-beta.2",
"@tmagic/form": "^1.0.0-beta.2",
"@tmagic/schema": "^1.0.0-beta.2",
"@tmagic/stage": "^1.0.0-beta.2",
"@tmagic/utils": "^1.0.0-beta.2",
"axios": "^0.25.0",
"axios-jsonp": "^1.0.4",
"core-js": "^3.20.0",
"element-plus": "^2.0.2",
"js-cookie": "^3.0.0",
"moment": "^2.29.1",
"vue": "^3.2.26",
"vue-router": "^4.0.0-0"
},
"devDependencies": {
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.16.7",
"@babel/plugin-proposal-optional-chaining": "^7.16.7",
"@babel/preset-env": "^7.16.11",
"@types/jest": "^24.0.19",
"@types/js-cookie": "^2.2.7",
"@types/serialize-javascript": "^5.0.1",
"@typescript-eslint/eslint-plugin": "^4.18.0",
"@typescript-eslint/parser": "^4.18.0",
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-plugin-router": "~4.5.0",
"@vue/cli-plugin-typescript": "~4.5.0",
"@vue/cli-plugin-unit-jest": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"@vue/compiler-sfc": "^3.2.26",
"@vue/eslint-config-prettier": "^6.0.0",
"@vue/eslint-config-typescript": "^7.0.0",
"@vue/test-utils": "^2.0.0-rc.17",
"eslint": "^7.29.0",
"eslint-config-tencent": "^1.0.1",
"eslint-plugin-prettier": "^3.3.1",
"eslint-plugin-simple-import-sort": "^7.0.0",
"eslint-plugin-vue": "^7.0.0",
"moment-timezone": "^0.5.34",
"prettier": "^2.5.1",
"sass": "^1.45.0",
"sass-loader": "^8.0.2",
"serialize-javascript": "^6.0.0",
"typescript": "~4.1.5",
"vue-jest": "^5.0.0-0"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

View File

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.png" type="image/png">
<title>魔方</title>
</head>
<body>
<noscript>
<strong>不支持此浏览器请使用chrome浏览器</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

View File

@ -0,0 +1,160 @@
/*
* Tencent is pleased to support the open source community by making MagicEditor available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// 活动相关请求
import fetch, { Res } from '@src/util/request';
// 排序项
export interface OrderItem {
columnName: string;
direction: string;
}
// 活动查询参数
export interface ActListQuery {
where: {
onlySelf: boolean;
search: string | string[];
pageTitle: string;
actStatus: number;
};
orderBy: OrderItem[];
pgIndex: number;
pgSize: number;
userName: string;
}
// 活动复制参数
export interface CopyInfo {
actId: number;
userName: string;
}
// 活动创建参数
export interface ActInfoDetail {
actName: string;
actBeginTime: string;
actEndTime: string;
operator: string;
}
// 活动页面基本信息
export interface PageInfo extends PageListItem {
id: number;
actId?: number;
}
export interface PageListItem {
pageTitle: string;
pagePublishTime?: string;
pagePublishStatus: number;
pagePublishOperator?: string;
}
// 活动列表项
export interface ActListItem {
actId: number;
actCryptoId: string;
actName: string;
actBeginTime: string;
actEndTime: string;
operator: string;
actStatus: number;
items: PageInfo[];
[key: string]: any;
}
// 活动列表返回类型
export interface ActListRes {
data: ActListItem[];
total: number;
fetch: boolean;
errorMsg: string;
}
export default {
/**
*
* @param {Object} options
* @param {ActListQuery} options.data
* @returns {Promise<ActListRes>}
*/
getList(options: { data: ActListQuery }): Promise<ActListRes> {
return fetch.post({
_c: 'act',
_a: 'getList',
...options,
});
},
/**
*
* @param {Object} options
* @param {ActInfoDetail} options.data
* @returns {Promise<Res<{ actId: number }>>} Id
*/
saveAct(options: { data: ActInfoDetail }): Promise<Res<{ actId: number }>> {
return fetch.post({
_c: 'act',
_a: 'create',
...options,
});
},
/**
*
* @param {Object} options
* @param {CopyInfo} options.data
* @returns {Promise<Res>}
*/
copyAct(options: { data: CopyInfo }): Promise<Res> {
return fetch.post({
_c: 'act',
_a: 'copy',
...options,
});
},
/**
* id获取活动详情
* @param {Object} params
* @param {number} options.id ID
* @returns {Promise<Res>}
*/
getAct(params: { id: number }): Promise<Res> {
return fetch.get({
_c: 'act',
_a: 'get',
params,
});
},
/**
*
* @param {Object} options
* @param {number} options.pageId
* @returns {Promise<Res>}
*/
removePage(options: { data: { pageId: number } }): Promise<Res> {
return fetch.post({
_c: 'act',
_a: 'removePage',
...options,
});
},
};

View File

@ -0,0 +1,47 @@
/*
* Tencent is pleased to support the open source community by making MagicEditor available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import fetch, { Res } from '@src/util/request';
// 编辑器左侧组件分类
export interface CompClassifyForEditor {
title: string;
items: CompListInClassify[];
}
// 编辑器左侧组件列表
export interface CompListInClassify {
icon: string;
id: number;
renderType: number;
reportType: string;
text: string;
type: string;
}
export default {
/**
*
* @returns {Promise<Res>}
*/
getComponentList(): Promise<Res> {
return fetch.get({
_c: 'editor',
_a: 'getComponentList',
});
},
};

View File

@ -0,0 +1,55 @@
/*
* Tencent is pleased to support the open source community by making MagicEditor available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { ActInfo } from '@src/typings';
import fetch, { Res } from '@src/util/request';
export default {
/**
*
* @param {Object} options
* @param {Object} options.data
* @param {ActInfo} options.data.actInfo
* @param {string} options.data.rootInfo
* @returns {Promise<Res>}
*/
saveActInfo(options: { data: { actInfo: ActInfo; rootInfo: string } }): Promise<Res> {
return fetch.post({
_c: 'publish',
_a: 'saveActInfo',
...options,
});
},
/**
*
* @param {Object} options
* @param {Object} options.data
* @param {number} options.data.actId ID
* @param {string[]} options.data.publishPages
* @param {string} options.data.rootInfo
* @returns {Promise<Res>}
*/
publishPage(options: { data: { actId: number; publishPages: string[]; rootInfo: string } }): Promise<Res> {
return fetch.post({
_c: 'publish',
_a: 'publish',
...options,
});
},
};

View File

@ -0,0 +1,32 @@
/*
* Tencent is pleased to support the open source community by making MagicEditor available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import fetch, { Res } from '@src/util/request';
export interface UserState {
loginName: string;
}
export default {
getUser(): Promise<Res<UserState>> {
return fetch.get({
_c: 'user',
_a: 'getUser',
});
},
};

140
magic-admin/web/src/app.vue Normal file
View File

@ -0,0 +1,140 @@
<template>
<el-container style="height: 100%; display: flex" class="app" v-if="!hideFrame" :class="{ 'hide-nav': hideNav }">
<m-aside v-if="!$route.meta.hideAside"></m-aside>
<el-container>
<el-header style="height: 40px; padding: 0">
<m-header @asideTrigger="asideTrigger" />
</el-header>
<el-main style="background: #ffffff; padding: 0" class="main-container">
<router-view :style="$route.meta.hideAside ? '' : 'padding: 20px'"></router-view>
</el-main>
</el-container>
<div class="fixed-layer" v-if="$route.name === 'ui-editor' && !isChrome">
<b style="font-size: 28px">魔方仅支持chrome浏览器编辑使用哦~</b>
</div>
</el-container>
<router-view v-else class="app" :class="{ 'hide-nav': hideNav }"></router-view>
</template>
<script lang="ts">
import { computed, defineComponent, provide, reactive, watchEffect } from 'vue';
import { useRoute } from 'vue-router';
import Cookies from 'js-cookie';
import { AsideFormConfig } from '@src/config/aside-config';
import type { AsideState } from '@src/typings';
export default defineComponent({
name: 'app',
setup() {
const aside = reactive<AsideState>(AsideFormConfig);
//
const hideFrame = computed(() => {
const urlHideFrame = new URL(location.href).searchParams.get('hideFrame');
return urlHideFrame || useRoute().meta?.hideFrame;
});
//
const asideTrigger: () => void = () => {
aside.collapse = !aside.collapse;
};
provide('aside', aside);
watchEffect(() => {
const userName = process.env.VUE_APP_USER_NAME || 'defaultName';
Cookies.set('userName', userName);
});
return {
isChrome: navigator.userAgent.toLowerCase().includes('chrome'),
hideFrame,
hideNav: !!new URL(location.href).searchParams.get('hideNav'),
asideTrigger,
};
},
});
</script>
<style>
* {
margin: 0;
padding: 0;
}
html,
body {
width: 100%;
height: 100%;
overflow: auto;
font-family: 'Microsoft YaHei', '微软雅黑';
}
.clearfix:before,
.clearfix:after {
display: table;
content: ' ';
}
.clearfix:after {
clear: both;
}
.f-left {
float: left;
}
.f-right {
float: right;
}
.text-center {
text-align: center;
}
.text-right {
text-align: right;
}
a {
color: #409eff;
text-decoration: none;
}
.app .el-scrollbar__wrap {
overflow-x: hidden;
}
.el-scrollbar__thumb {
background-color: rgba(144, 147, 153, 1);
}
.el-card .el-card__header {
font-weight: bold;
}
.m-fields-table .m-fields-relate .el-form-item {
margin-bottom: 0;
}
.el-message-box {
max-height: 100%;
overflow: auto;
}
.app .el-card__header {
padding: 10px 20px;
}
.fixed-layer {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(255, 255, 255, 0.92);
z-index: 100;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
/* 不显示面包屑导航 */
.app.hide-nav > .el-breadcrumb {
display: none;
}
</style>

View File

@ -0,0 +1,61 @@
<!-- 创建活动卡片 -->
<template>
<el-row>
<el-col :span="24">
<el-card shadow="hover" class="el-card--newact" @click="$emit('add')">
<div class="card-add--newact">+</div>
<h4>搭建活动</h4>
<div class="step-disc">
<span>新建活动</span>
<span></span>
<span>填写活动信息</span>
<span></span>
<span class="step-disc__highlight">配置前端样式</span>
<span></span>
<span>活动预览预发布</span>
</div>
</el-card>
</el-col>
</el-row>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
name: 'act-create-card',
});
</script>
<style lang="scss" scoped>
.el-card--newact {
min-height: 319px;
cursor: pointer;
position: relative;
text-align: center;
.card-add--newact {
width: 100%;
line-height: 1;
text-align: center;
font-size: 100px;
}
h4 {
font-size: 20px;
line-height: 32px;
height: 32px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.step-disc {
margin-top: 40px;
font-size: 16px;
.step-disc__highlight {
color: rgb(221, 75, 57);
}
}
}
</style>

View File

@ -0,0 +1,131 @@
<!-- 活动配置抽屉页 -->
<template>
<el-button ref="showActInfo" size="small" type="text" :icon="Setting" @click="buttonHandler()">活动配置</el-button>
<teleport to="body">
<el-drawer
ref="actDrawer"
custom-class="magic-editor-app-drawer"
title="活动配置"
v-model="appDrawerVisibility"
:direction="direction"
size="600px"
>
<m-form
size="small"
ref="configForm"
:init-values="values"
:config="config"
@change="configChangeHandler"
></m-form>
</el-drawer>
</teleport>
</template>
<script lang="ts">
import { computed, defineComponent, nextTick, ref, watch } from 'vue';
import { Setting } from '@element-plus/icons';
import { ElMessage } from 'element-plus';
import { drawerFormConfig } from '@src/config/drawer-config';
import magicStore from '@src/store/index';
import type { PageInfo } from '@src/typings';
export default defineComponent({
name: 'act-info-drawer',
setup() {
const configForm = ref<any>();
const appDrawerVisibility = ref(false);
const pagesName = ref<string[]>([]);
const pages = computed<PageInfo[]>(() => magicStore.get('pages'));
const drawerForm = drawerFormConfig(pagesName);
const config = ref(drawerForm);
const configChangeHandler = async function () {
try {
const values = await configForm.value.submitForm();
magicStore.set('actInfo', values);
} catch (e: any) {
ElMessage({
type: 'error',
duration: 10000,
showClose: true,
message: e.message,
dangerouslyUseHTMLString: true,
});
}
};
const buttonHandler = () => {
appDrawerVisibility.value = true;
};
watch(appDrawerVisibility, (visible: boolean) => {
if (visible) {
nextTick(() => configForm.value);
pages.value.forEach((page) => {
pagesName.value.push(page.pageName);
});
}
});
return {
Setting,
appDrawerVisibility,
direction: 'rtl',
config,
configForm,
values: computed(() => magicStore.get('actInfo')),
configChangeHandler,
buttonHandler,
};
},
});
</script>
<style lang="scss">
.magic-editor-app-drawer {
// element-uibughttps://github.com/ElemeFE/element/issues/18448
&:focus {
outline: none !important;
box-shadow: none !important;
}
*:focus {
outline: none !important;
box-shadow: none !important;
}
.el-drawer {
overflow-y: scroll;
&::-webkit-scrollbar {
width: 3px;
}
&::-webkit-scrollbar-thumb {
background: #d8d8d8;
border-radius: 10px;
}
&::-webkit-scrollbar-track-piece {
background: transparent;
}
&:focus {
outline-color: #ffffff;
}
}
&__form {
min-height: 100%;
padding: 0 10px;
.el-form-item {
margin-bottom: 10px;
}
.el-form-item__error {
padding: 0;
}
.tool-bar {
border-top: 1px solid #d8dee8;
background-color: #f8fbff;
height: 40px;
padding: 6px;
text-align: right;
}
}
}
</style>

View File

@ -0,0 +1,276 @@
<!-- 活动列表 -->
<template>
<div>
<el-card class="box-card" shadow="always">
<template #header>
<el-row :gutter="20">
<el-col :span="2"><span>活动列表</span></el-col>
<el-col :span="1">
<el-button id="create" type="primary" @click="newHandler" size="mini"> 新建活动 </el-button>
</el-col>
<el-col :span="3" :offset="9">
<!-- 活动状态选项框 -->
<el-select
v-model="query.where.actStatus"
placeholder="请选择状态"
size="small"
style="width: 100%"
@change="actStatusChangeHandle"
>
<el-option label="全部活动" :value="-1"> </el-option>
<el-option v-for="(value, index) in actStatus" :key="index" :label="value" :value="index"> </el-option>
</el-select>
</el-col>
<el-col :span="6">
<!-- 活动搜索框 -->
<el-input
v-model="query.where.search"
placeholder="输入活动ID活动名称加密ID创建人查询.."
@change="searchChangeHandler"
size="small"
></el-input>
</el-col>
<el-col :span="3">
<el-input
v-model="query.where.pageTitle"
placeholder="页面标题"
@change="pageTitleChangeHandler"
size="small"
></el-input>
</el-col>
</el-row>
</template>
<m-table :data="tableData.res" :config="columns" @sort-change="sortChange" />
<div class="bottom clearfix" style="margin-top: 10px; text-align: right">
<el-pagination
v-if="tableData.res?.total > 12"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="query.pgIndex + 1"
:page-size="query.pgSize"
:total="tableData.res.total"
layout="total, sizes, prev, pager, next, jumper"
>
</el-pagination>
</div>
</el-card>
<form-dialog
:visible="formDialogVisible"
:values="actValues"
:action="action"
@afterAction="afterAction"
@close="closeFormDialogHandler"
:config="formConfig"
title="新建活动"
></form-dialog>
</div>
</template>
<script lang="ts">
import { reactive, ref, toRefs } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { defineComponent, onMounted, watch } from '@vue/runtime-core';
import { ElMessage } from 'element-plus';
import Cookies from 'js-cookie';
import actApi, { ActListItem, ActListQuery, ActListRes, CopyInfo, OrderItem } from '@src/api/act';
import FormDialog from '@src/components/form-dialog.vue';
import MTable from '@src/components/table.vue';
import { getActListFormConfig } from '@src/config/act-list-config';
import { BlankActFormConfig } from '@src/config/blank-act-config';
import { ActStatus } from '@src/config/status';
import type { ActFormValue, ColumnItem } from '@src/typings';
import { status } from '@src/use/use-status';
import { Res } from '@src/util/request';
import { datetimeFormatter } from '@src/util/utils';
export default defineComponent({
name: 'act-list',
components: { FormDialog, MTable },
setup() {
const actStatus = [...status.actStatus];
const pageStatus = [...status.pageStatus];
const router = useRouter();
const route = useRoute();
const userName = Cookies.get('userName') ?? '';
const query = reactive<ActListQuery>({
where: {
onlySelf: route.params.type === 'my',
search: route.params.query,
pageTitle: '',
actStatus: route.params.status ? parseInt(route.params.status as string, 10) : ActStatus.ALL,
},
orderBy: [{ columnName: 'actId', direction: 'descending' }],
pgIndex: +route.params.page,
pgSize: 10,
userName,
});
const actValues = reactive<{ data: ActFormValue }>({
data: {
operator: '',
actBeginTime: '',
actEndTime: '',
},
});
const tableData = reactive<{ res: ActListRes }>({
res: { data: [], fetch: false, errorMsg: '', total: 0 },
});
const formDialogVisible = ref<boolean>(false);
//
const getActs = async () => {
const res = await actApi.getList({
data: query,
});
res?.fetch
? (tableData.res = res)
: ElMessage({
message: res?.errorMsg || '拉取活动列表失败',
type: 'error',
duration: 5000,
showClose: true,
});
};
const pageStatusFormatter = (v: string | number) => {
if (typeof v === 'number') {
return pageStatus[v];
}
return '';
};
const actStatusFormatter = (v: string | number) => {
if (typeof v === 'number') {
return actStatus[v];
}
return '';
};
const copyActHandler = async (row: ActListItem) => {
if (row.operator !== userName) {
ElMessage.error('复制失败!不是这个活动的创建人不能复制');
return;
}
const copyInfo: CopyInfo = {
actId: row.actId,
userName: userName || '',
};
const res = await actApi.copyAct({ data: copyInfo });
if (res?.ret === 0) {
ElMessage.success(res.msg || '复制成功');
} else {
ElMessage.error(res?.msg || '复制失败');
}
};
const copyActAfterHandler = async () => {
await getActs();
};
const columns: ColumnItem[] = getActListFormConfig(
pageStatusFormatter,
actStatusFormatter,
router,
copyActHandler,
copyActAfterHandler,
);
onMounted(async () => {
await getActs();
//
if (route.query.create) newHandler();
});
//
watch(
() => route.path,
async () => {
const { type, page, query: queryStr, status } = route.params;
tableData.res.fetch = false;
query.where.onlySelf = type === 'my';
query.pgIndex = +page;
query.where.search = queryStr || '';
query.where.actStatus = status ? parseInt(status as string, 10) : ActStatus.ALL;
await getActs();
},
);
//
const searchChangeHandler = () => {
router.push(`/act/${route.params.type}/0/${query.where.actStatus}/${query.where.search}`);
};
//
const handleCurrentChange = (val: number) => {
router.push(`/act/${route.params.type}/${val - 1}/${query.where.actStatus}/${query.where.search}`);
};
//
const actStatusChangeHandle = async (val: number) => {
router.push(`/act/${route.params.type}/0/${val}/${query.where.search}`);
};
//
const pageTitleChangeHandler = async () => {
tableData.res.fetch = false;
query.pgIndex = 0;
await getActs();
};
//
const sortChange = async (column: { prop: string; order: string }) => {
tableData.res.fetch = false;
const orderItem: OrderItem = {
columnName: column.prop,
direction: column.order,
};
query.orderBy = [orderItem];
await getActs();
};
//
const handleSizeChange = async (val: number) => {
tableData.res.fetch = false;
query.pgSize = val;
await getActs();
};
//
const newHandler = () => {
actValues.data = {
operator: userName || '',
actBeginTime: datetimeFormatter(new Date()),
actEndTime: datetimeFormatter(new Date()),
};
formDialogVisible.value = true;
};
const afterAction = (res: Res<{ actId: number }>) => {
const { actId } = res.data as any;
router.push(`/editor/${actId}`);
};
const closeFormDialogHandler = () => {
formDialogVisible.value = false;
};
return {
actStatus,
columns,
query,
tableData,
actValues: toRefs(actValues).data,
formDialogVisible,
formConfig: BlankActFormConfig,
action: actApi.saveAct,
searchChangeHandler,
actStatusChangeHandle,
pageTitleChangeHandler,
sortChange,
handleSizeChange,
handleCurrentChange,
afterAction,
closeFormDialogHandler,
newHandler,
};
},
});
</script>

View File

@ -0,0 +1,120 @@
<!-- 侧边栏 -->
<template>
<div class="m-aside">
<h1 class="logo" ref="logo">{{ aside.collapse ? '魔方' : '魔方系统' }}</h1>
<el-scrollbar :style="`height: ${height}`">
<el-aside
class="app-aside"
style="overflow-y: auto; height: 100%"
:class="{ collapse: aside.collapse }"
:width="aside.collapse ? '64px' : '200px'"
>
<el-menu
v-if="aside.data.length"
background-color="#f8fbff"
text-color="#353140"
active-text-color="#fff"
unique-opened
:router="true"
:collapse="aside.collapse"
:default-active="defaultActive"
>
<template v-for="menu in aside.data">
<el-menu-item v-if="!menu.items" :index="menu.url" :key="menu.url">
<i :class="menu.icon"></i>
<template #title>
<span>{{ menu.text }}</span>
</template>
</el-menu-item>
<el-sub-menu :index="menu.url" v-else :key="menu.url">
<template #title>
<i :class="menu.icon"></i>
<span>{{ menu.text }}</span>
</template>
<el-menu-item v-for="item in menu.items" :index="menu.url + item.url" :key="menu.url + item.url">
{{ item.text }}
</el-menu-item>
</el-sub-menu>
</template>
</el-menu>
</el-aside>
</el-scrollbar>
</div>
</template>
<script lang="ts">
import { ComponentInternalInstance, computed, defineComponent, getCurrentInstance, inject, onMounted, ref } from 'vue';
import { useRoute } from 'vue-router';
import type { AsideState } from '@src/typings';
export default defineComponent({
name: 'm-aside',
setup() {
const height = ref('auto');
const aside = inject('aside') as AsideState;
onMounted(() => {
//
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const logoHeight = (proxy?.$refs.logo as Element)?.clientHeight || 40;
height.value = `${window.document.body.clientHeight - logoHeight}px`;
window.addEventListener('resize', () => {
height.value = `${window.document.body.clientHeight - logoHeight}px`;
});
});
return {
aside,
height,
//
defaultActive: computed(() => {
const route = useRoute();
if (route.path.startsWith('/act/my')) {
return '/act/my';
}
if (route.path.startsWith('/act/all')) {
return '/act/all';
}
return route.path;
}),
};
},
});
</script>
<style lang="scss">
.m-aside {
background: #f8fbff url(https://puui.qpic.cn/vupload/0/1572869106200_gxvvrqpf1g.png/0) no-repeat bottom;
background-size: contain;
.logo {
text-align: center;
height: 40px;
line-height: 40px;
background: #2882e0;
color: #fff;
font-weight: 200;
font-size: 22px;
}
.el-aside .el-menu {
border: none;
}
.el-aside .el-menu-item.is-active,
.el-aside .el-sub-menu .el-menu-item.is-active {
background: #2882e0 !important;
}
.app-aside {
transition: width ease 0.4s;
}
.app-aside.collapse {
overflow: visible;
}
.app-aside .el-sub-menu .el-menu-item {
background: #f8fbff !important;
}
}
</style>

View File

@ -0,0 +1,144 @@
<!-- 新建活动对话框 -->
<template>
<el-dialog
custom-class="m-dialog"
top="10%"
:title="title"
:model-value="dialogVisible"
:appendToBody="true"
:close-on-click-modal="false"
:before-close="closeHandler"
>
<div class="m-dialog-body" :style="`max-height: ${bodyHeight}; overflow-y: auto; overflow-x: hidden;`">
<m-form v-if="dialogVisible" ref="form" :config="config" :init-values="formInitValues"></m-form>
<slot></slot>
</div>
<template #footer>
<el-row class="dialog-footer">
<el-col :span="12" style="text-align: left">
<div style="min-height: 1px">
<slot name="left"></slot>
</div>
</el-col>
<el-col :span="12">
<slot name="footer">
<el-button @click="$emit('close')" size="small"> </el-button>
<el-button type="primary" size="small" :loading="saveFetch" @click="save">确定</el-button>
</slot>
</el-col>
</el-row>
</template>
</el-dialog>
</template>
<script lang="ts">
import { computed, defineComponent, PropType, ref } from 'vue';
import { ElMessage } from 'element-plus';
import { MForm } from '@tmagic/form';
import type { ActFormValue, FormConfigItem } from '@src/typings';
import { Res } from '@src/util/request';
export default defineComponent({
name: 'm-form-dialog',
props: {
values: {
type: Object as PropType<ActFormValue>,
default: () => ({}),
},
title: String,
config: {
type: Array as PropType<FormConfigItem[]>,
default: () => [],
},
visible: {
type: Boolean,
default: () => false,
},
action: {
type: Function as PropType<(options: { data: any }) => Res>,
},
},
emits: ['afterAction', 'close'],
setup(props, { emit }) {
const form = ref<InstanceType<typeof MForm>>();
const saveFetch = ref(false);
const dialogVisible = computed(() => props.visible);
const formInitValues = computed(() => props.values);
//
const closeHandler = () => {
emit('close');
form.value?.resetForm();
};
//
const save = async () => {
if (saveFetch.value) {
return;
}
saveFetch.value = true;
try {
const values = await form.value?.submitForm();
if (!values) {
emit('close');
return;
}
const res = await props.action?.({ data: values });
if (res) {
if (res.ret === 0) {
ElMessage.success(res.msg || '保存成功');
emit('close');
emit('afterAction', res);
} else {
ElMessage({
type: 'error',
duration: 10000,
showClose: true,
dangerouslyUseHTMLString: true,
message: res.msg || '保存失败',
});
}
} else {
emit('close');
}
} catch (e) {
ElMessage({
type: 'error',
duration: 10000,
showClose: true,
message: (e as Error).message,
dangerouslyUseHTMLString: true,
});
}
saveFetch.value = false;
};
return {
dialogVisible,
saveFetch,
form,
formInitValues,
bodyHeight: ref(`${document.body.clientHeight - 194}px`),
closeHandler,
save,
};
},
});
</script>
<style>
.m-dialog .el-dialog__body {
padding: 0 !important;
}
.m-dialog .m-dialog-body {
padding: 0 20px;
}
.el-table .m-form-item .el-form-item {
margin-bottom: 0;
}
</style>

View File

@ -0,0 +1,99 @@
<!-- 导航栏 -->
<template>
<el-row class="m-header">
<el-col :span="12">
<!-- 折叠侧边栏按钮 -->
<div
v-if="!$route.meta.hideAside && aside && aside.data.length"
class="aside-trigger"
@click="$emit('asideTrigger')"
:class="{ active: aside.collapse }"
>
<i
:class="{
'el-icon-s-fold': !aside.collapse,
'el-icon-s-unfold': aside.collapse,
}"
></i>
</div>
<div class="nav-list">
<a href="/">首页</a>
<a href="https://tencent.github.io/tmagic-editor/docs/" target="_blank">帮助文档</a>
</div>
</el-col>
<el-col class="user" :span="12">
<i class="el-icon-user"></i>
<span>
<span>{{ userName }}</span>
</span>
</el-col>
</el-row>
</template>
<script lang="ts">
import { defineComponent, inject } from 'vue';
import Cookies from 'js-cookie';
import type { AsideState } from '@src/typings';
export default defineComponent({
name: 'm-header',
emits: ['asideTrigger'],
setup() {
return {
aside: inject('aside') as AsideState,
userName: Cookies.get('userName'),
};
},
});
</script>
<style lang="scss">
.m-header {
background-color: #2882e0;
.user {
height: 40px;
line-height: 40px;
text-align: right;
padding-right: 20px;
color: white;
font-size: 14px;
}
.nav-list {
height: 40px;
line-height: 40px;
color: #fff;
display: flex;
& > * {
margin-left: 10px;
padding: 0 8px;
&:hover,
&:active {
background: rgba(0, 0, 0, 0.2);
}
}
}
a {
color: white;
cursor: pointer;
}
}
.aside-trigger {
width: 40px;
height: 40px;
float: left;
position: relative;
z-index: 1;
line-height: 40px;
text-align: center;
cursor: pointer;
color: #ece5e5;
font-size: 28px;
}
</style>

View File

@ -0,0 +1,160 @@
<!-- 活动发布确认弹窗 -->
<template>
<el-button size="small" type="text" :icon="View" @click="buttonHandler()">发布</el-button>
<el-dialog append-to-body title="确认以下发布信息" v-model="publishPageListVisible">
<div class="publish-page-container">
<el-row>请勾选需要发布的页面</el-row>
<el-checkbox
class="publish-page-title"
:indeterminate="isIndeterminate"
v-model="checkAll"
@change="handleCheckAllChange"
>全选</el-checkbox
>
<div style="margin: 15px 0"></div>
<el-checkbox-group v-model="checkedPages" @change="handleCheckedPagesChange">
<el-checkbox v-for="page in pageList" :label="page" :key="page">
<div class="publish-page-title">{{ page }}</div>
</el-checkbox>
</el-checkbox-group>
<el-row class="publish-page-tip">
<p v-if="tipVisible">* 请选择需要发布的页面</p>
</el-row>
</div>
<el-row :gutter="20">
<el-col :span="8" :push="16" class="publish-page-button-group">
<el-button @click="handlePageCheckCancel">取消</el-button>
<el-button type="primary" @click="handlePageCheckConfirm" :disabled="tipVisible">确认</el-button>
</el-col>
</el-row>
</el-dialog>
</template>
<script lang="ts">
import { computed, defineComponent, reactive, ref, toRefs, watch } from 'vue';
import { useRoute } from 'vue-router';
import { View } from '@element-plus/icons';
import { ElMessage } from 'element-plus';
import { editorService } from '@tmagic/editor';
import publishApi from '@src/api/publish';
import magicStore from '@src/store/index';
import type { ABTest, ActInfo, EditorInfo, PageInfo } from '@src/typings';
import { Res } from '@src/util/request';
import { serializeConfig } from '@src/util/utils';
import { publishHandler } from '../use/use-publish';
interface PageList {
checkAll: boolean;
isIndeterminate: boolean;
pageList: string[];
checkedPages: string[];
}
export default defineComponent({
name: 'publish-page-list',
setup() {
const route = useRoute();
const root = computed(() => editorService.get('root'));
const publishPageListVisible = ref(false);
const tipVisible = computed(() => state.checkedPages.length === 0);
const state = reactive<PageList>({
checkAll: true,
isIndeterminate: false,
pageList: [],
checkedPages: [],
});
const handleCheckAllChange = (val: string[]) => {
state.checkedPages = val ? [...state.pageList] : [];
state.isIndeterminate = false;
};
const handleCheckedPagesChange = (value: string[]) => {
const checkedCount = value.length;
state.checkAll = checkedCount === state.pageList.length;
state.isIndeterminate = checkedCount > 0 && checkedCount < state.pageList.length;
};
const handlePageCheckCancel = () => {
getPageName();
state.checkAll = true;
state.isIndeterminate = false;
publishPageListVisible.value = false;
};
const handlePageCheckConfirm = async () => {
const root = computed(() => editorService.get('root'));
const rootInfo = root.value as EditorInfo;
const rootInfoString = serializeConfig(rootInfo);
const res: Res = await publishApi.publishPage({
data: {
actId: Number(route.params.actId),
publishPages: state.checkedPages,
rootInfo: rootInfoString,
},
});
if (res.ret === 0) {
ElMessage.success({
message: res.msg,
type: 'success',
});
publishPageListVisible.value = false;
} else {
ElMessage.error(res.msg);
}
};
const getPageName = () => {
// magic-editor root
state.pageList = [];
root.value?.items.forEach((item: PageInfo) => {
state.pageList.push(item.name as string);
});
const actInfo = magicStore.get('actInfo') as ActInfo;
actInfo?.abTest?.forEach((ABTestItem: ABTest) => {
state.pageList.push(ABTestItem.name);
});
state.checkedPages = [...state.pageList];
};
watch(publishPageListVisible, (visible: boolean) => {
if (visible) {
getPageName();
}
});
return {
View,
publishPageListVisible,
tipVisible,
...toRefs(state),
handleCheckAllChange,
handleCheckedPagesChange,
handlePageCheckCancel,
handlePageCheckConfirm,
async buttonHandler() {
const isSave = await publishHandler();
if (isSave) {
publishPageListVisible.value = true;
}
},
};
},
});
</script>
<style scoped>
.publish-page-title {
margin: 15px 0;
}
.publish-page-container {
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
padding: 15px;
border-radius: 4px;
}
.publish-page-button-group {
display: flex;
justify-content: center;
align-items: center;
margin-top: 10px;
}
</style>

View File

@ -0,0 +1,155 @@
<!-- 表格组件 -->
<template>
<el-table
:data="tableData?.data"
:empty-text="tableData?.errorMsg || '暂无数据'"
@sort-change="sortChange"
v-loading="!tableData?.fetch"
border
>
<!-- 解析表格配置 -->
<template v-for="(item, columnIndex) in columns">
<!-- 操作栏 -->
<el-table-column
:key="columnIndex + '1'"
v-if="item.actions"
:prop="item.prop"
:label="item.label"
:width="item.width"
:fixed="item.fixed"
>
<template #default="{ row, $index }">
<el-button
class="action-btn"
v-for="(action, actionIndex) in item.actions"
:key="actionIndex"
@click="actionHandler(action, row, $index)"
type="text"
size="small"
v-html="action.text"
></el-button>
</template>
</el-table-column>
<!-- 数据展示栏 -->
<el-table-column
v-else
:key="columnIndex + '2'"
:prop="item.prop"
:label="item.label"
:width="item.width"
:fixed="item.fixed"
:sortable="item.sortable ? item.sortable : false"
show-overflow-tooltip
:type="item.type"
>
<template #default="{ row }">
<!-- 展示为文字链接 -->
<el-button v-if="item.action === 'actionLink'" type="text" @click="item.handler(row)">
{{ formatter(item, row) }}
</el-button>
<!-- 展示为标签 -->
<el-tag v-else-if="item.action === 'tag'" :type="statusTagType[row[item.prop]]" close-transition>
{{ formatter(item, row) }}
</el-tag>
<!-- 扩展表格子表 -->
<el-table
v-else-if="item.table"
:data="row.pages"
empty-text="暂无数据"
border
size="small"
class="sub-table"
>
<!-- 解析子表 -->
<el-table-column
v-for="(column, columnIndex) in item.table"
:key="columnIndex"
:prop="column.prop"
:label="column.label"
>
<template #default="page">
{{ formatter(column, page.row) }}
</template>
</el-table-column>
</el-table>
<div v-else v-html="formatter(item, row)"></div>
</template>
</el-table-column>
</template>
</el-table>
</template>
<script lang="ts">
import { computed, defineComponent, PropType } from 'vue';
import { ActListItem, ActListRes } from '@src/api/act';
import type { ActionItem, ColumnItem } from '@src/typings';
import { status } from '@src/use/use-status';
export default defineComponent({
name: 'm-table',
props: {
data: {
type: Object as PropType<ActListRes>,
default: () => ({ data: [], fetch: true, errorMsg: '', total: 0 }),
},
config: {
type: Array,
default: () => [],
},
},
emits: ['sort-change'],
setup(props, { emit }) {
const tableData = computed(() => props.data);
const columns = computed(() => props.config);
const isValidProp = (row: object, prop: string) => prop && prop in row;
return {
tableData,
columns,
statusTagType: [...status.statusTagType],
//
actionHandler: async (action: ActionItem, row: ActListItem) => {
await action.handler?.(row);
action.after?.();
},
//
formatter: (item: ColumnItem, row: ActListItem) => {
if (!isValidProp(row, item.prop)) {
return '';
}
if (item.formatter) {
try {
return item.formatter(row[item.prop], row);
} catch (e) {
console.log((e as Error).message);
return row[item.prop];
}
} else {
return row[item.prop];
}
},
//
sortChange: (column: { prop: string; order: string }) => {
emit('sort-change', column);
},
};
},
});
</script>
<style lang="scss">
.sub-table {
margin-top: 10px;
margin-bottom: 10px;
th {
background-color: #ffffff;
color: #909399;
}
}
</style>

View File

@ -0,0 +1,119 @@
/*
* Tencent is pleased to support the open source community by making MagicEditor available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { Router } from 'vue-router';
import type { ActListItem } from '@src/api/act';
import type { ColumnItem } from '@src/typings';
import { datetimeFormatter } from '@src/util/utils';
// 活动列表表单
export const getActListFormConfig = (
pageStatusFormatter: ColumnItem['formatter'],
actStatusFormatter: ColumnItem['formatter'],
router: Router,
copyActHandler: ColumnItem['handler'],
copyActAfterHandler: ColumnItem['handler'],
): ColumnItem[] => [
{
prop: '',
type: 'expand',
table: [
{
prop: 'pageTitle',
label: '页面标题',
},
{
prop: 'pagePublishTime',
label: '页面发布时间',
formatter: datetimeFormatter,
},
{
prop: 'pagePublishStatus',
label: '页面状态',
formatter: pageStatusFormatter,
},
{
prop: 'pagePublishOperator',
label: '发布人',
formatter: (v: string | number | Date) => (v as string) || '-',
},
],
},
{
prop: 'actId',
label: '活动ID',
width: '100',
sortable: 'custom',
formatter: (v: string | number | Date) => `<span style="user-select: text;">${v}</span>`,
},
{
prop: 'actName',
label: '活动名称',
action: 'actionLink',
handler: (row: ActListItem) => {
router.push(`/editor/${row.actId}`);
},
},
{
prop: 'actBeginTime',
label: '开始时间',
formatter: datetimeFormatter,
sortable: 'custom',
},
{
prop: 'actEndTime',
label: '结束时间',
formatter: datetimeFormatter,
sortable: 'custom',
},
{
prop: 'actStatus',
label: '活动状态',
action: 'tag',
formatter: actStatusFormatter,
},
{
prop: 'operator',
label: '创建人',
},
{
prop: 'actCryptoId',
label: '活动ID加密KEY',
width: '220',
formatter: (v: string | number | Date) => `<span style="user-select: text;">${v}</span>`,
},
{
prop: '',
label: '操作',
actions: [
{
text: '查看',
handler: (row: ActListItem) => {
router.push(`/editor/${row.actId}`);
},
},
{
text: '复制',
type: 'copy',
handler: copyActHandler,
after: copyActAfterHandler,
},
],
},
];

View File

@ -0,0 +1,50 @@
/*
* Tencent is pleased to support the open source community by making MagicEditor available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// 编辑器左侧边栏表单配置
export const AsideFormConfig = {
data: [
{
id: 1,
url: '/act',
icon: 'el-icon-date',
text: '活动管理',
items: [
{
id: 101,
url: '/create',
icon: '',
text: '新建活动',
},
{
id: 102,
url: '/my',
icon: '',
text: '我的活动',
},
{
id: 103,
url: '/all',
icon: '',
text: '全部活动',
},
],
},
],
collapse: false,
};

View File

@ -0,0 +1,48 @@
/*
* Tencent is pleased to support the open source community by making MagicEditor available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// 新建活动表单配置
export const BlankActFormConfig = [
{ name: 'actName', text: '活动名称', rules: [{ required: true, message: '请输入活动名称', trigger: 'blur' }] },
{
names: ['actBeginTime', 'actEndTime'],
text: '活动时间',
type: 'daterange',
rules: [
{ required: true, message: '请输入活动时间', trigger: 'blur' },
{
trigger: 'blur',
validator: (
args: { callback: (arg0?: string | undefined) => void },
data: { model: { c_b_time: string | number | Date; c_e_time: string | number | Date } },
) => {
const start = new Date(data.model.c_b_time);
const end = new Date(data.model.c_e_time);
if (start > end) args.callback('结束有效期不能小于开始期');
else args.callback();
},
},
],
},
{
name: 'operator',
text: '创建人',
rules: [{ required: true }],
disabled: true,
},
];

View File

@ -0,0 +1,126 @@
/*
* Tencent is pleased to support the open source community by making MagicEditor available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Ref } from 'vue';
import magicStore from '@src/store/index';
import type { PageInfo } from '@src/typings';
export const drawerFormConfig = (pagesName: Ref<string[]>) => [
{
type: 'tab',
active: '0',
items: [
{
title: '属性',
labelWidth: '100px',
items: [
{
text: '活动ID',
name: 'actId',
type: 'display',
},
{
text: '活动名称',
name: 'actName',
type: 'text',
rules: [{ required: true, message: '请输入活动名称', trigger: 'blur' }],
},
{
name: 'type',
type: 'hidden',
},
{
names: ['actBeginTime', 'actEndTime'],
text: '活动时间',
type: 'daterange',
rules: [{ required: true, message: '请输入活动时间', trigger: 'blur' }, { trigger: 'blur' }],
},
{
name: 'operator',
text: '责任人',
type: 'text',
rules: [{ required: true, message: '请输入责任人', trigger: 'blur' }],
},
{
type: 'fieldset',
text: 'abTest',
labelWidth: '100px',
items: [
{
type: 'groupList',
name: 'abTest',
defaultValue: [],
items: [
{
text: '名称',
name: 'name',
type: 'text',
rules: [
{ required: true, message: '请输入内容', trigger: 'blur' },
{
trigger: 'blur',
validator: ({ value, callback }: any) => {
if (pagesName.value.includes(value)) {
callback(new Error('测试页面不能与页面名称重名'));
} else {
callback();
}
},
},
],
},
{
text: '分桶方式',
name: 'type',
type: 'select',
multiple: true,
options: [{ text: 'pgv_pvid', value: 'pgv_pvid' }],
},
{
type: 'table',
name: 'pageList',
items: [
{
type: 'select',
name: 'pageName',
label: '页面',
width: 100,
align: 'top',
options: () =>
magicStore.get<PageInfo[]>('pages').map((item: PageInfo) => ({
text: item.pageName,
value: item.pageName,
})),
},
{
name: 'proportion',
label: '比例',
append: '%',
},
],
},
],
},
],
},
],
},
],
},
];

View File

@ -0,0 +1,31 @@
/*
* Tencent is pleased to support the open source community by making MagicEditor available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// 活动状态
export enum ActStatus {
ALL = -1, // 查询传参使用:全部状态占位
MODIFYING, // 修改中
PART_PUBLISHED, // 部分页面已发布
PUBLISHED, // 全部页面已发布
}
// 页面状态
export enum PageStatus {
MODIFYING = 0, // 修改中
PUBLISHED, // 已预发布
}

View File

@ -0,0 +1,41 @@
/*
* Tencent is pleased to support the open source community by making MagicEditor available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createApp } from 'vue';
import ElementPlus from 'element-plus';
import MagicEditor, { editorService } from '@tmagic/editor';
import MagicForm from '@tmagic/form';
import App from '@src/app.vue';
import editorPlugin from '@src/plugins/editor';
import router from '@src/router';
import installComponents from '@src/use/use-comp';
import 'element-plus/dist/index.css';
import '@tmagic/editor/dist/style.css';
import '@tmagic/form/dist/style.css';
const app = createApp(App);
app.use(ElementPlus);
app.use(MagicEditor);
editorService.usePlugin(editorPlugin);
app.use(MagicForm);
app.use(router);
installComponents(app);
app.mount('#app');

View File

@ -0,0 +1,62 @@
/*
* Tencent is pleased to support the open source community by making MagicEditor available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ElMessage, ElMessageBox } from 'element-plus';
import { MNode } from '@tmagic/schema';
import actApi from '@src/api/act';
export default {
/**
* (hook)
* @returns void
*/
beforeRemove: async () => {
try {
await ElMessageBox.confirm('确认删除该页面吗?', '提示', {
confirmButtonText: '删除',
cancelButtonText: '取消',
type: 'warning',
center: true,
});
} catch (error) {
throw new Error('delete canceled');
}
},
/**
* (hook)
* @param {MNode} config
* @returns void
*/
afterRemove: async (config: MNode) => {
const pageId = Number(config.id);
try {
await actApi.removePage({ data: { pageId } });
ElMessage({
type: 'success',
message: '页面删除成功',
});
} catch (error) {
ElMessage({
type: 'error',
message: '页面删除失败',
});
}
},
};

View File

@ -0,0 +1,26 @@
/*
* Tencent is pleased to support the open source community by making MagicEditor available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import ElementPlus from 'element-plus';
import locale from 'element-plus/lib/locale/lang/zh-cn';
import 'element-plus/lib/theme-chalk/index.css';
export default (app: any) => {
app.use(ElementPlus, { locale });
};

View File

@ -0,0 +1,52 @@
/*
* Tencent is pleased to support the open source community by making MagicEditor available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';
const routes: Array<RouteRecordRaw> = [
// 活动列表路由
{
path: '/',
redirect: '/act/my/0',
},
{
path: '/act/:type/:page?/:status?/:query?',
name: 'ActList',
component: () => import('@src/views/list-view.vue'),
},
{
path: '/act/create',
name: 'NewAct',
component: () => import('@src/views/template-list.vue'),
},
// 编辑器路由
{
path: '/editor/:actId',
name: 'Editor',
meta: {
hideAside: true,
},
component: () => import(/* webpackChunkName: "editor" */ '@src/views/editor.vue'),
},
];
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes,
});
export default router;

5
magic-admin/web/src/shims-vue.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
declare module '*.vue' {
import type { DefineComponent } from 'vue';
const component: DefineComponent<{}, {}, any>;
export default component;
}

View File

@ -0,0 +1,39 @@
/*
* Tencent is pleased to support the open source community by making MagicEditor available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { reactive } from 'vue';
import type { MagicStore, MagicStoreType } from '@src/typings';
const state = reactive<MagicStore>({
actInfo: {},
pages: [],
uiConfigs: {},
editorDefaultSelected: '',
});
export default {
set<T = MagicStoreType>(name: keyof MagicStore, value: T) {
(state as any)[name] = value;
console.log('admin store set ', name, ' ', value);
},
get<T = MagicStoreType>(name: keyof typeof state): T {
return (state as any)[name];
},
};

178
magic-admin/web/src/typings/index.d.ts vendored Normal file
View File

@ -0,0 +1,178 @@
/*
* Tencent is pleased to support the open source community by making MagicEditor available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export type MagicStoreType = ActInfo | PageInfo[] | UiConfig;
// 全局通用store
export interface MagicStore {
// 活动基础信息
actInfo: ActInfo | {};
// 页面信息及组件配置内容
pages: PageInfo[];
// 完整活动配置
uiConfigs: UiConfig | {};
// 编辑器默认选中节点ID
editorDefaultSelected: string | number;
}
// 表单项配置
export interface FormRuleItem {
required?: boolean;
message?: string;
trigger?: string;
validator?: (args: any, data: any) => void;
}
// 表单配置
export interface FormConfigItem {
name?: string;
names?: string[];
items?: FormConfigItem[];
text?: string;
placeholder?: string;
inputType?: string;
rules?: FormRuleItem[];
type?: string;
}
// 活动基础信息定义
export interface ActInfo {
/** 活动id */
actId: number;
/** 活动加密id */
actCryptoId: string;
/** 活动名称 */
actName: string;
/** 活动开始时间 */
actBeginTime?: string;
/** 活动结束时间 */
actEndTime?: string;
/** 活动修改时间 */
actModifyTime?: string;
/** 活动创建时间 */
actCreateTime?: string;
/** 操作人 */
operator: string;
/** 锁定人(预留) */
locker?: string;
/** 锁定时间(预留) */
lockTime?: string;
/** 活动状态0:修改中 1部分已发布 2已发布 */
actStatus: int;
/** abtest配置 */
abTest?: abTest[];
}
// 活动页面信息定义
export interface PageInfo {
actId?: number;
id: number;
pageTitle: string;
pageName: string;
pageCreateTime?: string;
pageModifyTime?: string;
pagePublishStatus?: number;
pagePublishTime?: string;
pagePublishOperator?: string;
pagePublishUiVersion?: string;
srcCode?: any;
distCode?: string;
plugins?: string[];
items?: [];
type?: string;
title?: string;
name?: string;
}
// 编辑器组件配置定义
export interface UiConfig {
actId: number;
type: string;
id?: string;
name?: string;
operator?: string;
items?: PageInfo[];
abTest?: ABTest[];
useLastPackage?: string;
}
// 活动页面abtest定义
export interface ABTest {
name: string;
type: string;
pageList?: ABTestPageList[];
}
// 活动页面abtest pagelist定义
export interface ABTestPageList {
pageTitle: string;
proportion: string;
}
// 从editor拿到的活动页面信息
export interface EditorInfo {
type: string;
items: PageInfo[];
id?: number | string;
name?: string;
}
// 新建活动的初始值类型
export interface ActFormValue {
operator: string;
actBeginTime: string;
actEndTime: string;
}
// 侧边栏配置
export interface AsideState {
data: {
id: number;
url: string;
icon: string;
text: string;
items?: {
id: number;
url: string;
icon: string;
text: string;
}[];
}[];
collapse: boolean;
}
// m-table表格栏配置项
export interface ColumnItem {
prop: string;
label?: string;
width?: string;
sortable?: string;
action?: string;
formatter?: (v: string | number, row?: ActListItem) => string;
type?: ((v: string | number, row?: ActListItem) => string) | string;
handler?: (row?: ActListItem) => void | Promise<void>;
showHeader?: boolean;
table?: ColumnItem[];
fixed?: string;
actions?: ActionItem[];
}
// 表格项操作
export interface ActionItem {
text: string;
handler?: (row: ActListItem) => void;
type?: string;
after?: () => void;
display?: (v?: string, row?: ActListItem) => boolean;
}

View File

@ -0,0 +1,28 @@
/*
* Tencent is pleased to support the open source community by making MagicEditor available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import MAside from '@src/components/aside.vue';
import MHeader from '@src/components/header.vue';
const components: any[] = [MAside, MHeader];
export default (app: any) => {
components.forEach((comp: any) => {
app.component(comp?.name, comp);
});
};

View File

@ -0,0 +1,68 @@
/*
* Tencent is pleased to support the open source community by making MagicEditor available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// 编辑器顶部菜单栏
import { ArrowLeft, Document, Finished } from '@element-plus/icons';
import { MenuBarData } from '@tmagic/editor';
import ActInfoDrawer from '@src/components/act-info-drawer.vue';
import PublishPageList from '@src/components/publish-page-list.vue';
import router from '@src/router';
import { commitHandler } from '@src/use/use-publish';
export const topMenu = (): MenuBarData => ({
left: [
{
type: 'button',
text: '返回',
icon: ArrowLeft,
handler: (): void => {
if (router) {
router.push('/');
}
},
},
],
center: ['delete', 'undo', 'redo', 'zoom-in', 'zoom-out'],
right: [
{
type: 'button',
text: '保存',
icon: Finished,
handler: (): void => {
commitHandler();
},
},
{
type: 'component',
component: PublishPageList,
},
{
type: 'component',
component: ActInfoDrawer,
},
{
type: 'button',
icon: Document,
text: '源码',
handler: (service) => service?.uiService.set('showSrc', !service?.uiService.get('showSrc')),
},
],
});

View File

@ -0,0 +1,127 @@
/*
* Tencent is pleased to support the open source community by making MagicEditor available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { computed } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { editorService } from '@tmagic/editor';
import actApi from '@src/api/act';
import publishApi from '@src/api/publish';
import magicStore from '@src/store/index';
import type { ActInfo, EditorInfo, PageInfo } from '@src/typings';
import { serializeConfig } from '@src/util/utils';
let actInfoSave: ActInfo;
let pagesSave: EditorInfo;
// 根据活动id查询活动并初始化为编辑器所需格式
export const initConfigByActId = async ({ actId }: { actId: number }) => {
const res = await actApi.getAct({ id: Number(actId) });
if (res.ret !== 0) {
ElMessage.error(res.msg || '活动查询失败!');
return;
}
const { pages, ...actInfo } = res.data;
const pageItems: any[] = [];
pages?.forEach((page: PageInfo) => {
if (page.srcCode) {
// 可能包含组件自定义添加的code代码并非纯粹的json对象
/* eslint-disable-next-line */
page.srcCode = eval(`(${page.srcCode})`);
pageItems.push(page.srcCode.items[0]);
} else {
pageItems.push({
id: page.id,
type: 'page',
name: page.pageTitle,
title: page.pageTitle,
style: {
height: 728,
width: 375,
},
items: [],
});
}
});
const uiConfigs = {
type: 'app',
id: actInfo.actId,
items: pageItems,
abTest: actInfo.abTest || [],
};
magicStore.set('actInfo', actInfo);
magicStore.set('pages', pages);
magicStore.set('uiConfigs', uiConfigs);
magicStore.set('editorDefaultSelected', pageItems[0]?.id);
};
// 编辑器保存方法
export const commitHandler = async () => {
const actInfo = computed(() => magicStore.get('actInfo') as unknown as ActInfo);
// 从magic-editor root 拿到最新的页面信息
const root = computed(() => editorService.get('root'));
console.log('从magic-editor root 拿到最新的页面信息: ', root);
const rootInfo = root.value as unknown as EditorInfo;
const rootInfoString = serializeConfig(rootInfo);
const res = await publishApi.saveActInfo({
data: {
actInfo: actInfo.value,
rootInfo: rootInfoString,
},
});
if (res.ret === 0) {
ElMessage.success({
message: res.msg,
type: 'success',
onClose: () => initConfigByActId({ actId: actInfo.value.actId }),
});
} else {
ElMessage.error(res.msg);
}
};
// 编辑器发布方法
const saveAndContinue = async (step: string) => {
try {
await ElMessageBox.confirm(`有修改未保存,是否先保存再${step}`, '提示', {
confirmButtonText: `保存并${step}`,
cancelButtonText: step,
type: 'warning',
});
await commitHandler();
} catch (err) {
console.error(err);
}
};
const isSaved = () => {
const actInfo = computed<ActInfo>(() => magicStore.get('actInfo'));
if (actInfo.value === actInfoSave || !actInfoSave) {
const root = computed(() => editorService.get('root'));
const newPages = root.value;
if (pagesSave === newPages || !pagesSave) {
return true;
}
}
return false;
};
export const publishHandler = async () => {
if (!isSaved()) {
await saveAndContinue('发布');
return true;
}
return true;
};

View File

@ -0,0 +1,23 @@
/*
* Tencent is pleased to support the open source community by making MagicEditor available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export const status = {
actStatus: ['修改中', '部分已发布', '已发布'],
pageStatus: ['修改中', '已发布'],
statusTagType: ['info', '', 'success'],
};

View File

@ -0,0 +1,184 @@
/*
* Tencent is pleased to support the open source community by making MagicEditor available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
import axiosJsonp from 'axios-jsonp';
interface ReqOptions<T> {
url?: string;
method?: 'GET' | 'POST';
headers?: Record<string, any>;
withCredentials?: boolean;
json?: boolean;
cache?: boolean;
mode?: string;
_a?: string;
_c?: string;
data?: string | T;
timeout?: number;
[key: string]: any;
}
export interface OrderItem {
columnName: string;
direction: string;
}
/**
*
*/
export interface Query {
// 排序
orderBy: OrderItem[];
// 分页时,当前页数
pgIndex: number;
// 分页时,当前总数据数
pgSize: number;
// 其他过滤数据
query?: string;
[key: string]: any;
}
/**
*
*/
export interface Res<T = any> {
ret: number;
msg: string;
data?: T;
}
/**
*
*/
export interface ListRes<T> {
ret: number;
msg: string;
data?: T[];
total?: number;
}
// 缓存数据当设置了cache为true时启用
const cacheDataMap: Record<string, any> = {};
// 缓存请求,用于同一个请求连续发起时
const cacheRequestMap: Record<string, any> = {};
// 将json数据转换为key=value形式
const json2url = function (json: { [key: string]: any }): string {
const arr: string[] = [];
Object.entries(json).forEach(([i, item]) => arr.push(`${i}=${item}`));
return arr.join('&');
};
const base = '/api';
const requestFuc = function <T>(options: ReqOptions<T>) {
const method = (options.method || 'POST').toLocaleUpperCase();
const url = `${base}/${options._c}/${options._a}`;
delete options._a;
delete options._c;
let body = null;
if (options.json === true) {
try {
body = JSON.stringify(options.data);
} catch (e) {
console.error('options.data cannot transform to a json string');
}
} else if (typeof options.data === 'string') {
body = `data=${options.data}`;
} else if (options.data) {
body = `data=${encodeURIComponent(JSON.stringify(options.data))}`;
} else if (!options.url) {
body = json2url(options);
}
const config = {
...options,
url,
headers: Object.assign(
{
Accept: 'application/json, text/javascript, */*',
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
},
options.json === true
? {
'Content-Type': 'application/json',
}
: {},
options.headers || {},
),
withCredentials: options.withCredentials ?? true,
mode: options.mode || 'no-cors',
data: method === 'POST' ? body : null,
responseType: 'json',
};
const storageKey = `m-fetch-post-cache://${url}?${body}`;
if (options.cache && cacheDataMap[storageKey]) {
return Promise.resolve(cacheDataMap[storageKey]);
}
if (cacheRequestMap[storageKey]) {
return new Promise((resolve) => {
cacheRequestMap[storageKey].push(resolve);
});
}
cacheRequestMap[storageKey] = [];
return axios
.request(config as AxiosRequestConfig)
.then((response) => {
if (cacheRequestMap[storageKey]?.length) {
cacheRequestMap[storageKey].forEach((resolve: (arg0: AxiosResponse<any>) => any) => resolve(response));
}
delete cacheRequestMap[storageKey];
if (options.cache) {
cacheDataMap[storageKey] = response;
}
return response;
})
.catch((error) => {
console.error(error);
});
};
export default {
request<T>(opitons: ReqOptions<T>): Promise<AxiosResponse<Res>> {
return requestFuc(opitons);
},
post<T>(options: ReqOptions<T>): Promise<Res & any> {
options.method = 'POST';
return requestFuc(options).then((response) => response?.data);
},
get<T>(options: ReqOptions<T>): Promise<Res> {
options.method = 'GET';
return requestFuc(options).then((response) => response?.data);
},
jsonp<T>(options: ReqOptions<T>): Promise<Res> {
return axiosJsonp(options).then((res) => res.data);
},
};

View File

@ -0,0 +1,29 @@
/*
* Tencent is pleased to support the open source community by making MagicEditor available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Cookies from 'js-cookie';
import { getUrlParam } from '@src/util/url';
const env = getUrlParam('magic_env');
if (env) {
Cookies.set('env', env === 'test' ? env : 'production', {
expires: 365,
path: '/',
});
}

View File

@ -0,0 +1,38 @@
/*
* Tencent is pleased to support the open source community by making MagicEditor available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export const filterXSS = function (str: string): string {
return str.replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&apos;');
};
export const getUrlParam = function (p: string, url?: string): string {
const u = url || location.href;
const reg = new RegExp(`[?&#]${p}=([^&#]+)`, 'gi');
const matches = u.match(reg);
let strArr;
if (matches && matches.length > 0) {
strArr = matches[matches.length - 1].split('=');
if (strArr && strArr.length > 1) {
// 过滤XSS字符
return filterXSS(strArr[1]);
}
return '';
}
return '';
};

View File

@ -0,0 +1,44 @@
/*
* Tencent is pleased to support the open source community by making MagicEditor available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import momentTimezone from 'moment-timezone';
import serialize from 'serialize-javascript';
import { EditorInfo } from '@src/typings';
export const datetimeFormatter = function (v: string | number | Date): string {
if (v) {
let time = null;
time = momentHandler(v);
// 格式化为北京时间
if (time !== 'Invalid date') {
return time;
}
return '-';
}
return '-';
};
const momentHandler = (v: string | number | Date) =>
momentTimezone.tz(v, 'Asia/Shanghai').format('YYYY-MM-DD HH:mm:ss');
export const serializeConfig = function (value: EditorInfo): string {
return serialize(value, {
space: 2,
unsafe: true,
}).replace(/"(\w+)":\s/g, '$1: ');
};

View File

@ -0,0 +1,86 @@
<template>
<m-editor
:menu="menu"
:runtime-url="runtimeUrl"
:component-group-list="componentList"
:modelValue="uiConfigs"
:props-values="magicPresetValues"
:props-configs="magicPresetConfigs"
:event-method-list="magicPresetEvents"
:default-selected="editorDefaultSelected"
></m-editor>
</template>
<script lang="ts">
import { computed, defineComponent, reactive, ref } from 'vue';
import { useRoute } from 'vue-router';
import { ComponentGroup } from '@tmagic/editor';
import { asyncLoadJs } from '@tmagic/utils';
import editorApi from '@src/api/editor';
import magicStore from '@src/store/index';
import { topMenu } from '@src/use/use-menu';
import { initConfigByActId } from '@src/use/use-publish';
export default defineComponent({
name: 'App',
setup() {
const route = useRoute();
const uiConfigs = computed(() => magicStore.get('uiConfigs'));
const editorDefaultSelected = computed(() => magicStore.get('editorDefaultSelected'));
const componentList = reactive<ComponentGroup[]>([]);
const magicPresetValues = ref<Record<string, any>>({});
const magicPresetConfigs = ref<Record<string, any>>({});
const magicPresetEvents = ref<Record<string, any>>({});
//
const getComponentList = async () => {
const { data: list } = await editorApi.getComponentList();
componentList.push(...list);
};
// id
const getActById = async () => {
await initConfigByActId({ actId: Number(route.params.actId) });
};
asyncLoadJs('/runtime/vue3/assets/config.js').then(() => {
magicPresetConfigs.value = (window as any).magicPresetConfigs;
});
asyncLoadJs('/runtime/vue3/assets/value.js').then(() => {
magicPresetValues.value = (window as any).magicPresetValues;
});
asyncLoadJs('/runtime/vue3/assets/event.js').then(() => {
magicPresetEvents.value = (window as any).magicPresetEvents;
});
getComponentList();
getActById();
return {
componentList,
menu: topMenu(),
uiConfigs,
runtimeUrl: '/runtime/vue3/playground.html',
magicPresetValues,
magicPresetConfigs,
magicPresetEvents,
editorDefaultSelected,
};
},
});
</script>
<style lang="scss">
#app {
width: 100%;
height: 100%;
display: flex;
}
.m-editor {
flex: 1;
height: 100%;
}
</style>

View File

@ -0,0 +1,44 @@
<!-- 活动列表视图 -->
<template>
<div>
<!-- 面包屑导航 -->
<el-breadcrumb separator="/">
<el-breadcrumb-item v-for="item in breadCrumbData" :key="item.index">
{{ item.text }}
</el-breadcrumb-item>
</el-breadcrumb>
<!-- 活动列表 -->
<act-list></act-list>
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue';
import List from '@src/components/act-list.vue';
export default defineComponent({
name: 'act-list-view',
components: { 'act-list': List },
setup() {
return {
breadCrumbData: ref([
{
text: '首页',
},
{
text: `活动列表`,
},
]),
};
},
});
</script>
<style>
.el-breadcrumb {
margin-bottom: 20px;
}
</style>

View File

@ -0,0 +1,64 @@
<template>
<div>
<el-breadcrumb separator="/" style="margin-bottom: 20px">
<el-breadcrumb-item v-for="item in breadcrumb" :to="{ path: item.url }" :key="item.url">
{{ item.text }}
</el-breadcrumb-item>
</el-breadcrumb>
<el-card class="box-card">
<template #header>
<div class="clearfix">
<span>创建新活动</span>
</div>
</template>
<act-create-card @add="newActHandler" />
</el-card>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useRouter } from 'vue-router';
import ActCreateCard from '@src/components/act-created-card.vue';
export interface BreadCrumbItem {
url: string;
text: string;
}
export default defineComponent({
name: 'template-list',
components: { ActCreateCard },
setup() {
const router = useRouter();
const breadcrumb: BreadCrumbItem[] = [
{
url: '/',
text: '首页',
},
{
url: '/template/list',
text: '模板',
},
];
const newActHandler = () => {
router.push({
path: '/act/my',
query: {
create: 'true',
},
});
};
return {
breadcrumb,
newActHandler,
};
},
});
</script>

View File

@ -0,0 +1,29 @@
/*
* Tencent is pleased to support the open source community by making MagicEditor available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { mount } from '@vue/test-utils';
import ActCreateCard from '@src/components/act-created-card.vue';
describe('ActCreateCard', () => {
it('点击卡片按钮', async () => {
const wrapper = mount(ActCreateCard);
await wrapper.find('el-card').trigger('click');
expect(wrapper.emitted()).toHaveProperty('add');
});
});

View File

@ -0,0 +1,105 @@
/*
* Tencent is pleased to support the open source community by making MagicEditor available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { h } from 'vue';
import { flushPromises, mount } from '@vue/test-utils';
import { ElDrawer } from 'element-plus';
import ActInfoDrawer from '@src/components/act-info-drawer.vue';
import magicStore from '@src/store/index';
import { components } from '@tests/utils';
const pages = [
{
pageName: 'index',
},
{
pageName: 'page1',
},
];
const actInfo = {
abTest: [
{
name: 'index',
pageList: [
{
pageName: 'index',
},
],
},
],
};
const getWrapper = () =>
mount(ActInfoDrawer, {
global: {
components: {
...components,
'el-drawer': ElDrawer,
'm-form': h('div', {
initValues: {},
config: {},
change: jest.fn(() => {}),
}),
},
},
});
magicStore.set('pages', pages);
magicStore.set('actInfo', actInfo);
describe('ActInfoDrawer', () => {
it('活动配置抽屉页默认收起来', () => {
const wrapper = getWrapper();
const elDrawer = wrapper.findComponent(ElDrawer);
expect(elDrawer.exists()).toBe(true);
expect(elDrawer.isVisible()).toBe(false);
});
it('活动配置抽屉页点击展开', async () => {
const wrapper = getWrapper();
const { vm } = wrapper;
vm.appDrawerVisibility = false;
await wrapper.findComponent({ ref: 'showActInfo' }).trigger('click');
expect(vm.appDrawerVisibility).toBe(true);
});
it('表格提交正确', async () => {
const wrapper = getWrapper();
const mockForm = {
resetForm: jest.fn(),
submitForm: jest.fn(() => 'test'),
};
const { vm } = wrapper;
vm.configForm = mockForm;
vm.configChangeHandler();
expect(vm.values).toStrictEqual(actInfo);
await flushPromises();
expect(magicStore.get('actInfo')).toBe('test');
});
it('表格提交失败', async () => {
const wrapper = getWrapper();
const mockForm = {
resetForm: jest.fn(),
submitForm: jest.fn(() => {
throw Error('submit form fail');
}),
};
const { vm } = wrapper;
vm.configForm = mockForm;
vm.configChangeHandler();
await flushPromises();
expect(document.querySelector('.el-message')).toBeTruthy();
expect(document.querySelector('.el-message')?.textContent).toBe('submit form fail');
});
});

View File

@ -0,0 +1,260 @@
/*
* Tencent is pleased to support the open source community by making MagicEditor available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import vueRouter from 'vue-router';
import { flushPromises, mount } from '@vue/test-utils';
import { ElSelect } from 'element-plus';
import actApi from '@src/api/act';
import List from '@src/components/act-list.vue';
import FormDialog from '@src/components/form-dialog.vue';
import MTable from '@src/components/table.vue';
import { components } from '@tests/utils';
beforeEach(() => {
jest.clearAllMocks();
});
// mock
jest.mock('js-cookie', () => {
const map = new Map();
return {
get: (key: string) => map.get(key),
set: (key: string, val: any) => map.set(key, val),
};
});
// mock
jest.mock('vue-router', () => {
let route = {
path: '',
params: {
type: 'all',
page: 0,
query: 'query',
status: -1,
},
query: {
create: false,
},
};
return {
useRoute: jest.fn(() => route),
useRouter: jest.fn(() => ({
push: jest.fn((query) => {
route.path = query;
}),
})),
setRoute: (mockRoute: any) => {
route = mockRoute;
},
};
});
// mock
jest.mock('@src/api/act', () => {
const acts: any[] = [];
for (let i = 0; i < 12; i++) {
acts.push({
actId: i,
actName: `活动${i}`,
operator: `operator${i}`,
pagePublishStatus: 1,
actStatusFormatter: 1,
});
}
let actsRes = {
data: acts,
fetch: true,
errorMsg: '',
total: 15,
};
let copyRes = {
ret: 0,
msg: '复制成功',
};
return {
getList: jest.fn(() => Promise.resolve(actsRes)),
copyAct: jest.fn(() => Promise.resolve(copyRes)),
setCopyRet: (ret: any) => {
copyRes = ret;
},
setActsRes: (res: any) => {
actsRes = res;
},
};
});
// 获取列表实例
const getWrapper = () =>
mount(List, {
global: {
components: {
'form-dialog': FormDialog,
'm-table': MTable,
...components,
},
directives: {
loading: {},
},
},
});
window.ResizeObserver =
window.ResizeObserver ||
jest.fn().mockImplementation(() => ({
disconnect: jest.fn(),
observe: jest.fn(),
unobserve: jest.fn(),
}));
describe('List', () => {
it('活动查询', async () => {
const wrapper = getWrapper();
const queryInput = wrapper.find('input[placeholder="输入活动ID活动名称加密ID创建人查询.."]');
await queryInput.setValue('test');
// 模拟回车
await queryInput.trigger('keydown.enter');
const queryData = (actApi.getList as jest.Mock).mock.calls[0][0].data;
expect(queryData.where.search).toBe('test');
});
it('输入切换页面', async () => {
const wrapper = getWrapper();
// 等待获取活动列表
await flushPromises();
const pageInput = wrapper.find('input[max="2"]');
await pageInput.setValue(2);
// 模拟回车
await pageInput.trigger('keydown.enter');
expect((vueRouter as any).useRoute().path).toBe('/act/all/1/-1/query');
});
it('按活动状态筛选', async () => {
const wrapper = getWrapper();
const select = wrapper.findComponent(ElSelect);
select.vm.$emit('change', 0);
expect((vueRouter as any).useRoute().path).toBe('/act/all/0/0/query');
});
it('按页面标题查询', async () => {
const wrapper = getWrapper();
const queryInput = wrapper.find('input[placeholder="页面标题"]');
await queryInput.setValue('test');
// 模拟回车
await queryInput.trigger('keydown.enter');
const queryData = (actApi.getList as jest.Mock).mock.calls[0][0].data;
expect(queryData.where.pageTitle).toBe('test');
});
it('关闭对话框', () => {
const wrapper = getWrapper();
const dialog = wrapper.findComponent(FormDialog);
dialog.vm.$emit('close');
expect(dialog.vm.$props.visible).toBe(false);
});
it('复制成功', () => {
const wrapper = getWrapper();
const buttons = wrapper.findAll('button');
const copyBtn = buttons.find((btn) => btn.text() === '复制');
copyBtn?.trigger('click');
setTimeout((done) => {
expect(document.querySelector('.el-message')?.textContent).toBe('复制成功');
done();
}, 0);
});
it('复制成功-无msg', () => {
(actApi as any).setCopyRet({
ret: 0,
});
const wrapper = getWrapper();
const buttons = wrapper.findAll('button');
const copyBtn = buttons.find((btn) => btn.text() === '复制');
copyBtn?.trigger('click');
setTimeout((done) => {
expect(document.querySelector('.el-message')?.textContent).toBe('复制失败');
done();
}, 0);
});
it('复制失败-有msg', () => {
(actApi as any).setCopyRet({
ret: -1,
msg: '失败原因',
});
const wrapper = getWrapper();
const buttons = wrapper.findAll('button');
const copyBtn = buttons.find((btn) => btn.text() === '复制');
copyBtn?.trigger('click');
setTimeout((done) => {
expect(document.querySelector('.el-message')?.textContent).toBe('失败原因');
done();
}, 0);
});
it('无权限复制', () => {
const wrapper = getWrapper();
const buttons = wrapper.findAll('button');
const copyBtn = buttons.find((btn) => btn.text() === '复制');
copyBtn?.trigger('click');
expect(actApi.copyAct).toBeCalledTimes(0);
});
it('查看', () => {
const wrapper = getWrapper();
const buttons = wrapper.findAll('button');
const copyBtn = buttons.find((btn) => btn.text() === '查看');
copyBtn?.trigger('click');
expect((vueRouter as any).useRoute().path).toBe('/editor/undefined');
});
it('新建活动出现弹窗', async () => {
// 在创建活动页点击创建跳转到当前页面
const wrapper = getWrapper();
const createButton = wrapper.find('#create');
await createButton.trigger('click');
expect(wrapper.vm.formDialogVisible).toBe(true);
});
it('路由的活动状态值缺省', () => {
(vueRouter as any).setRoute({
path: '',
params: {
type: 'all',
// 第一页
page: 0,
query: 'query',
// 活动状态
status: undefined,
},
query: {
create: false,
},
});
getWrapper();
expect((actApi.getList as jest.Mock).mock.calls[0][0].data.where.actStatus).toBe(-1);
});
});

View File

@ -0,0 +1,107 @@
/*
* Tencent is pleased to support the open source community by making MagicEditor available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import vueRouter from 'vue-router';
import { mount } from '@vue/test-utils';
import Aside from '@src/components/aside.vue';
import type { AsideState } from '@src/typings';
import { components } from '@tests/utils';
jest.mock('vue-router', () => {
let path = '/act/my/0';
return {
setRoute: (newPath: string) => (path = newPath),
useRoute: jest.fn(() => ({ path })),
};
});
const getWrapper = (
aside: AsideState = {
data: [
{
id: 1,
url: '/act',
icon: 'el-icon-date',
text: '活动管理',
items: [
{
id: 101,
url: '/create',
icon: '',
text: '新建活动',
},
{
id: 102,
url: '/my',
icon: '',
text: '我的活动',
},
{
id: 103,
url: '/all',
icon: '',
text: '全部活动',
},
],
},
],
collapse: false,
},
) =>
mount(Aside, {
global: {
components,
provide: {
aside,
},
},
});
describe('Aside', () => {
it('渲染菜单', () => {
const wrapper = getWrapper();
expect(wrapper.findAll('[class="el-sub-menu__title"]').length).toBe(1);
expect(wrapper.findAll('[role="menuitem"]').length).toBe(4);
// 当前路由对应的菜单项高亮:我的活动
expect(wrapper.find('[class="el-menu-item is-active"]').text()).toBe('我的活动');
});
it('空菜单', () => {
const aside: AsideState = { data: [], collapse: false };
const wrapper = getWrapper(aside);
expect(wrapper.find('[class="el-sub-menu__title"]').exists()).toBe(false);
expect(wrapper.find('[class="el-menu-item"]').exists()).toBe(false);
});
it('当前路由下对应的菜单项高亮:全部活动', () => {
(vueRouter as any).setRoute('/act/all/0');
const wrapper = getWrapper();
expect(wrapper.find('[class="el-menu-item is-active"]').text()).toBe('全部活动');
});
it('当前路由下对应的菜单项高亮:创建活动', () => {
(vueRouter as any).setRoute('/act/create');
const wrapper = getWrapper();
expect(wrapper.find('[class="el-menu-item is-active"]').text()).toBe('新建活动');
});
it('高度适应窗口变化', () => {
window.dispatchEvent(new Event('resize'));
});
});

View File

@ -0,0 +1,131 @@
/*
* Tencent is pleased to support the open source community by making MagicEditor available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { flushPromises, mount } from '@vue/test-utils';
import { ElDialog } from 'element-plus';
import FormDialog from '@src/components/form-dialog.vue';
import { components } from '@tests/utils';
const getWrapper = (
formProps: any = {
values: {
text: 'test',
},
config: [{ name: 'text', text: '测试输入' }],
visible: true,
action: jest.fn(() => ({ ret: 0 })),
title: '测试',
},
) =>
mount(FormDialog, {
props: formProps,
global: {
components: {
...components,
'el-dialog': ElDialog,
},
},
});
describe('FormDialog', () => {
it('关闭对话框', async () => {
const wrapper = getWrapper();
const dialog = wrapper.findComponent(ElDialog);
(dialog.vm.$props as any).beforeClose();
expect(wrapper.emitted()).toHaveProperty('close');
});
it('保存活动-成功', async () => {
const wrapper = getWrapper();
const mockForm = {
resetForm: jest.fn(),
submitForm: jest.fn(() => 'test'),
};
wrapper.vm.form = mockForm;
wrapper.vm.save();
await flushPromises();
expect(wrapper.emitted()).toHaveProperty('close');
expect(wrapper.emitted()).toHaveProperty('afterAction');
expect((wrapper.emitted() as any).afterAction[0][0]).toEqual({ ret: 0 });
});
it('保存活动-请求出错', async () => {
const failProps = {
values: {
text: 'test',
},
config: [{ name: 'text', text: '测试输入' }],
visible: true,
action: jest.fn(() => ({ ret: -1 })),
title: '测试',
};
const wrapper = getWrapper(failProps);
const mockForm = {
resetForm: jest.fn(),
submitForm: jest.fn(() => 'test'),
};
wrapper.vm.form = mockForm;
wrapper.vm.save();
});
it('保存活动-返回参数为空', async () => {
const failProps = {
values: {
text: 'test',
},
config: [{ name: 'text', text: '测试输入' }],
visible: true,
action: jest.fn(), // 返回为空
title: '测试',
};
const wrapper = getWrapper(failProps);
const mockForm = {
resetForm: jest.fn(),
submitForm: jest.fn(() => 'test'),
};
wrapper.vm.form = mockForm;
wrapper.vm.save();
await flushPromises();
expect(wrapper.emitted()).toHaveProperty('close');
});
it('保存活动-表单内容为空', async () => {
const wrapper = getWrapper();
const mockForm = {
resetForm: jest.fn(),
submitForm: jest.fn(() => {}),
};
wrapper.vm.form = mockForm;
wrapper.vm.save();
await flushPromises();
expect(wrapper.emitted()).toHaveProperty('close');
});
it('空参数测试', () => {
const wrapper = getWrapper({});
expect(wrapper.vm.$props).toEqual({
values: {},
config: [],
visible: false,
title: undefined,
action: undefined,
});
});
});

View File

@ -0,0 +1,61 @@
/*
* Tencent is pleased to support the open source community by making MagicEditor available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { mount } from '@vue/test-utils';
import Header from '@src/components/header.vue';
const getWrapper = () =>
mount(Header, {
global: {
provide: {
aside: {
data: [
{
id: 101,
url: '/create',
icon: '',
text: '新建活动',
},
],
collapse: false,
},
},
mocks: {
$route: {
meta: {
hideAside: false,
},
},
},
},
});
jest.mock('js-cookie', () => ({
get: jest.fn(() => 'testName'),
}));
describe('Header', () => {
it('侧边栏折叠', async () => {
const wrapper = getWrapper();
await wrapper.find('[class="aside-trigger"]').trigger('click');
expect(wrapper.emitted()).toHaveProperty('asideTrigger');
expect(wrapper.find('i').attributes('class')).toBe('el-icon-s-fold');
});
});

View File

@ -0,0 +1,221 @@
/*
* Tencent is pleased to support the open source community by making MagicEditor available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { nextTick, ref } from 'vue';
import { mount } from '@vue/test-utils';
import { editorService } from '@tmagic/editor';
import publishApi from '@src/api/publish';
import PublishPageList from '@src/components/publish-page-list.vue';
import magicStore from '@src/store/index';
// mock的活动配置数据
const root = {
type: 'app',
id: 73,
name: '7月活动',
items: [
{
type: 'page',
id: 74,
name: 'index',
items: [],
},
{
type: 'page',
id: 75,
name: 'page1',
items: [],
},
],
abTest: [
{
name: 'index',
pageList: [
{
pageName: 'index',
},
],
},
],
};
// mock的活动基础信息
const actInfo = {
actId: 73,
actCryptoId: 'sd23z1vt2',
actName: '7月活动',
operator: 'parisma',
actStatus: 0,
abTest: [
{
name: 'test',
pageList: [
{
pageName: 'index',
proportion: '100',
},
],
},
],
};
editorService.set('root', root);
magicStore.set('actInfo', actInfo);
jest.mock('vue-router', () => {
let route = {
path: '',
params: {},
};
return {
useRoute: jest.fn(() => route),
setRoute: (mockRoute: any) => {
route = mockRoute;
},
};
});
jest.mock('@src/api/publish', () => ({
publishPage: jest
.fn(() => ({
ret: 0,
msg: '',
}))
.mockImplementationOnce(() => ({
ret: -1,
msg: '发布失败',
}))
.mockImplementationOnce(() => ({
ret: -1,
msg: '发布成功',
})),
}));
describe('PublishPageList', () => {
it('发布失败', async () => {
const wrapper = mount(PublishPageList, {
global: {
provide: {
publishPageListVisible: ref(true),
},
},
});
const buttons = wrapper.findAll('el-button');
// 确认按钮
const btn = buttons.find((btn) => btn.text() === '确认');
await btn?.trigger('click');
setTimeout((done) => {
expect(document.querySelector('.el-message')?.textContent).toBe('发布失败');
done();
}, 0);
});
it('发布成功', async () => {
const wrapper = mount(PublishPageList, {
global: {
provide: {
publishPageListVisible: ref(true),
},
},
});
const buttons = wrapper.findAll('el-button');
// 确认按钮
const btn = buttons.find((btn) => btn.text() === '确认');
await btn?.trigger('click');
setTimeout((done) => {
expect(document.querySelector('.el-message')?.textContent).toBe('发布成功');
done();
}, 0);
});
it('发布弹窗能正确显示', () => {
const wrapper = mount(PublishPageList, {
global: {
provide: {
publishPageListVisible: ref(false),
},
},
});
wrapper.vm.publishPageListVisible = true;
expect(wrapper.html()).toContain('请勾选需要发布的页面');
});
it('全选', () => {
const wrapper = mount(PublishPageList);
wrapper.vm.handleCheckAllChange(['index', 'page1']);
expect(wrapper.vm.checkAll).toBe(true);
});
it('全选异常情况', () => {
const wrapper = mount(PublishPageList);
wrapper.vm.handleCheckAllChange();
expect(wrapper.vm.checkedPages).toHaveLength(0);
});
it('未选择发布页面,发布按钮不可点击', async () => {
const wrapper = mount(PublishPageList);
wrapper.vm.handleCheckedPagesChange([]);
expect(wrapper.vm.tipVisible).toBe(true);
});
it('取消全选', async () => {
const wrapper = mount(PublishPageList);
wrapper.vm.handleCheckAllChange([]);
expect(wrapper.vm.checkedPages).toHaveLength(0);
});
it('选择页面', async () => {
const wrapper = mount(PublishPageList, {
global: {
provide: {
publishPageListVisible: ref(false),
},
},
});
wrapper.vm.publishPageListVisible = true;
wrapper.vm.handleCheckedPagesChange(['index']);
expect(wrapper.vm.isIndeterminate).toBe(false);
});
it('点击取消', async () => {
const wrapper = mount(PublishPageList, {
global: {
provide: {
publishPageListVisible: ref(true),
},
},
});
const buttons = wrapper.findAll('el-button');
// 取消按钮
const btn = buttons.find((btn) => btn.text() === '取消');
await btn?.trigger('click');
expect(wrapper.vm.publishPageListVisible).toBe(false);
});
it('点击发布', async () => {
const wrapper = mount(PublishPageList, {
global: {
provide: {
publishPageListVisible: ref(true),
},
},
});
const res = {
ret: 0,
msg: '发布成功',
};
publishApi.publishPage = jest.fn(() => Promise.resolve(res));
const buttons = wrapper.findAll('el-button');
// 确认按钮
const btn = buttons.find((btn) => btn.text() === '确认');
await btn?.trigger('click');
await nextTick();
expect(wrapper.vm.publishPageListVisible).toBe(false);
});
});

View File

@ -0,0 +1,123 @@
/*
* Tencent is pleased to support the open source community by making MagicEditor available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { flushPromises, mount } from '@vue/test-utils';
import { ElTable } from 'element-plus';
import Table from '@src/components/table.vue';
import { components } from '@tests/utils';
const mockHandler = jest.fn();
const mockAfter = jest.fn();
const mockFormatter = jest.fn();
const mockErrorFormatter = jest.fn(() => {
throw new Error('err');
});
const data = {
data: [
{
test: 'test',
format: 'format',
},
],
fetch: true,
errorMsg: '',
total: 1,
};
const columns = [
{
prop: 'test',
formatter: mockErrorFormatter,
},
{
prop: 'format',
formatter: mockFormatter,
},
{
actions: [
{
text: 'action',
handler: mockHandler,
after: mockAfter,
},
],
},
{},
];
declare global {
interface Window {
ResizeObserver: any;
}
}
window.ResizeObserver =
window.ResizeObserver ||
jest.fn().mockImplementation(() => ({
disconnect: jest.fn(),
observe: jest.fn(),
unobserve: jest.fn(),
}));
const getWrapper = (mockData = data, mockColumns = columns) =>
mount(Table, {
props: {
data: mockData,
config: mockColumns,
},
global: {
components,
directives: {
loading: {},
},
},
});
describe('table', () => {
it('基础', async () => {
const wrapper = getWrapper();
const button = wrapper.find('button');
button.trigger('click');
await flushPromises();
expect(mockHandler).toBeCalled();
expect(mockAfter).toBeCalled();
expect(mockFormatter).toBeCalled();
expect(mockErrorFormatter).toBeCalled();
});
it('props为空', () => {
mount(Table, {
global: {
components,
directives: {
loading: {},
},
},
});
});
it('排序', () => {
const wrapper = getWrapper();
const table = wrapper.findComponent(ElTable);
table.vm.$emit('sort-change', {});
expect(wrapper.emitted()).toHaveProperty('sort-change', [[{}]]);
});
});

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