Browse Source

feat:需求管理首次提交

AlieLee 3 years ago
parent
commit
93c9bda668

+ 3 - 0
.browserslistrc

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

+ 29 - 0
.eslintrc.js

@@ -0,0 +1,29 @@
+module.exports = {
+  root: true,
+  env: {
+    node: true
+  },
+  extends: [
+    'plugin:vue/essential',
+    '@vue/standard',
+    '@vue/typescript/recommended'
+  ],
+  parserOptions: {
+    ecmaVersion: 2020
+  },
+  rules: {
+    'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
+    'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off'
+  },
+  overrides: [
+    {
+      files: [
+        '**/__tests__/*.{j,t}s?(x)',
+        '**/tests/unit/**/*.spec.{j,t}s?(x)'
+      ],
+      env: {
+        jest: true
+      }
+    }
+  ]
+}

+ 28 - 0
.gitignore

@@ -0,0 +1,28 @@
+.DS_Store
+node_modules
+.vscode
+bdtp-digital-front
+/dist
+/web-cli
+package-lock.json
+
+# local env files
+.env.local
+.env.*.local
+
+# Log files
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+# Editor directories and files
+.idea
+.vscode
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+*.zip
+node_modules

+ 90 - 1
README.md

@@ -1 +1,90 @@
-# bdtp-digital-front
+# web-cli
+
+* 基于ts开发可以避免上线时的很多错误
+* 框架按需加载meri-design的组件,减少热启动编译速度和打包体积
+* 自动处理页面跳转loading
+* 自动处理面包屑记录
+* 页面缓存机制
+* 自测单元
+* 同步meri-desin的theme主题
+* 自动注册组件、路由、方法等
+* vuex 存储用户信息
+* 封装友好的方法,如登录、上传、下载
+* 自动处理打包信息
+
+## 操作命令
+
+| 操作 | 命令                    |
+| ------------- | ----------- |
+| 启动      | npm run serve       |
+| 打包   | npm run build     |
+| 单元测试   | npm run test:unit     |
+| 语法检查   | npm run lint     |
+
+## 配置
+### 代理
+`vue.config.js`
+```javascript
+const proxy = 'http://192.168.100.236' // 需要代理请求的nginx地址
+```
+### 项目信息
+`package.json`
+```javascript
+  "name": "web-cli", // 前端项目名称,如electronicpatrol
+  "title": "前端框架", // 产品名称,如电子巡更
+  "version": "0.1.0", // 产品版本号,如v1.0.0
+```
+
+## 组件
+### 按需注册meri-design组件
+`src/utils/components.ts`
+```javascript
+import { Button, Message, Loading } from 'meri-design'
+export default (Vue: any) => {
+  Vue.use(Button)
+  Vue.prototype.$message = Message
+  Vue.prototype.$loading = Loading
+}
+```
+### 注册全局组件
+`src/components/common`
+
+该文件下的vue组件,会自动注册为全局组件。
+
+## 路由
+`src/router`自动注册该文件夹导出的路由
+```javascript
+const list = () => import('@/views/notice/list.vue')
+export default [
+  {
+    path: '/list',
+    name: 'list',
+    component: list,
+    meta: {
+      keepAlive: true, //是否需要缓存组件
+      title: '公告管理' //面包屑记录的页面标题
+    }
+  }
+]
+```
+## 请求
+`src/api`该文件下导出的url会自动挂载到vue原型链上
+```javascript
+this.$axios.post(this.$api.xxx,{params})
+```
+## 方法
+### 判断用户是否有资源权限
+```javascript
+state.authId.includes(key)
+```
+### 取用户相关信息
+```javascript
+const {user_id, person_id,xx} = state.userInfo
+```
+
+## 规范
+
+* [代码管理规范](https://thoughts.teambition.com/workspaces/5eb42f82399747001a74bd94/docs/5eb430869f61dd00011b61bb)
+
+* [开发提测规范](https://thoughts.teambition.com/workspaces/5eb42f82399747001a74bd94/docs/5fb235574cc5830001e13742)
+

+ 16 - 0
babel.config.js

@@ -0,0 +1,16 @@
+module.exports = {
+  presets: [
+    '@vue/cli-plugin-babel/preset'
+  ],
+  plugins: [
+    [
+      'component',
+      {
+        libraryName: 'meri-design',
+        camel2Dash: false,
+        libDir: 'dist',
+        styleLibrary: { name: 'theme', base: true }
+      }
+    ]
+  ]
+}

+ 3 - 0
jest.config.js

@@ -0,0 +1,3 @@
+module.exports = {
+  preset: '@vue/cli-plugin-unit-jest/presets/typescript-and-babel'
+}

+ 55 - 0
package.json

@@ -0,0 +1,55 @@
+{
+  "name": "bdtp-digital-front",
+  "title": "数据字典编辑工具",
+  "version": "0.0.0",
+  "remotePath": "/前端产品/其他",
+  "private": true,
+  "scripts": {
+    "serve": "vue-cli-service serve",
+    "build": "vue-cli-service build",
+    "test:unit": "vue-cli-service test:unit",
+    "lint": "vue-cli-service lint"
+  },
+  "dependencies": {
+    "core-js": "^3.6.5",
+    "vue": "^2.6.11",
+    "vue-class-component": "^7.2.3",
+    "vue-property-decorator": "^8.4.2",
+    "vue-router": "^3.2.0",
+    "vuex": "^3.4.0"
+  },
+  "devDependencies": {
+    "@types/axios": "^0.14.0",
+    "@types/jest": "^24.0.19",
+    "@types/node": "^14.14.6",
+    "@typescript-eslint/eslint-plugin": "^2.33.0",
+    "@typescript-eslint/parser": "^2.33.0",
+    "@vue/cli-plugin-babel": "^4.5.0",
+    "@vue/cli-plugin-eslint": "^4.5.0",
+    "@vue/cli-plugin-router": "^4.5.0",
+    "@vue/cli-plugin-typescript": "^4.5.0",
+    "@vue/cli-plugin-unit-jest": "^4.5.0",
+    "@vue/cli-plugin-vuex": "^4.5.0",
+    "@vue/cli-service": "^4.5.0",
+    "@vue/eslint-config-standard": "^5.1.2",
+    "@vue/eslint-config-typescript": "^5.0.2",
+    "@vue/test-utils": "^1.0.3",
+    "babel-plugin-component": "^1.1.1",
+    "compressing": "^1.5.1",
+    "element-ui": "^2.15.6",
+    "eslint": "^6.7.2",
+    "eslint-plugin-import": "^2.20.2",
+    "eslint-plugin-node": "^11.1.0",
+    "eslint-plugin-promise": "^4.2.1",
+    "eslint-plugin-standard": "^4.0.0",
+    "eslint-plugin-vue": "^6.2.2",
+    "less": "^3.0.4",
+    "less-loader": "^5.0.0",
+    "meri-design": "1.5.2",
+    "moment": "^2.29.1",
+    "typescript": "~3.9.3",
+    "vue-svg-loader": "^0.16.0",
+    "vue-template-compiler": "^2.6.11",
+    "webpack-version-zip": "^0.0.9"
+  }
+}

BIN
public/favicon.ico


+ 17 - 0
public/index.html

@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+  <head>
+    <meta charset="utf-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <meta name="viewport" content="width=device-width,initial-scale=1.0">
+    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
+    <title><%= htmlWebpackPlugin.options.title %></title>
+  </head>
+  <body>
+    <noscript>
+      <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
+    </noscript>
+    <div id="app"></div>
+    <!-- built files will be auto injected -->
+  </body>
+</html>

+ 44 - 0
src/App.vue

@@ -0,0 +1,44 @@
+<template>
+  <div id="app">
+    <keep-alive>
+      <router-view v-if="$route.meta.keepAlive"></router-view>
+    </keep-alive>
+    <router-view v-if="!$route.meta.keepAlive"></router-view>
+  </div>
+</template>
+<style lang="less">
+* {
+  padding: 0;
+  margin: 0;
+}
+b{
+  font-weight:normal;
+}
+html,
+body,
+#app {
+  width: 100%;
+  height: 100%;
+  font-size: 14px;
+  font-family: Arial, "Microsoft Yahei", sans-serif;
+  color: #333;
+  // 滚动条样式
+  &::-webkit-scrollbar {
+    width: 5px;
+    height: 5px;
+  }
+  &::-webkit-scrollbar-track {
+    background-color: rgba(0, 0, 0, 0);
+    border-radius: 3px;
+  }
+  &::-webkit-scrollbar-thumb {
+    border-radius: 3px;
+    background: #d1d1d1;
+    border: 1px solid #d1d1d1;
+  }
+  &::-webkit-scrollbar-thumb:hover {
+    background: #d1d1d1;
+    border: 1px solid #d1d1d1;
+  }
+}
+</style>

+ 7 - 0
src/api/common.ts

@@ -0,0 +1,7 @@
+export default {
+  login: '/EMS_SaaS_Web/Spring/MVC/entrance/unifier/getPersonByUserNameService', // 登录
+  upload: '/upload/meos', // 上传接口
+  download: '/download/meos', // 下载接口 ($api.download/key?filename="xx"  (filename对下载的文件重命名))
+  downloadbyparams: '/download/postByParams/', // 通过参数下载接口
+  downloadpdf: '/meos/pdf' // 导出pdf
+}

+ 11 - 0
src/api/demandmanage.ts

@@ -0,0 +1,11 @@
+const APIPRE = '/dmp-rwd-version/rwdedit/demand/'
+export default {
+  demandSave: APIPRE + 'save', // 32001 保存草稿
+  demandSubmit: APIPRE + 'submit', // 32002 提交需求
+  demandDelete: APIPRE + 'delete', // 32003 删除需求
+  demandAnswer: APIPRE + 'answer', // 32004 回复需求
+  demandQueryDraft: APIPRE + 'queryDraft', // 32005 查询草稿箱
+  demandQuerySubmitted: APIPRE + 'querySubmitted', // 32006 查询已提交
+  demandQueryTodo: APIPRE + 'queryTodo', // 32007 查询待办
+  demandQueryDone: APIPRE + 'queryDone'// 32008 查询已办
+}

+ 73 - 0
src/components/common/Bread.vue

@@ -0,0 +1,73 @@
+<template>
+  <ul class="cli-bread">
+    <li v-for="(item, index) in breadcrumbArr" :key="index">
+      <router-link
+        class="elli"
+        replace
+        :to="item.path"
+        v-text="item.name"
+        v-title
+      ></router-link>
+      <ArrowRight class="cli-bread-curm" v-if="isLast(index)" />
+    </li>
+  </ul>
+</template>
+<script>
+import { mapState, mapMutations } from 'vuex'
+import ArrowRight from 'meri-design/lib/static/iconSvg/arrow_right.svg'
+export default {
+  data () {
+    return {}
+  },
+  components: { ArrowRight },
+  computed: {
+    ...mapState(['breadcrumbArr'])
+  },
+  methods: {
+    ...mapMutations(['changeBreadcrumb']),
+    isLast (index) {
+      return index !== this.breadcrumbArr.length - 1
+    }
+  }
+}
+</script>
+
+<style lang="less">
+.cli-bread {
+  word-break: keep-all;
+  display: flex;
+  align-items: center;
+  height: 24px;
+  line-height: 24px;
+  width: 100%;
+  overflow: hidden;
+  li {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    a {
+      color: #8d9399;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+      max-width: 100px;
+      display: inline-block;
+      &:not(:last-child):hover {
+        color: #0091ff;
+      }
+      &:last-child {
+        cursor: default;
+        color: #1f2429;
+      }
+    }
+  }
+  &-curm {
+    margin-left: 4px;
+    margin-right: 4px;
+    vertical-align: middle;
+    width: 8px;
+    height: 14px;
+    margin-bottom: 2px;
+  }
+}
+</style>

+ 16 - 0
src/main.ts

@@ -0,0 +1,16 @@
+import Vue from 'vue'
+import App from './App.vue'
+import { router, store } from './utils'
+import ElementUI from 'element-ui'
+import 'element-ui/lib/theme-chalk/index.css'
+import moment from 'moment'
+
+Vue.prototype.$moment = moment
+Vue.config.productionTip = false
+Vue.use(ElementUI)
+
+new Vue({
+  router,
+  store,
+  render: h => h(App)
+}).$mount('#app')

+ 24 - 0
src/router/demandmanage.ts

@@ -0,0 +1,24 @@
+/** 需求管理**/
+const demandmanageuser = () => import('@/views/demandmanage/user.vue')
+const demandmanageeditor = () => import('@/views/demandmanage/editor.vue')
+export default [
+  //
+  {
+    path: '/demandmanageuser',
+    name: 'demandmanageuser',
+    component: demandmanageuser,
+    meta: {
+      keepAlive: true,
+      title: '需求管理(使用者)'
+    }
+  },
+  {
+    path: '/demandmanageeditor',
+    name: 'demandmanageeditor',
+    component: demandmanageeditor,
+    meta: {
+      keepAlive: true,
+      title: '需求管理(编辑者)'
+    }
+  }
+]

+ 1 - 0
src/shims-all.d.ts

@@ -0,0 +1 @@
+declare module 'meri-design';

+ 13 - 0
src/shims-tsx.d.ts

@@ -0,0 +1,13 @@
+import Vue, { VNode } from 'vue'
+
+declare global {
+  namespace JSX {
+    // tslint:disable no-empty-interface
+    interface Element extends VNode {}
+    // tslint:disable no-empty-interface
+    interface ElementClass extends Vue {}
+    interface IntrinsicElements {
+      [elem: string]: any;
+    }
+  }
+}

+ 4 - 0
src/shims-vue.d.ts

@@ -0,0 +1,4 @@
+declare module '*.vue' {
+  import Vue from 'vue'
+  export default Vue
+}

+ 1 - 0
src/store/actions.ts

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

+ 14 - 0
src/store/getters.ts

@@ -0,0 +1,14 @@
+export default {
+  /*
+  * 权限判断
+  * @ params key 权限的key
+  * @ return Blean
+  */
+  hasAuth: (state: any) => (key: string) => {
+    try {
+      return state.authId.includes(key)
+    } catch {
+      return false
+    }
+  }
+}

+ 40 - 0
src/store/mutations.ts

@@ -0,0 +1,40 @@
+export default {
+  // 更改用户信息
+  userInfo(state: any, params: Record<string, any>) {
+    state.userInfo = params;
+    // 权限集合
+    if (params.authorizations && params.authorizations.length) {
+      const authId = new Set(
+        params.authorizations
+          .map(({ authorizationId }: any) => authorizationId)
+          .filter((item: any) => item)
+      )
+      state.authId = [...authId];
+    } else {
+      state.authId = [];
+    }
+  },
+  // 更改项目
+  project_id(state: any, params: any) {
+    state.project_id = params
+  },
+  // 更改权限数组
+  authId(state: any, params: Record<string, any>) {
+    state.authId = params
+  },
+  // 更改面包屑数据
+  changeBreadcrumb(state: any, params: any) {
+    if (Array.isArray(params)) {
+      state.breadcrumbArr = params || []
+    } else {
+      const sessionBreadcrumbArr = sessionStorage.getItem('breadcrumbArr')
+      if (sessionBreadcrumbArr && sessionBreadcrumbArr != '[]') {
+        state.breadcrumbArr = JSON.parse(sessionBreadcrumbArr)
+        sessionStorage.removeItem('breadcrumbArr')
+      }
+      const findIndex = state.breadcrumbArr.findIndex((n: any) => n.path == params.path)
+      if (findIndex === -1) return state.breadcrumbArr.push(params)
+      state.breadcrumbArr.splice(findIndex + 1)
+    }
+  }
+}

+ 18 - 0
src/store/state.ts

@@ -0,0 +1,18 @@
+/* eslint-disable @typescript-eslint/camelcase */
+export default {
+  userInfo: { // 用户信
+    user_id: '',
+    userId: '',
+    groupCode: '',
+    pd: '',
+    person_id: ''
+  },
+  // 项目信息
+  project_id: '',
+  // 用户权限集合
+  authId: [],
+  // 面包屑数组
+  breadcrumbArr: [],
+  // 应用id
+  appId: ''
+}

+ 6 - 0
src/utils/api.ts

@@ -0,0 +1,6 @@
+const files = require.context('../api', true, /.(ts|js)$/)
+// 合并所有的API
+const allApi = files.keys().reduce((con: any, src: string) => {
+  return Object.assign(con, files(src).default)
+}, {})
+export default allApi

+ 85 - 0
src/utils/axios.ts

@@ -0,0 +1,85 @@
+/* eslint-disable @typescript-eslint/camelcase */
+import axios from 'axios'
+import { store } from './index'
+// axios默认配置
+axios.defaults.headers.post['Content-Type'] = 'application/json,charset=utf-8'
+axios.defaults.timeout = 1000 * 60 * 60 * 24
+axios.defaults.baseURL = '/api/meos'
+
+// 添加请求拦截器
+axios.interceptors.request.use((config: any) => {
+  const project_id = store.state.project_id
+  const projectId = store.state.project_id
+  const appId = store.state.appId
+  const { pd, user_id, person_id, groupCode, userId } = store.state.userInfo
+  // 公共参数
+  const params = { user_id, pd, person_id }
+  Object.assign(params, project_id ? { project_id } : {})
+  // 运维系统需要额外的参数
+  console.log(config)
+  if (config.url && config.url.includes('EMS_SaaS_Web')) {
+    Object.assign(params, {
+      puser: {
+        userId: user_id,
+        loginDevice: 'PC',
+        pd
+      }
+    })
+  }
+  // BDTP 后台带请求头参数
+  const paramBDTP = {
+    groupCode, userId, projectId, appId
+  }
+  if (config.url && config.url.includes('dmp-rwd-version')) {
+    config.baseURL = ''
+  }
+  // 合并参数
+  if (config.headers.mergeParams !== false) {
+    if (config.method === 'post') {
+      config.params = paramBDTP
+      // config.data = Object.assign(params, config.data)
+    } else {
+      config.params = Object.assign(params, config.params)
+    }
+  }
+  return config
+}, (error: any) => {
+  return Promise.reject(error)
+})
+
+// 添加响应拦截器
+axios.interceptors.response.use((response: any) => {
+  // 如果responseType='blob'  则是下载文件
+  if (response.request.responseType === 'blob') {
+    const { data, config } = response
+    let fileName = ''
+    let mimeType = ''
+    if (config.data) {
+      const configData = Object.prototype.toString.call(response.config.data) === '[Object Object]' ? config.data : JSON.parse(config.data)
+      fileName = configData.fileName || ''
+      mimeType = response.config.data.mimeType
+    }
+    const blob = mimeType ? new Blob([data], { type: mimeType }) : new Blob([data]) // 构造一个blob对象来处理数据
+    // 对于<a>标签,只有 Firefox 和 Chrome(内核) 支持 download 属性
+    // IE10以上支持blob但是依然不支持download
+    if ('download' in document.createElement('a')) {
+      // 支持a标签download的浏览器
+      const link = document.createElement('a') // 创建a标签
+      link.download = fileName // a标签添加属性
+      link.style.display = 'none'
+      link.href = URL.createObjectURL(blob)
+      document.body.appendChild(link)
+      link.click() // 执行下载
+      URL.revokeObjectURL(link.href) // 释放url
+      document.body.removeChild(link) // 释放标签
+    } else {
+      // 其他浏览器
+      navigator.msSaveBlob(blob, fileName)
+    }
+  }
+  return response
+}, (error: any) => {
+  return Promise.reject(error)
+})
+
+export default axios

+ 14 - 0
src/utils/components.ts

@@ -0,0 +1,14 @@
+// 按需加载meri-design组件
+import { Button, Message, Loading } from 'meri-design'
+// 导入需要全局注册的公共组件
+const comments = require.context('../components/common', true, /.(vue)$/)
+
+export default (Vue: any) => {
+  Vue.use(Button)
+  Vue.prototype.$pmessage = Message
+  Vue.prototype.$ploading = Loading
+
+  comments.keys().forEach((fileName: any) => {
+    Vue.component(fileName.match(/^.\/(.*).vue$/)[1], comments(fileName).default)
+  })
+}

+ 20 - 0
src/utils/directives.ts

@@ -0,0 +1,20 @@
+/**
+ * 注册全局的自定义指令
+*/
+
+type typeKey = {
+    [key: string]: any;
+}
+
+const allDirectives: typeKey = {
+  title: {
+    inserted (el: any) {
+      const { clientWidth, scrollWidth, title } = el
+      if (!title && scrollWidth > clientWidth) el.title = el.innerText
+    }
+  }
+}
+
+export default (Vue: any) => {
+  Object.keys(allDirectives).forEach((n: any) => Vue.directive(n, allDirectives[n]))
+}

+ 33 - 0
src/utils/index.ts

@@ -0,0 +1,33 @@
+
+import Vue from 'vue'
+
+// 导入需要按需引入的组件
+import meri from './components'
+
+// 引入vuex
+import Vuex from 'vuex'
+import storeConfig from './store'
+
+// 导入axios模块
+import axios from './axios'
+
+// 导入 router
+import router from './router'
+
+// 需要注册的全局指令
+import directive from './directives'
+
+// 导出 api
+import api from './api'
+Vue.use(meri)
+Vue.use(Vuex)
+const store = new Vuex.Store(storeConfig)
+Vue.prototype.$axios = axios
+Vue.use(directive)
+Vue.prototype.$api = api
+
+// 导出相关
+export {
+  router,
+  store
+}

+ 78 - 0
src/utils/router.ts

@@ -0,0 +1,78 @@
+/* eslint-disable @typescript-eslint/camelcase */
+import Vue from 'vue'
+import VueRouter, { Route } from 'vue-router'
+import { store } from './index'
+
+Vue.use(VueRouter)
+
+// 导出相关的路由
+const routeFiles = require.context('../router', true, /ts|js$/)
+const routes = routeFiles.keys().reduce((con, cur) => {
+  return con.concat(routeFiles(cur).default)
+}, [])
+
+const router = new VueRouter({
+  mode: 'history',
+  base: process.env.BASE_URL,
+  routes
+})
+
+let globalLoading: any
+// 路由拦截信息
+router.beforeEach(async (to, from, next) => {
+  globalLoading = Vue.prototype.$ploading({ type: 'global' })
+  const { user_id } = store.state.userInfo
+  if (user_id) return next()
+  const loginName = to.query.loginName
+  const project_id = to.query.project_id
+  if (project_id) store.commit('project_id', project_id)
+  const sessionUserInfo = sessionStorage.getItem('userInfo')
+  if (sessionUserInfo) {
+    const parseUserInfo = JSON.parse(sessionUserInfo)
+    if (parseUserInfo && parseUserInfo.user_id) {
+      if (!loginName || loginName === parseUserInfo.userName) {
+        store.commit('userInfo', parseUserInfo)
+        return next()
+      }
+    }
+  }
+  // 登录
+  if (!loginName) return
+  store.commit('project_id', to.query.project_id) // 存储当前的project_id
+  const { data: { result, content } } = await Vue.prototype.$axios.post(Vue.prototype.$api.login, {
+    loginName: loginName,
+    loginDevice: 'PC'
+  })
+  // 登录成功
+  if (result === 'success' && content.length) {
+    const userInfo = content[0]
+    userInfo.user_id = userInfo.userId
+    store.commit('userInfo', userInfo)
+    next()
+  } else {
+    console.log('登录失败!')
+  }
+})
+
+// 路由守卫
+router.afterEach((to: Route, from: Route) => {
+  // 关闭页面loading
+  Vue.prototype.$ploading.close(globalLoading)
+  // 需要添加面包屑信息
+  if (to.meta?.title) {
+    const { meta, params, path, query } = to
+    store.commit('changeBreadcrumb', { name: meta?.title, params, query, path })
+  }
+})
+
+// vuex持久化(监听页面卸载并将vuex的信息加密存入)
+window.addEventListener('unload', () => {
+  sessionStorage.setItem('userInfo', JSON.stringify(store.state.userInfo))
+  sessionStorage.setItem('breadcrumbArr', JSON.stringify(store.state.breadcrumbArr))
+})
+// 删除敏感信息
+window.addEventListener('load', () => {
+  sessionStorage.removeItem('userInfo')
+})
+
+export default router

+ 11 - 0
src/utils/store.ts

@@ -0,0 +1,11 @@
+
+import state from '../store/state'
+import mutations from '../store/mutations'
+import getters from '../store/getters'
+import actions from '../store/actions'
+export default {
+  state,
+  mutations,
+  getters,
+  actions
+}

+ 57 - 0
src/views/demandmanage/Confirm.vue

@@ -0,0 +1,57 @@
+<template>
+  <Modal
+    :show="value"
+    :title="title"
+    mode="tip"
+    :type="type"
+    @close="cancel"
+  >
+    <template #content>
+      {{desc}}
+    </template>
+    <template #handle>
+      <Button @click="cancel" :type="cancelButtonType">{{cancelButtonText}}</Button>
+      <Button @click="confirm" :type="confirmButtonType">{{confirmButtonText}}</Button>
+    </template>
+  </Modal>
+</template>
+
+<script>
+import { Modal, Button } from 'meri-design';
+
+export default {
+  components: {
+    Modal,
+    Button
+  },
+  props: {
+    value: {},
+    title: {},
+    desc: {},
+    type: {
+      default: 'tip'
+    },
+    cancelButtonText: {
+      default: '取消'
+    },
+    cancelButtonType: {
+      default: 'default'
+    },
+    confirmButtonText: {
+      default: '确认'
+    },
+    confirmButtonType: {
+      default: 'primary'
+    }
+  },
+  methods: {
+    //   取消
+    cancel() {
+      this.$emit('cancel');
+    },
+    confirm() {
+      this.$emit('confirm');
+    }
+  }
+};
+</script>

+ 381 - 0
src/views/demandmanage/Detail.vue

@@ -0,0 +1,381 @@
+<template>
+  <Modal
+    :show="value"
+    :title="handle.type"
+    mode="default"
+    @close="cancel"
+  >
+    <template #content>
+      <div style="max-height:700px;width:100%;padding:12px 32px 32px 32px;">
+        <!-- 使用者 详情 状态回复信息行 -->
+        <div class="stateCon" v-if="handle.type!='新增需求'&&detail.billState!=0&&handle.user">
+          <div class="state-con-1" v-if="handle.user">
+            <div><div class="title">状态:</div><Tag type="dot" :color="detail.dot">{{detail.text}}</Tag></div>
+            <!-- 需求状态 - 0:待提交 1:待回复 2:已接受 3:已拒绝 4:待完善 5:论证中 -->
+            <div v-if="detail.billState==2||detail.billState==3||detail.billState==4||detail.billState==5"><div>回复人:</div><div v-text="detail.respondent"></div></div>
+            <div v-if="detail.billState==2||detail.billState==3||detail.billState==4"><div>回复时间:</div><div v-text="toTime(detail.answerTime)"></div></div>
+            <div v-if="detail.billState==5"><div>预计回复时间:</div><div v-text="toTime(detail.estimateTime)"></div></div>
+          </div>
+          <div class="state-con-2" v-if="detail.billState==2||detail.billState==3||detail.billState==4">
+            <div><div class="title">回复信息:</div><div class="state-con-reply" v-text="detail.reply||'--'"></div></div>
+          </div>
+        </div>
+        <div class="gridCon" v-if="handle.type!='新增需求'&&detail.billState!=0&&(((detail.billState!=4||(detail.billState==4&&handle.type=='详情'))&&handle.user)||!handle.user)">
+          <div class="gridRow">
+            <div class="gridCell1">用户名</div><div v-text="detail.userName"></div>
+          </div>
+          <div class="gridRow">
+            <div class="gridCell1">部门</div><div v-text="detail.deptName"></div>
+          </div>
+          <div class="gridRow">
+            <div class="gridCell1">条线</div><div v-text="line[detail.productLine]"></div>
+          </div>
+          <div class="gridRow">
+            <div class="gridCell1">提出时间</div><div v-text="toTime(detail.creationTime)"></div>
+          </div>
+          <div class="gridRow">
+            <div class="gridCell1">编码</div><div v-text="detail.code"></div>
+          </div>
+            <div class="gridRow">
+            <div class="gridCell1">主题</div><div v-text="detail.subject"></div>
+          </div>
+          <div class="gridRow">
+            <div class="gridCell1">需求内容</div><div v-text="detail.content"></div>
+          </div>
+        </div>
+        <!-- 使用者 新增修改 -->
+        <div class="editCon" v-if="handle.type=='新增需求'||handle.type=='修改需求'">
+          <div class="editCell">
+            <div class="editCellTitle">用户名</div>
+            <Input width="100%" placeholder="用户名" disabled v-model="detail.userName"/>
+          </div>
+          <div class="editCell">
+            <div class="editCellTitle"><em>*</em> 部门</div>
+            <Input width="100%" placeholder="请输入该用户在钉钉中的部门信息" v-model="detail.deptName"/>
+          </div>
+          <div class="editCell">
+            <div class="editCellTitle"><em>*</em> 条线</div>
+            <Select
+              isReadOnly
+              :selectdata="lineData"
+              width="536"
+              v-model="detail.productLine"
+              :placeholder="'请选择内容'"
+              hideClear
+            />
+          </div>
+          <div class="editCell">
+            <div class="editCellTitle"><em>*</em> 主题</div>
+            <Input placeholder="示例:新增对象-策略" v-model="detail.subject" :maxLength="30"/>
+          </div>
+          <div class="editCell">
+            <div class="editCellTitle"><em>*</em> 需求内容</div>
+            <Input width="100%" placeholder="请输入内容" type="textarea" :maxLength="210" v-model="detail.content" @input="inputContent" :error-info="errorMsg"/>
+          </div>
+          <!-- <div class="editCell">
+            <div class="editCellTitle"><em>*</em> 需求内容</div>
+            <el-input
+              type="textarea"
+              placeholder="请输入内容"
+              v-model="detail.userName"
+              maxlength="30"
+              show-word-limit
+            />
+          </div> -->
+        </div>
+        <!-- 编辑者 回复 -->
+        <div class="replyCon"  v-if="handle.type=='回复'||handle.type=='继续回复'">
+          <div class="replyInfo"><b class="replyInfo1">需求回复</b> <b class="replyInfo2" v-if="handle.type=='继续回复'">上次回复时间:<b class="replyInfo3" v-text="toTime(detail.estimateTime,true)"></b></b></div>
+          <div>
+             <radio-group :group-data.sync="stateGroup" type="single" @change="stateChange"/>
+          </div>
+          <Input v-if="stateSel!=5" width="100%" placeholder="请输入回复的信息" type="textarea" :maxLength="210" v-model="reply" @input="inputContent" :error-info="errorMsg"/>
+          <div v-if="stateSel==5" class="estimateTime"><div>预计回复时间</div><PickerDate @change="dateChange" :picker="['day']" placeholder="请选择日期"/></div>
+          <div></div>
+        </div>
+        <!-- 编辑者 详情 状态回复信息行 -->
+        <div class="stateCon" v-if="handle.type=='详情'&&!handle.user" style="padding-top:14px">
+          <div class="state-con-1">
+            <div><div class="title">状态:</div><Tag type="dot" :color="detail.dot">{{detail.text}}</Tag></div>
+            <!-- 需求状态 - 0:待提交 1:待回复 2:已接受 3:已拒绝 4:待完善 5:论证中 -->
+            <div v-if="detail.billState==2||detail.billState==3||detail.billState==4"><div>操作时间:</div><div v-text="toTime(detail.answerTime,true)"></div></div>
+          </div>
+          <div class="state-con-2" v-if="detail.billState==2||detail.billState==3||detail.billState==4">
+            <div><div class="title">回复信息:</div><div class="state-con-reply" v-text="detail.reply||'--'"></div></div>
+          </div>
+        </div>
+      </div>
+    </template>
+    <template #handle>
+      <div v-if="handle.type=='新增需求'||handle.type=='修改需求'||handle.type=='回复'||handle.type=='继续回复'">
+        <Button @click="cancel" :type="cancelButtonType">{{cancelButtonText}}</Button>
+        <Button v-if="handle.user" @click="save" type="default">保存</Button>
+        <Button @click="confirm" :type="confirmButtonType">{{confirmButtonText}}</Button>
+      </div>
+    </template>
+  </Modal>
+</template>
+
+<script>
+import { Modal, Input, Select, Tag, RadioGroup, PickerDate } from 'meri-design'
+
+export default {
+  components: {
+    Modal,
+    Input,
+    Select,
+    Tag,
+    RadioGroup,
+    PickerDate
+  },
+  props: {
+    value: {}, // 显示隐藏
+    handle: {
+      default: {
+        user: true,
+        type: '新增需求', // 新增需求 详情 修改需求 回复 继续回复
+        detail: {}
+      }
+    }, // 操作
+    cancelButtonText: {
+      default: '取消'
+    },
+    cancelButtonType: {
+      default: 'text'
+    },
+    confirmButtonText: {
+      default: '提交'
+    },
+    confirmButtonType: {
+      default: 'primary'
+    }
+  },
+  data () {
+    return {
+      respondent: '',
+      reply: '',
+      estimateTime: null,
+      stateSel: 2,
+      stateGroup: [
+        {
+          id: 2,
+          name: '接受',
+          checked: 'checked',
+          disabled: false
+        },
+        {
+          id: 3,
+          name: '拒绝',
+          checked: 'uncheck',
+          disabled: false
+        },
+        {
+          id: 4,
+          name: '待完善',
+          checked: 'uncheck',
+          disabled: false
+        },
+        {
+          id: 5,
+          name: '待论证',
+          checked: 'uncheck',
+          disabled: false
+        }
+      ],
+      line: { 1: '业务', 2: '研发', 3: '实施' },
+      lineData: [{ id: 1, name: '业务' }, { id: 2, name: '研发' }, { id: 3, name: '实施' }],
+      detail: {
+        userName: '',
+        deptName: '',
+        productLine: null,
+        subject: '',
+        content: '',
+        text: '',
+        dot: '#f0f'
+      },
+      errorMsg: ''
+    }
+  },
+  methods: {
+    dateChange (date) {
+      this.estimateTime = this.$moment(date, 'YYYY.MM.DD').valueOf()
+    },
+    stateChange (e) {
+      this.stateSel = e[0]
+      this.reply = ''
+      this.estimateTime = null
+    },
+    toTime (time, format) {
+      return time ? this.$moment(time).format(format ? 'YYYY.MM.DD' : 'YYYY-MM-DD HH:mm:ss') : '--'
+    },
+    //   取消
+    cancel () {
+      this.$emit('cancel')
+    },
+    confirm () {
+      if (this.handle.type === '回复' || this.handle.type === '继续回复') {
+        const temp = {
+          ids: [this.handle.detail.id],
+          billState: this.stateSel,
+          respondent: this.respondent,
+          reply: this.reply,
+          estimateTime: this.estimateTime
+        }
+        // 校验
+        let error = ''
+        if (temp.billState === 5) {
+          if (!temp.estimateTime)error = '请选择预计回复时间'
+        } else {
+          if (!temp.reply || temp.reply.length > 200)error = '填写内容有误,请查验!'
+        }
+        if (error) {
+          this.$pmessage({ type: 'error', message: error })
+        } else {
+          this.$emit('confirm', temp)
+        }
+      } else {
+        const check = this.check()
+        if (check) this.$emit('confirm', check)
+      }
+    },
+    save () {
+      const check = this.check()
+      if (check) this.$emit('save', check)
+    },
+    inputContent (val) {
+      if (val.length > 200) {
+        this.errorMsg = '限制200字符'
+      } else {
+        this.errorMsg = ''
+      }
+    },
+    check () {
+      // 校验
+      const detail = this.detail
+      if (!detail.userName || !detail.deptName || !detail.productLine || !detail.subject || !detail.content || detail.content.length > 200) {
+        this.$pmessage({ type: 'error', message: '填写内容有误,请查验!' })
+        return false
+      }
+      return detail
+    }
+  },
+  mounted () {
+    console.log(212)
+    if (this.handle.userName && this.handle.user) { // 新建
+      this.detail.userName = this.handle.userName
+    } else if (this.handle.userName && !this.handle.user) { // 回复
+      this.respondent = this.handle.userName
+      this.detail = this.handle.detail
+      if (this.handle.type === '继续回复') {
+        this.stateGroup[3].disabled = true
+      }
+    } else {
+      this.detail = this.handle.detail
+    }
+  }
+}
+</script>
+<style lang="less" scoped>
+.editCon{
+  .editCell{
+    .editCellTitle{
+      padding: 8px 0;
+      em{
+        color:#F54E45;
+      }
+    }
+    .p-input,.p-textarea{
+      width: 100% !important;
+    }
+  }
+}
+.stateCon{
+  color: #646C73;
+  .title{
+    color: #1F2429;
+  }
+  &>div{
+    display: flex;
+    padding: 8px 0;
+  }
+  .state-con-1{
+    /deep/.p-tag-wrapper{
+      height: 18px !important;
+    }
+    &>div {
+      display: flex;
+      align-items: center;
+      padding-left: 30px;
+    }
+    &>div:first-child{
+      padding-left: 0px;
+    }
+  }
+  .state-con-2{
+    padding-bottom: 0;
+  }
+  .state-con-reply{
+    padding-top: 6px;
+    font-size: 16px;
+    line-height: 28px;
+  }
+}
+.gridCon {
+  border: 1px solid #D8D8D8;
+  margin-top: 12px;
+  .gridRow {
+    min-height: 50px;
+    border-bottom: 1px solid #eff0f1;
+    display: flex;
+    .gridCell1 {
+      width: 82px;
+      text-align: right;
+      padding: 0 14px 0 0;
+      background: #F5F7FA;
+      color: #646C73;
+      font-weight: 500;
+      display: flex;
+      justify-content: flex-end;
+      align-items: center;
+    }
+    &>div:nth-child(2) {
+      width: calc(100% - 82px);
+      padding: 12px;
+      word-break: break-all;
+      color: #1F2429;
+      line-height: 22px;
+    }
+  }
+  .gridRow:last-child{
+    border-bottom: none;
+  }
+}
+.replyCon{
+  .replyInfo{
+    padding-top: 20px;
+    .replyInfo1{
+      color: #1F2329;
+      font-weight: 500;
+    }
+    .replyInfo2{
+      color: #646C73;
+    }
+    .replyInfo3{
+      color: #F54E45;;
+    }
+  }
+  .p-input,.p-textarea{
+    width: 100% !important;
+  }
+  .p-radio-group {
+    padding-left: 0;
+  }
+  .estimateTime{
+    display: flex;
+    align-items: center;
+    &>div:first-child{
+      padding-right: 10px;
+    }
+  }
+}
+</style>

+ 397 - 0
src/views/demandmanage/Draft.vue

@@ -0,0 +1,397 @@
+<template>
+  <div class="draft" ref="localDom">
+    <div class="breadCon">
+      <Breadcrumb :data="breadData" @change='breadChange' />
+    </div>
+    <div class="topCon">
+      <div class="topLeft"></div>
+      <div class="topRight">
+        <Input placeholder="主题" @clear="pressEnter" iconType="search" v-model="matchingCond" @pressEnter="pressEnter"/>
+      </div>
+    </div>
+    <div class="mainCon">
+      <div class="tableCon" id="tableCon-draft">
+        <TableGrid
+          v-if="tableHeight"
+          width="100%"
+          :maxHeight="tableHeight"
+          :header="header"
+          :data="tableData"
+          key="tableGrid"
+          :rowTools="rowTools"
+          @sortHandle="sortHandle"
+          @screenHandle="screenHandle"
+          @rowToolHandle="rowToolHandle"
+        />
+      </div>
+      <div class="paginationCon">
+        <Pagination
+          :pageCountShow="true"
+          :page.sync="pagination.current"
+          :pageSize="pagination.size"
+          :pageCount="pageCount"
+          :pageSizeSetting="true"
+          @change="paginationChange"
+        />
+      </div>
+    </div>
+    <Detail
+      v-if="showDetail"
+      v-model="showDetail"
+      :handle="handleDetail"
+      @cancel="DetailCancel($event), (showDetail = false)"
+      @confirm="DetailConfirm($event), (showDetail = false)"
+      @save="DetailSave($event), (showDetail = false)"
+    />
+    <Modal
+      :show="modalStatusTip"
+      title="提示"
+      mode="tip"
+      type="info"
+      @close="modalClose"
+    >
+      <template #content>
+        数据删除不可恢复,是否继续删除?
+      </template>
+      <template #handle>
+        <Button @click="modalClose" type="default">取消</Button>
+        <Button @click="modalConfirm" type="primary">确认</Button>
+      </template>
+    </Modal>
+  </div>
+</template>
+<script>
+import { mapState } from 'vuex'
+import {
+  Input,
+  TableGrid,
+  Pagination,
+  Breadcrumb,
+  Modal
+} from 'meri-design'
+import Detail from './Detail'
+export default {
+  name: 'draft',
+  components: {
+    Detail,
+    Input,
+    TableGrid,
+    Pagination,
+    Breadcrumb,
+    Modal
+  },
+  data () {
+    return {
+      modalStatusTip: false,
+      detailId: '',
+      breadData: [
+        { id: 'home', name: '需求管理' },
+        { id: 2, name: '草稿箱' }
+      ],
+      handleDetail: {
+        user: true,
+        type: '详情'
+      },
+      showDetail: false,
+      tableHeight: 300,
+      pagination: {
+        current: 1,
+        size: 10
+      },
+      pageCount: 99,
+      rowTools: {
+        open: true, // 开启显示操作
+        fixed: 'right', // 与header数据的fixed一样
+        width: 120, // 与header数据的width一样
+        align: '', // 与header数据的align一样
+        text: '', // 头部显示的文字,默认是操作
+        moreOpen: true,
+        event: 'rowToolHandle'
+      },
+      name: 'ddd',
+      creatorCond: 1, // 下拉筛选条件 - 0:所有需求 1:我的需求
+      creatorConds: [{ id: 0, name: '所有需求' }, { id: 1, name: '我的需求' }],
+      matchingCond: '', // 模糊匹配 - 编码、主题
+      productCond: [1, 2, 3], // 产线条件
+      stateCond: [], // 状态条件
+      orders: [],
+      line: { 1: '业务', 2: '研发', 3: '实施' },
+      state: { 1: '待回复', 2: '已接受', 3: '已拒绝', 4: '待完善', 5: '论证中' },
+      header: [
+        {
+          key: 'userName',
+          text: '用户名',
+          width: 100
+        },
+        {
+          key: 'deptName',
+          text: '部门',
+          width: 180
+        },
+        {
+          key: 'productLine',
+          text: '条线',
+          width: 100
+        },
+        {
+          key: 'subject',
+          text: '主题'
+        },
+        {
+          key: 'creationTime',
+          text: '创建时间',
+          width: 180,
+          sort: { open: true, event: 'sortHandle' }
+        }
+      ],
+      tableData: []
+    }
+  },
+  methods: {
+    modalClose () {
+      this.modalStatusTip = false
+    },
+    modalConfirm () {
+      this.$axios.post(this.$api.demandDelete, [this.detailId]).then(() => {
+        this.getTableData()
+      })
+      this.modalStatusTip = false
+    },
+    breadChange (id) {
+      if (id === 'home') {
+        this.$emit('back')
+      }
+    },
+    // 详情 编辑 弹窗操作
+    DetailCancel (detail) {
+      console.log(detail)
+    },
+    async DetailConfirm (detail) {
+      await this.$axios.post(this.$api.demandSubmit, detail)
+      this.getTableData()
+    },
+    async DetailSave (detail) {
+      await this.$axios.post(this.$api.demandSave, detail)
+    },
+    // 触发 新增需求弹窗
+    addDemand () {
+      this.handleDetail = {
+        user: true,
+        type: '新增需求',
+        userName: this.UserAndPro.userName,
+        detail: {}
+      }
+      this.showDetail = true
+    },
+    // 列表操作按钮的处理
+    rowToolHandle (handle) {
+      // {itemId: '1438483235070820353', toolId: 'xq'}
+      const { itemId, toolId } = handle
+      const detail = this.tableData.filter(obj => obj.id === itemId)[0]?.detail
+      if (toolId === 'xg') { // 修改
+        this.handleDetail = {
+          user: true,
+          type: '修改需求',
+          detail: detail
+        }
+        this.showDetail = true
+      } else if (toolId === 'sc') {
+        this.detailId = detail.id
+        this.modalStatusTip = true
+      }
+    },
+    // 页码操作
+    paginationChange (page) {
+      // {newPage: 1, newPageSize: 40}
+      this.getTableData(page)
+    },
+    // 列表数据格式处理
+    toTableData (data) {
+      const arr = []
+      data.forEach(obj => {
+        const temp = {}
+        temp.detail = JSON.parse(JSON.stringify(obj))
+        Object.keys(obj).forEach(key => {
+          const cell = {
+            text: ''
+          }
+          if (!obj[key] && obj[key] !== 0) {
+            cell.text = '--'
+          } else {
+            switch (key) {
+              case 'productLine':
+                cell.text = this.line[obj[key]]
+                break
+              case 'creationTime':
+                cell.text = this.$moment(obj[key]).format('YYYY-MM-DD HH:mm:ss')
+                break
+              default:
+                cell.text = obj[key]
+            }
+          }
+          temp[key] = key === 'id' ? obj[key] : cell
+        })
+        temp.tools = {
+          data: [
+            { id: 'xg', name: '修改' },
+            { id: 'sc', name: '删除' }
+          ]
+        }
+        arr.push(temp)
+      })
+      return arr
+    },
+    // 所有需求 自己需求 切换
+    creatorCondChange (data) {
+      // {id: 1, name: '我的需求'}
+      this.creatorCond = data.id
+      this.getTableData()
+    },
+    // 搜索回车查询
+    pressEnter () {
+      this.getTableData()
+    },
+    // 处理滚动条 hover显示不挤占内容位置
+    toTableHeight () {
+      const dom = document.getElementById('tableCon-draft')
+      if (!dom) return
+      this.tableHeight = dom.offsetHeight
+    },
+    // 排序操作
+    sortHandle (type, key) {
+      // ord submitTime
+      // ord submitTime
+      this.orders = [
+        {
+          column: key,
+          asc: type === 'seq'
+        }
+      ]
+      this.getTableData()
+    },
+    // 筛选操作
+    screenHandle (key, type) {
+      // productLine all
+      // billState all
+      console.log(type)
+      if (key === 'productLine') {
+        this.productCond = type === 'all' ? [1, 2, 3] : [Number(type)]
+      } else {
+        this.stateCond = type === 'all' ? [1, 2, 3, 4, 5] : [Number(type)]
+      }
+      this.getTableData()
+    },
+    // 获取列表
+    getTableData (page) {
+      if (page) {
+        if (page.newPageSize !== this.pagination.size) this.pagination.size = page.newPageSize
+      } else {
+        this.pagination.current = 1
+      }
+      this.$axios.post(this.$api.demandQueryDraft, {
+        ...this.pagination,
+        creatorCond: this.creatorCond, // 下拉筛选条件 - 0:所有需求 1:我的需求
+        matchingCond: this.matchingCond, // 模糊匹配 - 编码、主题
+        productCond: this.productCond, // 产线条件
+        stateCond: this.stateCond, // 状态条件
+        orders: this.orders
+      }).then((res) => {
+        const data = res.data
+        this.pageCount = data.count
+        this.tableData = this.toTableData(data.data)
+      })
+    }
+  },
+  computed: {
+    ...mapState({
+      ProjectList: (state) =>
+        state.userInfo.projects.map(
+          ({ projectId: id, projectName: name, projectLocalID: localId }) => ({
+            id,
+            name,
+            localId
+          })
+        ),
+      UserAndPro (state) {
+        const argu = {
+          projectId: [],
+          projectLocalId: [],
+          userId: state.userInfo.userId,
+          userName: state.userInfo.userName
+        }
+
+        state.userInfo.projects.forEach(({ projectId, projectLocalID }) => {
+          argu.projectId.push(projectId)
+          argu.projectLocalId.push(projectLocalID)
+        })
+
+        return argu
+      }
+    })
+  },
+  async beforeMount () {
+    console.log('beforeMount')
+  },
+  mounted () {
+    console.log('mounted')
+    window.addEventListener('resize', this.toTableHeight, false)
+    this.$nextTick(() => {
+      this.toTableHeight()
+      // this.$ploading.local({
+      //   el: this.$refs.localDom,
+      //   size: 'min'
+      // })
+      this.getTableData()
+    })
+  },
+  beforeDestroy () {
+    // 页面刷新机制 销毁绑定事件
+    window.removeEventListener('resize', this.toTableHeight, false)
+  }
+}
+</script>
+
+<style lang="less" scoped>
+  .draft{
+    width: 100%;
+    height: 100%;
+    padding: 0 14px 0 16px;
+    position:absolute;
+    top: 0;
+    left: 0;
+    background: #fff;
+    z-index: 10;
+    .breadCon{
+      height: 40px;
+      background: #F8F9FA;
+      display: flex;
+      align-items: center;
+    }
+    .topCon{
+      height: 72px;
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      .topRight{
+        &>div{
+          margin-left: 12px;
+        }
+      }
+    }
+    .mainCon{
+      height: calc(100% - 112px);
+      overflow: hidden;
+      .tableCon{
+        height: calc(100% - 70px);
+      }
+      .pageCon{
+        height: 70px;
+      }
+    }
+    .paginationCon {
+      height: 70px;
+      display: flex;
+      justify-content: flex-end;
+    }
+  }
+</style>

+ 479 - 0
src/views/demandmanage/editor.vue

@@ -0,0 +1,479 @@
+<template>
+  <div class="demandmanageuser" ref="localDom">
+    <div class="topCon">
+      <div class="topLeft">
+        <Select
+          isReadOnly
+          @change="demandTypeChange"
+          :selectdata="demandTypes"
+          v-model="demandType"
+          :placeholder="'请选择'"
+          hideClear
+        />
+        <div class="waitReply">待回复需求 <b v-text="pageCount">5</b> 条</div>
+      </div>
+      <div class="topRight">
+        <Input placeholder="主题、编码" @clear="pressEnter" iconType="search" v-model="matchingCond" @pressEnter="pressEnter"/>
+      </div>
+    </div>
+    <div class="mainCon">
+      <div class="tableCon" id="tableCon-user">
+        <TableGrid
+          v-if="tableHeight"
+          width="100%"
+          :maxHeight="tableHeight"
+          :header="header"
+          :data="tableData"
+          :key="'tableGrid'+demandType"
+          :rowTools="rowTools"
+          @sortHandle="sortHandle"
+          @screenHandle="screenHandle"
+          @rowToolHandle="rowToolHandle"
+        />
+      </div>
+      <div class="paginationCon">
+        <Pagination
+          :pageCountShow="true"
+          :page.sync="pagination.current"
+          :pageSize="pagination.size"
+          :pageCount="pageCount"
+          :pageSizeSetting="true"
+          @change="paginationChange"
+        />
+      </div>
+    </div>
+    <Detail
+      v-if="showDetail"
+      v-model="showDetail"
+      :handle="handleDetail"
+      cancelButtonType="default"
+      @cancel="DetailCancel($event), (showDetail = false)"
+      @confirm="DetailConfirm($event), (showDetail = false)"
+    />
+  </div>
+</template>
+<script>
+import { mapState } from 'vuex'
+import {
+  Select,
+  Input,
+  TableGrid,
+  Pagination
+} from 'meri-design'
+import Detail from './Detail'
+export default {
+  name: 'demandmanageuser',
+  components: {
+    Detail,
+    Select,
+    Input,
+    TableGrid,
+    Pagination
+  },
+  data () {
+    return {
+      handleDetail: {
+        user: true,
+        type: '详情'
+      },
+      showDetail: false,
+      tableHeight: 300,
+      pagination: {
+        current: 1,
+        size: 10
+      },
+      pageCount: 99,
+      rowTools: {
+        open: true, // 开启显示操作
+        fixed: 'right', // 与header数据的fixed一样
+        width: 120, // 与header数据的width一样
+        align: '', // 与header数据的align一样
+        text: '', // 头部显示的文字,默认是操作
+        moreOpen: true,
+        event: 'rowToolHandle'
+      },
+      name: 'ddd',
+      demandType: 0, // 下拉筛选条件 - 0:所有需求 1:我的需求
+      demandTypeTemp: 0,
+      demandTypes: [{ id: 0, name: '待回复' }, { id: 1, name: '已回复' }],
+      matchingCond: '', // 模糊匹配 - 编码、主题
+      productCond: [1, 2, 3], // 产线条件
+      stateCond: [1, 5], // 状态条件
+      orders: [
+        // {
+        //   column: 'submitTime',
+        //   asc: true
+        // }
+      ],
+      line: { 1: '业务', 2: '研发', 3: '实施' },
+      state: { 1: '待回复', 2: '已接受', 3: '已拒绝', 4: '待完善', 5: '论证中' },
+      header: [
+        {
+          key: 'userName',
+          text: '用户名',
+          width: 100
+        },
+        {
+          key: 'deptName',
+          text: '部门',
+          width: 180
+        },
+        {
+          key: 'productLine',
+          text: '条线',
+          width: 100,
+          screen: {
+            open: true,
+            status: 'single',
+            data: [
+              { id: 'all', name: '全部' },
+              { id: '1', name: '业务' },
+              { id: '2', name: '研发' },
+              { id: '3', name: '实施' }
+            ],
+            selectId: '1',
+            selectName: '业务',
+            event: 'screenHandle'
+          }
+        },
+        {
+          key: 'code',
+          text: '编码',
+          width: 100
+        },
+        {
+          key: 'subject',
+          text: '主题'
+        },
+        {
+          key: 'submitTime',
+          text: '提交时间',
+          width: 180,
+          sort: { open: true, event: 'sortHandle' }
+        },
+        {
+          key: 'answerTime',
+          text: '回复时间',
+          width: 180,
+          sort: { open: true, event: 'sortHandle' }
+        },
+        {
+          key: 'billState', // 需求状态 - 0:待提交 1:待回复 2:已接受 3:已拒绝 4:待完善 5:论证中
+          text: '当前状态',
+          width: 120,
+          screen: {
+            open: true,
+            status: 'single',
+            data: [
+              { id: 'all', name: '全部' },
+              { id: '1', name: '待回复' },
+              // { id: '2', name: '已接受' },
+              // { id: '3', name: '已拒绝' },
+              // { id: '4', name: '待完善' },
+              { id: '5', name: '论证中' }
+            ],
+            event: 'screenHandle'
+          }
+        }
+      ],
+      tableData: []
+    }
+  },
+  methods: {
+    // 详情 编辑 弹窗操作
+    DetailCancel (detail) {
+      console.log(detail)
+    },
+    async DetailConfirm (detail) {
+      await this.$axios.post(this.$api.demandAnswer, detail)
+      this.getTableData()
+    },
+    // 触发 新增需求弹窗
+    addDemand () {
+      this.handleDetail = {
+        user: true,
+        type: '新增需求',
+        userName: this.UserAndPro.userName,
+        detail: {}
+      }
+      this.showDetail = true
+    },
+    // 列表操作按钮的处理
+    rowToolHandle (handle) {
+      // {itemId: '1438483235070820353', toolId: 'xq'}
+      const { itemId, toolId } = handle
+      const detail = this.tableData.filter(obj => obj.id === itemId)[0]?.detail
+      if (toolId === 'hf') {
+        this.handleDetail = {
+          user: false,
+          type: detail.billState === 5 ? '继续回复' : '回复',
+          userName: this.UserAndPro.userName,
+          detail: detail
+        }
+        this.showDetail = true
+      } else {
+        this.handleDetail = {
+          user: false,
+          type: '详情',
+          detail: detail
+        }
+        this.showDetail = true
+      }
+    },
+    // 页码操作
+    paginationChange (page) {
+      // {newPage: 1, newPageSize: 40}
+      this.getTableData(page)
+    },
+    // 状态颜色处理
+    toColor (detail) {
+      const { submitTime, estimateTime, billState } = detail
+      // 待回复:蓝色 #0091FF
+      // 论证中回复且时间未超过的:黄色 #F58300
+      // 论证中回复且时间超过了:红色 #F54E45
+      // 待回复且时间超过了:红色 #F54E45
+      // 已拒绝:灰色 #8D9399
+      // 已接受:绿色 #34C724
+      // 待完善:灰色 #8D9399
+      // 需求状态 - 0:待提交 1:待回复 2:已接受 3:已拒绝 4:待完善 5:论证中
+      let color = ''
+      const flag = estimateTime && this.$moment(estimateTime, 'YYYYMMDDHHmmss').valueOf() < this.$moment().valueOf()
+      const flag2 = submitTime && this.$moment().diff(this.$moment(submitTime, 'YYYYMMDDHHmmss'), 'day') >= 7
+      switch (billState) {
+        case 1:
+          color = flag2 ? '#F54E45' : '#0091FF'
+          break
+        case 2:
+          color = '#34C724'
+          break
+        case 3:
+          color = '#8D9399'
+          break
+        case 4:
+          color = '#8D9399'
+          break
+        default:
+          color = flag ? '#F54E45' : '#F58300'
+      }
+      return color
+    },
+    // 列表数据格式处理
+    toTableData (data) {
+      const arr = []
+      data.forEach(obj => {
+        const temp = {}
+        temp.detail = JSON.parse(JSON.stringify(obj))
+        Object.keys(obj).forEach(key => {
+          const cell = {
+            text: ''
+          }
+          if (!obj[key] && obj[key] !== 0) {
+            cell.text = '--'
+          } else {
+            switch (key) {
+              case 'productLine':
+                cell.text = this.line[obj[key]]
+                break
+              case 'submitTime':
+              case 'answerTime':
+                cell.text = this.$moment(obj[key]).format('YYYY-MM-DD HH:mm:ss')
+                break
+              case 'billState':
+                cell.text = this.state[obj[key]]
+                cell.dot = this.toColor(temp.detail)
+                Object.assign(temp.detail, cell)
+                break
+              default:
+                cell.text = obj[key]
+            }
+          }
+
+          temp[key] = key === 'id' ? obj[key] : cell
+        })
+        if (obj.billState === 1 || obj.billState === 5) {
+          temp.tools = {
+            data: [
+              { id: 'hf', name: '回复' }
+            ]
+          }
+        } else {
+          temp.tools = {
+            data: [
+              { id: 'xq', name: '详情' }
+            ]
+          }
+        }
+        arr.push(temp)
+      })
+      return arr
+    },
+    // 待回复 已回复 切换
+    demandTypeChange (data) {
+      // {id: 1, name: '我的需求'}
+      if (this.demandTypeTemp === data.id) return
+      this.demandTypeTemp = this.demandType
+      if (data.id === 0) {
+        this.stateCond = [1, 5]
+        this.header[7].screen.data = [
+          { id: 'all', name: '全部' },
+          { id: '1', name: '待回复' },
+          { id: '5', name: '论证中' }
+        ]
+      } else {
+        this.stateCond = [2, 3, 4]
+        this.header[7].screen.data = [
+          { id: 'all', name: '全部' },
+          { id: '2', name: '已接受' },
+          { id: '3', name: '已拒绝' },
+          { id: '4', name: '待完善' }
+        ]
+      }
+      this.getTableData()
+    },
+    // 搜索回车查询
+    pressEnter () {
+      this.getTableData()
+    },
+    // 处理滚动条 hover显示不挤占内容位置
+    toTableHeight () {
+      const dom = document.getElementById('tableCon-user')
+      if (!dom) return
+      this.tableHeight = dom.offsetHeight
+    },
+    // 排序操作
+    sortHandle (type, key) {
+      // ord submitTime
+      // ord submitTime
+      this.orders = [
+        {
+          column: key,
+          asc: type === 'seq'
+        }
+      ]
+      this.getTableData()
+    },
+    // 筛选操作
+    screenHandle (key, type) {
+      // productLine all
+      // billState all
+      console.log(type)
+      if (key === 'productLine') {
+        this.productCond = type === 'all' ? [1, 2, 3] : [Number(type)]
+      } else {
+        this.stateCond = type === 'all' ? (this.demandType ? [2, 3, 4] : [1, 5]) : [Number(type)]
+      }
+      this.getTableData()
+    },
+    // 获取列表
+    getTableData (page) {
+      if (page) {
+        if (page.newPageSize !== this.pagination.size) this.pagination.size = page.newPageSize
+      } else {
+        this.pagination.current = 1
+      }
+      this.$axios.post(this.$api[this.demandType ? 'demandQueryDone' : 'demandQueryTodo'], {
+        ...this.pagination,
+        creatorCond: 0, // 下拉筛选条件 - 0:所有需求 1:我的需求
+        matchingCond: this.matchingCond, // 模糊匹配 - 编码、主题
+        productCond: this.productCond, // 产线条件
+        stateCond: this.stateCond, // 状态条件
+        orders: this.orders
+      }).then((res) => {
+        const data = res.data
+        this.pageCount = data.count
+        this.tableData = this.toTableData(data.data)
+      })
+    }
+  },
+  computed: {
+    ...mapState({
+      ProjectList: (state) =>
+        state.userInfo.projects.map(
+          ({ projectId: id, projectName: name, projectLocalID: localId }) => ({
+            id,
+            name,
+            localId
+          })
+        ),
+      UserAndPro (state) {
+        const argu = {
+          projectId: [],
+          projectLocalId: [],
+          userId: state.userInfo.userId,
+          userName: state.userInfo.userName
+        }
+
+        state.userInfo.projects.forEach(({ projectId, projectLocalID }) => {
+          argu.projectId.push(projectId)
+          argu.projectLocalId.push(projectLocalID)
+        })
+
+        return argu
+      }
+    })
+  },
+  async beforeMount () {
+    console.log('beforeMount')
+  },
+  mounted () {
+    console.log('mounted')
+    window.addEventListener('resize', this.toTableHeight, false)
+    this.$nextTick(() => {
+      this.toTableHeight()
+      // this.$ploading.local({
+      //   el: this.$refs.localDom,
+      //   size: 'min'
+      // })
+      this.getTableData()
+    })
+  },
+  beforeDestroy () {
+    // 页面刷新机制 销毁绑定事件
+    window.removeEventListener('resize', this.toTableHeight, false)
+  }
+}
+</script>
+
+<style lang="less" scoped>
+  .demandmanageuser{
+    width: 100%;
+    height: 100%;
+    padding: 0 14px 0 16px;
+    .topCon{
+      height: 72px;
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      .topLeft{
+        display: flex;
+        align-items: center;
+        .waitReply{
+          padding-left: 12px;
+          &>b{
+            color: #F54E45;
+          }
+        }
+      }
+      .topRight{
+        &>div{
+          margin-left: 12px;
+        }
+      }
+    }
+    .mainCon{
+      height: calc(100% - 72px);
+      overflow: hidden;
+      .tableCon{
+        height: calc(100% - 70px);
+      }
+      .pageCon{
+        height: 70px;
+      }
+    }
+    .paginationCon {
+      height: 70px;
+      display: flex;
+      justify-content: flex-end;
+    }
+  }
+</style>

+ 564 - 0
src/views/demandmanage/user.vue

@@ -0,0 +1,564 @@
+<template>
+  <div class="demandmanageuser" ref="localDom">
+    <div class="topCon">
+      <div class="topLeft">
+        <Select
+          isReadOnly
+          @change="creatorCondChange"
+          :selectdata="creatorConds"
+          v-model="creatorCond"
+          :placeholder="'请选择'"
+          hideClear
+        />
+      </div>
+      <div class="topRight">
+        <Input placeholder="主题、编码" @clear="pressEnter" iconType="search" v-model="matchingCond" @pressEnter="pressEnter"/>
+        <Badge :content="draft.count">
+          <Button type="default" @click="openDraft">草稿箱</Button>
+        </Badge>
+        <Button type="primary" @click="addDemand">新增需求</Button>
+      </div>
+    </div>
+    <div class="mainCon">
+      <div class="tableCon" id="tableCon-user">
+        <TableGrid
+          v-if="tableHeight"
+          width="100%"
+          :maxHeight="tableHeight"
+          :header="header"
+          :data="tableData"
+          key="tableGrid"
+          :rowTools="rowTools"
+          @sortHandle="sortHandle"
+          @screenHandle="screenHandle"
+          @rowToolHandle="rowToolHandle"
+        />
+      </div>
+      <div class="paginationCon">
+        <Pagination
+          :pageCountShow="true"
+          :page.sync="pagination.current"
+          :pageSize="pagination.size"
+          :pageCount="pageCount"
+          :pageSizeSetting="true"
+          @change="paginationChange"
+        />
+      </div>
+    </div>
+    <Detail
+      v-if="showDetail"
+      v-model="showDetail"
+      :handle="handleDetail"
+      @cancel="DetailCancel($event), (showDetail = false)"
+      @confirm="DetailConfirm($event), (showDetail = false)"
+      @save="DetailSave($event), (showDetail = false)"
+    />
+    <Draft v-if="showDraft"
+      @back="DraftBack($event),(showDraft = false)"
+    />
+    <Modal
+      :show="modalStatusTip"
+      title="提示"
+      mode="tip"
+      type="info"
+      @close="modalClose"
+    >
+      <template #content>
+        数据删除不可恢复,是否继续删除?
+      </template>
+      <template #handle>
+        <Button @click="modalClose" type="default">取消</Button>
+        <Button @click="modalConfirm" type="primary">确认</Button>
+      </template>
+    </Modal>
+  </div>
+</template>
+<script>
+import { mapState } from 'vuex'
+import {
+  Select,
+  Badge,
+  Input,
+  TableGrid,
+  Pagination,
+  Modal
+} from 'meri-design'
+import Detail from './Detail'
+import Draft from './Draft'
+export default {
+  name: 'demandmanageuser',
+  components: {
+    Detail,
+    Draft,
+    Select,
+    Badge,
+    Input,
+    TableGrid,
+    Pagination,
+    Modal
+  },
+  data () {
+    return {
+      detailId: '',
+      modalStatusTip: false,
+      showDraft: false,
+      handleDetail: {
+        user: true,
+        type: '详情'
+      },
+      showDetail: false,
+      tableHeight: 300,
+      pagination: {
+        current: 1,
+        size: 10
+      },
+      pageCount: 99,
+      rowTools: {
+        open: true, // 开启显示操作
+        fixed: 'right', // 与header数据的fixed一样
+        width: 120, // 与header数据的width一样
+        align: '', // 与header数据的align一样
+        text: '', // 头部显示的文字,默认是操作
+        moreOpen: true,
+        event: 'rowToolHandle'
+      },
+      name: 'ddd',
+      creatorCond: 0, // 下拉筛选条件 - 0:所有需求 1:我的需求
+      creatorConds: [{ id: 0, name: '所有需求' }, { id: 1, name: '我的需求' }],
+      matchingCond: '', // 模糊匹配 - 编码、主题
+      productCond: [1, 2, 3], // 产线条件
+      stateCond: [1, 2, 3, 4, 5], // 状态条件
+      orders: [
+        // {
+        //   column: 'submitTime',
+        //   asc: true
+        // }
+      ],
+      draft: {
+        data: [],
+        count: 0
+      },
+      line: { 1: '业务', 2: '研发', 3: '实施' },
+      state: { 1: '待回复', 2: '已接受', 3: '已拒绝', 4: '待完善', 5: '论证中' },
+      header: [
+        {
+          key: 'userName',
+          text: '用户名',
+          width: 100
+        },
+        {
+          key: 'deptName',
+          text: '部门',
+          width: 180
+        },
+        {
+          key: 'productLine',
+          text: '条线',
+          width: 100,
+          screen: {
+            open: true,
+            status: 'single',
+            data: [
+              { id: 'all', name: '全部' },
+              { id: '1', name: '业务' },
+              { id: '2', name: '研发' },
+              { id: '3', name: '实施' }
+            ],
+            selectId: '1',
+            selectName: '业务',
+            event: 'screenHandle'
+          }
+        },
+        {
+          key: 'code',
+          text: '编码',
+          width: 100
+        },
+        {
+          key: 'subject',
+          text: '主题'
+        },
+        {
+          key: 'submitTime',
+          text: '提交时间',
+          width: 180,
+          sort: { open: true, event: 'sortHandle' }
+        },
+        {
+          key: 'answerTime',
+          text: '回复时间',
+          width: 180,
+          sort: { open: true, event: 'sortHandle' }
+        },
+        {
+          key: 'billState', // 需求状态 - 0:待提交 1:待回复 2:已接受 3:已拒绝 4:待完善 5:论证中
+          text: '当前状态',
+          width: 120,
+          screen: {
+            open: true,
+            status: 'single',
+            data: [
+              { id: 'all', name: '全部' },
+              { id: '1', name: '待回复' },
+              { id: '2', name: '已接受' },
+              { id: '3', name: '已拒绝' },
+              { id: '4', name: '待完善' },
+              { id: '5', name: '论证中' }
+            ],
+            event: 'screenHandle'
+          }
+        }
+      ],
+      temp: [
+        {
+          id: '1438483235070820355',
+          valid: 1,
+          creationTime: 1631796103000,
+          creator: '1e7051ee780d4355813795f65809f815',
+          code: '10002',
+          userName: 'csgo',
+          deptName: 'BDTP',
+          productLine: 1,
+          subject: '主题',
+          content: '内容',
+          submitTime: '202109091212',
+          answerTime: '',
+          billState: 1
+        }
+      ],
+      tableData: [
+        // {
+        //   id: 'id',
+        //   userName: { text: 'answerTime' },
+        //   deptName: { text: 'answerTime' },
+        //   productLine: { text: '研发' },
+        //   code: { text: 'answerTime' },
+        //   subject: { text: 'answerTime' },
+        //   content: { text: 'answerTime' },
+        //   submitTime: { text: 'answerTime' },
+        //   answerTime: { text: 'answerTime' },
+        //   billState: { text: '待回复' },
+        //   tools: {
+        //     data: [
+        //       { id: 'bj', name: '编辑' },
+        //       { id: 'sc', name: '删除' }
+        //     ]
+        //   }
+        // }
+      ]
+    }
+  },
+  methods: {
+    modalClose () {
+      this.modalStatusTip = false
+    },
+    modalConfirm () {
+      this.$axios.post(this.$api.demandDelete, [this.detailId]).then(() => {
+        this.getTableData()
+      })
+      this.modalStatusTip = false
+    },
+    openDraft () {
+      this.showDraft = true
+    },
+    DraftBack () {
+      this.getTableData()
+      this.getDraft()
+    },
+    // 详情 编辑 弹窗操作
+    DetailCancel (detail) {
+      console.log(detail)
+    },
+    async DetailConfirm (detail) {
+      await this.$axios.post(this.$api.demandSubmit, detail)
+      this.getTableData()
+    },
+    async DetailSave (detail) {
+      await this.$axios.post(this.$api.demandSave, detail)
+      this.getDraft()
+    },
+    // 触发 新增需求弹窗
+    addDemand () {
+      this.handleDetail = {
+        user: true,
+        type: '新增需求',
+        userName: this.UserAndPro.userName,
+        detail: {}
+      }
+      this.showDetail = true
+    },
+    // 列表操作按钮的处理
+    rowToolHandle (handle) {
+      // {itemId: '1438483235070820353', toolId: 'xq'}
+      const { itemId, toolId } = handle
+      const detail = this.tableData.filter(obj => obj.id === itemId)[0]?.detail
+      if (toolId === 'xg') { // 修改
+        this.handleDetail = {
+          user: true,
+          type: '修改需求',
+          detail: detail
+        }
+        this.showDetail = true
+      } else if (toolId === 'cx') {
+        this.detailId = detail.id
+        this.modalStatusTip = true
+      } else {
+        this.handleDetail = {
+          user: true,
+          type: '详情',
+          detail: detail
+        }
+        this.showDetail = true
+      }
+    },
+    // 页码操作
+    paginationChange (page) {
+      // {newPage: 1, newPageSize: 40}
+      this.getTableData(page)
+    },
+    // 状态颜色处理
+    toColor (detail) {
+      const { submitTime, estimateTime, billState } = detail
+      // 待回复:蓝色 #0091FF
+      // 论证中回复且时间未超过的:黄色 #F58300
+      // 论证中回复且时间超过了:红色 #F54E45
+      // 待回复且时间超过了:红色 #F54E45
+      // 已拒绝:灰色 #8D9399
+      // 已接受:绿色 #34C724
+      // 待完善:灰色 #8D9399
+      // 需求状态 - 0:待提交 1:待回复 2:已接受 3:已拒绝 4:待完善 5:论证中
+      let color = ''
+      const flag = estimateTime && this.$moment(estimateTime, 'YYYYMMDDHHmmss').valueOf() < this.$moment().valueOf()
+      const flag2 = submitTime && this.$moment().diff(this.$moment(submitTime, 'YYYYMMDDHHmmss'), 'day') >= 7
+      switch (billState) {
+        case 1:
+          color = flag2 ? '#F54E45' : '#0091FF'
+          break
+        case 2:
+          color = '#34C724'
+          break
+        case 3:
+          color = '#8D9399'
+          break
+        case 4:
+          color = '#8D9399'
+          break
+        default:
+          color = flag ? '#F54E45' : '#F58300'
+      }
+      return color
+    },
+    // 列表数据格式处理
+    toTableData (data) {
+      const arr = []
+      data.forEach(obj => {
+        const temp = {}
+        temp.detail = JSON.parse(JSON.stringify(obj))
+        Object.keys(obj).forEach(key => {
+          const cell = {
+            text: ''
+          }
+          if (!obj[key] && obj[key] !== 0) {
+            cell.text = '--'
+          } else {
+            switch (key) {
+              case 'productLine':
+                cell.text = this.line[obj[key]]
+                break
+              case 'submitTime':
+              case 'answerTime':
+                cell.text = this.$moment(obj[key]).format('YYYY-MM-DD HH:mm:ss')
+                break
+              case 'billState':
+                cell.text = this.state[obj[key]]
+                cell.dot = this.toColor(temp.detail)
+                Object.assign(temp.detail, cell)
+                break
+              default:
+                cell.text = obj[key]
+            }
+          }
+
+          temp[key] = key === 'id' ? obj[key] : cell
+        })
+        if (obj.creator === this.UserAndPro.userId && obj.billState !== 2 && obj.billState !== 3 && obj.billState !== 5) {
+          temp.tools = obj.billState === 4 ? {
+            data: [
+              { id: 'xg', name: '修改' },
+              { id: 'cx', name: '撤销' }
+            ]
+          } : {
+            data: [
+              { id: 'xq', name: '详情' },
+              { id: 'cx', name: '撤销' }
+            ]
+          }
+        } else {
+          temp.tools = {
+            data: [
+              { id: 'xq', name: '详情' }
+            ]
+          }
+        }
+        arr.push(temp)
+      })
+      return arr
+    },
+    // 所有需求 自己需求 切换
+    creatorCondChange (data) {
+      // {id: 1, name: '我的需求'}
+      // this.creatorCond = data.id
+      this.getTableData()
+    },
+    // 搜索回车查询
+    pressEnter () {
+      this.getTableData()
+    },
+    // 处理滚动条 hover显示不挤占内容位置
+    toTableHeight () {
+      const dom = document.getElementById('tableCon-user')
+      if (!dom) return
+      this.tableHeight = dom.offsetHeight
+    },
+    // 排序操作
+    sortHandle (type, key) {
+      // ord submitTime
+      // ord submitTime
+      this.orders = [
+        {
+          column: key,
+          asc: type === 'seq'
+        }
+      ]
+      this.getTableData()
+    },
+    // 筛选操作
+    screenHandle (key, type) {
+      // productLine all
+      // billState all
+      console.log(type)
+      if (key === 'productLine') {
+        this.productCond = type === 'all' ? [1, 2, 3] : [Number(type)]
+      } else {
+        this.stateCond = type === 'all' ? [1, 2, 3, 4, 5] : [Number(type)]
+      }
+      this.getTableData()
+    },
+    // 获取列表
+    getTableData (page) {
+      if (page) {
+        if (page.newPageSize !== this.pagination.size) this.pagination.size = page.newPageSize
+      } else {
+        this.pagination.current = 1
+      }
+      this.$axios.post(this.$api.demandQuerySubmitted, {
+        ...this.pagination,
+        creatorCond: this.creatorCond, // 下拉筛选条件 - 0:所有需求 1:我的需求
+        matchingCond: this.matchingCond, // 模糊匹配 - 编码、主题
+        productCond: this.productCond, // 产线条件
+        stateCond: this.stateCond, // 状态条件
+        orders: this.orders
+      }).then((res) => {
+        const data = res.data
+        this.pageCount = data.count
+        this.tableData = this.toTableData(data.data)
+      })
+    },
+    // 获取草稿
+    getDraft () {
+      this.$axios.post(this.$api.demandQueryDraft, {
+        creatorCond: 1,
+        matchingCond: '',
+        productCond: [1, 2, 3],
+        stateCond: [],
+        size: 20,
+        current: 1,
+        orders: []
+      }).then((res) => {
+        this.draft = res.data
+      })
+    }
+  },
+  computed: {
+    ...mapState({
+      ProjectList: (state) =>
+        state.userInfo.projects.map(
+          ({ projectId: id, projectName: name, projectLocalID: localId }) => ({
+            id,
+            name,
+            localId
+          })
+        ),
+      UserAndPro (state) {
+        const argu = {
+          projectId: [],
+          projectLocalId: [],
+          userId: state.userInfo.userId,
+          userName: state.userInfo.userName
+        }
+
+        state.userInfo.projects.forEach(({ projectId, projectLocalID }) => {
+          argu.projectId.push(projectId)
+          argu.projectLocalId.push(projectLocalID)
+        })
+
+        return argu
+      }
+    })
+  },
+  async beforeMount () {
+    console.log('beforeMount')
+  },
+  mounted () {
+    console.log('mounted')
+    window.addEventListener('resize', this.toTableHeight, false)
+    this.$nextTick(() => {
+      this.toTableHeight()
+      // this.$ploading.local({
+      //   el: this.$refs.localDom,
+      //   size: 'min'
+      // })
+      this.getDraft()
+      this.getTableData()
+    })
+  },
+  beforeDestroy () {
+    // 页面刷新机制 销毁绑定事件
+    window.removeEventListener('resize', this.toTableHeight, false)
+  }
+}
+</script>
+
+<style lang="less" scoped>
+  .demandmanageuser{
+    width: 100%;
+    height: 100%;
+    padding: 0 14px 0 16px;
+    .topCon{
+      height: 72px;
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      .topRight{
+        &>div{
+          margin-left: 12px;
+        }
+      }
+    }
+    .mainCon{
+      height: calc(100% - 72px);
+      overflow: hidden;
+      .tableCon{
+        height: calc(100% - 70px);
+      }
+      .pageCon{
+        height: 70px;
+      }
+    }
+    .paginationCon {
+      height: 70px;
+      display: flex;
+      justify-content: flex-end;
+    }
+  }
+</style>

+ 42 - 0
tsconfig.json

@@ -0,0 +1,42 @@
+{
+  "compilerOptions": {
+    "target": "esnext",
+    "module": "esnext",
+    "strict": true,
+    "jsx": "preserve",
+    "importHelpers": true,
+    "moduleResolution": "node",
+    "experimentalDecorators": true,
+    "skipLibCheck": true,
+    "esModuleInterop": true,
+    "allowSyntheticDefaultImports": true,
+    "suppressImplicitAnyIndexErrors":true,
+    "sourceMap": true,
+    "baseUrl": ".",
+    "types": [
+      "webpack-env",
+      "jest"
+    ],
+    "paths": {
+      "@/*": [
+        "src/*"
+      ]
+    },
+    "lib": [
+      "esnext",
+      "dom",
+      "dom.iterable",
+      "scripthost"
+    ]
+  },
+  "include": [
+    "src/**/*.ts",
+    "src/**/*.tsx",
+    "src/**/*.vue",
+    "tests/**/*.ts",
+    "tests/**/*.tsx"
+  ],
+  "exclude": [
+    "node_modules"
+  ]
+}

+ 56 - 0
version.js

@@ -0,0 +1,56 @@
+/* eslint-disable @typescript-eslint/no-var-requires */
+/** 自动记录版本信息**/
+const fs = require('fs')
+const buildPath = require('./package.json').name// 放置 version.txt的路径
+const execSync = require('child_process').execSync // 同步子进程
+const date = new Date() // Date对象
+const isZip = true // 是否自动压缩(需要安装compressing模块)
+
+// 获取时间函数
+const getDate = (df, tf) => {
+  const year = date.getFullYear(); const month = date.getMonth() + 1; const day = date.getDate(); const hour = date.getHours(); const minute = date.getMinutes()
+  const dArr = [year, month > 9 ? month : `0${month}`, day > 9 ? day : `0${day}`]
+  const tArr = [hour > 9 ? hour : `0${hour}`, minute > 9 ? minute : `0${minute}`]
+  return `${dArr.join(df)}${tf ? ' ' : ''}${tArr.join(tf)}`
+}
+
+// 写入最新的版本信息
+const branch = execSync('git symbolic-ref --short -q HEAD').toString().trim() // 分支
+const commit = execSync('git show -s --format=%h').toString().trim() // 版本号
+const message = execSync('git show -s --format=%s').toString().trim() // 说明
+const name = execSync('git show -s --format=%cn').toString().trim() // 姓名
+const email = execSync('git show -s --format=%ce').toString().trim() // 邮箱
+// const date = execSync('git show -s --format=%cd').toString().trim(); //日期
+const versionStr = `git:${commit}<${branch}>\n作者:${name}<${email}>\n日期:${getDate('-', ':')}\n说明:${message}\n`
+fs.writeFile(`${buildPath}/version.txt`, versionStr, (err) => {
+  if (err) console.err('写入失败', err)
+})
+
+// 打包文件命名
+const distName = `${buildPath}_${getDate('', '')}_git_${commit}`
+
+// 压缩文件
+if (isZip) {
+  try {
+    const compressing = require('compressing') // 压缩模块
+    compressing.zip.compressDir(buildPath, `${distName}.zip`).then(() => {
+      console.info('\x1B[32m%s\x1b[0m', '压缩成功!')
+    }).catch(err => {
+      console.error(err)
+    })
+  } catch {
+    console.error('压缩失败!')
+  }
+}
+
+// 程序运行结束
+console.info('\x1B[32m%s\x1b[0m', [
+  distName,
+  versionStr,
+  '██████╗ ███████╗██████╗ ███████╗ █████╗  ██████╗██╗   ██╗',
+  '██╔══██╗██╔════╝██╔══██╗██╔════╝██╔══██╗██╔════╝╚██╗ ██╔╝',
+  '██████╔╝█████╗  ██████╔╝███████╗███████║██║  ███╗╚████╔╝ ',
+  '██╔═══╝ ██╔══╝  ██╔══██╗╚════██║██╔══██║██║   ██║ ╚██╔╝  ',
+  '██║     ███████╗██║  ██║███████║██║  ██║╚██████╔╝  ██║   ',
+  '╚═╝     ╚══════╝╚═╝  ╚═╝╚══════╝╚═╝  ╚═╝ ╚═════╝   ╚═╝   '
+].join('\n'))

+ 74 - 0
vue.config.js

@@ -0,0 +1,74 @@
+/* eslint-disable @typescript-eslint/no-var-requires */
+const { name, title, version } = require('./package.json') // 项目信息
+// const proxy = 'http://develop.persagy.com' // 需要代理请求的nginx地址
+const webpackVersionZip = require('webpack-version-zip')
+module.exports = {
+  publicPath: `/${name}`, // 相对路径
+  outputDir: name, // 打包名称
+  assetsDir: 'static', // 静态目录
+  lintOnSave: false, // 关闭lint代码
+  productionSourceMap: false, // 生产环境是否开启sourceMap
+  parallel: require('os').cpus().length > 1, // 启用多核打包
+  css: {
+    loaderOptions: {
+      less: {
+        modifyVars: {},
+        javascriptEnabled: true
+      }
+    }
+  },
+  chainWebpack: (config) => {
+    config.plugin('html').tap(args => {
+      args[0].title = title // 修改标题
+      return args
+    })
+    // 使用svg组件
+    config.performance.set('hints', false)
+    const svgRule = config.module.rule('svg')
+    svgRule.uses.clear()
+    svgRule
+      .use('babel-loader')
+      .loader('babel-loader')
+      .end()
+      .use('vue-svg-loader')
+      .loader('vue-svg-loader')
+    // 打包时创建version文件
+    if (process.env.NODE_ENV === 'production') {
+      config.plugin('version')
+        .use(webpackVersionZip, [name, false])
+    }
+  },
+  // 配置跨域
+  devServer: {
+    proxy: {
+      // '/api': {
+      //   target: proxy,
+      //   pathRewrite: {
+      //     '^/api': proxy + '/api'
+      //   }
+      // }
+      '/api/meos/EMS_SaaS_Web': {
+        target: 'http://develop.persagy.com', // 测试环境
+        changeOrigin: true,
+        pathRewrite: {
+          '^/api/meos/EMS_SaaS_Web': 'http://develop.persagy.com/api/meos/EMS_SaaS_Web' // 测试环境
+        }
+      },
+      '/api': {
+        // target: 'http://192.168.0.39:9999', // PC-39-nginx环境
+        // target: 'http://192.168.0.28:9999', // Mac-28-nginx环境
+        // target: 'http://test.persagy.com', // 测试环境
+        target: 'http://develop.persagy.com', // 开发环境
+        pathRewrite: {
+          // '^/api': 'http://192.168.0.39:9999' // PC-39-nginx环境
+          // '^/api': 'http://192.168.0.28:9999' // Mac-28-nginx环境
+          // '^/api': 'http://test.persagy.com/api' // 测试环境
+          '^/api': 'http://develop.persagy.com/api' // 开发环境
+        }
+      },
+      '/dmp-rwd-version': {
+        target: 'http://develop.persagy.com'
+      }
+    }
+  }
+}