Эх сурвалжийг харах

feat: 新增创建订单相关组件

cuiyalong 3 сар өмнө
parent
commit
e9e1cda757
47 өөрчлөгдсөн 6142 нэмэгдсэн , 0 устгасан
  1. 37 0
      src/api/interceptors.js
  2. 47 0
      src/api/modules/index.js
  3. 7 0
      src/api/request.js
  4. 5 0
      src/api/service.js
  5. 107 0
      src/assets/css/_variables.scss
  6. 159 0
      src/components/common/form-schema-renderer.vue
  7. 2 0
      src/store.js
  8. 252 0
      src/store/order.js
  9. 1 0
      src/utils/data/index.js
  10. 46 0
      src/utils/date/index.js
  11. 5 0
      src/utils/event/bus.js
  12. 26 0
      src/utils/mixins/selector-v-model.js
  13. 25 0
      src/utils/str/index.js
  14. 29 0
      src/utils/utils.js
  15. 104 0
      src/views/create-order/components/ProductInfoCard.vue
  16. 156 0
      src/views/create-order/components/ProductInfoCardList.vue
  17. 205 0
      src/views/create-order/components/baseInfoModule.vue
  18. 264 0
      src/views/create-order/components/contractInfoModule.vue
  19. 122 0
      src/views/create-order/components/create.vue
  20. 147 0
      src/views/create-order/components/otherInfoModule.vue
  21. 320 0
      src/views/create-order/components/paymentPlanModule.vue
  22. 331 0
      src/views/create-order/components/performanceBelongsModule.vue
  23. 86 0
      src/views/create-order/components/product-info-submodule/AccountNumbers.vue
  24. 63 0
      src/views/create-order/components/product-info-submodule/CheckboxGroup.vue
  25. 94 0
      src/views/create-order/components/product-info-submodule/ContractAmount.vue
  26. 286 0
      src/views/create-order/components/product-info-submodule/ElOnlineContractForm.vue
  27. 194 0
      src/views/create-order/components/product-info-submodule/ProductTypeSelector.vue
  28. 75 0
      src/views/create-order/components/product-info-submodule/RadioGroup.vue
  29. 193 0
      src/views/create-order/components/product-info-submodule/RelatedOrderTable.vue
  30. 269 0
      src/views/create-order/components/product-info-submodule/RelatedOrders.vue
  31. 314 0
      src/views/create-order/components/product-info-submodule/ServiceList.vue
  32. 29 0
      src/views/create-order/components/product-info-submodule/TextCard.vue
  33. 294 0
      src/views/create-order/components/product-info-submodule/ValidityPeriod.vue
  34. 262 0
      src/views/create-order/components/product-info-submodule/select-tree.vue
  35. 143 0
      src/views/create-order/components/productInfoModule.vue
  36. 276 0
      src/views/create-order/components/schema-form/products/common.js
  37. 63 0
      src/views/create-order/components/schema-form/products/svip.js
  38. 472 0
      src/views/create-order/components/schema-form/schema-form.vue
  39. 76 0
      src/views/create-order/components/schema-form/schema.js
  40. 2 0
      src/views/create-order/data/index.js
  41. 284 0
      src/views/create-order/data/options.js
  42. 7 0
      src/views/create-order/data/var.js
  43. 17 0
      src/views/create-order/hooks/index.js
  44. 28 0
      src/views/create-order/index.vue
  45. 43 0
      src/views/create-order/ui/ModuleCard.vue
  46. 87 0
      src/views/create-order/ui/NumberInput.vue
  47. 88 0
      src/views/create-order/ui/ProductCard.vue

+ 37 - 0
src/api/interceptors.js

@@ -0,0 +1,37 @@
+import service from "./service";
+
+function getToken() {
+  const token = localStorage.getItem('admin_token')
+  return token
+}
+
+service.interceptors.request.use(
+  (config) => {
+    const token = getToken()
+    if (token) {
+      config.headers.token = token
+    }
+    return config
+  },
+  (error) => {
+    return Promise.reject(error)
+  }
+)
+
+service.interceptors.response.use(
+  (response) => {
+    const res = response.data
+    if (response.status === 200) {
+      // do something
+    }
+    else {
+      return Promise.reject(new Error('Error'))
+    }
+    return res
+  },
+  (error) => {
+    return Promise.reject(error)
+  }
+)
+
+export default service

+ 47 - 0
src/api/modules/index.js

@@ -0,0 +1,47 @@
+import request from '@/api/request'
+// import qs from 'qs'
+
+export function getSalesList() {
+  return request({
+    url: '/jyOrderManager/common/getSalesList',
+    method: 'get',
+  })
+}
+
+export function ajaxGetSelectOptions() {
+  return request({
+    url: '/jyOrderManager/common/getSelectItem',
+    method: 'get',
+  })
+}
+
+export function ajaxGetUserService(data) {
+  return request({
+    url: '/jyOrderManager/common/userService',
+    method: 'post',
+    data,
+  })
+}
+
+export function ajaxGetProductionList(query) {
+  return request({
+    url: '/jyOrderManager/product/list',
+    method: 'get',
+    query,
+  })
+}
+
+
+
+export function getWorkDay(data) {
+  return request({
+    url: '/jyOrderManager/order/getWorkDay',
+    method: 'post',
+    data,
+  })
+}
+
+
+
+
+

+ 7 - 0
src/api/request.js

@@ -0,0 +1,7 @@
+import service from "./service";
+import backService from '../plugins/request-promise'
+import './interceptors'
+
+const inWorkDesktop = true
+
+export default inWorkDesktop ? service : backService

+ 5 - 0
src/api/service.js

@@ -0,0 +1,5 @@
+import axios from 'axios'
+
+export default axios.create({
+  baseURL: ''
+})

+ 107 - 0
src/assets/css/_variables.scss

@@ -0,0 +1,107 @@
+// ---- 运行时可能会改变的内容,变量名使用 _ 分割,如 color, background-color 等
+// ---- 运行时不会改变的内容,变量名使用 - 分割,如 font-size, padding 等
+// Color Palette
+$black: #000;
+$white: #fff;
+
+$gray_0: #fafafa;
+$gray_1: #f5f6f7;
+$gray_2: #f2f3f5;
+$gray_3: #edeff2;
+$gray_4: #dcdee0;
+$gray_5: #c0c4cc;
+$gray_6: #9b9ca3;
+$gray-64: #646566;
+$gray_7: #5f5e64;
+$gray_8: #33323a; // VIP背景灰色
+$gray_89: #1b1a2a; // VIP背景灰色
+$gray_9: #171826;
+
+$main: #2abed1;
+$green: #00d086;
+$blue: #3399ff;
+$purple: #8e6df2;
+$orange: #ff9f40;
+$red: #fb483d;
+$gold: #f1d090; // VIP 金色
+
+$blue_light: #00d1ff;
+$orange_light: #ffcc66;
+$red_light: #f74e29;
+$gold_light: #fae7ca; // VIP 金色light
+
+$main_deep: #1db5e5;
+$purple_deep: #6d00db;
+$red_deep: #ef3024;
+$gold_deep: #d69e55; // VIP 金色deep
+$orange_red: #fa6f33;
+
+$color_main: $main;
+$color_red: $red;
+$gray_vip: $gray_9;
+
+// Background
+// 透明背景色使用时,需要配合白色背景使用
+$color_main_background: rgb($color_main, 0.1);
+$color_red_background: rgba($color_red, 0.1);
+$color_gold_deep_background: rgba($gold_deep, 0.1);
+
+// Border
+$border_color_1: rgba($white, 0.5);
+$border_color_2: rgba($white, 0.9);
+$border_color_3: rgba($black, 0.04);
+$border_color_4: rgba($black, 0.1);
+$border_color_5: rgba($black, 0.15);
+$border_color_6: rgba($black, 0.3);
+
+$gradient_main: linear-gradient(to bottom, $main, $main_deep);
+$gradient_green: linear-gradient(to bottom, #0bd991, #00b031);
+$gradient_blue: linear-gradient(to bottom, $blue_light, $blue);
+$gradient_purple: linear-gradient(to bottom, $purple, $purple_deep);
+$gradient_orange: linear-gradient(to bottom, $orange_light, $orange);
+$gradient_red: linear-gradient(to bottom, $red, $red_deep);
+// VIP金色渐变
+$gradient_gold: linear-gradient(to right, $gold_light, $gold);
+// VIP 深色渐变
+$gradient_gray: linear-gradient(135deg, #3e3e52 0%, #2f2f3d 100%, #2f2f3d 100%);
+
+$gradient_search_header: linear-gradient(
+  280.62deg,
+  #d7f6fb 1.93%,
+  #e7fcff 49.44%,
+  #e7f2ff 98.41%
+);
+
+// Radius
+$radius_base: 2px;
+$border_radius_1: $radius_base;
+$border_radius_2: $radius_base * 2;
+$border_radius_3: $radius_base * 3;
+$border_radius_4: $radius_base * 4;
+
+// Component Colors
+$text-color: $gray_9;
+$active-color: $gray_2;
+$active-opacity: 0.7;
+$disabled-opacity: 0.5;
+$background-color: $gray_1;
+$background-color-light: $gray_0;
+$text-link-color: #576b95;
+
+// ---- 运行不会改变的内容,变量名使用 - 分割,如 font-size, padding 等
+// Padding
+$padding-base: 4px;
+$padding-xs: $padding-base * 2;
+$padding-sm: $padding-base * 3;
+$padding-md: $padding-base * 4;
+$padding-lg: $padding-base * 6;
+$padding-xl: $padding-base * 8;
+
+// Animation
+$animation-duration-base: 0.3s;
+$animation-duration-fast: 0.2s;
+$animation-timing-function-enter: ease-out;
+$animation-timing-function-leave: ease-in;
+
+// app头部高度
+$app-header-padding-top: 40px;

+ 159 - 0
src/components/common/form-schema-renderer.vue

@@ -0,0 +1,159 @@
+<template>
+  <!-- 产品信息 -->
+  <el-form ref="form" :model="state" :rules="rules" label-width="126px" class="order-other-info-container">
+    <el-form-item
+      v-for="(option, index) in schemaList"
+      :key="option.key"
+      :id="option.key"
+      :label="option.label"
+      :prop="option.key"
+      :class="[option.className]"
+      :required="option.required"
+      :show-message="option.showMessage"
+      size="medium"
+    >
+      <Component
+        :ref="prefix.component + index"
+        :id="prefix.component + index"
+        v-if="option.component"
+        :is="option.component"
+        v-bind="{
+          ...getObjectValue(option, 'props'),
+          value: state[option.key],
+        }"
+        v-on="{
+          input: ($event) => onComponentInput($event, option, index),
+          change: ($event) => onComponentChange($event, option, index),
+          ...getObjectValue(option, 'hooks'),
+        }"
+      ></Component>
+      <slot
+        :ref="prefix.slot + index"
+        :id="prefix.slot + index"
+        v-else-if="option.slot"
+        :name="option.slot"
+        :option="option"
+      ></slot>
+    </el-form-item>
+  </el-form>
+</template>
+
+<script>
+import { isEqual } from 'lodash'
+
+export default {
+  name: 'FormSchemaRenderer',
+  props: {
+    state: {
+      type: Object,
+      default: () => {}
+    },
+    schema: {
+      type: Array,
+      default() {
+        return [
+          // {
+          //   label: '约定支付方式',
+          //   key: 'payWay',
+          //   className: 'pay-way-form-item',
+          //   component: {}, // component和slot二选一,component优先级高
+          //   slot: 'slotName',
+          //   required: false,
+          //   props: {},
+          //   hooks: {},
+          // }
+        ]
+      }
+    },
+    rules: {
+      type: Object,
+      default() {
+        return {}
+      }
+    }
+  },
+  model: {
+    prop: 'state',
+    model: 'input'
+  },
+  data() {
+    return {
+      prefix: {
+        item: 'form-item-',
+        component: 'form-component-',
+        slot: 'form-slot-'
+      },
+    }
+  },
+  computed: {
+    schemaList() {
+      return this.schema.filter(r => r.show)
+    }
+  },
+  methods: {
+
+    validate() {
+      return new Promise((resolve, reject) => {
+        this.$refs.form.validate((valid) => {
+          if (valid) {
+            resolve()
+          } else {
+            reject()
+          }
+        })
+      })
+    },
+    clearValidate() {
+      this.$refs.form.clearValidate()
+    },
+    getObjectValue(option, key, defaultType = {}) {
+      if (option[key]) {
+        return option[key]
+      } else {
+        return defaultType
+      }
+    },
+    /**
+     * 对比默认值与当前缓存区数据状态是否一致
+     * @param key
+     * @returns {boolean}
+     */
+    diffValue(key) {
+      return !isEqual(this.state[key], this.defaultState[key])
+    },
+    getRef(option, index) {
+      let vm = null
+      if (option.component) {
+        vm = this.getCurrentRef(this.prefix.component + index)
+      } else if (option.content.slot) {
+        vm = this.getCurrentRef(this.prefix.slot + index)
+      }
+      if (Array.isArray(vm)) {
+        vm = vm[0]
+      }
+      return vm
+    },
+    getCurrentRef (id) {
+      const ref = this.$current.page.selectComponent('#' + id)
+      const refVue = this.$refs[id] ? this.$refs[id][0] : {}
+      return ref || refVue
+    },
+    onComponentInput(e, option, index) {
+      // const ref = this.getRef(option, index)
+      this.$emit(
+        'input',
+        Object.assign({}, this.state, {
+          [option.key]: e
+        })
+      )
+    },
+    onComponentChange(e, option, index) {
+      this.onComponentInput(e, option, index)
+    },
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 2 - 0
src/store.js

@@ -2,6 +2,7 @@ import Vue from 'vue'
 import Vuex from 'vuex'
 import admin from './store/admin'
 import combo from './store/combo'
+import order from './store/order'
 import message from './store/message'
 import organ from './store/organ'
 
@@ -14,6 +15,7 @@ export default new Vuex.Store({
     modules: {
         admin,
         combo,
+        order,
         message,
         organ
     },

+ 252 - 0
src/store/order.js

@@ -0,0 +1,252 @@
+import {
+  ajaxGetProductionList,
+  ajaxGetUserService,
+  ajaxGetSelectOptions,
+} from "../api/modules"
+import { findProductInThreeLevel } from '@/views/create-order/hooks'
+import { ActivityProductName } from '@/views/create-order/data'
+import { getRandomString } from '@/utils/utils'
+import { cloneDeep } from "lodash"
+
+class OrderProductCardItem {
+  constructor(title, subtitle2) {
+    this.title = title
+    this.subtitle2 = subtitle2
+    this.id = getRandomString()
+    this.productCardInfo = {
+      info: {},
+      form: {},
+      result: {},
+    }
+  }
+
+  get subtitle() {
+    if (this.subtitle2) {
+      return this.subtitle2
+    } else {
+      const { info } = this.productCardInfo
+      // auto[number]: 是否支持自动开通 1是 2否
+      if (info && info.auto === 2) {
+        return '该产品暂不支持系统自动开通权限,请联系运维开通'
+      }
+    }
+    return ''
+  }
+
+  get activityCode() {
+    return this.productCardInfo.result?.activityCode
+  }
+}
+
+const defaultPageFormValue = {
+  buySubject: 1,
+  companyName: '',
+  username: '',
+  userTel: '',
+  accountTel: '13283800000',
+}
+
+export default {
+  namespaced: true,
+  state: {
+    schemaKey: Date.now(),
+    // 配置信息
+    conf: {
+      channel: [],
+      deptTree: [],
+    },
+    // 产品列表
+    productList: [],
+    // 页面表单数据
+    pageForm: cloneDeep(defaultPageFormValue),
+    orderInfo: {
+      hasSamePower: false,
+      productInfoList: [],
+    },
+  },
+  getters: {
+    userProductInfoList(state) {
+      const { productInfoList } = state.orderInfo
+      return productInfoList.map(card => {
+        return {
+          _origin: card.productCardInfo,
+          ...card.productCardInfo.form,
+          ...card.productCardInfo.result,
+        }
+      })
+    },
+    depTreeList(state) {
+      let depTree  = cloneDeep(state.conf.deptTree)
+      // children下面是部门,salers是部门人员
+
+      // 先递归把空部门过滤掉(部门下没人的)
+      function filterEmptyDept(dep) {
+        if (Array.isArray(dep.children) && dep.children.length > 0) {
+          dep.children = dep.children.filter(r => {
+            const hasSubDep = Array.isArray(r.children) && r.children.length > 0
+            const hasSubPerson = Array.isArray(r.salers) && r.salers.length > 0
+            return hasSubDep || hasSubPerson
+          })
+          dep.children.forEach(filterEmptyDept)
+        }
+      }
+      depTree.forEach(filterEmptyDept)
+
+      // 整理数据结构
+      function sortDep(dep) {
+        const dep2 = {
+          label: dep.label,
+          value: dep.value,
+          children: []
+        }
+        if (Array.isArray(dep.salers) && dep.salers.length > 0) {
+          dep2.children = dep.salers.map(item => {
+            item.label = item.name
+            item.value = item.id
+            item.parentLabel = dep2.label
+            item.parentValue = dep2.value
+            return item
+          })
+        }
+        if (Array.isArray(dep.children) && dep.children.length > 0) {
+          const t = dep.children.map(sortDep)
+          dep2.children = dep2.children.concat(t)
+        }
+        return dep2
+      }
+      return depTree.map(sortDep)
+    },
+    saleChannelOptions(state) {
+      return cloneDeep(state.conf.channel)
+    },
+  },
+  mutations: {
+    refreshSchema(state) {
+      state.schemaKey = Date.now()
+    },
+    setConfInfo(state, { key, value }) {
+      state.conf[key] = value
+    },
+    setProductList (state, payload = []) {
+      state.productList = payload
+    },
+    setOrderProductInfoList(state, payload = []) {
+      state.orderInfo.productInfoList = payload
+    },
+    setPageForm(state, payload = {}) {
+      const { key, data } = payload
+      if (!key) return
+      state.pageForm[key] = data
+    },
+    resetPageForm(state, payload = cloneDeep(defaultPageFormValue)) {
+      state.pageForm = payload
+    },
+    addOrderProductItem(state) {
+      state.orderInfo.productInfoList.push(
+        new OrderProductCardItem('产品')
+      )
+    },
+    removeOrderProductItem(state, index) {
+      if (state.orderInfo.productInfoList.length === 0) return
+      if (state.orderInfo.productInfoList.length === 1) {
+        this.commit('order/setOrderProductInfoList', [])
+        this.commit('order/addOrderProductItem', [])
+        return
+      }
+      state.orderInfo.productInfoList.splice(index, 1)
+    },
+    refreshOrderProductItemCard(state, payload = {}) {
+      const { index, data, key } = payload
+      if (index === undefined || index === null) return
+      if (!data) return
+      const s = state.orderInfo.productInfoList[index]
+      if (s) {
+        s[key] = data
+      }
+    },
+  },
+  actions: {
+    // 获取产品备选项信息
+    async getProductList ({ dispatch }, payload) {
+      const { error_code: code, error_msg: msg, data } = await ajaxGetProductionList(payload)
+      if (code === 0 && data) {
+        dispatch('initProductListData', data)
+      } else {
+        console.log(msg)
+      }
+    },
+    // 初始化产品备选项信息
+    initProductListData({ commit }, payload = {}) {
+      const { productList, activityList } = payload
+      let pList = []
+      // 整理productList
+      if (Array.isArray(productList) && productList.length > 0) {
+        pList = productList.map(level1 => {
+          let children = []
+          if (Array.isArray(level1.productClassList) && level1.productClassList.length > 0) {
+            children = level1.productClassList.map(clsItem => {
+              return {
+                label: clsItem.class_name,
+                value: clsItem.code,
+                ...clsItem
+              }
+            })
+          }
+          return {
+            label: level1.name,
+            value: level1.name,
+            children,
+          }
+        })
+      }
+      // 整理活动数据
+      const aList = activityList.map(act => {
+        act.label = act.name
+        act.value = act.code
+        act.activityMark = 1
+        // 找到code所在商品,在使用时,要固定产品类型为当前product_code,且不能更改
+        if (Array.isArray(act.products)) {
+          act.product_list = act.products.map(product => {
+            const productCode = product.product_code
+            // 从productList中找到对应产品
+            const productItem = findProductInThreeLevel(pList, pl => {
+              return pl.code === productCode
+            })
+            return productItem
+          })
+
+        }
+        return act
+      })
+
+      pList.push({
+        label: ActivityProductName,
+        value: ActivityProductName,
+        children: aList
+      })
+
+      commit('setProductList', pList)
+    },
+    // 检查该联系人手机号或公司名称是否存在目同产品类型且权益已开通或已过期的订单
+    async getUserService(_, payload = {}) {
+      const { error_code: code, error_msg: msg, data } = await ajaxGetUserService(payload)
+      if (code === 0 && data) {
+        return data
+      } else {
+        console.log(msg)
+      }
+    },
+    // 获取备选项
+    async getSelectOptions({ commit }) {
+      const { error_code: code, error_msg: msg, data } = await ajaxGetSelectOptions()
+      if (code === 0 && data) {
+        for (const key in data) {
+          commit('setConfInfo', { key, value: data[key] })
+        }
+        return data
+      } else {
+        console.log(msg)
+      }
+    },
+  },
+}

+ 1 - 0
src/utils/data/index.js

@@ -0,0 +1 @@
+export const phoneRegExp = /^[1][3-9][0-9]{9}$/

+ 46 - 0
src/utils/date/index.js

@@ -0,0 +1,46 @@
+/**
+ * 时间单位求和函数(支持日/月/季/年)
+ * @param {Object} input1 第一个时间输入 { value: number, unit: '日'|'月'|'季'|'年' }
+ * @param {Object} input2 第二个时间输入 { value: number, unit: '日'|'月'|'季'|'年' }
+ * @returns {string} 格式化后的时间字符串(x年x个月x日,含零值)
+ */
+export function sumTimeUnits(input1, input2) {
+  // 单位换算:统一转换为天数(简化为30天/月,3月/季,12月/年)
+  const unitMap = {
+    日: 1,
+    月: 30,
+    季: 90, // 3月×30天
+    年: 365 // 12月×30天
+  };
+
+  // 输入验证
+  if (!unitMap[input1.unit] || !unitMap[input2.unit]) {
+    throw new Error('无效的时间单位,必须为日/月/季/年');
+  }
+  if (input1.value < 0 || input2.value < 0) {
+    throw new Error('时间值不能为负数');
+  }
+
+  // 计算总天数
+  const totalDays = 
+    input1.value * unitMap[input1.unit] +
+    input2.value * unitMap[input2.unit];
+
+  // 分解为年、月、日(保留零值)
+  const years = Math.floor(totalDays / 360);
+  const daysAfterYear = totalDays % 360;
+  const months = Math.floor(daysAfterYear / 30);
+  const days = daysAfterYear % 30;
+
+  // 格式化输出(强制保留年/月/日,含零值)
+  return `${years}年${months}个月${days}日`;
+}
+
+// 示例用法
+// const inputA = { value: 1, unit: '年' }; // 360天
+// const inputB = { value: 2, unit: '季' }; // 180天
+// console.log(sumTimeUnits(inputA, inputB)); // 1年6个月0日(540天)
+
+// const inputC = { value: 5, unit: '月' }; // 150天
+// const inputD = { value: 10, unit: '日' }; // 10天
+// console.log(sumTimeUnits(inputC, inputD)); // 0年5个月10日(160天)

+ 5 - 0
src/utils/event/bus.js

@@ -0,0 +1,5 @@
+import Vue from 'vue'
+
+const $bus = new Vue()
+
+export default $bus

+ 26 - 0
src/utils/mixins/selector-v-model.js

@@ -0,0 +1,26 @@
+export const selectorVModelMixin = {
+  props: {
+    state: {
+      type: [Object, Array]
+    }
+  },
+  model: {
+    prop: 'state',
+    event: 'change'
+  },
+  watch: {
+    state: {
+      immediate: true,
+      handler(n) {
+        this.setState(n)
+      }
+    }
+  },
+  methods: {
+    onChange() {
+      const payload = this.getState()
+      console.log(payload)
+      this.$emit('change', payload)
+    }
+  }
+}

+ 25 - 0
src/utils/str/index.js

@@ -0,0 +1,25 @@
+// 金额添加,的格式化
+export function locateMoney(val) {
+  val = val + ''
+  return val.toLocaleString('zh', { style: 'currency', currency: 'CNY' })
+}
+// 用来替代toLocaleString('zh', { style: 'currency', currency: 'CNY' })
+export function currencyFormat(val) {
+  // 检查输入是否为数字
+  if (typeof val!== 'number') {
+    val = val - 0
+    if (isNaN(val)) {
+      throw new Error('输入必须合法的字符串');
+    }
+  }
+
+  // 处理小数位数,默认保留两位
+  let formattedNumber = parseFloat(val).toFixed(2);
+
+  // 处理千分位分隔符
+  const parts = formattedNumber.toString().split('.');
+  parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
+
+  // 拼接人民币符号和格式化后的数字
+  return '¥' + parts.join('.');
+}

+ 29 - 0
src/utils/utils.js

@@ -0,0 +1,29 @@
+import { phoneRegExp } from './data/index'
+export function checkPhonePass(phone) {
+  const reg = phoneRegExp
+  return reg.test(phone)
+}
+
+export function toNumber(t) {
+  const cache = Number(t)
+  return isNaN(cache) ? t : cache
+}
+
+export const getRandomString = (len) => {
+  let randomString = ''
+  if (len) {
+    /** 默认去掉了容易混淆的字符oOLl,9gq,Vv,Uu,I1 **/
+    const $chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678'
+    const maxPos = $chars.length
+    for (let i = 0; i < len; i++) {
+      randomString += $chars.charAt(Math.floor(Math.random() * maxPos))
+    }
+  } else {
+    // Math.random()  生成随机数字, eg: 0.123456
+    // .toString(36)  转化成36进制 : "0.4fzyo82mvyr"
+    // .substring(2)  去掉前面两位 : "yo82mvyr"
+    // .slice(-8)  截取最后八位 : "yo82mvyr"
+    randomString = Math.random().toString(36).substring(2)
+  }
+  return randomString
+}

+ 104 - 0
src/views/create-order/components/ProductInfoCard.vue

@@ -0,0 +1,104 @@
+<template>
+  <div class="product-info-card">
+    <el-form label-width="126px">
+      <el-row :gutter="2">
+        <el-col :span="24">
+          <ProductTypeSelector
+            v-model="productTypeResult"
+            :productTypeOptions="productTypeOptions"
+            @change="onProductionChange"
+          />
+        </el-col>
+      </el-row>
+      <el-row :gutter="2" v-if="productTypeResult.productCode">
+        <el-col :span="24">
+          <ProductSchemaForm
+            :productType="productTypeResult.productCode"
+            :productInfo="productTypeInfo"
+            v-model="form"
+            :index="index"
+            @input="onSchemaFormChange"
+          />
+        </el-col>
+      </el-row>
+    </el-form>
+  </div>
+</template>
+
+<script>
+import ProductTypeSelector from './product-info-submodule/ProductTypeSelector.vue'
+import ProductSchemaForm from './schema-form/schema-form.vue'
+import { mapState } from 'vuex'
+
+export default {
+  name: 'ProductInfoCard',
+  components: {
+    ProductTypeSelector,
+    ProductSchemaForm,
+  },
+  props: {
+    type: {
+      type: String,
+      default: ''
+    },
+    index: {
+      type: [String, Number],
+      default: '0'
+    },
+
+  },
+  data() {
+    return {
+      productTypeResult: {
+        productCode: '',
+        activityCode: '',
+      },
+      productTypeInfo: {},
+      form: {}
+    }
+  },
+  computed: {
+    ...mapState({
+      productTypeOptions: state => state.order.productList,
+    }),
+  },
+  methods: {
+    onProductionChange(e) {
+      this.productTypeInfo = e.info
+      this.onChange()
+    },
+    onSchemaFormChange() {
+      this.onChange()
+    },
+    getState() {
+      const p = {
+        result: this.productTypeResult,
+        info: this.productTypeInfo,
+        form: this.form,
+      }
+      return JSON.parse(JSON.stringify(p))
+    },
+    setState(t) {
+      if (!t) return
+      const { result: e, info, form } = t
+      if (e) {
+        this.productTypeResult = e
+      }
+      if (info) {
+        this.productTypeInfo = info
+      }
+      if (form) {
+        this.form = form
+      }
+    },
+    onChange() {
+      const payload = this.getState()
+      this.$emit('change', payload)
+    },
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 156 - 0
src/views/create-order/components/ProductInfoCardList.vue

@@ -0,0 +1,156 @@
+<template>
+  <div class="product-info-card-list">
+    <ProductCard
+      v-for="(product, index) in productCardList"
+      :key="product.id"
+      :title="product.title + (index+1)"
+      :subtitle="product.subtitle"
+    >
+      <template #actions>
+        <el-button
+          v-if="productCardList.length > 1"
+          type="text"
+          icon="el-icon-delete"
+          @click="removeProduct(index)"
+        >删除</el-button>
+      </template>
+      <template #default>
+        <ProductInfoCard
+          ref="productInfoCard"
+          :index="index"
+          @change="onProductInfoCardChange($event, index)"
+          @power="onProductInfoCardPowerCheck($event, index)"
+        />
+      </template>
+    </ProductCard>
+    <!-- <div class="activity-product-container product-module">
+      <div class="highlight-text">活动商品:买2年大会员送:1年大会员+人脉管理+阳光直采+1000条数据流量包高级字段包+腾讯视频年卡</div>
+      <div class="desc-detail-info-list">
+        <div class="desc-detail-info-item">
+          <div class="desc-label">合同金额合计:</div>
+          <div class="desc-value">¥1000</div>
+        </div>
+        <div class="desc-detail-info-item">
+          <div class="desc-label">标准售价合计:</div>
+          <div class="desc-value">¥1000</div>
+        </div>
+        <div class="desc-detail-info-item">
+          <div class="desc-label">折扣率:</div>
+          <div class="desc-value">¥1000</div>
+        </div>
+      </div>
+    </div> -->
+    <div class="product-module product-actions">
+      <el-button size="medium" class="highlight-button" plain icon="el-icon-plus" @click="addProduct">添加产品</el-button>
+    </div>
+  </div>
+</template>
+
+<script>
+import ProductInfoCard from './ProductInfoCard.vue'
+import ProductCard from '../ui/ProductCard.vue'
+import { mapState } from 'vuex'
+
+export default {
+  name: 'ProductInfoCardList',
+  components: {
+    ProductCard,
+    ProductInfoCard,
+  },
+  props: {
+    type: {
+      type: String,
+      default: ''
+    },
+  },
+  computed: {
+    ...mapState({
+      productCardList: state => state.order.orderInfo.productInfoList
+    }),
+  },
+  created() {
+    this.getProductList()
+    this.addProduct()
+  },
+  methods: {
+    getProductList() {
+      this.$store.dispatch('order/getProductList', { subject: 0 })
+    },
+    addProduct() {
+      this.$store.commit('order/addOrderProductItem')
+    },
+    removeProduct(index) {
+      const activityProduct = this.isActivityProduct(index)
+      if (activityProduct) {
+        this.$confirm('该商品是活动商品,删除该商品时相关活动商品会同时被删除?', '提示', {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+        }).then(() => {
+          this.$store.commit('order/removeOrderProductItem', index)
+          this.$message({
+            type: 'success',
+            message: '删除成功!'
+          });
+        }).catch(() => {
+          console.log('取消删除!')
+        })
+      } else {
+        this.$store.commit('order/removeOrderProductItem', index)
+      }
+    },
+    isActivityProduct(index) {
+      const t = this.productCardList[index]
+      return t.activityCode
+    },
+    onProductInfoCardChange(e, index) {
+      const p = {
+        key: 'productCardInfo',
+        data: e,
+        index,
+      }
+      this.$store.commit('order/refreshOrderProductItemCard', p)
+    },
+    onProductInfoCardPowerCheck(e, index) {
+      const p = {
+        key: 'userService',
+        data: e,
+        index,
+      }
+      this.$store.commit('order/refreshOrderProductItemCard', p)
+    },
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.product-module {
+  margin-bottom: 16px;
+}
+.highlight-button {
+  background: $white;
+  border-color: $main;
+  color: $main;
+}
+.product-info-card-list {
+  .activity-product-container {
+    padding: 14px 20px;
+    border-radius: 8px;
+    border: 1px solid $main;
+    background-color: rgba($main, 0.08);
+  }
+  .desc-detail-info-list,
+  .desc-detail-info-item {
+    display: flex;
+    align-items: center;
+  }
+  .desc-detail-info-list {
+    margin-top: 8px;
+  }
+  .desc-detail-info-item {
+    margin-right: 24px;
+    .desc-label {
+      color: #686868;
+    }
+  }
+}
+</style>

+ 205 - 0
src/views/create-order/components/baseInfoModule.vue

@@ -0,0 +1,205 @@
+<template>
+  <!-- 基本信息 -->
+  <el-form ref="form" :model="form" :rules="rules" label-width="126px" class="order-base-info-container">
+    <el-form-item label="购买主体" prop="buySubject" :required="required.buySubject">
+      <RadioGroup
+        :value="form.buySubject"
+        @input="onElInput($event, 'buySubject')"
+        :options="conf.buySubjectOptions"
+      />
+    </el-form-item>
+    <el-form-item label="公司名称" prop="companyName" :required="companyRequired">
+      <el-input
+        :value="form.companyName"
+        @input="onElInput($event, 'companyName')"
+        placeholder="请输入完整的公司名称"
+        size="medium"
+        maxlength="50"
+      ></el-input>
+    </el-form-item>
+    <el-row :gutter="2">
+      <el-col :span="12">
+        <el-form-item label="联系人姓名" prop="username">
+          <el-input
+            :value="form.username"
+            @input="onElInput($event, 'username')"
+            placeholder="请输入联系人姓名"
+            size="medium"
+            maxlength="50"
+          ></el-input>
+        </el-form-item>
+      </el-col>
+      <el-col :span="12">
+        <el-form-item label="联系人手机号" prop="userTel" :required="required.userTel">
+          <el-input
+            :value="form.userTel"
+            @input="onElInput($event, 'userTel')"
+            placeholder="请输入联系人手机号"
+            size="medium"
+            maxlength="11"
+          ></el-input>
+        </el-form-item>
+      </el-col>
+    </el-row>
+    <el-row :gutter="2">
+      <el-col :span="12">
+        <el-form-item label="开通权益手机号" prop="accountTel" :required="required.accountTel">
+        <el-input
+          :value="form.accountTel"
+          @input="onElInput($event, 'accountTel')"
+          placeholder="请输入联系人手机号,非权益开通手机号"
+          size="medium"
+          maxlength="11"
+        ></el-input>
+      </el-form-item>
+      </el-col>
+    </el-row>
+  </el-form>
+</template>
+
+<script>
+import RadioGroup from '@/views/create-order/components/product-info-submodule/RadioGroup'
+import { buySubjectOptions } from '@/views/create-order/data/index.js'
+import { selectorVModelMixin } from '@/utils/mixins/selector-v-model'
+import { checkPhonePass } from '@/utils/utils'
+import { phoneRegExp } from '@/utils/data'
+import { mapState, mapMutations } from 'vuex'
+
+export default {
+  name: 'OrderBaseInfo',
+  mixins: [selectorVModelMixin],
+  components: {
+    RadioGroup
+  },
+  data() {
+    return {
+      conf: {
+        buySubjectOptions,
+      },
+      required: {
+        buySubject: true,
+        userTel: true,
+        accountTel: true,
+      },
+      // form: {
+      //   buySubject: 1,
+      //   companyName: '',
+      //   username: '',
+      //   userTel: '',
+      //   accountTel: '',
+      // },
+    }
+  },
+  computed: {
+    ...mapState({
+      form: state => state.order.pageForm,
+    }),
+    requiredList() {
+      return [
+        {
+          field: 'buySubject',
+          message: '购买主体为必填项', // 错误提示文案
+          rank: 1, // 提示优先级,值越小优先级越高
+          required: this.required.buySubject,
+        },
+        {
+          field: 'companyName',
+          message: '公司名称为必填项', // 错误提示文案
+          rank: 1, // 提示优先级,值越小优先级越高
+          required: this.companyRequired,
+        },
+        {
+          field: 'userTel',
+          message: '联系人手机号为必填项', // 错误提示文案
+          rank: 1, // 提示优先级,值越小优先级越高
+          required: this.required.userTel,
+          validator: () => {}
+        },
+        {
+          field: 'accountTel',
+          message: '开通权益手机号为必填项', // 错误提示文案
+          rank: 1, // 提示优先级,值越小优先级越高
+          required: this.required.accountTel,
+        },
+      ].filter(f => f.required)
+    },
+    rules() {
+      const companyRequired = this.companyRequired
+
+      return {
+        companyName: [
+          { required: companyRequired, message: '请输入公司名称', trigger: 'blur' }
+        ],
+        userTel: [
+          { required: true, message: '请输入联系人手机号', trigger: 'blur' },
+          { type: 'string', pattern: phoneRegExp, message: '手机号格式不正确', trigger: 'blur'}
+        ],
+        accountTel: [
+          { required: true, message: '请输入开通权益手机号', trigger: 'blur' },
+          { type: 'string', pattern: phoneRegExp, message: '手机号格式不正确', trigger: 'blur'}
+        ]
+      }
+    },
+    companyRequired() {
+      return this.form.buySubject === 2
+    },
+  },
+  watch: {
+    companyRequired() {
+      setTimeout(() => {
+        this.$refs.form.clearValidate()
+      }, 0)
+    }
+  },
+  methods: {
+    ...mapMutations('order', [
+      'setPageForm',
+      'setOrderProductInfoList',
+      'addOrderProductItem',
+    ]),
+    validate() {
+      return new Promise((resolve, reject) => {
+        this.$refs.form.validate((valid) => {
+          if (valid) {
+            resolve()
+          } else {
+            reject()
+          }
+        })
+      })
+    },
+    getState() {
+      return Object.assign({}, this.form)
+    },
+    setState(t) {
+      if (!t) return
+      Object.assign(this.form, t)
+    },
+    phoneCheck(text) {
+      return checkPhonePass(text)
+    },
+    onElInput(e, key) {
+      this.onInputChange(e, key)
+      this.setPageForm({
+        key,
+        data: e
+      })
+    },
+    onInputChange(e, key) {
+      if (key === 'buySubject') {
+        // 清空产品列表
+        this.setOrderProductInfoList([])
+        this.addOrderProductItem()
+      } else if (key === 'accountTel') {
+        // 清空产品列表
+        this.setOrderProductInfoList([])
+        this.addOrderProductItem()
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 264 - 0
src/views/create-order/components/contractInfoModule.vue

@@ -0,0 +1,264 @@
+<template>
+  <!-- 签约信息 -->
+  <el-form ref="form" :model="form" :rules="rules" label-width="126px" class="order-contract-info-container">
+    <el-row :gutter="2">
+      <el-col :span="12">
+        <el-form-item label="协议状态" prop="agreeStatus" :required="required.agreeStatus">
+          <RadioGroup v-model="form.agreeStatus" :options="conf.agreeStatusOptions" />
+        </el-form-item>
+      </el-col>
+      <el-col :span="12">
+        <el-form-item label="签约主体" prop="signUnit" v-if="showXieYi">
+          <el-select
+            v-model="form.signUnit"
+            size="medium"
+            class="el-select-w100"
+            placeholder="请选择签约主体">
+            <el-option
+              v-for="item in conf.signUnitOptions"
+              :key="item.value"
+              :label="item.label"
+              :value="item.value">
+            </el-option>
+          </el-select>
+        </el-form-item>
+      </el-col>
+    </el-row>
+    <template v-if="showXieYi">
+      <el-row :gutter="2">
+        <el-col :span="12">
+          <el-form-item label="协议签订时间" prop="signTime">
+            <el-date-picker
+              v-model="form.signTime"
+              value-format="timestamp"
+              type="date"
+              size="medium"
+              placeholder="请选择协议签订时间">
+            </el-date-picker>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="协议编号" prop="signCode" v-if="showSingCode">
+            <el-input v-model="form.signCode" placeholder="请输入协议编号" size="medium" maxlength="40"></el-input>
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <!-- <el-row :gutter="2">
+        <el-col :span="24">
+          <el-form-item label="协议备注" prop="signRemark" v-if="showOnlineContractForm">
+            <el-input v-model="form.signRemark" placeholder="请输入协议备注" size="medium" maxlength="180"></el-input>
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row :gutter="2">
+        <el-col :span="12">
+          <el-form-item label="电子协议类型" prop="signType" v-if="showOnlineContractForm">
+            <RadioGroup v-model="form.signType" :options="conf.eSignTypeOptions" />
+          </el-form-item>
+        </el-col>
+      </el-row> -->
+
+      <!-- 电子协议表单 -->
+      <ElOnlineContractForm
+        ref="onlineContractFormRef"
+        v-if="showOnlineContractForm"
+        :showMore="showMore && showOnlineContractForm"
+        :buySubject="buySubject"
+        :orderEntName="companyName"
+        :e_contract_type.sync="e_.contract_type"
+        :e_contract_userA_type.sync="e_.contract_userA_type"
+        :e_contract_userA_name.sync="e_.contract_userA_name"
+        :e_contract_userA_contacts_name.sync="e_.contract_userA_contacts_name"
+        :e_contract_userA_contacts_tel.sync="e_.contract_userA_contacts_tel"
+        :e_contract_userA_contacts_address.sync="e_.contract_userA_contacts_address"
+        :e_contract_userB_contacts_name.sync="e_.contract_userB_contacts_name"
+        :e_contract_remark.sync="e_.contract_remark" />
+
+      <el-divider>
+        <el-button
+          type="text"
+          :icon="this.showMore ? 'el-icon-arrow-up' : 'el-icon-arrow-down'"
+          @click="showMoreForm">更多信息</el-button>
+      </el-divider>
+    </template>
+  </el-form>
+</template>
+
+<script>
+import RadioGroup from '@/views/create-order/components/product-info-submodule/RadioGroup'
+import ElOnlineContractForm from '@/views/create-order/components/product-info-submodule/ElOnlineContractForm'
+import { agreeStatusOptions, eSignTypeOptions, signUnitOptions } from '@/views/create-order/data/index.js'
+import { selectorVModelMixin } from '@/utils/mixins/selector-v-model'
+import { mapState, mapGetters } from 'vuex'
+
+export default {
+  name: 'ContractInfoModule',
+  mixins: [selectorVModelMixin],
+  components: {
+    ElOnlineContractForm,
+    RadioGroup
+  },
+  data() {
+    return {
+      conf: {
+        agreeStatusOptions,
+        eSignTypeOptions,
+        signUnitOptions,
+      },
+      required: {
+        agreeStatus: true,
+      },
+      form: {
+        agreeStatus: '1',
+        signUnit: 'h01',
+        signTime: '',
+        signCode: '',
+        signRemark: '',
+        signType: '2',
+      },
+      showMoreXieYi: false,
+      // 电子协议相关
+      e_: {
+        contract_type: 1, // 电子协议类型
+        contract_userA_type: 1, // 协议甲方类型
+        contract_userA_name: '', // 协议甲方
+        contract_userA_contacts_name: '', // 协议甲方联系人
+        contract_userA_contacts_tel: '', // 协议甲方联系方式
+        contract_userA_contacts_address: '', // 协议甲方联系地址
+        contract_userB_contacts_name: '', // 协议乙方联系人
+        contract_remark: '', // 协议备注
+      },
+    }
+  },
+  computed: {
+    ...mapState({
+      pageForm: state => state.order.pageForm,
+    }),
+    ...mapGetters('order', ['userProductInfoList']),
+    buySubject() {
+      return this.pageForm.buySubject
+    },
+    companyName() {
+      return this.pageForm.companyName
+    },
+    requiredList() {
+      return [
+        {
+          field: 'agreeStatus',
+          message: '协议状态为必填项', // 错误提示文案
+          rank: 1,
+          required: this.required.agreeStatus,
+        },
+        {
+          field: 'signUnit',
+          message: '签约主体为必填项',
+          required: this.companyRequired,
+        }
+      ].filter(f => f.required)
+    },
+    rules() {
+      const companyRequired = this.companyRequired
+
+      return {
+        companyName: [
+          { required: companyRequired, message: '请输入公司名称', trigger: 'blur' }
+        ],
+        userTel: [
+          { required: true, message: '请输入联系人手机号', trigger: 'blur' },
+        ],
+        accountTel: [
+          { required: true, message: '请输入开通权益手机号', trigger: 'blur' },
+        ]
+      }
+    },
+    showXieYi() {
+      return this.form.agreeStatus === '1'
+    },
+    showMore() {
+      return this.showMoreXieYi
+    },
+    companyRequired() {
+      return this.showXieYi
+    },
+    showSingCode() {
+      return !this.showOnlineContractForm
+    },
+    showOnlineContractForm() {
+      const paybackCompanyCheck = this.form.signUnit == 'h01' // 签约主体为:北京剑鱼信息技术有限公司/h01
+      const contractStatusCheck = this.form.agreeStatus == '1' // 协议状态为签协议
+
+
+      // 4.支持线上生成电子协议:
+      // (1)签约主体为“北京剑鱼信息技术有限公司”;
+      // (2)仅有1个产品销售策略为“售卖”;
+      // (3)且销售策略为“售卖”的产品规格及购买主体有配置电子协议模板;
+      // 销售策略为“售卖”的产品规格付费类型为“购买”、“续费”、“试用”。
+
+      return true
+      // 展示条件:
+      // 1. 产品类型是超级订阅(且付费类型为“购买”、“续费”)才展示。
+      // 2. 产品类型是“大会员”且会员套餐为“商机版2.0”、“专家版2.0”(且服务类型为“新购服务”、“延长服务”)
+      // memberLevelMap.id:6'商机版2.0'、7'专家版2.0'
+      // const memberLevel = this.member.level
+      // if (!memberLevel || memberLevel == 'undefined') return false
+      // const memberLevelMap = JSON.parse(memberLevel)
+      // // console.log(memberLevelMap)
+      // // createType '1'新建  '2'补充  '3'延期
+      // const createType = this.nums == 0 ? this.member.createType : '1'
+      // // 这里的单省版 是商机版2.0单省版
+      // const bigmemberCheck = (memberLevelMap.id == 6 || memberLevelMap.id == 7 || memberLevelMap.s_name.indexOf('单省版') > -1) && (createType == '1' || createType == '3')
+      // const productTypeCheck = bigmemberCheck
+      // return paybackCompanyCheck && contractStatusCheck && productTypeCheck
+    },
+  },
+  watch: {
+    companyRequired() {
+      setTimeout(() => {
+        this.$refs.form.clearValidate()
+      }, 0)
+    },
+    // showXieYi: {
+    //   immediate: true,
+    //   handler(n) {
+    //     if (n) {
+    //       this.initSignTime()
+    //     }
+    //   }
+    // },
+  },
+  created() {
+    this.initSignTime()
+  },
+  methods: {
+    validate() {
+      return new Promise((resolve, reject) => {
+        this.$refs.form.validate((valid) => {
+          if (valid) {
+            resolve()
+          } else {
+            reject()
+          }
+        })
+      })
+    },
+    getState() {
+      return Object.assign({}, this.form)
+    },
+    setState(t) {
+      if (!t) return
+      Object.assign(this.form, t)
+    },
+    initSignTime() {
+      // 初始化协议签订时间为当天
+      this.form.signTime = Date.now()
+    },
+    showMoreForm() {
+      this.showMoreXieYi = !this.showMoreXieYi
+    },
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 122 - 0
src/views/create-order/components/create.vue

@@ -0,0 +1,122 @@
+<template>
+  <div class="create-order-content-container">
+    <ModuleCard class="create-order-module" title="基本信息">
+      <BaseInfo ref="moduleBaseInfo" />
+    </ModuleCard>
+    <ModuleCard class="create-order-module" title="产品信息">
+      <ProductInfoModule ref="moduleProductInfo" />
+    </ModuleCard>
+    <ModuleCard class="create-order-module" title="协议信息">
+      <ContractInfoModule ref="moduleContractInfo" />
+    </ModuleCard>
+    <!-- 合同金额为0,则不展示回款计划模块 -->
+    <ModuleCard class="create-order-module" title="回款计划" v-if="totalMoney > 0">
+      <PaymentPlanModule ref="modulePaymentPlan" :totalMoney="totalMoney" :contractStatus="pageForm.agreeStatus" />
+    </ModuleCard>
+    <ModuleCard class="create-order-module" title="业绩归属">
+      <PerformanceBelongsModule ref="modulePerformanceBelong" setDefaultUser />
+    </ModuleCard>
+    <ModuleCard class="create-order-module" title="其他信息">
+      <OtherInfoModule ref="moduleOtherInfo" />
+    </ModuleCard>
+  </div>
+</template>
+
+<script>
+import ModuleCard from '../ui/ModuleCard.vue'
+import BaseInfo from './baseInfoModule.vue'
+import PaymentPlanModule from './paymentPlanModule.vue'
+import ProductInfoModule from './productInfoModule.vue'
+import PerformanceBelongsModule from './performanceBelongsModule.vue'
+import OtherInfoModule from './otherInfoModule.vue'
+import ContractInfoModule from './contractInfoModule.vue'
+import { mapState } from 'vuex'
+
+export default {
+  name: 'CreateOrderContent',
+  components: {
+    ModuleCard,
+    BaseInfo,
+    PaymentPlanModule,
+    ProductInfoModule,
+    OtherInfoModule,
+    PerformanceBelongsModule,
+    ContractInfoModule,
+  },
+  data() {
+    return {
+      totalMoney: 1
+    }
+  },
+  computed: {
+    ...mapState({
+      pageForm: state => state.order.pageForm,
+    }),
+  },
+  created() {
+    this.$store.dispatch('order/getSelectOptions')
+  },
+  methods: {
+    async validate() {
+      const refsArr = this.getFormRefs()
+      const asyncArr = refsArr.map(r => r?.validate())
+      const allPromise = Promise.all(asyncArr)
+      try {
+        await allPromise
+        console.log('success')
+      } catch (error) {
+        console.log('error', error)
+      } finally {
+        console.log('finally', allPromise)
+      }
+    },
+    getFormRefs() {
+      const refs = this.$refs
+      const refsArr = []
+      for (const key in refs) {
+        if (key.startsWith('module')) {
+          refsArr.push(refs[key])
+        }
+      }
+      return refsArr
+    },
+    getState() {
+      const refsArr = this.getFormRefs()
+      const result = {}
+      refsArr.forEach(r => {
+        const t = r?.getState()
+        Object.assign(result, t)
+      })
+      return result
+    },
+    setState() {
+      const refsArr = this.getFormRefs()
+      console.log('setState', refsArr)
+    },
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+::v-deep {
+  .el-form-item {
+    margin-bottom: 12px;
+  }
+  .el-form-item__label {
+    color: #686868;
+  }
+  .el-form-item__error {
+    padding-top: 0;
+  }
+
+  .el-select-w100.el-select {
+    width: 100%;
+  }
+}
+.create-order-content-container {
+  padding-top: 20px;
+}
+.create-order-module {
+  font-size: 14px;
+}
+</style>

+ 147 - 0
src/views/create-order/components/otherInfoModule.vue

@@ -0,0 +1,147 @@
+<template>
+  <!-- 其他信息 -->
+  <el-form ref="form" :model="form" :rules="rules" label-width="126px" class="order-other-info-container">
+    <el-row :gutter="2">
+      <el-col :span="12">
+        <el-form-item label="约定支付方式" prop="reservationPayWay" required>
+          <el-select
+            v-model="form.reservationPayWay"
+            size="medium"
+            placeholder="请选择约定支付方式">
+            <el-option
+              v-for="item in conf.payWayOptions"
+              :key="item.value"
+              :label="item.label"
+              :value="item.value">
+            </el-option>
+          </el-select>
+        </el-form-item>
+      </el-col>
+      <el-col :span="12">
+        <el-form-item label="下单渠道" prop="orderChannel" required>
+          <el-select
+            v-model="form.orderChannel"
+            class="el-select-w100"
+            size="medium"
+            placeholder="请选择下单渠道">
+            <el-option
+              v-for="item in conf.orderChannelOptions"
+              :key="item.value"
+              :label="item.label"
+              :value="item.value">
+            </el-option>
+          </el-select>
+        </el-form-item>
+      </el-col>
+    </el-row>
+    <el-row :gutter="2">
+      <el-col :span="24">
+        <el-form-item label="付款户名" prop="paymentAccountName">
+          <el-input v-model="form.paymentAccountName" placeholder="个人打款为姓名,公司打款为公司名称,对公转账将根据付款户名自动关联回款信息" size="medium" maxlength="100"></el-input>
+        </el-form-item>
+      </el-col>
+    </el-row>
+    <el-row :gutter="2">
+      <el-col :span="24">
+        <el-form-item label="订单备注" prop="remark">
+          <el-input v-model="form.remark" placeholder="请输入订单备注" size="medium" maxlength="100"></el-input>
+        </el-form-item>
+      </el-col>
+    </el-row>
+  </el-form>
+</template>
+
+<script>
+import { payWayOptions } from '@/views/create-order/data/index.js'
+import { selectorVModelMixin } from '@/utils/mixins/selector-v-model'
+import { mapState } from 'vuex'
+
+export default {
+  name: 'OtherInfoModule',
+  mixins: [selectorVModelMixin],
+  data() {
+    return {
+      conf: {
+        payWayOptions,
+        orderChannelOptions: [],
+      },
+      required: {
+        reservationPayWay: true,
+        orderChannel: true,
+      },
+      form: {
+        reservationPayWay: '',
+        orderChannel: 'd01',
+        paymentAccountName: '',
+        remark: '',
+      }
+    }
+  },
+  computed: {
+    ...mapState({
+      pageForm: state => state.order.pageForm,
+    }),
+    companyName() {
+      return this.pageForm.companyName
+    },
+    rules() {
+      const { reservationPayWay, orderChannel } = this.required
+      return {
+        reservationPayWay: [
+          { required: reservationPayWay, message: '请选择约定支付方式', trigger: 'blur' }
+        ],
+        orderChannel: [
+          { required: orderChannel, message: '请选择下单渠道', trigger: 'blur' },
+        ]
+      }
+    },
+  },
+  watch: {
+    companyName(n) {
+      this.form.paymentAccountName = n
+    },
+  },
+  created() {
+    this.getOrderChannel()
+  },
+  methods: {
+    validate() {
+      return new Promise((resolve, reject) => {
+        this.$refs.form.validate((valid) => {
+          if (valid) {
+            resolve()
+          } else {
+            reject()
+          }
+        })
+      })
+    },
+    getState() {
+      return Object.assign({}, this.form)
+    },
+    setState(t) {
+      if (!t) return
+      Object.assign(this.form, t)
+    },
+    getOrderChannel() {
+      if (this.conf.orderChannelOptions.length > 0) {
+        return
+      }
+      this.$request('/order/getDictItem').data({ name: '下单渠道' }).success((res) => {
+        if (Array.isArray(res?.data?.data)) {
+          this.conf.orderChannelOptions = res.data.data.map(s => {
+            return {
+              label: s.item_name,
+              value: s.item_code,
+            }
+          })
+        }
+      }).post()
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 320 - 0
src/views/create-order/components/paymentPlanModule.vue

@@ -0,0 +1,320 @@
+<template>
+  <!-- 回款计划 -->
+  <el-form ref="form" label-width="206px" class="payment-plan-container">
+    <el-form-item class="payment-plan-line" label-width="156px" label="回款次数" :required="required.payback">
+      <number-input v-model.number="form.payback" @input="onPaybackChange" placeholder="请输入" maxlength="2">
+        <template #append>次</template>
+      </number-input>
+      <div class="payback-table-container" v-if="payBackTimesMoreThan1">
+        <el-table
+          :data="paybackTableData"
+          border
+          stripe
+          :summary-method="getSummaries"
+          show-summary>
+          <el-table-column
+            type="index"
+            header-align="center"
+            align="center"
+            width="100"
+            label="序号">
+          </el-table-column>
+          <el-table-column
+            prop="name"
+            header-align="center"
+            align="center"
+            label="预计回款时间">
+            <template slot-scope="scope">
+              <el-date-picker
+                v-model="paybackTableData[scope.$index].time"
+                type="date"
+                placeholder="请选择日期">
+              </el-date-picker>
+            </template>
+          </el-table-column>
+          <el-table-column
+            prop="money"
+            header-align="center"
+            align="center"
+            label="预计回款金额(元)">
+            <template slot-scope="scope">
+              <el-input
+                v-model.number="paybackTableData[scope.$index].money"
+                type="number"
+                placeholder="请输入"
+                @input="onPaybackSplitMoneyChange(scope)"
+                size="medium"
+                :disabled="scope.$index === paybackTableData.length - 1"
+              ></el-input>
+            </template>
+          </el-table-column>
+        </el-table>
+      </div>
+    </el-form-item>
+    <el-form-item class="payment-plan-line" :label="paybackTimeLabel" :required="required.paymentDeadline" v-if="!payBackTimesMoreThan1">
+      <number-input v-model.number="form.paymentDeadline" @input="onPaymentDeadlineChange" placeholder="请输入" maxlength="3">
+        <template #append>个工作日回款,预计回款时间:<span style="color: #36a3f7">{{ expectedPaymentDeadlineTimeString }}</span></template>
+      </number-input>
+    </el-form-item>
+  </el-form>
+</template>
+
+<script>
+import { debounce } from 'lodash'
+import {dateFormatter} from '@/assets/js/date'
+import { toNumber } from '@/utils/utils'
+import { selectorVModelMixin } from '@/utils/mixins/selector-v-model'
+import { agreeStatusOptions } from '@/views/create-order/data/index.js'
+import NumberInput from '@/views/create-order/ui/NumberInput.vue'
+import { getWorkDay } from '@/api/modules'
+
+class PayBackTableRow {
+  constructor(time = '', money = '') {
+    this.time = time
+    this.money = money
+  }
+}
+
+export default {
+  name: 'PaymentPlanModule',
+  mixins: [selectorVModelMixin],
+  components: {
+    NumberInput
+  },
+  props: {
+    totalMoney: {
+      type: [Number, String],
+      default: undefined
+    },
+    contractStatus: {
+      type: String,
+      default: '0',
+      validator(v) {
+        return agreeStatusOptions.map(a => a.value).includes(v)
+      }
+    },
+    contractTime: {
+      type: String,
+      default: ''
+    }
+  },
+  data() {
+    return {
+      conf: {
+        paybackMax: 5
+      },
+      required: {
+        payback: true,
+        paymentDeadline: true,
+      },
+      form: {
+        payback: '1',
+        paymentDeadline: '',
+      },
+      paybackTableData: [],
+      expectedPaymentDeadlineTime: '',
+    }
+  },
+  computed: {
+    payBackTimesMoreThan1() {
+      return this.form.payback > 1
+    },
+    exceptedPayBackMoney() {
+      // exceptPayBackMoney
+      return this.totalMoney
+    },
+    requiredList() {
+      return [
+        {
+          field: 'payback',
+          message: '回款次数为必填项', // 错误提示文案
+          rank: 2, // 提示优先级,值越小优先级越高
+          required: this.required.payback,
+        },
+        {
+          field: 'paymentDeadline',
+          message: '回款时间为必填项', // 错误提示文案
+          rank: 2,
+          required: this.required.paymentDeadline && !this.payBackTimesMoreThan1,
+        },
+      ].filter(f => f.required)
+    },
+    expectedPaymentDeadlineTimeString() {
+      return this.expectedPaymentDeadlineTime || '-'
+    },
+    paybackTimeLabel() {
+      return '自协议签订/订单创建之日起'
+      // return this.contractStatus === '1' ? '自协议签订之日起': '自订单创建之日起'
+    }
+  },
+  methods: {
+    validate() {
+      return new Promise((resolve, reject) => {
+        const { payback, paymentDeadline } = this.form
+        const pass = payback && paymentDeadline
+        if (pass) {
+          resolve()
+        } else {
+          reject()
+        }
+      })
+    },
+    getState() {
+      return Object.assign({}, this.form)
+    },
+    setState(t) {
+      if (!t) return
+      Object.assign(this.form, t)
+    },
+    onPaybackChange() {
+      const max = this.conf.paybackMax
+      const i = this.form.payback
+      if (i > max) {
+        this.form.payback = max
+        this.$message({
+          message: `最大值为${max}`,
+          type: 'warning'
+        })
+      }
+      if (i > 1) {
+        this.initPaybackTableData()
+      }
+    },
+    calcLastColumnMoney() {
+      // 1. 计算最后一个格子的金额
+      // 1.1 先计算除了最后一个的总和
+      let eNum = 0
+      this.paybackTableData.forEach((p, index) => {
+        if (index <= this.paybackTableData.length - 2) {
+          eNum += Number(p.money)
+        }
+      })
+      
+      const lastMoney = this.exceptedPayBackMoney - eNum
+
+      return lastMoney
+    },
+    onPaybackSplitMoneyChange(scope) {
+      const index = scope.$index
+      if (this.exceptedPayBackMoney === undefined || this.exceptedPayBackMoney === null) {
+        return this.$message({
+          message: '请先输入合同金额',
+          type: 'warning'
+        })
+      }
+      if (scope.row.money === 0 || scope.row.money === '0') {
+        scope.row.money = ''
+        return this.$message({
+          message: '回款金额不能为0',
+          type: 'warning'
+        })
+      }
+      let lastMoney = this.calcLastColumnMoney()
+      // 1.2 限制当前格子的金额
+      if (lastMoney < 0) {
+        this.paybackTableData[index].money = ''
+        this.$message({
+          message: '需小于合同金额',
+          type: 'warning'
+        })
+        lastMoney = this.calcLastColumnMoney()
+      }
+      this.paybackTableData[this.paybackTableData.length - 1].money = lastMoney
+    },
+    // 回款计划,工作日变更
+    onPaymentDeadlineChange: debounce(function() {
+      this.expectedPaymentDeadlineTime = ''
+
+      // 签协议:预计回款时间=协议签订时间+填写的工作日
+      // 不签协议:预计回款时间=订单创建时间+填写的工作日
+
+      // 回款工作日为0
+      const paymentDeadline = toNumber(this.form.paymentDeadline)
+      if (paymentDeadline === 0) {
+        if (this.contractStatus === '1') {
+          this.expectedPaymentDeadlineTime  = dateFormatter(this.contractTime || new Date(), 'yyyy-MM-dd')
+        }else{
+          this.expectedPaymentDeadlineTime = dateFormatter(new Date(), 'yyyy-MM-dd')
+        }
+      } else {
+        let start = ''
+        if (this.contractStatus === '1') { // 签协议
+          start = dateFormatter(this.contractTime || new Date(), 'yyyy-MM-dd')
+        } else { // 不签协议
+          start = dateFormatter(new Date(), 'yyyy-MM-dd')
+        }
+        if (!paymentDeadline  || !start) {
+          return
+        }
+        const params = {
+          inputTime: start,
+          workDayNum: toNumber(paymentDeadline)
+        }
+        getWorkDay(params).then((res) => {
+          if (res.data) {
+            this.expectedPaymentDeadlineTime = res.data
+          }
+        })
+      }
+    }, 700),
+    initPaybackTableData() {
+      const arr = new Array(this.form.payback).fill(undefined).map(() => new PayBackTableRow())
+      this.paybackTableData = arr
+    },
+    getSummaries(param) {
+      const { columns } = param
+      const sums = [];
+      columns.forEach((column, index) => {
+        if (index === 0) {
+          sums[index] = '合计';
+          return;
+        }
+        if (index === 1) {
+          sums[index] = '-';
+          return
+        }
+        if (index === 2) {
+          sums[index] = this.exceptedPayBackMoney || '-';
+          return
+        }
+      });
+      return sums;
+    },
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.payment-plan-line {
+  $radius: 4px;
+  ::v-deep {
+    .el-input-group__append {
+      background: transparent;
+      border: none;
+      color: #1d1d1d;
+    }
+    .el-input-group--append {
+      width: auto;
+      .el-input__inner {
+        width: 128px;
+        border-top-right-radius: $radius;
+        border-bottom-right-radius: $radius;
+      }
+    }
+  }
+}
+.payback-table-container {
+  margin-top: 8px;
+  ::v-deep {
+    .el-table {
+      border-radius: 6px;
+    }
+    .el-table__header {
+      tr,
+      th.el-table__cell {
+        background: #f5f7fa;
+      }
+    }
+  }
+}
+</style>

+ 331 - 0
src/views/create-order/components/performanceBelongsModule.vue

@@ -0,0 +1,331 @@
+<template>
+  <!-- 业绩归属 -->
+  <el-form ref="form" label-width="156px" class="performance-belongs-container">
+    <el-form-item class="performance-belongs-line" label="销售人员" :required="required.salePerson">
+      <SelectTree
+        v-model="form.salePerson"
+        class="sales-person-select"
+        @change="onSalePersonSelectChange"
+        :options="salesDepList"
+        placeholder="请选择销售人员">
+      </SelectTree>
+      <div class="sale-person-list-container" v-if="form.salePerson.length > 1">
+        <el-table
+          :data="form.salePersonTableList"
+          stripe
+          border>
+          <el-table-column
+            prop="name"
+            header-align="center"
+            align="center"
+            label="销售人员">
+          </el-table-column>
+          <el-table-column
+            prop="money"
+            header-align="center"
+            align="center"
+            label="预计回款金额(元)">
+            <template slot-scope="scope">
+              <number-input
+                v-model.number="form.salePersonTableList[scope.$index].money"
+                type="number"
+                placeholder="请输入"
+                @input="onSalePersonSplitMoneyChange(scope)"
+                size="medium"
+              ></number-input>
+            </template>
+          </el-table-column>
+          <el-table-column
+            prop="saleWay"
+            header-align="center"
+            align="center"
+            label="销售渠道">
+            <template slot-scope="scope" >
+              <template v-if="scope.$index === form.salePersonTableList.length - 1">-</template>
+              <template v-else>
+                <el-cascader
+                  :options="saleChannelOptions"
+                  :props="conf.cascaderProps"
+                  v-model="form.salePersonTableList[scope.$index].saleWay"
+                  size="medium"
+                  placeholder="请选择销售渠道"
+                  clearable></el-cascader>
+              </template>
+            </template>
+          </el-table-column>
+        </el-table>
+      </div>
+    </el-form-item>
+    <el-form-item class="performance-belongs-line" label="销售渠道" v-if="onlyOneSale" :required="required.saleWay && onlyOneSale">
+      <el-cascader
+        v-model="form.saleWay"
+        :options="saleChannelOptions"
+        :props="conf.cascaderProps"
+        size="medium"
+        placeholder="请选择销售渠道"
+        clearable></el-cascader>
+    </el-form-item>
+  </el-form>
+</template>
+
+<script>
+import { debounce } from 'lodash'
+import { selectorVModelMixin } from '@/utils/mixins/selector-v-model'
+import NumberInput from '@/views/create-order/ui/NumberInput.vue'
+import SelectTree from '@/views/create-order/components/product-info-submodule/select-tree'
+import { mapState, mapGetters } from 'vuex'
+
+class SalePersonTableRow {
+  constructor(name = '', id = '', money = '', saleWay = '') {
+    this.name = name
+    this.id = id
+    this.money = money
+    this.saleWay = saleWay
+  }
+}
+
+export default {
+  name: 'PerformanceBelongsModule',
+  mixins: [selectorVModelMixin],
+  components: {
+    NumberInput,
+    SelectTree,
+  },
+  props: {
+    setDefaultUser: {
+      type: Boolean,
+      default: false
+    }
+  },
+  data() {
+    return {
+      required: {
+        salePerson: true,
+        saleWay: true,
+      },
+      conf: {
+        cascaderProps: {
+          label: 'item_name',
+          value: 'item_code',
+          multiple: false,
+        },
+        saleWayOptions: [],
+      },
+      loading: {
+        salePerson: false,
+        saleWay: false,
+      },
+      form: {
+        salePerson: [],
+        saleWay: [],
+        salePersonTableList: [],
+      },
+    }
+  },
+  computed: {
+    ...mapGetters([
+      'getAdminUser',
+    ]),
+    ...mapGetters('order', [
+      'depTreeList',
+      'saleChannelOptions',
+    ]),
+    salesDepList() {
+      return this.depTreeList
+    },
+    onlyOneSale() {
+      return this.form.salePerson.length === 1
+    },
+    salePersonDetailInfo() {
+      return this.form.salePerson.map(fs => {
+        return this.deepestNodesArr.find(r => r.value === fs)
+      })
+    },
+    deepestNodesArr() {
+      return this.findDeepestNodes(this.salesDepList)
+    },
+    requiredList() {
+      return [
+        {
+          field: 'paymentCount',
+          message: '回款次数为必填项', // 错误提示文案
+          rank: 2, // 提示优先级,值越小优先级越高
+          required: this.required.paymentCount,
+        },
+        {
+          field: 'paymentDeadline',
+          message: '回款时间为必填项', // 错误提示文案
+          rank: 2,
+          required: this.required.paymentDeadline,
+        },
+      ].filter(f => f.required)
+    },
+  },
+  watch: {
+    'form.salePerson': function() {
+      this.initSaleWayDefaultValue()
+    }
+  },
+  methods: {
+    validate() {
+      return new Promise((resolve, reject) => {
+        const { salePerson, saleWay } = this.form
+        const pass = salePerson.length > 0 && saleWay.length > 0
+        if (pass) {
+          resolve()
+        } else {
+          reject()
+        }
+      })
+    },
+    getState() {
+      return Object.assign({}, this.form)
+    },
+    setState(t) {
+      if (!t) return
+      Object.assign(this.form, t)
+    },
+    findDeepestNodes(treeData = []) {
+      let deepestNodes = []
+      function findDeepestValues(node) {
+        if (!node.children || node.children.length === 0) {
+          return [node];
+        }
+        let nodes = [];
+        node.children.forEach(child => {
+          nodes = nodes.concat(findDeepestValues(child));
+        });
+        return nodes
+      }
+      treeData.forEach(node => {
+        deepestNodes = deepestNodes.concat(findDeepestValues(node));
+      });
+
+      return deepestNodes
+    },
+    getSaleWayOption() {
+      if (this.conf.saleWayOptions.length > 0) return
+      this.loading.saleWay = true
+      const r = this.$store.dispatch('order/getSelectOptions')
+      this.$request('/order/getSalesChannelItem').data({ name: '销售渠道' }).success((res) => {
+        this.conf.saleWayOptions = res.data?.data || []
+      }).complete(() => {
+        
+      }).post()
+
+      this.loading.saleWay = false
+    },
+    getSelectSalePersonDetailList() {
+      // 按照选择的顺序取
+      const { salePerson } = this.form
+      const arr = []
+      salePerson.forEach(id => {
+        const target = this.deepestNodesArr.find(opt => opt.value === id)
+        if (target) {
+          arr.push(target)
+        }
+      })
+      return arr
+    },
+    onSalePersonSelectChange() {
+      this.initSalePersonList()
+    },
+    initSalePersonList() {
+      const selectDetailList = this.getSelectSalePersonDetailList()
+      const arr = selectDetailList.map(item => {
+        return new SalePersonTableRow(item.label, item.value)
+      })
+      arr.push(new SalePersonTableRow('合计'))
+      this.form.salePersonTableList = arr
+    },
+    calcSalePersonLastColumnMoney() {
+      // 1. 计算最后一个格子的金额
+      let eNum = 0
+      this.form.salePersonTableList.forEach((p, index) => {
+        if (index <= this.form.salePersonTableList.length - 2) {
+          eNum += Number(p.money)
+        }
+      })
+      return eNum
+    },
+    compareDepName(a = '', b = '') {
+      return a.includes(b) || b.includes(a)
+    },
+    initSaleWayDefaultValue() {
+      const { salePerson } = this.form
+      if (salePerson.length === 1) {
+        const depInfo = this.salePersonDetailInfo[0]
+        const saleWayKey = this.getDefaultSaleWayKey(depInfo.parentLabel)
+        this.form.saleWay = saleWayKey
+      } else {
+        this.salePersonDetailInfo.forEach((depInfo, index) => {
+          const saleWayKey = this.getDefaultSaleWayKey(depInfo.parentLabel)
+          this.form.salePersonTableList[index].saleWay = saleWayKey
+        })
+      }
+    },
+    getDefaultSaleWayKey(name) {
+      let parentLevel = null
+      let childLevel = null
+      const arr = []
+      parentLevel = this.saleChannelOptions.find(first => {
+        if (Array.isArray(first.children)) {
+          const target = first.children.find(s => {
+            const res = this.compareDepName(s.item_name, name)
+            if (res) {
+              childLevel = s
+              return s
+            }
+          })
+          if (target) {
+            return first
+          }
+        }
+      })
+      if (parentLevel && childLevel) {
+        arr.push(parentLevel.item_code)
+        arr.push(childLevel.item_code)
+      }
+      return arr || []
+    },
+    onSalePersonSplitMoneyChange: debounce(function(scope) {
+      if (scope.$index === this.form.salePersonTableList.length - 1) {
+        // 最后一行,不做处理
+        return
+      }
+      // 不是最后一行,计算最后一行的合计
+      const total = this.calcSalePersonLastColumnMoney()
+      this.form.salePersonTableList[this.form.salePersonTableList.length - 1].money = total
+    }, 200),
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.performance-belongs-line {
+  $radius: 4px;
+  ::v-deep {
+    .el-input-group__append {
+      background: transparent;
+      border: none;
+      color: #1d1d1d;
+    }
+    .el-input-group--append {
+      width: auto;
+      .el-input__inner {
+        width: 128px;
+        border-top-right-radius: $radius;
+        border-bottom-right-radius: $radius;
+      }
+    }
+  }
+
+  .sales-person-select {
+    ::v-deep {
+      .el-select {
+        width: 100%;
+      }
+    }
+  }
+}
+</style>

+ 86 - 0
src/views/create-order/components/product-info-submodule/AccountNumbers.vue

@@ -0,0 +1,86 @@
+<template>
+  <div class="account-numbers-container">
+    <span>付费</span>
+    <number-input
+      v-model.number="payCount"
+      @input="onInputChange"
+      class="number-input"
+      :placeholder="placeholder"
+      maxlength="3"
+    ></number-input>
+    <span>个</span>
+    <template v-if="showFreeCount">
+      <span>,赠送</span>
+      <number-input
+        v-model.number="freeCount"
+        @input="onInputChange"
+        class="number-input"
+        :placeholder="placeholder"
+        maxlength="3"
+      ></number-input>
+      <span>个,合计:<span class="highlight-text">{{ totalCount }}</span>个</span>
+    </template>
+  </div>
+</template>
+
+<script>
+import NumberInput from '@/views/create-order/ui/NumberInput.vue'
+export default {
+  name: 'AccountNumbers',
+  components: {
+    NumberInput
+  },
+  props: {
+    showFreeCount: {
+      type: Boolean,
+      default: false
+    },
+    total: {
+      type: Number,
+      default: 10,
+    },
+    placeholder: {
+      type: String,
+      default: '请输入账号数量'
+    },
+  },
+  data() {
+    return {
+      payCount: '',
+      freeCount: ''
+    }
+  },
+  computed: {
+    totalCount() {
+      return Number(this.payCount) + Number(this.freeCount)
+    }
+  },
+  methods: {
+    onInputChange() {
+      const payload = this.getState()
+      this.$emit('change', payload)
+    },
+    setState(e = {}) {
+      if (!e) return
+      const { payCount, freeCount } = e
+      this.payCount = payCount
+      this.freeCount = freeCount
+    },
+    getState() {
+      return {
+        payCount: this.payCount,
+        freeCount: this.freeCount,
+      }
+    },
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.account-numbers-container {
+  .number-input {
+    margin: 0 8px;
+    width: 128px;
+  }
+}
+</style>

+ 63 - 0
src/views/create-order/components/product-info-submodule/CheckboxGroup.vue

@@ -0,0 +1,63 @@
+<template>
+  <el-radio-group class="c-checkbox-group" :value="value" @input="onCheckboxInput">
+    <el-checkbox
+      v-for="item in options"
+      :disabled="item.disabled || disabled"
+      :key="item.value"
+      :label="item.value">
+      <span v-html="item.label"></span>
+    </el-checkbox>
+  </el-radio-group>
+</template>
+
+<script>
+export default {
+  name: 'CheckboxGroup',
+  props: {
+    value: {
+      type: Array,
+      default() {
+        return []
+      }
+    },
+    disabled: {
+      type: Boolean,
+      default: false
+    },
+    options: {
+      type: Array,
+      default() {
+        return [
+          // {
+          //   label:'label1',
+          //   value: 'value1',
+          // },
+          // {
+          //   label: 'label2',
+          //   value: 'value2',
+          // },
+        ]
+      }
+    },
+  },
+  methods: {
+    onCheckboxInput(e) {
+      this.$emit('input', e)
+    },
+    getState() {
+      return this.value
+    },
+    setState(e) {
+      this.$emit('input', e)
+    },
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+::v-deep {
+  .el-radio {
+    line-height: 36px;
+  }
+}
+</style>

+ 94 - 0
src/views/create-order/components/product-info-submodule/ContractAmount.vue

@@ -0,0 +1,94 @@
+<template>
+  <div class="contract-amount-container">
+    <template v-if="showContractMoney">
+      <number-input
+        :value="value"
+        @input="onInputChange"
+        :decimal="2"
+        class="number-input number-input-long"
+        placeholder="请填写合同金额"
+      ></number-input>
+      <span>元</span>
+    </template>
+    <span class="sub-info-container">
+      <span class="sub-info-item no-amount-money" v-if="noStandardMoney">该产品暂无法计算标准售价</span>
+      <span class="sub-info-item" v-else>标准售价:{{ formatMoney(standardMoney) }}</span>
+
+      <span class="sub-info-item">折扣率:{{ formatMoney(discountRate) }}</span>
+    </span>
+  </div>
+</template>
+
+<script>
+import NumberInput from '@/views/create-order/ui/NumberInput.vue'
+import { currencyFormat } from '@/utils/str'
+
+export default {
+  name: 'ContractAmount',
+  props: {
+    value: {
+      type: [String, Number],
+      default: '',
+    },
+    showContractMoney: {
+      type: Boolean,
+      default: false
+    },
+    noStandardMoney: {
+      type: Boolean,
+      default: false
+    },
+    standardMoney: {
+      type: [String, Number],
+      default: '11111',
+    },
+    discountRate: {
+      type: [String, Number],
+      default: '',
+    },
+  },
+  components: {
+    NumberInput
+  },
+  methods: {
+    onInputChange(e) {
+      this.$emit('input', e)
+    },
+    located(val) {
+      return currencyFormat(val)
+    },
+    formatMoney(val) {
+      val = val - 0
+      if (val && !isNaN(val)) {
+        return this.located(val)
+      } else {
+        return '-'
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.contract-amount-container {
+  .number-input {
+    margin-right: 8px;
+    width: 128px;
+  }
+  .number-input-long {
+    width: 300px;
+  }
+
+  .sub-info-container {
+    .sub-info-item {
+      margin-left: 80px;
+    }
+  }
+
+  .no-amount-money {
+    font-size: 14px;
+    line-height: 22px;
+    color: #F56500;
+  }
+}
+</style>

+ 286 - 0
src/views/create-order/components/product-info-submodule/ElOnlineContractForm.vue

@@ -0,0 +1,286 @@
+<template>
+  <el-form label-width="126px" class="online-contract-form-container">
+    <el-row :gutter="2">
+      <el-col :span="24">
+        <el-form-item label="协议备注" prop="e_contract_remark">
+          <el-input
+            :value="e_contract_remark"
+            size="medium"
+            maxlength="180"
+            type="textarea"
+            placeholder="请输入协议备注"
+            @input="inputChange($event, 'e_contract_remark')"
+            :autosize="{minRows: 2,maxRows: 5}"
+            :disabled="hasDisabled && disabledFiled && disabledFiled.indexOf('e_contract_remark') > -1"
+          ></el-input>
+        </el-form-item>
+      </el-col>
+    </el-row>
+
+    <el-row :gutter="2">
+      <el-col :span="12">
+        <el-form-item label="电子协议类型" prop="e_contract_type" required :show-message="false">
+          <RadioGroup
+            :value="e_contract_type"
+            @input="contractTypeChange"
+            :options="conf.contractTypeOptions"
+          />
+        </el-form-item>
+      </el-col>
+      <el-col :span="12">
+        <el-form-item label="协议甲方类型" prop="e_contract_userA_type" required :show-message="false" v-if="buySubject == '1'">
+          <RadioGroup
+            :value="e_contract_userA_type"
+            @input="contractUserATypeChange"
+            :options="conf.buySubjectOptions"
+          />
+        </el-form-item>
+      </el-col>
+    </el-row>
+
+    <el-row :gutter="2" v-if="contractUserANameShow" v-show="showMore">
+      <el-col :span="24">
+        <el-form-item label="协议甲方" prop="e_contract_userA_name">
+          <el-input
+            :value="e_contract_userA_name"
+            @input="inputChange($event, 'e_contract_userA_name')"
+            placeholder="请输入甲方名称"
+            maxlength="40"
+            size="medium"
+          ></el-input>
+        </el-form-item>
+      </el-col>
+    </el-row>
+
+    <el-row :gutter="2" v-show="showMore">
+      <el-col :span="12">
+        <el-form-item label="甲方联系人" prop="e_contract_userA_contacts_name" required :show-message="false">
+          <el-input
+            :value="e_contract_userA_contacts_name"
+            @input="inputChange($event, 'e_contract_userA_contacts_name')"
+            placeholder="请输入甲方联系人"
+            maxlength="40"
+            size="medium"
+          ></el-input>
+        </el-form-item>
+      </el-col>
+      <el-col :span="12">
+        <el-form-item label="甲方联系方式" prop="e_contract_userA_contacts_tel">
+          <el-input
+            :value="e_contract_userA_contacts_tel"
+            @input="inputChange($event, 'e_contract_userA_contacts_tel')"
+            placeholder="请输入甲方联系方式"
+            size="medium"
+            show-word-limit
+            maxlength="40"
+          ></el-input>
+        </el-form-item>
+      </el-col>
+    </el-row>
+
+    <el-row :gutter="2" v-show="showMore">
+      <el-col :span="24">
+        <el-form-item label="甲方联系地址" prop="e_contract_userA_contacts_address">
+          <el-input
+            :value="e_contract_userA_contacts_address"
+            @input="inputChange($event, 'e_contract_userA_contacts_address')"
+            placeholder="请输入甲方联系地址"
+            :autosize="{minRows: 2,maxRows: 5}"
+            size="medium"
+            maxlength="100"
+          ></el-input>
+        </el-form-item>
+      </el-col>
+    </el-row>
+
+    <el-row :gutter="2" v-show="showMore">
+      <el-col :span="24">
+        <el-form-item label="乙方联系人" required>
+          <el-input
+            :value="e_contract_userB_contacts_name"
+            @input="inputChange($event, 'e_contract_userB_contacts_name')"
+            placeholder="请输入乙方联系人"
+            size="medium"
+            maxlength="40"
+          ></el-input>
+        </el-form-item>
+      </el-col>
+    </el-row>
+  </el-form>
+</template>
+<script>
+import RadioGroup from './RadioGroup.vue'
+import { buySubjectOptions, eSignTypeOptions } from '@/views/create-order/data/index.js'
+
+export default {
+  name: 'OnlineContractForm',
+  components: {
+    RadioGroup
+  },
+  props: {
+    // 购买主体
+    buySubject: {
+      type: [String, Number],
+      default: '1' // '1'个人  '2'企业
+    },
+    // 订单公司名称
+    orderEntName: {
+      type: String,
+      default: ''
+    },
+    // 订单手机号
+    orderPhone: {
+      type: String,
+      default: '',
+    },
+    e_contract_type: {
+      type: [Number, String],
+      default: 1
+    },
+    e_contract_userA_type: {
+      type: [Number, String],
+      default: 1
+    },
+    e_contract_userA_name: {
+      type: String,
+      default: ''
+    },
+    e_contract_userA_contacts_name: {
+      type: String,
+      default: ''
+    },
+    e_contract_userA_contacts_tel: {
+      type: String,
+      default: ''
+    },
+    e_contract_userA_contacts_address: {
+      type: String,
+      default: ''
+    },
+    e_contract_userB_contacts_name: {
+      type: String,
+      default: ''
+    },
+    e_contract_remark: {
+      type: String,
+      default: ''
+    },
+    showMore:{
+      type: Boolean,
+      default: true
+    },
+    // 是否有需要禁止的字段
+    hasDisabled: {
+      type: Boolean,
+      default: false
+    },
+    // 禁止的字段有哪些
+    disabledFiled: {
+      type: Array,
+      default: () => {
+        return []
+      }
+    }
+  },
+  data () {
+    return {
+      conf: {
+        contractTypeOptions: eSignTypeOptions,
+        buySubjectOptions,
+      },
+      e_: {
+        contract_type: 1, // 电子协议类型:  1有电子章  2无电子章
+        contract_userA_type: 1, // 协议甲方类型: 1个人  2企业
+        contract_userA_name: '', // 协议甲方
+        contract_userA_contacts_name: '', // 协议甲方联系人
+        contract_userA_contacts_tel: '', // 协议甲方联系方式
+        contract_userA_contacts_address: '', // 协议甲方联系地址
+        contract_userB_contacts_name: '', // 协议乙方联系人
+        contract_remark: '', // 协议备注
+      }
+    }
+  },
+  computed: {
+      contractUserANameShow () {
+        // 如购买主体和甲方类型都为“个人”:则展示此字段。否则不展示,并且默认甲方名称为订单公司名称(此时订单公司名称必填)
+        if (this.buySubject == '1' && this.e_contract_userA_type == 1) {
+          return true
+        } else {
+          return false
+        }
+      }
+  },
+  watch: {
+      buySubject: {
+          immediate: false,
+          handler (n) {
+            this.contractUserATypeChange(n - 0)
+            if (n === '2') {
+              // 订单购买主体变为企业时候,需要同步企业名
+              this.syncEntName(this.orderEntName)
+            }
+          }
+      },
+      orderEntName: {
+          handler (n) {
+            if (!this.contractUserANameShow) {
+              if (n) {
+                this.inputChangeVal(n, 'e_contract_userA_name')
+              }
+            }
+          }
+      },
+      // contractUserANameShow为false时候,默认甲方名称为订单公司名称
+      contractUserANameShow: {
+        immediate: true,
+        handler (n) {
+          if (!n) {
+            this.syncEntName(this.orderEntName)
+          }
+        }
+      },
+
+  },
+  created () {
+    this.initData()
+  },
+  methods: {
+      initData () {
+          // 初始化默认值
+          if (this.orderEntName) {
+            this.inputChangeVal(this.orderEntName, 'e_contract_userA_name')
+          }
+          // 默认填写订单人的名字
+          if (!this.e_contract_userB_contacts_name) {
+            const defaultName = this.$store.getters.getAdminUser.username
+            this.inputChangeVal(defaultName, 'e_contract_userB_contacts_name')
+          }
+          if (this.buySubject == '2') {
+            // 如购买主体为“企业”. 协议甲方类型不展示,并同步默认值为“企业”;
+            this.contractUserATypeChange(this.buySubject - 0)
+            this.syncEntName(this.orderEntName)
+          }
+      },
+      syncEntName (entName) {
+        if (entName) {
+          this.inputChangeVal(entName, 'e_contract_userA_name')
+        }
+      },
+      contractTypeChange (val) {
+        this.$emit('update:e_contract_type', val)
+      },
+      contractUserATypeChange (val) {
+        this.$emit('update:e_contract_userA_type', val)
+      },
+      inputChange (value, key) {
+        this.$emit(`update:${key}`, value)
+      },
+      inputChangeVal (value, key) {
+        this.$emit(`update:${key}`, value)
+      },
+  }
+}
+</script>
+<style lang="scss" scoped>
+.online-contract-form-container {}
+</style>

+ 194 - 0
src/views/create-order/components/product-info-submodule/ProductTypeSelector.vue

@@ -0,0 +1,194 @@
+<template>
+  <div class="product-type-selector-container">
+    <el-form-item label="选择产品" required>
+      <el-cascader
+        :options="productTypeOptions"
+        class="el-cascader-w100"
+        :props="conf.cascaderProps"
+        v-model="productType1"
+        size="medium"
+        @change="firstTypeChange"
+        placeholder="请选择产品"
+        filterable
+        clearable></el-cascader>
+    </el-form-item>
+    <el-form-item label="产品类型" required v-if="activityMark">
+      <el-cascader
+        :options="activityTypeOptions"
+        class="el-cascader-w100"
+        :props="conf.cascaderProps"
+        v-model="productType2"
+        size="medium"
+        @change="secondTypeChange"
+        placeholder="请选择产品类型"
+        filterable
+        clearable></el-cascader>
+    </el-form-item>
+  </div>
+</template>
+
+<script>
+import { productTypeOptions2 } from '@/views/create-order/data/index.js'
+import { ActivityProductName } from '@/views/create-order/data'
+import { cloneDeep } from 'lodash'
+
+
+export default {
+  name: 'ProductTypeSelector',
+  props: {
+    value: {
+      type: [Object],
+      default() {
+        return {}
+      }
+    },
+    productTypeOptions: {
+      type: Array,
+      default() {
+        return [
+          // {
+          //   label: 'xx',
+          //   value: 'xx',
+          //   children: [],
+          // }
+        ]
+      }
+    },
+  },
+  watch: {
+    value: {
+      immediate: true,
+      deep: true,
+      handler(n) {
+        this.setState(n)
+      },
+    },
+  },
+  data() {
+    return {
+      conf: {
+        cascaderProps: {
+          // label: 'item_name',
+          // value: 'item_code',
+          multiple: false,
+        },
+        productTypeOptions2,
+      },
+      activityTypeOptions: [],
+      productType1: [],
+      productType2: [],
+    }
+  },
+  computed: {
+    productType1Item() {
+      return this.findProductWithNameAndCode(this.productType1)
+    },
+    productType2Item() {
+      return this.activityTypeOptions.find(a => a.code === this.productType2.join(''))
+    },
+    activityMark() {
+      if (this.productType1Item && this.productType1Item.activityMark) {
+        return true
+      } else {
+        return false
+      }
+    }
+  },
+  methods: {
+    onInput() {
+      const state = this.getState()
+      this.$emit('input', state)
+      this.onChange()
+    },
+    onChange() {
+      const p = {
+        info: cloneDeep(this.productType1Item),
+      }
+      this.$emit('change', p)
+    },
+    getState() {
+      const p = {}
+      if (this.activityMark) {
+        Object.assign(p, {
+          productCode: this.productType2[0],
+          activityCode: this.productType1[1],
+        })
+      } else {
+        Object.assign(p, {
+          productCode: this.productType1[1],
+        })
+      }
+      return p
+    },
+    setState(e) {
+      if (!e) return
+      const { activityCode, productCode } = e || {}
+      if (activityCode) {
+        this.productType1 = [ActivityProductName, activityCode]
+        // 商品id从活动商品中查找
+        this.$nextTick(() => {
+          this.activityTypeOptions = this.productType1Item.product_list
+          this.productType2 = [productCode]
+        })
+      } else {
+        let productGroupName = this.findProductGroupName(productCode)
+        if (productGroupName) {
+          this.productType1 = [productGroupName, productCode]
+        }
+      }
+      this.onChange()
+    },
+    findProductWithNameAndCode(e) {
+      const firstName = e[0]
+      const secondName = e[1]
+      const targetItem = this.productTypeOptions.find(item => item.value === firstName)
+      if (targetItem) {
+        return targetItem.children.find(oc => oc.code === secondName)
+      }
+    },
+    firstTypeChange() {
+      if (this.activityMark) {
+        // do something
+        if (this.productType1Item) {
+          this.activityTypeOptions = this.productType1Item.product_list
+        }
+      } else {
+        this.onInput()
+      }
+    },
+    secondTypeChange() {
+      this.onInput()
+    },
+    findProductGroupName(code) {
+      const target = this.findProductGroup(code)
+      if (target) {
+        return target.value
+      }
+    },
+    findProductGroup(code) {
+      // 从排除活动的列表中查找
+      const options = this.productTypeOptions.filter(p => !p.activityMark)
+      let target = null
+      for (let i = 0; i < options.length; i++) {
+        const opt = options[i]
+        if (Array.isArray(opt.children)) {
+          const t = opt.children.find(item => item.code === code)
+          if (t) {
+            target = opt
+            break
+          }
+        }
+      }
+      return target
+    },
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+::v-deep {
+  .el-cascader-w100 {
+    width: 100%;
+  }
+}
+</style>

+ 75 - 0
src/views/create-order/components/product-info-submodule/RadioGroup.vue

@@ -0,0 +1,75 @@
+<template>
+  <el-radio-group class="c-radio-group" :value="value" @input="onRadioInput">
+    <el-radio
+      v-for="item in options"
+      :disabled="item.disabled || disabled"
+      :key="item.value"
+      @click.native="handleRadioClick(item)"
+      :label="item.value">
+      <span v-html="item.label"></span>
+    </el-radio>
+  </el-radio-group>
+</template>
+
+<script>
+export default {
+  name: 'RadioGroup',
+  props: {
+    value: {
+      type: [String, Number],
+      default: ''
+    },
+    disabled: {
+      type: Boolean,
+      default: false
+    },
+    options: {
+      type: Array,
+      default() {
+        return [
+          // {
+          //   label:'label1',
+          //   value: 'value1',
+          // },
+          // {
+          //   label: 'label2',
+          //   value: 'value2',
+          // },
+        ]
+      }
+    },
+  },
+  methods: {
+    onRadioInput(e) {
+      this.$emit('input', e)
+    },
+    getState() {
+      return this.value
+    },
+    setState(e) {
+      this.$emit('input', e)
+    },
+    handleRadioClick(item) {
+      if (item.disabled) {
+        this.$message({
+          message: item.disabledToastText,
+          type: 'warning'
+        });
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+::v-deep {
+  .el-radio {
+    line-height: 36px;
+    &:focus:not(.is-focus):not(:active):not(.is-disabled) {
+      .el-radio__inner {
+        box-shadow: none;
+      }
+    }
+  }
+}
+</style>

+ 193 - 0
src/views/create-order/components/product-info-submodule/RelatedOrderTable.vue

@@ -0,0 +1,193 @@
+<template>
+  <!-- 关联订单表格 -->
+  <div class="relate-order-table">
+    <el-table
+      :data="tableData"
+      border
+      :row-key="getRowKey"
+      :span-method="objectSpanMethod"
+      highlight-current-row
+      @row-click="handleRowClick"
+      stripe>
+      <el-table-column
+        v-if="selection"
+        align="center"
+        label="请选择"
+      >
+        <template #default="scope">
+          <el-checkbox
+            :value="value.includes(scope.row[defaultKey])"
+            @change="handleRadioChange(scope.row)"
+          ></el-checkbox>
+        </template>
+      </el-table-column>
+      <el-table-column
+        prop="productTypeText"
+        header-align="center"
+        align="center"
+        label="产品类型及规格">
+      </el-table-column>
+      <el-table-column
+        prop="empowerCountText"
+        header-align="center"
+        align="center"
+        label="账号数量">
+        <template slot-scope="scope">
+          {{ scope.row.empowerCountText }}
+        </template>
+      </el-table-column>
+      <el-table-column
+        prop="serviceEndTimeText"
+        header-align="center"
+        align="center"
+        label="到期时间">
+      </el-table-column>
+      <el-table-column
+        prop="buySubjectText"
+        header-align="center"
+        align="center"
+        label="购买主体">
+      </el-table-column>
+
+      <el-table-column
+        prop="order_code"
+        header-align="center"
+        align="center"
+        label="订单编号">
+      </el-table-column>
+      <el-table-column
+        prop="service_type"
+        header-align="center"
+        align="center"
+        label="付费类型">
+      </el-table-column>
+      <el-table-column
+        prop="create_time"
+        header-align="center"
+        align="center"
+        label="创建时间">
+      </el-table-column>
+    </el-table>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'RelatedOrderTable',
+  props: {
+    defaultKey: {
+      type: String,
+      default: 'linkedId'
+    },
+    defaultSelected: {
+      type: Boolean,
+      default: false,
+    },
+    required: {
+      type: Boolean,
+      default: false,
+    },
+    selection: {
+      type: Boolean,
+      default: false,
+    },
+    value: {
+      type: Array,
+      default() {
+        return []
+      }
+    },
+    tableData: {
+      type: Array,
+      default() {
+        return []
+      }
+    }
+  },
+  watch: {
+    tableData: {
+      immediate: true,
+      handler() {
+        if (this.defaultSelected) {
+          this.initSelected()
+        }
+      }
+    }
+  },
+  methods: {
+    onInput(e) {
+      this.$emit('input', e)
+    },
+    getRowKey(row) {
+      return row?._linkedId || row[this.defaultKey]
+    },
+    initSelected() {
+      if (this.tableData.length === 1) {
+        // 默认勾选
+        const id = this.tableData[0][this.defaultKey]
+        this.onInput([id])
+      }
+    },
+    handleRowClick(row) {
+      this.handleRadioChange(row)
+    },
+    handleRadioChange(row) {
+      const id = row[this.defaultKey]
+      let selectedRowId = []
+      if (this.required) {
+        selectedRowId = [id]
+      } else {
+        if (this.value.includes(id)) {
+          selectedRowId = []
+        } else {
+          selectedRowId = [id]
+        }
+      }
+      this.onInput(selectedRowId)
+    },
+    objectSpanMethod({ row, column, rowIndex, columnIndex }) {
+      const needSpan = Array.isArray(row.linkedOrder) && row.linkedOrder.length > 1
+      const unSpanKey = ['order_code', 'service_type', 'create_time']
+      const notSpanColumn = unSpanKey.includes(column.property)
+      if (needSpan && !notSpanColumn) {
+        if (rowIndex % 2 === 0) {
+          return {
+            rowspan: 2,
+            colspan: 1
+          };
+        } else {
+          return {
+            rowspan: 0,
+            colspan: 0
+          };
+        }
+      }
+    },
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+::v-deep {
+  .el-table__body tr.current-row > td {
+    background-color: #eef1f6;
+  }
+  .el-table__row {
+    cursor: pointer;
+  }
+}
+.relate-order-table {
+  margin-top: 8px;
+  ::v-deep {
+    .el-table {
+      border-radius: 6px;
+    }
+    .el-table__header {
+      tr,
+      th.el-table__cell {
+        background: #f5f7fa;
+      }
+    }
+  }
+}
+</style>

+ 269 - 0
src/views/create-order/components/product-info-submodule/RelatedOrders.vue

@@ -0,0 +1,269 @@
+<template>
+  <!-- 关联订单 -->
+  <div class="related-orders-container">
+    <div class="relate-order-select">
+      <el-select
+        :value="linkedIdText"
+        disabled
+        class="selected-input"
+        placeholder="请选择关联订单">
+      </el-select>
+      <div class="selected-mask" @click="handleSelectClick"></div>
+    </div>
+    <div class="relate-order-table-container" v-if="selectedTableData.length > 0">
+      <RelatedOrderTable :tableData="selectedTableData"></RelatedOrderTable>
+    </div>
+    <el-dialog title="选择关联订单" :visible.sync="dialogTableVisible">
+      <RelatedOrderTable
+        selection
+        v-model="cacheLinkedIdArr"
+        :tableData="tableData"
+        :defaultSelected="required"
+        :required="required"
+      ></RelatedOrderTable>
+      <span slot="footer" class="dialog-footer">
+        <el-button @click="showTableDialog(false)">取 消</el-button>
+        <el-button type="primary" @click="onDialogConfirm">确 定</el-button>
+      </span>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import RelatedOrderTable from './RelatedOrderTable.vue'
+
+export default {
+  name: 'RelatedOrders',
+  components: {
+    RelatedOrderTable
+  },
+  props: {
+    required: {
+      type: Boolean,
+      default: false
+    },
+    options: {
+      type: Array,
+      default() {
+        return [
+          // {
+          //   buySubject: 1,
+          //   comboId: 0, // 套餐id
+          //   empowerCount: 2, // 人数
+          //   linkedId: '00xxx', // 关联id
+          //   name: 'cjdyxx',
+          //   provinceCount: 0, // 订阅省份
+          //   serviceStartTime: '2025-3-8', // 服务开始时间
+          //   serviceEndTime: '2025-4-8', // 结束时间
+          //   serviceList: [],
+          //   linkedOrder: [
+          //     {
+          //       order_code: 'xxxxxx1',
+          //       create_time: '2025-3-8',
+          //       service_type: '1',
+          //       order_detail_id: 'xxxxxx1',
+          //     },
+          //   ],
+          //   vipExist: false, //当前服务是否在有限期内
+          // },
+          // {
+          //   buySubject: 1,
+          //   comboId: 0, // 套餐id
+          //   empowerCount: 2, // 人数
+          //   linkedId: 'xxx12222', // 关联id
+          //   name: 'cjdyxx',
+          //   provinceCount: 0, // 订阅省份
+          //   serviceStartTime: '2025-3-8', // 服务开始时间
+          //   serviceEndTime: '2025-4-8', // 结束时间
+          //   serviceList: [],
+          //   linkedOrder: [
+          //     {
+          //       order_code: 'xxxxxx1',
+          //       create_time: '2025-3-8',
+          //       service_type: '1',
+          //       order_detail_id: 'xxxxxx1',
+          //     },
+          //   ],
+          //   vipExist: false, //当前服务是否在有限期内
+          // },
+          // {
+          //   buySubject: 1,
+          //   comboId: 0, // 套餐id
+          //   empowerCount: 2, // 人数
+          //   linkedId: 'zzz111', // 关联id
+          //   name: 'cjdy',
+          //   provinceCount: 0, // 订阅省份
+          //   serviceStartTime: '2025-3-8', // 服务开始时间
+          //   serviceEndTime: '2025-4-8', // 结束时间
+          //   serviceList: [],
+          //   linkedOrder: [
+          //     {
+          //       order_code: 'xxxxxx1',
+          //       create_time: '2025-3-8',
+          //       service_type: '1',
+          //       order_detail_id: 'xxxxxx1',
+          //     },
+          //     {
+          //       order_code: 'xxxxxx2',
+          //       create_time: '2025-3-8',
+          //       service_type: '1',
+          //       order_detail_id: 'xxxxxx2',
+          //     },
+          //   ],
+          //   vipExist: false, //当前服务是否在有限期内
+          // },
+          // {
+          //   buySubject: 2,
+          //   comboId: 1, // 套餐id
+          //   empowerCount: 3, // 人数
+          //   linkedId: 'ccc1211', // 关联id
+          //   name: 'dyh',
+          //   provinceCount: 0, // 订阅省份
+          //   serviceStartTime: '2025-3-8', // 服务开始时间
+          //   serviceEndTime: '2025-4-8', // 结束时间
+          //   serviceList: [],
+          //   linkedOrder: [
+          //     {
+          //       order_code: 'zz1',
+          //       create_time: '2025-3-8',
+          //       service_type: '1',
+          //       order_detail_id: 'zz1',
+          //     },
+          //     {
+          //       order_code: 'zz2',
+          //       create_time: '2025-3-8',
+          //       service_type: '1',
+          //       order_detail_id: 'zz3',
+          //     },
+          //   ],
+          //   vipExist: false, //当前服务是否在有限期内
+          // },
+        ]
+      }
+    },
+  },
+  data(){
+    return {
+      dialogTableVisible: false,
+      linkedIdArr: [],
+      cacheLinkedIdArr: [],
+    }
+  },
+  computed: {
+    // 对options展开
+    tableData() {
+      const arr = []
+      this.options.forEach(item => {
+        if (Array.isArray(item.linkedOrder) && item.linkedOrder.length > 0) {
+          item.linkedOrder.forEach((t, index) => {
+            arr.push({
+              ...item,
+              linkedId: item.linkedId,
+              _linkedId: `${item.linkedId}-${index}`,
+              linkedOrderSplit: t,
+            })
+          })
+        } else {
+          arr.push(item)
+        }
+      })
+      return arr.map(t => {
+        return {
+          ...t,
+          ...this.sortTableText(t),
+        }
+      })
+    },
+    selectedTableData() {
+      return this.tableData.filter(r => this.linkedIdArr.includes(r.linkedId))
+    },
+    linkedIdText() {
+      return this.selectedTableData.map(r => r.order_code).join(',') || '-'
+    },
+  },
+  methods: {
+    onInput(e) {
+      this.$emit('input', e)
+    },
+    getState() {
+      return JSON.parse(JSON.stringify(this.linkedIdArr))
+    },
+    setState(e) {
+      if (Array.isArray(e)) {
+        this.linkedIdArr = e
+        this.onInput(e)
+      }
+    },
+    showTableDialog(f = false) {
+      this.dialogTableVisible = f
+    },
+    handleSelectClick() {
+      if (this.tableData.length > 0) {
+        this.showTableDialog(true)
+      } else {
+        this.showAlert()
+      }
+    },
+    showAlert() {
+      this.$confirm('原因:该手机号或公司名称不存在“订单状态”为“已完成”的订单数据', '无可选择的关联订单', {
+        confirmButtonText: '我知道了',
+        showCancelButton: false,
+      })
+    },
+    sortTableText(service) {
+      const order = service.linkedOrderSplit || {}
+      return {
+        productTypeText: service.name || '-',
+        empowerCountText: service.empowerCount ? `${service.empowerCount}个` : '-',
+        serviceEndTimeText: service.serviceEndTime,
+        buySubjectText: service.buySubject === 1 ? '个人' : '企业',
+        phone: service.phone,
+        order_code: order.order_code || '-',
+        service_type: order.service_type || '-',
+        create_time: order.create_time || '-',
+      }
+    },
+    onDialogConfirm() {
+      this.showTableDialog(false)
+      this.linkedIdArr = this.cacheLinkedIdArr
+      this.onInput(this.linkedIdArr)
+      this.onConfirm()
+    },
+    onConfirm() {
+      const payload = {
+        selected: JSON.parse(JSON.stringify(this.selectedTableData)),
+      }
+      this.$emit('confirm', payload)
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+::v-deep {
+  .el-input.is-disabled {
+    .el-input__inner {
+      background-color: #fff;
+      border-color: #dcdfe6;
+      color: inherit;
+      cursor: pointer;
+      pointer-events: auto;
+    }
+  }
+}
+
+.relate-order-select {
+  position: relative;
+  display: inline-block;
+  z-index: 1;
+}
+.selected-mask {
+  position: absolute;
+  left: 0;
+  right: 0;
+  top: 0;
+  bottom: 0;
+  z-index: 2;
+  cursor: pointer;
+}
+</style>

+ 314 - 0
src/views/create-order/components/product-info-submodule/ServiceList.vue

@@ -0,0 +1,314 @@
+<template>
+  <div class="service-list-container">
+    <span v-html="onlyText" v-if="onlyText"></span>
+    <template v-else>
+      <div class="service-list-content">
+        <div class="service-list-item" v-for="service in serviceList" :key="service.value">
+          <div class="service-list-item-header">
+            <el-checkbox
+              v-model="service.checked"
+              :disabled="service.disabled"
+              :indeterminate="service.indeterminate"
+              @change="handleCheckAllChange($event, service)"
+            >{{ service.label }}</el-checkbox>
+          </div>
+          <div class="service-list-item-content">
+            <template v-if="Array.isArray(service.children) && service.children.length > 0">
+              <el-checkbox-group v-model="service.childrenValue" @change="handleChildrenCheckedChange($event, service)">
+                <el-checkbox v-for="sc in service.children" :label="sc.value" :key="sc.value">
+                  {{ sc.label }}
+                  <el-select
+                    v-if="Array.isArray(sc.options) && sc.options.length"
+                    v-model="sc.checked"
+                    class="el-select-w120"
+                    size="mini"
+                    :placeholder="'0' + service.optionsUnit">
+                    <el-option
+                      v-for="item in sc.options"
+                      :key="item.value"
+                      :label="item.label"
+                      :value="item.value">
+                    </el-option>
+                  </el-select>
+                </el-checkbox>
+              </el-checkbox-group>
+            </template>
+            <template v-else-if="service.childrenText">
+              {{ service.childrenText }}
+            </template>
+            <template v-else>
+              该配置未定义children或者childrenText
+            </template>
+          </div>
+        </div>
+      </div>
+    </template>
+  </div>
+</template>
+
+<script>
+import { selectorVModelMixin } from '@/utils/mixins/selector-v-model'
+export default {
+  name: 'ServiceList',
+  mixins: [selectorVModelMixin],
+  props: {
+    onlyText: {
+      type: String,
+      default: ''
+    },
+  },
+  data() {
+    return {
+      serviceList: [
+        {
+          label: '基础服务',
+          value: 'base',
+          disabled: true,
+          checked: true,
+          indeterminate: false,
+          childrenValue: [],
+          childrenText: '项目信息+招标采购信息+标讯服务(附件下载、查看原文链接等)+秘书服务(订阅等)+专家服务(招投标攻略)+企业管理',
+        },
+        {
+          label: '业务拓展',
+          value: 'yewu',
+          checked: false,
+          indeterminate: false,
+          optionsUnit: '个',
+          childrenValue: [],
+          children: [
+            {
+              label: '项目进度监控',
+              value: 'xmjdjk',
+              disabled: false,
+              checked: '',
+              options: [
+                {
+                  label: '100个',
+                  value: 100,
+                },
+                {
+                  label: '200个',
+                  value: 200,
+                },
+                {
+                  label: '300个',
+                  value: 300,
+                },
+              ]
+            },
+            {
+              label: '企业情报监控',
+              value: 'qyqbjk',
+              disabled: false,
+              checked: '',
+              options: [
+                {
+                  label: '100个',
+                  value: 100,
+                },
+                {
+                  label: '200个',
+                  value: 200,
+                },
+                {
+                  label: '300个',
+                  value: 300,
+                },
+              ]
+            },
+            {
+              label: '业主监控',
+              value: 'yzjk1',
+              disabled: false,
+              checked: '',
+              options: [
+                {
+                  label: '100个',
+                  value: 100,
+                },
+                {
+                  label: '200个',
+                  value: 200,
+                },
+                {
+                  label: '300个',
+                  value: 300,
+                },
+              ]
+            },
+            {
+              label: '潜在项目预测',
+              value: 'yzjk22',
+              disabled: false,
+              checked: '',
+            },
+            {
+              label: '向业主推荐/向业主唯一推荐(1次)',
+              value: 'yzjk2',
+              disabled: false,
+              checked: '',
+            },
+            {
+              label: '潜在客户挖掘',
+              value: 'yzjk3',
+              disabled: false,
+              checked: '',
+            },
+            {
+              label: '潜在竞争对手/合作伙伴挖掘',
+              value: 'yzjk4',
+              disabled: false,
+              checked: '',
+            },
+          ]
+        },
+        {
+          label: '营销分析',
+          value: 'yxfx',
+          disabled: false,
+          checked: false,
+          optionsUnit: '份',
+          indeterminate: false,
+          childrenValue: [],
+          children: [
+            {
+              label: '市场分析周报/月报',
+              value: 'xmjdjk',
+              disabled: false,
+              checked: '',
+            },
+            {
+              label: '市场分析定制报告,报告下载',
+              value: 'qyqbjk',
+              disabled: false,
+              checked: '',
+              options: [
+                {
+                  label: '100份',
+                  value: 100,
+                },
+                {
+                  label: '200份',
+                  value: 200,
+                },
+                {
+                  label: '300份',
+                  value: 300,
+                },
+              ]
+            },
+            {
+              label: '企业分析(企业画像,含企业通讯录),报告下载',
+              value: 'yzjk11',
+              disabled: false,
+              checked: '',
+              options: [
+                {
+                  label: '100份',
+                  value: 100,
+                },
+                {
+                  label: '200份',
+                  value: 200,
+                },
+                {
+                  label: '300份',
+                  value: 300,
+                },
+              ]
+            },
+            {
+              label: '业主分析(采购单位画像,含采购单位通讯录),报告下载',
+              value: 'yzjk3342',
+              disabled: false,
+              checked: '',
+              options: [
+                {
+                  label: '100份',
+                  value: 100,
+                },
+                {
+                  label: '200份',
+                  value: 200,
+                },
+                {
+                  label: '300份',
+                  value: 300,
+                },
+              ]
+            },
+            {
+              label: '投标决策分析',
+              value: 'tbjcfx22',
+              disabled: false,
+              checked: '',
+            },
+            {
+              label: '中标企业预测(200次)',
+              value: 'zbqyycv',
+              disabled: false,
+              checked: '',
+            },
+          ]
+        },
+      ]
+    }
+  },
+  created() {},
+  methods: {
+    handleCheckAllChange(value, service) {
+      if (value) {
+        service.childrenValue = service.children.map(item => item.value)
+      } else {
+        service.childrenValue = []
+      }
+      service.indeterminate = false
+    },
+    handleChildrenCheckedChange(value, service) {
+      const checkedCount = value.length
+      const checkAll = checkedCount === service.children.length
+      service.checked = checkAll
+      service.indeterminate = checkedCount > 0 && checkedCount < service.children.length
+    },
+    getState() {
+      const map = {}
+      this.serviceList.forEach(service => {
+        console.log(service)
+      })
+    },
+    setState() {},
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+::v-deep {
+  .el-select-w120.el-select {
+    width: 120px;
+  }
+}
+.service-list-container {
+  color: #1d1d1d;
+  font-size: 14px;
+  .service-list-content {
+    padding: 8px;
+  }
+  .service-list-item {
+    display: flex;
+    align-items: flex-start;
+    justify-content: space-between;
+    line-height: 28px;
+    &:not(:last-of-type) {
+      margin-bottom: 12px;
+    }
+    &-header {
+      width: 92px;
+      font-weight: bold;
+    }
+    &-content {
+      flex: 1;
+    }
+  }
+}
+</style>

+ 29 - 0
src/views/create-order/components/product-info-submodule/TextCard.vue

@@ -0,0 +1,29 @@
+<template>
+  <div class="text-card-container">
+    <span v-html="text"></span>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'TextCard',
+  props: {
+    text: {
+      type: String,
+      default: ''
+    },
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+.text-card-container {
+  color: #1d1d1d;
+  font-size: 14px;
+  ::v-deep {
+    .text-color-main {
+      color: $color_main;
+    }
+  }
+}
+</style>

+ 294 - 0
src/views/create-order/components/product-info-submodule/ValidityPeriod.vue

@@ -0,0 +1,294 @@
+<template>
+  <div class="validity-period-container">
+    <div class="validity-period-line">
+      <template v-if="amountText">
+        <span v-html="amountText"></span>
+      </template>
+      <template v-else>
+        <template v-if="showPayInput">
+          <span>付费</span>
+          <number-input
+            v-model.number="pay.count"
+            @input="onNumberInputChange('pay')"
+            class="number-input"
+            placeholder="请输入付费周期"
+            maxlength="3"
+          ></number-input>
+          <el-select
+            v-model="pay.unit"
+            :disabled="payUnitDisabled"
+            @change="onUnitChange('pay')"
+            class="unit-select">
+            <el-option
+              v-for="item in payUnitOptions"
+              size="medium"
+              :key="item.value"
+              :label="item.label"
+              :value="item.value">
+            </el-option>
+          </el-select>
+        </template>
+        <span v-if="showFreeInput && showPayInput">,</span>
+        <template v-if="showFreeInput">
+          <span>赠送</span>
+          <number-input
+            v-model.number="free.count"
+            @input="onNumberInputChange('free')"
+            class="number-input"
+            placeholder="请输入付费周期"
+            maxlength="3"
+          ></number-input>
+          <el-select
+            v-model="free.unit"
+            :disabled="freeUnitDisabled"
+            @change="onUnitChange('free')"
+            class="unit-select">
+            <el-option
+              v-for="item in freeUnitOptions"
+              size="medium"
+              :key="item.value"
+              :label="item.label"
+              :value="item.value">
+            </el-option>
+          </el-select>
+          <span>合计:<span class="highlight-text">{{ totalCountText }}</span></span>
+        </template>
+      </template>
+      <span class="action-container" v-if="showOpenOnPaymentDay">
+        <el-checkbox v-model="openOnPaymentDay" @change="onNumberInputChange" size="medium">全额回款当日开通</el-checkbox>
+      </span>
+    </div>
+    <div class="validity-period-line validity-period-line-2" v-if="showButtonTip">
+      <span class="validity-tip-text">全额回款后将自动续费开通权益,如若回款时间晚于原服务到期时间,则开始日期为关联回款日期。</span>
+    </div>
+  </div>
+</template>
+
+<script>
+import NumberInput from '@/views/create-order/ui/NumberInput.vue'
+import { selectorVModelMixin } from '@/utils/mixins/selector-v-model'
+import { cloneDeep } from 'lodash'
+
+export default {
+  name: 'ValidityPeriod',
+  mixins: [selectorVModelMixin],
+  components: {
+    NumberInput
+  },
+  props: {
+    showPayUnit: {
+      type: Array,
+      default() {
+        return [
+          // '月',
+          // '日',
+        ]
+      }
+    },
+    showFreeUnit: {
+      type: Array,
+      default() {
+        return [
+          // '月',
+          // '日',
+        ]
+      }
+    },
+    sameUnit: {
+      type: Boolean,
+      default: false,
+    },
+    payUnitDisabled: {
+      type: Boolean,
+      default: false,
+    },
+    freeUnitDisabled: {
+      type: Boolean,
+      default: false,
+    },
+    showOpenOnPaymentDay: {
+      type: Boolean,
+      default: true,
+    },
+    showButtonTip: {
+      type: Boolean,
+      default: false,
+    },
+    showPayInput: {
+      type: Boolean,
+      default: true,
+    },
+    showFreeInput: {
+      type: Boolean,
+      default: true,
+    },
+    amountText: {
+      type: String,
+      default: '',
+    }
+  },
+  data() {
+    return {
+      conf: {
+        unitOptions: [
+          {
+            label: '日',
+            value: '日',
+            textValue: '天',
+            rank: 1,
+          },
+          {
+            label: '月',
+            value: '月',
+            textValue: '个月',
+            rank: 3,
+          },
+          {
+            label: '季',
+            value: '季',
+            textValue: '季',
+            rank: 6,
+          },
+          {
+            label: '年',
+            value: '年',
+            rank: 9,
+          },
+        ]
+      },
+      openOnPaymentDay: false,
+      pay: {
+        count: '',
+        unit: '月',
+      },
+      free: {
+        count: '',
+        unit: '日',
+      }
+    }
+  },
+  computed: {
+    payUnitOptions() {
+      return this.calcUnitOptions(this.showPayUnit)
+    },
+    freeUnitOptions() {
+      return this.calcUnitOptions(this.showFreeUnit)
+    },
+    unitTextArr() {
+      const opt = cloneDeep(this.conf.unitOptions)
+      return opt.map(p => p.value)
+    },
+    totalCountText() {
+      const payCount = Number(this.pay.count)
+      const payUnit = this.pay.unit
+      const freeCount = Number(this.free.count)
+      const freeUnit = this.free.unit
+
+      const payUnitIndex = this.unitTextArr.indexOf(payUnit)
+      const freeUnitIndex = this.unitTextArr.indexOf(freeUnit)
+
+
+      if (payUnitIndex === freeUnitIndex) {
+        if (payCount || freeCount) {
+          return `${payCount + freeCount}${payUnit}`
+        } else {
+          return '-'
+        }
+      } else if (payUnitIndex > freeUnitIndex) {
+        // 说明pay的单位大
+        return `${payCount}${payUnit}${freeCount}${freeUnit}`
+      } else {
+        return `${freeCount}${freeUnit}${payCount}${payUnit}`
+      }
+    }
+  },
+  watch: {
+    sameUnit: {
+      immediate: true,
+      handler(n) {
+        if (n) {
+          this.initSameUnit()
+        }
+      } 
+    }
+  },
+  methods: {
+    initSameUnit() {
+      this.free.unit = this.pay.unit
+    },
+    calcUnitOptions(limitArr = []) {
+      if (limitArr.length > 0) {
+        return this.conf.unitOptions.filter(r => limitArr.includes(r.value))
+      } else {
+        return this.conf.unitOptions
+      }
+    },
+    onNumberInputChange() {
+      const payload = this.getState()
+      this.$emit('change', payload)
+    },
+    onUnitChange(type) {
+      if (type === 'pay' && this.sameUnit) {
+        this.initSameUnit()
+      }
+    },
+    setState(e = {}) {
+      if (!e) return
+      const { openOnPaymentDay, payCount, payUnit, freeCount, freeUnit } = e
+      this.openOnPaymentDay = !!openOnPaymentDay
+      if (payCount) {
+        this.pay.count = payCount
+      }
+      if (payUnit) {
+        this.pay.unit = payUnit
+      }
+      if (freeCount) {
+        this.free.count = freeCount
+      }
+      if (freeUnit) {
+        this.free.unit = freeUnit
+      }
+    },
+    getState() {
+      if (this.amountText) {
+        return {
+          openOnPaymentDay: this.openOnPaymentDay,
+        }
+      } else {
+        const params = {
+          openOnPaymentDay: this.openOnPaymentDay,
+          payCount: this.pay.count,
+          payUnit: this.pay.unit,
+          freeCount: this.free.count,
+          freeUnit: this.free.unit,
+        }
+        return params
+      }
+    },
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.validity-period-container {
+  .number-input {
+    margin: 0 8px;
+    width: 128px;
+  }
+  .unit-select {
+    margin-right: 8px;
+    width: 64px;
+  }
+  .action-container{
+    margin-left: 10px;
+  }
+
+  .validity-period-line-2 {
+    line-height: 22px;
+  }
+  .validity-tip-text {
+    color: #F56C6C;
+    font-size: 12px;
+  }
+}
+</style>

+ 262 - 0
src/views/create-order/components/product-info-submodule/select-tree.vue

@@ -0,0 +1,262 @@
+<template>
+  <div class="el-select-tree">
+    <el-select
+      :value="tagLabel"
+      @input="onSelectValueInput"
+      :multiple="multiple"
+      :popper-append-to-body="false"
+      size="medium"
+      :placeholder="placeholder">
+      <template #empty>
+        <div class="el-select-tree-content">
+          <div class="el-select-tree-hd">
+            <el-input
+              prefix-icon="el-icon-search"
+              size="small"
+              v-model="searchValue"
+              placeholder="搜索"
+            ></el-input>
+          </div>
+          <div class="el-select-tree-bd">
+            <el-tree
+              :data="options"
+              show-checkbox
+              default-expand-all
+              check-on-click-node
+              :default-checked-keys="value"
+              :node-key="treeNodeKey"
+              ref="tree"
+              highlight-current
+              @check-change="treeChange"
+              :filter-node-method="filterNode"
+              :props="defaultProps">
+            </el-tree>
+          </div>
+        </div>
+      </template>
+    </el-select>
+  </div>
+</template>
+
+<script>
+import { debounce } from 'lodash'
+
+export default {
+  name: 'select-tree',
+  props: {
+    value: {
+      type: Array,
+      default() {
+        return []
+      }
+    },
+    multiple: {
+      type: Boolean,
+      default: true
+    },
+    placeholder: {
+      type: String,
+      default: '请选择',
+    },
+    treeNodeKey: {
+      type: String,
+      default: 'value'
+    },
+    options: {
+      type: Array,
+      default() {
+        return [
+          // {
+          //   label: '一级 1',
+          //   value: '1',
+          //   children: [
+          //     {
+          //       label: '二级 1-1',
+          //       value: '1-1',
+          //       children: [
+          //         {
+          //           label: '三级 1-1-1',
+          //           value: '1-1-1'
+          //         },
+          //         {
+          //           label: '三级 1-1-2',
+          //           value: '1-1-2'
+          //         },
+          //       ]
+          //     },
+          //   ]
+          // },
+          // {
+          //   label: '一级 2',
+          //   value: '2',
+          //   children: [
+          //     {
+          //       label: '二级 2-1',
+          //       value: '2-1',
+          //     },
+          //     {
+          //       label: '二级 2-2',
+          //       value: '2-2',
+          //     },
+          //   ]
+          // },
+          // {
+          //   label: '一级 3',
+          //   value: '3',
+          //   children: [
+          //     {
+          //       label: '二级 3-1',
+          //       value: '3-1',
+          //     },
+          //     {
+          //       label: '二级 3-2',
+          //       value: '3-2',
+          //     },
+          //   ]
+          // },
+        ]
+      }
+    }
+  },
+  data() {
+    return {
+      defaultProps: {
+        children: 'children',
+        label: 'label'
+      },
+      tagList: [],
+      searchValue: '',
+    }
+  },
+  computed: {
+    deepestNodesArr() {
+      return this.findDeepestNodes(this.options)
+    },
+    tagLabel() {
+      return this.tagList.map(item => item[this.defaultProps.label])
+    },
+    tagValue() {
+      return this.tagList.map(item => item[this.treeNodeKey])
+    },
+  },
+  watch: {
+    value(n) {
+      this.setState(n)
+    },
+    searchValue(val) {
+      this.$refs.tree.filter(val)
+    },
+  },
+  methods: {
+
+    onInput(e) {
+      this.$emit('input', e)
+    },
+    onChange() {
+      const p = this.getState()
+      this.onInput(p)
+      this.$emit('change', p)
+    },
+    getState() {
+      const treeArr = this.getCheckedKeys()
+      const deepestKeysArr = this.deepestNodesArr.map(item => item[this.treeNodeKey])
+      const selectedDeepestKeysArr = this.getArrIntersection(treeArr, deepestKeysArr)
+      this.refreshTagValue(selectedDeepestKeysArr)
+      return selectedDeepestKeysArr
+    },
+    setState(arr = []) {
+      if (!Array.isArray(arr)) return
+      this.tagList = this.getArrItemWithValue(arr)
+      this.setCheckedKeys(arr)
+    },
+    refreshTagValue(selectArr = []) {
+      this.tagList = this.getArrItemWithValue(selectArr)
+    },
+    filterNode(value, data) {
+      if (!value) return true;
+      return data[this.defaultProps.label].indexOf(value) !== -1;
+    },
+    treeChange: debounce(function () {
+      this.$nextTick(() => {
+        this.onChange()
+      })
+    }, 200),
+    getCheckedKeys() {
+      return this.$refs.tree.getCheckedKeys()
+    },
+    setCheckedKeys(arr = []) {
+      this.$refs.tree.setCheckedKeys(arr)
+    },
+    resetChecked() {
+      this.setCheckedKeys()
+    },
+    onSelectValueInput(e) {
+      // label情况
+      const arrItems = this.getArrItemWithLabel(e)
+      const keys = arrItems.map(item => item[this.treeNodeKey])
+      this.setCheckedKeys(keys)
+      this.onChange()
+    },
+    findDeepestNodes(treeData = []) {
+      let deepestNodes = []
+      function findDeepestValues(node) {
+        if (!node.children || node.children.length === 0) {
+          return [node];
+        }
+        let nodes = [];
+        node.children.forEach(child => {
+          nodes = nodes.concat(findDeepestValues(child));
+        });
+        return nodes
+      }
+      treeData.forEach(node => {
+        deepestNodes = deepestNodes.concat(findDeepestValues(node));
+      });
+
+      return deepestNodes
+    },
+    getArrIntersection(arr1, arr2) {
+      return arr1.filter(value => arr2.includes(value));
+    },
+    getArrItemWithLabel(valueArr = []) {
+      return valueArr.map(v => {
+        return this.deepestNodesArr.find(item => item[this.defaultProps.label] === v)
+      }).filter(r => !!r)
+    },
+    getArrItemWithValue(valueArr = []) {
+      return valueArr.map(v => {
+        return this.deepestNodesArr.find(item => item[this.treeNodeKey] === v)
+      }).filter(r => !!r)
+    },
+    visibleChange(e) {
+      this.$emit('visible-change', e)
+    },
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.el-select-tree-content {
+  padding: 8px;
+}
+::v-deep {
+  .el-select-dropdown {
+    transition: none;
+  }
+}
+.el-select-tree-hd {
+  ::v-deep {
+    .el-input__inner {
+      border-left: 0;
+      border-right: 0;
+      border-top: 0;
+      border-radius: 0;
+    }
+  }
+}
+.el-select-tree-bd {
+  margin-top: 8px;
+  max-height: 260px;
+  overflow: auto;
+}
+</style>

+ 143 - 0
src/views/create-order/components/productInfoModule.vue

@@ -0,0 +1,143 @@
+<template>
+  <!-- 产品信息 -->
+  <div class="order-product-info-container">
+    <div class="product-info-content">
+      <ProductInfoCardList ref="productionInfoCardList" />
+    </div>
+    <div class="product-info-footer">
+      <el-form ref="form" :model="form" :rules="rules" label-width="126px" class="order-base-info-container">
+        <div class="desc-detail-info-list">
+          <div class="desc-detail-info-item">
+            <div class="desc-label">合同金额合计</div>
+            <div class="desc-value">{{ located(form.totalContractMoney) }}</div>
+          </div>
+          <div class="desc-detail-info-item">
+            <div class="desc-label">标准售价合计</div>
+            <div class="desc-value">{{ located(form.totalStandardMoney) }}</div>
+          </div>
+          <div class="desc-detail-info-item">
+            <div class="desc-label">折扣率</div>
+            <div class="desc-value">{{ form.rate }}</div>
+          </div>
+        </div>
+        <el-row :gutter="2">
+          <el-col :span="12">
+            <el-form-item label="渠道佣金" prop="channelCommission" :required="required.channelCommission">
+              <number-input
+                v-model="form.channelCommission"
+                placeholder="请填写合同金额"
+                :decimal="2"
+              >
+                <template #append>元</template>
+              </number-input>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-form-item label="0元订单类型" prop="orderMoney0Type" :required="required.orderMoney0Type">
+          <RadioGroup
+            v-model="form.orderMoney0Type"
+            :options="conf.orderMoney0TypeOptions"
+          />
+        </el-form-item>
+      </el-form>
+    </div>
+  </div>
+</template>
+
+<script>
+import { selectorVModelMixin } from '@/utils/mixins/selector-v-model'
+import ProductInfoCardList from './ProductInfoCardList.vue'
+import NumberInput from '@/views/create-order/ui/NumberInput.vue'
+import { orderMoney0TypeOptions } from '@/views/create-order/data/index.js'
+import { currencyFormat } from '@/utils/str'
+import RadioGroup from '@/views/create-order/components/product-info-submodule/RadioGroup'
+
+export default {
+  name: 'ProductInfoModule',
+  mixins: [selectorVModelMixin],
+  components: {
+    RadioGroup,
+    ProductInfoCardList,
+    NumberInput
+  },
+  data() {
+    return {
+      conf: {
+        orderMoney0TypeOptions,
+      },
+      required: {
+        channelCommission: false,
+        orderMoney0Type: true,
+      },
+      form: {
+        totalContractMoney: '3121231',
+        totalStandardMoney: '4513131',
+        rate: '111%',
+
+        channelCommission: '0',
+        orderMoney0Type: '1',
+      },
+    }
+  },
+  computed: {
+    rules() {
+      const channelCommissionRequired = this.required.channelCommission
+
+      return {
+        channelCommission: [
+          { required: channelCommissionRequired, message: '请输入渠道佣金金额', trigger: 'blur' }
+        ]
+      }
+    },
+  },
+  created() {
+    this.initDefaultProductType()
+  },
+  methods: {
+    located(val) {
+      return currencyFormat(val)
+    },
+    validate() {
+      return new Promise((resolve, reject) => {
+        this.$refs.form.validate((valid) => {
+          if (valid) {
+            resolve()
+          } else {
+            reject()
+          }
+        })
+      })
+    },
+    async validateChildren() {
+      const card = this.$refs.productionInfoCardList
+    },
+    getState() {},
+    setState() {},
+    initDefaultProductType() {},
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+::v-deep {
+  .el-input-group__append {
+    background: transparent;
+    border: none;
+    color: #1d1d1d;
+  }
+}
+.desc-detail-info-list,
+.desc-detail-info-item {
+  display: flex;
+  align-items: center;
+}
+.desc-detail-info-list {
+  margin-bottom: 12px;
+}
+.desc-detail-info-item {
+  margin-right: 24px;
+  .desc-label {
+    color: #686868;
+  }
+}
+</style>

+ 276 - 0
src/views/create-order/components/schema-form/products/common.js

@@ -0,0 +1,276 @@
+import RadioGroup from '@/views/create-order/components/product-info-submodule/RadioGroup'
+import CheckboxGroup from '@/views/create-order/components/product-info-submodule/CheckboxGroup'
+import RelatedOrders from '@/views/create-order/components/product-info-submodule/RelatedOrders'
+import TextCard from '@/views/create-order/components/product-info-submodule/TextCard'
+import AccountNumbers from '@/views/create-order/components/product-info-submodule/AccountNumbers'
+import ValidityPeriod from '@/views/create-order/components/product-info-submodule/ValidityPeriod'
+import ContractAmount from '@/views/create-order/components/product-info-submodule/ContractAmount'
+import ServiceList from '@/views/create-order/components/product-info-submodule/ServiceList'
+import {
+  schemaKeyMap,
+  upgradeContentOptions,
+} from '@/views/create-order/data'
+
+// 定义数据结构
+class SchemaItem {
+  label = ''
+  key = ''
+  className = ''
+  show = true
+
+  // component和slot二选一,component优先级高
+  component = {}
+  slot = ''
+  
+  defaultValue = ''
+  required = false
+  requiredMessage = ''
+  requiredTrigger = 'blur'
+
+  // 子组件传参
+  props = {}
+  hooks = {}
+
+  get requiredText() {
+    if (this.requiredMessage) {
+      return this.requiredMessage
+    } else {
+      return `${this.label}为必填项`
+    }
+  }
+}
+
+// 辅助函数,将数组或者对象添加到列表中
+export function connectArr(...argsArr) {
+  let arr = []
+  for (let i = 0; i < argsArr.length; i++) {
+    const item = argsArr[i]
+    if (Array.isArray(item)) {
+      arr = arr.concat(item)
+    } else {
+      arr.push(item)
+    }
+  }
+  return arr
+}
+
+// 定义生成函数
+export function createSchemaItem(conf) {
+  const s = new SchemaItem()
+  Object.assign(s, conf)
+  return s
+}
+
+// 通用销售策略规则生成
+// 1仅购买 2仅赠送 3购买+赠送
+export function createSaleGiftsSchema(lim, state) {
+  const options = [
+    {
+      label: '售卖',
+      value: 1,
+      disabled: false,
+      validate: [1, 3],
+      disabledToastText: '该产品不支持售卖',
+    },
+    {
+      label: '赠送',
+      value: 2,
+      disabled: false,
+      validate: [2, 3],
+      disabledToastText: '该产品不支持赠送',
+    },
+  ]
+
+
+  let validateArr = options.filter(i => i.validate.includes(lim))
+  if (validateArr.length === 0) {
+    validateArr = options
+  }
+
+  const key = 'saleGifts'
+  const defaultValue = validateArr[0].value
+  return createSchemaItem({
+    label: '销售策略',
+    key,
+    className: 'sale-gifts',
+    component: RadioGroup, // component和slot二选一,component优先级高
+    defaultValue: state[key] || defaultValue,
+    required: true,
+    props: {
+      options: validateArr
+    }
+  })
+}
+
+// 付费类型规则
+// 是否支持试用 1是 2否
+export function createPaymentTypeSchema(lim, state) {
+  const options = [
+    {
+      label: '购买',
+      value: 1,
+    },
+    {
+      label: '续费',
+      value: 2,
+      disabled: true,
+      disabledToastText: '该客户暂无相同产品类型订单,不支持续费',
+    },
+    {
+      label: '升级',
+      value: 3,
+      disabled: true,
+      disabledToastText: '该客户暂无相同产品类型订单,不支持升级',
+    },
+    {
+      label: '试用',
+      value: 4,
+      disabled: false,
+      hide: lim && lim.trial !== 1,
+    },
+  ]
+  const validateArr = options.filter(r => !r.hide)
+  const key = schemaKeyMap.payment
+  const defaultValue = validateArr[0].value
+  return createSchemaItem({
+    label: '付费类型',
+    key,
+    className: 'payment-type',
+    component: RadioGroup, // component和slot二选一,component优先级高
+    defaultValue: state[key] || defaultValue,
+    required: true,
+    props: {
+      options: validateArr,
+    }
+  })
+}
+
+// 通用关联订单规则生成
+export function createRelatedOrdersSchema() {
+  return createSchemaItem({
+    label: '关联订单',
+    key: 'relatedOrders',
+    className: 'related-orders',
+    component: RelatedOrders,
+    showMessage: false,
+    props: {
+      options: undefined
+    }
+  })
+}
+
+// 升级内容
+export function createUpgradeContentSchema(state) {
+  const options = upgradeContentOptions
+  const validateArr = options.filter(r => !r.hide)
+  const key = schemaKeyMap.upgradeContent
+  const defaultValue = state[key] || []
+
+  return createSchemaItem({
+    label: '升级内容',
+    key,
+    className: 'upgrade-content',
+    component: CheckboxGroup,
+    showMessage: false,
+    required: true,
+    defaultValue,
+    props: {
+      options: validateArr,
+    }
+  })
+}
+
+
+// 账号数量
+export function createAccountSchema() {
+  return createSchemaItem({
+    label: '账号数量',
+    key: 'accountNumbers',
+    className: 'account-numbers',
+    component: AccountNumbers,
+    props: {
+      showGiftCount: true,
+    }
+  })
+}
+
+// 主子账号数量
+export function createMainSubAccountSchema() {
+  const subAccount = createSchemaItem({
+    label: '子账号数量',
+    key: 'subAccountNumbers',
+    className: 'sub-account-numbers',
+    component: AccountNumbers,
+    props: {
+      showFreeCount: true,
+    }
+  })
+  const mainAccount = createSchemaItem({
+    label: '主账号数量',
+    key: 'mainAccountNumbers',
+    className: 'main-account-numbers',
+    component: TextCard,
+    required: true,
+    props: {
+      text: `付费1个&nbsp;&nbsp;合计:<span class="text-color-main">1</span>个`
+    }
+  })
+  return [mainAccount, subAccount]
+}
+
+// 有效周期
+export function createValidityPeriodSchema() {
+  return createSchemaItem({
+    label: '有效周期',
+    key: 'validityPeriod',
+    className: 'validity-period',
+    component: ValidityPeriod,
+    required: true,
+    showMessage: false,
+    props: {
+      amountText: '',
+    }
+  })
+}
+
+// 合同金额
+export function createContractAmountSchema() {
+  return createSchemaItem({
+    label: '合同金额',
+    key: 'contractAmount',
+    className: 'contract-amount',
+    component: ContractAmount,
+    required: true,
+    props: {
+      showSubInfo: true,
+    }
+  })
+}
+
+// 服务列表
+export function createServiceListSchema() {
+  return createSchemaItem({
+    label: '服务列表',
+    key: 'ServiceList',
+    className: 'service-list',
+    component: ServiceList,
+    props: {}
+  })
+}
+
+// 通用模块生成
+export function createCommonSchemaList(conf = {}) {
+  const info = conf.info || {}
+  const state = conf.state || {}
+  return connectArr(
+    // 销售策略
+    createSaleGiftsSchema(info.tactics, state),
+    // 付费类型
+    createPaymentTypeSchema({ trial: info.trial }, state),
+    // 关联订单
+    createRelatedOrdersSchema(),
+    // 升级内容
+    createUpgradeContentSchema(state),
+  )
+}
+

+ 63 - 0
src/views/create-order/components/schema-form/products/svip.js

@@ -0,0 +1,63 @@
+import {
+  connectArr,
+  createSchemaItem,
+  createMainSubAccountSchema,
+  createValidityPeriodSchema,
+  createContractAmountSchema,
+  createCommonSchemaList,
+} from './common'
+import RadioGroup from '@/views/create-order/components/product-info-submodule/RadioGroup'
+import {
+  schemaKeyMap,
+} from '@/views/create-order/data'
+
+function createProductSpecificationSchema({ info, state }) {
+  let options = []
+  if (Array.isArray(info.product_list)) {
+    options = info.product_list.map(p => {
+      return {
+        ...p,
+        label: p.name,
+        value: p.code,
+        disabled: false,
+      }
+    })
+  }
+  const validateArr = options.filter(r => !r.hide)
+  const key = schemaKeyMap.specification
+  const defaultValue = validateArr[0].value
+
+  return createSchemaItem({
+    label: '产品规格',
+    key,
+    className: 'product-specification',
+    component: RadioGroup, // component和slot二选一,component优先级高
+    defaultValue: state[key] || defaultValue,
+    required: true,
+    props: {
+      options: validateArr,
+    }
+  })
+}
+
+
+export function createSvipSchemaList(conf) {
+  const info = conf.info || {}
+  const state = conf.state || {}
+  const beforeAreaChange = conf.beforeAreaChange || undefined
+
+  
+  let schema = connectArr(
+    // 销售策略、付费类型、关联订单
+    createCommonSchemaList({ info, state }),
+    // 商品规格
+    createProductSpecificationSchema({ info, state }),
+    // 主子账号
+    createMainSubAccountSchema(),
+    // 有效周期
+    createValidityPeriodSchema(),
+    // 合同金额
+    createContractAmountSchema(),
+  )
+  return schema
+}

+ 472 - 0
src/views/create-order/components/schema-form/schema-form.vue

@@ -0,0 +1,472 @@
+<template>
+  <div class="order-schema-form-container">
+    <SchemaFormRenderer
+      ref="schemaForm"
+      :schema="schema"
+      :state="value"
+      @input="onInput"
+      :rules="rules"
+    />
+  </div>
+</template>
+
+<script>
+import SchemaFormRenderer from '@/components/common/form-schema-renderer.vue'
+import { createSchema } from './schema'
+import { mapState, mapMutations } from 'vuex'
+import { schemaKeyMap } from '@/views/create-order/data'
+
+export default {
+  name: 'SchemaForm',
+  components: {
+    SchemaFormRenderer
+  },
+  props: {
+    productType: {
+      type: String,
+      default: 'cjdy'
+    },
+    productInfo: {
+      type: Object,
+      default() {
+        return {}
+      }
+    },
+    // 产品卡片的索引
+    index: {
+      type: [String, Number],
+      default: '0'
+    },
+    value: {
+      type: Object,
+      default() {
+        return {}
+      }
+    },
+  },
+  data() {
+    return {
+      schema: [
+        // {
+        //   label: '约定支付方式',
+        //   key: 'payWay',
+        //   className: 'pay-way-form-item',
+        //   component: {}, // component和slot二选一,component优先级高
+        //   slot: 'slotName',
+        //   required: false,
+        //   props: {},
+        //   hooks: {},
+        // }
+      ],
+      rules: {},
+      selectedRelatedOrder: {},
+    }
+  },
+  computed: {
+    ...mapState({
+      schemaKey: state => state.order.schemaKey,
+      pageForm: state => state.order.pageForm,
+      productInfoList: state => state.order.orderInfo.productInfoList,
+    }),
+    thisProductInfoListItem() {
+      return this.productInfoList[this.index]
+    }
+    // ...mapGetters(),
+  },
+  watch: {
+    productType: {
+      immediate: true,
+      handler(n) {
+        this.getOrderProductInfo(n)
+        this.beforeGetUserPower()
+      }
+    },
+    schemaKey() {
+      this.getOrderProductInfo(this.productType)
+      this.beforeGetUserPower()
+      setTimeout(() => {
+        this.$refs.schemaForm.clearValidate()
+      }, 0)
+    },
+  },
+  methods: {
+    ...mapMutations('order', [
+      'refreshOrderProductItemCard',
+      'removeOrderProductItem',
+    ]),
+    async validate() {
+      return this.$refs.schemaForm.validate()
+    },
+    onInput(e) {
+      this.$emit('input', e)
+      this.afterInput() // 用于动态修改用户限制选中项
+    },
+    onInitSchema() {
+      this.$emit('initd')
+    },
+    refreshValue(obj = {}) {
+      const r = Object.assign({}, this.value, obj)
+      this.onInput(r)
+    },
+    getOrderProductInfo(type) {
+      // rules和schema可以根据productType、productInfo、form生成
+      const { schemaList, value, rules } = createSchema({
+        type: type || this.productType,
+        info: this.productInfo,
+        state: this.value,
+        index: this.index,
+      })
+      this.schema = schemaList
+      this.rules = rules
+      this.onInput(value)
+      this.onInitSchema()
+    },
+    async beforeGetUserPower(ent = false) {
+      // 判断购买主体是企业还是个人
+      const { buySubject, accountTel, companyName } = this.pageForm
+      if (buySubject === 1) {
+        // 个人
+        if (accountTel) {
+          await this.getUserService()
+        }
+      } else if (buySubject === 2 && ent) {
+        if (accountTel && companyName) {
+          this.getUserService()
+        }
+      }
+      this.afterInput()
+    },
+    async getUserService() {
+      const payload = {
+        phone: this.pageForm.accountTel,
+        entName: this.pageForm.companyName,
+        buySubject: this.pageForm.buySubject + '',
+        productType: this.productType,
+      }
+      if (!payload.productType) return
+      const r = await this.$store.dispatch('order/getUserService', payload)
+      this.refreshOrderProductItemCard({
+        key: 'productUserService',
+        index: this.index,
+        data: r,
+      })
+      if (r.willEffect) {
+        this.$alert('已存在未生效订单', '提示', {
+          confirmButtonText: '确定',
+          callback: action => {
+            if (action === 'confirm') {
+              this.removeOrderProductItem(this.index)
+            }
+          }
+        })
+      }
+      return r
+    },
+    getSchemaItemWithKey(key) {
+      return this.schema.find(item => item.key === key)
+    },
+    // 处理并分发各个产品卡片操作的限制
+    afterInput() {
+      this.$nextTick(() => {
+        // this.changeSchemaCommon()
+        if (this.productType === 'cjdy') {
+          this.groupCjdySchema()
+        } else if (this.productInfo === 'dhy') {
+          this.changeDhySchema()
+        }
+      })
+      console.log(this.thisProductInfoListItem, this.schema)
+    },
+    // 工具:动态修改选中可用的类型
+    utilChangeAvailablePayment() {
+      const ma = this.getSchemaItemWithKey(schemaKeyMap.payment)
+      const options = ma.props.options
+      // 获取当前
+      const paymentValue = this.value[schemaKeyMap.payment]
+      let changed = false
+
+      const target = options.find(item => item.value === paymentValue)
+      if (target && !target.disabled) {
+        // do something
+      } else {
+        // 被禁用或者,不存在都需要修改选中
+        const enableArr = options.filter(r => !r.disabled)
+        if (enableArr.length > 0) {
+          const v = enableArr[0].value
+          this.refreshValue({ [schemaKeyMap.payment]: v })
+          changed = true
+        }
+      }
+      return changed
+    },
+    // 工具:检查是否可以升级,并动态禁用启用付费类型相关选项
+    utilCheckAndEnabledUpgradeRenew() {
+      let canRenewUpgrade = false
+      const ma = this.getSchemaItemWithKey(schemaKeyMap.payment)
+      if (this.thisProductInfoListItem?.productUserService) {
+        const us = this.thisProductInfoListItem.productUserService
+        if (Array.isArray(us.serviceArrMap) && us.serviceArrMap.length > 0) {
+          if (us.serviceArrMap[0].vipExist) {
+            // 可以续费
+            canRenewUpgrade = true
+          }
+        }
+      }
+
+      // 修改options
+      ma.props.options.forEach(item => {
+        // 续费或升级
+        if (item.value === 2 || item.value === 3) {
+          item.disabled = !canRenewUpgrade
+        }
+      })
+
+      // 如果被禁用的项被选中,则自动调整选中项
+      const changed = this.utilChangeAvailablePayment()
+      return {
+        canRenewUpgrade,
+        changed,
+      }
+    },
+    // 工具:检查当前是否是商品属性
+    utilCheckIsVipService() {
+      // 产品属性 1会员服务 2 资源包 3实物 4其他
+      return this.thisProductInfoListItem?.productCardInfo?.info?.attribute === 1
+    },
+    // 工具:检查当前属性是否是资源包
+    utilCheckIsSourcePack() {
+      // 产品属性 1会员服务 2 资源包 3实物 4其他
+      return this.thisProductInfoListItem?.productCardInfo?.info?.attribute === 2
+    },
+    // common: 通用修改付费类型相关逻辑
+    commonChangePaymentSchema() {
+      const { buySubject } = this.pageForm
+      // 1. 付费类型相关调整
+      // 根据UserService限制用户可以选择的付费类型
+      // const ma = this.getSchemaItemWithKey(schemaKeyMap.payment)
+      // 产品属性为会员服务且
+      if (this.utilCheckIsVipService()) {
+        if (buySubject === 1) {
+          // 1.1 个人: serviceArrMap不能位空,且,vipExist为true。此时可以续费(升级),否则不可以续费(升级)
+          this.utilCheckAndEnabledUpgradeRenew()
+        } else if (buySubject === 2) {
+          // 1.2 企业: 选择关联订单后再次查询。然后做1.1相关判断
+          // 此处判断在关联订单选择完成后判断
+        }
+      }
+    },
+    // common: 通用关联订单相关逻辑
+    commonChangeRelateOrderSchema() {
+      const { buySubject } = this.pageForm
+      // 付费类型为“续费”、“升级”则为必填
+      const relatedOrders = this.getSchemaItemWithKey('relatedOrders')
+      if (Array.isArray(this.thisProductInfoListItem?.productUserService?.serviceArrMap)) {
+        const opt = this.thisProductInfoListItem?.productUserService?.serviceArrMap || []
+        relatedOrders.props.options = opt.map(t => {
+          return {
+            ...t,
+            phone: this.pageForm.accountTel,
+          }
+        })
+      }
+      if (this.value[schemaKeyMap.payment] === 2 || this.value[schemaKeyMap.payment] === 3) {
+        // 续费或升级
+        relatedOrders.required = true
+      } else {
+        relatedOrders.required = false
+      }
+      relatedOrders.props.required = relatedOrders.required
+
+      this.$set(relatedOrders.hooks, 'confirm', async (p) => {
+        if (p.selected[0]) {
+          this.selectedRelatedOrder = p.selected[0]
+        }
+        // 企业下,根据选中的权益判断是否可以进行续费和升级
+        if (buySubject === 2) {
+          // 重新调用
+          await this.getUserService()
+          this.utilCheckAndEnabledUpgradeRenew()
+        }
+      })
+    },
+    // common: 升级内容
+    commonChangeUpgradeContentSchema() {
+      const { buySubject } = this.pageForm
+      // 付费类型为“续费”、“升级”则为必填
+      const uc = this.getSchemaItemWithKey(schemaKeyMap.upgradeContent)
+      // 1.展示:仅当产品属性为“会员服务”且付费类型为“升级”才展示,否则不展示;
+      uc.show = this.utilCheckIsVipService() && this.value[schemaKeyMap.payment] === 3
+      // 2.可选:增购子账号、补充服务(如若只有1个可选,则作为默认值);
+      if (uc.show) {
+        // 3.增购子账号,展示条件:仅当购买主体为“企业”或“产品类型”为“大会员”才展示,否则不展示。
+        const showValidator = buySubject === 2 || this.productType === 'dhy'
+        if (!showValidator) {
+          uc.props.options = uc.props.options.filter(c => !c.subCount)
+        }
+      }
+    },
+    // 账号数量、主账号/子账号数量
+    commonChangeAccountNumberSchema() {
+      const { buySubject } = this.pageForm
+      const { subAccountNumbers, [schemaKeyMap.payment]: payment } = this.value
+      const main = this.getSchemaItemWithKey('mainAccountNumbers')
+      const sub = this.getSchemaItemWithKey('subAccountNumbers')
+
+      // 1.子账号展示:满足以下任意条件即展示:
+      const hasAddSubAccount = true // 是否勾选了增购子账号
+      // (1)产品属性为“会员服务”且购买主体为“企业”且付费类型不是升级;
+      const entNotUpgrade = this.utilCheckIsVipService() && buySubject === 2 && payment !== 3
+      // (2)产品属性为“会员服务”且购买主体为“企业”且付费类型是升级且升级内容有“增购子账号”;
+      const entUpgradeAddSubAccount = this.utilCheckIsVipService() && buySubject === 2 && payment === 3 && hasAddSubAccount 
+      // (3)产品类型为大会员且付费类型不是升级;
+      const bigNotUpgrade = this.productType === 'dhy' && payment !== 3
+      // (4)产品类型为大会员且付费类型是升级且升级内容有“增购子账号”。
+      const bigNotUpgradeAddSubAccount = this.productType === 'dhy' && payment === 3 && hasAddSubAccount
+
+      const show = entNotUpgrade || entUpgradeAddSubAccount || bigNotUpgrade || bigNotUpgradeAddSubAccount
+      main.show = show
+      sub.show = show
+
+      // 2.账号展示规则
+      // (1)主账号默认为1个;
+      // (2)子账号付费:仅可输入≥1的正整数;如若未填写则提示“请输入付费账号数量”;
+      // (3)子账号赠送:非必填,仅可输入≥0的正整数;
+      // (4)合计=付费+赠送,如若未填写则按照0计算
+      // 特殊情况说明如下:付费类型为续费,则反显关联订单付费和赠送账号数量,不支持修改,历史数据0元订单,默认为赠送,非0元订单,都默认为付费。
+
+      // 计算主账号1个+子账号+赠送账号的和
+      const mainAccountCount = 1
+      let allTotal = 0
+      allTotal += mainAccountCount
+      if (subAccountNumbers) {
+        allTotal += subAccountNumbers.payCount
+        allTotal += subAccountNumbers.freeCount
+      }
+      main.props.text = `付费${mainAccountCount}个&nbsp;&nbsp;合计:<span class="text-color-main">${allTotal}</span>个`
+    },
+    // 服务周期
+    commonChangeValidityPeriod() {
+      const { saleGifts, [schemaKeyMap.payment]: payment } = this.value
+      const m = this.getSchemaItemWithKey('validityPeriod')
+
+      // 2.产品属性为“会员服务”:
+      if (this.utilCheckIsVipService()) {
+        m.show = true
+        // (1)销售策略为“售卖”:
+        if (saleGifts === 1) {
+          // 1)付费类型为购买、试用,展示如下图:
+          //  1 付费:文本框,非必填,仅可输入1和正整数,单位可选:月(初始值)、天
+          //  2 赠送:文本框,非必填,仅可输入0和正整数,单位同付费,不支持选择。
+          //  3  合计:付费+赠送,初始值为-,单位同付费,如若为0,则提示“请输入有效周期”;
+          this.$set(m.props, 'showPayUnit', ['月', '日'])
+          this.$set(m.props, 'freeUnitDisabled', true)
+          this.$set(m.props, 'showPayInput', true)
+          this.$set(m.props, 'sameUnit', true)
+          if (payment === 1 || payment === 4) {
+            // 购买、试用
+            // do something...
+            this.$set(m.props, 'showOpenOnPaymentDay', true)
+            this.$set(m.props, 'showButtonTip', false)
+          } else if (payment === 2) {
+            // 续费
+            // 不展示自动开通模块
+            this.$set(m.props, 'showOpenOnPaymentDay', false)
+            // 显示底部提示
+            this.$set(m.props, 'showButtonTip', true)
+          } else {
+            // 升级
+            m.show = false
+          }
+        } else if (saleGifts === 2) {
+          // 赠送
+          this.$set(m.props, 'showPayUnit', ['月', '日'])
+          this.$set(m.props, 'freeUnitDisabled', true)
+          this.$set(m.props, 'showPayInput', false)
+          // this.$set(m.props, 'sameUnit', true)
+          if (payment === 1 || payment === 4) {
+            // 购买、试用
+            // do something...
+            this.$set(m.props, 'showOpenOnPaymentDay', true)
+            this.$set(m.props, 'showButtonTip', false)
+          } else if (payment === 2) {
+            // 续费
+            // 不展示自动开通模块
+            this.$set(m.props, 'showOpenOnPaymentDay', false)
+            // 显示底部提示
+            this.$set(m.props, 'showButtonTip', true)
+          } else {
+            // 升级
+            m.show = false
+          }
+        }
+      } else if (this.utilCheckIsSourcePack()) {
+        // 【产品属性为资源包】且【该产品类型支持系统自动开通权限】
+        // 并且仅展示自动开通
+        // m.show = false // 有自动开通权限【展示】
+        this.$set(m.props, 'showPayInput', false)
+        this.$set(m.props, 'showFreeInput', false)
+        this.$set(m.props, 'showOpenOnPaymentDay', true)
+        this.$set(m.props, 'showButtonTip', false)
+      } else {
+        m.show = false
+      }
+    },
+    // 合同金额
+    commonChangeContractAmount() {
+      const { saleGifts } = this.value
+      const m = this.getSchemaItemWithKey('contractAmount')
+      if (saleGifts === 1) {
+        // 售卖
+        this.$set(m.props, 'showContractMoney', true)
+      } else if (saleGifts === 2) {
+        // 赠送
+        // 不展示合同金额
+        this.$set(m.props, 'showContractMoney', false)
+      }
+
+      // const price = this.getPrice()
+      // this.$set(m.props, 'noStandardMoney', price === -1)
+      // this.$set(m.props, 'standardMoney', price)
+
+      // const rate = discountRate()
+      // this.$set(m.props, 'discountRate', rate)
+    },
+    // 组合以及定制:超级订阅规则
+    groupCjdySchema() {
+      // 付费类型相关调整
+      this.commonChangePaymentSchema()
+      // 关联订单
+      this.commonChangeRelateOrderSchema()
+      // 升级内容
+      this.commonChangeUpgradeContentSchema()
+      // 产品规格
+      this.commonChangeSpecificationSchema()
+      this.cjdyChangeSpecificationSchema()
+      // 账号数量
+      this.commonChangeAccountNumberSchema()
+      // 服务周期
+      this.commonChangeValidityPeriod()
+      // 合同金额
+      this.commonChangeContractAmount()
+    },
+    commonChangeSpecificationSchema() {
+      const { buySubject } = this.pageForm
+      const { [schemaKeyMap.payment]: payment } = this.value
+      const ma = this.getSchemaItemWithKey(schemaKeyMap.payment)
+      // 1付费类型为“续费”,默认为关联订单的产品规格,不支持修改;
+      // 2付费类型为“升级”且“升级内容”仅为“增购子账号”,默认为关联订单的产品规格,其他产品规格不展示;
+      // 3付费类型为“升级”且“升级内容”有“补充服务”或为空,仅可选择不低于关联订单的产品规格(注:特殊处理,大会员自定义版默认展示),其他产品规格不展示。
+      ma.show = ma.props.options.length > 1
+    },
+    cjdyChangeSpecificationSchema() {
+      // const { buySubject } = this.pageForm
+      // const { [schemaKeyMap.payment]: payment } = this.value
+      // const ma = this.getSchemaItemWithKey(schemaKeyMap.payment)
+    },
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 76 - 0
src/views/create-order/components/schema-form/schema.js

@@ -0,0 +1,76 @@
+import { createSvipSchemaList } from './products/svip'
+
+// 生成规则
+export function createSchemaListWithType(conf = {}) {
+  const type = conf.type || []
+  let schemaList = []
+  if (!type) {
+    console.warn('未传入type')
+    return schemaList
+  }
+  if (type === 'cjdy') {
+    schemaList = createSvipSchemaList(conf)
+  }
+  return schemaList
+}
+
+// 根据规则生成默认值
+export function calcDefaultValueWithSchema(schema = []) {
+  if (!Array.isArray(schema)) return
+  const map = {}
+  schema.forEach(item => {
+    map[item.key] = item.defaultValue
+  })
+  return map
+}
+
+// 根据规则,生成rules
+export function calcRulesWithSchema(schema = []) {
+  if (!Array.isArray(schema)) return
+  const map = {}
+  schema.filter(r => r.required).forEach(item => {
+    const blur = {
+      required: item.required,
+      message: item.requiredText,
+      trigger: item.requiredTrigger,
+    }
+    const change = {
+      required: item.required,
+      message: item.requiredText,
+      trigger: 'change',
+    }
+    if (Array.isArray(map[item.key])) {
+      map[item.key].push(blur)
+      map[item.key].push(change)
+    } else {
+      map[item.key] = [blur, change]
+    }
+  })
+  return map
+}
+
+// 创建无限制规则
+export function createSchema(conf = {}) {
+  const type = conf.type || ''
+  const info = conf.info || {}
+  const state = conf.state || {}
+  const index = conf.index || '0'
+  const beforeAreaChange = conf.beforeAreaChange || undefined
+  const schemaList = createSchemaListWithType({
+    beforeAreaChange,
+    type,
+    info,
+    state,
+    index,
+  })
+  const defaultValue = calcDefaultValueWithSchema(schemaList)
+  const rules = calcRulesWithSchema(schemaList)
+  const value = Object.assign({}, defaultValue, state)
+
+  return {
+    rules,
+    schemaList,
+    value,
+    defaultValue,
+  }
+}

+ 2 - 0
src/views/create-order/data/index.js

@@ -0,0 +1,2 @@
+export * from './var'
+export * from './options'

+ 284 - 0
src/views/create-order/data/options.js

@@ -0,0 +1,284 @@
+// 购买主体备选项目
+export const buySubjectOptions = [
+  {
+    label: '个人',
+    value: 1
+  },
+  {
+    label: '企业',
+    value: 2
+  }
+]
+
+export const upgradeContentOptions = [
+  {
+    label: '补充服务',
+    value: 1,
+  },
+  {
+    label: '增购子账号',
+    value: 2,
+    subCount: true,
+  },
+]
+
+// 是否要签协议
+export const agreeStatusOptions = [
+  {
+    label: '签协议',
+    value: '1'
+  },
+  {
+    label: '不签协议',
+    value: '0'
+  },
+]
+
+// 电子协议类型
+export const eSignTypeOptions = [
+  {
+    label: '有电子章',
+    value: 1
+  },
+  {
+    label: '无电子章',
+    value: 2
+  }
+]
+
+// 产品类型
+export const productTypeOptions = [
+  {
+    label: '广告',
+    value: '1'
+  },
+  {
+    label: '结构化数据',
+    value: '2'
+  },
+  {
+    label: '企业商机管理',
+    value: '3'
+  },
+  {
+    label: '历史数据',
+    value: '4'
+  },
+  {
+    label: '超级订阅',
+    value: '5'
+  },
+  {
+    label: '线下课程培训',
+    value: '6'
+  },
+  {
+    label: '课程分销',
+    value: '7'
+  },
+  {
+    label: '标书制作',
+    value: '8'
+  },
+  {
+    label: '打赏',
+    value: '9'
+  },
+  {
+    label: '数据文件',
+    value: '11'
+  },
+  {
+    label: 'ISO体系认证',
+    value: '12'
+  },
+  {
+    label: '3A信用认证',
+    value: '13'
+  },
+  {
+    label: '权益码',
+    value: '14'
+  },
+]
+// 产品类型
+export const productTypeOptions2 = [
+  {
+    label: '常选产品',
+    value: '0',
+    children: [
+      {
+        label: '超级订阅',
+        value: '01',
+      },
+      {
+        label: '大会员',
+        value: '02',
+      },
+    ]
+  },
+  {
+    label: '通用产品',
+    value: '1',
+    children: [
+      {
+        label: '超级订阅',
+        value: '01',
+      },
+      {
+        label: '大会员',
+        value: '02',
+      },
+      {
+        label: '商机管理',
+        value: '03',
+      },
+      {
+        label: '阳光直采(供应)',
+        value: '04',
+      },
+      {
+        label: '分析下载报告',
+        value: '05',
+      },
+    ]
+  },
+  {
+    label: '数据产品',
+    value: '1',
+    children: [
+      {
+        label: '数据导出',
+        value: '04',
+      },
+      {
+        label: '企业数据导出',
+        value: '05',
+      },
+    ]
+  },
+]
+
+// 广告来源
+export const adFromOptions = [
+  {
+    label: '广告联盟',
+    value: '1'
+  },
+  {
+    label: '微信流量主',
+    value: '2'
+  },
+  {
+    label: '剑鱼广告位',
+    value: '3'
+  },
+]
+
+// 支付方式
+export const payWayOptions = [
+  {
+    label: '对公转账',
+    value: 'transferAccounts'
+  },
+  {
+    label: '微信',
+    value: 'wx'
+  },
+  {
+    label: '支付宝',
+    value: 'ali'
+  },
+]
+
+// 收费方式
+export const chargeStatusOptions = [
+  {
+    label: '免费',
+    value: '0'
+  },
+  {
+    label: '收费',
+    value: '1'
+  },
+]
+
+// 单位
+export const dateTimeUnitOptions = [
+  {
+    label: '年',
+    value: '1'
+  },
+  {
+    label: '月',
+    value: '2'
+  },
+  {
+    label: '天',
+    value: '3'
+  },
+  {
+    label: '季',
+    value: '4'
+  },
+]
+
+// 购买的数据类型
+export const buyDataTypeOptions = [
+  {
+    label: '增量数据',
+    value: '1'
+  },
+  {
+    label: '历史数据',
+    value: '2'
+  },
+  {
+    label: '增量+历史数据',
+    value: '3'
+  },
+]
+
+// 数据导出类型
+export const dataExportTypeOptions = [
+  {
+    label: '高级字段包',
+    value: '1'
+  },
+  {
+    label: '标准字段包',
+    value: '2'
+  },
+  {
+    label: '自定义字段包',
+    value: '3'
+  },
+]
+
+// 0元订单类型
+export const orderMoney0TypeOptions = [
+  {
+    label: '赠送',
+    value: '1'
+  },
+  {
+    label: '分期付款权益补充',
+    value: '2'
+  },
+  {
+    label: '原订单不支持开通多项权益',
+    value: '3'
+  },
+]
+
+// 签约主体
+export const signUnitOptions = [
+  {
+    label: '北京剑鱼信息技术有限公司',
+    value: 'h01',
+  },
+  {
+    label: '北京拓普丰联信息科技股份有限公司',
+    value: 'h02',
+  },
+]

+ 7 - 0
src/views/create-order/data/var.js

@@ -0,0 +1,7 @@
+export const ActivityProductName = '活动产品'
+
+export const schemaKeyMap = {
+  payment: 'paymentType',
+  upgradeContent: 'upgradeContent',
+  specification: 'productSpecification',
+}

+ 17 - 0
src/views/create-order/hooks/index.js

@@ -0,0 +1,17 @@
+// productList中找到指定id的商品配置
+export function findProductInThreeLevel(pList, cb) {
+  let productArr = []
+  pList.forEach(p => {
+    if (Array.isArray(p.children) && p.children.length) {
+      productArr = productArr.concat(p.children)
+    }
+  })
+  const target = productArr.find(p => {
+    if (Array.isArray(p.product_list) && p.product_list.length > 0) {
+      return p.product_list.find(pl => cb(pl))
+    } else {
+      return false
+    }
+  })
+  return target
+}

+ 28 - 0
src/views/create-order/index.vue

@@ -0,0 +1,28 @@
+<template>
+  <div class="page-container">
+    <h1>创建订单</h1>
+    <div class="create-order-container">
+      <CreateOrder />
+    </div>
+  </div>
+</template>
+
+<script>
+import CreateOrder from './components/create.vue'
+
+export default {
+  name: 'CreateOrderIndex',
+  components: {
+    CreateOrder
+  },
+  created() {},
+}
+</script>
+
+<style lang="scss" scoped>
+.page-container {
+  background-color: #fff;
+  padding: 8px;
+  border-radius: 8px;
+}
+</style>

+ 43 - 0
src/views/create-order/ui/ModuleCard.vue

@@ -0,0 +1,43 @@
+<template>
+  <div class="module-card">
+    <div class="module-card-header">
+      <slot name="header">
+        <h3 class="module-title">
+          <slot name="title">{{ title }}</slot>
+        </h3>
+      </slot>
+    </div>
+    <div class="module-content">
+      <slot name="default"></slot>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'ModuleCard',
+  props: {
+    title: {
+      type: String,
+      default: ''
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.module-card {
+  font-size: 14px;
+  &:not(:last-of-type) {
+    margin-bottom: 20px;
+  }
+}
+.module-title {
+  color: $main;
+  font-size: 14px;
+  line-height: 22px;
+}
+.module-content {
+  margin-top: 16px;
+}
+</style>

+ 87 - 0
src/views/create-order/ui/NumberInput.vue

@@ -0,0 +1,87 @@
+<template>
+  <el-input
+    class="number-input"
+    :value="value"
+    @input="handleInput"
+    :placeholder="placeholder"
+    type="text"
+    size="medium"
+    min="0"
+    :maxlength="maxlength"
+  >
+    <template #prepend>
+      <slot name="prepend"></slot>
+    </template>
+    <template #append>
+      <slot name="append"></slot>
+    </template>
+  </el-input>
+</template>
+
+<script>
+export default {
+  name: 'NumberInput',
+  props: {
+    value: {
+      type: [Number, String],
+      default: ''
+    },
+    // 保留n位小数
+    decimal: {
+      type: Number,
+      default: 0
+    },
+    placeholder: {
+      type: String,
+      default: '请输入'
+    },
+    maxlength: {
+      type: String,
+      default: '10'
+    }
+  },
+  methods: {
+    handleInput(val) {
+      val = val + ''
+      let newVal = ''
+      if (this.decimal <= 0) {
+        newVal = this.formatNumber(val)
+      } else {
+        newVal = this.formatFloat(val, this.decimal)
+      }
+      this.emitInput(newVal)
+    },
+    emitInput(e) {
+      this.$emit('input', e)
+    },
+    formatNumber(val) {
+      const reg = /^\d*$/;
+      if (reg.test(val)) {
+        return val
+      } else {
+        return val.replace(/[^\d]/g, '')
+      }
+    },
+    formatFloat(v, decimalPlaces) {
+      // 去掉非法字符(非数字和小数点)
+      let value = v.replace(/[^0-9.]/g, "");
+
+      // 如果有多个小数点,只保留第一个
+      value = value.replace(/\.{2,}/g, ".");
+
+      // 如果第一个字符是小数点,前面补0
+      if (value.startsWith(".")) {
+        value = "0" + value;
+      }
+
+      // 如果有小数点,限制小数位数
+      if (value.includes(".")) {
+        let [integerPart, decimalPart] = value.split(".");
+        decimalPart = decimalPart.slice(0, decimalPlaces);
+        value = integerPart + "." + decimalPart;
+      }
+      return value
+    },
+  }
+}
+</script>

+ 88 - 0
src/views/create-order/ui/ProductCard.vue

@@ -0,0 +1,88 @@
+<template>
+  <div class="product-card">
+    <div class="product-card-header">
+      <div class="product-header-left">
+        <h3 class="product-header-title">
+          <slot name="title">{{ title }}</slot>
+        </h3>
+        <span class="product-header-subtitle">
+          <slot name="subtitle">{{ subtitle }}</slot>
+        </span>
+      </div>
+      <div class="product-header-right product-header-actions">
+        <slot name="actions"></slot>
+      </div>
+    </div>
+    <div class="product-card-content">
+      <slot name="default"></slot>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'ProductCard',
+  props: {
+    title: {
+      type: String,
+      default: ''
+    },
+    subtitle: {
+      type: String,
+      default: ''
+    },
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.product-card {
+  font-size: 14px;
+  border: 1px solid #E0E0E0;
+  border-radius: 8px;
+  &:not(:last-of-type) {
+    margin-bottom: 20px;
+  }
+}
+.product-card-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  height: 40px;
+  .product-header-left {
+    display: flex;
+    align-items: center;
+    height: 100%;
+  }
+  .product-header-title {
+    display: flex;
+    align-items: center;
+    padding: 0 32px;
+    color: #fff;
+    font-size: 16px;
+    line-height: 24px;
+    white-space: nowrap;
+    height: 100%;
+    background-color: $main;
+    border-radius: 8px 0 8px 0;
+    position: relative;
+    left: -1px;
+    top: -1px;
+  }
+  .product-header-subtitle {
+    padding: 0 20px;
+    color: #F56500;
+    font-size: 14px;
+    line-height: 22px;
+  }
+  .product-header-actions {
+    display: flex;
+    align-items: center;
+    padding: 0 16px;
+  }
+}
+.product-card-content {
+  margin-top: 12px;
+  padding-right: 8px;
+}
+</style>