فهرست منبع

feat: 新增工作桌面首页

cuiyalong 3 سال پیش
والد
کامیت
e8dec4660b

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

@@ -12,3 +12,4 @@ export * from './svip'
 export * from './file'
 export * from './file'
 export * from './customer'
 export * from './customer'
 export * from './dataExport'
 export * from './dataExport'
+export * from './workspace'

+ 10 - 0
src/api/modules/user.js

@@ -116,3 +116,13 @@ export function latestNews () {
     method: 'post'
     method: 'post'
   })
   })
 }
 }
+
+export function readMark (data) {
+  data = qs.stringify(data)
+  return request({
+    baseURL: '/jymessageCenter',
+    url: '/markRead',
+    method: 'post',
+    data
+  })
+}

+ 21 - 0
src/api/modules/workspace.js

@@ -0,0 +1,21 @@
+import request from '@/api'
+// import qs from 'qs'
+
+// 商机管理 - 我的客户
+export function customerQuery (data) {
+  return request({
+    baseURL: '/entnicheNew',
+    url: '/customer/query',
+    method: 'post',
+    data
+  })
+}
+
+// 商机管理 - 客户关注列表
+export function entNewFollowClientList () {
+  return request({
+    baseURL: '/entnicheNew',
+    url: '/customer/list',
+    method: 'get'
+  })
+}

+ 69 - 44
src/components/common/Empty.vue

@@ -1,6 +1,6 @@
 <template>
 <template>
-  <div class="empty-container mtb60">
-    <div class="empty-content-position">
+  <div class="empty-container" :class="{ mtb60 }">
+    <div class="empty-content-position" :class="[directions]">
       <el-image :src="images"></el-image>
       <el-image :src="images"></el-image>
       <div class="empty-main">
       <div class="empty-main">
         <slot name="default">{{ tip }}</slot>
         <slot name="default">{{ tip }}</slot>
@@ -22,6 +22,17 @@ export default {
       type: String,
       type: String,
       default: '这里什么也没有'
       default: '这里什么也没有'
     },
     },
+    direction: {
+      type: String,
+      default: 'column',
+      validator (v) {
+        return ['row', 'column'].includes(v)
+      }
+    },
+    mtb60: {
+      type: Boolean,
+      default: true
+    },
     images: {
     images: {
       type: String,
       type: String,
       default () {
       default () {
@@ -29,57 +40,71 @@ export default {
         return require('@/assets/images/empty/jy-back.png')
         return require('@/assets/images/empty/jy-back.png')
       }
       }
     }
     }
+  },
+  computed: {
+    // 对row、column包装,避免与bootstrap类名冲突
+    directions () {
+      return `v-${this.direction}`
+    }
   }
   }
 }
 }
 </script>
 </script>
 
 
 <style lang="scss" scoped>
 <style lang="scss" scoped>
-  .empty-container {
-    display: flex;
+.empty-container {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  flex: 1;
+  padding: 16px;
+  box-sizing: border-box;
+  &.mtb60 {
+    margin: 60px auto;
+  }
+}
+.empty-content-position {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  &.v-row {
+    flex-direction: row;
+  }
+  &.v-column {
     flex-direction: column;
     flex-direction: column;
-    align-items: center;
-    justify-content: center;
-    flex: 1;
-    padding: 16px;
-    box-sizing: border-box;
-    &.mtb60 {
-      margin: 60px auto;
-    }
-
-    .empty-content-position {
-      display: flex;
-      align-items: center;
-      flex-direction: column;
-      justify-content: center;
-    }
-
-    .mt50 {
-      margin-top: -50px;
-    }
-
     .empty-main {
     .empty-main {
-      @extend .empty-content-position;
-      font-family: Microsoft YaHei, Microsoft YaHei-Regular;
       margin-top: 13px;
       margin-top: 13px;
-      font-size: 14px;
-      font-weight: 400;
-      color: #999999;
-      line-height: 22px;
-      // 默认文字按钮组合样式
-      span + button {
-        margin-top: 32px;
-      }
-      ::v-deep.el-icon-plus {
-        font-weight: bold;
-      }
-      .el-button [class*=el-icon-]+span {
-        margin-left: 9px;
-      }
     }
     }
+  }
+}
 
 
-    .el-image {
-      width: 200px;
-      height: 200px;
-    }
+.mt50 {
+  margin-top: -50px;
+}
+
+.empty-main {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex-direction: column;
+  font-size: 14px;
+  color: #999;
+  line-height: 22px;
+  // 默认文字按钮组合样式
+  span + button {
+    margin-top: 32px;
+  }
+  ::v-deep.el-icon-plus {
+    font-weight: bold;
+  }
+  .el-button [class*=el-icon-]+span {
+    margin-left: 9px;
   }
   }
+}
+
+.el-image {
+  flex-shrink: 0;
+  width: 200px;
+  height: 200px;
+}
 </style>
 </style>

+ 4 - 2
src/router/router.js

@@ -1,6 +1,7 @@
 import Vue from 'vue'
 import Vue from 'vue'
 import VueRouter from 'vue-router'
 import VueRouter from 'vue-router'
 import routers from './routers'
 import routers from './routers'
+import workspace from './workspace'
 import vipRouters from './svip-routers'
 import vipRouters from './svip-routers'
 
 
 if (process.env.NODE_ENV !== 'production') {
 if (process.env.NODE_ENV !== 'production') {
@@ -12,6 +13,7 @@ const router = new VueRouter({
   base: window.__RouterBase || process.env.VUE_APP_BASE_URL,
   base: window.__RouterBase || process.env.VUE_APP_BASE_URL,
   routes: [
   routes: [
     ...routers,
     ...routers,
+    ...workspace,
     ...vipRouters,
     ...vipRouters,
     {
     {
       path: '*',
       path: '*',
@@ -34,8 +36,8 @@ router.beforeEach((to, from, next) => {
   next()
   next()
 })
 })
 
 
-const originalPush = Router.prototype.push
-Router.prototype.push = function push (location) {
+const originalPush = VueRouter.prototype.push
+VueRouter.prototype.push = function push (location) {
   return originalPush.call(this, location).catch(err => err)
   return originalPush.call(this, location).catch(err => err)
 }
 }
 
 

+ 9 - 0
src/router/workspace.js

@@ -0,0 +1,9 @@
+// 工作桌面
+export default [
+  // 工作桌面首页
+  {
+    path: '/workspace/dashboard',
+    name: 'workspace_dashboard',
+    component: () => import('@/views/workspace/dashboard.vue')
+  }
+]

+ 2 - 0
src/store/index.js

@@ -3,6 +3,7 @@ import Vuex from 'vuex'
 
 
 import user from './user'
 import user from './user'
 import forcast from './forcast'
 import forcast from './forcast'
+import workspace from './workspace'
 
 
 if (process.env.NODE_ENV !== 'production') {
 if (process.env.NODE_ENV !== 'production') {
   Vue.use(Vuex)
   Vue.use(Vuex)
@@ -17,6 +18,7 @@ export default new Vuex.Store({
   getters: {},
   getters: {},
   modules: {
   modules: {
     user,
     user,
+    workspace,
     forcast
     forcast
   }
   }
 })
 })

+ 10 - 1
src/store/user.js

@@ -123,5 +123,14 @@ export default {
       } catch (error) {}
       } catch (error) {}
     }
     }
   },
   },
-  getters: {}
+  getters: {
+    // 大会员权限
+    power: state => state.power,
+    // 是否是商机管理
+    entniche: state => state.info.entniche,
+    // 是否大会员
+    bigmember: state => state.info.memberStatus > 0,
+    // 是否是超级订阅
+    svip: state => state.info.vipStatus > 0
+  }
 }
 }

+ 22 - 0
src/store/workspace.js

@@ -0,0 +1,22 @@
+import commonUse from './workspace/common-use'
+import message from './workspace/message'
+import subscribe from './workspace/subscribe'
+import customer from './workspace/my-customer'
+import collections from './workspace/collections'
+import customerWatcher from './workspace/customer-watcher'
+
+export default {
+  namespaced: true,
+  state: () => ({}),
+  mutations: {},
+  actions: {},
+  getters: {},
+  modules: {
+    commonUse,
+    message,
+    subscribe,
+    customer,
+    collections,
+    customerWatcher
+  }
+}

+ 77 - 0
src/store/workspace/collections.js

@@ -0,0 +1,77 @@
+import { getEntCollectionList } from '@/api/modules'
+
+export default {
+  namespaced: true,
+  state: () => ({
+    params: {
+      buyerPhone: 0,
+      buyerclass: '',
+      label: '',
+      pagenum: 1,
+      pagesize: 5,
+      selectTime: '',
+      winnerPhone: 0
+    },
+    hasKey: false,
+    loading: true,
+    loaded: false,
+    list: []
+  }),
+  mutations: {
+    changeList (state, list = []) {
+      if (Array.isArray(list)) {
+        state.list = list
+      }
+    },
+    changeLoading (state, f = false) {
+      state.loading = f
+    },
+    changeLoaded (state, f = false) {
+      state.loaded = f
+    },
+    changeHasKeyState (state, f = false) {
+      state.hasKey = f
+    }
+  },
+  actions: {
+    async getList ({ state, dispatch }) {
+      const { params } = state
+      await dispatch('doRequest', {
+        ...params
+      })
+    },
+    async doRequest ({ commit }, payload) {
+      try {
+        commit('changeLoading', true)
+        commit('changeLoaded', false)
+        const { data = {}, error_code: code } = await getEntCollectionList(payload)
+        if (code === 0 && data && Array.isArray(data.res)) {
+          const list = data.res.map(v => {
+            // const visited = this.pathVisited(
+            //   this.createPathItem(
+            //     '/article/content/*.html',
+            //     `id=${v._id}`
+            //   )
+            // )
+            return {
+              ...v,
+              // visited,
+              _id: v._id,
+              title: v.title,
+              unread: false,
+              time: v.publishtime ? v.publishtime * 1000 : 0
+            }
+          })
+          commit('changeList', list)
+        }
+        commit('changeLoading', false)
+        commit('changeLoaded', true)
+        return data || []
+      } catch (error) {
+        commit('changeLoading', false)
+        commit('changeLoaded', true)
+        console.log(error)
+      }
+    }
+  }
+}

+ 88 - 0
src/store/workspace/common-use.js

@@ -0,0 +1,88 @@
+import { getAllFunctions, getCanUseFunctions, saveCommonFunctions } from '@/api/modules'
+
+export default {
+  namespaced: true,
+  state: () => ({
+    maxCount: 8,
+    dialogShow: false,
+    allFunctions: [], // 所有功能
+    commonList: [], // 常用功能
+    saveData: null // 要提交的数据
+  }),
+  mutations: {
+    changeDialogState (state, show) {
+      state.dialogShow = show
+    },
+    setAllFunctions (state, list = []) {
+      if (Array.isArray(list)) {
+        state.allFunctions = list
+      }
+    },
+    setCommonList (state, list = []) {
+      if (Array.isArray(list)) {
+        state.commonList = list
+      }
+    },
+    transferSave (state, data) {
+      state.saveData = data
+    }
+  },
+  actions: {
+    // 获取所有功能
+    async getAllFunctions ({ commit }) {
+      try {
+        // 平台参数 platform 平台:默认PC,微信:WX,app:APP, 可不传
+        const { data = [], error_code: code } = await getAllFunctions()
+        if (code === 0 && data) {
+          commit('setAllFunctions', data)
+        } else {
+          commit('setAllFunctions', [])
+        }
+        return data || []
+      } catch (error) {
+        return []
+      }
+    },
+    // 获取常用功能
+    async getCanUseFunctions ({ commit }) {
+      try {
+        const { data, error_code: code } = await getCanUseFunctions()
+        if (code === 0 && data && Array.isArray(data)) {
+          data.forEach(v => {
+            if (v.name.indexOf('/') > -1) {
+              v.name = v.name.replace('/', '/<br>')
+            }
+          })
+          commit('setCommonList', data)
+        } else {
+          commit('setCommonList', [])
+        }
+        return data || []
+      } catch (error) {
+        return []
+      }
+    },
+    // dialog 卡片组件保存按钮提交事件
+    async confirmSave ({ dispatch, commit, state }) {
+      if (!state.saveData) {
+        return commit('changeDialogState', false)
+      }
+      try {
+        const { data, error_code: code, error_msg: msg } = await saveCommonFunctions({
+          platform: 'PC',
+          names: state.saveData.toString()
+        })
+        if (code === 0 && data) {
+          dispatch('getCanUseFunctions')
+        } else {
+          throw new Error(msg)
+        }
+        commit('changeDialogState', false)
+      } catch (error) {
+        console.log(error)
+        commit('changeDialogState', false)
+      }
+    }
+  },
+  getters: {}
+}

+ 62 - 0
src/store/workspace/customer-watcher.js

@@ -0,0 +1,62 @@
+import { entNewFollowClientList } from '@/api/modules'
+
+export default {
+  namespaced: true,
+  state: () => ({
+    loading: true,
+    loaded: false,
+    list: []
+  }),
+  mutations: {
+    changeList (state, list = []) {
+      if (Array.isArray(list)) {
+        state.list = list
+      }
+    },
+    changeLoading (state, f = false) {
+      state.loading = f
+    },
+    changeLoaded (state, f = false) {
+      state.loaded = f
+    }
+  },
+  actions: {
+    async getList ({ state, dispatch }) {
+      const { params } = state
+      await dispatch('doRequest', params)
+    },
+    async doRequest ({ commit }, payload) {
+      try {
+        commit('changeLoading', true)
+        commit('changeLoaded', false)
+        const { data = {}, error_code: code } = await entNewFollowClientList(payload)
+        if (code === 0 && data && Array.isArray(data.list)) {
+          const list = data.list.slice(0, 5).map(v => {
+            // const visited = this.pathVisited(
+            //   this.createPathItem(
+            //     '/unit_portrayal/*',
+            //     `id=${v.name}
+            //   )
+            // )
+            return {
+              ...v,
+              // visited,
+              title: v.name,
+              unread: false,
+              time: v.followdate ? +new Date(v.followdate) : 0
+            }
+          })
+          commit('changeList', list)
+        }
+        commit('changeLoading', false)
+        commit('changeLoaded', true)
+        return data || []
+      } catch (error) {
+        commit('changeLoading', false)
+        commit('changeLoaded', true)
+        console.log(error)
+      }
+    }
+  },
+  getters: {}
+}

+ 84 - 0
src/store/workspace/message.js

@@ -0,0 +1,84 @@
+import { latestNews, readMark } from '@/api/modules'
+import { dateMatter } from '@/utils/'
+
+const messageTypeMap = {
+  1: '活动优惠',
+  2: '服务通知',
+  3: '订阅消息',
+  4: '项目动态 ',
+  5: '企业动态',
+  6: '分析报告 ',
+  7: '系统通知'
+}
+
+function messageTypeText (val) {
+  return messageTypeMap[val]
+}
+
+export default {
+  namespaced: true,
+  state: () => ({
+    loading: true,
+    loaded: false,
+    messageList: [],
+    unReadCount: 0
+  }),
+  mutations: {
+    changeUnReadCount (state, count) {
+      state.unReadCount = count
+    },
+    changeMessageList (state, list = []) {
+      if (Array.isArray(list)) {
+        state.messageList = list
+      }
+    },
+    changeLoading (state, f = false) {
+      state.loading = f
+    },
+    changeLoaded (state, f = false) {
+      state.loaded = f
+    }
+  },
+  actions: {
+    // 获取所有
+    async getMessageList ({ commit }) {
+      try {
+        commit('changeLoading', true)
+        commit('changeLoaded', false)
+        const { count: unReadCount = 0, data = [], status } = await latestNews()
+        if (status === 1) {
+          commit('changeUnReadCount', unReadCount)
+          if (Array.isArray(data)) {
+            data.forEach(v => {
+              v.messageType = messageTypeText(v.msg_type)
+              v.createTime = dateMatter(v.createtime)
+            })
+            commit('changeMessageList', data)
+          }
+        } else {
+          // commit('changeUnReadCount', 0)
+          // commit('changeMessageList', [])
+        }
+        commit('changeLoading', false)
+        commit('changeLoaded', true)
+        return data || []
+      } catch (error) {
+        commit('changeLoading', false)
+        commit('changeLoaded', true)
+        console.log(error)
+      }
+    },
+    // 标记已读
+    async remarkRead (_, payload) {
+      if (payload.isRead === 0) {
+        try {
+          payload.isRead = 1
+          await readMark({ msgId: payload.id, msgType: payload.msg_type })
+        } catch (error) {
+          console.log(error)
+        }
+      }
+    }
+  },
+  getters: {}
+}

+ 79 - 0
src/store/workspace/my-customer.js

@@ -0,0 +1,79 @@
+import { customerQuery } from '@/api/modules'
+
+export default {
+  namespaced: true,
+  state: () => ({
+    params: {
+      area: [],
+      business_scope: [],
+      end_time: 0,
+      followStatus: 999,
+      industry: '',
+      label: '',
+      manage: '0',
+      page_index: 1,
+      page_size: 10,
+      query_name: '',
+      reqAlloc: '',
+      sourceType: 999,
+      staff_names: '',
+      start_time: 0
+    },
+    loading: true,
+    loaded: false,
+    list: []
+  }),
+  mutations: {
+    changeList (state, list = []) {
+      if (Array.isArray(list)) {
+        state.list = list
+      }
+    },
+    changeLoading (state, f = false) {
+      state.loading = f
+    },
+    changeLoaded (state, f = false) {
+      state.loaded = f
+    }
+  },
+  actions: {
+    async getList ({ state, dispatch }) {
+      const { params } = state
+      await dispatch('doRequest', params)
+    },
+    async doRequest ({ commit }, payload) {
+      try {
+        commit('changeLoading', true)
+        commit('changeLoaded', false)
+        const { data = {}, error_code: code } = await customerQuery(payload)
+        if (code === 0 && data && Array.isArray(data.list)) {
+          const list = data.list.slice(0, 5).map(v => {
+            // const visited = this.pathVisited(
+            //   this.createPathItem(
+            //     '/myCustomer/*',
+            //     `id=${v.customer_id}
+            //   )
+            // )
+            return {
+              ...v,
+              // visited,
+              _id: v.customer_id,
+              title: v.customer_name,
+              unread: false,
+              time: 0
+            }
+          })
+          commit('changeList', list)
+        }
+        commit('changeLoading', false)
+        commit('changeLoaded', true)
+        return data || []
+      } catch (error) {
+        commit('changeLoading', false)
+        commit('changeLoaded', true)
+        console.log(error)
+      }
+    }
+  },
+  getters: {}
+}

+ 105 - 0
src/store/workspace/subscribe.js

@@ -0,0 +1,105 @@
+import { getPushList } from '@/api/modules'
+
+const vtMap = {
+  free: 'f',
+  entniche: 's',
+  svip: 'v',
+  bigmember: 'm'
+}
+
+export default {
+  namespaced: true,
+  state: () => ({
+    params: {
+      pageNum: 1,
+      pageSize: 5,
+      format: 'table',
+      area: '',
+      time: ''
+    },
+    hasKey: false,
+    loading: true,
+    loaded: false,
+    list: []
+  }),
+  mutations: {
+    changeList (state, list = []) {
+      if (Array.isArray(list)) {
+        state.list = list
+      }
+    },
+    changeLoading (state, f = false) {
+      state.loading = f
+    },
+    changeLoaded (state, f = false) {
+      state.loaded = f
+    },
+    changeHasKeyState (state, f = false) {
+      state.hasKey = f
+    }
+  },
+  actions: {
+    async getList ({ state, getters, dispatch }) {
+      const { params } = state
+      await dispatch('doRequest', {
+        ...params,
+        vt: getters.vt
+      })
+    },
+    async doRequest ({ commit }, payload) {
+      try {
+        commit('changeLoading', true)
+        commit('changeLoaded', false)
+        // 按照商机管理、大会员、超级订阅、免费显示
+        const { haskey: hasKey = true, data = [] } = await getPushList(payload)
+        if (Array.isArray(data)) {
+          const list = data.map(v => {
+            // const visited = this.pathVisited(
+            //   this.createPathItem(
+            //     '/article/content/*.html',
+            //     `id=${v._id}`
+            //   )
+            // )
+            return {
+              ...v,
+              // visited,
+              _id: v._id,
+              title: v.title,
+              unread: false,
+              time: v.publishtime ? v.publishtime * 1000 : 0
+            }
+          })
+          commit('changeHasKeyState', hasKey)
+          commit('changeList', list)
+        }
+        commit('changeLoading', false)
+        commit('changeLoaded', true)
+        return data || []
+      } catch (error) {
+        commit('changeLoading', false)
+        commit('changeLoaded', true)
+        console.log(error)
+      }
+    }
+  },
+  getters: {
+    // 免费f / 商机管理s / 超级订阅v / 大会员m
+    vt (state, getters, rootState, rootGetters) {
+      const {
+        'user/bigmember': bigmember,
+        'user/entniche': entniche,
+        'user/svip': svip
+      } = rootGetters
+
+      if (entniche) {
+        return vtMap.entniche
+      } else if (bigmember) {
+        return vtMap.bigmember
+      } else if (svip) {
+        return vtMap.svip
+      } else {
+        return vtMap.free
+      }
+    }
+  }
+}

+ 135 - 0
src/views/workspace/components/BusinessProfile.vue

@@ -0,0 +1,135 @@
+<template>
+  <WorkspaceCard title="核心数据" class="business-profile">
+    <div slot="header-right" class="desc">近7天(北京拓普风险信息工程有限公司-销售部-xxx)</div>
+    <div class="profile-content">
+      <div class="content-header">
+        <span class="title">商机过程管理</span>
+        <span class="more-data">更多分析<i class="el-icon-arrow-right"></i></span>
+      </div>
+      <div class="content-list">
+        <div class="list-item" v-for="item in 4" :key="item">
+          <span class="i-label">新增项目</span>
+          <span class="text-container">
+            <span class="i-count">10</span>
+            <span class="suffix-unit">个</span>
+          </span>
+        </div>
+      </div>
+    </div>
+  </WorkspaceCard>
+</template>
+
+<script>
+import { Icon } from 'element-ui'
+import WorkspaceCard from './WorkspaceCard'
+
+export default {
+  name: 'BusinessProfile',
+  components: {
+    [Icon.name]: Icon,
+    WorkspaceCard
+  },
+  data () {
+    return {
+      loading: true,
+      loaded: false
+    }
+  },
+  created () {},
+  methods: {}
+}
+</script>
+
+<style lang="scss" scoped>
+::v-deep {
+  .card-header {
+    justify-content: flex-start;
+  }
+}
+.blue {
+  background-color: #32C8FB;
+}
+.green {
+  background-color: #38D8BD;
+}
+.purple {
+  background-color: #9985FF;
+}
+.orange {
+  background-color: #FFB559;
+}
+.desc {
+  font-size: 12px;
+  color: #686868;
+  line-height: 18px;
+}
+.content-header {
+  margin-bottom: 16px;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  font-size: 14px;
+  line-height: 22px;
+  .title {
+    color: #1D1D1D;
+  }
+  .more-data {
+    color: #2CB7CA;
+    cursor: pointer;
+  }
+}
+.content-list {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  .list-item {
+    position: relative;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    flex: 1;
+    padding: 16px 0;
+    font-size: 14px;
+    line-height: 22px;
+    box-shadow: 2px 2px 8px 0px rgba(0,0,0,0.08);
+    border-radius: 4px;
+    &:not(:last-of-type) {
+      margin-right: 16px;
+    }
+    &::before {
+      content: '';
+      position: absolute;
+      left: 0;
+      top: 0;
+      bottom: 0;
+      width: 8px;
+      border-radius: 4px 0 0 4px;
+    }
+    &:nth-of-type(1)::before {
+      @extend .blue;
+    }
+    &:nth-of-type(2)::before {
+      @extend .green;
+    }
+    &:nth-of-type(3)::before {
+      @extend .purple;
+    }
+    &:nth-of-type(4)::before {
+      @extend .orange;
+    }
+
+    .i-label {
+      margin-bottom: 8px;
+      color: #686868;
+    }
+    .text-container {
+      color: #1D1D1D;
+    }
+    .i-count {
+      font-size: 36px;
+      line-height: 36px;
+    }
+  }
+}
+</style>

+ 180 - 0
src/views/workspace/components/CommonUse.vue

@@ -0,0 +1,180 @@
+<template>
+  <WorkspaceCard class="work-common" title="常用功能">
+    <span slot="header-right" class="header-right-set" @click="changeDialogState(true)"><i class="icon-set"></i> 设置</span>
+    <div class="common-lists">
+      <div class="list-item" v-for="(item, index) in commonList" :key="index" @click="openLink(item.url)">
+        <img class="item-img" :src="item.img" alt="常用功能">
+        <span v-html="item.name" class="item-name"></span>
+      </div>
+      <div v-if="commonList && commonList.length < maxCount" class="list-add" @click="changeDialogState(true)">
+        <span class="icon-add"></span>
+        <span class="add-text">添加常用功能</span>
+      </div>
+    </div>
+    <!-- 设置常用功能dialog -->
+    <el-dialog
+      custom-class="fn-dialog"
+      :visible.sync="dialogShow"
+      :close-on-click-modal="false"
+      :show-close="false"
+      v-if="dialogShow"
+      center
+      width="696px">
+      <SelectorCard @onCancel="changeDialogState(false)" @onConfirm="confirmSaveFn" confirmText="确定">
+        <div slot="header">常用功能设置</div>
+        <div class="transfer-content">
+          <Transfer :left="allFunctions" :right="commonList" @onSave="onTransferSave"></Transfer>
+        </div>
+        <p class="more-tips">最多可选择 <em style="color:#2CB7CA;">{{ maxCount }}</em> 个常用功能</p>
+      </SelectorCard>
+    </el-dialog>
+  </WorkspaceCard>
+</template>
+
+<script>
+import { mapState, mapMutations, mapActions } from 'vuex'
+import { Dialog } from 'element-ui'
+import WorkspaceCard from './WorkspaceCard'
+import SelectorCard from '@/components/selector/SelectorCard'
+import Transfer from '@/components/work-desktop/Transfer'
+
+export default {
+  name: 'CommonUse',
+  components: {
+    [Dialog.name]: Dialog,
+    WorkspaceCard,
+    SelectorCard,
+    Transfer
+  },
+  computed: {
+    ...mapState({
+      maxCount: state => state.workspace.commonUse.maxCount,
+      dialogShow: state => state.workspace.commonUse.dialogShow,
+      allFunctions: state => state.workspace.commonUse.allFunctions, // 所有功能
+      commonList: state => state.workspace.commonUse.commonList // 常用功能
+    })
+  },
+  created () {
+    this.getCanUseFunctions()
+    this.getAllFunctions()
+  },
+  methods: {
+    ...mapMutations('workspace/commonUse', [
+      'changeDialogState',
+      'transferSave',
+      'openLink'
+    ]),
+    ...mapActions('workspace/commonUse', [
+      'getAllFunctions',
+      'getCanUseFunctions',
+      'confirmSave'
+    ]),
+    // 穿梭框子组件传来的组件
+    onTransferSave (data) {
+      this.transferSave(data)
+    },
+    confirmSaveFn () {
+      try {
+        this.confirmSave()
+      } catch (error) {
+        this.$toast(error)
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+$main: #2cb7ca;
+
+::v-deep{
+  .fn-dialog{
+    .el-dialog__header,.el-dialog__body{
+      padding: 0;
+    }
+  }
+  .transfer-content{
+    display: flex;
+    align-items: center;
+    justify-content: center;
+  }
+  .selector-card-header{
+    margin: 0 0 28px!important;
+  }
+  .selector-card.s-card{
+    width: 100%;
+  }
+  .selector-card-content{
+    display: block!important;
+    padding: 0 30px;
+  }
+  .more-tips{
+    margin-top: 20px;
+    font-size: 14px;
+    line-height: 22px;
+    text-align: center;
+    color: #686868;
+  }
+}
+
+.icon-set{
+  display: inline-block;
+  width: 18px;
+  height: 18px;
+  margin-right: 6px;
+  background: url('~@/assets/images/icon/icon-set.png') no-repeat center center;
+  background-size: contain;
+}
+.icon-add{
+  display: inline-block;
+  width: 44px;
+  height: 44px;
+  background: url('~@/assets/images/icon/icon-add.png') no-repeat center center;
+  background-size: contain;
+}
+
+.header-right-set {
+  display: flex;
+  align-items: center;
+  color: $main;
+  font-size: 14px;
+  cursor: pointer;
+}
+.common-lists{
+  padding: 0 20px;
+  display: flex;
+  .list-item,
+  .list-add{
+    width: 120px;
+    height: 120px;
+    padding: 18px 0 24px;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    text-align: center;
+    cursor: pointer;
+  }
+  .list-item{
+    // flex: 1;
+    &:hover{
+      span{
+        color: $main;
+      }
+    }
+  }
+  .item-name,.add-text{
+    margin-top: 10px;
+    font-size: 14px;
+    line-height: 20px;
+    color: #1D1D1D;
+    white-space: nowrap;
+  }
+  .item-img{
+    width: 44px;
+    height: 44px;
+  }
+  .add-text{
+    color: #686868;
+  }
+}
+</style>

+ 53 - 0
src/views/workspace/components/CustomerWatcher.vue

@@ -0,0 +1,53 @@
+<template>
+  <!-- 商机管理我关注的客户 -->
+  <ListCard
+    class="customer-watcher"
+    :list="list"
+    title="客户监控"
+    @clickListItem="clickListItem"
+    @linkMore="linkMore"
+    :loading="loading"
+    :loaded="loaded">
+    <div slot="empty-content" class="empty-content">暂未关注任何客户</div>
+  </ListCard>
+</template>
+
+<script>
+import { mapState, mapActions } from 'vuex'
+import ListCard from './ListCard'
+
+export default {
+  name: 'CustomerWatcher',
+  components: {
+    ListCard
+  },
+  computed: {
+    ...mapState({
+      loading: state => state.workspace.customerWatcher.loading,
+      loaded: state => state.workspace.customerWatcher.loaded,
+      list: state => state.workspace.customerWatcher.list
+    })
+  },
+  created () {
+    this.getList()
+  },
+  methods: {
+    ...mapActions('workspace/customerWatcher', [
+      'getList'
+    ]),
+    clickListItem (item) {
+      // this.pathVisiting(
+      //   this.createPathItem(
+      //     '/unit_portrayal/*',
+      //     `id=${item.name}`
+      //   )
+      // )
+      const href = `/entpc/unit_portrayal/${item.name}`
+      window.open(href)
+    },
+    linkMore () {
+      window.open('/entpc/newBus/client_follow')
+    }
+  }
+}
+</script>

+ 182 - 0
src/views/workspace/components/ListCard.vue

@@ -0,0 +1,182 @@
+<template>
+  <WorkspaceCard :title="title" class="list-card">
+    <slot name="header-right">
+      <div slot="header-right" class="h-header-more" v-show="list.length !== 0" @click="linkMore">
+        <span class="more-text">更多</span>
+        <span class="el-icon-jy-blue-more"></span>
+      </div>
+    </slot>
+    <div class="list-container" v-loading="loading">
+      <div
+        class="list-item"
+        :class="{ visited: item.visited, last: index === list.length - 1 }"
+        v-for="(item, index) in list"
+        @click="clickListItem(item)"
+        :key="index">
+        <div class="list-item-l ellipsis visited-hd">{{ item.title }}</div>
+        <div class="list-item-r">
+          <span class="red-dot" v-if="item.unread && item.unread !== 0"></span>
+          <span class="r-time" v-if="item.time">{{ dateFormatter(item.time, 'yyyy-MM-dd') }}</span>
+        </div>
+      </div>
+      <div class="empty-wrapper" v-show="list.length === 0 && loaded">
+        <Empty class="empty-mini" direction="row" :mtb60="false">
+          <slot name="empty-content">暂无数据</slot>
+        </Empty>
+      </div>
+    </div>
+  </WorkspaceCard>
+</template>
+
+<script>
+import WorkspaceCard from './WorkspaceCard'
+import Empty from '@/components/common/Empty.vue'
+import { dateFormatter } from '@/utils/'
+
+export default {
+  name: 'ListCard',
+  components: {
+    Empty,
+    WorkspaceCard
+  },
+  props: {
+    title: {
+      type: String,
+      default: ''
+    },
+    loading: {
+      type: Boolean,
+      default: false
+    },
+    loaded: {
+      type: Boolean,
+      default: true
+    },
+    list: {
+      type: Array,
+      default () {
+        return [
+          // {
+          //   _id: '',
+          //   visited: false,
+          //   title: '',
+          //   unread: '',
+          //   time: 0
+          // }
+        ]
+      }
+    }
+  },
+  methods: {
+    dateFormatter,
+    linkMore () {
+      this.$emit('linkMore')
+    },
+    clickListItem (item) {
+      this.$emit('clickListItem', item)
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+@include diy-icon('blue-more', 16, 16);
+::v-deep {
+  .empty-button {
+    width: 108px;
+    height: 30px;
+    font-size: 14px;
+    line-height: 22px;
+    text-align: center;
+    color: #fff;
+    background-color: #2cb7ca;
+    border-radius: 4px;
+  }
+  .mt12 {
+    margin-top: 12px;
+  }
+}
+.list-card {
+  max-width: 490px;
+  ::v-deep {
+    .card-content {
+      padding-top: 8px;
+      padding-bottom: 8px;
+    }
+  }
+}
+
+.empty-tip{
+  color: #999;
+  font-size: 14px;
+  line-height: 22px;
+}
+
+.h-header-more {
+  display: flex;
+  align-items: center;
+  font-size: 14px;
+  color: #2cb7ca;
+  line-height: 22px;
+  cursor: pointer;
+  .more-text{
+    margin-right: 4px;
+  }
+}
+
+.list-container {
+  min-height: 246px;
+}
+
+.list-item {
+  padding: 12px 0;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  &:not(.last) {
+    box-shadow: 0px -1px 0px 0px rgba(0,0,0,0.05) inset;
+  }
+  .list-item-l {
+    margin-right: 12px;
+    font-size: 14px;
+    color: #1d1d1d;
+    line-height: 22px;
+    cursor: pointer;
+    &:hover {
+      color: #2cb7ca!important;
+    }
+  }
+  .list-item-r {
+    display: flex;
+    align-items: center;
+  }
+  .r-time {
+    margin-left: 6px;
+    font-size: 12px;
+    line-height: 20px;
+    color: #999;
+    text-align: right;
+    white-space: nowrap;
+  }
+}
+
+.empty-wrapper {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  min-height: inherit;
+}
+.empty-mini {
+  padding: 0;
+  ::v-deep {
+    .empty-content-position {
+      width: 100%;
+    }
+    .el-image {
+      margin-right: 16px;
+      width: 148px;
+      height: 148px;
+    }
+  }
+}
+</style>

+ 130 - 0
src/views/workspace/components/MessageTips.vue

@@ -0,0 +1,130 @@
+<template>
+  <WorkspaceCard class="message-card" title="我的消息" v-show="messageList.length">
+    <div slot="header-right" class="header-right-set" @click="toMessageCenter">更多<i class="icon el-icon-arrow-right"></i></div>
+    <div class="message-content message-list" v-loading="loading">
+      <div class="message-item" v-for="item in messageList" :key="item.id">
+        <div class="l-msg">
+          <i class="red-dot" :class="{ invisible: item.isRead !== 0 }"></i>
+          <!-- <h3 class="msg-type">{{item.msg_type}}</h3> -->
+          <span class="text ellipsis" @click="titleGoto(item)">{{item.title}}</span>
+        </div>
+        <p class="time">{{item.createTime}}</p>
+      </div>
+    </div>
+  </WorkspaceCard>
+</template>
+
+<script>
+import { mapState, mapActions } from 'vuex'
+import { Icon } from 'element-ui'
+import WorkspaceCard from './WorkspaceCard'
+
+export default {
+  name: 'MessageTips',
+  components: {
+    [Icon.name]: Icon,
+    WorkspaceCard
+  },
+  computed: {
+    emptyShow () {
+      return this.messageList.length === 0 && this.loaded
+    },
+    ...mapState({
+      loading: state => state.workspace.message.loading,
+      loaded: state => state.workspace.message.loaded,
+      unReadCount: state => state.workspace.message.unReadCount,
+      messageList: state => state.workspace.message.messageList
+    })
+  },
+  created () {
+    this.getList()
+  },
+  methods: {
+    ...mapActions('workspace/message', [
+      'getMessageList',
+      'remarkRead'
+    ]),
+    async getList () {
+      await this.getMessageList()
+    },
+    async titleGoto (item) {
+      try {
+        await this.remarkRead(item)
+        this.toMessageCenter()
+      } catch (error) {
+        this.toMessageCenter()
+      }
+    },
+    toMessageCenter () {
+      location.href = '/swordfish/frontPage/messageCenter/sess/index'
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+::v-deep{
+  .fn-dialog{
+    .el-dialog__header,
+    .el-dialog__body {
+      padding: 0;
+    }
+  }
+}
+
+.header-right-set {
+  display: flex;
+  align-items: center;
+  color: #2cb7ca;
+  font-size: 14px;
+  text-decoration: none;
+  cursor: pointer;
+  .icon {
+    margin-left: 4px;
+  }
+}
+
+.message-content {
+  min-height: 100px;
+}
+.message-item {
+  display: flex;
+  flex-direction: initial;
+  align-items: center;
+  justify-content: space-between;
+  &:not(:last-of-type) {
+    margin-bottom: 8px;
+  }
+
+  .l-msg {
+    display: flex;
+    align-items: center;
+    max-width: 65%;
+    .msg-type {
+      margin-left: 8px;
+      font-size: 16px;
+      font-weight: bold;
+      color: #2CB7CA;
+      line-height: 24px;
+      white-space: nowrap;
+    }
+    .text {
+      margin-left: 8px;
+      font-size: 14px;
+      color: #1D1D1D;
+      line-height: 22px;
+      flex: 1;
+      &:hover {
+        color: #2CB7CA;
+        cursor: pointer;
+      }
+    }
+  }
+  .time {
+    font-size: 12px;
+    color: #999999;
+    line-height: 18px;
+    white-space: nowrap;
+  }
+}
+</style>

+ 62 - 0
src/views/workspace/components/MyCollections.vue

@@ -0,0 +1,62 @@
+<template>
+  <ListCard
+    class="my-collections"
+    :list="subscribeList"
+    title="最近收藏的标讯"
+    @clickListItem="clickListItem"
+    @linkMore="linkMore"
+    :loading="loading"
+    :loaded="loaded">
+    <div slot="empty-content" class="empty-content">
+      <p>暂未收藏标讯</p>
+    </div>
+  </ListCard>
+</template>
+
+<script>
+import { mapState, mapActions } from 'vuex'
+import ListCard from './ListCard'
+
+export default {
+  name: 'MyCollections',
+  components: {
+    ListCard
+  },
+  computed: {
+    ...mapState({
+      loading: state => state.workspace.collections.loading,
+      loaded: state => state.workspace.collections.loaded,
+      subscribeList: state => state.workspace.collections.list
+    })
+  },
+  created () {
+    this.getList()
+  },
+  methods: {
+    ...mapActions('workspace/collections', [
+      'getList'
+    ]),
+    resolveLink (link) {
+      const { href } = this.$router.resolve(link)
+      return href
+    },
+    clickListItem (item) {
+      // this.pathVisiting(
+      //   this.createPathItem(
+      //     '/article/content/*.html',
+      //     `id=${item._id}`
+      //   )
+      // )
+      const link = `/article/content/${item._id}.html`
+      window.open(link)
+    },
+    linkMore () {
+      window.open('/swordfish/frontPage/collection/sess/index')
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 56 - 0
src/views/workspace/components/MyCustomer.vue

@@ -0,0 +1,56 @@
+<template>
+  <ListCard
+    class="my-customer"
+    :list="list"
+    title="我的客户"
+    @clickListItem="clickListItem"
+    @linkMore="linkMore"
+    :loading="loading"
+    :loaded="loaded">
+    <div slot="empty-content" class="empty-content">暂无我的客户</div>
+  </ListCard>
+</template>
+
+<script>
+import { mapState, mapActions } from 'vuex'
+import ListCard from './ListCard'
+
+export default {
+  name: 'MyCustomer',
+  components: {
+    ListCard
+  },
+  computed: {
+    ...mapState({
+      loading: state => state.workspace.customer.loading,
+      loaded: state => state.workspace.customer.loaded,
+      list: state => state.workspace.customer.list
+    })
+  },
+  created () {
+    this.getList()
+  },
+  methods: {
+    ...mapActions('workspace/customer', [
+      'getList'
+    ]),
+    resolveLink (link) {
+      const { href } = this.$router.resolve(link)
+      return href
+    },
+    clickListItem (item) {
+      // this.pathVisiting(
+      //   this.createPathItem(
+      //     '/myCustomer/*',
+      //     `id=${item._id}`
+      //   )
+      // )
+      const href = `/entpc/newBus/myCustomer/${item._id}`
+      window.open(href)
+    },
+    linkMore () {
+      window.open('/entpc/newBus/myCustomer?m1=1&m2=1')
+    }
+  }
+}
+</script>

+ 77 - 0
src/views/workspace/components/SubscribeList.vue

@@ -0,0 +1,77 @@
+<template>
+  <ListCard
+    class="subscribe-list"
+    :list="subscribeList"
+    title="订阅信息"
+    @clickListItem="clickListItem"
+    @linkMore="linkMore"
+    :loading="loading"
+    :loaded="loaded">
+    <div slot="empty-content" class="empty-content">
+      <p>暂无订阅信息<span v-if="!emptyButtonShow">,</span></p>
+      <p>
+        <span v-if="!isSubCount">
+          {{ hasKey ? '正在根据您的订阅条件为您匹配信息中,' : '请前往设置订阅条件,接收最新招投标信息' }}
+        </span>
+        <span v-if="hasKey">请耐心等待~</span>
+      </p>
+      <button class="empty-button mt12" @click="addSet" v-if="!emptyButtonShow">前往订阅管理</button>
+    </div>
+  </ListCard>
+</template>
+
+<script>
+import { mapState, mapActions } from 'vuex'
+import ListCard from './ListCard'
+
+export default {
+  name: 'SubscribeList',
+  components: {
+    ListCard
+  },
+  computed: {
+    // 有关键词或者大会员子账号不显示按钮
+    emptyButtonShow () {
+      return this.hasKey || this.isSubCount
+    },
+    ...mapState({
+      isSubCount: state => state.user.info.isSubCount, // 是否是子账号
+      hasKey: state => state.workspace.subscribe.hasKey, // 是否有订阅关键词
+      loading: state => state.workspace.subscribe.loading,
+      loaded: state => state.workspace.subscribe.loaded,
+      subscribeList: state => state.workspace.subscribe.list
+    })
+  },
+  created () {
+    this.getList()
+  },
+  methods: {
+    ...mapActions('workspace/subscribe', [
+      'getList'
+    ]),
+    resolveLink (link) {
+      const { href } = this.$router.resolve(link)
+      return href
+    },
+    clickListItem (item) {
+      // this.pathVisiting(
+      //   this.createPathItem(
+      //     '/article/content/*.html',
+      //     `id=${item._id}`
+      //   )
+      // )
+      const link = `/article/content/${item._id}.html`
+      window.open(link)
+    },
+    linkMore () {
+      const link = this.resolveLink('/big_subscribe')
+      window.open(link)
+    },
+    addSet () {
+      // 前往订阅管理
+      // this.$router.push('/set_subscribe/config')
+      window.open(this.resolveLink('/set_subscribe/config'))
+    }
+  }
+}
+</script>

+ 69 - 0
src/views/workspace/components/WorkspaceCard.vue

@@ -0,0 +1,69 @@
+<template>
+  <div class="workspace-card">
+    <div class="card-header">
+      <span class="header-title">
+        <slot name="header-title">{{ title }}</slot>
+      </span>
+      <span class="header-right">
+        <slot name="header-right"></slot>
+      </span>
+    </div>
+    <div class="card-content">
+      <slot name="default"></slot>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'WorkspaceCard',
+  props: {
+    title: {
+      type: String,
+      default: ''
+    }
+  },
+  created () {},
+  methods: {}
+}
+</script>
+
+<style lang="scss" scoped>
+::v-deep {
+  .red-dot {
+    width: 6px;
+    height: 6px;
+    background: #fb483d;
+    border-radius: 50%;
+    &.invisible {
+      visibility: hidden;
+    }
+  }
+}
+
+.workspace-card {
+  background-color: #fff;
+  border-radius: 4px;
+  box-shadow: 0px 0px 18px 0px rgba(0,0,0,0.02);
+}
+.card-header {
+  padding: 12px 20px 8px;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+.card-content {
+  padding: 16px 20px;
+}
+.header-title {
+  font-size: 18px;
+  color: #1D1D1D;
+  line-height: 28px;
+}
+.header-right {
+  margin-left: 20px;
+  font-size: 12px;
+  color: #686868;
+  line-height: 18px;
+}
+</style>

+ 72 - 0
src/views/workspace/dashboard.vue

@@ -0,0 +1,72 @@
+<template>
+  <el-container class="workspace-dashboard">
+    <el-main>
+      <CommonUse class="main-module"></CommonUse>
+      <BusinessProfile class="main-module"></BusinessProfile>
+      <div class="main-module card-list-module">
+        <MyCustomer></MyCustomer>
+        <CustomerWatcher></CustomerWatcher>
+      </div>
+      <div class="main-module card-list-module">
+        <SubscribeList></SubscribeList>
+        <MyCollections></MyCollections>
+      </div>
+    </el-main>
+    <el-aside width="396px">
+      <MessageTips></MessageTips>
+    </el-aside>
+  </el-container>
+</template>
+
+<script>
+import { Container, Aside, Main } from 'element-ui'
+import MessageTips from './components//MessageTips.vue'
+import CommonUse from './components/CommonUse.vue'
+import BusinessProfile from './components/BusinessProfile.vue'
+import MyCustomer from './components/MyCustomer.vue'
+import CustomerWatcher from './components/CustomerWatcher.vue'
+import SubscribeList from './components/SubscribeList.vue'
+import MyCollections from './components/MyCollections.vue'
+export default {
+  name: 'WorkspaceDashboard',
+  components: {
+    [Container.name]: Container,
+    [Main.name]: Main,
+    [Aside.name]: Aside,
+    MessageTips,
+    CommonUse,
+    BusinessProfile,
+    MyCustomer,
+    CustomerWatcher,
+    SubscribeList,
+    MyCollections
+  },
+  data () {
+    return {}
+  }
+}
+</script>
+<style lang="scss" scoped>
+.workspace-dashboard {
+  padding: 24px;
+}
+.el-main {
+  padding: 0;
+  margin-right: 16px;
+}
+.main-module {
+  &:not(:first-of-type) {
+    margin-top: 16px;
+  }
+}
+.card-list-module {
+  display: flex;
+  .list-card {
+    flex: 1;
+    max-width: 49%;
+    &:not(:first-of-type) {
+      margin-left: 16px;
+    }
+  }
+}
+</style>