Ver Fonte

创建项目

guhongwei há 3 anos atrás
commit
1ed6797266

+ 2 - 0
.env

@@ -0,0 +1,2 @@
+VUE_APP_AXIOS_BASE_URL = ''
+VUE_APP_ROUTER="tobaccocms"

+ 33 - 0
.eslintrc.js

@@ -0,0 +1,33 @@
+// https://eslint.org/docs/user-guide/configuring
+
+module.exports = {
+  root: true,
+  env: {
+    node: true,
+  },
+  extends: ["plugin:vue/essential", "@vue/prettier"],
+  plugins: ["vue"],
+  rules: {
+    "max-len": [
+      "warn",
+      {
+        code: 10000,
+      },
+    ],
+    "no-unused-vars": "off",
+    "no-console": "off",
+    "prettier/prettier": [
+      "warn",
+      {
+        singleQuote: true,
+        trailingComma: "es5",
+        bracketSpacing: true,
+        jsxBracketSameLine: true,
+        printWidth: 160,
+      },
+    ],
+  },
+  parserOptions: {
+    parser: "babel-eslint",
+  },
+};

+ 24 - 0
.gitignore

@@ -0,0 +1,24 @@
+.DS_Store
+node_modules
+package-lock.json
+/dist
+
+
+# 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?

+ 0 - 0
README.md


+ 3 - 0
babel.config.js

@@ -0,0 +1,3 @@
+module.exports = {
+  presets: ["@vue/cli-plugin-babel/preset"],
+};

+ 67 - 0
package.json

@@ -0,0 +1,67 @@
+{
+  "name": "study-cms",
+  "version": "0.1.0",
+  "private": true,
+  "scripts": {
+    "serve": "vue-cli-service serve",
+    "build": "vue-cli-service build",
+    "lint": "vue-cli-service lint"
+  },
+  "dependencies": {
+    "@stomp/stompjs": "^6.1.0",
+    "animate.css": "^4.1.1",
+    "axios": "^0.21.1",
+    "babel-polyfill": "^6.26.0",
+    "core-js": "^3.6.5",
+    "echarts": "^5.1.0",
+    "element-ui": "^2.15.1",
+    "html2canvas": "^1.0.0-rc.7",
+    "jsonwebtoken": "^8.5.1",
+    "jspdf": "^2.3.1",
+    "lodash": "^4.17.21",
+    "moment": "^2.29.1",
+    "naf-core": "^0.1.2",
+    "vue": "^2.6.11",
+    "vue-i18n": "^8.24.4",
+    "vue-meta": "^2.4.0",
+    "vue-router": "^3.2.0",
+    "vuex": "^3.4.0",
+    "wangeditor": "3.1.1"
+  },
+  "devDependencies": {
+    "@vue/cli-plugin-babel": "~4.5.0",
+    "@vue/cli-plugin-eslint": "~4.5.0",
+    "@vue/cli-plugin-router": "~4.5.0",
+    "@vue/cli-plugin-vuex": "~4.5.0",
+    "@vue/cli-service": "~4.5.0",
+    "@vue/eslint-config-prettier": "^6.0.0",
+    "babel-eslint": "^10.1.0",
+    "eslint": "^6.7.2",
+    "eslint-plugin-prettier": "^3.3.1",
+    "eslint-plugin-vue": "^6.2.2",
+    "less": "^3.0.4",
+    "less-loader": "^5.0.0",
+    "prettier": "^2.2.1",
+    "vue-template-compiler": "^2.6.11"
+  },
+  "eslintConfig": {
+    "root": true,
+    "env": {
+      "node": true
+    },
+    "extends": [
+      "plugin:vue/essential",
+      "eslint:recommended",
+      "@vue/prettier"
+    ],
+    "parserOptions": {
+      "parser": "babel-eslint"
+    },
+    "rules": {}
+  },
+  "browserslist": [
+    "> 1%",
+    "last 2 versions",
+    "not dead"
+  ]
+}

BIN
public/favicon.ico


+ 17 - 0
public/index.html

@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html lang="">
+  <head>
+    <meta charset="utf-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <meta name="viewport" content="width=device-width,initial-scale=1.0">
+    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
+    <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>

+ 39 - 0
src/App.vue

@@ -0,0 +1,39 @@
+<template>
+  <div id="App">
+    <router-view></router-view>
+  </div>
+</template>
+
+<script>
+export default {
+  metaInfo: { title: 'App' },
+  name: 'App',
+  props: {},
+  components: {},
+  data: function () {
+    return {};
+  },
+  created() {},
+  methods: {},
+  computed: {},
+};
+</script>
+
+<style>
+body {
+  margin: 0;
+}
+.w_1200 {
+  width: 1200px;
+  margin: 0 auto;
+}
+p {
+  margin: 0;
+  padding: 0;
+}
+.textOver {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+</style>

BIN
src/assets/logo.png


+ 88 - 0
src/components/HelloWorld.vue

@@ -0,0 +1,88 @@
+<template>
+  <div class="hello">
+    <h1>{{ msg }}</h1>
+    <p>
+      For a guide and recipes on how to configure / customize this project,<br />
+      check out the
+      <a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
+    </p>
+    <h3>Installed CLI Plugins</h3>
+    <ul>
+      <li>
+        <a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a>
+      </li>
+      <li>
+        <a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-router" target="_blank" rel="noopener">router</a>
+      </li>
+      <li>
+        <a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-vuex" target="_blank" rel="noopener">vuex</a>
+      </li>
+      <li>
+        <a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" target="_blank" rel="noopener">eslint</a>
+      </li>
+    </ul>
+    <h3>Essential Links</h3>
+    <ul>
+      <li>
+        <a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a>
+      </li>
+      <li>
+        <a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a>
+      </li>
+      <li>
+        <a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a>
+      </li>
+      <li>
+        <a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a>
+      </li>
+      <li>
+        <a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a>
+      </li>
+    </ul>
+    <h3>Ecosystem</h3>
+    <ul>
+      <li>
+        <a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a>
+      </li>
+      <li>
+        <a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a>
+      </li>
+      <li>
+        <a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a>
+      </li>
+      <li>
+        <a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a>
+      </li>
+      <li>
+        <a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a>
+      </li>
+    </ul>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'HelloWorld',
+  props: {
+    msg: String,
+  },
+};
+</script>
+
+<!-- Add "scoped" attribute to limit CSS to this component only -->
+<style scoped lang="less">
+h3 {
+  margin: 40px 0 0;
+}
+ul {
+  list-style-type: none;
+  padding: 0;
+}
+li {
+  display: inline-block;
+  margin: 0 10px;
+}
+a {
+  color: #42b983;
+}
+</style>

+ 198 - 0
src/components/list/list-page.vue

@@ -0,0 +1,198 @@
+<template>
+  <div id="list-page">
+    <el-row>
+      <el-col :span="24" class="main">
+        <el-col :span="24" class="top">
+          <el-col :span="3" class="left">
+            <span>|</span> <span>{{ title }}</span>
+          </el-col>
+          <el-col :span="21" class="right">
+            <el-row type="flex" justify="end">
+              <el-col :span="12" class="tabs" v-if="useTab">
+                <tabs :displayList="displayList" :dropList="dropList" @toSearch="change"></tabs>
+              </el-col>
+              <el-col :span="12" class="search" v-if="useSearch">
+                <search @toSearch="toSearch"></search>
+              </el-col>
+            </el-row>
+          </el-col>
+        </el-col>
+        <el-col :span="24" class="down">
+          <slot></slot>
+          <!-- <el-col :span="24" class="list" v-for="(item, index) in list" :key="index">
+            <el-col :span="21" class="name" @click.native="clickDetail(item.id)">
+              {{ item.p1 }}
+            </el-col>
+            <el-col :span="3" class="date">
+              {{ getDate(item.p2) }}
+            </el-col>
+            <el-col :span="24" class="brief"> 成果简介:{{ item.p3 || '暂无' }} </el-col>
+          </el-col> -->
+        </el-col>
+        <el-col :span="24" class="page" v-if="usePage">
+          <el-pagination @current-change="search" :current-page="currentPage" layout="total, prev, pager, next, jumper" :total="total" :page-size="pageSize">
+          </el-pagination>
+        </el-col>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script>
+import tabs from './tabs.vue';
+import search from './search.vue';
+import { mapState, createNamespacedHelpers } from 'vuex';
+export default {
+  name: 'list-page',
+  props: {
+    useTab: { type: Boolean, default: false },
+    useSearch: { type: Boolean, default: true },
+    displayList: { type: Array, default: () => [] },
+    dropList: { type: Array, default: () => [] },
+    list: { type: Array, default: () => [] },
+    searchModel: { type: String, default: 'name' },
+    title: { type: String },
+    total: { type: Number, default: 0 },
+    pageSize: { type: Number, default: 5 },
+    usePage: { type: Boolean, default: true },
+  },
+  components: { tabs, search },
+  data: function () {
+    return {
+      searchInfo: undefined,
+      currentPage: 1,
+      condition: {},
+    };
+  },
+  created() {},
+  methods: {
+    search(page = 1) {
+      this.currentPage = page;
+      const skip = (this.currentPage - 1) * this.pageSize;
+      let condition = { skip, limit: this.pageSize, ...this.condition };
+      if (this.searchInfo && this.searchInfo !== '') condition[this.searchModel] = this.searchInfo;
+      this.$emit('toSearch', condition);
+    },
+    toSearch(condition) {
+      if (condition) {
+        this.$set(this, `searchInfo`, condition);
+      } else {
+        this.$set(this, `searchInfo`, undefined);
+      }
+      this.currentPage = 1;
+      this.search();
+    },
+    change(condition) {
+      this.$set(this, `condition`, condition);
+      this.currentPage = 1;
+      this.search();
+      // this.$emit('toChangeTab', condition);
+    },
+  },
+  computed: {
+    ...mapState(['user', 'menuParams']),
+    pageTitle() {
+      return `${this.$route.meta.title}`;
+    },
+  },
+  metaInfo() {
+    return { title: this.$route.meta.title };
+  },
+};
+</script>
+
+<style lang="less" scoped>
+.main {
+  .top {
+    height: 49px;
+    border-bottom: 1px solid #ccc;
+    padding: 5px 0 0 0;
+    .left {
+      text-align: left;
+      span:first-child {
+        color: #22529a;
+        font-weight: bold;
+        font-size: 25px;
+      }
+      span:last-child {
+        color: #22529a;
+        font-size: 20px;
+        font-weight: bold;
+      }
+    }
+    .right {
+      .tabs {
+        span {
+          font-size: 16px;
+          padding: 8px 10px;
+          display: inline-block;
+          font-weight: bold;
+        }
+        span:hover {
+          cursor: pointer;
+          color: #409eff;
+        }
+        .btn {
+          padding: 0px 0 0 0;
+          position: relative;
+          top: 1px;
+          i {
+            font-size: 20px;
+            font-weight: bold;
+          }
+        }
+      }
+      .search {
+        /deep/.el-input__inner {
+          height: 35px;
+          line-height: 35px;
+        }
+      }
+    }
+  }
+  .down {
+    height: 500px;
+    overflow: hidden;
+    .list {
+      padding: 10px 0;
+      .name {
+        font-size: 18px;
+        font-weight: bold;
+      }
+      .date {
+        font-size: 16px;
+        text-align: center;
+      }
+      .brief {
+        font-size: 16px;
+        overflow: hidden;
+        text-overflow: ellipsis;
+        -webkit-line-clamp: 2;
+        word-break: break-all;
+        display: -webkit-box;
+        -webkit-box-orient: vertical;
+        margin: 10px 0 0 0;
+        max-height: 42px;
+      }
+    }
+    .list:hover {
+      cursor: pointer;
+      .name {
+        -webkit-transform: translateY(-3px);
+        -ms-transform: translateY(-3px);
+        transform: translateY(-3px);
+        -webkit-box-shadow: 0 0 6px #999;
+        box-shadow: 0 0 6px #999;
+        -webkit-transition: all 0.5s ease-out;
+        transition: all 0.5s ease-out;
+        color: #0085d2;
+      }
+    }
+  }
+  .page {
+    text-align: center;
+    height: 30px;
+    overflow: hidden;
+  }
+}
+</style>

+ 37 - 0
src/components/list/search.vue

@@ -0,0 +1,37 @@
+<template>
+  <div id="search">
+    <el-input placeholder="请输入名称" v-model="input" class="input-with-select" clearable>
+      <el-button slot="append" icon="el-icon-search" @click="searchData()"></el-button>
+    </el-input>
+  </div>
+</template>
+
+<script>
+import { mapState, createNamespacedHelpers } from 'vuex';
+export default {
+  name: 'search',
+  components: {},
+  data: function () {
+    return {
+      input: undefined,
+    };
+  },
+  created() {},
+  methods: {
+    searchData() {
+      this.$emit('toSearch', !this.input ? undefined : this.input);
+    },
+  },
+  computed: {
+    ...mapState(['user', 'menuParams']),
+    pageTitle() {
+      return `${this.$route.meta.title}`;
+    },
+  },
+  metaInfo() {
+    return { title: this.$route.meta.title };
+  },
+};
+</script>
+
+<style lang="less" scoped></style>

+ 65 - 0
src/components/list/tabs.vue

@@ -0,0 +1,65 @@
+<template>
+  <div id="tabs">
+    <!-- 主要成果单位 -->
+    <span v-for="(item, index) in displayList" :key="index" @click="change(item.name)">{{ item.name }}</span>
+    <!-- 其他 -->
+    <el-dropdown trigger="click" @command="change">
+      <span class="el-dropdown-link btn"><i class="el-icon-d-arrow-right"></i> </span>
+      <el-dropdown-menu slot="dropdown">
+        <el-dropdown-item v-for="(item, index) in dropList" :key="index" :command="item.name">{{ item.name }}</el-dropdown-item>
+      </el-dropdown-menu>
+    </el-dropdown>
+  </div>
+</template>
+
+<script>
+import { mapState, createNamespacedHelpers } from 'vuex';
+export default {
+  name: 'tabs',
+  props: {
+    displayList: { type: Array, default: () => [] },
+    dropList: { type: Array, default: () => [] },
+  },
+  components: {},
+  data: function () {
+    return {};
+  },
+  created() {},
+  methods: {
+    change(data) {
+      this.$emit('toSearch', { company: data });
+    },
+  },
+  computed: {
+    ...mapState(['user', 'menuParams']),
+    pageTitle() {
+      return `${this.$route.meta.title}`;
+    },
+  },
+  metaInfo() {
+    return { title: this.$route.meta.title };
+  },
+};
+</script>
+
+<style lang="less" scoped>
+span {
+  font-size: 16px;
+  padding: 8px 10px;
+  display: inline-block;
+  font-weight: bold;
+}
+span:hover {
+  cursor: pointer;
+  color: #409eff;
+}
+.btn {
+  padding: 0px 0 0 0;
+  position: relative;
+  top: 1px;
+  i {
+    font-size: 20px;
+    font-weight: bold;
+  }
+}
+</style>

+ 43 - 0
src/main.js

@@ -0,0 +1,43 @@
+import Vue from 'vue';
+import App from './App.vue';
+import router from './router';
+import store from './store';
+import '@/plugins/element.js';
+import '@/plugins/axios';
+import '@/plugins/check-res';
+import '@/plugins/meta';
+import '@/plugins/filters';
+import '@/plugins/loading';
+import '@/plugins/var';
+import '@/plugins/methods';
+import '@/plugins/setting';
+import '@/plugins/components';
+import '@common/src/assets/icon/iconfont.css';
+import 'animate.css';
+// 管理中心页面效果布局所需插件
+import ElementUI from 'element-ui';
+import VueI18n from 'vue-i18n';
+import 'babel-polyfill';
+import { messages } from '@common/src/components/admin-frame/i18n';
+import '@common/src/assets/css/main.css';
+// 默认样式
+import '@common/src/assets/css/color-dark.css';
+// element默认主题
+import '@common/src/assets/css/theme-ele/index.css';
+import '@common/src/assets/css/theme-ele/color-ele.css';
+// 浅绿色
+// import '@common/src/assets/css/theme-green/index.css';
+// import '@common/src/assets/css/theme-green/color-green.css';
+// 红色
+// import '@common/src/assets/css/theme-red/index.css';
+// import '@common/src/assets/css/theme-red/color-red.css';
+// 粉色
+// import '@common/src/assets/css/theme-pink/index.css';
+// import '@common/src/assets/css/theme-pink/color-pink.css';
+
+Vue.config.productionTip = false;
+Vue.use(VueI18n);
+Vue.use(ElementUI, { size: 'small' });
+const i18n = new VueI18n({ locale: 'zh', messages });
+new Vue({ router, store, i18n, render: (h) => h(App) }).$mount('#app');
+window.vm = new Vue({ router });

+ 19 - 0
src/plugins/axios.js

@@ -0,0 +1,19 @@
+import Vue from 'vue';
+import AxiosWrapper from '@/util/axios-wrapper';
+
+const Plugin = {
+  install(vue, options) {
+    // 3. 注入组件
+    vue.mixin({
+      created() {
+        if (this.$store && !this.$store.$axios) {
+          this.$store.$axios = this.$axios;
+        }
+      },
+    });
+    // 4. 添加实例方法
+    vue.prototype.$axios = new AxiosWrapper(options);
+  },
+};
+
+Vue.use(Plugin, { baseUrl: process.env.VUE_APP_AXIOS_BASE_URL });

+ 39 - 0
src/plugins/check-res.js

@@ -0,0 +1,39 @@
+/* eslint-disable no-underscore-dangle */
+/* eslint-disable no-param-reassign */
+/* eslint-disable no-unused-vars */
+/* eslint-disable no-shadow */
+import Vue from 'vue';
+import _ from 'lodash';
+import { Message } from 'element-ui';
+
+const vm = new Vue({});
+const Plugin = {
+  install(Vue, options) {
+    // 4. 添加实例方法
+    Vue.prototype.$checkRes = async (res, okText, errText) => {
+      let _okText = okText;
+      let _errText = errText;
+      if (!_.isFunction(okText) && _.isObject(okText) && okText != null) {
+        ({ okText: _okText, errText: _errText } = okText);
+      }
+      const { errcode = 0, errmsg } = res || {};
+      if (errcode === 0) {
+        if (_.isFunction(_okText)) {
+          return _okText();
+        }
+        if (_okText) {
+          Message.success(_okText);
+        }
+        return true;
+      }
+      if (_.isFunction(_errText)) {
+        return _errText();
+      }
+      Message.error(_errText || errmsg);
+      // Message({ message: _errText || errmsg, duration: 60000 });
+      return false;
+    };
+  },
+};
+
+Vue.use(Plugin);

+ 12 - 0
src/plugins/components.js

@@ -0,0 +1,12 @@
+import Vue from 'vue';
+import dataTable from '@common/src/components/frame/filter-page-table.vue';
+import dataForm from '@common/src/components/frame/form.vue';
+import eUpload from '@common/src/components/frame/e-upload.vue';
+const Plugin = (vue) => {
+  vue.prototype.$dev_mode = process.env.NODE_ENV === 'development';
+  vue.component('data-table', dataTable);
+  vue.component('data-form', dataForm);
+  vue.component('eUpload', eUpload);
+};
+
+Vue.use(Plugin);

+ 5 - 0
src/plugins/element.js

@@ -0,0 +1,5 @@
+import Vue from 'vue';
+import Element from 'element-ui';
+import 'element-ui/lib/theme-chalk/index.css';
+
+Vue.use(Element);

+ 6 - 0
src/plugins/filters.js

@@ -0,0 +1,6 @@
+import Vue from 'vue';
+import filters from '@/util/filters';
+
+for (const method in filters) {
+  Vue.filter(method, filters[method]);
+}

+ 27 - 0
src/plugins/loading.js

@@ -0,0 +1,27 @@
+/* eslint-disable no-console */
+/* eslint-disable no-param-reassign */
+
+import Vue from 'vue';
+
+const Plugin = {
+  // eslint-disable-next-line no-unused-vars
+  install(vue, options) {
+    // 3. 注入组件
+    vue.mixin({
+      created() {
+        // eslint-disable-next-line no-underscore-dangle
+        const isRoot = this.constructor === Vue;
+        // console.log(`rootId:${rootVue_uid}; thisId:${this._uid}`);
+        // if (rootVue_uid !== 3) {
+        //   console.log(this);
+        // }
+        if (isRoot) {
+          const el = document.getElementById('loading');
+          if (el) el.style.display = 'none';
+        }
+      },
+    });
+  },
+};
+
+Vue.use(Plugin, { baseUrl: process.env.VUE_APP_AXIOS_BASE_URL });

+ 4 - 0
src/plugins/meta.js

@@ -0,0 +1,4 @@
+import Vue from 'vue';
+import Meta from 'vue-meta';
+
+Vue.use(Meta);

+ 33 - 0
src/plugins/methods.js

@@ -0,0 +1,33 @@
+import Vue from 'vue';
+import _ from 'lodash';
+const Plugin = {
+  install(Vue, options) {
+    // 3. 注入组件
+    Vue.mixin({
+      created() {
+        if (this.$store && !this.$store.$toUndefined) {
+          this.$store.$toUndefined = this.$toUndefined;
+        }
+      },
+    });
+    // 4. 添加实例方法
+    Vue.prototype.$toUndefined = (object) => {
+      let keys = Object.keys(object);
+      keys.map((item) => {
+        object[item] = object[item] === '' ? (object[item] = undefined) : object[item];
+      });
+      return object;
+    };
+    Vue.prototype.$turnTo = (item) => {
+      if (item.info_type == 1) {
+        window.open(item.url);
+      } else {
+        let router = window.vm.$router;
+        let route = window.vm.$route.path;
+        router.push({ path: `/info/detail?id=${item.id}` });
+      }
+    };
+  },
+};
+
+Vue.use(Plugin);

+ 21 - 0
src/plugins/setting.js

@@ -0,0 +1,21 @@
+import Vue from 'vue';
+
+Vue.config.weixin = {
+  // baseUrl: process.env.BASE_URL + 'weixin',
+  baseUrl: `http://${location.host}`,
+};
+
+Vue.config.stomp = {
+  // brokerURL: 'ws://192.168.1.118/ws',
+  brokerURL: '/ws', // ws://${location.host}/ws
+  // brokerURL: 'ws://127.0.0.1:8000/ws',
+  connectHeaders: {
+    host: 'platform',
+    login: 'visit', //visit
+    passcode: 'visit', //visit123
+  },
+  // debug: true,
+  reconnectDelay: 5000,
+  heartbeatIncoming: 4000,
+  heartbeatOutgoing: 4000,
+};

+ 65 - 0
src/plugins/stomp.js

@@ -0,0 +1,65 @@
+/**
+ * 基于WebStomp的消息处理插件
+ */
+
+import Vue from 'vue';
+import _ from 'lodash';
+import assert from 'assert';
+import { Client } from '@stomp/stompjs/esm5/client';
+
+const Plugin = {
+  install(Vue, options) {
+    assert(_.isObject(options));
+    if (options.debug && !_.isFunction(options.debug)) {
+      options.debug = (str) => {
+        console.log(str);
+      };
+    }
+    assert(_.isString(options.brokerURL));
+    if (!options.brokerURL.startsWith('ws://')) {
+      options.brokerURL = `ws://${location.host}${options.brokerURL}`;
+    }
+
+    // 3. 注入组件
+    Vue.mixin({
+      beforeDestroy: function () {
+        if (this.$stompClient) {
+          this.$stompClient.deactivate();
+          delete this.$stompClient;
+        }
+      },
+    });
+
+    // 4. 添加实例方法
+    Vue.prototype.$stomp = function (subscribes = {}) {
+      // connect to mq
+      const client = new Client(options);
+      client.onConnect = (frame) => {
+        // Do something, all subscribes must be done is this callback
+        // This is needed because this will be executed after a (re)connect
+        console.log('[stomp] connected');
+        Object.keys(subscribes)
+          .filter((p) => _.isFunction(subscribes[p]))
+          .forEach((key) => {
+            client.subscribe(key, subscribes[key]);
+          });
+      };
+
+      client.onStompError = (frame) => {
+        // Will be invoked in case of error encountered at Broker
+        // Bad login/passcode typically will cause an error
+        // Complaint brokers will set `message` header with a brief message. Body may contain details.
+        // Compliant brokers will terminate the connection after any error
+        console.log('Broker reported error: ' + frame.headers['message']);
+        console.log('Additional details: ' + frame.body);
+      };
+
+      client.activate();
+
+      this.$stompClient = client;
+    };
+  },
+};
+export default () => {
+  Vue.use(Plugin, Vue.config.stomp);
+};

+ 25 - 0
src/plugins/var.js

@@ -0,0 +1,25 @@
+import Vue from 'vue';
+import _ from 'lodash';
+
+const getSiteId = () => {
+  let host = `${window.location.hostname}`; //`999991.smart.jilinjobswx.cn ${window.location.hostname}`
+  let schId;
+  host = host.replace('http://', '');
+  let arr = host.split('.');
+  if (arr.length > 0) {
+    schId = arr[0];
+    if (schId === 'smart') schId = 'master';
+    else `${schId}`.includes('localhost') || `${schId}`.includes('127.0.0.1') ? (schId = '99991') : '';
+    sessionStorage.setItem('schId', `${schId}`.includes('localhost') || `${schId}`.includes('127.0.0.1') ? '99991' : schId);
+  }
+  return schId;
+};
+const Plugin = {
+  install(vue, options) {
+    // 4. 添加实例方法
+    vue.prototype.$limit = 10;
+    vue.prototype.$site = getSiteId();
+  },
+};
+
+Vue.use(Plugin);

+ 77 - 0
src/router/index.js

@@ -0,0 +1,77 @@
+import Vue from 'vue';
+import VueRouter from 'vue-router';
+import store from '@/store/index';
+const jwt = require('jsonwebtoken');
+const originalPush = VueRouter.prototype.push;
+VueRouter.prototype.push = function push(location) {
+  return originalPush.call(this, location).catch((err) => err);
+};
+Vue.use(VueRouter);
+const web = [
+  {
+    path: '/',
+    redirect: '/adminCenter/homeIndex',
+  },
+  // 管理登录
+  {
+    path: '/login',
+    name: 'login',
+    meta: { title: '管理登录' },
+    component: () => import('../views/login.vue'),
+  },
+  // 管理中心
+  {
+    path: '/adminCenter/homeIndex',
+    name: 'adminCenter',
+    component: () => import('@common/src/components/admin-frame/Home.vue'),
+    children: [
+      {
+        path: '/adminCenter/homeIndex',
+        name: 'admin_homeIndex',
+        meta: { title: '首页' },
+        component: () => import('../views/adminCenter/homeIndex/index.vue'),
+      },
+      {
+        path: '/adminCenter/test/index',
+        meta: { title: '测试菜单' },
+        component: () => import('../views/adminCenter/test/index.vue'),
+      },
+      {
+        path: '/adminCenter/test/detail',
+        meta: { title: '测试菜单-信息管理' },
+        component: () => import('../views/adminCenter/test/detail.vue'),
+      },
+      {
+        path: '/adminCenter/statistics/index',
+        meta: { title: '绩效目标表' },
+        component: () => import('../views/adminCenter/statistics/index.vue'),
+      },
+    ],
+  },
+];
+const routes = [...web];
+const router = new VueRouter({
+  mode: 'history',
+  base: process.env.VUE_APP_ROUTER,
+  routes,
+});
+router.beforeEach((to, from, next) => {
+  document.title = `${to.meta.title} `;
+  const token = localStorage.getItem('token');
+  if (to.path == '/adminCenter/homeIndex') {
+    if (!token) {
+      // next('/login');
+      next();
+    } else {
+      let user = jwt.decode(token);
+      store.commit('setUser', user, { root: true });
+      next();
+    }
+  } else {
+    let user = jwt.decode(token);
+    store.commit('setUser', user, { root: true });
+    next();
+  }
+});
+
+export default router;

+ 14 - 0
src/store/index.js

@@ -0,0 +1,14 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import * as ustate from '@common/src/store/user/state';
+import * as umutations from '@common/src/store/user/mutations';
+import test from '@common/src/store/test';
+
+Vue.use(Vuex);
+
+export default new Vuex.Store({
+  state: { ...ustate },
+  mutations: { ...umutations },
+  actions: {},
+  modules: { test },
+});

+ 117 - 0
src/util/axios-wrapper.js

@@ -0,0 +1,117 @@
+/* eslint-disable no-console */
+/* eslint-disable no-param-reassign */
+
+import _ from 'lodash';
+import Axios from 'axios';
+import { Util, Error } from 'naf-core';
+// import { Indicator } from 'mint-ui';
+import util from './user-util';
+
+const { trimData, isNullOrUndefined } = Util;
+const { ErrorCode } = Error;
+
+let currentRequests = 0;
+
+export default class AxiosWrapper {
+  constructor({ baseUrl = '', unwrap = true } = {}) {
+    this.baseUrl = baseUrl;
+    this.unwrap = unwrap;
+  }
+
+  // 替换uri中的参数变量
+  static merge(uri, query = {}) {
+    if (!uri.includes(':')) {
+      return uri;
+    }
+    const keys = [];
+    const regexp = /\/:([a-z0-9_]+)/gi;
+    let res;
+    // eslint-disable-next-line no-cond-assign
+    while ((res = regexp.exec(uri)) != null) {
+      keys.push(res[1]);
+    }
+    keys.forEach((key) => {
+      if (!isNullOrUndefined(query[key])) {
+        uri = uri.replace(`:${key}`, query[key]);
+      }
+    });
+    return uri;
+  }
+
+  $get(uri, query, options) {
+    return this.$request(uri, null, query, options);
+  }
+
+  $post(uri, data = {}, query, options) {
+    return this.$request(uri, data, query, options);
+  }
+  $delete(uri, data = {}, router, query, options = {}) {
+    options = { ...options, method: 'delete' };
+    return this.$request(uri, data, query, options, router);
+  }
+  async $request(uri, data, query, options) {
+    // TODO: 合并query和options
+    if (_.isObject(query) && _.isObject(options)) {
+      options = { ...options, params: query, method: 'get' };
+    } else if (_.isObject(query) && !query.params) {
+      options = { params: query };
+    } else if (_.isObject(query) && query.params) {
+      options = query;
+    }
+    if (!options) options = {};
+    if (options.params) options.params = trimData(options.params);
+    const url = AxiosWrapper.merge(uri, options.params);
+    currentRequests += 1;
+    // Indicator.open({
+    //   spinnerType: 'fading-circle',
+    // });
+
+    try {
+      const axios = Axios.create({
+        baseURL: this.baseUrl,
+      });
+      axios.defaults.headers.common.Authorization = util.token;
+      let res = await axios.request({
+        method: isNullOrUndefined(data) ? 'get' : 'post',
+        url,
+        data,
+        responseType: 'json',
+        ...options,
+      });
+      res = res.data;
+      const { errcode, errmsg, details } = res;
+      if (errcode) {
+        console.warn(`[${uri}] fail: ${errcode}-${errmsg} ${details}`);
+        return res;
+      }
+      // unwrap data
+      if (this.unwrap) {
+        res = _.omit(res, ['errmsg', 'details']);
+        const keys = Object.keys(res);
+        if (keys.length === 1 && keys.includes('data')) {
+          res = res.data;
+        }
+      }
+      return res;
+    } catch (err) {
+      let errmsg = '接口请求失败,请稍后重试';
+      if (err.response) {
+        const { status } = err.response;
+        if (status === 401) errmsg = '用户认证失败,请重新登录';
+        if (status === 403) errmsg = '当前用户不允许执行该操作';
+      }
+      console.error(
+        `[AxiosWrapper] 接口请求失败: ${err.config && err.config.url} - 
+        ${err.message}`
+      );
+      return { errcode: ErrorCode.SERVICE_FAULT, errmsg, details: err.message };
+    } finally {
+      /* eslint-disable */
+      currentRequests -= 1;
+      if (currentRequests <= 0) {
+        currentRequests = 0;
+        // Indicator.close();
+      }
+    }
+  }
+}

+ 10 - 0
src/util/filters.js

@@ -0,0 +1,10 @@
+import _ from 'lodash';
+
+const filters = {
+  getName(object) {
+    const { data, searchItem } = object;
+    return _.get(data, searchItem) === undefined ? '' : _.get(data, searchItem);
+  },
+};
+
+export default filters;

+ 50 - 0
src/util/methods-util.js

@@ -0,0 +1,50 @@
+import { Util } from 'naf-core';
+
+const { isNullOrUndefined } = Util;
+
+export default {
+  //判断信息是否过期
+  isDateOff(dataDate) {
+    const now = new Date(new Date().getTime() - 24 * 60 * 60 * 1000);
+    dataDate = new Date(dataDate);
+    return now.getTime() <= dataDate.getTime();
+  },
+  //判断企业是否可以执行此动作/显示
+  checkCorp(data) {
+    const { role, unit, selfUnit, status, displayType, userid } = data;
+    if (!isNullOrUndefined(selfUnit) && !isNullOrUndefined(status)) {
+      return role === 'corp' && selfUnit === unit && status === '0';
+    } else if (!isNullOrUndefined(displayType)) {
+      if (role === 'corp') {
+        return role === displayType;
+      } else {
+        return role === displayType && !isNullOrUndefined(userid);
+      }
+    }
+  },
+  //获取url的参数params
+  getParams() {
+    let str = location.href;
+    let num = str.indexOf('?');
+    const param = {};
+    str = str.substr(num + 1);
+    let num2 = str.indexOf('#');
+    let str2 = '';
+    if (num2 > 0) {
+      str2 = str.substr(0, num2);
+    } else {
+      num2 = str.indexOf('/');
+      str2 = str.substr(0, num2);
+    }
+    const arr = str2.split('&');
+    for (let i = 0; i < arr.length; i++) {
+      num = arr[i].indexOf('=');
+      if (num > 0) {
+        const name = arr[i].substring(0, num);
+        const value = arr[i].substr(num + 1);
+        param[name] = decodeURI(value);
+      }
+    }
+    return param;
+  },
+};

+ 163 - 0
src/util/print.js

@@ -0,0 +1,163 @@
+// 打印类属性、方法定义
+/* eslint-disable */
+const Print = function (dom, options) {
+  if (!(this instanceof Print)) return new Print(dom, options);
+
+  this.options = this.extend({
+    'noPrint': '.no-print'
+  }, options);
+
+  if ((typeof dom) === "string") {
+    this.dom = document.querySelector(dom);
+  } else {
+    this.isDOM(dom)
+    this.dom = this.isDOM(dom) ? dom : dom.$el;
+  }
+
+  this.init();
+};
+Print.prototype = {
+  init: function () {
+    var content = this.getStyle() + this.getHtml();
+    this.writeIframe(content);
+  },
+  extend: function (obj, obj2) {
+    for (var k in obj2) {
+      obj[k] = obj2[k];
+    }
+    return obj;
+  },
+
+  getStyle: function () {
+    var str = "",
+      styles = document.querySelectorAll('style,link');
+    for (var i = 0; i < styles.length; i++) {
+      str += styles[i].outerHTML;
+    }
+    str += "<style>" + (this.options.noPrint ? this.options.noPrint : '.no-print') + "{display:none;}</style>";
+
+    return str;
+  },
+
+  getHtml: function () {
+    var inputs = document.querySelectorAll('input');
+    var textareas = document.querySelectorAll('textarea');
+    var selects = document.querySelectorAll('select');
+
+    for (var k = 0; k < inputs.length; k++) {
+      if (inputs[k].type == "checkbox" || inputs[k].type == "radio") {
+        if (inputs[k].checked == true) {
+          inputs[k].setAttribute('checked', "checked")
+        } else {
+          inputs[k].removeAttribute('checked')
+        }
+      } else if (inputs[k].type == "text") {
+        inputs[k].setAttribute('value', inputs[k].value)
+      } else {
+        inputs[k].setAttribute('value', inputs[k].value)
+      }
+    }
+
+    for (var k2 = 0; k2 < textareas.length; k2++) {
+      if (textareas[k2].type == 'textarea') {
+        textareas[k2].innerHTML = textareas[k2].value
+      }
+    }
+
+    for (var k3 = 0; k3 < selects.length; k3++) {
+      if (selects[k3].type == 'select-one') {
+        var child = selects[k3].children;
+        for (var i in child) {
+          if (child[i].tagName == 'OPTION') {
+            if (child[i].selected == true) {
+              child[i].setAttribute('selected', "selected")
+            } else {
+              child[i].removeAttribute('selected')
+            }
+          }
+        }
+      }
+    }
+    // 包裹要打印的元素
+    // fix: https://github.com/xyl66/vuePlugs_printjs/issues/36
+    let outerHTML = this.wrapperRefDom(this.dom).outerHTML
+    return outerHTML;
+  },
+  // 向父级元素循环,包裹当前需要打印的元素
+  // 防止根级别开头的 css 选择器不生效
+  wrapperRefDom: function (refDom) {
+    let prevDom = null
+    let currDom = refDom
+    // 判断当前元素是否在 body 中,不在文档中则直接返回该节点
+    if (!this.isInBody(currDom)) return currDom
+
+    while (currDom) {
+      if (prevDom) {
+        let element = currDom.cloneNode(false)
+        element.appendChild(prevDom)
+        prevDom = element
+      } else {
+        prevDom = currDom.cloneNode(true)
+      }
+
+      currDom = currDom.parentElement
+    }
+
+    return prevDom
+  },
+
+  writeIframe: function (content) {
+    var w, doc, iframe = document.createElement('iframe'),
+      f = document.body.appendChild(iframe);
+    iframe.id = "myIframe";
+    //iframe.style = "position:absolute;width:0;height:0;top:-10px;left:-10px;";
+    iframe.setAttribute('style', 'position:absolute;width:0;height:0;top:-10px;left:-10px;');
+    w = f.contentWindow || f.contentDocument;
+    doc = f.contentDocument || f.contentWindow.document;
+    doc.open();
+    doc.write(content);
+    doc.close();
+    var _this = this
+    iframe.onload = function(){
+      _this.toPrint(w);
+      setTimeout(function () {
+        document.body.removeChild(iframe)
+      }, 100)
+    }
+  },
+
+  toPrint: function (frameWindow) {
+    try {
+      setTimeout(function () {
+        frameWindow.focus();
+        try {
+          if (!frameWindow.document.execCommand('print', false, null)) {
+            frameWindow.print();
+          }
+        } catch (e) {
+          frameWindow.print();
+        }
+        frameWindow.close();
+      }, 10);
+    } catch (err) {
+      console.log('err', err);
+    }
+  },
+  // 检查一个元素是否是 body 元素的后代元素且非 body 元素本身
+  isInBody: function (node) {
+    return (node === document.body) ? false : document.body.contains(node);
+  },
+  isDOM: (typeof HTMLElement === 'object') ?
+    function (obj) {
+      return obj instanceof HTMLElement;
+    } :
+    function (obj) {
+      return obj && typeof obj === 'object' && obj.nodeType === 1 && typeof obj.nodeName === 'string';
+    }
+};
+const MyPlugin = {}
+MyPlugin.install = function (Vue, options) {
+  // 4. 添加实例方法
+  Vue.prototype.$print = Print
+}
+export default MyPlugin

+ 69 - 0
src/util/user-util.js

@@ -0,0 +1,69 @@
+/* eslint-disable no-console */
+export default {
+  get user() {
+    const val = sessionStorage.getItem('user');
+    try {
+      if (val) return JSON.parse(val);
+    } catch (err) {
+      console.error(err);
+    }
+    return null;
+  },
+  set user(userinfo) {
+    sessionStorage.setItem('user', JSON.stringify(userinfo));
+  },
+  get token() {
+    return sessionStorage.getItem('token');
+  },
+  set token(token) {
+    sessionStorage.setItem('token', token);
+  },
+  get openid() {
+    return sessionStorage.getItem('openid');
+  },
+  set openid(openid) {
+    sessionStorage.setItem('openid', openid);
+  },
+  get isGuest() {
+    return !this.user || this.user.role === 'guest';
+  },
+  save({ userinfo, token }) {
+    sessionStorage.setItem('user', JSON.stringify(userinfo));
+    sessionStorage.setItem('token', token);
+  },
+
+  get corpInfo() {
+    const val = sessionStorage.getItem('corpInfo');
+    if (val) return JSON.parse(val);
+    return null;
+  },
+  set corpInfo(corpInfo) {
+    sessionStorage.setItem('corpInfo', JSON.stringify(corpInfo));
+  },
+  saveCorpInfo(corpInfo) {
+    sessionStorage.setItem('corpInfo', JSON.stringify(corpInfo));
+  },
+
+  get unit() {
+    const val = sessionStorage.getItem('unit');
+    if (val) return JSON.parse(val);
+    return null;
+  },
+  set unit(unitList) {
+    sessionStorage.setItem('unit', JSON.stringify(unitList));
+  },
+  saveUnit(unitList) {
+    sessionStorage.setItem('unit', JSON.stringify(unitList));
+  },
+  get userInfo() {
+    const val = sessionStorage.getItem('userInfo');
+    if (val) return JSON.parse(val);
+    return null;
+  },
+  set userInfo(userInfo) {
+    sessionStorage.setItem('userInfo', JSON.stringify(userInfo));
+  },
+  saveUserInfo(userInfo) {
+    sessionStorage.setItem('userInfo', JSON.stringify(userInfo));
+  },
+};

+ 36 - 0
src/views/adminCenter/homeIndex/index.vue

@@ -0,0 +1,36 @@
+<template>
+  <div id="index">
+    <el-row>
+      <el-col :span="24" class="main animate__animated animate__backInRight"> test </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script>
+import { mapState, createNamespacedHelpers } from 'vuex';
+export default {
+  name: 'index',
+  props: {},
+  components: {},
+  data: function () {
+    return {};
+  },
+  created() {},
+  methods: {},
+  computed: {
+    ...mapState(['user']),
+  },
+  metaInfo() {
+    return { title: this.$route.meta.title };
+  },
+  watch: {
+    test: {
+      deep: true,
+      immediate: true,
+      handler(val) {},
+    },
+  },
+};
+</script>
+
+<style lang="less" scoped></style>

+ 37 - 0
src/views/adminCenter/index.vue

@@ -0,0 +1,37 @@
+<template>
+  <div id="index">
+    <admin-frame></admin-frame>
+  </div>
+</template>
+
+<script>
+import adminFrame from '@common/src/components/adminCommon/frame.vue';
+import { mapState, createNamespacedHelpers } from 'vuex';
+export default {
+  name: 'index',
+  props: {},
+  components: {
+    adminFrame,
+  },
+  data: function () {
+    return {};
+  },
+  created() {},
+  methods: {},
+  computed: {
+    ...mapState(['user']),
+  },
+  metaInfo() {
+    return { title: this.$route.meta.title };
+  },
+  watch: {
+    test: {
+      deep: true,
+      immediate: true,
+      handler(val) {},
+    },
+  },
+};
+</script>
+
+<style lang="less" scoped></style>

+ 314 - 0
src/views/adminCenter/statistics/index.vue

@@ -0,0 +1,314 @@
+<template>
+  <div id="index">
+    <el-row>
+      <el-col :span="24" class="main">
+        <el-form :model="form" :rules="rules" ref="form">
+          <el-col :span="24" class="one">
+            <el-col :span="24" class="one_1">中央对地方共同财政事权转移支付区域绩效目标表</el-col>
+            <el-col :span="24" class="one_1 one_2">(2022年度)</el-col>
+          </el-col>
+          <el-col :span="24" class="two">
+            <table border cellspacing="0">
+              <tr align="center" valign="center">
+                <td>项目名称</td>
+                <td colspan="4" align="left">
+                  <el-form-item label="" prop="name">
+                    <el-input v-model="form.name"></el-input>
+                  </el-form-item>
+                </td>
+              </tr>
+              <tr align="center" valign="center">
+                <td>中央主管部门</td>
+                <td colspan="4" align="left">
+                  <el-form-item label="" prop="center_dept">
+                    <el-input v-model="form.center_dept"></el-input>
+                  </el-form-item>
+                </td>
+              </tr>
+              <tr align="center" valign="center">
+                <td>省级财政部门</td>
+                <td colspan="2">
+                  <el-form-item label="" prop="province_money_dept">
+                    <el-input v-model="form.province_money_dept"></el-input>
+                  </el-form-item>
+                </td>
+                <td>省级主管部门</td>
+                <td align="left">
+                  <el-form-item label="" prop="province_center_dept">
+                    <el-input v-model="form.province_center_dept"></el-input>
+                  </el-form-item>
+                </td>
+              </tr>
+              <tr align="center" valign="center">
+                <td rowspan="3">资金情况(万元)</td>
+                <td colspan="2">年度金额:</td>
+                <td colspan="2" align="left">
+                  <el-form-item label="" prop="year_money">
+                    <el-input v-model="form.year_money"></el-input>
+                  </el-form-item>
+                </td>
+              </tr>
+              <tr align="center" valign="center">
+                <td colspan="2">其中:中央补助</td>
+                <td colspan="2" align="left">
+                  <el-form-item label="" prop="center_year_money">
+                    <el-input v-model="form.center_year_money"></el-input>
+                  </el-form-item>
+                </td>
+              </tr>
+              <tr align="center" valign="center">
+                <td colspan="2">地方资金</td>
+                <td colspan="2" align="left">
+                  <el-form-item label="" prop="local_year_money">
+                    <el-input v-model="form.local_year_money"></el-input>
+                  </el-form-item>
+                </td>
+              </tr>
+              <tr align="center" valign="center">
+                <td rowspan="2">年度总体目标</td>
+                <td rowspan="2" colspan="4" align="left">
+                  <el-form-item label="" prop="year_target">
+                    <el-input v-model="form.year_target" type="textarea" :autosize="{ minRows: 4, maxRows: 6 }"> </el-input>
+                  </el-form-item>
+                </td>
+              </tr>
+              <tr></tr>
+              <tr align="center" valign="center">
+                <td rowspan="29">绩效指标</td>
+                <td>一级指标</td>
+                <td>二级指标</td>
+                <td>三级指标</td>
+                <td>指标值</td>
+              </tr>
+              <tr align="center" valign="center">
+                <td rowspan="14">产出指标</td>
+                <td rowspan="13">数量指标</td>
+                <td align="left">支持自由探索类基础研究项目数量</td>
+                <td><el-input v-model="form.produce_index.num_1" placeholder="个" /></td>
+              </tr>
+              <tr align="left" valign="center">
+                <td>支持省部共建国家重点实验室项目数量</td>
+                <td><el-input v-model="form.produce_index.num_2" placeholder="个" /></td>
+              </tr>
+              <tr align="left" valign="center">
+                <td>支持国家临床医学研究中心项目数量</td>
+                <td><el-input v-model="form.produce_index.num_3" placeholder="个" /></td>
+              </tr>
+              <tr align="left" valign="center">
+                <td>支持新型研发机构项目数量</td>
+                <td><el-input v-model="form.produce_index.num_4" placeholder="个" /></td>
+              </tr>
+              <tr align="left" valign="center">
+                <td>转化科技成果数量</td>
+                <td><el-input v-model="form.produce_index.num_5" placeholder="项" /></td>
+              </tr>
+              <tr align="left" valign="center">
+                <td>支持科技特派员项目数量</td>
+                <td><el-input v-model="form.produce_index.num_6" placeholder="人 " /></td>
+              </tr>
+              <tr align="left" valign="center">
+                <td>支持国家高新技术产业开发区项目数量</td>
+                <td><el-input v-model="form.produce_index.num_7" placeholder="个" /></td>
+              </tr>
+              <tr align="left" valign="center">
+                <td>支持国家创新型城市项目数量</td>
+                <td><el-input v-model="form.produce_index.num_8" placeholder="个" /></td>
+              </tr>
+              <tr align="left" valign="center">
+                <td>支持国家创新型县(市)项目数量</td>
+                <td><el-input v-model="form.produce_index.num_9" placeholder="个" /></td>
+              </tr>
+              <tr align="left" valign="center">
+                <td>支持国家自主创新示范区项目数量</td>
+                <td><el-input v-model="form.produce_index.num_10" placeholder="个" /></td>
+              </tr>
+              <tr align="left" valign="center">
+                <td>支持国家科技成果转移转化示范区项目数量</td>
+                <td><el-input v-model="form.produce_index.num_11" placeholder="个" /></td>
+              </tr>
+              <tr align="left" valign="center">
+                <td>支持国家级科技企业孵化器、大学科技园、众创空间、双创示范基地、星创天地项目数量</td>
+                <td><el-input v-model="form.produce_index.num_12" placeholder="个" /></td>
+              </tr>
+              <tr align="left" valign="center">
+                <td>支持东西部科技合作及区域协同创新项目数量</td>
+                <td><el-input v-model="form.produce_index.num_13" placeholder="个" /></td>
+              </tr>
+              <tr align="center" valign="center">
+                <td>时效指标</td>
+                <td align="left">资金拨付及时率</td>
+                <td><el-input v-model="form.produce_index.time_1" placeholder="90%" /></td>
+              </tr>
+              <tr align="center" valign="center">
+                <td rowspan="13">效益指标</td>
+                <td rowspan="7">经济效益指标</td>
+                <td align="left">带动社会投入与引导资金投入比例</td>
+                <td><el-input v-model="form.benefit_index.economics_1"></el-input></td>
+              </tr>
+              <tr align="left" valign="center">
+                <td align="left">支持高新技术企业数量</td>
+                <td><el-input v-model="form.benefit_index.economics_2" placeholder="家"></el-input></td>
+              </tr>
+              <tr align="left" valign="center">
+                <td align="left">支持科技型中小企业数量</td>
+                <td><el-input v-model="form.benefit_index.economics_3" placeholder="家"></el-input></td>
+              </tr>
+              <tr align="left" valign="center">
+                <td align="left">新增在孵企业数量</td>
+                <td><el-input v-model="form.benefit_index.economics_4" placeholder="家"></el-input></td>
+              </tr>
+              <tr align="left" valign="center">
+                <td align="left">促进技术合同成交额</td>
+                <td><el-input v-model="form.benefit_index.economics_5" placeholder="万元"></el-input></td>
+              </tr>
+              <tr align="left" valign="center">
+                <td align="left">促进科技投融资金额</td>
+                <td><el-input v-model="form.benefit_index.economics_6" placeholder="万元"></el-input></td>
+              </tr>
+              <tr align="left" valign="center">
+                <td align="left">带动地方投入东西科技合作及区域协同创新资金</td>
+                <td><el-input v-model="form.benefit_index.economics_7" placeholder="万元"></el-input></td>
+              </tr>
+              <tr align="center" valign="center">
+                <td rowspan="6">社会效益指标</td>
+                <td align="left">培训从事技术创新服务人员数量</td>
+                <td><el-input v-model="form.benefit_index.sociology_1" placeholder="人次"></el-input></td>
+              </tr>
+              <tr align="left" valign="center">
+                <td align="left">提供技术咨询/技术服务数量</td>
+                <td><el-input v-model="form.benefit_index.sociology_2" placeholder="人次"></el-input></td>
+              </tr>
+              <tr align="left" valign="center">
+                <td align="left">培训和指导农业科技服务数量</td>
+                <td><el-input v-model="form.benefit_index.sociology_3" placeholder="人次"></el-input></td>
+              </tr>
+              <tr align="left" valign="center">
+                <td align="left">培训技术经纪人数量</td>
+                <td><el-input v-model="form.benefit_index.sociology_4" placeholder="人次"></el-input></td>
+              </tr>
+              <tr align="left" valign="center">
+                <td align="left">开展创业辅导活动</td>
+                <td><el-input v-model="form.benefit_index.sociology_5" placeholder="场"></el-input></td>
+              </tr>
+              <tr align="left" valign="center">
+                <td align="left">科技特派员服务农民</td>
+                <td><el-input v-model="form.benefit_index.sociology_6" placeholder="户"></el-input></td>
+              </tr>
+              <tr align="center" valign="center">
+                <td>满意度指标</td>
+                <td>服务对象满意度指标</td>
+                <td align="left">被服务对象满意度</td>
+                <td><el-input v-model="form.satisfaction" placeholder="85%"></el-input></td>
+              </tr>
+            </table>
+          </el-col>
+          <el-col :span="24" class="thr">
+            <el-button type="danger" size="mini" @click="onReset('form')">重置信息</el-button>
+            <el-button type="primary" size="mini" @click="onSubmit('form')">提交保存</el-button>
+          </el-col>
+        </el-form>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script>
+import { mapState, createNamespacedHelpers } from 'vuex';
+export default {
+  name: 'index',
+  props: {},
+  components: {},
+  data: function () {
+    return {
+      form: {
+        produce_index: {},
+        benefit_index: {},
+      },
+      rules: {
+        name: [
+          {
+            required: true,
+            message: '请输入项目名称',
+            trigger: 'blur',
+            validator: (rule, value, callback) => {
+              if (rule.required && !value) this.$message({ type: `error`, message: `${rule.message}`, duration: 1000 });
+              else callback();
+            },
+          },
+        ],
+      },
+    };
+  },
+  created() {},
+  methods: {
+    onSubmit(formName) {
+      this.$refs[formName].validate((valid) => {
+        if (valid) {
+          let data = this.form;
+          console.log(data);
+        }
+      });
+    },
+    onReset(formName) {
+      this.$refs[formName].resetFields();
+    },
+  },
+  computed: {
+    ...mapState(['user']),
+  },
+  metaInfo() {
+    return { title: this.$route.meta.title };
+  },
+  watch: {
+    test: {
+      deep: true,
+      immediate: true,
+      handler(val) {},
+    },
+  },
+};
+</script>
+
+<style lang="less" scoped>
+.main {
+  padding: 10px;
+  border-radius: 5px;
+  .one {
+    .one_1 {
+      // text-align: center;
+      font-size: 30px;
+      margin: 10px 0 5px 0;
+      font-family: fangsong;
+      font-weight: bold;
+    }
+    .one_2 {
+      margin: 0;
+      font-size: 20px;
+    }
+  }
+  .two {
+    // padding: 0 12%;
+    margin: 10px 0 0 0;
+    /deep/.el-form-item {
+      margin: 0;
+    }
+    /deep/.el-input__inner {
+      height: 35px;
+      line-height: 35px;
+      border: none;
+      border-radius: 0;
+      text-align: center;
+      color: #000;
+      font-size: 16px;
+    }
+    /deep/.el-textarea__inner {
+      border: none;
+      font-size: 16px;
+    }
+  }
+  .thr {
+    margin: 10px 0 0 0;
+    // text-align: center;
+  }
+}
+</style>

+ 74 - 0
src/views/adminCenter/test/detail.vue

@@ -0,0 +1,74 @@
+<template>
+  <div id="detail">
+    <el-row>
+      <el-col :span="24" class="main animate__animated animate__backInRight">
+        <el-col :span="24" class="one">
+          <data-form :fields="fields" :data="form" @save="toSave" returns="/adminCenter/test/index"> </data-form>
+        </el-col>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script>
+import { mapState, createNamespacedHelpers } from 'vuex';
+const { mapActions: maptest } = createNamespacedHelpers('test');
+export default {
+  name: 'detail',
+  props: {},
+  components: {},
+  data: function () {
+    return {
+      form: {
+        img_url: [],
+      },
+      fields: [
+        { label: '标题', model: 'title' },
+        { label: '内容', model: 'content', type: 'editor', url: '/files/study/news_editor/upload' },
+      ],
+    };
+  },
+  created() {
+    if (this.id) this.search();
+  },
+  methods: {
+    ...maptest(['fetch', 'create', 'update']),
+    async search() {
+      let res = await this.fetch(this.id);
+      if (this.$checkRes(res)) {
+        this.$set(this, `form`, res.data);
+      }
+    },
+    async toSave({ data }) {
+      let dup = _.cloneDeep(data);
+      let res;
+      if (_.get(dup, 'id')) {
+        res = await this.update(dup);
+      } else {
+        res = await this.create(dup);
+      }
+      if (this.$checkRes(res, '保存成功', '保存失败')) {
+        if (this.$dev_mode) this.$router.push('/adminCenter/test/index');
+      }
+    },
+  },
+  computed: {
+    ...mapState(['user']),
+    id() {
+      return this.$route.query.id;
+    },
+  },
+  metaInfo() {
+    return { title: this.$route.meta.title };
+  },
+  watch: {
+    test: {
+      deep: true,
+      immediate: true,
+      handler(val) {},
+    },
+  },
+};
+</script>
+
+<style lang="less" scoped></style>

+ 82 - 0
src/views/adminCenter/test/index.vue

@@ -0,0 +1,82 @@
+<template>
+  <div id="index">
+    <el-row>
+      <el-col :span="24" class="main animate__animated animate__backInRight">
+        <el-col :span="24" class="one">
+          <data-table :fields="fields" :opera="opera" :data="list" :total="total" @query="search" @edit="toEdit" @delete="toDelete">
+            <template #selfbtn>
+              <el-button type="primary" size="mini" @click="toAdd">添加</el-button>
+            </template>
+          </data-table>
+        </el-col>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script>
+import { mapState, createNamespacedHelpers } from 'vuex';
+const { mapActions: maptest } = createNamespacedHelpers('test');
+export default {
+  name: 'index',
+  props: {},
+  components: {},
+  data: function () {
+    return {
+      list: [],
+      total: 0,
+      opera: [
+        { label: '修改', method: 'edit' },
+        { label: '删除', method: 'delete', type: 'danger' },
+      ],
+      fields: [
+        { label: '标题', prop: 'title', filter: 'input' },
+        { label: '信息', prop: 'content' },
+      ],
+    };
+  },
+  created() {
+    this.search();
+  },
+  methods: {
+    ...maptest(['query', 'delete']),
+    async search({ skip = 0, limit = 10, ...info } = {}) {
+      let res = await this.query({ skip, limit, ...info });
+      if (this.$checkRes(res)) {
+        this.$set(this, `list`, res.data);
+        this.$set(this, `total`, res.total);
+      }
+    },
+    // 添加
+    toAdd() {
+      this.$router.push({ path: '/adminCenter/test/detail' });
+    },
+    // 修改
+    toEdit({ data }) {
+      this.$router.push({ path: '/adminCenter/test/detail', query: { id: data.id } });
+    },
+    // 删除
+    async toDelete({ data }) {
+      const res = await this.delete(data._id);
+      if (this.$checkRes(res, '删除成功', '删除失败')) {
+        this.search();
+      }
+    },
+  },
+  computed: {
+    ...mapState(['user']),
+  },
+  metaInfo() {
+    return { title: this.$route.meta.title };
+  },
+  watch: {
+    test: {
+      deep: true,
+      immediate: true,
+      handler(val) {},
+    },
+  },
+};
+</script>
+
+<style lang="less" scoped></style>

+ 46 - 0
src/views/login.vue

@@ -0,0 +1,46 @@
+<template>
+  <div id="login">
+    <el-row>
+      <el-col :span="24" class="main">
+        <div class="w_1200">
+          <el-col :span="24" class="one">
+            <el-button type="primary" size="mini" @click="onSubmit">登录</el-button>
+          </el-col>
+        </div>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script>
+import { mapState, createNamespacedHelpers } from 'vuex';
+export default {
+  name: 'login',
+  props: {},
+  components: {},
+  data: function () {
+    return {};
+  },
+  created() {},
+  methods: {
+    onSubmit() {
+      this.$router.push({ path: '/adminCenter/homeIndex' });
+    },
+  },
+  computed: {
+    ...mapState(['user']),
+  },
+  metaInfo() {
+    return { title: this.$route.meta.title };
+  },
+  watch: {
+    test: {
+      deep: true,
+      immediate: true,
+      handler(val) {},
+    },
+  },
+};
+</script>
+
+<style lang="less" scoped></style>

+ 32 - 0
vue.config.js

@@ -0,0 +1,32 @@
+const path = require('path');
+const common = path.resolve(__dirname, '../common');
+module.exports = {
+  publicPath: `/${process.env.VUE_APP_ROUTER}`,
+  outputDir: 'tobaccocms',
+  productionSourceMap: false,
+  configureWebpack: (config) => {
+    Object.assign(config, {
+      resolve: {
+        alias: {
+          '@': path.resolve(__dirname, './src'),
+          '@c': path.resolve(__dirname, './src/components'),
+          '@a': path.resolve(__dirname, './src/assets'),
+          '@common': common,
+        },
+      },
+    });
+  },
+  devServer: {
+    port: '8001',
+    proxy: {
+      '/files': {
+        target: 'http://broadcast.waityou24.cn',
+      },
+      '/api': {
+        target: 'http://127.0.0.1:9001',
+        changeOrigin: true,
+        ws: false,
+      },
+    },
+  },
+};