jinxyang vor 5 Jahren
Commit
297a7ce8cf
100 geänderte Dateien mit 14758 neuen und 0 gelöschten Zeilen
  1. 18 0
      .babelrc.js
  2. 12 0
      .eslintrc.js
  3. 3 0
      .gitignore
  4. 321 0
      README.md
  5. 11 0
      build/config-reverse.js
  6. 24 0
      build/config.js
  7. 9 0
      build/webpack/entry.js
  8. 28 0
      build/webpack/index.js
  9. 25 0
      build/webpack/optimization.js
  10. 16 0
      build/webpack/output.js
  11. 57 0
      build/webpack/plugins.js
  12. 11 0
      build/webpack/resolve.js
  13. 72 0
      build/webpack/rules.js
  14. 84 0
      examples/App.vue
  15. 31 0
      examples/actions/attribute/deleteAttributeDetail.js
  16. 28 0
      examples/actions/attribute/deleteAttributeMain.js
  17. 42 0
      examples/actions/attribute/editAttributeDetail.js
  18. 17 0
      examples/actions/attribute/editAttributeMain.js
  19. 140 0
      examples/actions/attribute/getAttributeDetail.js
  20. 96 0
      examples/actions/attribute/getAttributeList.js
  21. 32 0
      examples/actions/attribute/getAttributeMain.js
  22. 103 0
      examples/actions/attribute/getAttributeMainList.js
  23. 19 0
      examples/actions/attribute/goAttributeDetail.js
  24. 12 0
      examples/actions/attribute/goAttributeList.js
  25. 12 0
      examples/actions/attribute/goAttributeMain.js
  26. 26 0
      examples/actions/attribute/index.js
  27. 25 0
      examples/actions/category/deleteCategory.js
  28. 55 0
      examples/actions/category/editCategory.js
  29. 18 0
      examples/actions/category/editCategoryProductSort.js
  30. 18 0
      examples/actions/category/editCategorySort.js
  31. 180 0
      examples/actions/category/getCategory.js
  32. 197 0
      examples/actions/category/getCategoryList.js
  33. 86 0
      examples/actions/category/getCategoryProductSort.js
  34. 8 0
      examples/actions/category/getOption.js
  35. 12 0
      examples/actions/category/goCategoryDetail.js
  36. 10 0
      examples/actions/category/goCategoryList.js
  37. 10 0
      examples/actions/category/goCategoryProductSort.js
  38. 10 0
      examples/actions/category/goCategorySort.js
  39. 28 0
      examples/actions/category/index.js
  40. 17 0
      examples/actions/category/switchCategory.js
  41. 147 0
      examples/actions/comment/getCommentList.js
  42. 7 0
      examples/actions/comment/index.js
  43. 20 0
      examples/actions/config/getAttributes.js
  44. 18 0
      examples/actions/config/getAttributesDetail.js
  45. 22 0
      examples/actions/config/getCategories.js
  46. 22 0
      examples/actions/config/getCity.js
  47. 16 0
      examples/actions/config/getLabels.js
  48. 102 0
      examples/actions/config/index.js
  49. 35 0
      examples/actions/config/store.js
  50. 17 0
      examples/actions/index.js
  51. 26 0
      examples/actions/product/deleteProductList.js
  52. 94 0
      examples/actions/product/editProduct.js
  53. 580 0
      examples/actions/product/getProduct.js
  54. 219 0
      examples/actions/product/getProductList.js
  55. 12 0
      examples/actions/product/goProductDetail.js
  56. 16 0
      examples/actions/product/index.js
  57. 20 0
      examples/actions/product/switchProduct.js
  58. 158 0
      examples/actions/trash/getTrashList.js
  59. 29 0
      examples/actions/trash/handleTrashList.js
  60. 9 0
      examples/actions/trash/index.js
  61. 0 0
      examples/assets/styles/main.scss
  62. 67 0
      examples/components/Layout.vue
  63. 103 0
      examples/components/Sign.vue
  64. 42 0
      examples/components/attribute/AttributeList.vue
  65. 50 0
      examples/components/attribute/index.vue
  66. 47 0
      examples/components/category/CategoryList.vue
  67. 54 0
      examples/components/category/index.vue
  68. 40 0
      examples/components/comment/index.vue
  69. 31 0
      examples/components/common/editor/detail.vue
  70. 87 0
      examples/components/common/editor/index.vue
  71. 202 0
      examples/components/common/editor/ueditor.vue
  72. 34 0
      examples/components/common/menu/index.vue
  73. 78 0
      examples/components/product/attribute.vue
  74. 64 0
      examples/components/product/index.vue
  75. 98 0
      examples/components/product/parameter.vue
  76. 391 0
      examples/components/product/pattern.vue
  77. 76 0
      examples/components/product/receive.vue
  78. 71 0
      examples/components/product/tag.vue
  79. 40 0
      examples/components/trash/index.vue
  80. 40 0
      examples/index.html
  81. 29 0
      examples/index.js
  82. 20 0
      examples/plugins/format.js
  83. 58 0
      examples/plugins/http.js
  84. 10 0
      examples/plugins/hub.js
  85. 12 0
      examples/plugins/index.js
  86. 62 0
      examples/plugins/stack.js
  87. 23 0
      examples/router/index.js
  88. 47 0
      examples/router/routes.js
  89. 2 0
      examples/store/actions.js
  90. 2 0
      examples/store/getters.js
  91. 22 0
      examples/store/index.js
  92. 84 0
      examples/store/modules/filter.js
  93. 8 0
      examples/store/modules/index.js
  94. 63 0
      examples/store/modules/user.js
  95. 2 0
      examples/store/mutations.js
  96. 4 0
      examples/store/state.js
  97. 9138 0
      package-lock.json
  98. 63 0
      package.json
  99. 2 0
      scripts/build.js
  100. 0 0
      scripts/pack.js

+ 18 - 0
.babelrc.js

@@ -0,0 +1,18 @@
+
+module.exports = api => {
+  api.cache(true)
+
+  const presets = [
+    '@babel/preset-env',
+  ]
+
+  const plugins = [
+    '@babel/plugin-transform-runtime',
+    '@babel/plugin-syntax-dynamic-import',
+  ]
+
+  return {
+    presets,
+    plugins,
+  }
+}

+ 12 - 0
.eslintrc.js

@@ -0,0 +1,12 @@
+
+module.exports = {
+  parser: 'vue-eslint-parser',
+  parserOptions: {
+    parser: 'babel-eslint',
+    sourceType: 'module',
+  },
+  extends: 'standard',
+  rules: {
+    'comma-dangle': ['error', 'always-multiline']
+  }
+}

+ 3 - 0
.gitignore

@@ -0,0 +1,3 @@
+.DS_Store
+.vscode
+node_modules

+ 321 - 0
README.md

@@ -0,0 +1,321 @@
+# vue-reverse
+
+该项目依赖于iview UI框架
+
+
+
+## 使用
+
+```javascript
+import Reverse from 'vue-reverse'
+import 'vue-reverse/dist/reverse.css'
+const actions = {}
+Vue.use(VueReverse, actions)
+// 必选的第二个参数,详见下方说明
+```
+
+
+
+### Actions
+
+类型:Object
+
+```javascript
+// 该对象的所有方法供CommonFilter和CommonDetail使用
+const actions = {
+  getCategoryList: (args) => { // 值需返回Promise,参数根据组件调用有所不同
+		return new Promise((resolve, reject) => {
+      // 接口调用,返回值根据调用组件会有所不同,见下方详细的组件说明
+    })
+  },
+}
+```
+
+
+
+
+
+## 组件
+
+### CommonStack
+
+该组件用于生成路由组件的内部路由(没做url匹配),所有内部页面都由该组件管理。
+
+**Props**
+
+- title
+  - 说明:页面标题
+  - 类型:String|Number
+- query
+  - 说明:外部参数,用于slot传递
+  - 类型:Object
+
+**Demo**
+
+```html
+<CommonStack title="商品分类" :query="{ name: 'rainbow' }">
+  <template v-slot:default="{ stackQuery }">
+  	<!-- 业务组件写在这里 -->
+  </template>
+</CommonStack>
+```
+
+
+
+### CommonWrapper
+
+该组件是Stack内部页面组件,以抽屉形式打开。
+
+**Props**
+
+- name
+  - 说明:页面标识
+
+**Demo**
+
+```html
+<CommonStack title="商品分类">
+  <template v-slot:default="{ stackQuery }">
+    <CommonWrapper name="category-list">
+    	<!-- 业务组件写在这里 -->
+    </CommonWrapper>
+    <CommonWrapper name="category-detail">
+    	<!-- 业务组件写在这里 -->
+    </CommonWrapper>
+  </template>
+</CommonStack>
+```
+
+
+
+### CommonFilter
+
+数据列表组件。
+
+**Props**
+
+- action(非必选)
+  - 类型:String
+  - 说明:Actions中的属性(方法名),返回对象传给该组件
+- actionOption (非必选)
+  - 类型:Object
+  - 说明:作为action的第二个参数,第一个参数为列表的筛选项
+- response(非必选)
+  - 类型:Object
+  - 说明:没有action的时候,response作为本地对象传给该组件
+- defaultValues(非必选)
+  - 类型:Object
+  - 说明:用于首次请求的筛选项
+- fixedValues (非必选)
+  - 类型:Object
+  - 说明:用于固定不变的筛选项,UI不可见
+- selection(非必选)
+  - 类型:Object
+  - 说明:用于声明用于选择的字段和已选列表
+
+**Demo**
+
+```html
+<CommonStack title="商品分类">
+  <template v-slot:default="{ stackQuery }">
+    <CommonWrapper name="category-list">
+    	<CommonFilter action="getCategoryList" />
+    </CommonWrapper>
+  </template>
+</CommonStack>
+```
+
+```javascript
+const actions = {
+  getCategoryList: (payload, option) => {
+    return new Promise(async (resolve, reject) => {
+      const filters = [
+        {
+          type: 'input', // 组件类型
+          title: '分类名称', // 搜索字段名称
+          key: 'name', // 搜索字段
+          props: { // 组件props
+            placeholder: '输入分类名称',
+            clearable: true,
+          },
+        },
+        // ...
+      ]
+			const actions = [
+      	{
+          title: '添加分类',
+          action: () => { // 支持同'getCategoryList'一样
+            // stack.push用于打开内部页面,第一个参数为CommonWrapper组件的name
+      			Vue.stack.push('category-detail', '添加分类')
+    			},
+          props: { // button组件props
+            type: 'primary',
+          },
+        },
+        // ...
+			]
+      // 表头的API和iview的表格是完全一样的,同样支持render,额外有些特殊支持
+      const columns = [
+        {
+          title: '分类名称',
+          key: 'name',
+          align: 'center',
+          width: 100,
+        },
+        { // 支持开关
+          nodeType: 'iSwitch',
+          payload: {
+            title: '开启',
+            key: 'is_open',
+            action: 'switchCategory', // 同'getCategoryList'
+          },
+        },
+        {  // 按钮组
+          nodeType: 'action',
+          payload: {
+            title: '操作',
+            list: [
+              {
+                title: '编辑',
+                action: row => {
+                  // 支持同'getCategoryList'一样
+                  Vue.stack.push('category-detail', '添加分类', { row.id })
+                },
+                props: {
+                  loading: false,
+                },
+              },
+              {
+                title: '删除',
+                action: 'deleteCategory', // 同'getCategoryList'
+              },
+            ],
+          },
+        },
+      ]
+      try {
+        const { data, extra, meta } = await Vue.http.get('/product/category', {
+          params: payload,
+        })
+        const response = {
+          filters, // 筛选项
+          actions, // 表格上方按钮
+          columns, // 表头
+          data, // 列表数据
+          page: meta.pagination, // 分页数据
+        }
+        resolve(response)
+      } catch (e) {
+        reject(e)
+      }
+    })
+  },
+  // ...
+}
+```
+
+列表页的所有操作都必须是异步函数(返回Promise)
+
+
+
+### CommonDetail
+
+表单组件
+
+**Props**
+
+- query
+  - 类型:Object
+  - 说明:内部路由栈
+- name
+  - 类型:String
+  - 说明:通过name取当前组件参数,同外部CommonWrapper组件的name一致
+- resetAction
+  - 类型:String
+  - 说明:Actions中的属性(方法名),返回表单对应数据
+- resetResponse
+  - 类型:Object
+  - 说明:没有action的时候,response作为本地对象传给该组件
+- submitAction
+  - 类型:String
+  - 说明:Actions中的属性(方法名),提交表单数据
+- components
+  - 类型:Object
+  - 说明:自定义组件列表
+
+**Demo**
+
+```html
+<CommonStack title="商品分类">
+  <template v-slot:default="{ stackQuery }">
+    <CommonWrapper name="category-list">
+    	<CommonFilter action="getCategoryList" />
+    </CommonWrapper>
+    <CommonWrapper name="category-detail">
+    	<CommonDetail
+      	name="category-detail"
+        resetAction="getCategoryDetail"
+        submitAction="postCategoryDetail" />
+    </CommonWrapper>
+  </template>
+</CommonStack>
+```
+
+```javascript
+const actions = {
+  getCategoryDetail () {
+    return new Promise((resolve, reject) => {
+      const response = {
+        componentData: [
+          {
+            nodeType: 'block',
+            nodeList: [
+            	{
+                nodeType: 'item',
+                required: true, // *号,必填标识
+                label: '分类名称',
+                nodeList: [
+                  {
+                    nodeType: 'input', // 表单类型,支持几乎所有iview的表单组件,也可以为自定义组件
+                    keyword: 'name', // 提交字段
+                    hub: 'category-detail', // 当前路由名称
+                    props: { // 对应组件props
+                      placeholder: '输入分类名称',
+                    },
+                  },
+                ],
+              },
+            ],
+          },
+        ],
+        value: { // 表单各项的值
+          name: '',
+        },
+        option: {}, // nodeType为select时对应的选项{ value, label }
+        error: {}, // 用于表单错误返回,获取组件数据时一般为空
+        preview: false, // 是否隐藏保存按钮
+      }
+      resolve(response)
+    })
+  },
+  postCategoryDetail (payload) {
+    return new Promise(async (resolve, reject) => {
+      // payload为表单组件的value
+      // 此处对表单进行验证
+      // 验证失败时
+      return reject({ name: '请填写名称' })
+      try {
+        await Vue.http[method]('/product/category', payload)
+        resolve()
+      } catch (e) {
+        reject(e)
+      }
+    })
+  },
+}
+```
+
+
+
+
+

+ 11 - 0
build/config-reverse.js

@@ -0,0 +1,11 @@
+
+const path = require('path')
+
+const join = dir => path.join(__dirname, '..', dir)
+
+module.exports = {
+  publicPath: '/dist/',
+  srcPath: join('src/'),
+  distPath: join('dist/'),
+  appJs: join('src/index.js'),
+}

+ 24 - 0
build/config.js

@@ -0,0 +1,24 @@
+
+const path = require('path')
+
+const join = dir => path.join(__dirname, '..', dir)
+
+module.exports = {
+  port: 9529,
+  publicPath: '/',
+  srcPath: join('examples/'),
+  distPath: join('examples/dist'),
+  appJs: join('examples/index.js'),
+  appHtml: join('examples/index.html'),
+  defineEnv: {
+    development: {
+      SYSTEM_NAME: '"product"',
+      NODE_ENV: '"development"',
+      MAIN_URL: '"http://localhost:8800"',
+      API_URL: {
+        default: '"https://manage.dev.caihongxingqiu.com/"',
+        upload: '"https://manage.dev.caihongxingqiu.com/config/upload"',
+      },
+    },
+  },
+}

+ 9 - 0
build/webpack/entry.js

@@ -0,0 +1,9 @@
+
+const clientOptions = 'path=/__what&timeout=10000&reload=true&quiet=true'
+
+module.exports = (isEnvDevelopment, appJs) => ({
+  app: [
+    isEnvDevelopment && `webpack-hot-middleware/client?${clientOptions}`,
+    appJs,
+  ].filter(Boolean),
+})

+ 28 - 0
build/webpack/index.js

@@ -0,0 +1,28 @@
+
+module.exports = (env, config) => {
+  const isEnvDevelopment = env === 'development'
+  const {
+    port,
+    publicPath,
+    srcPath,
+    distPath,
+    appJs,
+    appHtml,
+    defineEnv,
+  } = config
+
+  return {
+    mode: isEnvDevelopment ? 'development' : 'production',
+    entry: require('./entry')(isEnvDevelopment, appJs),
+    output: require('./output')(isEnvDevelopment, distPath, publicPath),
+    module: {
+      rules: require('./rules')(isEnvDevelopment, srcPath),
+    },
+    optimization: require('./optimization')(isEnvDevelopment),
+    resolve: require('./resolve')(srcPath),
+    devtool: isEnvDevelopment
+      ? 'cheap-module-eval-source-map'
+      : 'eval-source-map',
+    plugins: require('./plugins')(env, defineEnv, publicPath, port, appHtml),
+  }
+}

+ 25 - 0
build/webpack/optimization.js

@@ -0,0 +1,25 @@
+
+const TerserPlugin = require('terser-webpack-plugin')
+const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
+
+module.exports = isEnvDevelopment => ({
+  minimize: !isEnvDevelopment,
+  minimizer: [
+    new TerserPlugin({
+      terserOptions: {
+        parse: {
+          ecma: 8,
+        },
+        compress: {
+          drop_console: true,
+        },
+        mangle: {
+          safari10: true,
+        },
+      },
+      cache: true,
+      parallel: true,
+    }),
+    new OptimizeCSSAssetsPlugin(),
+  ],
+})

+ 16 - 0
build/webpack/output.js

@@ -0,0 +1,16 @@
+
+module.exports = (isEnvDevelopment, distPath, publicPath) => {
+  const output = {
+    path: isEnvDevelopment ? undefined : distPath,
+    filename: isEnvDevelopment
+      ? 'bundle.js'
+      : 'reverse.js',
+    publicPath,
+  }
+  if (!isEnvDevelopment) {
+    output.library = 'vue-reverse'
+    output.libraryTarget = 'umd'
+    output.umdNamedDefine = true
+  }
+  return output
+}

+ 57 - 0
build/webpack/plugins.js

@@ -0,0 +1,57 @@
+
+// const path = require('path')
+const webpack = require('webpack')
+const HtmlWebpackPlugin = require('html-webpack-plugin')
+const VueLoaderPlugin = require('vue-loader/lib/plugin')
+const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin')
+const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin')
+const MiniCssExtractPlugin = require('mini-css-extract-plugin')
+
+module.exports = (env, defineEnv, publicPath, port, appHtml) => [
+  appHtml && new webpack.DefinePlugin({
+    'process.env': {
+      PUBLIC_PATH: `"${publicPath}"`,
+      ...defineEnv[env],
+    },
+  }),
+  appHtml && new HtmlWebpackPlugin(
+    Object.assign(
+      {},
+      {
+        template: appHtml,
+        inject: true,
+      },
+      env === 'development'
+        ? undefined
+        : {
+          minify: {
+            collapseWhitespace: true,
+            removeAttributeQuotes: true,
+            removeComments: true,
+          },
+        }
+    )
+  ),
+  new VueLoaderPlugin(),
+  env === 'development' && new webpack.HotModuleReplacementPlugin(),
+  env === 'development' && new CaseSensitivePathsPlugin(),
+  env === 'development' && new FriendlyErrorsWebpackPlugin({
+    compilationSuccessInfo: {
+      messages: [
+        `Your app is running here http://localhost:${port}`,
+      ],
+    },
+  }),
+  env !== 'development' && new webpack.HashedModuleIdsPlugin(),
+  env !== 'development' && (
+    appHtml
+      ? new MiniCssExtractPlugin({
+        filename: 'assets/styles/[name].[contenthash:8].css',
+        chunkFilename: 'assets/styles/[name].[contenthash:8].chunk.css',
+      })
+      : new MiniCssExtractPlugin({
+        filename: 'reverse.css',
+        chunkFilename: 'reverse.chunk.css',
+      })
+  ),
+].filter(Boolean)

+ 11 - 0
build/webpack/resolve.js

@@ -0,0 +1,11 @@
+
+module.exports = srcPath => ({
+  extensions: [
+    '.js',
+    '.json',
+    '.vue',
+  ],
+  alias: {
+    '@': srcPath,
+  },
+})

+ 72 - 0
build/webpack/rules.js

@@ -0,0 +1,72 @@
+
+const MiniCssExtractPlugin = require('mini-css-extract-plugin')
+
+module.exports = (isEnvDevelopment, srcPath) => {
+  const getStyleLoaders = (cssOptions, preProcessor) => {
+    const loaders = [
+      isEnvDevelopment ? 'style-loader' : {
+        loader: MiniCssExtractPlugin.loader,
+      },
+      {
+        loader: 'css-loader',
+        options: Object.assign(
+          {},
+          cssOptions,
+          {
+            sourceMap: !isEnvDevelopment,
+          },
+        ),
+      },
+    ].filter(Boolean)
+    if (preProcessor) {
+      loaders.push({
+        loader: preProcessor,
+        options: { sourceMap: !isEnvDevelopment },
+      })
+    }
+    return loaders
+  }
+
+  return [
+    {
+      test: /\.css$/,
+      use: getStyleLoaders({ importLoaders: 1 }),
+    },
+    {
+      test: /\.scss$/,
+      use: getStyleLoaders({ importLoaders: 2 }, 'sass-loader'),
+    },
+    {
+      test: /\.(png|jpe?g|gif|svg)$/,
+      loader: 'file-loader',
+      options: {
+        name: 'assets/images/[name].[ext]',
+      },
+    },
+    {
+      test: /\.(ttf|otf|eot|woff2?)$/,
+      loader: 'file-loader',
+      options: {
+        name: 'assets/fonts/[name].[ext]',
+      },
+    },
+    {
+      test: /\.(m?js|vue)$/,
+      enforce: 'pre',
+      include: srcPath,
+      loader: 'eslint-loader',
+      options: {
+        formatter: require('eslint-friendly-formatter'),
+      },
+    },
+    {
+      test: /\.m?js$/,
+      include: srcPath,
+      loader: 'babel-loader',
+    },
+    {
+      test: /\.vue$/,
+      loader: 'vue-loader',
+    },
+  ]
+}

+ 84 - 0
examples/App.vue

@@ -0,0 +1,84 @@
+<template>
+  <router-view v-if="ready" />
+</template>
+
+<script>
+import { mapMutations, mapActions } from 'vuex'
+
+export default {
+  data () {
+    return {
+      ready: false,
+    }
+  },
+  methods: {
+    ...mapMutations('user', {
+      updateUser: 'UPDATE_USER',
+    }),
+    ...mapActions('user', {
+      refreshUser: 'refresh',
+    }),
+    async checkUser () {
+      // 作为子站点打开
+      if (this.isMinion) return this.pushMessage('sign')
+      // 独立打开
+      const token = window.localStorage.getItem('token')
+      const ttl = window.localStorage.getItem('token_ttl')
+      if (token && ttl && new Date() < new Date(Number(ttl))) {
+        try {
+          await this.refreshUser()
+        } catch {
+          this.goSign()
+        }
+      } else this.goSign()
+    },
+    goSign () {
+      this.$router.replace({ name: 'sign' })
+    },
+    pushMessage (type, payload = {}) {
+      const { SYSTEM_NAME, MAIN_URL } = process.env
+      window.parent.postMessage({
+        system: SYSTEM_NAME,
+        type,
+        payload,
+      }, MAIN_URL)
+    },
+    receiveMessage ({ data: { type, payload }, origin }) {
+      if (origin === window.origin) return
+      const messageTypes = {
+        sign: () => {
+          this.updateUser(payload)
+          this.ready = true
+          this.pushMessage('ready')
+        },
+        view: () => {
+          this.$router.push({ path: `/${payload.view}/` })
+        },
+      }
+      if (messageTypes[type]) messageTypes[type]()
+    },
+  },
+  mounted () {
+    window.UPLOAD_URL = process.env.API_URL.upload ||
+      `${process.env.API_URL.default}config/upload`
+    const handleError = (content, cb) => {
+      this.$Message.error({ content })
+      if (cb) cb()
+    }
+    this.isMinion = this.$store.state.isMinion
+    this.ready = !this.isMinion
+
+    this.$hub.$on('GLOBAL_AUTH_ERROR', ({ message }) => {
+      handleError(message, () => {
+        if (this.isMinion) return this.pushMessage('sign', { resign: true })
+        this.$router.replace({ name: 'sign' })
+      })
+    })
+    this.$hub.$on('GLOBAL_ERROR', ({ message }) => {
+      handleError(message)
+    })
+    window.addEventListener('message', this.receiveMessage, false)
+    this.checkUser()
+  },
+}
+</script>

+ 31 - 0
examples/actions/attribute/deleteAttributeDetail.js

@@ -0,0 +1,31 @@
+
+import Vue from 'vue'
+import iview from 'iview'
+
+export default function deleteAttributeDetail (payload = {}) {
+  return new Promise((resolve, reject) => {
+    const {
+      id,
+      name,
+      attribute_category_name: attributeCategoryName,
+      attribute_type: attributeType,
+    } = payload
+    const type = attributeType ? '参数' : '规格'
+    iview.Modal.confirm({
+      title: `删除${type}`,
+      content: `确定要删除"${attributeCategoryName}"属性的"${name}"${type}吗?`,
+      onOk: async () => {
+        try {
+          await Vue.http.delete(`/product/attribute/item?id=${id}`)
+          iview.Message.success('删除成功')
+          resolve('remove')
+        } catch (e) {
+          reject(e)
+        }
+      },
+      onCancel: (e) => {
+        reject(e)
+      },
+    })
+  })
+}

+ 28 - 0
examples/actions/attribute/deleteAttributeMain.js

@@ -0,0 +1,28 @@
+
+import Vue from 'vue'
+import iview from 'iview'
+
+export default function deleteAttributeMain (payload = {}) {
+  return new Promise((resolve, reject) => {
+    const {
+      id,
+      name,
+    } = payload
+    iview.Modal.confirm({
+      title: '删除属性',
+      content: `确定要删除"${name}"属性吗?`,
+      onOk: async () => {
+        try {
+          await Vue.http.delete(`/product/attribute/category?id=${id}`)
+          iview.Message.success('删除成功')
+          resolve('remove')
+        } catch (e) {
+          reject(e)
+        }
+      },
+      onCancel: (e) => {
+        reject(e)
+      },
+    })
+  })
+}

+ 42 - 0
examples/actions/attribute/editAttributeDetail.js

@@ -0,0 +1,42 @@
+
+import Vue from 'vue'
+
+export default function editAttributeDetail (payload = {}, option = {}) {
+  return new Promise(async (resolve, reject) => {
+    const checkHandles = {
+      name: value => {
+        return value ? '' : '规格名称不能为空'
+      },
+      attribute_category_id: value => {
+        return value ? '' : '属性不能为空'
+      },
+      hand_add_status: (value, allValue) => {
+        if (!value && value !== 0) return '必须选择是否支持商家手动新增'
+        if (value === 0 && !allValue.input_list.length) return '可选列表为空时必须支持商家手动新增'
+        return ''
+      },
+      sort: value => {
+        return value ? '' : '排序不能为空'
+      },
+    }
+    const error = {}
+    Object.keys(payload).forEach(key => {
+      const handle = checkHandles[key]
+      if (!handle) return
+      error[key] = handle(payload[key], payload)
+    })
+    console.log(payload)
+    if (Object.keys(error).find(key => error[key])) {
+      return reject(error)
+    }
+
+    try {
+      payload.input_list = payload.input_list.join(',')
+      const method = payload.id ? 'put' : 'post'
+      await Vue.http[method]('/product/attribute/item', payload)
+      resolve()
+    } catch (e) {
+      reject(e)
+    }
+  })
+}

+ 17 - 0
examples/actions/attribute/editAttributeMain.js

@@ -0,0 +1,17 @@
+
+import Vue from 'vue'
+
+export default function editAttributeMain (payload = {}, option = {}) {
+  return new Promise(async (resolve, reject) => {
+    const { id, name } = payload
+    const error = { name: '属性名称不能为空' }
+    if (!name.trim()) return reject(error)
+    try {
+      const method = id ? 'put' : 'post'
+      await Vue.http[method]('/product/attribute/category', payload)
+      resolve()
+    } catch (e) {
+      reject(e)
+    }
+  })
+}

+ 140 - 0
examples/actions/attribute/getAttributeDetail.js

@@ -0,0 +1,140 @@
+
+export default function getAttributeDetail (payload = {}) {
+  return new Promise(async (resolve) => {
+    // 属性
+    const attributeComponentData = {
+      nodeType: 'item',
+      required: true,
+      label: '属性',
+      nodeList: [
+        {
+          nodeType: 'select',
+          keyword: 'attribute_category_id',
+          hub: 'attribute-detail',
+          props: {
+            placeholder: '选择属性',
+          },
+        },
+      ],
+    }
+
+    // 名称
+    const nameComponentData = {
+      nodeType: 'item',
+      required: true,
+      label: `${payload.attribute_type ? '参数' : '规格'}名称`,
+      nodeList: [
+        {
+          nodeType: 'input',
+          keyword: 'name',
+          hub: 'attribute-detail',
+          props: {
+            placeholder: `输入${payload.attribute_type ? '参数' : '规格'}名称`,
+          },
+        },
+      ],
+    }
+
+    // 可选列表
+    const listComponentData = {
+      nodeType: 'item',
+      required: false,
+      size: 'large',
+      label: '可选列表',
+      nodeList: [
+        {
+          nodeType: 'inputTag',
+          keyword: 'input_list',
+          hub: 'attribute-detail',
+          props: {},
+        },
+      ],
+    }
+
+    // 手动新增
+    const handComponentData = {
+      nodeType: 'item',
+      required: true,
+      label: '商家手动新增',
+      nodeList: [
+        {
+          nodeType: 'radioGroup',
+          keyword: 'hand_add_status',
+          hub: 'attribute-detail',
+          props: {},
+        },
+      ],
+    }
+
+    // 排序
+    const sortComponentData = {
+      nodeType: 'item',
+      required: true,
+      label: '排序',
+      nodeList: [
+        {
+          nodeType: 'inputNumber',
+          keyword: 'sort',
+          hub: 'attribute-detail',
+          props: {
+            min: 1,
+          },
+        },
+      ],
+    }
+
+    // 整体组件数据
+    const componentData = [
+      attributeComponentData,
+      nameComponentData,
+      listComponentData,
+      handComponentData,
+      sortComponentData,
+    ]
+
+    const value = {
+      attribute_type: payload.attribute_type,
+      name: null,
+      attribute_category_id: null,
+      input_list: [],
+      hand_add_status: null,
+      sort: 999,
+    }
+
+    const error = {}
+
+    const option = {
+      attribute_category_id: {
+        type: 'async',
+        list: [],
+      },
+      hand_add_status: {
+        list: [],
+      },
+    }
+
+    value.attribute_category_id = payload.attribute_category_id
+    option.attribute_category_id.list = [
+      {
+        value: payload.attribute_category_id,
+        label: payload.attribute_category_name,
+      },
+    ]
+
+    if (payload.id) {
+      value.id = payload.id
+      value.name = payload.name
+      value.input_list = payload.input_list.split(',')
+      value.hand_add_status = payload.hand_add_status
+      value.sort = payload.sort
+    }
+
+    const attributeDetailData = {
+      componentData,
+      value,
+      error,
+      option,
+    }
+    resolve(attributeDetailData)
+  })
+}

+ 96 - 0
examples/actions/attribute/getAttributeList.js

@@ -0,0 +1,96 @@
+
+import Vue from 'vue'
+
+export default function getAttributeList (payload = {}) {
+  return new Promise(async (resolve, reject) => {
+    const isSpec = payload.attribute_type === 0
+    const actions = [
+      {
+        title: '添加',
+        action: 'goAttributeDetail',
+        actionOption: {
+          type: payload.attribute_type,
+          id: payload.attribute_category_id,
+          name: Vue.stack.getStack().list.attribute.find(i => i.name === 'attribute-list').title,
+        },
+        props: {
+          type: 'primary',
+        },
+      },
+    ]
+
+    // 表格项
+    const columns = {
+      name: {
+        title: `${isSpec ? '规格' : '参数'}名称`,
+        key: 'name',
+        align: 'center',
+      },
+      attribute_category_name: {
+        title: '商品属性',
+        key: 'attribute_category_name',
+        align: 'center',
+      },
+      input_list: {
+        title: '可选值',
+        key: 'input_list',
+        align: 'center',
+      },
+      sort: {
+        title: '排序',
+        key: 'sort',
+        align: 'center',
+      },
+    }
+
+    const columnsExtra = {
+      nodeType: 'action',
+      payload: {
+        title: '操作',
+        list: [
+          {
+            title: '编辑',
+            action: 'goAttributeDetail',
+            props: {
+              type: 'primary',
+              loading: false,
+            },
+          },
+          {
+            title: '删除',
+            action: 'deleteAttributeDetail',
+          },
+        ],
+      },
+    }
+
+    const isOption = !payload.attribute_type && payload.attribute_type !== 0
+    const requestBody = !isOption
+      ? payload
+      : {
+        attribute_category_id: payload.attribute_category_id,
+        per_page: 1000,
+      }
+    try {
+      const { data, extra, meta } = await Vue.http.get('/product/attribute/item', {
+        params: requestBody,
+      })
+
+      if (isOption) return resolve(data)
+      const response = {
+        filters: [],
+        actions,
+        data,
+        columns: extra.columns
+          .map(key => columns[key])
+          .filter(Boolean)
+          .concat(columnsExtra),
+        page: meta.pagination,
+        title: isSpec ? '规格列表' : '参数列表',
+      }
+      resolve(response)
+    } catch (e) {
+      reject(e)
+    }
+  })
+}

+ 32 - 0
examples/actions/attribute/getAttributeMain.js

@@ -0,0 +1,32 @@
+
+export default function getAttributeMain (payload = {}) {
+  return new Promise((resolve, reject) => {
+    const { id, name } = payload
+    const response = {
+      componentData: [
+        {
+          nodeType: 'item',
+          required: true,
+          label: '属性名称',
+          nodeList: [
+            {
+              nodeType: 'input',
+              keyword: 'name',
+              hub: 'attribute-main',
+              props: {
+                placeholder: '输入属性名称',
+              },
+            },
+          ],
+        },
+      ],
+      value: { name: '' },
+      error: {},
+      option: {},
+    }
+    if (payload.id) {
+      response.value = { id, name }
+    }
+    resolve(response)
+  })
+}

+ 103 - 0
examples/actions/attribute/getAttributeMainList.js

@@ -0,0 +1,103 @@
+
+import Vue from 'vue'
+
+export default function getAttributeMainList (payload = {}, option = {}) {
+  return new Promise(async (resolve, reject) => {
+    // 表格项
+    const columns = {
+      name: {
+        title: '属性名称',
+        key: 'name',
+        align: 'center',
+      },
+      spec_number: {
+        title: '规格数量',
+        key: 'spec_number',
+        align: 'center',
+      },
+      parameter_number: {
+        title: '参数数量',
+        key: 'parameter_number',
+        align: 'center',
+      },
+    }
+
+    const actions = [
+      {
+        title: '添加',
+        action: 'goAttributeMain',
+        props: {
+          type: 'primary',
+        },
+      },
+    ]
+
+    // 表格扩展(数据操作)
+    const columnsExtra = [
+      {
+        nodeType: 'action',
+        payload: {
+          title: '查看',
+          list: [
+            {
+              title: '规格列表',
+              action: 'goAttributeList',
+              actionOption: {
+                type: 0,
+              },
+              props: {
+                loading: false,
+              },
+            },
+            {
+              title: '参数列表',
+              action: 'goAttributeList',
+              actionOption: {
+                type: 1,
+              },
+              props: {
+                loading: false,
+              },
+            },
+          ],
+        },
+      },
+      {
+        nodeType: 'action',
+        payload: {
+          title: '操作',
+          list: [
+            {
+              title: '编辑',
+              action: 'goAttributeMain',
+              props: {
+                loading: false,
+              },
+            },
+            {
+              title: '删除',
+              action: 'deleteAttributeMain',
+            },
+          ],
+        },
+      },
+    ]
+
+    try {
+      const { data, extra, meta } = await Vue.http.get('/product/attribute/category')
+      const response = {
+        filters: [],
+        actions,
+        data,
+        columns: extra.columns
+          .map(key => columns[key])
+          .filter(Boolean)
+          .concat(columnsExtra),
+        page: meta.pagination,
+      }
+      resolve(response)
+    } catch (e) {
+      reject(e)
+    }
+  })
+}

+ 19 - 0
examples/actions/attribute/goAttributeDetail.js

@@ -0,0 +1,19 @@
+
+import Vue from 'vue'
+
+export default function goAttributeDetail (payload = {}, option = {}) {
+  return new Promise(resolve => {
+    const { id, name } = payload
+    if (!id) {
+      payload.attribute_type = option.type
+      payload.attribute_category_id = option.id
+      payload.attribute_category_name = option.name
+      Vue.stack.push(
+        'attribute-detail',
+        `添加${option.type === 0 ? '规格' : '参数'}`,
+        payload
+      )
+    } else Vue.stack.push('attribute-detail', name, payload)
+    resolve()
+  })
+}

+ 12 - 0
examples/actions/attribute/goAttributeList.js

@@ -0,0 +1,12 @@
+
+import Vue from 'vue'
+
+export default function goAttributeList (payload = {}, option = {}) {
+  return new Promise(resolve => {
+    Vue.stack.push('attribute-list', payload.name, {
+      attribute_type: option.type,
+      attribute_category_id: payload.id,
+    })
+    resolve()
+  })
+}

+ 12 - 0
examples/actions/attribute/goAttributeMain.js

@@ -0,0 +1,12 @@
+
+import Vue from 'vue'
+
+export default function goAttributeMain (payload = {}) {
+  return new Promise(async (resolve) => {
+    const { id, name } = payload
+    if (!id) {
+      Vue.stack.push('attribute-main', '添加属性')
+    } else Vue.stack.push('attribute-main', name, { id, name })
+    resolve()
+  })
+}

+ 26 - 0
examples/actions/attribute/index.js

@@ -0,0 +1,26 @@
+
+import getAttributeMainList from './getAttributeMainList'
+import goAttributeMain from './goAttributeMain'
+import getAttributeMain from './getAttributeMain'
+import editAttributeMain from './editAttributeMain'
+import getAttributeDetail from './getAttributeDetail'
+import goAttributeDetail from './goAttributeDetail'
+import editAttributeDetail from './editAttributeDetail'
+import getAttributeList from './getAttributeList'
+import goAttributeList from './goAttributeList'
+import deleteAttributeMain from './deleteAttributeMain'
+import deleteAttributeDetail from './deleteAttributeDetail'
+
+export {
+  getAttributeMainList,
+  goAttributeMain,
+  getAttributeMain,
+  editAttributeMain,
+  getAttributeDetail,
+  goAttributeDetail,
+  editAttributeDetail,
+  getAttributeList,
+  goAttributeList,
+  deleteAttributeMain,
+  deleteAttributeDetail,
+}

+ 25 - 0
examples/actions/category/deleteCategory.js

@@ -0,0 +1,25 @@
+
+import Vue from 'vue'
+import iview from 'iview'
+
+export default function deleteCategory (payload = {}) {
+  return new Promise((resolve, reject) => {
+    const { id, name } = payload
+    iview.Modal.confirm({
+      title: '删除分类',
+      content: `确定要删除"${name}"分类吗?`,
+      onOk: async () => {
+        try {
+          await Vue.http.delete(`/product/category?id=${id}`)
+          iview.Message.success('删除成功')
+          resolve('remove')
+        } catch (e) {
+          reject(e)
+        }
+      },
+      onCancel: (e) => {
+        reject(e)
+      },
+    })
+  })
+}

+ 55 - 0
examples/actions/category/editCategory.js

@@ -0,0 +1,55 @@
+
+import Vue from 'vue'
+import iview from 'iview'
+
+export default function editCategory (payload = {}, option = {}) {
+  console.log(payload)
+  return new Promise(async (resolve, reject) => {
+    // 错误返回数据
+    const error = {}
+    // 提交数据
+    const requestBody = {
+      name: '',
+      ico: '',
+    }
+
+    // 字段验证方法
+    const checkFuncs = {
+      name (value) {
+        return value && value.trim() ? '' : '分类名称不能为空'
+      },
+      ico (value) {
+        return value && value.trim() ? '' : '必须上传分类图标'
+      },
+    }
+
+    const valid = Object.keys(payload).reduce((resultList, key) => {
+      const func = checkFuncs[key]
+      const value = payload[key]
+      const errorText = func ? func(value) : ''
+      if (!errorText && value) requestBody[key] = value
+      error[key] = errorText
+      resultList.push(!errorText)
+      return resultList
+    }, []).every(Boolean)
+
+    if (!valid) return reject(error)
+
+    const parentId = payload.category_id2 || payload.category_id1
+    if (parentId) {
+      delete requestBody.category_id1
+      delete requestBody.category_id2
+      requestBody.parent_id = parentId
+    }
+    if (payload.id) requestBody.id = payload.id
+
+    try {
+      const method = requestBody.id ? 'put' : 'post'
+      await Vue.http[method]('/product/category', requestBody)
+      iview.Message.success('保存成功')
+      resolve()
+    } catch (e) {
+      reject(e)
+    }
+  })
+}

+ 18 - 0
examples/actions/category/editCategoryProductSort.js

@@ -0,0 +1,18 @@
+
+import Vue from 'vue'
+import iview from 'iview'
+
+export default function editCategoryProductSort (payload = {}) {
+  return new Promise(async (resolve, reject) => {
+    const requestBody = {
+      data: payload.map(({ id, sort }) => ({ id, sort })),
+    }
+    try {
+      await Vue.http.put(`/product/operate/product/sort`, requestBody)
+      iview.Message.success('保存成功')
+      resolve(true)
+    } catch (e) {
+      reject(e)
+    }
+  })
+}

+ 18 - 0
examples/actions/category/editCategorySort.js

@@ -0,0 +1,18 @@
+
+import Vue from 'vue'
+import iview from 'iview'
+
+export default function editCategorySort (payload = {}, option = {}) {
+  return new Promise(async (resolve, reject) => {
+    const requestBody = {
+      data: payload.map(({ id, sort }) => ({ id, sort })),
+    }
+    try {
+      await Vue.http.put(`/product/category/sort`, requestBody)
+      iview.Message.success('保存成功')
+      resolve(true)
+    } catch (e) {
+      reject(e)
+    }
+  })
+}

+ 180 - 0
examples/actions/category/getCategory.js

@@ -0,0 +1,180 @@
+
+import Vue from 'vue'
+
+export default function getCategory (payload = {}) {
+  return new Promise(async (resolve, reject) => {
+    const id = payload.id
+    // 名称
+    const nameComponentData = {
+      nodeType: 'item',
+      required: true,
+      label: '分类名称',
+      nodeList: [
+        {
+          nodeType: 'input',
+          keyword: 'name',
+          hub: 'category-detail',
+          props: {
+            placeholder: '输入分类名称',
+          },
+        },
+      ],
+    }
+
+    // 分类
+    const levelComponentData = {
+      nodeType: 'item',
+      required: false,
+      label: '上级分类',
+      tip: '默认添加一级分类',
+      nodeList: [
+        {
+          nodeType: 'select',
+          keyword: 'category_id1',
+          hub: 'category-detail',
+          props: {
+            placeholder: '选择一级分类',
+            filterable: true,
+            clearable: !id,
+            // disabled: !!id,
+          },
+        },
+        {
+          nodeType: 'select',
+          keyword: 'category_id2',
+          hub: 'category-detail',
+          props: {
+            placeholder: '选择二级分类',
+            filterable: true,
+            clearable: !id,
+            // disabled: !!id,
+          },
+          dependency: {
+            type: 'hide',
+            keyword: 'category_id1',
+          },
+        },
+      ],
+    }
+
+    // 图标
+    const icoComponentData = {
+      nodeType: 'item',
+      required: true,
+      label: '分类图标',
+      tip: '图标尺寸还不知道',
+      nodeList: [
+        {
+          nodeType: 'uploader',
+          keyword: 'ico',
+          hub: 'category-detail',
+          props: {},
+        },
+      ],
+    }
+
+    // 描述
+    const descComponentData = {
+      nodeType: 'item',
+      required: false,
+      label: '分类描述',
+      nodeList: [
+        {
+          nodeType: 'input',
+          keyword: 'desc',
+          hub: 'category-detail',
+          props: {
+            type: 'textarea',
+          },
+        },
+      ],
+    }
+
+    // 整体组件数据
+    const componentData = [
+      {
+        nodeType: 'block',
+        nodeList: [
+          {
+            nodeType: 'note',
+            text: '默认添加一级分类',
+          },
+          nameComponentData,
+          levelComponentData,
+          icoComponentData,
+          descComponentData,
+        ],
+      },
+    ]
+
+    const value = {
+      name: null,
+      category_id1: null,
+      category_id2: null,
+      ico: null,
+      desc: null,
+    }
+
+    const error = {
+      name: '',
+      category_id1: '',
+      category_id2: '',
+      ico: '',
+      desc: '',
+    }
+
+    const option = {
+      category_id1: {
+        type: 'async',
+        list: [],
+        refresh: true,
+      },
+      category_id2: {
+        type: 'async',
+        dependency: {
+          keyword: 'category_id1',
+        },
+        list: [],
+        refresh: true,
+      },
+    }
+
+    const categoryDetailData = {
+      componentData,
+      value,
+      error,
+      option,
+    }
+
+    if (id) {
+      try {
+        const { data } = await Vue.http.get(`/product/category/detail?id=${id}`)
+        const { name, category1, category2, ico, desc } = data
+        if (category1.id) {
+          option.category_id1.list.push({ value: category1.id, label: category1.name })
+          value.category_id1 = category1.id
+        }
+        if (category2.id) {
+          option.category_id2.list.push({ value: category2.id, label: category2.name })
+          value.category_id2 = category2.id
+        }
+        value.id = data.id
+        value.name = name
+        value.ico = ico
+        value.desc = desc
+
+        levelComponentData.nodeList[0].props.disabled = !value.category_id1
+        levelComponentData.nodeList[1].props.disabled = !value.category_id2
+
+        resolve({
+          componentData,
+          value,
+          error,
+          option,
+        })
+      } catch (e) {
+        console.log(e)
+      }
+    } else resolve(categoryDetailData)
+  })
+}

+ 197 - 0
examples/actions/category/getCategoryList.js

@@ -0,0 +1,197 @@
+
+import Vue from 'vue'
+
+export default function getCategoryList (payload = {}, option = {}) {
+  return new Promise(async (resolve, reject) => {
+    // 筛选项
+    const filters = {
+      name: {
+        type: 'input',
+        title: '分类名称',
+        key: 'name',
+        props: {
+          placeholder: '输入分类名称',
+          clearable: true,
+        },
+      },
+    }
+    // 操作项
+    const actions = [
+      {
+        title: '添加分类',
+        action: 'goCategoryDetail',
+        props: {
+          type: 'primary',
+        },
+      },
+    ]
+    const sortActions = [
+      {
+        title: '保存',
+        action: 'editCategorySort',
+        props: {
+          type: 'success',
+        },
+      },
+    ]
+
+    // 表格项
+    const columns = {
+      name: {
+        title: '分类名称',
+        key: 'name',
+        align: 'center',
+        width: 100,
+      },
+      level: {
+        title: '级别',
+        key: 'level',
+        align: 'center',
+        width: 80,
+        format: {
+          type: 'config',
+        },
+      },
+      ico: {
+        nodeType: 'image',
+        payload: {
+          key: 'ico',
+          title: '分类图标',
+          width: 80,
+          height: 80,
+        },
+      },
+      desc: {
+        title: '分类描述',
+        key: 'desc',
+        align: 'center',
+      },
+      is_open: {
+        nodeType: 'iSwitch',
+        payload: {
+          title: '开启',
+          key: 'is_open',
+          action: 'switchCategory',
+        },
+      },
+    }
+
+    const sortColumns = {
+      name: {
+        title: '分类名称',
+        key: 'name',
+        align: 'center',
+      },
+      level: {
+        title: '级别',
+        key: 'level',
+        align: 'center',
+        format: {
+          type: 'config',
+        },
+      },
+      sort: {
+        nodeType: 'input',
+        payload: {
+          title: '排序',
+          key: 'sort',
+          props: {
+            type: 'number',
+            number: true,
+          },
+        },
+      },
+    }
+
+    // 表格扩展(数据操作)
+    const columnsExtra = {
+      nodeType: 'action',
+      payload: {
+        title: '操作',
+        list: [
+          {
+            title: '编辑',
+            action: 'goCategoryDetail',
+            props: {
+              loading: false,
+            },
+          },
+          {
+            title: '删除',
+            action: 'deleteCategory',
+          },
+        ],
+      },
+    }
+
+    const sortColumnsExtra = {
+      nodeType: 'action',
+      payload: {
+        title: '操作',
+        list: [
+          {
+            title: '商品排序',
+            action: 'goCategoryProductSort',
+            props: {
+              loading: false,
+              type: 'primary',
+            },
+          },
+        ],
+      },
+    }
+
+    try {
+      const isSort = option.type === 'sort'
+      if (isSort) payload.per_page = 1000
+      const { data, extra, meta } = await Vue.http.get('/product/category', {
+        params: payload,
+      })
+
+      // 处理级别功能和操作
+      const handleLevel = {
+        level1 () {
+          actions.push({
+            title: '排序',
+            action: 'goCategorySort',
+            actionOption: {
+              parent_id: 0,
+              type: 'sort',
+            },
+            props: {
+              type: 'primary',
+            },
+          })
+          this.level2()
+        },
+        level2 () {
+          columnsExtra.payload.list.unshift({
+            title: '查看子分类',
+            action: 'goCategoryList',
+            props: {
+              loading: false,
+              type: 'primary',
+            },
+          },)
+        },
+        level3 () {},
+      }
+      if (data.length) handleLevel[`level${data[0].level}`]()
+
+      // 返回数据
+      const response = {
+        filters: isSort ? [] : extra.filters.map(key => filters[key]).filter(Boolean),
+        actions: isSort ? sortActions : actions,
+        data,
+        columns: extra.columns
+          .map(key => (isSort ? sortColumns : columns)[key])
+          .filter(Boolean)
+          .concat(isSort ? sortColumnsExtra : columnsExtra),
+        page: isSort ? {} : meta.pagination,
+      }
+      resolve(response)
+    } catch (e) {
+      reject(e)
+    }
+  })
+}

+ 86 - 0
examples/actions/category/getCategoryProductSort.js

@@ -0,0 +1,86 @@
+
+import Vue from 'vue'
+
+export default function getCategoryProductSort (payload = {}) {
+  return new Promise(async resolve => {
+    const filters = {
+      keyword: {
+        type: 'input',
+        title: '商品名称/编码',
+        key: 'keyword',
+        props: {
+          placeholder: '商品名称/编码',
+          clearable: true,
+        },
+      },
+    }
+    const columns = {
+      'name&sku_name': {
+        title: '商品名称/编码',
+        key: 'name&sku_name',
+        align: 'center',
+      },
+      img: {
+        nodeType: 'image',
+        payload: {
+          key: 'img',
+          title: '商品图片',
+        },
+      },
+      price: {
+        title: '价格(元)',
+        key: 'price',
+        align: 'center',
+        width: 80,
+      },
+      sort: {
+        nodeType: 'input',
+        payload: {
+          title: '排序',
+          key: 'sort',
+          props: {
+            type: 'number',
+            number: true,
+          },
+        },
+      },
+      up_status: {
+        title: '上架',
+        key: 'up_status',
+        align: 'center',
+        width: 80,
+        format: {
+          type: 'config',
+        },
+      },
+    }
+
+    const actions = [
+      {
+        title: '保存',
+        action: 'editCategoryProductSort',
+        props: {
+          type: 'success',
+        },
+      },
+    ]
+
+    try {
+      const { data, extra, meta } = await Vue.http.get('/product/operate/product', {
+        params: payload,
+      })
+      resolve({
+        filters: extra.filters.map(key => filters[key]).filter(Boolean),
+        actions,
+        data,
+        columns: extra.columns
+          .map(key => columns[key])
+          .filter(Boolean),
+        page: meta.pagination,
+      })
+    } catch (e) {
+      console.log(e)
+    }
+    resolve()
+  })
+}

+ 8 - 0
examples/actions/category/getOption.js

@@ -0,0 +1,8 @@
+
+// import Vue from 'vue'
+
+export default function getOption (payload = {}) {
+  return new Promise(async (resolve) => {
+    resolve()
+  })
+}

+ 12 - 0
examples/actions/category/goCategoryDetail.js

@@ -0,0 +1,12 @@
+
+import Vue from 'vue'
+
+export default function goCategoryDetail (payload = {}) {
+  return new Promise(async (resolve) => {
+    const { id, name } = payload
+    if (!id) {
+      Vue.stack.push('category-detail', '添加分类')
+    } else Vue.stack.push('category-detail', name, { id })
+    resolve()
+  })
+}

+ 10 - 0
examples/actions/category/goCategoryList.js

@@ -0,0 +1,10 @@
+
+import Vue from 'vue'
+
+export default function goCategoryList (payload = {}) {
+  return new Promise(async (resolve) => {
+    const { id, name, level } = payload
+    Vue.stack.push(`category-list-${level + 1}`, name, { parent_id: id })
+    resolve()
+  })
+}

+ 10 - 0
examples/actions/category/goCategoryProductSort.js

@@ -0,0 +1,10 @@
+
+import Vue from 'vue'
+
+export default function goCategoryProductSort (payload = {}) {
+  return new Promise(resolve => {
+    const { id, name } = payload
+    Vue.stack.push('category-product-sort', `${name}商品`, { category_id1: id })
+    resolve()
+  })
+}

+ 10 - 0
examples/actions/category/goCategorySort.js

@@ -0,0 +1,10 @@
+
+import Vue from 'vue'
+
+export default function goCategorySort (payload = {}, option = {}) {
+  return new Promise(async (resolve) => {
+    console.log(option)
+    Vue.stack.push(`category-sort`, '排序', option)
+    resolve()
+  })
+}

+ 28 - 0
examples/actions/category/index.js

@@ -0,0 +1,28 @@
+
+import getCategoryList from './getCategoryList'
+import switchCategory from './switchCategory'
+import getCategory from './getCategory'
+import editCategory from './editCategory'
+import deleteCategory from './deleteCategory'
+import goCategoryList from './goCategoryList'
+import goCategoryDetail from './goCategoryDetail'
+import goCategorySort from './goCategorySort'
+import goCategoryProductSort from './goCategoryProductSort'
+import getCategoryProductSort from './getCategoryProductSort'
+import editCategoryProductSort from './editCategoryProductSort'
+import editCategorySort from './editCategorySort'
+
+export {
+  getCategoryList,
+  switchCategory,
+  getCategory,
+  editCategory,
+  deleteCategory,
+  goCategoryList,
+  goCategoryDetail,
+  goCategorySort,
+  goCategoryProductSort,
+  getCategoryProductSort,
+  editCategoryProductSort,
+  editCategorySort,
+}

+ 17 - 0
examples/actions/category/switchCategory.js

@@ -0,0 +1,17 @@
+
+import Vue from 'vue'
+
+export default function switchCategory (payload = {}) {
+  return new Promise(async (resolve, reject) => {
+    const { id, is_open: isOpen } = payload
+    const requestBody = {
+      id, is_open: isOpen,
+    }
+    try {
+      await Vue.http.put('/product/category', requestBody)
+      resolve()
+    } catch (e) {
+      reject(e)
+    }
+  })
+}

+ 147 - 0
examples/actions/comment/getCommentList.js

@@ -0,0 +1,147 @@
+
+import Vue from 'vue'
+
+export default function getCommentList (payload = {}, option = {}) {
+  return new Promise(async (resolve, reject) => {
+    // 筛选项
+    const filters = {
+      shop_id: {
+        type: 'input',
+        title: '商家',
+        key: 'shop_id',
+        props: {
+          placeholder: '请选择商家',
+          filterable: true,
+          clearable: true,
+        },
+      },
+      star: {
+        type: 'select',
+        title: '星级',
+        key: 'star',
+        props: {
+          placeholder: '评价星级',
+          filterable: true,
+          clearable: true,
+        },
+        option: {
+          list: [],
+        },
+      },
+      username: {
+        type: 'input',
+        title: '用户昵称',
+        key: 'username',
+        props: {
+          placeholder: '用户昵称',
+          clearable: true,
+        },
+      },
+      keyword: {
+        type: 'input',
+        title: '关键词',
+        key: 'keyword',
+        props: {
+          placeholder: '评价关键词/上架名称',
+          clearable: true,
+        },
+      },
+    }
+    // 操作项
+    const actions = []
+    const sortActions = []
+
+    // 表格项
+    // "sale_name"
+    const columns = {
+      username: {
+        title: '用户昵称',
+        key: 'username',
+        align: 'center',
+        width: 100,
+      },
+      product_name: {
+        title: '上架名称',
+        key: 'product_name',
+        align: 'center',
+        width: 80,
+      },
+      shop_name: {
+        title: '所属商家',
+        key: 'shop_name',
+        align: 'center',
+        width: 80,
+      },
+      category_name1: {
+        title: '一级分类',
+        key: 'category_name1',
+        align: 'center',
+      },
+      category_name2: {
+        title: '二级分类',
+        key: 'category_name2',
+        align: 'center',
+      },
+      category_name3: {
+        title: '三级分类',
+        key: 'category_name3',
+        align: 'center',
+      },
+      star: {
+        nodeType: 'rating',
+        payload: {
+          title: '评价',
+          key: 'star',
+          width: 200,
+          props: {
+            disabled: true,
+          },
+        },
+      },
+      content: {
+        title: '评论内容',
+        key: 'content',
+        align: 'center',
+      },
+      purchase_no: {
+        title: '订单号',
+        key: 'purchase_no',
+        align: 'center',
+      },
+      created_at: {
+        title: '评价时间时间',
+        key: 'created_at',
+        align: 'center',
+      },
+    }
+
+    const sortColumns = {}
+
+    // 表格扩展(数据操作)
+    const columnsExtra = {}
+
+    const sortColumnsExtra = {}
+
+    try {
+      const isSort = option.type === 'sort'
+      if (isSort) payload.per_page = 1000
+      const { data, extra, meta } = await Vue.http.get('/product/comment', {
+        params: payload,
+      })
+      const response = {
+        filters: isSort ? [] : extra.filters.map(key => filters[key]).filter(Boolean),
+        actions: isSort ? sortActions : actions,
+        data: data,
+        columns: extra.columns
+          .map(key => (isSort ? sortColumns : columns)[key])
+          .filter(Boolean)
+          .concat(isSort ? sortColumnsExtra : columnsExtra),
+        page: isSort ? {} : meta.pagination,
+        title: '评价列表',
+      }
+      resolve(response)
+    } catch (e) {
+      reject(e)
+    }
+  })
+}

+ 7 - 0
examples/actions/comment/index.js

@@ -0,0 +1,7 @@
+// 商品评价
+
+import getCommentList from './getCommentList'
+
+export {
+  getCommentList,
+}

+ 20 - 0
examples/actions/config/getAttributes.js

@@ -0,0 +1,20 @@
+
+import Vue from 'vue'
+
+export default function getAttributes () {
+  return new Promise(async resolve => {
+    try {
+      const { data } = await Vue.http.get('/product/attribute/category', {
+        params: {
+          per_page: 1000,
+        },
+      })
+      const list = data.map(attribute => {
+        return { value: attribute.id, label: attribute.name }
+      })
+      resolve(list)
+    } catch {
+      resolve([])
+    }
+  })
+}

+ 18 - 0
examples/actions/config/getAttributesDetail.js

@@ -0,0 +1,18 @@
+
+import Vue from 'vue'
+
+export default function getAttributesDetail (payload) {
+  return new Promise(async resolve => {
+    try {
+      const { data } = await Vue.http.get('/product/attribute/item', {
+        params: {
+          attribute_category_id: payload.attribute_category_id,
+          per_page: 1000,
+        },
+      })
+      resolve(data)
+    } catch {
+      resolve([])
+    }
+  })
+}

+ 22 - 0
examples/actions/config/getCategories.js

@@ -0,0 +1,22 @@
+
+import Vue from 'vue'
+
+export default function getCategories (payload) {
+  return new Promise(async resolve => {
+    try {
+      const { data } = await Vue.http.get('/product/category', {
+        params: {
+          parent_id: payload.parent_id,
+          per_page: 1000,
+          is_open: 1,
+        },
+      })
+      const list = data.map(category => {
+        return { value: category.id, label: category.name }
+      })
+      resolve(list)
+    } catch {
+      resolve([])
+    }
+  })
+}

+ 22 - 0
examples/actions/config/getCity.js

@@ -0,0 +1,22 @@
+
+import Vue from 'vue'
+
+export default function getCity (payload) {
+  return new Promise(async resolve => {
+    try {
+      const { data } = await Vue.http.get('/config/cityManagement/lists', {
+        params: {
+          per_page: 1000,
+          status: 1,
+        },
+      })
+      const list = data.map(i => {
+        return { value: i.city_id, label: i.city_name }
+      })
+      list.unshift({ label: '全部', value: 0 })
+      resolve(list)
+    } catch {
+      resolve([])
+    }
+  })
+}

+ 16 - 0
examples/actions/config/getLabels.js

@@ -0,0 +1,16 @@
+
+import Vue from 'vue'
+
+export default function getLabels () {
+  return new Promise(async resolve => {
+    try {
+      const res = await Vue.http.get('/product/label')
+      const list = res.product_labels.map(label => {
+        return { value: label.id, label: label.name }
+      })
+      resolve(list)
+    } catch {
+      resolve([])
+    }
+  })
+}

+ 102 - 0
examples/actions/config/index.js

@@ -0,0 +1,102 @@
+
+import Vue from 'vue'
+import store from './store'
+
+import getCategories from './getCategories'
+import getAttributes from './getAttributes'
+import getAttributesDetail from './getAttributesDetail'
+import getLabels from './getLabels'
+import getCity from './getCity'
+
+export {
+  getAttributesDetail,
+}
+
+export default function getConfig (payload, option = {}) {
+  return new Promise(async (resolve, reject) => {
+    const asyncOptionList = {
+      category_id1: {
+        action: getCategories,
+        payload: { parent_id: 0 },
+      },
+      category_id2: {
+        action: getCategories,
+        payload: { parent_id: option.category_id1 },
+      },
+      category_id3: {
+        action: getCategories,
+        payload: { parent_id: option.category_id2 },
+      },
+      attribute_category_id: {
+        action: getAttributes,
+      },
+    }
+
+    const autoOptionList = {
+      label: {
+        action: getLabels,
+      },
+      city_ids: {
+        action: getCity,
+      },
+    }
+
+    if (!payload || !payload.keyword) {
+      console.warn('获取公共配置必须带有key字段的参数')
+      reject(payload)
+    }
+
+    if (Array.isArray(payload.keyword)) {
+      const option = {}
+      const getList = async keyword => {
+        try {
+          if (store.getAll()[keyword]) {
+            option[keyword] = store.getAll()[keyword]
+            return
+          }
+          if (autoOptionList[keyword]) {
+            const { action, payload } = autoOptionList[keyword]
+            const list = await action(payload)
+            store.set(keyword, list)
+            option[keyword] = list
+            return
+          }
+          store.setAll(await Vue.http.get('/product/config'))
+          option[keyword] = store.getAll()[keyword]
+        } catch (e) {
+          console.error('Config Error: ', e)
+        }
+      }
+
+      for (let keyword of payload.keyword) {
+        await getList(keyword)
+      }
+      if (Object.keys(option).length !== payload.keyword.length) {
+        return reject(option)
+      }
+      return resolve(option)
+    }
+
+    const list = store.get(payload.keyword)
+    if (payload.type === 'async') {
+      try {
+        const config = asyncOptionList[payload.keyword]
+        resolve(await config.action(config.payload))
+      } catch (e) {
+        reject(e)
+      }
+    } else {
+      if (!list) {
+        try {
+          const configData = await Vue.http.get('/product/config')
+          store.setAll(configData)
+          resolve(store.get(payload.keyword) || [])
+        } catch (e) {
+          reject(e)
+        }
+      } else {
+        resolve(list)
+      }
+    }
+  })
+}

+ 35 - 0
examples/actions/config/store.js

@@ -0,0 +1,35 @@
+
+function store () {
+  const data = {}
+
+  return {
+    get (keyword) {
+      return data[keyword]
+    },
+    getAll () {
+      return Object.assign({}, data)
+    },
+    set (keyword, list) {
+      data[keyword] = list
+      this.log()
+    },
+    setAll (remoteData) {
+      Object.keys(remoteData).forEach(keyword => {
+        data[keyword] = Object.keys(remoteData[keyword]).map(value => {
+          return {
+            value: isNaN(Number(value)) ? value : Number(value),
+            label: remoteData[keyword][value],
+          }
+        })
+      })
+      this.log()
+    },
+    log () {
+      console.group('Config change:')
+      console.log(this.getAll())
+      console.groupEnd('Config change:')
+    },
+  }
+}
+
+export default store()

+ 17 - 0
examples/actions/index.js

@@ -0,0 +1,17 @@
+
+import getConfig, * as config from './config'
+import * as category from './category'
+import * as product from './product'
+import * as comment from './comment'
+import * as attribute from './attribute'
+import * as trash from './trash'
+
+export default {
+  getConfig,
+  ...config,
+  ...category,
+  ...product,
+  ...comment,
+  ...attribute,
+  ...trash,
+}

+ 26 - 0
examples/actions/product/deleteProductList.js

@@ -0,0 +1,26 @@
+
+import Vue from 'vue'
+import iview from 'iview'
+
+export default function deleteProductList (payload = {}) {
+  console.log(payload)
+  return new Promise(async (resolve, reject) => {
+    const { id, sale_name: saleName } = payload
+    iview.Modal.confirm({
+      title: '删除分类',
+      content: `确定要删除"${saleName}"分类吗?`,
+      onOk: async () => {
+        try {
+          await Vue.http.delete(`/product/product?id=${id}`)
+          iview.Message.success('删除成功')
+          resolve('remove')
+        } catch (e) {
+          reject(e)
+        }
+      },
+      onCancel: (e) => {
+        reject(e)
+      },
+    })
+  })
+}

+ 94 - 0
examples/actions/product/editProduct.js

@@ -0,0 +1,94 @@
+
+import Vue from 'vue'
+import iview from 'iview'
+
+export default function editCategory (payload = {}, option = {}) {
+  return new Promise(async (resolve, reject) => {
+    const data = JSON.parse(JSON.stringify(payload))
+    data.store_type_ids = data.store_type_ids.join()
+    const error = {}
+    const requestBody = {}
+
+    // 字段验证方法
+    const checkFuncs = {
+      city_ids (value) {
+        return value.length ? '' : '请选择可售卖城市'
+      },
+      name (value) {
+        return value && value.trim() ? '' : '商品名称不能为空'
+      },
+      sale_name (value) {
+        return value && value.trim() ? '' : '上架名称不能为空'
+      },
+      category_id1 (value) {
+        return data.category_id3 ? '' : '请选择商品分类'
+      },
+      img (value) {
+        return value ? '' : '请选择上传商品主图'
+      },
+      imgs (value) {
+        return value.length ? '' : '请上传至少一张商品图集'
+      },
+      store_type_ids (value) {
+        return value.length ? '' : '请选择商品储藏方式'
+      },
+      label (value) {
+        return value.length ? '' : '请选择商品检测标签'
+      },
+      attribute_category_id (value) {
+        return value ? '' : '请选择商品规格属性分类'
+      },
+      sku (value) {
+        return value.length ? '' : '请生成至少一条sku'
+      },
+      parameter (value) {
+        return value.length ? '' : '请选择至少一条商品参数'
+      },
+      deliver_type (value) {
+        return typeof value === 'number' && value > -1 ? '' : '请选择商品配送方式'
+      },
+      is_confirm_sale (value) {
+        return typeof value === 'number' && value > -1 ? '' : '请选择商品上架时间'
+      },
+      receive_type (value) {
+        return value === 0 || data.receive_time ? '' : '请输入具体送达时间'
+      },
+      limit_number (value) {
+        return typeof value === 'number' && value > -1 ? '' : '请输入正确的限购数量'
+      },
+      limit_type (value) {
+        return typeof value === 'number' && value > -1 ? '' : '请选择限购类型'
+      },
+    }
+
+    const valid = Object.keys(data).reduce((resultList, key) => {
+      const func = checkFuncs[key]
+      const value = data[key]
+      const errorText = func ? func(value) : ''
+      if (!errorText) requestBody[key] = value
+      error[key] = errorText
+      resultList.push(!errorText)
+      return resultList
+    }, []).every(Boolean)
+    if (!valid) return reject(error)
+
+    if (data.id) requestBody.id = data.id
+
+    delete requestBody.category_id1
+    delete requestBody.category_id2
+    delete requestBody.category_name1
+    delete requestBody.category_name2
+    delete requestBody.category_name3
+    delete requestBody.parameterData
+    delete requestBody.patternData
+
+    try {
+      const method = requestBody.id ? 'put' : 'post'
+      await Vue.http[method]('/product/product', requestBody)
+      iview.Message.success('保存成功')
+      resolve()
+    } catch (e) {
+      reject(e)
+    }
+  })
+}

+ 580 - 0
examples/actions/product/getProduct.js

@@ -0,0 +1,580 @@
+
+import Vue from 'vue'
+import store from '@/store'
+
+export default function getPattern (payload = {}) {
+  const role = store.state.user.role
+  return new Promise(async (resolve, reject) => {
+    const id = payload.id
+    // 城市
+    const cityComponentData = {
+      nodeType: 'item',
+      required: true,
+      label: `可售卖城市`,
+      nodeList: [
+        {
+          nodeType: 'select',
+          keyword: 'city_ids',
+          hub: 'product-detail',
+          props: {
+            placeholder: `请选择可售卖城市`,
+            multiple: true,
+          },
+        },
+      ],
+    }
+
+    // 对应商品
+    const codeComponentData = {
+      nodeType: 'item',
+      required: false,
+      label: `对应商品`,
+      nodeList: [
+        {
+          nodeType: 'input',
+          keyword: 'other_code',
+          hub: 'product-detail',
+          props: {
+            placeholder: `请选择对应商品`,
+          },
+        },
+      ],
+    }
+
+    // 商品名称
+    const nameComponentData = {
+      nodeType: 'item',
+      required: true,
+      label: '商品名称',
+      nodeList: [
+        {
+          nodeType: 'input',
+          keyword: 'name',
+          hub: 'product-detail',
+          props: {
+            placeholder: `请输入商品名称`,
+          },
+        },
+      ],
+    }
+
+    // 上架名称
+    const saleComponentData = {
+      nodeType: 'item',
+      required: true,
+      label: '上架名称',
+      nodeList: [
+        {
+          nodeType: 'input',
+          keyword: 'sale_name',
+          hub: 'product-detail',
+          props: {
+            placeholder: `请输入上架名称`,
+          },
+        },
+      ],
+    }
+
+    // 子标题
+    const subtitleComponentData = {
+      nodeType: 'item',
+      required: false,
+      label: '子标题',
+      nodeList: [
+        {
+          nodeType: 'input',
+          keyword: 'subtitle',
+          hub: 'product-detail',
+          props: {
+            placeholder: `请选输入子标题`,
+          },
+        },
+      ],
+    }
+
+    // 分类
+    const levelComponentData = {
+      nodeType: 'item',
+      required: true,
+      label: '商品分类',
+      nodeList: [
+        {
+          nodeType: 'select',
+          keyword: 'category_id1',
+          hub: 'product-detail',
+          props: {
+            placeholder: '选择一级分类',
+            filterable: true,
+            clearable: true,
+          },
+        },
+        {
+          nodeType: 'select',
+          keyword: 'category_id2',
+          hub: 'product-detail',
+          props: {
+            placeholder: '选择二级分类',
+            filterable: true,
+            clearable: true,
+          },
+          dependency: {
+            type: 'hide',
+            keyword: 'category_id1',
+          },
+        },
+        {
+          nodeType: 'select',
+          keyword: 'category_id3',
+          hub: 'product-detail',
+          props: {
+            placeholder: '选择三级分类',
+            filterable: true,
+            clearable: true,
+          },
+          dependency: {
+            type: 'hide',
+            keyword: 'category_id2',
+          },
+        },
+      ],
+    }
+
+    // 商品主图
+    const imgComponentData = {
+      nodeType: 'item',
+      required: true,
+      label: '商品主图',
+      tip: '图片建议尺寸:240*240像素',
+      nodeList: [
+        {
+          nodeType: 'uploader',
+          keyword: 'img',
+          hub: 'product-detail',
+          props: {},
+        },
+      ],
+    }
+
+    // 通栏大图
+    const bigImgComponentData = {
+      nodeType: 'item',
+      required: false,
+      label: '通栏大图',
+      tip: '图片建议尺寸:750*300像素,用于通栏模式下的商品列表页展示',
+      nodeList: [
+        {
+          nodeType: 'uploader',
+          keyword: 'big_img',
+          hub: 'product-detail',
+          props: {},
+        },
+      ],
+    }
+
+    // 商品视频
+    const videoComponentData = {
+      nodeType: 'item',
+      required: false,
+      label: '商品视频',
+      tip: '视频建议时长9-30秒,建议视频宽高比16:9',
+      nodeList: [
+        {
+          nodeType: 'uploader',
+          keyword: 'video',
+          hub: 'product-detail',
+          props: {
+            type: 'video',
+          },
+        },
+      ],
+    }
+
+    // 商品图集
+    const imgsComponentData = {
+      nodeType: 'item',
+      required: true,
+      label: '商品图集',
+      size: 'flow',
+      tip: '建议尺寸:340*340像素,比例为1:1,最多上传15张',
+      nodeList: [
+        {
+          nodeType: 'uploader',
+          keyword: 'imgs',
+          hub: 'product-detail',
+          props: {
+            max: 15,
+          },
+        },
+      ],
+    }
+
+    // 储藏方式
+    const storeTypeComponentData = {
+      nodeType: 'item',
+      required: true,
+      label: '储藏方式',
+      nodeList: [
+        {
+          nodeType: 'checkboxGroup',
+          keyword: 'store_type_ids',
+          hub: 'product-detail',
+          props: {},
+        },
+      ],
+    }
+
+    // 储藏方式
+    const tagComponentData = {
+      nodeType: 'item',
+      required: true,
+      label: '检测标签',
+      nodeList: [
+        {
+          nodeType: 'tag',
+          keyword: 'label',
+          hub: 'product-detail',
+          props: {},
+        },
+      ],
+    }
+
+    // 检测报告
+    const reportComponentData = {
+      nodeType: 'item',
+      required: false,
+      label: '检测报告',
+      nodeList: [
+        {
+          nodeType: 'uploader',
+          keyword: 'report',
+          hub: 'product-detail',
+          props: {
+            max: 9,
+          },
+        },
+      ],
+    }
+
+    const attributeComponentData = {
+      nodeType: 'item',
+      required: true,
+      label: '规格属性分类',
+      nodeList: [
+        {
+          nodeType: 'attribute',
+          keyword: 'attribute_category_id',
+          hub: 'product-detail',
+          props: {},
+        },
+      ],
+    }
+
+    const patternComponentData = {
+      nodeType: 'item',
+      required: true,
+      label: '规格属性',
+      size: 'flow',
+      nodeList: [
+        {
+          nodeType: 'pattern',
+          keyword: 'sku',
+          hub: 'product-detail',
+          props: {},
+        },
+      ],
+    }
+
+    const parameterComponentData = {
+      nodeType: 'item',
+      required: true,
+      label: '商品参数',
+      size: 'flow',
+      nodeList: [
+        {
+          nodeType: 'parameter',
+          keyword: 'parameter',
+          hub: 'product-detail',
+          props: {},
+        },
+      ],
+    }
+
+    const deliverTypeComponentData = {
+      nodeType: 'item',
+      required: true,
+      label: '配送方式',
+      nodeList: [
+        {
+          nodeType: 'radioGroup',
+          keyword: 'deliver_type',
+          hub: 'product-detail',
+          props: {},
+        },
+      ],
+    }
+
+    const confirmSaleComponentData = {
+      nodeType: 'item',
+      required: true,
+      label: '审核后立即上架',
+      nodeList: [
+        {
+          nodeType: 'radioGroup',
+          keyword: 'is_confirm_sale',
+          hub: 'product-detail',
+          props: {},
+        },
+      ],
+    }
+
+    const receiveComponentData = {
+      nodeType: 'item',
+      required: true,
+      label: '预计配送时间',
+      nodeList: [
+        {
+          nodeType: 'receive',
+          keyword: 'receive_type',
+          hub: 'product-detail',
+          props: {},
+        },
+      ],
+    }
+
+    const limitNumComponentData = {
+      nodeType: 'item',
+      required: true,
+      label: '限购数量',
+      tip: '数量为0则不限购',
+      nodeList: [
+        {
+          nodeType: 'inputNumber',
+          keyword: 'limit_number',
+          hub: 'product-detail',
+          props: {
+            min: 0,
+          },
+        },
+      ],
+    }
+
+    const limitTypeComponentData = {
+      nodeType: 'item',
+      required: true,
+      label: '限购类型',
+      nodeList: [
+        {
+          nodeType: 'radioGroup',
+          keyword: 'limit_type',
+          hub: 'product-detail',
+          props: {},
+        },
+      ],
+    }
+
+    const descComponentData = {
+      nodeType: 'item',
+      required: false,
+      label: '商品详情',
+      size: 'flow',
+      nodeList: [
+        {
+          nodeType: 'editor',
+          keyword: 'desc',
+          hub: 'product-detail',
+          props: {},
+        },
+      ],
+    }
+
+    // 整体组件数据
+    const componentData = [
+      {
+        nodeType: 'note',
+        text: '',
+      },
+      {
+        nodeType: 'block',
+        nodeList: [
+          {
+            nodeType: 'note',
+            text: '基本信息',
+          },
+          cityComponentData,
+          codeComponentData,
+          nameComponentData,
+          saleComponentData,
+          subtitleComponentData,
+          levelComponentData,
+          imgComponentData,
+          bigImgComponentData,
+          videoComponentData,
+          imgsComponentData,
+          storeTypeComponentData,
+          tagComponentData,
+          reportComponentData,
+        ],
+      },
+      {
+        nodeType: 'block',
+        nodeList: [
+          {
+            nodeType: 'note',
+            text: '价格库存',
+          },
+          attributeComponentData,
+          patternComponentData,
+          parameterComponentData,
+        ],
+      },
+      {
+        nodeType: 'block',
+        nodeList: [
+          {
+            nodeType: 'note',
+            text: '物流信息',
+          },
+          deliverTypeComponentData,
+        ],
+      },
+      {
+        nodeType: 'block',
+        nodeList: [
+          {
+            nodeType: 'note',
+            text: '其他信息',
+          },
+          confirmSaleComponentData,
+          receiveComponentData,
+          limitNumComponentData,
+          limitTypeComponentData,
+          descComponentData,
+        ],
+      },
+    ]
+
+    const value = {
+      city_ids: [],
+      other_code: null,
+      name: null,
+      sale_name: null,
+      subtitle: null,
+      category_id1: null,
+      category_id2: null,
+      category_id3: null,
+      img: null,
+      big_img: null,
+      video: null,
+      imgs: [],
+      store_type_ids: [],
+      label: [],
+      report: [],
+      attribute_category_id: null,
+      sku: [],
+      parameter: [],
+      deliver_type: null,
+      is_confirm_sale: null,
+      receive_type: 0,
+      receive_time: '',
+      limit_number: 0,
+      limit_type: 0,
+      desc: '',
+      // 组件使用数据:规格、参数
+      patternData: [],
+      parameterData: [],
+    }
+
+    const option = {
+      city_ids: {
+        list: [],
+      },
+      category_id1: {
+        type: 'async',
+        refresh: true,
+        list: [],
+      },
+      category_id2: {
+        type: 'async',
+        refresh: true,
+        dependency: {
+          keyword: 'category_id1',
+        },
+        list: [],
+      },
+      category_id3: {
+        type: 'async',
+        refresh: true,
+        dependency: {
+          keyword: 'category_id2',
+        },
+        list: [],
+      },
+      attribute_category_id: {
+        type: 'async',
+        list: [],
+      },
+      label: {
+        list: [],
+      },
+      store_type_ids: {
+        list: [],
+      },
+      deliver_type: {
+        list: [],
+      },
+      is_confirm_sale: {
+        list: [],
+      },
+      limit_type: {
+        list: [],
+      },
+    }
+
+    const patternDetailData = {
+      componentData,
+      value,
+      error: {},
+      option,
+    }
+
+    if (id) {
+      try {
+        const { data } = await Vue.http.get(`/product/product/detail?id=${id}`)
+        data.store_type_ids = data.store_type_ids.split(',').map(Number)
+        data.limit_type = Number(data.limit_type)
+        data.city_ids = data.city_ids.map(Number)
+        value.parameterData = data.parameter.map(i => {
+          return {
+            name: i.name,
+            list: [i.value],
+            selected: i.value,
+          }
+        })
+
+        value.patternData = data.spec.map(i => {
+          return {
+            name: i.name,
+            list: i.value,
+            selected: i.value,
+          }
+        })
+
+        Object.assign(value, data)
+        option.category_id1.list.push({ label: data.category_name1, value: data.category_id1 })
+        option.category_id2.list.push({ label: data.category_name2, value: data.category_id2 })
+        option.category_id3.list.push({ label: data.category_name3, value: data.category_id3 })
+
+        resolve({
+          componentData,
+          value,
+          error: {},
+          option,
+          preview: role === 'admin',
+        })
+      } catch (e) {
+        console.log(e)
+      }
+    }
+    resolve(patternDetailData)
+  })
+}

+ 219 - 0
examples/actions/product/getProductList.js

@@ -0,0 +1,219 @@
+
+import Vue from 'vue'
+import store from '@/store'
+
+export default function getProductList (payload = {}, option = {}) {
+  const role = store.state.user.role
+  // const role = 'shop'
+  return new Promise(async (resolve, reject) => {
+    // 筛选项
+    const filters = {
+      keyword: {
+        type: 'input',
+        title: '商品名称/编码',
+        key: 'keyword',
+        props: {
+          placeholder: '商品名称/编码',
+          clearable: true,
+        },
+      },
+      category_id1: {
+        type: 'select',
+        title: '商品分类',
+        key: 'category_id1',
+        props: {
+          placeholder: '商品分类',
+          filterable: true,
+          clearable: true,
+        },
+        option: {
+          type: 'async',
+          list: [],
+        },
+      },
+      up_status: {
+        type: 'select',
+        title: '商品状态',
+        key: 'up_status',
+        props: {
+          placeholder: '商品状态',
+          filterable: true,
+          clearable: true,
+        },
+        option: {
+          list: [],
+        },
+      },
+    }
+    // 操作项
+    let actions = [
+      {
+        title: '添加商品',
+        action: 'goProductDetail',
+        props: {
+          type: 'primary',
+        },
+      },
+    ]
+    if (role !== 'shop') actions = []
+
+    const sortActions = []
+
+    // 表格项
+    // "sale_name"
+    const columns = {
+      spu_code: {
+        title: 'SKU编码',
+        key: 'spu_code',
+        align: 'center',
+        width: 110,
+      },
+      id: {
+        title: '商品ID',
+        key: 'id',
+        align: 'center',
+      },
+      img: {
+        nodeType: 'image',
+        payload: {
+          key: 'img',
+          title: '商品图片',
+          width: 80,
+          height: 80,
+        },
+      },
+      'name&sku_name': {
+        title: '商品名称/编码',
+        key: 'name&sku_name',
+        align: 'center',
+        width: 100,
+      },
+      sale_name: {
+        title: '上架名称',
+        key: 'sale_name',
+        align: 'center',
+        width: 100,
+      },
+      price: {
+        title: '价格',
+        key: 'price',
+        align: 'center',
+        width: 80,
+        format: {
+          type: 'price',
+        },
+      },
+      category_name1: {
+        title: '一级分类',
+        key: 'category_name1',
+        align: 'center',
+        width: 100,
+      },
+      category_name2: {
+        title: '二级分类',
+        key: 'category_name2',
+        align: 'center',
+        width: 100,
+      },
+      category_name3: {
+        title: '三级分类',
+        key: 'category_name3',
+        align: 'center',
+        width: 100,
+      },
+      main_label_name: {
+        title: '检测标签',
+        key: 'main_label_name',
+        align: 'center',
+        width: 100,
+      },
+      label: {
+        title: '商品标签',
+        key: 'label',
+        align: 'center',
+        width: 100,
+        render: (h, { row }) => {
+          const element = []
+          row.label.forEach(item => {
+            element.push(h('div', [item]))
+          })
+          return h('div', {}, [element])
+        },
+      },
+      total_stock: {
+        title: '库存',
+        key: 'total_stock',
+        align: 'center',
+        width: 80,
+      },
+      total_count: {
+        title: '销量',
+        key: 'total_count',
+        align: 'center',
+        width: 80,
+      },
+      up_status: {
+        nodeType: 'iSwitch',
+        payload: {
+          title: role === 'shop' ? '上架' : '销售状态',
+          key: role === 'shop' ? 'up_status' : 'sale_status',
+          action: 'switchProduct',
+        },
+      },
+    }
+
+    const sortColumns = {}
+
+    // 表格扩展(数据操作)
+    const columnsExtra = {
+      nodeType: 'action',
+      payload: {
+        title: '操作',
+        list: [
+          {
+            title: role === 'shop' ? '编辑' : '查看',
+            action: 'goProductDetail',
+            props: {
+              loading: false,
+              type: 'primary',
+            },
+          },
+          {
+            title: '删除',
+            action: 'deleteProductList',
+            props: {
+              loading: false,
+              type: 'warning',
+            },
+          },
+        ],
+      },
+    }
+
+    if (role !== 'shop') columnsExtra.payload.list.splice(1, 1)
+
+    const sortColumnsExtra = {}
+    const url = role === 'shop' ? '/product/product' : '/product/operate/product'
+    try {
+      const isSort = option.type === 'sort'
+      if (isSort) payload.per_page = 1000
+      const { data, extra, meta } = await Vue.http.get(url, {
+        params: payload,
+      })
+      const response = {
+        filters: isSort ? [] : Object.keys(filters).map(i => filters[i]),
+        actions: isSort ? sortActions : actions,
+        data: data,
+        columns: extra.columns
+          .map(key => (isSort ? sortColumns : columns)[key])
+          .filter(Boolean)
+          .concat(isSort ? sortColumnsExtra : columnsExtra),
+        page: isSort ? {} : meta.pagination,
+        title: '商品列表',
+      }
+      resolve(response)
+    } catch (e) {
+      reject(e)
+    }
+  })
+}

+ 12 - 0
examples/actions/product/goProductDetail.js

@@ -0,0 +1,12 @@
+
+import Vue from 'vue'
+
+export default function goProductDetail (payload = {}) {
+  return new Promise(async (resolve) => {
+    const { id, sale_name: saleName } = payload
+    if (!id) {
+      Vue.stack.push('product-detail')
+    } else Vue.stack.push('product-detail', saleName, { id })
+    resolve()
+  })
+}

+ 16 - 0
examples/actions/product/index.js

@@ -0,0 +1,16 @@
+
+import editProduct from './editProduct'
+import getProduct from './getProduct'
+import getProductList from './getProductList'
+import deleteProductList from './deleteProductList'
+import switchProduct from './switchProduct'
+import goProductDetail from './goProductDetail'
+
+export {
+  getProduct,
+  editProduct,
+  getProductList,
+  deleteProductList,
+  switchProduct,
+  goProductDetail,
+}

+ 20 - 0
examples/actions/product/switchProduct.js

@@ -0,0 +1,20 @@
+
+import Vue from 'vue'
+import store from '@/store'
+
+export default function switchCategory (payload = {}, option) {
+  return new Promise(async (resolve, reject) => {
+    const { id } = payload
+    const role = store.state.user.role
+    const requestBody = {
+      id,
+    }
+    try {
+      const url = role === 'shop' ? '/product/product/up_status' : '/product/operate/product/sale_status'
+      await Vue.http.put(url, requestBody)
+      resolve()
+    } catch (e) {
+      reject(e)
+    }
+  })
+}

+ 158 - 0
examples/actions/trash/getTrashList.js

@@ -0,0 +1,158 @@
+
+import Vue from 'vue'
+
+export default function getTrashList (payload = {}, option = {}) {
+  return new Promise(async (resolve, reject) => {
+    // 筛选项
+    const filters = {
+
+      keyword: {
+        type: 'input',
+        title: '商品名称',
+        key: 'keyword',
+        props: {
+          placeholder: '商品名称',
+          clearable: true,
+        },
+      },
+      category_id1: {
+        type: 'select',
+        title: '商品分类',
+        key: 'category_id1',
+        props: {
+          placeholder: '商品分类',
+          filterable: true,
+          clearable: true,
+        },
+        option: {
+          type: 'async',
+          list: [],
+        },
+      },
+    }
+    // 操作项
+    const actions = []
+    const sortActions = []
+
+    // 表格项
+    // "sale_name"
+    const columns = {
+      spu_code: {
+        title: 'SKU编码',
+        key: 'spu_code',
+        align: 'center',
+        width: 110,
+      },
+      id: {
+        title: '商品ID',
+        key: 'id',
+        align: 'center',
+        width: 80,
+      },
+      img: {
+        nodeType: 'image',
+        payload: {
+          key: 'img',
+          title: '商品图片',
+          width: 80,
+          height: 80,
+        },
+      },
+      sale_name: {
+        title: '商品名称',
+        key: 'sale_name',
+        align: 'center',
+      },
+      category_name1: {
+        title: '一级分类',
+        key: 'category_name1',
+        align: 'center',
+      },
+      category_name2: {
+        title: '二级分类',
+        key: 'category_name2',
+        align: 'center',
+      },
+      category_name3: {
+        title: '三级分类',
+        key: 'category_name3',
+        align: 'center',
+      },
+      price: {
+        title: '商品价格',
+        key: 'price',
+        align: 'center',
+      },
+    }
+
+    const sortColumns = {}
+
+    // 表格扩展(数据操作)
+    const columnsExtra = {
+      nodeType: 'action',
+      payload: {
+        title: '操作',
+        list: [
+          {
+            title: '还原',
+            action: 'handleTrashList',
+            actionOption: {
+              status: 1,
+              type: 'one',
+            },
+            props: {
+              loading: false,
+              type: 'primary',
+            },
+          },
+          {
+            title: '删除',
+            action: 'handleTrashList',
+            actionOption: {
+              status: 0,
+              type: 'one',
+            },
+            props: {
+              loading: false,
+              type: 'warning',
+            },
+          },
+        ],
+      },
+    }
+
+    const sortColumnsExtra = {}
+
+    try {
+      const isSort = option.type === 'sort'
+      if (isSort) payload.per_page = 1000
+      const { data, extra, meta } = await Vue.http.get('/product/product?is_delete=1', {
+        params: payload,
+      })
+      const response = {
+        filters: isSort ? [] : extra.filters.map(key => filters[key]).filter(Boolean),
+        actions: isSort ? sortActions : actions,
+        data: data,
+        columns: extra.columns
+          .map(key => (isSort ? sortColumns : columns)[key])
+          .filter(Boolean)
+          .concat(isSort ? sortColumnsExtra : columnsExtra),
+        page: isSort ? {} : meta.pagination,
+        title: '回收站列表',
+        batches: [
+          { value: 'handleTrashList', label: '批量删除', option: { status: 0 } },
+          { value: 'handleTrashList', label: '批量还原', option: { status: 1 } },
+        ],
+      }
+      // 是否有批量操作
+      response.columns.unshift({
+        type: 'selection',
+        width: 60,
+        align: 'center',
+      })
+      resolve(response)
+    } catch (e) {
+      reject(e)
+    }
+  })
+}

+ 29 - 0
examples/actions/trash/handleTrashList.js

@@ -0,0 +1,29 @@
+
+import Vue from 'vue'
+import iview from 'iview'
+
+export default function handleTrashList (payload = {}, option) {
+  // 单个操作伪装成多个操作
+  payload = option.type && option.type === 'one' ? [payload] : payload
+  return new Promise(async (resolve, reject) => {
+    const saleName = payload.map(item => item.sale_name, {})
+    const ids = payload.map(item => item.id, {})
+    const tips = option.status === 0 ? '删除' : '还原'
+    iview.Modal.confirm({
+      title: `${tips}商品`,
+      content: `确定要${tips} "${saleName.join(',')}" 商品吗?`,
+      onOk: async () => {
+        try {
+          await Vue.http.put(`/product/product/waste_status`, { 'status': option.status, ids })
+          iview.Message.success(`${tips}成功`)
+          resolve('remove')
+        } catch (e) {
+          reject(e)
+        }
+      },
+      onCancel: (e) => {
+        reject(e)
+      },
+    })
+  })
+}

+ 9 - 0
examples/actions/trash/index.js

@@ -0,0 +1,9 @@
+// 回收站
+
+import getTrashList from './getTrashList'
+import handleTrashList from './handleTrashList'
+
+export {
+  getTrashList,
+  handleTrashList,
+}

+ 0 - 0
examples/assets/styles/main.scss


+ 67 - 0
examples/components/Layout.vue

@@ -0,0 +1,67 @@
+<template>
+  <div class="Layout">
+    <nav class="Layout__nav" v-if="!$store.state.isMinion">
+      <NavMenu :menus="menus" />
+    </nav>
+    <main class="Layout__main">
+      <article class="Layout__content">
+        <router-view />
+      </article>
+    </main>
+  </div>
+</template>
+
+<script>
+import { mapState } from 'vuex'
+import NavMenu from '@/components/common/menu'
+
+export default {
+  components: {
+    NavMenu,
+  },
+  computed: {
+    ...mapState('user', {
+      userInfo: 'info',
+      menus: 'menus',
+    }),
+  },
+  methods: {
+    goBack ({ keyCode }) {
+      if (keyCode === 27) this.$stack.go()
+    },
+  },
+  mounted () {
+    window.addEventListener('keydown', this.goBack, false)
+  },
+  beforeDestroy () {
+    window.removeEventListener('keydown', this.goBack, false)
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+.Layout {
+  display: flex;
+  width: 100vw;
+  height: 100vh;
+  overflow: hidden;
+  background: #fff;
+}
+
+.Layout__nav {
+  flex: 0 0 auto;
+  overflow: hidden;
+}
+
+.Layout__main {
+  display: flex;
+  flex-direction: column;
+  flex: 1;
+  overflow: hidden;
+}
+
+.Layout__content {
+  flex: 1;
+  overflow: hidden;
+}
+</style>

+ 103 - 0
examples/components/Sign.vue

@@ -0,0 +1,103 @@
+<template>
+  <div class="Sign">
+    <div class="wrap" :class="{ 'wrap--error': error }">
+      <Form>
+        <FormItem>
+          <Input
+            prefix="ios-contact-outline"
+            v-model="username"
+            placeholder="用户"
+            :disabled="loading"
+            @on-enter="submit" />
+        </FormItem>
+        <FormItem>
+          <Input
+            type="password"
+            prefix="ios-lock-outline"
+            v-model="password"
+            placeholder="密码"
+            :disabled="loading"
+            @on-enter="submit" />
+        </FormItem>
+        <FormItem>
+          <Button
+            type="primary"
+            long
+            :disabled="!username || !password"
+            :loading="loading"
+            @click="submit">登录</Button>
+        </FormItem>
+      </Form>
+    </div>
+  </div>
+</template>
+
+<script>
+import { mapActions } from 'vuex'
+
+export default {
+  data () {
+    return {
+      loading: false,
+      error: false,
+      username: null,
+      password: null,
+    }
+  },
+  methods: {
+    ...mapActions('user', [
+      'login',
+    ]),
+    async submit () {
+      this.loading = true
+      try {
+        await this.login({ username: this.username, password: this.password })
+        this.$router.replace({ name: 'root' })
+      } catch {
+        this.error = true
+        setTimeout(() => {
+          this.error = false
+          this.loading = false
+        }, 300)
+      }
+    },
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+.Sign {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  height: 100vh;
+  width: 100vw;
+  background: #fafafa;
+}
+
+.wrap {
+  width: 320px;
+  padding: 24px;
+  padding-bottom: 0;
+  background: #fff;
+  border-radius: 4px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, .08);
+
+  &--error {
+    @keyframes shake {
+      0%, 100% {
+        transform: translate(0, 0);
+      }
+
+      20%, 60% {
+        transform: translate(-8px, 0);
+      }
+
+      40%, 80% {
+        transform: translate(8px, 0);
+      }
+    }
+    animation: shake .3s linear forwards;
+  }
+}
+</style>

+ 42 - 0
examples/components/attribute/AttributeList.vue

@@ -0,0 +1,42 @@
+<template>
+  <div>
+    <CommonFilter
+      ref="filter"
+      action="getAttributeList"
+      :fixedValues="query['attribute-list']" />
+    <CommonWrapper name="attribute-detail">
+      <CommonDetail
+        name="attribute-detail"
+        :query="query"
+        resetAction="getAttributeDetail"
+        submitAction="editAttributeDetail"
+        @on-save="onDetailSave">
+      </CommonDetail>
+    </CommonWrapper>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'attribute-list',
+  props: {
+    query: {
+      type: Object,
+      required: false,
+      default () {
+        return {}
+      },
+    },
+  },
+  methods: {
+    init () {
+      this.$refs.filter.request()
+    },
+    onDetailSave () {
+      this.$emit('on-save')
+      this.$stack.go()
+      this.init()
+    },
+  },
+}
+</script>

+ 50 - 0
examples/components/attribute/index.vue

@@ -0,0 +1,50 @@
+<template>
+  <CommonStack title="商品属性" :query="query">
+    <template v-slot:default="{ stackQuery }">
+      <CommonFilter ref="filter" action="getAttributeMainList" />
+      <CommonWrapper name="attribute-main">
+        <CommonDetail
+          name="attribute-main"
+          :query="stackQuery"
+          resetAction="getAttributeMain"
+          submitAction="editAttributeMain"
+          @on-save="onMainSave" />
+      </CommonWrapper>
+      <CommonWrapper name="attribute-list">
+        <AttributeList
+          :query="stackQuery"
+          name="attribute-list"
+          @on-save="init" />
+      </CommonWrapper>
+    </template>
+  </CommonStack>
+</template>
+
+<script>
+import AttributeList from './AttributeList'
+
+export default {
+  name: 'attribute',
+  components: {
+    AttributeList,
+  },
+  props: {
+    query: {
+      type: Object,
+      required: false,
+      default () {
+        return {}
+      },
+    },
+  },
+  methods: {
+    init () {
+      this.$refs.filter.request()
+    },
+    onMainSave () {
+      this.$stack.go()
+      this.init()
+    },
+  },
+}
+</script>

+ 47 - 0
examples/components/category/CategoryList.vue

@@ -0,0 +1,47 @@
+<template>
+  <div>
+    <CommonFilter
+      ref="filter"
+      action="getCategoryList"
+      :actionOption="query[name]"
+      :fixedValues="{ parent_id: query[name].parent_id }"
+      @on-refresh="$emit('on-refresh')" />
+    <CommonWrapper name="category-detail">
+      <CommonDetail
+        name="category-detail"
+        :query="query"
+        resetAction="getCategory"
+        submitAction="editCategory"
+        @on-save="onDetailSave" />
+    </CommonWrapper>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'category-list',
+  props: {
+    query: {
+      type: Object,
+      required: false,
+      default () {
+        return {}
+      },
+    },
+    name: {
+      type: String,
+      required: false,
+      default: 'category',
+    },
+  },
+  methods: {
+    init () {
+      this.$refs.filter.request()
+    },
+    onDetailSave () {
+      this.$stack.go()
+      this.init()
+    },
+  },
+}
+</script>

+ 54 - 0
examples/components/category/index.vue

@@ -0,0 +1,54 @@
+<template>
+  <CommonStack title="商品分类" :query="Object.assign({}, query, { parent_id: 0 })">
+    <template v-slot:default="{ stackQuery }">
+      <CategoryList :query="stackQuery" ref="category" />
+      <CommonWrapper name="category-list-2">
+        <CategoryList :query="stackQuery" name="category-list-2" />
+      </CommonWrapper>
+      <CommonWrapper name="category-list-3">
+        <CategoryList :query="stackQuery" name="category-list-3" />
+      </CommonWrapper>
+      <CommonWrapper name="category-sort">
+        <CategoryList
+          :query="stackQuery"
+          name="category-sort"
+          @on-refresh="onRefresh" />
+      </CommonWrapper>
+      <CommonWrapper name="category-product-sort">
+        <CommonFilter
+          action="getCategoryProductSort"
+          :fixedValues="{
+            category_id1: stackQuery['category-product-sort']
+              ? stackQuery['category-product-sort'].category_id1
+              : {}
+          }"
+        />
+      </CommonWrapper>
+    </template>
+  </CommonStack>
+</template>
+
+<script>
+import CategoryList from './CategoryList'
+
+export default {
+  name: 'category',
+  props: {
+    query: {
+      type: Object,
+      required: false,
+      default () {
+        return {}
+      },
+    },
+  },
+  components: {
+    CategoryList,
+  },
+  methods: {
+    onRefresh () {
+      this.$refs.category.$refs.filter.request()
+    },
+  },
+}
+</script>

+ 40 - 0
examples/components/comment/index.vue

@@ -0,0 +1,40 @@
+<template>
+  <CommonStack title="商品评价" :query="Object.assign({}, query, {  })">
+    <template v-slot:default="{ stackQuery }">
+      <CommonFilter
+        ref="filter"
+        action="getCommentList"
+        :actionOption="query[name]"
+        :fixedValues="{  }"
+        @on-refresh="$emit('on-refresh')" />
+    </template>
+  </CommonStack>
+</template>
+
+<script>
+
+export default {
+  name: 'comment',
+  props: {
+    query: {
+      type: Object,
+      required: false,
+      default () {
+        return {}
+      },
+    },
+    name: {
+      type: String,
+      required: false,
+      default: 'comment',
+    },
+  },
+  mounted () {
+  },
+  components: {
+
+  },
+  methods: {
+  },
+}
+</script>

+ 31 - 0
examples/components/common/editor/detail.vue

@@ -0,0 +1,31 @@
+<template>
+  <div>
+    <editor :content="content" @on-change="onChange" />
+  </div>
+</template>
+
+<script>
+import editor from './index'
+
+export default {
+  components: {
+    editor,
+  },
+  props: ['data', 'value', 'option', 'error'],
+  data () {
+    return {
+      content: this.value[this.data.keyword],
+    }
+  },
+  methods: {
+    onChange (content) {
+      this.$hub.$emit(this.data.hub, {
+        type: 'value',
+        payload: Object.assign(this.value, {
+          [this.data.keyword]: content,
+        }),
+      })
+    },
+  },
+}
+</script>

+ 87 - 0
examples/components/common/editor/index.vue

@@ -0,0 +1,87 @@
+<template>
+  <div class="Editor">
+    <vue-ueditor-wrap :config="config" v-model="html" />
+    <Button class="Editor__preview" type="info" @click="preview">预览</Button>
+  </div>
+</template>
+
+<script>
+import VueUeditorWrap from './ueditor'
+
+export default {
+  props: ['content'],
+  components: {
+    VueUeditorWrap,
+  },
+  data () {
+    return {
+      html: '',
+      config: {
+        serverUrl: `${process.env.API_URL.default}config/ueupload`,
+        toolbars: [[
+          'fontsize',
+          'paragraph',
+          'lineheight',
+          'forecolor',
+          'backcolor',
+          'bold',
+          'italic',
+          'underline',
+          'strikethrough',
+          'justifyleft',
+          'justifyright',
+          'justifycenter',
+          'blockquote',
+          'insertimage',
+          'removeformat',
+          'formatmatch',
+          'source',
+        ]],
+        imageBlockLine: 'center',
+        autoHeightEnabled: false,
+        initialFrameWidth: 'auto',
+        initialFrameHeight: 400,
+      },
+    }
+  },
+  watch: {
+    content: {
+      type: String,
+      default: '',
+      immediate: true,
+      handler (v) {
+        this.html = v
+      },
+    },
+    html (html) {
+      this.$emit('on-change', html)
+    },
+  },
+  methods: {
+    preview () {
+      this.$Modal.info({
+        title: '预览',
+        scrollable: true,
+        render: h => {
+          return h('div', {
+            style: {
+              width: '375px',
+              height: '500px',
+              overflow: 'scroll',
+            },
+            domProps: {
+              innerHTML: this.html,
+            },
+          })
+        },
+      })
+    },
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+.Editor__preview {
+  margin-top: 10px;
+}
+</style>

+ 202 - 0
examples/components/common/editor/ueditor.vue

@@ -0,0 +1,202 @@
+<template>
+  <script :id="id" :name="name" type="text/plain"></script>
+</template>
+
+<script>
+// 一个简单的事件订阅发布的实现,取代原始Event对象,提升IE下的兼容性
+class LoadEvent {
+  constructor () {
+    this.listeners = {}
+  }
+  on (eventName, callback) {
+    // eslint-disable-next-line no-unused-expressions
+    this.listeners[eventName] === undefined ? this.listeners[eventName] = [] : ''
+    this.listeners[eventName].push(callback)
+  }
+  emit (eventName) {
+    this.listeners[eventName] && this.listeners[eventName].forEach(callback => callback())
+  }
+}
+export default {
+  name: 'VueUeditorWrap',
+  data () {
+    return {
+      id: 'editor' + Math.random().toString().slice(-10),
+      editor: null,
+      status: 0,
+      initValue: '',
+      defaultConfig: {
+        UEDITOR_HOME_URL: '/static/UEditor/',
+        enableAutoSave: false,
+      },
+    }
+  },
+  props: {
+    value: {
+      type: String,
+      default: '',
+    },
+    config: {
+      type: Object,
+      default: function () {
+        return {}
+      },
+    },
+    init: {
+      type: Function,
+      default: function () {
+        return () => {}
+      },
+    },
+    destroy: {
+      type: Boolean,
+      default: false,
+    },
+    name: {
+      type: String,
+      default: '',
+    },
+  },
+  computed: {
+    mixedConfig () {
+      return Object.assign({}, this.defaultConfig, this.config)
+    },
+  },
+  methods: {
+    // 添加自定义按钮
+    registerButton: ({ name, icon, tip, handler, UE = window.UE }) => {
+      UE.registerUI(name, (editor, name) => {
+        editor.registerCommand(name, {
+          execCommand: () => {
+            handler(editor, name)
+          },
+        })
+        const btn = new UE.ui.Button({
+          name,
+          title: tip,
+          cssRules: `background-image: url(${icon}) !important;background-size: cover;`,
+          onclick () {
+            editor.execCommand(name)
+          },
+        })
+        editor.addListener('selectionchange', () => {
+          const state = editor.queryCommandState(name)
+          if (state === -1) {
+            btn.setDisabled(true)
+            btn.setChecked(false)
+          } else {
+            btn.setDisabled(false)
+            btn.setChecked(state)
+          }
+        })
+        return btn
+      })
+    },
+    // 实例化编辑器
+    _initEditor () {
+      this.$nextTick(() => {
+        this.init()
+        this.editor = window.UE.getEditor(this.id, this.mixedConfig)
+        this.editor.addListener('ready', () => {
+          this.status = 2
+          this.editor.setContent(this.initValue)
+          this.$emit('ready', this.editor)
+          this.editor.addListener('contentChange', () => {
+            this.$emit('input', this.editor.getContent())
+          })
+        })
+      })
+    },
+    // 检测依赖,确保 UEditor 资源文件已加载完毕
+    _checkDependencies () {
+      return new Promise((resolve, reject) => {
+        // 判断ueditor.config.js和ueditor.all.js是否均已加载(仅加载完ueditor.config.js时UE对象和UEDITOR_CONFIG对象存在,仅加载完ueditor.all.js时UEDITOR_CONFIG对象存在,但为空对象)
+        let scriptsLoaded = !!window.UE && !!window.UEDITOR_CONFIG && Object.keys(window.UEDITOR_CONFIG).length !== 0 && !!window.UE.getEditor
+        if (scriptsLoaded) {
+          resolve()
+        } else if (window.loadEnv) { // 利用订阅发布,确保同时渲染多个组件时,不会重复创建script标签
+          window.loadEnv.on('scriptsLoaded', () => {
+            resolve()
+          })
+        } else {
+          window.loadEnv = new LoadEvent()
+          // 如果在其他地方只引用ueditor.all.min.js,在加载ueditor.config.js之后仍需要重新加载ueditor.all.min.js,所以必须确保ueditor.config.js已加载
+          this._loadConfig().then(() => this._loadCore()).then(() => {
+            resolve()
+            window.loadEnv.emit('scriptsLoaded')
+          })
+        }
+      })
+    },
+    _loadConfig () {
+      return new Promise((resolve, reject) => {
+        if (window.UE && window.UEDITOR_CONFIG && Object.keys(window.UEDITOR_CONFIG).length !== 0) {
+          resolve()
+          return
+        }
+        let configScript = document.createElement('script')
+        configScript.type = 'text/javascript'
+        configScript.src = this.mixedConfig.UEDITOR_HOME_URL + 'ueditor.config.js'
+        document.getElementsByTagName('head')[0].appendChild(configScript)
+        configScript.onload = function () {
+          if (window.UE && window.UEDITOR_CONFIG && Object.keys(window.UEDITOR_CONFIG).length !== 0) {
+            resolve()
+          } else {
+            console.error('加载ueditor.config.js失败,请检查您的配置地址UEDITOR_HOME_URL填写是否正确!\n', configScript.src)
+          }
+        }
+      })
+    },
+    _loadCore () {
+      return new Promise((resolve, reject) => {
+        if (window.UE && window.UE.getEditor) {
+          resolve()
+          return
+        }
+        let coreScript = document.createElement('script')
+        coreScript.type = 'text/javascript'
+        coreScript.src = this.mixedConfig.UEDITOR_HOME_URL + 'ueditor.all.min.js'
+        document.getElementsByTagName('head')[0].appendChild(coreScript)
+        coreScript.onload = function () {
+          if (window.UE && window.UE.getEditor) {
+            resolve()
+          } else {
+            console.error('加载ueditor.all.min.js失败,请检查您的配置地址UEDITOR_HOME_URL填写是否正确!\n', coreScript.src)
+          }
+        }
+      })
+    },
+    // 设置内容
+    _setContent (value) {
+      value === this.editor.getContent() || this.editor.setContent(value)
+    },
+  },
+  beforeDestroy () {
+    if (this.destroy && this.editor && this.editor.destroy) this.editor.destroy()
+  },
+  // v-model语法糖实现
+  watch: {
+    value: {
+      handler (value) {
+        // 0: 尚未初始化 1: 开始初始化但尚未ready 2 初始化完成并已ready
+        switch (this.status) {
+          case 0:
+            this.status = 1
+            this.initValue = value
+            this._checkDependencies().then(() => this._initEditor())
+            break
+          case 1:
+            this.initValue = value
+            break
+          case 2:
+            this._setContent(value)
+            break
+          default:
+            break
+        }
+      },
+      immediate: true,
+    },
+  },
+}
+</script>

+ 34 - 0
examples/components/common/menu/index.vue

@@ -0,0 +1,34 @@
+<template>
+  <ul>
+    <li v-for="menu in menus">
+      <h2 @click="go(menu.name)">{{menu.title}}</h2>
+    </li>
+  </ul>
+</template>
+
+<script>
+export default {
+  props: ['menus'],
+  data () {
+    return {
+      currentName: '',
+    }
+  },
+  watch: {
+    '$route' () {
+      this.checkRoute()
+    },
+  },
+  methods: {
+    go (name) {
+      this.$router.push({ path: `/${name}/` })
+    },
+    checkRoute () {
+      this.currentName = this.$route.name
+    },
+  },
+  mounted () {
+    this.checkRoute()
+  },
+}
+</script>

+ 78 - 0
examples/components/product/attribute.vue

@@ -0,0 +1,78 @@
+<template>
+  <div>
+    <Select
+      v-if="!value.id"
+      :value="value[data.keyword]"
+      :placeholder="data.props.placeholder"
+      :filterable="data.props.filterable || false"
+      :clearable="data.props.clearable || false"
+      :multiple="data.props.multiple || false"
+      :not-found-text="option[data.keyword].loading || '无匹配数据'"
+      transfer
+      @on-change="onChange"
+      @on-open-change="onOpenChange">
+      <Option
+        v-for="(item, index) in option[data.keyword].list"
+        :key="index"
+        :value="item.value">{{item.label}}</Option>
+    </Select>
+    <error :text="error[data.keyword]" />
+  </div>
+</template>
+<script>
+import allActions from '@/actions'
+
+export default {
+  props: ['data', 'value', 'option', 'error'],
+  data () {
+    return {
+      attribute_category_id: '',
+      attribute_item: [],
+    }
+  },
+  methods: {
+    onChange (e) {
+      this.getAttributeList(e)
+    },
+    onOpenChange (open) {
+      if (!open) return
+      const currentOption = this.option[this.data.keyword]
+      this.$hub.$emit(this.data.hub, {
+        type: 'option',
+        payload: {
+          type: currentOption.type || 'sync',
+          keyword: currentOption.keyword || this.data.keyword,
+        },
+      })
+    },
+    async getAttributeList (v) {
+      const res = await allActions.getAttributesDetail({ attribute_category_id: v })
+      const patternData = []
+      const parameterData = []
+      res.forEach(i => {
+        if (!i.attribute_type) {
+          i.list = i.input_list.split(',')
+          i.selected = []
+          patternData.push(i)
+        } else {
+          i.list = i.input_list.split(',')
+          i.text = ''
+          i.selected = ''
+          parameterData.push(i)
+        }
+      })
+      this.$hub.$emit(this.data.hub, {
+        type: 'value',
+        payload: Object.assign(this.value, {
+          attribute_category_id: v,
+          patternData,
+          parameterData,
+        }),
+      })
+    },
+  },
+}
+</script>
+<style lang="scss" scoped>
+
+</style>

+ 64 - 0
examples/components/product/index.vue

@@ -0,0 +1,64 @@
+<template>
+  <CommonStack title="商品列表" :query="Object.assign({}, query, {  })">
+    <template v-slot:default="{ stackQuery }">
+      <CommonFilter
+        ref="filter"
+        action="getProductList"
+        :actionOption="query[name]"
+        :fixedValues="{  }" />
+      <CommonWrapper name="product-detail">
+        <CommonDetail
+          name="product-detail"
+          :query="stackQuery"
+          resetAction="getProduct"
+          submitAction="editProduct"
+          :components="{ pattern, attribute, parameter, receive, tag, editor }"
+          @on-save="save" />
+        </CommonDetail>
+      </CommonWrapper>
+    </template>
+  </CommonStack>
+</template>
+
+<script>
+import pattern from './pattern'
+import attribute from './attribute'
+import parameter from './parameter'
+import receive from './receive'
+import tag from './tag'
+import editor from '@/components/common/editor/detail'
+
+export default {
+  name: 'comment',
+  props: {
+    query: {
+      type: Object,
+      required: false,
+      default () {
+        return {}
+      },
+    },
+    name: {
+      type: String,
+      required: false,
+      default: 'trash',
+    },
+  },
+  data () {
+    return {
+      pattern,
+      attribute,
+      parameter,
+      receive,
+      tag,
+      editor,
+    }
+  },
+  methods: {
+    save () {
+      this.$stack.go()
+      this.$refs.filter.request()
+    },
+  },
+}
+</script>

+ 98 - 0
examples/components/product/parameter.vue

@@ -0,0 +1,98 @@
+<template>
+  <div>
+    <div class="layout" v-if="parameterData">
+      <template v-for="(attribute, index) in parameterData">
+        <div class="item">
+          <div class="label">{{attribute.name}}</div>
+          <Select
+            v-model="attribute.selected"
+            transfer
+            clearable
+            class="select"
+            @on-change="onChange">
+            <Option
+              v-for="(item, index) in attribute.list"
+              :key="index"
+              :value="item">
+              {{item}}
+            </Option>
+          </Select>
+          <Input
+            class="input"
+            v-model="attribute.text"
+            icon="ios-add-circle"
+            placeholder="添加参数选项"
+            @on-enter="add(index)"
+            @on-click="add(index)">
+          </Input>
+        </div>
+      </template>
+    </div>
+    <error :text="error[data.keyword]" />
+  </div>
+</template>
+<script>
+
+export default {
+  props: ['data', 'value', 'option', 'error'],
+  computed: {
+    parameterData () {
+      return this.value.parameterData
+    },
+  },
+  data () {
+    return {
+
+    }
+  },
+  methods: {
+    add (index) {
+      const current = this.parameterData[index]
+      if (!current.text.trim()) return
+      current.list.push(current.text)
+      current.selected = current.text
+      current.text = ''
+      this.onChange()
+    },
+    onChange () {
+      const data = this.parameterData.filter(i => i.selected).map(i => (
+        {
+          attribute_item_id: i.id,
+          value: i.selected,
+        }
+      ))
+      console.log(data)
+      this.$hub.$emit(this.data.hub, {
+        type: 'value',
+        payload: Object.assign(this.value, { [this.data.keyword]: data }),
+      })
+    },
+  },
+}
+</script>
+<style lang="scss" scoped>
+
+  .layout {
+    padding: 10px;
+    border: 1px solid #ddd;
+  }
+
+  .item {
+    display: flex;
+    align-items: center;
+    padding: 4px;
+  }
+
+  .label {
+    flex: 0 0 50px;
+  }
+
+  .select {
+    margin: 0 6px;
+  }
+
+  .input {
+
+  }
+
+</style>

+ 391 - 0
examples/components/product/pattern.vue

@@ -0,0 +1,391 @@
+<template>
+  <div>
+    <div v-if="patternData">
+      <div class="attr__box">
+        <div class="attr__list" v-if="!value.id">
+          <div class="item" v-for="(attr, index) in patternData" :key="index">
+            <div class="attr__title">
+              <div class="attr__name">{{attr.name}}</div>
+              <Input
+                class="attr__input"
+                v-if="attr.hand_add_status"
+                v-model="attr.text"
+                icon="md-add-circle"
+                @on-enter="addAttr(attr.text, index)"
+                @on-click="addAttr(attr.text, index)"></Input>
+            </div>
+            <div class="attr__item">
+              <CheckboxGroup v-model="attr.selected" @on-change="onCheckGroupChange">
+                <Checkbox
+                  class="tag"
+                  v-for="(value, valueIndex) in attr.list"
+                  :key="valueIndex"
+                  :label="value">
+                  <button class="close">
+                    <Icon
+                      size="20"
+                      color="#ed4014"
+                      type="md-close-circle"
+                      v-on:click.stop.capture="remove(attr, valueIndex)" />
+                  </button>
+                  <span style="user-select: none;">{{value}}</span>
+                </Checkbox>
+              </CheckboxGroup>
+            </div>
+          </div>
+        </div>
+        <div class="attr__table" v-if="tableData.tableHead.length > 5">
+          <div class="thead">
+            <div class="tr">
+              <div class="th" v-for="(head, headIndex) in tableData.tableHead" :key="headIndex">{{head}}</div>
+            </div>
+          </div class="thead">
+          <div class="tbody">
+            <div
+              v-for="(body, bodyIndex) in tableData.tableBody"
+              :key="bodyIndex"
+              class="tr"
+              :class="{ 'disabled': body.invisible }">
+              <div class="td" v-for="(value, valueIndex) in body.value" :key="valueIndex">
+                {{value}}
+              </div>
+              <div class="td">
+                <InputNumber
+                  :disabled="!!body.invisible"
+                  v-model="values[body.id].price"
+                  :min="0"
+                  size="small"
+                  @on-change="onValueChange" />
+              </div>
+              <div class="td">
+                <InputNumber
+                  :disabled="!!body.invisible"
+                  v-model="values[body.id].stock"
+                  :min="0"
+                  :precision="0"
+                  size="small"
+                  @on-change="onValueChange" />
+              </div>
+              <div class="td">
+                <InputNumber
+                  :disabled="!!body.invisible"
+                  v-model="values[body.id].cost_price"
+                  :min="0"
+                  size="small"
+                  @on-change="onValueChange" />
+              </div>
+              <div class="td">
+                <InputNumber
+                  :disabled="!!body.invisible"
+                  v-model="values[body.id].origin_price"
+                  :min="0"
+                  size="small"
+                  @on-change="onValueChange" />
+              </div>
+              <div class="td">
+                <Button
+                  size="small"
+                  :type="values[body.id].is_main ? 'primary' : 'default'"
+                  :disabled="!!values[body.id].invisible"
+                  @click="onMainChange(body.id)">主规格</Button>
+                <Button size="small" @click="onHidden(body.id, values[body.id].invisible)">
+                  {{ values[body.id].invisible ? '恢复' : '隐藏' }}
+                </Button>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+    <error :text="error[data.keyword]" />
+  </div>
+</template>
+<script>
+export default {
+  props: ['data', 'value', 'option', 'error'],
+  computed: {
+    patternData () {
+      return this.value.patternData
+    },
+  },
+  data () {
+    return {
+      products: [],
+      tableData: {
+        tableHead: [],
+        tableBody: [],
+      },
+      values: {},
+    }
+  },
+  methods: {
+    addAttr (v, index) {
+      if (!v.trim()) return
+      this.patternData[index].list.push(v)
+      this.patternData[index].text = ''
+    },
+    remove (attr, valueIndex) {
+      const removeItem = attr.list[valueIndex]
+      const selectedIndex = attr.selected.indexOf(removeItem)
+      if (selectedIndex !== -1) attr.selected.splice(selectedIndex, 1)
+      attr.list.splice(valueIndex, 1)
+      this.getTable()
+    },
+    onCheckGroupChange (e) {
+      this.getTable()
+    },
+    cartesianProduct (array) {
+      if (array.length < 2) return array[0] || []
+      return [].reduce.call(array, (last, current) => {
+        let res = []
+        last.forEach((l) => {
+          current.forEach((c) => {
+            let temp = [].concat(Array.isArray(l) ? l : [l])
+            temp.push(c)
+            res.push(temp)
+          })
+        })
+        return res
+      })
+    },
+    getTable () {
+      const values = Object.assign({}, this.values)
+      const tableHead = this.patternData
+        .filter(i => i.selected.length)
+        .map(i => i.name)
+        .concat(['售价(元)', '库存', '成本价(元)', '划线价(元)', '操作'])
+
+      const tableBody = this.cartesianProduct(
+        this.patternData.filter(i => i.selected.length).map(i => i.selected)
+      ).map(item => {
+        const id = Array.isArray(item) ? item.join('_') : item
+        values[id] = values[id] || {
+          stock: 0,
+          price: 0,
+          origin_price: 0,
+          cost_price: 0,
+          is_main: 0,
+          invisible: 0,
+        }
+        return {
+          id,
+          stock: 0,
+          price: 0,
+          origin_price: 0,
+          cost_price: 0,
+          is_main: 0,
+          invisible: 0,
+          value: Array.isArray(item) ? item : [item],
+        }
+      })
+
+      if (this.value.id) {
+        const exsitIds = []
+        this.value.sku.forEach(({
+          names,
+          price,
+          origin_price: originPrice,
+          cost_price: costPrice,
+          ...others
+        }, index) => {
+          const id = names.map(i => i.value).join('_')
+          exsitIds.push(tableBody.find(t => t.id === id).id)
+          Object.assign(values[id], {
+            invisible: 0,
+            price: price / 100,
+            origin_price: originPrice / 100,
+            cost_price: costPrice / 100,
+            ...others,
+          })
+        })
+        Object.keys(values).forEach(id => {
+          if (this.value.id && !exsitIds.includes(id)) {
+            const index = tableBody.findIndex(i => i.id === id)
+            tableBody.splice(index, 1)
+            delete values[id]
+          }
+        })
+      }
+
+      if (tableBody.length) {
+        let exist = 0
+        Object.keys(values).find(id => {
+          const isMain = values[id].is_main
+          values[id].is_main = Number(tableBody.find(t => t.id === id) && isMain)
+          exist = values[id].is_main
+          return exist
+        })
+        if (!exist) values[tableBody[0].id].is_main = 1
+      }
+      this.tableData.tableHead = tableHead
+      this.tableData.tableBody = tableBody
+      this.values = values
+    },
+    onMainChange (id) {
+      Object.keys(this.values).forEach(i => {
+        this.values[i].is_main = Number(i === id)
+      })
+      this.onValueChange()
+    },
+    onHidden (id, status) {
+      this.values[id].invisible = Number(!this.values[id].invisible)
+      if (status) {
+        let hasMain = false
+        this.tableData.tableBody.forEach(i => {
+          if (this.values[i.id].is_main && !this.values[i.id].invisible) hasMain = true
+        })
+        if (!hasMain) {
+          const item = this.tableData.tableBody.find(i => {
+            return !this.values[i.id].invisible
+          })
+          if (item) this.onMainChange(item.id)
+        }
+      } else {
+        if (this.values[id].is_main) {
+          const item = this.tableData.tableBody.find(i => {
+            return !this.values[i.id].invisible
+          })
+          if (item) this.onMainChange(item.id)
+        }
+      }
+      this.onValueChange()
+    },
+    onValueChange () {
+      const data = Object.keys(this.values).map(id => {
+        const {
+          invisible,
+          price,
+          origin_price: originPrice,
+          cost_price: costPrice,
+          ...other
+        } = this.values[id]
+        const result = !invisible && this.tableData.tableBody.map(i => i.id).includes(id)
+          ? {
+            price: !price ? price : price.toFixed(2) * 100,
+            origin_price: !originPrice ? originPrice : originPrice.toFixed(2) * 100,
+            cost_price: !costPrice ? costPrice : costPrice.toFixed(2) * 100,
+            ...other,
+          }
+          : null
+        if (result && !this.value.id) {
+          result.value = id.split('_')
+        }
+        return result
+      }).filter(Boolean)
+      const item = this.value.patternData.filter(i => i.selected.length).map(i => (
+        {
+          id: i.id,
+          value: i.selected,
+        }
+      ))
+      this.$hub.$emit(this.data.hub, {
+        type: 'value',
+        payload: Object.assign(this.value, {
+          [this.data.keyword]: data,
+          attribute_item: item,
+        }),
+      })
+    },
+  },
+  mounted () {
+    this.getTable()
+  },
+}
+</script>
+<style lang="scss" scoped>
+
+.attr__list {
+  border: 1px solid #ddd;
+  padding: 4px 8px;
+
+  .attr__title {
+    display: flex;
+    align-items: center;
+    padding: 4px 0;
+    background: #eaeaea;
+    .attr__name {
+      width: 100px;
+      font-size: 16px;
+      padding-left: 10px;
+    }
+
+    .attr__input {
+      width: 150px;
+    }
+  }
+
+  .attr__item {
+    margin-top: 4px;
+  }
+}
+
+.item + .item {
+  margin-top: 4px;
+}
+
+.tag {
+  box-sizing: border-box;
+  position: relative;
+  padding: 2px 4px;
+  border: 1px solid #ccc;
+  border-radius: 2px;
+  font-size: 14px;
+
+  .close {
+    position: absolute;
+    top: -10px;
+    right: -10px;
+    opacity: 0;
+    transition: all .2s;
+    border: 0;
+    background: transparent;
+    outline: none;
+  }
+
+  &:hover .close {
+    opacity: 1;
+  }
+}
+
+.attr__table {
+  box-sizing: border-box;
+  width: 100%;
+  font-size: 14px;
+  margin-top: 4px;
+  border: 1px solid #ddd;
+
+  .thead {
+    font-weight: bold;
+    padding: 5px;
+  }
+
+  .tr {
+    box-sizing: inherit;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+
+    .th, .td {
+      box-sizing: inherit;
+      flex: 1;
+      text-align: center;
+    }
+  }
+
+  .tbody {
+    .tr {
+      padding: 5px;
+      border-top: 1px solid #ccc;
+
+      &:hover {
+        background: #ccc;
+      }
+    }
+  }
+}
+
+.disabled {
+  background: #ddd;
+}
+
+</style>

+ 76 - 0
examples/components/product/receive.vue

@@ -0,0 +1,76 @@
+<template>
+  <div>
+    <RadioGroup
+      v-model="send_type"
+      @on-change="e => onChange(e, 'receive_type')">
+      <Radio class="radio" :label="0">
+        次日达
+      </Radio>
+      <Radio class="radio" :label="1">
+        下单
+        <InputNumber
+          v-model="send_days"
+          :min="2"
+          :precision="0"
+          @on-change="e => onChange(e, 'receive_days')"></InputNumber> 日送达
+      </Radio>
+      <Radio class="radio" :label="2">
+        自定义配送时间
+        <Input
+          :maxlength="18"
+          v-model="send_time"
+          @input="e => onChange(e, 'receive_time')"></Input>
+      </Radio>
+    </RadioGroup>
+    <error :text="error[data.keyword]" />
+  </div>
+</template>
+<script>
+export default {
+  props: ['data', 'value', 'option', 'error'],
+  data () {
+    return {
+      send_type: null,
+      send_days: null,
+      send_time: '',
+      receive_time: '',
+    }
+  },
+  methods: {
+    onChange (e, keyword) {
+      if (this.send_type === 1) this.receive_time = this.send_days
+      if (this.send_type === 2) this.receive_time = this.send_time
+      this.$hub.$emit(this.data.hub, {
+        type: 'value',
+        payload: Object.assign(this.value, {
+          receive_type: this.send_type,
+          receive_time: this.receive_time,
+        }),
+      })
+    },
+  },
+  mounted () {
+    this.send_type = this.value[this.data.keyword]
+    if (!this.value[this.data.keyword]) return
+    if (this.value[this.data.keyword] === 1) {
+      this.send_days = Number(this.value.receive_time)
+    } else {
+      this.send_time = this.value.receive_time
+    }
+  },
+}
+</script>
+<style lang="scss" scoped>
+
+.radiogroup {
+  display: flex;
+  flex-direction: column;
+  padding: 10px 0;
+}
+
+.radio {
+  display: block;
+  margin-bottom: 4px;
+}
+
+</style>

+ 71 - 0
examples/components/product/tag.vue

@@ -0,0 +1,71 @@
+<template>
+  <div>
+    <CheckboxGroup v-model="values" @on-change="onChange">
+      <Checkbox
+        v-for="({ value, label }, index) in option[data.keyword].list"
+        :key="index"
+        :label="value">
+        {{label}}
+      </Checkbox>
+    </CheckboxGroup>
+    <div class="title">主推标签:</div>
+    <Tag
+      class="tag"
+      v-for="(item, index) in values"
+      :key="index"
+      :color="item === main? 'primary' : 'default'"
+      @click.native="onClick(item)"
+    >{{option[data.keyword].list.find(i => i.value === item).label}}</Tag>
+    <error :text="error[data.keyword]" />
+  </div>
+</template>
+<script>
+export default {
+  props: ['data', 'value', 'option', 'error'],
+  data () {
+    return {
+      values: [],
+      main: '',
+    }
+  },
+  methods: {
+    onChange (val) {
+      if (!val.includes(this.main)) this.main = val[0]
+      this.emitData()
+    },
+    onClick (id) {
+      this.main = id
+      this.emitData()
+    },
+    emitData () {
+      const data = this.values.map(i => (
+        {
+          id: i,
+          is_main: Number(this.main === i),
+        }
+      ))
+      this.$hub.$emit(this.data.hub, {
+        type: 'value',
+        payload: Object.assign(this.value, {
+          [this.data.keyword]: data,
+        }),
+      })
+    },
+  },
+  mounted () {
+    this.values = this.value[this.data.keyword].map(i => i.id)
+    if (!this.value[this.data.keyword].length) return
+    this.main = this.value[this.data.keyword].find(i => i.is_main).id
+  },
+}
+</script>
+<style lang="scss" scoped>
+.tag {
+  user-select: none;
+}
+
+.title {
+  font-size: 14px;
+  padding: 6px 0;
+}
+</style>

+ 40 - 0
examples/components/trash/index.vue

@@ -0,0 +1,40 @@
+<template>
+  <CommonStack title="商品回收站" :query="Object.assign({}, query, {  })">
+    <template v-slot:default="{ stackQuery }">
+      <CommonFilter
+        ref="filter"
+        action="getTrashList"
+        :actionOption="query[name]"
+        :fixedValues="{  }"
+        @on-refresh="$emit('on-refresh')" />
+    </template>
+  </CommonStack>
+</template>
+
+<script>
+
+export default {
+  name: 'comment',
+  props: {
+    query: {
+      type: Object,
+      required: false,
+      default () {
+        return {}
+      },
+    },
+    name: {
+      type: String,
+      required: false,
+      default: 'trash',
+    },
+  },
+  mounted () {
+  },
+  components: {
+
+  },
+  methods: {
+  },
+}
+</script>

+ 40 - 0
examples/index.html

@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8">
+    <meta
+      name="viewport"
+      content="width=device-width, initial-scale=1, shrink-to-fit=no"
+    />
+    <title>商品系统</title>
+    <style>
+      body {
+        margin: 0;
+      }
+      #root {
+        display: flex;
+        flex-direction: column;
+        justify-content: center;
+        align-items: center;
+        width: 100vw;
+        height: 100vh;
+        background: #fff;
+        color: #444;
+        font-size: 16px;
+      }
+
+      #root img {
+        display: block;
+        width: 200px;
+        height: 200px;
+        margin-bottom: 20px;
+      }
+    </style>
+  </head>
+  <body>
+    <div id="root">
+      <img src="http://oss.caihongxingqiu.net/management/rainbowstart_logo.png" alt="Logo">
+      <span>系统正在加载中,请耐心等待</span>
+    </div>
+  </body>
+</html>

+ 29 - 0
examples/index.js

@@ -0,0 +1,29 @@
+
+import Vue from 'vue'
+import iView from 'iview'
+import 'iview/dist/styles/iview.css'
+
+import VueReverse from '../src'
+
+// import VueReverse from '../dist/reverse'
+// import '../dist/reverse.css'
+
+import allActions from './actions'
+
+import store from './store'
+import router from './router'
+import './plugins'
+
+import '@/assets/styles/main.scss'
+import Viewer from 'v-viewer'
+import App from './App'
+
+Vue.use(iView)
+Vue.use(Viewer)
+Vue.use(VueReverse, { allActions })
+
+new Vue({
+  router,
+  store,
+  render: h => h(App),
+}).$mount('#root')

+ 20 - 0
examples/plugins/format.js

@@ -0,0 +1,20 @@
+
+export default {
+  install (vue) {
+    const format = {
+      price (price, currency = 'rmb') {
+        const currencyList = {
+          rmb: {
+            icon: '¥',
+            unit: '元',
+          },
+        }
+        return currencyList[currency].icon +
+          Number(price).toFixed(2) +
+          currencyList[currency].unit
+      },
+    }
+    vue.prototype.$format = format
+    vue.format = format
+  },
+}

+ 58 - 0
examples/plugins/http.js

@@ -0,0 +1,58 @@
+
+import axios from 'axios'
+
+export default {
+  install (Vue) {
+    const http = axios.create({
+      baseURL: process.env.API_URL.default,
+      timeout: 60000,
+    })
+
+    http.interceptors.request.use(
+      config => {
+        const [ url, version ] = config.url.split(':').reverse()
+        if (version) {
+          config.baseURL = process.env.API_URL[version]
+          config.url = url
+        }
+
+        const token = window.localStorage.getItem('token')
+        if (token) config.headers['Authorization'] = token
+        return config
+      },
+      error => {
+        return Promise.reject(error)
+      }
+    )
+
+    http.interceptors.response.use(
+      response => {
+        const { status, data } = response
+        const errorTypes = {
+          401: 'GLOBAL_AUTH_ERROR',
+          500: 'GLOBAL_ERROR',
+        }
+
+        if (data.code in errorTypes) {
+          const errorData = { status, message: data.message }
+          Vue.hub.$emit(errorTypes[data.code], errorData)
+          return Promise.reject(errorData)
+        }
+        return data
+      },
+      error => {
+        // console.log(JSON.parse(JSON.stringify(error)))
+        const errorData = { message: '网络超时,请重试' }
+        if (error.response) {
+          errorData.status = error.response.status
+          errorData.message = error.response.data.message
+        }
+        Vue.hub.$emit('GLOBAL_ERROR', errorData)
+        return Promise.reject(errorData)
+      }
+    )
+
+    Vue.prototype.$http = http
+    Vue.http = http
+  },
+}

+ 10 - 0
examples/plugins/hub.js

@@ -0,0 +1,10 @@
+
+import Vue from 'vue'
+
+export default {
+  install (vue) {
+    const hub = new Vue()
+    vue.prototype.$hub = hub
+    vue.hub = hub
+  },
+}

+ 12 - 0
examples/plugins/index.js

@@ -0,0 +1,12 @@
+
+import Vue from 'vue'
+
+import http from './http'
+import hub from './hub'
+import format from './format'
+import stack from './stack'
+
+Vue.use(http)
+Vue.use(hub)
+Vue.use(format)
+Vue.use(stack)

+ 62 - 0
examples/plugins/stack.js

@@ -0,0 +1,62 @@
+
+import Vue from 'vue'
+import router from '../router'
+
+export default {
+  install (vue) {
+    const stack = {
+      list: {},
+      query: {},
+    }
+    const actions = {}
+    const method = {
+      create (name, title, query = {}) {
+        stack.list[name] = [{ name, title }]
+        stack.query[name] = { [name]: query }
+        this.emit(name)
+      },
+      push (name, title = '未命名', query = {}, routeName = router.history.current.name) {
+        const currentStackList = stack.list[routeName]
+        const currentStackQuery = stack.query[routeName]
+        if (!currentStackList) return new Error(`没有找到${routeName}的栈`)
+        currentStackList.push({ name, title })
+        currentStackQuery[name] = query
+        this.emit(routeName)
+      },
+      replace (name, title, query = {}, routeName = router.history.current.name) {
+        this.go(-1, routeName, false)
+        this.push(name, title, query)
+      },
+      go (backward = -1, routeName = router.history.current.name, emit = true) {
+        const currentStackList = stack.list[routeName]
+        if (!currentStackList) return new Error(`没有找到${routeName}的栈`)
+        if (currentStackList.length === 1) return
+        currentStackList.length += backward
+        if (emit) this.emit(routeName)
+      },
+      emit (name) {
+        Vue.hub.$emit('STACK_CHANGE', {
+          list: stack.list[name],
+          query: stack.query[name],
+        })
+        // console.group('Stack Change:')
+        // console.log(stack)
+        // console.groupEnd('Stack Change:')
+      },
+      addAction (name, action, payload = {}, option = {}) {
+        if (!name || !action) return new Error('栈名和action是必须参数')
+        actions[name] = { action, payload, option }
+        Vue.hub.$emit('STACK_ACTION', actions)
+        // console.group('Stack Action:')
+        // console.log(actions)
+        // console.groupEnd('Stack Action:')
+      },
+      getStack () {
+        return stack
+      },
+    }
+
+    vue.prototype.$stack = method
+    vue.stack = method
+  },
+}

+ 23 - 0
examples/router/index.js

@@ -0,0 +1,23 @@
+
+import Vue from 'vue'
+import iView from 'iview'
+import VueRouter from 'vue-router'
+import routes from './routes'
+
+Vue.use(VueRouter)
+
+const router = new VueRouter({
+  mode: 'history',
+  routes,
+})
+
+router.beforeEach((to, from, next) => {
+  iView.LoadingBar.start()
+  next()
+})
+
+router.afterEach(() => {
+  iView.LoadingBar.finish()
+})
+
+export default router

+ 47 - 0
examples/router/routes.js

@@ -0,0 +1,47 @@
+
+const routes = [
+  {
+    name: 'sign',
+    path: '/sign',
+    component: () => import(/* webpackChunkName: 'sign' */ '@/components/Sign'),
+  },
+  {
+    name: 'root',
+    path: '/',
+    component: () => import(/* webpackChunkName: 'layout' */ '@/components/Layout'),
+    children: [
+      {
+        name: 'product',
+        path: '/product/*',
+        component: () => import(/* webpackChunkName: 'product' */ '@/components/product'),
+        props: route => ({ query: route.query }),
+      },
+      {
+        name: 'category',
+        path: '/category/*',
+        component: () => import(/* webpackChunkName: 'category' */ '@/components/category'),
+        props: route => ({ query: route.query }),
+      },
+      {
+        name: 'attribute',
+        path: '/attribute/*',
+        component: () => import(/* webpackChunkName: 'attribute' */ '@/components/attribute'),
+        props: route => ({ query: route.query }),
+      },
+      {
+        name: 'comment',
+        path: '/comment/*',
+        component: () => import(/* webpackChunkName: 'comment' */ '@/components/comment'),
+        props: route => ({ query: route.query }),
+      },
+      {
+        name: 'trash',
+        path: '/trash/*',
+        component: () => import(/* webpackChunkName: 'trash' */ '@/components/trash'),
+        props: route => ({ query: route.query }),
+      },
+    ],
+  },
+]
+
+export default routes

+ 2 - 0
examples/store/actions.js

@@ -0,0 +1,2 @@
+
+export default {}

+ 2 - 0
examples/store/getters.js

@@ -0,0 +1,2 @@
+
+export default {}

+ 22 - 0
examples/store/index.js

@@ -0,0 +1,22 @@
+
+import Vue from 'vue'
+import Vuex from 'vuex'
+
+import state from './state'
+import getters from './getters'
+import mutations from './mutations'
+import actions from './actions'
+import modules from './modules'
+
+Vue.use(Vuex)
+
+const debug = process.env.NODE_ENV === 'development'
+
+export default new Vuex.Store({
+  strict: debug,
+  state,
+  getters,
+  mutations,
+  actions,
+  modules,
+})

+ 84 - 0
examples/store/modules/filter.js

@@ -0,0 +1,84 @@
+
+import Vue from 'vue'
+
+export default {
+  namespaced: true,
+  state: {
+    filters: {
+      id: {
+        keyword: 'id',
+        type: 'input',
+        label: 'ID',
+      },
+      per_page: {
+        keyword: 'per_page',
+        type: 'input',
+        label: '每页数量',
+      },
+      category_name: {
+        keyword: 'name',
+        type: 'input',
+        label: '分类名称',
+      },
+      parent_id: {
+        keyword: 'parent_id',
+        type: 'input',
+        label: 'oops!',
+      },
+    },
+    option: {
+      list: {},
+      expired: true,
+    },
+    columns: {
+      id: {
+        title: 'ID',
+        key: 'id',
+        align: 'center',
+        width: 80,
+      },
+      ico: {
+        component: 'Ico',
+      },
+      desc: {
+        title: '描述',
+        key: 'desc',
+        align: 'center',
+      },
+      category_name: {
+        title: '分类名称',
+        key: 'name',
+        align: 'center',
+        width: 80,
+      },
+      category_level: {
+        title: '级别',
+        key: 'level',
+        align: 'center',
+        width: 80,
+      },
+      category_switch: {
+        component: 'categorySwitch',
+      },
+      category_action: {
+        component: 'categoryAction',
+      },
+    },
+  },
+  mutations: {
+    CHANGE_OPTION (state, option) {
+      state.option = option
+    },
+  },
+  actions: {
+    async getOption ({ commit, state }) {
+      if (!state.option.expired) return
+      try {
+        const { data: list, expired } = await Vue.http.get('/config/test')
+        commit('CHANGE_OPTION', { list, expired })
+      } catch (e) {
+        console.log(e)
+      }
+    },
+  },
+}

+ 8 - 0
examples/store/modules/index.js

@@ -0,0 +1,8 @@
+
+import user from './user'
+import filter from './filter'
+
+export default {
+  user,
+  filter,
+}

+ 63 - 0
examples/store/modules/user.js

@@ -0,0 +1,63 @@
+
+import Vue from 'vue'
+
+export default {
+  namespaced: true,
+  state: {
+    role: 'shop',
+    info: {},
+    menus: [],
+  },
+  getters: {},
+  mutations: {
+    UPDATE_USER (state, { role, data }) {
+      const { token, token_ttl: ttl, menu, ...info } = data
+      window.localStorage.setItem('token', token)
+      const timestamp = ttl > 1e+9 ? ttl : new Date().getTime() + ttl * 60 * 1000
+      window.localStorage.setItem('token_ttl', timestamp)
+      state.role = role
+      state.info = info
+      const defaultMenus = [
+        {
+          name: 'product',
+          title: '商品管理',
+        },
+        {
+          name: 'category',
+          title: '分类管理',
+        },
+        {
+          name: 'attribute',
+          title: '属性管理',
+        },
+        {
+          name: 'comment',
+          title: '评论管理',
+        },
+        {
+          name: 'trash',
+          title: '回收站',
+        },
+      ]
+      state.menus = window.parent !== window ? menu : defaultMenus
+    },
+  },
+  actions: {
+    async login ({ commit }, payload) {
+      try {
+        commit('UPDATE_USER', await Vue.http.post('/admin/login', payload))
+        return Promise.resolve()
+      } catch (e) {
+        return Promise.reject(e)
+      }
+    },
+    async refresh ({ commit }) {
+      try {
+        commit('UPDATE_USER', await Vue.http.post('/admin/refresh'))
+      } catch {
+        window.localStorage.removeItem('token')
+        window.localStorage.removeItem('token_ttl')
+      }
+    },
+  },
+}

+ 2 - 0
examples/store/mutations.js

@@ -0,0 +1,2 @@
+
+export default {}

+ 4 - 0
examples/store/state.js

@@ -0,0 +1,4 @@
+
+export default {
+  isMinion: window.parent !== window,
+}

Datei-Diff unterdrückt, da er zu groß ist
+ 9138 - 0
package-lock.json


+ 63 - 0
package.json

@@ -0,0 +1,63 @@
+{
+  "name": "vue-reverse",
+  "version": "1.0.37",
+  "description": "Build component with data for Vue.",
+  "private": true,
+  "scripts": {
+    "start": "node scripts/server.js",
+    "build": "node scripts/build.js"
+  },
+  "dependencies": {
+    "@babel/core": "^7.2.2",
+    "@babel/plugin-syntax-dynamic-import": "^7.2.0",
+    "@babel/plugin-transform-runtime": "^7.2.0",
+    "@babel/preset-env": "^7.3.1",
+    "@babel/runtime": "^7.3.1",
+    "argv": "0.0.2",
+    "axios": "^0.19.0",
+    "babel-eslint": "^10.0.1",
+    "babel-loader": "^8.0.5",
+    "case-sensitive-paths-webpack-plugin": "^2.2.0",
+    "chalk": "^2.4.2",
+    "connect-history-api-fallback": "^1.6.0",
+    "copy-webpack-plugin": "^5.0.3",
+    "css-loader": "^2.1.0",
+    "eslint": "^5.13.0",
+    "eslint-config-standard": "^12.0.0",
+    "eslint-friendly-formatter": "^4.0.1",
+    "eslint-loader": "^2.1.2",
+    "eslint-plugin-html": "^5.0.3",
+    "eslint-plugin-import": "^2.16.0",
+    "eslint-plugin-node": "^8.0.1",
+    "eslint-plugin-promise": "^4.0.1",
+    "eslint-plugin-standard": "^4.0.0",
+    "express": "^4.16.4",
+    "file-loader": "^3.0.1",
+    "friendly-errors-webpack-plugin": "^1.7.0",
+    "fs-extra": "^7.0.1",
+    "html-webpack-plugin": "^3.2.0",
+    "iview": "^3.2.2",
+    "lodash": "^4.17.11",
+    "mini-css-extract-plugin": "^0.5.0",
+    "node-sass": "^4.12.0",
+    "opn": "^5.4.0",
+    "optimize-css-assets-webpack-plugin": "^5.0.1",
+    "ora": "^3.1.0",
+    "ramda": "^0.26.1",
+    "sass-loader": "^7.1.0",
+    "style-loader": "^0.23.1",
+    "terser-webpack-plugin": "^1.2.2",
+    "v-viewer": "^0.3.2",
+    "vue": "^2.6.6",
+    "vue-eslint-parser": "^6.0.2",
+    "vue-loader": "^15.6.2",
+    "vue-router": "^3.0.2",
+    "vue-template-compiler": "^2.6.6",
+    "vuex": "^3.1.0",
+    "webpack": "^4.29.3",
+    "webpack-bundle-analyzer": "^3.0.4",
+    "webpack-cli": "^3.2.3",
+    "webpack-dev-middleware": "^3.5.2",
+    "webpack-hot-middleware": "^2.24.3"
+  }
+}

+ 2 - 0
scripts/build.js

@@ -0,0 +1,2 @@
+
+require('./pack')('production')

+ 0 - 0
scripts/pack.js


Einige Dateien werden nicht angezeigt, da zu viele Dateien in diesem Diff geändert wurden.