zhengnaiwen_citu 4 月之前
父节点
当前提交
ac17565a5f

+ 52 - 12
src/views/dataBook/components/mGraph.vue

@@ -45,23 +45,33 @@
       :on-line-click="onLineClick"
     >
       <template #node="{node}">
-        <div>
+        <div @contextmenu.prevent="handleContextmenu($event, node)">
           <div
             :style="{ width: node.width + 'px', height: node.height + 'px' }"
-            :class="{ 'node-active':  $route.params.id === node.id }"
+            :class="{ 'node-active':  menu.activeId === +node.id }"
             class="rounded-circle"
           >
           </div>
           <div
             :style="{ 'background-color': node.color + '44', width: 20 * node.text.length + 'px' }"
             class="node-text"
-            :class="{ 'node-active':  $route.params.id === node.id }"
+            :class="{ 'node-active':  menu.activeId === +node.id }"
           >
             {{ node.text }}
           </div>
         </div>
       </template>
+      <template #graph-plug>
+        <v-menu v-model="menu.show" attach :position-x="menu.x" :position-y="menu.y" absolute offset-y min-width="200">
+          <v-list dense>
+            <v-list-item v-for="(k, i) in menu.items" :key="i" @click="k.handle">
+              <v-list-item-title>{{ k.title }}</v-list-item-title>
+            </v-list-item>
+          </v-list>
+        </v-menu>
+      </template>
     </relation-graph>
+
   </div>
 </template>
 
@@ -108,6 +118,16 @@ export default {
   },
   data () {
     return {
+      menu: {
+        x: 0,
+        y: 0,
+        show: false,
+        items: [
+          { title: '查看', handle: this.handleView }
+        ],
+        activeId: +this.$route.params.id,
+        item: {}
+      },
       empty: true,
       items: [
         { text: '全链关系', value: 'all' },
@@ -117,7 +137,7 @@ export default {
       type: 'all',
       loading: false,
       graphOptions: {
-        defaultJunctionPoint: 'border',
+        defaultJunctionPoint: 'lr',
         // 这里可以参考"Graph 图谱"中的参数进行设置 https://www.relation-graph.com/#/docs/graph
         debug: false, // 是否开始调试模式,调试模式下会在控制台打印额外的日志信息
         showDebugPanel: false, // 是否显示调试按钮,通过此按钮可以打印配置、数据等
@@ -236,6 +256,18 @@ export default {
     this.init()
   },
   methods: {
+    handleContextmenu (v, node) {
+      const { left, top } = this.$refs.graphRef.$el.getBoundingClientRect()
+      this.menu.x = v.clientX - left
+      this.menu.y = v.clientY - top
+      this.menu.item = node
+      this.menu.show = true
+    },
+    handleView () {
+      this.draw({ id: +this.menu.item.id }, () => {
+        this.menu.activeId = +this.menu.item.id
+      })
+    },
     handleChangeMeta (val) {
       this.showMeta = val
       this.init()
@@ -248,20 +280,25 @@ export default {
     },
     async init () {
       const query = {
-        ...this.query,
-        type: this.type,
-        meta: this.showMeta
+        ...this.query
       }
       if (this.$route.params.id) {
         Object.assign(query, {
           id: +this.$route.params.id
         })
       }
+      this.draw(query)
+    },
+    async draw (query, successCallback = () => {}) {
       // 清空再渲染
       this.loading = true
       this.empty = false
       try {
-        const { data } = await this.toApi(query)
+        const { data } = await this.toApi({
+          ...query,
+          type: this.type,
+          meta: this.showMeta
+        })
         if (!data.nodes || !data.nodes.length) {
           this.empty = true
           this.loading = false
@@ -279,10 +316,13 @@ export default {
         data.lines.forEach(ele => {
           ele.color = LINE_COLOR_MAP[ele.text]
         })
-        this.$refs.graphRef.setOptions(this.graphOptions, async (graphInstance) => {
-          this.$refs.graphRef.setJsonData(data, async (_graphInstance) => {
-            await _graphInstance.setZoom(75)
-            this.loading = false
+        this.$nextTick(() => {
+          this.$refs.graphRef.setOptions(this.graphOptions, async (graphInstance) => {
+            this.$refs.graphRef.setJsonData(data, async (_graphInstance) => {
+              await _graphInstance.setZoom(75)
+              successCallback()
+              this.loading = false
+            })
           })
         })
       } catch (error) {

+ 7 - 0
src/views/dataBook/components/mGraphList.vue

@@ -183,4 +183,11 @@ export default {
   box-sizing: border-box;
   min-width: 120px;
 }
+::v-deep .rel-toolbar {
+  background-color: #f39930;
+  color: #ffffff;
+  .c-current-zoom {
+    color: #ffffff;
+  }
+}
 </style>

+ 1 - 1
src/views/dataBook/components/mInfo.vue

@@ -5,7 +5,7 @@
         <div class="mb-3 d-flex" v-for="item in info" :key="item.label">
           <template v-if="item.format === 'array'">
             <div class="label">{{ item.label }}: </div>
-            <div>
+            <div style="flex: 1" class="d-flex flex-wrap">
               <div v-for="_t in item.value" :key="_t.label">
                   <div v-if="_t.label" class="mb-1">{{ _t.label }}</div>
                   <v-chip v-for="i in _t.value" :key="i" color="primary" class="mr-3 mb-3">{{ i }}</v-chip>

+ 1 - 3
src/views/dataBook/dataModel/details.vue

@@ -11,7 +11,7 @@
           v-for="item in items"
           :key="item.path"
         >
-          <component :is="item.path" />
+          <component :is="item.path" :key="$route.fullPath" />
         </v-tab-item>
       </v-tabs-items>
     </div>
@@ -73,13 +73,11 @@ export default {
   created () {
     this.$route.meta.title = this.$route.params.name
     const savedTab = new URLSearchParams(window.location.search).get('tab')
-    console.log(111, savedTab)
     if (!savedTab) {
       this.activeIndex = 0
       return
     }
     this.activeIndex = this.items.findIndex(e => e.path === savedTab)
-    console.log(this.activeIndex)
   },
   methods: {
     handleClick (item) {

+ 166 - 0
src/views/dataFactory/dataCollection/autoCollection/dynamic/pageParsing.vue

@@ -0,0 +1,166 @@
+<template>
+  <div>
+    <div class="d-flex pa-3">
+      <v-textarea
+        outlined
+        name="input-7-4"
+        class="mr-3"
+        dense
+        rows="1"
+        label="url抓取数据"
+        placeholder="请输入需要爬取的页面,多个页面请用 ',' 隔开"
+        hide-details
+        v-model="urls"
+      ></v-textarea>
+      <v-btn color="primary" @click="handleRun">执行</v-btn>
+    </div>
+    <v-container>
+      <v-row v-if="contents.length">
+        <v-col
+          md="6"
+          v-for="(content, index) in contents"
+          :key="index"
+        >
+          <v-card
+            height="500"
+            elevation="2"
+            :loading="!content.data"
+            class="d-flex flex-column"
+          >
+            <v-card-title class="justify-space-between">
+              <div class="overflow">{{ content.url }}</div>
+              <div>
+                <v-btn icon class="mr-3" @click="showPage(content)"><v-icon>mdi-file-eye-outline</v-icon></v-btn>
+                <v-btn icon @click="handleReload(content)"><v-icon>mdi-refresh</v-icon></v-btn>
+              </div>
+            </v-card-title>
+            <v-divider></v-divider>
+            <div v-if="content.data && content.data.data">
+              <v-tabs v-model="content.tab">
+                <v-tab v-for="(_, k) in content.data.data[0]" :key="k">{{ k }}</v-tab>
+              </v-tabs>
+            </div>
+            <v-divider></v-divider>
+            <div v-if="content.data" class="flex-grow-1 flex-shrink-1 overflow-y-auto" >
+              <template v-if="typeof content.data === 'string'">{{ content.data }}</template>
+              <v-tabs-items v-else v-model="content.tab">
+                <v-tab-item v-for="(v, k) in content.data.data[0]" :key="k" class="pa-3 text-subtitle-2">
+                  <template v-if="k === 'html'">
+                    <v-btn
+                      icon
+                      color="primary"
+                      style="position: sticky; top: 20px; float: right; z-index: 99"
+                      @click="content.showHtml = !content.showHtml">
+                      <v-icon>mdi-file-arrow-left-right</v-icon>
+                    </v-btn>
+                    <pre v-if="!content.showHtml">{{ v }}</pre>
+                    <div v-else v-html="v"></div>
+                  </template>
+                  <pre v-else>{{ v }}</pre>
+                </v-tab-item>
+              </v-tabs-items>
+            </div>
+          </v-card>
+        </v-col>
+      </v-row>
+    </v-container>
+    <v-navigation-drawer
+      v-model="drawer"
+      fixed
+      hide-overlay
+      right
+      temporary
+      width="800"
+      style="z-index: 99999999"
+    >
+      <iframe style="width: 100%; height: 100%;" :src="drawerUrl" frameborder="0"></iframe>
+    </v-navigation-drawer>
+  </div>
+</template>
+
+<script>
+import FirecrawlApp from '@mendable/firecrawl-js'
+export default {
+  name: 'page-parsing',
+  data () {
+    return {
+      drawer: false,
+      drawerUrl: null,
+      urls: 'https://mp.weixin.qq.com/s/gtCcUeXZUXkQi5CR25vjew',
+      contents: []
+    }
+  },
+  async created () {
+
+  },
+  methods: {
+    showPage (content) {
+      this.drawer = true
+      this.drawerUrl = content.url
+    },
+    async handleReload (content) {
+      content.data = null
+      const res = await this.handle(content.url)
+      content.tab = 0
+      content.data = res
+    },
+    async handleRun () {
+      if (!this.urls) {
+        return
+      }
+      this.contents = []
+      const urls = this.urls.split(',')
+
+      const run = async (url) => {
+        this.contents.push({
+          url,
+          tab: 0,
+          showHtml: false,
+          data: null
+        })
+        const res = await this.handle(url)
+        this.$set(this.contents, this.contents.length - 1, {
+          url,
+          tab: 0,
+          showHtml: false,
+          data: res
+        })
+        if (this.contents.length < urls.length) {
+          await run(urls[this.contents.length])
+        }
+      }
+      await run(urls[this.contents.length])
+    },
+    async handle (url) {
+      try {
+        const app = new FirecrawlApp({ apiKey: 'fc-85c1550c6db64ce4ae8f2d2cd2606e6f' })
+
+        const crawlResponse = await app.crawlUrl(url, {
+          limit: 100,
+          scrapeOptions: {
+            formats: ['markdown', 'html']
+          }
+        })
+
+        if (!crawlResponse.success) {
+          throw new Error(`Failed to crawl: ${crawlResponse.error}`)
+        }
+
+        return crawlResponse
+      } catch (error) {
+        return error.message
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.overflow {
+  white-space: nowrap;         /* 禁止换行 */
+  overflow: hidden;            /* 隐藏溢出内容 */
+  text-overflow: ellipsis;
+  flex: 1;
+  width: 0;
+}
+</style>

+ 63 - 0
src/views/dataFactory/dataCollection/autoCollection/dynamic/spiderManagement.vue

@@ -0,0 +1,63 @@
+<template>
+  <v-container>
+    <v-row>
+      <v-col :cols="3">
+        <v-card class="d-flex justify-center align-center mb-3" height="100" dark color="success" elevation="5">
+          运行中 10
+          <v-icon>mdi-play</v-icon>
+        </v-card>
+        <v-card class="d-flex justify-center align-center" height="100" color="error" dark elevation="5">
+          已停止 10
+          <v-icon>mdi-pause</v-icon>
+        </v-card>
+      </v-col>
+      <v-col :cols="9">
+        <v-card height="212" elevation="5">
+          <v-card-title>状态监控</v-card-title>
+        </v-card>
+      </v-col>
+    </v-row>
+    <v-row>
+      <v-col :cols="12">
+        <v-card height="500" elevation="5" class="pa-3">
+          <m-table
+            :headers="headers"
+            :items="items"
+            :is-tools="false"
+            :loading="loading"
+            :elevation="0"
+            :show-select="false"
+          ></m-table>
+        </v-card>
+      </v-col>
+    </v-row>
+  </v-container>
+</template>
+
+<script>
+import MTable from '@/components/List/table'
+export default {
+  name: 'spider-management',
+  components: {
+    MTable
+  },
+  data () {
+    return {
+      headers: [
+        { text: '任务名称', value: 'name' },
+        { text: '任务类型', value: 'type' },
+        { text: '任务状态', value: 'status' },
+        { text: '创建时间', value: 'createTime' },
+        { text: '更新时间', value: 'updateTime' },
+        { text: '操作', value: 'actions' }
+      ],
+      items: [],
+      loading: false
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 53 - 0
src/views/dataFactory/dataCollection/autoCollection/index.vue

@@ -0,0 +1,53 @@
+<template>
+  <div>
+    <div>
+      <v-tabs v-model="tab">
+        <v-tab v-for="item in items" :key="item.path" @click="handleTo(item.path)">{{ item.title }}</v-tab>
+      </v-tabs>
+    </div>
+    <div class="white" style="font-size: 16px;">
+      <v-tabs-items v-model="tab">
+        <v-tab-item v-for="item in items" :key="item.path">
+          <component :is="item.path"></component>
+        </v-tab-item>
+      </v-tabs-items>
+    </div>
+  </div>
+</template>
+
+<script>
+import { loadDynamicComponents } from '@/utils'
+const vueComponent = require.context('./dynamic', true, /\.vue$/)
+const autoComponent = loadDynamicComponents(vueComponent)
+export default {
+  name: 'auto-collection',
+  components: { ...autoComponent },
+  data () {
+    return {
+      tab: 0,
+      items: []
+    }
+  },
+  created () {
+    this.items = this.$route.meta.roles.filter(item => +item.active)
+      .sort((a, b) => a.sort - b.sort)
+      .map(e => ({ title: e.label, path: e.name }))
+
+    const savedTab = this.$route.query.tab
+    if (!savedTab) {
+      this.tab = 0
+      return
+    }
+    this.tab = this.items.findIndex(e => e.path === savedTab)
+  },
+  methods: {
+    handleTo (path) {
+      this.$router.push(`${this.$route.path}?tab=${path}`)
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 143 - 0
src/views/dataFactory/dataCollection/manualCollection/components/MCamera.vue

@@ -0,0 +1,143 @@
+<template>
+  <m-dialog title="拍照识别" :visible.sync="show" widthType="2" :footer="false">
+    <div class="box" ref="box">
+      <video
+        ref="video"
+        autoplay
+        playsinline
+      ></video>
+      <canvas ref="canvas"></canvas>
+    </div>
+  </m-dialog>
+</template>
+
+<script>
+import MDialog from '@/components/Dialog'
+export default {
+  name: 'm-camera',
+  components: {
+    MDialog
+  },
+  data () {
+    return {
+      show: false,
+      videoStream: null,
+      animationId: null
+    }
+  },
+  methods: {
+    open () {
+      this.startCamera()
+    },
+    oGetUserMedia (constraints) {
+      const getUserMedia = navigator.mediaDevices?.getUserMedia ||
+                          navigator.getUserMedia ||
+                          navigator.webkitGetUserMedia ||
+                          navigator.mozGetUserMedia ||
+                          navigator.msGetUserMedia ||
+                          navigator.oGetUserMedia
+
+      return new Promise((resolve, reject) => {
+        if (!getUserMedia) {
+          return reject(new Error('当前浏览器不支持,请更换浏览器再尝试'))
+        }
+        getUserMedia.call(navigator.mediaDevices || navigator, constraints)
+          .then(resolve)
+          .catch(_ => {
+            const str = '相机打开失败'
+            reject(str)
+          })
+      })
+    },
+    async startCamera () {
+      try {
+        this.videoStream = await this.oGetUserMedia({
+          video: { facingMode: 'user' }
+        })
+        this.show = true
+        this.$emit('rendered')
+        this.$nextTick(() => {
+          // 设置容器大小
+          const video = this.$refs.video
+          video.srcObject = this.videoStream
+          video.onloadedmetadata = () => {
+            video.play()
+            setTimeout(() => {
+              this.resizeCanvas()
+              this.drawToCanvas()
+            }, 500)
+          }
+        })
+      } catch (error) {
+        this.$snackbar.error(error)
+      }
+    },
+    resizeCanvas () {
+      const { width: boxWidth } = this.$refs.box.getBoundingClientRect()
+      const { width, height } = this.$refs.video.getBoundingClientRect()
+      const rate = width / height
+      const canvas = this.$refs.canvas
+      canvas.width = boxWidth
+      canvas.height = boxWidth / rate
+    },
+    drawToCanvas () {
+      const canvas = this.$refs.canvas
+      const context = canvas.getContext('2d')
+      const video = this.$refs.video
+      // 将视频画面绘制到canvas上
+      const drawFrame = () => {
+        context.clearRect(0, 0, canvas.width, canvas.height)
+        context.save()
+        // 水平翻转画布
+        context.scale(-1, 1)
+        // 在翻转的画布上绘制视频,调整绘制位置
+        context.drawImage(video, -canvas.width, 0, canvas.width, canvas.height)
+        // 恢复画布状态
+        context.restore()
+        this.animationId = requestAnimationFrame(drawFrame)
+      }
+
+      // 开始动画循环
+      drawFrame()
+    },
+    async handleTake () {
+      const canvas = this.$refs.canvas
+      // 使用 toDataURL 方法获取 Base64 图像数据
+      const img = canvas.toDataURL('image/png') // 可以选择其他格式如 'image/jpeg'
+      try {
+        const response = await fetch(img)
+        const blob = await response.blob()
+        const file = new File([blob], 'canvasImage.png', { type: 'image/png' })
+        const query = new FormData()
+        query.append('file', file)
+      } catch (error) {
+        this.$snackbar.error(error)
+      }
+    }
+  },
+  beforeDestroy () {
+    // 停止视频流和动画
+    if (this.videoStream) {
+      this.videoStream.getTracks().forEach(track => track.stop())
+    }
+    if (this.animationId) {
+      cancelAnimationFrame(this.animationId)
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.box {
+  position: relative;
+  width: 100%;
+  video {
+    z-index: -1;
+    width: 100%;
+    position: absolute;
+  }
+  canvas {
+    display: block;
+  }
+}
+</style>

+ 54 - 0
src/views/dataFactory/dataCollection/manualCollection/components/ManualImportEdit.vue

@@ -0,0 +1,54 @@
+<template>
+  <form-list :items="items"></form-list>
+</template>
+
+<script>
+import FormList from '@/components/Form/list'
+export default {
+  name: 'manual-import-edit',
+  components: {
+    FormList
+  },
+  data () {
+    return {
+      items: {
+        options: [
+          {
+            type: 'text',
+            key: 'title',
+            value: null,
+            label: '请输入标题 *',
+            outlined: true,
+            dense: true,
+            rules: [v => !!v || '请输入标题']
+          },
+          {
+            type: 'text',
+            key: 'name',
+            value: null,
+            label: '请输入姓名 *',
+            outlined: true,
+            dense: true,
+            rules: [v => !!v || '请输入姓名']
+          },
+          {
+            type: 'text',
+            key: 'origin',
+            value: null,
+            label: '请输入来源 *',
+            outlined: true,
+            dense: true,
+            rules: [v => !!v || '请输入来源']
+          }
+        ]
+      }
+    }
+  },
+  methods: {
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 123 - 0
src/views/dataFactory/dataCollection/manualCollection/dynamic/excelImport.vue

@@ -0,0 +1,123 @@
+<template>
+  <div>
+    <m-filter :option="filter" @search="handleSearch" />
+    <m-table
+      class="mt-3"
+      :loading="loading"
+      :headers="headers"
+      :items="items"
+      :total="total"
+      :show-select="false"
+      :is-tools="false"
+      :page-info="pageInfo"
+      @pageHandleChange="pageHandleChange"
+      @sort="handleSort"
+    >
+      <template #navBtn>
+        <UploadBtn
+          class="buttons"
+          rounded
+          elevation="5"
+          color="primary"
+          accept="application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
+          @change="handleImport"
+        >
+          <v-icon left>mdi-import</v-icon>
+          导入
+        </UploadBtn>
+      </template>
+    </m-table>
+    <m-dialog :title="title" :visible.sync="show" @submit="handleSubmit">
+    </m-dialog>
+  </div>
+</template>
+
+<script>
+import UploadBtn from '@/components/UploadBtn'
+import MFilter from '@/components/Filter'
+import MTable from '@/components/List/table.vue'
+import MDialog from '@/components/Dialog'
+// import EditPage from './components/edit'
+export default {
+  name: 'excel-import',
+  components: { MFilter, MTable, MDialog, UploadBtn },
+  data () {
+    return {
+      loading: false,
+      show: false,
+      filter: {
+        list: [
+          { type: 'textField', value: '', label: '名称', key: 'name' },
+          { type: 'autocomplete', value: null, label: '选择', key: 'type', items: [] }
+        ]
+      },
+      queryData: {
+        name: null
+      },
+      headers: [
+        { text: '标题', align: 'start', value: 'title' },
+        { text: '姓名', align: 'start', value: 'name' },
+        { text: '来源', align: 'start', value: 'source' },
+        { text: '创建日期', align: 'start', value: 'createDate' },
+        { text: '操作', align: 'start', value: 'actions' }
+      ],
+      itemData: {},
+      items: [],
+      orders: [],
+      pageInfo: {
+        size: 10,
+        current: 1
+      },
+      total: 0
+    }
+  },
+  computed: {
+    title () {
+      return Object.keys(this.itemData).length ? '编辑' : '新增'
+    }
+  },
+  created () {
+    this.init()
+  },
+  methods: {
+    async init () {},
+    handleSearch (val) {
+      Object.assign(this.queryData, val)
+      this.pageInfo.current = 1
+      this.init()
+    },
+    handleImport (file) {
+      console.log(file)
+    },
+    async handleEdit (item) {
+      this.itemData = item
+      this.show = true
+    },
+    handleDelete (ids) {
+      if (Array.isArray(ids) && !ids.length) return this.$snackbar.warning('请选择要删除的选项')
+      this.$confirm('提示', '是否删除该选项')
+        .then(async () => {
+          try {
+            this.$snackbar.success('删除成功')
+            this.init()
+          } catch (error) {
+            this.$snackbar.error('删除失败')
+          }
+        })
+    },
+    handleSubmit () {},
+    pageHandleChange (page) {
+      this.pageInfo.current = page
+      this.init()
+    },
+    handleSort (val) {
+      this.orders = val
+      this.init()
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 124 - 0
src/views/dataFactory/dataCollection/manualCollection/dynamic/imageImport.vue

@@ -0,0 +1,124 @@
+<template>
+  <div>
+    <m-filter :option="filter" @search="handleSearch" />
+    <m-table
+      class="mt-3"
+      :loading="loading"
+      :headers="headers"
+      :items="items"
+      :total="total"
+      :show-select="false"
+      :is-tools="false"
+      :page-info="pageInfo"
+      @pageHandleChange="pageHandleChange"
+      @sort="handleSort"
+    >
+      <template #navBtn>
+        <!-- <v-btn class="buttons mr-3" rounded elevation="5" color="primary" @click="handleTake" :loading="takeLoading">
+          <v-icon left>mdi-camera</v-icon>
+          拍照
+        </v-btn> -->
+        <UploadBtn
+          class="buttons"
+          rounded
+          elevation="5"
+          color="primary"
+          accept="image/*"
+          @change="handleImport"
+        >
+          <v-icon left>mdi-import</v-icon>
+          导入
+        </UploadBtn>
+      </template>
+    </m-table>
+    <m-camera ref="camera" @rendered="takeLoading  = false"></m-camera>
+  </div>
+</template>
+
+<script>
+import UploadBtn from '@/components/UploadBtn'
+import MFilter from '@/components/Filter'
+import MTable from '@/components/List/table.vue'
+import MCamera from '../components/MCamera'
+export default {
+  name: 'image-import',
+  components: { MFilter, MTable, MCamera, UploadBtn },
+  data () {
+    return {
+      takeLoading: false,
+      loading: false,
+      show: false,
+      filter: {
+        list: [
+          { type: 'textField', value: '', label: '名称', key: 'name' },
+          { type: 'autocomplete', value: null, label: '选择', key: 'type', items: [] }
+        ]
+      },
+      queryData: {
+        name: null
+      },
+      headers: [
+        { text: '标题', align: 'start', value: 'title' },
+        { text: '姓名', align: 'start', value: 'name' },
+        { text: '来源', align: 'start', value: 'source' },
+        { text: '创建日期', align: 'start', value: 'createDate' },
+        { text: '操作', align: 'start', value: 'actions' }
+      ],
+      itemData: {},
+      items: [],
+      orders: [],
+      pageInfo: {
+        size: 10,
+        current: 1
+      },
+      total: 0
+    }
+  },
+  created () {
+    this.init()
+  },
+  methods: {
+    async init () {},
+    handleSearch (val) {
+      Object.assign(this.queryData, val)
+      this.pageInfo.current = 1
+      this.init()
+    },
+    handleImport () {
+      this.itemData = {}
+    },
+    handleTake () {
+      this.takeLoading = true
+      this.$refs.camera.open()
+    },
+    async handleEdit (item) {
+      this.itemData = item
+    },
+    handleDelete (ids) {
+      if (Array.isArray(ids) && !ids.length) return this.$snackbar.warning('请选择要删除的选项')
+      this.$confirm('提示', '是否删除该选项')
+        .then(async () => {
+          try {
+            this.$snackbar.success('删除成功')
+            this.init()
+          } catch (error) {
+            this.$snackbar.error('删除失败')
+          }
+        })
+    },
+    handleSubmit () {},
+    pageHandleChange (page) {
+      this.pageInfo.current = page
+      this.init()
+    },
+    handleSort (val) {
+      this.orders = val
+      this.init()
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 117 - 0
src/views/dataFactory/dataCollection/manualCollection/dynamic/manualImport.vue

@@ -0,0 +1,117 @@
+<template>
+  <div>
+    <m-filter :option="filter" @search="handleSearch" />
+    <m-table
+      class="mt-3"
+      :loading="loading"
+      :headers="headers"
+      :items="items"
+      :total="total"
+      :show-select="false"
+      :is-tools="false"
+      :page-info="pageInfo"
+      @pageHandleChange="pageHandleChange"
+      @sort="handleSort"
+    >
+      <template #navBtn>
+        <v-btn class="buttons" rounded elevation="5" color="primary" @click="handleImport">
+          <v-icon left>mdi-plus</v-icon>
+          新增
+        </v-btn>
+      </template>
+    </m-table>
+    <m-dialog :title="title" :visible.sync="show" @submit="handleSubmit">
+      <ManualImportEdit></ManualImportEdit>
+    </m-dialog>
+  </div>
+</template>
+
+<script>
+import MFilter from '@/components/Filter'
+import MTable from '@/components/List/table.vue'
+import MDialog from '@/components/Dialog'
+import ManualImportEdit from '../components/ManualImportEdit'
+export default {
+  name: 'manual-import',
+  components: { MFilter, MTable, MDialog, ManualImportEdit },
+  data () {
+    return {
+      loading: false,
+      show: false,
+      filter: {
+        list: [
+          { type: 'textField', value: '', label: '名称', key: 'name' },
+          { type: 'autocomplete', value: null, label: '选择', key: 'type', items: [] }
+        ]
+      },
+      queryData: {
+        name: null
+      },
+      headers: [
+        { text: '标题', align: 'start', value: 'title' },
+        { text: '姓名', align: 'start', value: 'name' },
+        { text: '来源', align: 'start', value: 'source' },
+        { text: '创建日期', align: 'start', value: 'createDate' },
+        { text: '操作', align: 'start', value: 'actions' }
+      ],
+      itemData: {},
+      items: [],
+      orders: [],
+      pageInfo: {
+        size: 10,
+        current: 1
+      },
+      total: 0
+    }
+  },
+  computed: {
+    title () {
+      return Object.keys(this.itemData).length ? '编辑' : '新增'
+    }
+  },
+  created () {
+    this.init()
+  },
+  methods: {
+    async init () {},
+    handleSearch (val) {
+      Object.assign(this.queryData, val)
+      this.pageInfo.current = 1
+      this.init()
+    },
+    handleImport () {
+      this.itemData = {}
+      this.show = true
+    },
+    async handleEdit (item) {
+      this.itemData = item
+      this.show = true
+    },
+    handleDelete (ids) {
+      if (Array.isArray(ids) && !ids.length) return this.$snackbar.warning('请选择要删除的选项')
+      this.$confirm('提示', '是否删除该选项')
+        .then(async () => {
+          try {
+            this.$snackbar.success('删除成功')
+            this.init()
+          } catch (error) {
+            this.$snackbar.error('删除失败')
+          }
+        })
+    },
+    handleSubmit () {},
+    pageHandleChange (page) {
+      this.pageInfo.current = page
+      this.init()
+    },
+    handleSort (val) {
+      this.orders = val
+      this.init()
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 53 - 0
src/views/dataFactory/dataCollection/manualCollection/index.vue

@@ -0,0 +1,53 @@
+<template>
+  <div>
+    <div>
+      <v-tabs v-model="tab">
+        <v-tab v-for="item in items" :key="item.path" @click="handleTo(item.path)">{{ item.title }}</v-tab>
+      </v-tabs>
+    </div>
+    <div class="pa-3 white">
+      <v-tabs-items v-model="tab">
+        <v-tab-item v-for="item in items" :key="item.path">
+          <component :is="item.path"></component>
+        </v-tab-item>
+      </v-tabs-items>
+    </div>
+  </div>
+</template>
+
+<script>
+import { loadDynamicComponents } from '@/utils'
+const vueComponent = require.context('./dynamic', true, /\.vue$/)
+const autoComponent = loadDynamicComponents(vueComponent)
+export default {
+  name: 'manual-collection',
+  components: { ...autoComponent },
+  data () {
+    return {
+      tab: 0,
+      items: []
+    }
+  },
+  created () {
+    this.items = this.$route.meta.roles.filter(item => +item.active)
+      .sort((a, b) => a.sort - b.sort)
+      .map(e => ({ title: e.label, path: e.name }))
+
+    const savedTab = this.$route.query.tab
+    if (!savedTab) {
+      this.tab = 0
+      return
+    }
+    this.tab = this.items.findIndex(e => e.path === savedTab)
+  },
+  methods: {
+    handleTo (path) {
+      this.$router.push(`${this.$route.path}?tab=${path}`)
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+
+</style>