mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2025-06-16 01:49:25 +08:00
feat: 新增管理端demo代码
feat: 补充遗漏的文件 fix: 移除license
This commit is contained in:
parent
66eb52f8da
commit
2bfb85bdbf
@ -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'
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
|
227
docs/src/admin/introduction.md
Normal file
227
docs/src/admin/introduction.md
Normal 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
|
||||
│ ├── api(web 端接口文件)
|
||||
│ ├── 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(数据库实例初始化文件)
|
||||
│ ├── service(service 文件)
|
||||
│ ├── 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
227
magic-admin/README.md
Normal 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
|
||||
│ ├── api(web 端接口文件)
|
||||
│ ├── 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(数据库实例初始化文件)
|
||||
│ ├── service(service 文件)
|
||||
│ ├── 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
12
magic-admin/package.json
Normal 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": ""
|
||||
}
|
10
magic-admin/server/.babelrc
Normal file
10
magic-admin/server/.babelrc
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"presets": [
|
||||
[
|
||||
"@babel/preset-env"
|
||||
],
|
||||
[
|
||||
"@babel/preset-typescript"
|
||||
]
|
||||
]
|
||||
}
|
3
magic-admin/server/.eslintignore
Normal file
3
magic-admin/server/.eslintignore
Normal file
@ -0,0 +1,3 @@
|
||||
dist
|
||||
node_modules
|
||||
pm2.config.js
|
51
magic-admin/server/.eslintrc.js
Normal file
51
magic-admin/server/.eslintrc.js
Normal 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
9
magic-admin/server/.gitignore
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
node_modules
|
||||
dist
|
||||
yarn.lock
|
||||
package.json.lock
|
||||
coverage
|
||||
.vscode
|
||||
assets
|
||||
src/config/database.ts
|
||||
src/config/key.ts
|
7
magic-admin/server/jest.config.ts
Normal file
7
magic-admin/server/jest.config.ts
Normal 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
9886
magic-admin/server/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
75
magic-admin/server/package.json
Normal file
75
magic-admin/server/package.json
Normal 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"
|
||||
}
|
||||
}
|
41
magic-admin/server/src/config/config.ts
Normal file
41
magic-admin/server/src/config/config.ts
Normal 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>';
|
27
magic-admin/server/src/config/database-example.ts
Normal file
27
magic-admin/server/src/config/database-example.ts
Normal 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',
|
||||
};
|
22
magic-admin/server/src/config/key-example.ts
Normal file
22
magic-admin/server/src/config/key-example.ts
Normal 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',
|
||||
};
|
120
magic-admin/server/src/controller/act.ts
Normal file
120
magic-admin/server/src/controller/act.ts
Normal 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();
|
41
magic-admin/server/src/controller/editor.ts
Normal file
41
magic-admin/server/src/controller/editor.ts
Normal 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();
|
65
magic-admin/server/src/controller/publish.ts
Normal file
65
magic-admin/server/src/controller/publish.ts
Normal 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();
|
34
magic-admin/server/src/database/init.sql
Normal file
34
magic-admin/server/src/database/init.sql
Normal 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表'
|
48
magic-admin/server/src/index.ts
Normal file
48
magic-admin/server/src/index.ts
Normal 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}`);
|
73
magic-admin/server/src/models/act.ts
Normal file
73
magic-admin/server/src/models/act.ts
Normal 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;
|
||||
}
|
22
magic-admin/server/src/models/index.ts
Normal file
22
magic-admin/server/src/models/index.ts
Normal 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];
|
76
magic-admin/server/src/models/page.ts
Normal file
76
magic-admin/server/src/models/page.ts
Normal 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;
|
||||
}
|
40
magic-admin/server/src/routers/act.ts
Normal file
40
magic-admin/server/src/routers/act.ts
Normal 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;
|
31
magic-admin/server/src/routers/editor.ts
Normal file
31
magic-admin/server/src/routers/editor.ts
Normal 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;
|
38
magic-admin/server/src/routers/index.ts
Normal file
38
magic-admin/server/src/routers/index.ts
Normal 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;
|
31
magic-admin/server/src/routers/publish.ts
Normal file
31
magic-admin/server/src/routers/publish.ts
Normal 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;
|
41
magic-admin/server/src/routers/static.ts
Normal file
41
magic-admin/server/src/routers/static.ts
Normal 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;
|
41
magic-admin/server/src/sequelize/index.ts
Normal file
41
magic-admin/server/src/sequelize/index.ts
Normal 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;
|
||||
}
|
||||
}
|
380
magic-admin/server/src/service/act.ts
Normal file
380
magic-admin/server/src/service/act.ts
Normal 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);
|
||||
};
|
27
magic-admin/server/src/service/editor.ts
Normal file
27
magic-admin/server/src/service/editor.ts
Normal 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;
|
||||
}
|
196
magic-admin/server/src/service/page.ts
Normal file
196
magic-admin/server/src/service/page.ts
Normal 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);
|
||||
|
||||
/**
|
||||
* 格式化code,将srcCode和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('保存新添加的页面失败');
|
||||
}
|
||||
};
|
||||
}
|
273
magic-admin/server/src/service/publish.ts
Normal file
273
magic-admin/server/src/service/publish.ts
Normal 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);
|
||||
});
|
||||
};
|
45
magic-admin/server/src/template/editor/get-component-list.ts
Normal file
45
magic-admin/server/src/template/editor/get-component-list.ts
Normal 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',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
26
magic-admin/server/src/template/editor/get-web-plugins.ts
Normal file
26
magic-admin/server/src/template/editor/get-web-plugins.ts
Normal 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: '会员登录认证',
|
||||
},
|
||||
],
|
||||
};
|
20
magic-admin/server/src/template/publish/page-tmp.html
Normal file
20
magic-admin/server/src/template/publish/page-tmp.html
Normal 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>
|
98
magic-admin/server/src/typings/index.d.ts
vendored
Normal file
98
magic-admin/server/src/typings/index.d.ts
vendored
Normal 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;
|
||||
}
|
52
magic-admin/server/src/utils/crypto/crypto.ts
Normal file
52
magic-admin/server/src/utils/crypto/crypto.ts
Normal 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);
|
||||
},
|
||||
};
|
96
magic-admin/server/src/utils/index.ts
Normal file
96
magic-admin/server/src/utils/index.ts
Normal 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 };
|
39
magic-admin/server/src/utils/logger.ts
Normal file
39
magic-admin/server/src/utils/logger.ts
Normal 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();
|
43
magic-admin/server/src/utils/transform.ts
Normal file
43
magic-admin/server/src/utils/transform.ts
Normal 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 编译失败');
|
||||
}
|
||||
}
|
129
magic-admin/server/tests/unit/act.spec.ts
Normal file
129
magic-admin/server/tests/unit/act.spec.ts
Normal 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",
|
||||
});
|
||||
});
|
||||
});
|
56
magic-admin/server/tests/unit/editor.spec.ts
Normal file
56
magic-admin/server/tests/unit/editor.spec.ts
Normal 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([]);
|
||||
});
|
||||
});
|
138
magic-admin/server/tests/unit/publish.spec.ts
Normal file
138
magic-admin/server/tests/unit/publish.spec.ts
Normal 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 ')'",
|
||||
});
|
||||
});
|
||||
});
|
35
magic-admin/server/tsconfig.json
Normal file
35
magic-admin/server/tsconfig.json
Normal 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
39
magic-admin/setup.sh
Normal 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
|
||||
|
||||
|
3
magic-admin/web/.browserslistrc
Normal file
3
magic-admin/web/.browserslistrc
Normal file
@ -0,0 +1,3 @@
|
||||
> 1%
|
||||
last 2 versions
|
||||
not dead
|
6
magic-admin/web/.eslintignore
Normal file
6
magic-admin/web/.eslintignore
Normal file
@ -0,0 +1,6 @@
|
||||
dist/
|
||||
node_modules/
|
||||
babel.config.js
|
||||
vue.config.js
|
||||
jest.config.js
|
||||
tscofnig.json
|
62
magic-admin/web/.eslintrc.js
Normal file
62
magic-admin/web/.eslintrc.js
Normal 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
27
magic-admin/web/.gitignore
vendored
Normal 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
|
4
magic-admin/web/babel.config.js
Normal file
4
magic-admin/web/babel.config.js
Normal 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'],
|
||||
};
|
29
magic-admin/web/index_index.html
Normal file
29
magic-admin/web/index_index.html
Normal 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/).
|
13
magic-admin/web/jest.config.js
Normal file
13
magic-admin/web/jest.config.js
Normal 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
18785
magic-admin/web/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
58
magic-admin/web/package.json
Normal file
58
magic-admin/web/package.json
Normal 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"
|
||||
}
|
||||
}
|
BIN
magic-admin/web/public/favicon.png
Normal file
BIN
magic-admin/web/public/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.6 KiB |
17
magic-admin/web/public/index.html
Normal file
17
magic-admin/web/public/index.html
Normal 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>
|
160
magic-admin/web/src/api/act.ts
Normal file
160
magic-admin/web/src/api/act.ts
Normal 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,
|
||||
});
|
||||
},
|
||||
};
|
47
magic-admin/web/src/api/editor.ts
Normal file
47
magic-admin/web/src/api/editor.ts
Normal 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',
|
||||
});
|
||||
},
|
||||
};
|
55
magic-admin/web/src/api/publish.ts
Normal file
55
magic-admin/web/src/api/publish.ts
Normal 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,
|
||||
});
|
||||
},
|
||||
};
|
32
magic-admin/web/src/api/user.ts
Normal file
32
magic-admin/web/src/api/user.ts
Normal 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
140
magic-admin/web/src/app.vue
Normal 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>
|
61
magic-admin/web/src/components/act-created-card.vue
Normal file
61
magic-admin/web/src/components/act-created-card.vue
Normal 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>
|
131
magic-admin/web/src/components/act-info-drawer.vue
Normal file
131
magic-admin/web/src/components/act-info-drawer.vue
Normal 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-ui的bug:https://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>
|
276
magic-admin/web/src/components/act-list.vue
Normal file
276
magic-admin/web/src/components/act-list.vue
Normal 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>
|
120
magic-admin/web/src/components/aside.vue
Normal file
120
magic-admin/web/src/components/aside.vue
Normal 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>
|
144
magic-admin/web/src/components/form-dialog.vue
Normal file
144
magic-admin/web/src/components/form-dialog.vue
Normal 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>
|
99
magic-admin/web/src/components/header.vue
Normal file
99
magic-admin/web/src/components/header.vue
Normal 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>
|
160
magic-admin/web/src/components/publish-page-list.vue
Normal file
160
magic-admin/web/src/components/publish-page-list.vue
Normal 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>
|
155
magic-admin/web/src/components/table.vue
Normal file
155
magic-admin/web/src/components/table.vue
Normal 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>
|
119
magic-admin/web/src/config/act-list-config.ts
Normal file
119
magic-admin/web/src/config/act-list-config.ts
Normal 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,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
50
magic-admin/web/src/config/aside-config.ts
Normal file
50
magic-admin/web/src/config/aside-config.ts
Normal 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,
|
||||
};
|
48
magic-admin/web/src/config/blank-act-config.ts
Normal file
48
magic-admin/web/src/config/blank-act-config.ts
Normal 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,
|
||||
},
|
||||
];
|
126
magic-admin/web/src/config/drawer-config.ts
Normal file
126
magic-admin/web/src/config/drawer-config.ts
Normal 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: '%',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
31
magic-admin/web/src/config/status.ts
Normal file
31
magic-admin/web/src/config/status.ts
Normal 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, // 已预发布
|
||||
}
|
41
magic-admin/web/src/main.ts
Normal file
41
magic-admin/web/src/main.ts
Normal 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');
|
62
magic-admin/web/src/plugins/editor.ts
Normal file
62
magic-admin/web/src/plugins/editor.ts
Normal 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: '页面删除失败',
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
26
magic-admin/web/src/plugins/element.ts
Normal file
26
magic-admin/web/src/plugins/element.ts
Normal 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 });
|
||||
};
|
52
magic-admin/web/src/router/index.ts
Normal file
52
magic-admin/web/src/router/index.ts
Normal 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
5
magic-admin/web/src/shims-vue.d.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue';
|
||||
const component: DefineComponent<{}, {}, any>;
|
||||
export default component;
|
||||
}
|
39
magic-admin/web/src/store/index.ts
Normal file
39
magic-admin/web/src/store/index.ts
Normal 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
178
magic-admin/web/src/typings/index.d.ts
vendored
Normal 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;
|
||||
}
|
28
magic-admin/web/src/use/use-comp.ts
Normal file
28
magic-admin/web/src/use/use-comp.ts
Normal 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);
|
||||
});
|
||||
};
|
68
magic-admin/web/src/use/use-menu.ts
Normal file
68
magic-admin/web/src/use/use-menu.ts
Normal 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')),
|
||||
},
|
||||
],
|
||||
});
|
127
magic-admin/web/src/use/use-publish.ts
Normal file
127
magic-admin/web/src/use/use-publish.ts
Normal 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;
|
||||
};
|
23
magic-admin/web/src/use/use-status.ts
Normal file
23
magic-admin/web/src/use/use-status.ts
Normal 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'],
|
||||
};
|
184
magic-admin/web/src/util/request.ts
Normal file
184
magic-admin/web/src/util/request.ts
Normal 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);
|
||||
},
|
||||
};
|
29
magic-admin/web/src/util/set-env.ts
Normal file
29
magic-admin/web/src/util/set-env.ts
Normal 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: '/',
|
||||
});
|
||||
}
|
38
magic-admin/web/src/util/url.ts
Normal file
38
magic-admin/web/src/util/url.ts
Normal 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, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
||||
};
|
||||
|
||||
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 '';
|
||||
};
|
44
magic-admin/web/src/util/utils.ts
Normal file
44
magic-admin/web/src/util/utils.ts
Normal 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: ');
|
||||
};
|
86
magic-admin/web/src/views/editor.vue
Normal file
86
magic-admin/web/src/views/editor.vue
Normal 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>
|
44
magic-admin/web/src/views/list-view.vue
Normal file
44
magic-admin/web/src/views/list-view.vue
Normal 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>
|
64
magic-admin/web/src/views/template-list.vue
Normal file
64
magic-admin/web/src/views/template-list.vue
Normal 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>
|
@ -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');
|
||||
});
|
||||
});
|
105
magic-admin/web/tests/unit/components/act-info-drawer.spec.ts
Normal file
105
magic-admin/web/tests/unit/components/act-info-drawer.spec.ts
Normal 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');
|
||||
});
|
||||
});
|
260
magic-admin/web/tests/unit/components/act-list.spec.ts
Normal file
260
magic-admin/web/tests/unit/components/act-list.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
107
magic-admin/web/tests/unit/components/aside.spec.ts
Normal file
107
magic-admin/web/tests/unit/components/aside.spec.ts
Normal 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'));
|
||||
});
|
||||
});
|
131
magic-admin/web/tests/unit/components/form-dialog.spec.ts
Normal file
131
magic-admin/web/tests/unit/components/form-dialog.spec.ts
Normal 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,
|
||||
});
|
||||
});
|
||||
});
|
61
magic-admin/web/tests/unit/components/header.spec.ts
Normal file
61
magic-admin/web/tests/unit/components/header.spec.ts
Normal 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');
|
||||
});
|
||||
});
|
221
magic-admin/web/tests/unit/components/publish-page-list.spec.ts
Normal file
221
magic-admin/web/tests/unit/components/publish-page-list.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
123
magic-admin/web/tests/unit/components/table.spec.ts
Normal file
123
magic-admin/web/tests/unit/components/table.spec.ts
Normal 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
Loading…
x
Reference in New Issue
Block a user