龚成明 3 éve
commit
0334b75538

+ 25 - 0
.gitignore

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

+ 24 - 0
README.md

@@ -0,0 +1,24 @@
+# eth_viewer_webapp
+
+## Project setup
+```
+yarn install
+```
+
+### Compiles and hot-reloads for development
+```
+yarn serve
+```
+
+### Compiles and minifies for production
+```
+yarn build
+```
+
+### Lints and fixes files
+```
+yarn lint
+```
+
+### Customize configuration
+See [Configuration Reference](https://cli.vuejs.org/config/).

+ 5 - 0
babel.config.js

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

+ 59 - 0
package.json

@@ -0,0 +1,59 @@
+{
+  "name": "410ETH",
+  "version": "0.1.0",
+  "private": true,
+  "scripts": {
+    "serve": "vue-cli-service serve",
+    "build": "vue-cli-service build",
+    "lint": "vue-cli-service lint"
+  },
+  "dependencies": {
+    "@mdi/font": "^5.6.55",
+    "core-js": "^3.6.5",
+    "md5-node": "^1.0.1",
+    "vue": "^2.6.11",
+    "vuetify": "^2.2.11"
+  },
+  "devDependencies": {
+    "@vue/cli-plugin-babel": "~4.5.0",
+    "@vue/cli-plugin-eslint": "~4.5.0",
+    "@vue/cli-service": "~4.5.0",
+    "axios": "^0.18.1",
+    "babel-eslint": "^10.1.0",
+    "eslint": "^6.7.2",
+    "eslint-plugin-vue": "^6.2.2",
+    "sass": "^1.19.0",
+    "sass-loader": "^8.0.0",
+    "vue-cli-plugin-vuetify": "~2.0.7",
+    "vue-template-compiler": "^2.6.11",
+    "vuetify-loader": "^1.3.0"
+  },
+  "eslintConfig": {
+    "root": true,
+    "env": {
+      "node": true
+    },
+    "extends": [
+      "plugin:vue/essential",
+      "eslint:recommended"
+    ],
+    "parserOptions": {
+      "parser": "babel-eslint"
+    },
+    "rules": {}
+  },
+  "browserslist": [
+    "> 1%",
+    "last 2 versions",
+    "not dead"
+  ],
+  "rules": {
+    "generator-star-spacing": "off",
+    "no-tabs": "off",
+    "no-unused-vars": "off",
+    "no-console": "off",
+    "no-irregular-whitespace": "off",
+    "no-debugger": "off",
+    "no-undef": "off"
+  }
+}

BIN
public/favicon.ico


+ 19 - 0
public/index.html

@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html lang="en">
+  <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>
+    <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900">
+    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@latest/css/materialdesignicons.min.css">
+  </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>

+ 151 - 0
src/App.vue

@@ -0,0 +1,151 @@
+<template>
+  <v-app>
+    <v-app-bar
+      app
+      elevation="1"
+    >
+      <div class="d-flex align-center">
+        <v-img
+          alt="Logo"
+          class="shrink mr-2"
+          contain
+          src="./assets/logo-ether.png"
+          transition="scale-transition"
+          width="180"
+        />
+      </div>
+
+      <v-tabs
+        v-model="tab"
+        v-if="calcPwdMD5() === pwdMD5"
+      >
+        <v-tab
+          key="history"
+        >
+          历史记录
+        </v-tab>
+        <v-tab
+          key="new"
+        >
+          最新动态
+        </v-tab>
+        <v-tab
+            key="my"
+        >
+          我的交易
+        </v-tab>
+        <v-tab
+            key="config"
+        >
+          参数配置
+        </v-tab>
+      </v-tabs>
+
+      <v-spacer></v-spacer>
+
+      <v-btn
+        href="https://etherscan.io/"
+        target="_blank"
+        text
+      >
+        <span class="mr-2">Etherscan</span>
+        <v-icon>mdi-open-in-new</v-icon>
+      </v-btn>
+    </v-app-bar>
+
+    <v-main>
+      <v-tabs-items
+        v-model="tab"
+        v-if="calcPwdMD5() !== pwdMD5"
+      >
+        <v-container>
+          <v-row>
+            <v-col cols="5">
+
+            </v-col>
+            <v-col cols="2">
+              <v-img src="./assets/money_transfer__monochromatic.svg"></v-img>
+            </v-col>
+          </v-row>
+          <v-row>
+            <v-col cols="3">
+
+            </v-col>
+            <v-col cols="6">
+              <v-text-field
+                v-model="pwd"
+                append-icon="mdi-cloud-search"
+                outlined
+                label="Search Address"
+                type="text"
+                @click:append="jump"
+              ></v-text-field>
+            </v-col>
+            <v-col cols="3">
+
+            </v-col>
+          </v-row>
+        </v-container>
+      </v-tabs-items>
+      <v-tabs-items
+        v-model="tab"
+        v-else
+      >
+        <v-tab-item key="history">
+          <History></History>
+        </v-tab-item>
+        <v-tab-item key="new">
+          <New></New>
+        </v-tab-item>
+        <v-tab-item key="my">
+          <My></My>
+        </v-tab-item>
+        <v-tab-item key="config">
+          <Config></Config>
+        </v-tab-item>
+      </v-tabs-items>
+    </v-main>
+  </v-app>
+</template>
+
+<script>
+
+import History from "@/components/History";
+import New from "@/components/New";
+import My from "@/components/My";
+import Config from "@/components/Config";
+import md5 from 'md5-node'
+
+export default {
+  name: 'App',
+
+  components: {
+    History,
+    New,
+    My,
+    Config
+  },
+
+  data: () => ({
+    tab: 'history',
+    pwdMD5: '7f5029bbd4daae18f3ae4cd910f0f4f0',
+    pwd: ''
+  }),
+
+  methods: {
+    calcPwdMD5 () {
+      return md5(this.pwd)
+    },
+    jump () {
+      window.open('https://etherscan.io/address/' + this.pwd)
+    }
+  }
+};
+</script>
+
+<style slot-scope="sass">
+.v-card {
+  padding: 20px;
+  margin-top: 2px;
+}
+</style>

BIN
src/assets/logo-ether.png


BIN
src/assets/logo.png


+ 1 - 0
src/assets/logo.svg

@@ -0,0 +1 @@
+<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 87.5 100"><defs><style>.cls-1{fill:#1697f6;}.cls-2{fill:#7bc6ff;}.cls-3{fill:#1867c0;}.cls-4{fill:#aeddff;}</style></defs><title>Artboard 46</title><polyline class="cls-1" points="43.75 0 23.31 0 43.75 48.32"/><polygon class="cls-2" points="43.75 62.5 43.75 100 0 14.58 22.92 14.58 43.75 62.5"/><polyline class="cls-3" points="43.75 0 64.19 0 43.75 48.32"/><polygon class="cls-4" points="64.58 14.58 87.5 14.58 43.75 100 43.75 62.5 64.58 14.58"/></svg>

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 0 - 0
src/assets/money_transfer__monochromatic.svg


+ 356 - 0
src/components/Config.vue

@@ -0,0 +1,356 @@
+<template>
+  <v-card
+      elevation="0"
+  >
+    <v-card-title>
+      Config
+      <v-spacer></v-spacer>
+      <v-text-field
+        v-model="search"
+        append-icon="mdi-magnify"
+        label="Search"
+        single-line
+        hide-details
+      ></v-text-field>
+      <v-spacer></v-spacer>
+      <v-btn
+          color="primary"
+          dark
+          class="mb-2"
+          @click="dialog = true"
+          elevation="0"
+      >
+        New Item
+      </v-btn>
+      <v-spacer></v-spacer>
+      <v-btn
+          color="primary"
+          dark
+          class="mb-2"
+          @click="get"
+          elevation="0"
+      >
+        Flush
+      </v-btn>
+    </v-card-title>
+
+    <v-dialog
+        v-model="dialog"
+        max-width="500px"
+    >
+      <v-card>
+        <v-card-title>
+          <span class="headline">{{ formTitle }}</span>
+        </v-card-title>
+
+        <v-card-text>
+          <v-container>
+            <v-row>
+              <v-col cols="12" sm="6" md="6">
+                <v-text-field type="number" v-model="editedItem.ts" label="Ts"></v-text-field>
+              </v-col>
+              <v-col cols="12" sm="6" md="6">
+                <v-text-field v-model="editedItem.type" label="Type"></v-text-field>
+              </v-col>
+              <v-col cols="12" sm="6" md="12">
+                <v-text-field v-model="editedItem.hs" label="Hash"></v-text-field>
+              </v-col>
+              <v-col cols="12" sm="12" md="12">
+                <v-textarea outlined v-model="editedItem.o" label="Origin"></v-textarea>
+              </v-col>
+            </v-row>
+          </v-container>
+        </v-card-text>
+
+        <v-card-actions>
+          <v-spacer></v-spacer>
+          <v-btn
+              color="blue darken-1"
+              text
+              @click="close"
+          >
+            Cancel
+          </v-btn>
+          <v-btn
+              color="blue darken-1"
+              text
+              @click="save"
+          >
+            Save
+          </v-btn>
+        </v-card-actions>
+      </v-card>
+    </v-dialog>
+    <v-dialog v-model="dialogDelete" max-width="500px">
+      <v-card>
+        <v-card-title class="headline">Are you sure you want to delete this item?</v-card-title>
+        <v-card-actions>
+          <v-spacer></v-spacer>
+          <v-btn color="blue darken-1" text @click="closeDelete">Cancel</v-btn>
+          <v-btn color="blue darken-1" text @click="deleteItemConfirm">OK</v-btn>
+          <v-spacer></v-spacer>
+        </v-card-actions>
+      </v-card>
+    </v-dialog>
+
+    <v-data-table
+      dense
+      hide-default-footer
+      multi-sort
+      :headers="headers"
+      :items="tableData"
+      :search="search"
+      :items-per-page="10000"
+      :loading="loading"
+      :sort-by="['ts', 'block', 'nonce']"
+      :sort-desc="[true, true, true]"
+    >
+      <template v-slot:item.ts="{ item }">
+        {{ getTime(item.ts) }}
+      </template>
+      <!-- Hash -->
+      <template v-slot:item.hs="{ item }">
+        <v-tooltip right>
+          <template v-slot:activator="{ on, attrs }">
+            <v-btn small :href="'https://etherscan.io/tx/' + item.hs" target="_blank" text v-on="on" v-bind="attrs">{{ getSimpleStr(item.hs) }}</v-btn>
+          </template>
+          <span>{{ item.hs }}</span>
+        </v-tooltip>
+      </template>
+      <template v-slot:item.actions="{ item }">
+        <v-icon
+            small
+            class="mr-2"
+            @click="editItem(item)"
+        >
+          mdi-pencil
+        </v-icon>
+        <v-icon
+            small
+            @click="deleteItem(item)"
+        >
+          mdi-delete
+        </v-icon>
+      </template>
+    </v-data-table>
+
+    <v-snackbar centered top
+                v-model="snackbar.visible"
+                :timeout="snackbar.timeout" :color="snackbar.color">
+      <v-icon left>{{ snackbar.icon }}</v-icon>
+      {{ snackbar.msg }}
+    </v-snackbar>
+  </v-card>
+</template>
+
+<script>
+import axios from 'axios'
+
+export default {
+  name: 'Config',
+
+  methods: {
+    // 时间戳转换chart时间
+    getTime (timestamp) {
+      // 组织日期格式并返回
+      return this.dateFormat('YYYY-mm-dd HH:MM:SS:sss', new Date(timestamp * 1000))
+    },
+    dateFormat(fmt, date) {
+      let ret;
+      const opt = {
+        "Y+": date.getFullYear().toString(),        // 年
+        "m+": (date.getMonth() + 1).toString(),     // 月
+        "d+": date.getDate().toString(),            // 日
+        "H+": date.getHours().toString(),           // 时
+        "M+": date.getMinutes().toString(),         // 分
+        "S+": date.getSeconds().toString(),         // 秒
+        "s+": date.getMilliseconds().toString()     // 毫秒
+        // 有其他格式化字符需求可以继续添加,必须转化成字符串
+      };
+      for (let k in opt) {
+        ret = new RegExp("(" + k + ")").exec(fmt);
+        if (ret) {
+          fmt = fmt.replace(ret[1], (ret[1].length === 1) ? (opt[k]) : (opt[k].padStart(ret[1].length, "0")))
+        }
+      }
+      return fmt;
+    },
+    // 最后四位
+    getSimpleStr (str) {
+      if (str && str.indexOf('x') !== -1) {
+        return str.substr(0, 7) + '...' + str.substr(-4)
+      } else {
+        return ''
+      }
+    },
+    jump (pairAddress) {
+      window.open('https://etherscan.io/address/' + pairAddress)
+    },
+    editItem (item) {
+      this.editedIndex = this.tableData.indexOf(item)
+      this.editedItem = Object.assign({}, item)
+      this.dialog = true
+    },
+
+    deleteItem (item) {
+      this.editedIndex = this.tableData.indexOf(item)
+      this.editedItem = Object.assign({}, item)
+      this.dialogDelete = true
+    },
+
+    async deleteItemConfirm () {
+      this.loading = true
+      const { data: rst } = await axios.get(this.url + '/config/delete', { params: {
+        hs: this.tableData[this.editedIndex].hs
+      }})
+      this.loading = false
+
+      if (rst.state) {
+        this.tableData.splice(this.editedIndex, 1)
+
+        this.closeDelete()
+        this.snackbarSuccess(rst.msg)
+      } else {
+        this.snackbarFail(rst.msg)
+      }
+    },
+
+    close () {
+      this.dialog = false
+      this.$nextTick(() => {
+        this.editedItem = Object.assign({}, this.defaultItem)
+        this.editedIndex = -1
+      })
+    },
+
+    closeDelete () {
+      this.dialogDelete = false
+      this.$nextTick(() => {
+        this.editedItem = Object.assign({}, this.defaultItem)
+        this.editedIndex = -1
+      })
+    },
+
+    async save () {
+      this.editedItem.o = this.editedItem.o.replaceAll('[', '【').replaceAll(']', '】')
+      this.loading = true
+
+      if (this.editedIndex > -1) {
+        const { data: rst } = await axios.get(this.url + '/config/update', { params: this.editedItem })
+
+        if (rst.state) {
+          Object.assign(this.tableData[this.editedIndex], rst.data)
+          this.snackbarSuccess(rst.msg)
+
+          this.close()
+        } else {
+          this.snackbarFail(rst.msg)
+        }
+      } else {
+        const { data: rst } = await axios.get(this.url + '/config/add', { params: this.editedItem })
+
+        if (rst.state) {
+          this.tableData.push(rst.data)
+          this.snackbarSuccess(rst.msg)
+
+          this.close()
+        } else {
+          this.snackbarFail(rst.msg)
+        }
+      }
+      this.loading = false
+    },
+
+    async get () {
+      this.loading = true
+      const { data: rst } = await axios.get(this.url + '/config/get')
+
+      if (rst.state) {
+        this.tableData = rst.data.list
+        this.snackbarSuccess(rst.msg)
+      } else {
+        this.snackbarFail(rst.msg)
+      }
+      this.loading = false
+    },
+
+    snackbarSuccess (msg, timeout = 2000) {
+      this.snackbar.msg = msg
+      this.snackbar.visible = true
+      this.snackbar.timeout = timeout
+      this.snackbar.icon = 'mdi-check-circle-outline'
+      this.snackbar.color = 'teal'
+    },
+
+    snackbarFail (msg, timeout = 2000) {
+      this.snackbar.msg = msg
+      this.snackbar.visible = true
+      this.snackbar.timeout = timeout
+      this.snackbar.icon = 'mdi-alert-octagram-outline'
+      this.snackbar.color = 'purple'
+    }
+  },
+
+  data: () => ({
+    query: {},
+
+    search: '',
+
+    loading: false,
+
+    headers: [
+      { text: 'Save Time', value: 'ts' },
+      { text: '类型', value: 'type' },
+      { text: 'Hash', value: 'hs' },
+      { text: '  ', value: 'o' },
+      { text: '操作', value: 'actions' }
+    ],
+
+    tableData: [],
+
+    dialog: false,
+
+    dialogDelete: false,
+
+    editedIndex: -1,
+
+    editedItem: {
+      ts: 0,
+      hs: '',
+      type: '',
+      o: ''
+    },
+
+    defaultItem: {
+      ts: 0,
+      hs: '',
+      type: '',
+      o: ''
+    },
+
+    url: 'http://410eth.com',
+
+    snackbar: {
+      msg: '嗯哼?',
+      visible: false,
+      timeout: 2000,
+      color: 'green',
+      icon: ''
+    }
+  }),
+
+  computed: {
+    formTitle () {
+      return this.editedIndex === -1 ? 'New Item' : 'Edit Item'
+    },
+  },
+
+  mounted() {
+    if (process.env.NODE_ENV === 'development') {
+      this.url = '/api'
+    }
+
+    this.get()
+  }
+}
+</script>

+ 334 - 0
src/components/History.vue

@@ -0,0 +1,334 @@
+<template>
+  <v-card
+      elevation="1"
+  >
+    <v-container>
+      <v-row>
+        <!-- Start Time过滤 -->
+        <v-col
+            cols="18"
+            md="2"
+        >
+          <v-text-field
+              v-model="query.t1_str"
+              label="Start Time(YYYY/mm/DD HH:MM:SS)"
+              required
+          ></v-text-field>
+        </v-col>
+
+        <!-- End Time过滤 -->
+        <v-col
+            cols="18"
+            md="2"
+        >
+          <v-text-field
+              v-model="query.t2_str"
+              label="End Time(YYYY/mm/DD HH:MM:SS)"
+              required
+          ></v-text-field>
+        </v-col>
+
+        <!-- hash过滤 -->
+        <v-col
+            cols="18"
+            md="2"
+        >
+          <v-text-field
+              v-model="query.hs"
+              label="Hash"
+              required
+          ></v-text-field>
+        </v-col>
+
+        <!-- From -->
+        <v-col
+            cols="18"
+            md="2"
+        >
+          <v-text-field
+              v-model="query.fm"
+              label="From"
+              required
+          ></v-text-field>
+        </v-col>
+
+        <!-- To -->
+        <v-col
+            cols="18"
+            md="2"
+        >
+          <v-text-field
+              v-model="query.t"
+              label="To"
+              required
+          ></v-text-field>
+        </v-col>
+
+        <!-- State -->
+        <v-col
+            cols="18"
+            md="1"
+        >
+          <v-text-field
+              v-model="query.state"
+              label="State"
+              required
+          ></v-text-field>
+        </v-col>
+
+        <v-col
+            cols="18"
+            md="1"
+        >
+          <v-btn outlined tile color="primary" @click="getRecord">
+            <v-icon left>mdi-cloud-search-outline</v-icon>
+            Search
+          </v-btn>
+        </v-col>
+      </v-row>
+    </v-container>
+
+    <v-card
+      elevation="0"
+    >
+      <v-card-title>
+        History Data
+        <v-spacer></v-spacer>
+        <v-text-field
+            v-model="search"
+            append-icon="mdi-magnify"
+            label="Search"
+            single-line
+            hide-details
+        ></v-text-field>
+      </v-card-title>
+
+      <v-data-table
+        dense
+        hide-default-footer
+        multi-sort
+        :headers="headers"
+        :items="tableData"
+        :search="search"
+        :items-per-page="10000"
+        :loading="loading"
+        :sort-by="['ts', 'block', 'nonce']"
+        :sort-desc="[true, true, true]"
+      >
+        <template v-slot:item.ts="{ item }">
+          {{ getTime(item.ts) }}
+        </template>
+        <!-- Type -->
+        <template v-slot:item.type="{ item }">
+          <v-chip small v-if="item.o.type.indexOf('err') !== -1" outlined label color="grey">
+            <v-icon small left>mdi-filter-remove</v-icon>
+            {{ item.o.type.split(':')[1] }}
+          </v-chip>
+          <v-chip small v-else-if="item.o.type.indexOf('WARN') !== -1" outlined label color="red lighten-2">
+            <v-icon small left>mdi-alert-circle-outline</v-icon>
+            {{ item.o.type.split(':')[1] }}
+          </v-chip>
+          <v-chip small v-else-if="item.o.type.indexOf('DS:') !== -1" outlined label color="cyan darken-3">
+            <v-icon small left>mdi-horse-human</v-icon>
+            {{ item.o.type.split(':')[1] }}
+          </v-chip>
+          <v-chip small v-else-if="item.o.type.indexOf('CASH') !== -1" outlined label color="black">
+            <v-icon small left>mdi-currency-usd</v-icon>
+            {{ item.o.type.split(':')[1] }}
+          </v-chip>
+          <v-chip small v-else-if="item.o.type.indexOf('ZIJI') !== -1" outlined label color="blue">
+            <v-icon small left>mdi-panda</v-icon>
+            {{ item.o.type.split(':')[1] }}
+          </v-chip>
+        </template>
+        <!-- Hash -->
+        <template v-slot:item.hs="{ item }">
+          <v-tooltip right>
+            <template v-slot:activator="{ on, attrs }">
+              <v-btn small :href="'https://etherscan.io/tx/' + item.hs" target="_blank" text v-on="on" v-bind="attrs">{{ getSimpleStr(item.hs) }}</v-btn>
+            </template>
+            <span>{{ item.hs }}</span>
+          </v-tooltip>
+        </template>
+        <!-- From -->
+<!--        <template v-slot:item.fm="{ item }">-->
+<!--          <v-tooltip right>-->
+<!--            <template v-slot:activator="{ on, attrs }">-->
+<!--              <v-btn :href="'https://etherscan.io/address/' + item.fm" target="_blank" text v-on="on" v-bind="attrs">{{ getSimpleStr(item.fm) }}</v-btn>-->
+<!--            </template>-->
+<!--            <span>{{ item.fm }}</span>-->
+<!--          </v-tooltip>-->
+<!--        </template>-->
+        <!-- Address -->
+        <template v-slot:item.symbolAddress="{ item }">
+          <v-tooltip right>
+            <template v-slot:activator="{ on, attrs }">
+              <v-btn small :href="'https://etherscan.io/address/' + item.symbolAddress" target="_blank" text v-on="on" v-bind="attrs">{{ getSimpleStr(item.symbolAddress) }}</v-btn>
+            </template>
+            <span>{{ item.symbolAddress }}</span>
+          </v-tooltip>
+        </template>
+        <!-- Gas -->
+        <template v-slot:item.o="{ item }">
+          {{ item.o.gas }}
+        </template>
+        <!-- 利润 -->
+        <template v-slot:item.profit="{ item }">
+          {{ item.o.profit }}
+        </template>
+        <!-- Block -->
+        <template v-slot:item.block="{ item }">
+          {{ item.o.blockNumber }}
+        </template>
+        <!-- Nonce -->
+        <template v-slot:item.nonce="{ item }">
+          {{ item.o.nonce }}
+        </template>
+        <!-- origin -->
+        <template v-slot:item.origin>
+          {{ '' }}
+        </template>
+        <!-- State -->
+        <template v-slot:item.state="{ item }">
+          <v-chip small v-if="item.state === 'ok'" outlined label color="teal"> ok </v-chip>
+          <v-chip small v-else-if="item.state === 'pending'" outlined label color="orange"> {{ item.state }} </v-chip>
+          <v-chip small v-else-if="item.state === 'fail'" outlined label color="red lighten-3"> {{ item.state }} </v-chip>
+          <v-chip small v-else-if="item.state === 'cancel'" outlined label color="blue-grey"> {{ item.state }} </v-chip>
+        </template>
+        <!-- Amount -->
+        <template v-slot:item.amount="{ item }">
+          <div v-if="!item.o.intoken">
+            <v-chip small outlined color="red accent-1">-{{ item.o.inamount }}(TOKEN)</v-chip>
+            >
+            <v-chip small outlined color="teal lighten-1" @click="jump(item.o.pairAddress)">+{{ item.o.outamount }}({{ item.o.outtoken }})</v-chip>
+          </div>
+          <div v-else-if="!item.o.outtoken">
+            <v-chip small outlined color="red accent-1">-{{ item.o.inamount }}({{ item.o.intoken }})</v-chip>
+            >
+            <v-chip small outlined color="teal lighten-1" @click="jump(item.o.pairAddress)">+{{ item.o.outamount }}(TOKEN)</v-chip>
+          </div>
+          <div v-else>
+            <v-chip small outlined color="red accent-1">-{{ item.o.inamount }}({{ item.o.intoken }})</v-chip>
+            >
+            <v-chip small outlined color="teal lighten-1" @click="jump(item.o.pairAddress)">+{{ item.o.outamount }}({{ item.o.outtoken }})</v-chip>
+          </div>
+        </template>
+      </v-data-table>
+    </v-card>
+  </v-card>
+</template>
+
+<script>
+  import axios from 'axios'
+
+  export default {
+    name: 'History',
+
+    methods: {
+      // 时间戳转换chart时间
+      getTime (timestamp) {
+        // 组织日期格式并返回
+        return this.dateFormat('YYYY-mm-dd HH:MM:SS:sss', new Date(timestamp * 1000))
+      },
+      dateFormat(fmt, date) {
+        let ret;
+        const opt = {
+          "Y+": date.getFullYear().toString(),        // 年
+          "m+": (date.getMonth() + 1).toString(),     // 月
+          "d+": date.getDate().toString(),            // 日
+          "H+": date.getHours().toString(),           // 时
+          "M+": date.getMinutes().toString(),         // 分
+          "S+": date.getSeconds().toString(),         // 秒
+          "s+": date.getMilliseconds().toString()     // 毫秒
+          // 有其他格式化字符需求可以继续添加,必须转化成字符串
+        };
+        for (let k in opt) {
+          ret = new RegExp("(" + k + ")").exec(fmt);
+          if (ret) {
+            fmt = fmt.replace(ret[1], (ret[1].length === 1) ? (opt[k]) : (opt[k].padStart(ret[1].length, "0")))
+          }
+        }
+        return fmt;
+      },
+      // 最后四位
+      getSimpleStr (str) {
+        if (str && str.indexOf('x') !== -1) {
+          return str.substr(0, 7) + '...' + str.substr(-4)
+        } else {
+          return ''
+        }
+      },
+      async getRecord () {
+        let url = 'http://410eth.com/data/getRecord'
+        if (process.env.NODE_ENV === 'development') {
+          url = '/api/data/getRecord'
+        }
+        this.tableData.length = 0
+        this.loading = true
+        this.query.t1 = this.query.t1_str ? this.dateToTimestamp(this.query.t1_str) : ''
+        this.query.t2 = this.query.t2_str ? this.dateToTimestamp(this.query.t2_str) : ''
+        const data = await axios.get(url, { params: this.query })
+
+        data.data.data.map(function (one) {
+          one.origin = one.o
+          one.o = JSON.parse(one.o.replaceAll("'", '"'))
+          one.blockNumber = one.o.blockNumber
+          one.gas = one.o.gas
+          one.nonce = one.o.nonce
+          one.profit = one.o.profit
+          one.symbolAddress = one.o.symbolAddress
+        })
+
+        this.tableData = data.data.data
+        this.loading = false
+      },
+      dateToTimestamp (str) {
+        let [a, b] = str.split(' ')
+        let [year, month, day] = a.split('/')
+        let [hour, minute, second] = b.split(':')
+
+        return new Date(year, month - 1, day, hour, minute, second).getTime() / 1000
+      },
+      jump (pairAddress) {
+        window.open('https://etherscan.io/address/' + pairAddress)
+      }
+    },
+
+    data: () => ({
+      query: {
+        t1: '',
+        t1_str: '',
+        t2: '',
+        t2_str: '',
+        fm: '',
+        t: '',
+        state: '',
+        hs: ''
+      },
+      search: '',
+      loading: false,
+      headers: [
+        { text: 'Save Time', value: 'ts' },
+        // { text: 'Sender', value: 'fm' },
+        { text: '类型', value: 'type' },
+        { text: 'Symbol', value: 'symbolAddress' },
+        { text: '交易数量', value: 'amount' },
+        { text: '利润', value: 'profit' },
+        { text: 'Gas', value: 'gas' },
+        { text: '状态', value: 'state' },
+        { text: 'BlockNumber', value: 'blockNumber' },
+        { text: 'Nonce', value: 'nonce' },
+        { text: 'Hash', value: 'hs' },
+        { text: '  ', value: 'origin' }
+      ],
+      history: {"data":[{"t":"0x8a2a17e7adb6cbf6635380a35c04c01db473ed5ca919706f1c492d3aa1d9f1a9","fm":"0x6ff6f16a2459114fad74eed1604b402b97b02717","hs":"0x8a2a17e7adb6cbf6635380a35c04c01db473ed5ca919706f1c492d3aa1d9f1a9","state":"ok","ts":1602503469,"o":"{'type': 'NO TOKEN', 'intoken': 'WETH', 'outtoken': false, 'inamount': 0.314674952033383, 'outamount': 48953401866254456333, 'symbol': false, 'gas': 56.100000233, 'profit': 0, 'balance': 0, 'stocks': 0, 'decimals': 18, 'symbolAddress': '0x054f76beed60ab6dbeb23502178c52d6c5debe40', 'pairAddress': false, 'from': '0x6ff6f16a2459114fad74eed1604b402b97b02717', 'to': '0x8a2a17e7adb6cbf6635380a35c04c01db473ed5ca919706f1c492d3aa1d9f1a9', 'hs': '0x8a2a17e7adb6cbf6635380a35c04c01db473ed5ca919706f1c492d3aa1d9f1a9'}"}],"count":1,"state":"ok"},
+      tableData: []
+    }),
+
+    mounted() {
+      this.query.t2_str = '2099/1/1 0:0:0'
+      this.query.t1_str = this.dateFormat('YYYY/mm/dd HH:MM:SS', new Date(new Date().getTime() - 10 * 60 * 1000))
+    }
+  }
+</script>
+cl

+ 355 - 0
src/components/My.vue

@@ -0,0 +1,355 @@
+<template>
+  <v-card
+      elevation="1"
+  >
+    <v-container>
+      <v-row>
+        <!-- Start Time过滤 -->
+        <v-col
+            cols="18"
+            md="2"
+        >
+          <v-text-field
+              v-model="query.t1_str"
+              label="Start Time(YYYY/mm/DD HH:MM:SS)"
+              required
+          ></v-text-field>
+        </v-col>
+
+        <!-- End Time过滤 -->
+        <v-col
+            cols="18"
+            md="2"
+        >
+          <v-text-field
+              v-model="query.t2_str"
+              label="End Time(YYYY/mm/DD HH:MM:SS)"
+              required
+          ></v-text-field>
+        </v-col>
+
+        <!-- hash过滤 -->
+        <v-col
+            cols="18"
+            md="2"
+        >
+          <v-text-field
+              v-model="query.hs"
+              label="Hash"
+              required
+          ></v-text-field>
+        </v-col>
+
+        <!-- From -->
+        <v-col
+            cols="18"
+            md="2"
+        >
+          <v-text-field
+              v-model="query.fm"
+              label="From"
+              required
+          ></v-text-field>
+        </v-col>
+
+        <!-- To -->
+        <v-col
+            cols="18"
+            md="2"
+        >
+          <v-text-field
+              v-model="query.t"
+              label="To"
+              required
+          ></v-text-field>
+        </v-col>
+
+        <!-- State -->
+        <v-col
+            cols="18"
+            md="1"
+        >
+          <v-text-field
+              v-model="query.state"
+              label="State"
+              required
+          ></v-text-field>
+        </v-col>
+
+        <v-col
+            cols="18"
+            md="1"
+        >
+          <v-btn outlined tile color="primary" @click="searchData">
+            <v-icon left>mdi-cloud-search-outline</v-icon>
+            Search
+          </v-btn>
+        </v-col>
+      </v-row>
+    </v-container>
+
+    <v-card
+      elevation="0"
+    >
+      <v-card-title>
+        My Data
+        <v-spacer></v-spacer>
+        <v-text-field
+            v-model="search"
+            append-icon="mdi-magnify"
+            label="Search"
+            single-line
+            hide-details
+        ></v-text-field>
+      </v-card-title>
+
+      <v-data-table
+        dense
+        hide-default-footer
+        multi-sort
+        :headers="headers"
+        :items="tableData"
+        :search="search"
+        :items-per-page="10000"
+        :loading="loading"
+        :sort-by="['ts', 'block', 'nonce']"
+        :sort-desc="[true, true, true]"
+      >
+        <template v-slot:item.ts="{ item }">
+          {{ getTime(item.ts) }}
+        </template>
+        <!-- Type -->
+        <template v-slot:item.type="{ item }">
+          <v-chip small v-if="item.o.type.indexOf('err') !== -1" outlined label color="grey">
+            <v-icon small left>mdi-filter-remove</v-icon>
+            {{ item.o.type.split(':')[1] }}
+          </v-chip>
+          <v-chip small v-else-if="item.o.type.indexOf('WARN') !== -1" outlined label color="red lighten-2">
+            <v-icon small left>mdi-alert-circle-outline</v-icon>
+            {{ item.o.type.split(':')[1] }}
+          </v-chip>
+          <v-chip small v-else-if="item.o.type.indexOf('DS:') !== -1" outlined label color="cyan darken-3">
+            <v-icon small left>mdi-horse-human</v-icon>
+            {{ item.o.type.split(':')[1] }}
+          </v-chip>
+          <v-chip small v-else-if="item.o.type.indexOf('CASH') !== -1" outlined label color="black">
+            <v-icon small left>mdi-currency-usd</v-icon>
+            {{ item.o.type.split(':')[1] }}
+          </v-chip>
+          <v-chip small v-else-if="item.o.type.indexOf('ZIJI') !== -1" outlined label color="blue">
+            <v-icon small left>mdi-panda</v-icon>
+            {{ item.o.type.split(':')[1] }}
+          </v-chip>
+        </template>
+        <!-- Hash -->
+        <template v-slot:item.hs="{ item }">
+          <v-tooltip right>
+            <template v-slot:activator="{ on, attrs }">
+              <v-btn small :href="'https://etherscan.io/tx/' + item.hs" target="_blank" text v-on="on" v-bind="attrs">{{ getSimpleStr(item.hs) }}</v-btn>
+            </template>
+            <span>{{ item.hs }}</span>
+          </v-tooltip>
+        </template>
+        <!-- From -->
+<!--        <template v-slot:item.fm="{ item }">-->
+<!--          <v-tooltip right>-->
+<!--            <template v-slot:activator="{ on, attrs }">-->
+<!--              <v-btn :href="'https://etherscan.io/address/' + item.fm" target="_blank" text v-on="on" v-bind="attrs">{{ getSimpleStr(item.fm) }}</v-btn>-->
+<!--            </template>-->
+<!--            <span>{{ item.fm }}</span>-->
+<!--          </v-tooltip>-->
+<!--        </template>-->
+        <!-- Address -->
+        <template v-slot:item.symbolAddress="{ item }">
+          <v-tooltip right>
+            <template v-slot:activator="{ on, attrs }">
+              <v-btn small :href="'https://etherscan.io/address/' + item.symbolAddress" target="_blank" text v-on="on" v-bind="attrs">{{ getSimpleStr(item.symbolAddress) }}</v-btn>
+            </template>
+            <span>{{ item.symbolAddress }}</span>
+          </v-tooltip>
+        </template>
+        <!-- Gas -->
+        <template v-slot:item.o="{ item }">
+          {{ item.o.gas }}
+        </template>
+        <!-- 利润 -->
+        <template v-slot:item.profit="{ item }">
+          {{ item.o.profit }}
+        </template>
+        <!-- Block -->
+        <template v-slot:item.block="{ item }">
+          {{ item.o.blockNumber }}
+        </template>
+        <!-- Nonce -->
+        <template v-slot:item.nonce="{ item }">
+          {{ item.o.nonce }}
+        </template>
+        <!-- origin -->
+        <template v-slot:item.origin>
+          {{ '' }}
+        </template>
+        <!-- State -->
+        <template v-slot:item.state="{ item }">
+          <v-chip small v-if="item.state === 'ok'" outlined label color="teal"> ok </v-chip>
+          <v-chip small v-else-if="item.state === 'pending'" outlined label color="orange"> {{ item.state }} </v-chip>
+          <v-chip small v-else-if="item.state === 'fail'" outlined label color="red lighten-3"> {{ item.state }} </v-chip>
+          <v-chip small v-else-if="item.state === 'cancel'" outlined label color="blue-grey"> {{ item.state }} </v-chip>
+        </template>
+        <!-- Amount -->
+        <template v-slot:item.amount="{ item }">
+          <div v-if="!item.o.intoken">
+            <v-chip small outlined color="red accent-1">-{{ item.o.inamount }}(TOKEN)</v-chip>
+            >
+            <v-chip small outlined color="teal lighten-1" @click="jump(item.o.pairAddress)">+{{ item.o.outamount }}({{ item.o.outtoken }})</v-chip>
+          </div>
+          <div v-else-if="!item.o.outtoken">
+            <v-chip small outlined color="red accent-1">-{{ item.o.inamount }}({{ item.o.intoken }})</v-chip>
+            >
+            <v-chip small outlined color="teal lighten-1" @click="jump(item.o.pairAddress)">+{{ item.o.outamount }}(TOKEN)</v-chip>
+          </div>
+          <div v-else>
+            <v-chip small outlined color="red accent-1">-{{ item.o.inamount }}({{ item.o.intoken }})</v-chip>
+            >
+            <v-chip small outlined color="teal lighten-1" @click="jump(item.o.pairAddress)">+{{ item.o.outamount }}({{ item.o.outtoken }})</v-chip>
+          </div>
+        </template>
+      </v-data-table>
+    </v-card>
+  </v-card>
+</template>
+
+<script>
+  import axios from 'axios'
+
+  export default {
+    name: 'History',
+
+    methods: {
+      // 时间戳转换chart时间
+      getTime (timestamp) {
+        // 组织日期格式并返回
+        return this.dateFormat('YYYY-mm-dd HH:MM:SS:sss', new Date(timestamp * 1000))
+      },
+      dateFormat(fmt, date) {
+        let ret;
+        const opt = {
+          "Y+": date.getFullYear().toString(),        // 年
+          "m+": (date.getMonth() + 1).toString(),     // 月
+          "d+": date.getDate().toString(),            // 日
+          "H+": date.getHours().toString(),           // 时
+          "M+": date.getMinutes().toString(),         // 分
+          "S+": date.getSeconds().toString(),         // 秒
+          "s+": date.getMilliseconds().toString()     // 毫秒
+          // 有其他格式化字符需求可以继续添加,必须转化成字符串
+        };
+        for (let k in opt) {
+          ret = new RegExp("(" + k + ")").exec(fmt);
+          if (ret) {
+            fmt = fmt.replace(ret[1], (ret[1].length === 1) ? (opt[k]) : (opt[k].padStart(ret[1].length, "0")))
+          }
+        }
+        return fmt;
+      },
+      // 最后四位
+      getSimpleStr (str) {
+        if (str && str.indexOf('x') !== -1) {
+          return str.substr(0, 7) + '...' + str.substr(-4)
+        } else {
+          return ''
+        }
+      },
+      async getRecord () {
+        let url = 'http://410eth.com/data/getRecordByMySelf'
+        if (process.env.NODE_ENV === 'development') {
+          url = '/api/data/getRecordByMySelf'
+        }
+        this.loading = true
+        if (this.searched) {
+          this.query.t1 = this.query.t1_str ? this.dateToTimestamp(this.query.t1_str) : ''
+          this.query.t2 = this.query.t2_str ? this.dateToTimestamp(this.query.t2_str) : ''
+        }
+        const data = await axios.get(url, { params: this.query })
+        this.tableData.length = 0
+
+        data.data.data.map(function (one) {
+          one.origin = one.o
+          one.o = JSON.parse(one.o.replaceAll("'", '"'))
+          one.blockNumber = one.o.blockNumber
+          one.gas = one.o.gas
+          one.nonce = one.o.nonce
+          one.profit = one.o.profit
+          one.symbolAddress = one.o.symbolAddress
+        })
+
+        this.tableData = data.data.data
+        this.loading = false
+      },
+      dateToTimestamp (str) {
+        let [a, b] = str.split(' ')
+        let [year, month, day] = a.split('/')
+        let [hour, minute, second] = b.split(':')
+
+        return new Date(year, month - 1, day, hour, minute, second).getTime() / 1000
+      },
+      jump (pairAddress) {
+        window.open('https://etherscan.io/address/' + pairAddress)
+      },
+      searchData () {
+        this.searched = true
+        this.getRecord()
+        clearInterval(this.intervalID)
+      }
+    },
+
+    data: () => ({
+      query: {
+        t1: '',
+        t1_str: '',
+        t2: '',
+        t2_str: '',
+        fm: '',
+        t: '',
+        state: '',
+        hs: ''
+      },
+      search: '',
+      loading: false,
+      headers: [
+        { text: 'Save Time', value: 'ts' },
+        // { text: 'Sender', value: 'fm' },
+        { text: '类型', value: 'type' },
+        { text: 'Symbol', value: 'symbolAddress' },
+        { text: '交易数量', value: 'amount' },
+        { text: '利润', value: 'profit' },
+        { text: 'Gas', value: 'gas' },
+        { text: '状态', value: 'state' },
+        { text: 'BlockNumber', value: 'blockNumber' },
+        { text: 'Nonce', value: 'nonce' },
+        { text: 'Hash', value: 'hs' },
+        { text: '  ', value: 'origin' }
+      ],
+      history: {"data":[{"t":"0x8a2a17e7adb6cbf6635380a35c04c01db473ed5ca919706f1c492d3aa1d9f1a9","fm":"0x6ff6f16a2459114fad74eed1604b402b97b02717","hs":"0x8a2a17e7adb6cbf6635380a35c04c01db473ed5ca919706f1c492d3aa1d9f1a9","state":"ok","ts":1602503469,"o":"{'type': 'NO TOKEN', 'intoken': 'WETH', 'outtoken': false, 'inamount': 0.314674952033383, 'outamount': 48953401866254456333, 'symbol': false, 'gas': 56.100000233, 'profit': 0, 'balance': 0, 'stocks': 0, 'decimals': 18, 'symbolAddress': '0x054f76beed60ab6dbeb23502178c52d6c5debe40', 'pairAddress': false, 'from': '0x6ff6f16a2459114fad74eed1604b402b97b02717', 'to': '0x8a2a17e7adb6cbf6635380a35c04c01db473ed5ca919706f1c492d3aa1d9f1a9', 'hs': '0x8a2a17e7adb6cbf6635380a35c04c01db473ed5ca919706f1c492d3aa1d9f1a9'}"}],"count":1,"state":"ok"},
+      tableData: [],
+      intervalID: 0,
+      searched: false
+    }),
+
+    mounted() {
+      this.query.t2_str = '2099/1/1 0:0:0'
+      this.query.t1_str = this.dateFormat('YYYY/mm/dd HH:MM:SS', new Date(new Date().getTime() - 10 * 60 * 1000))
+
+      const app = this
+
+      app.query.t1 = parseInt(((new Date().getTime()) / 1000 - 2 * 60) + '')
+      app.getRecord()
+
+      app.intervalID = setInterval(function () {
+        if (!app.loading) {
+          app.query.t1 = parseInt(((new Date().getTime()) / 1000) + '')
+          app.getRecord()
+        }
+      }, 10 * 1000)
+    }
+  }
+</script>
+cl

+ 238 - 0
src/components/New.vue

@@ -0,0 +1,238 @@
+<template>
+  <v-card
+      elevation="1"
+  >
+    <v-card
+        elevation="0"
+    >
+      <v-card-title>
+        New Data
+        <v-spacer></v-spacer>
+        <v-text-field
+            v-model="search"
+            append-icon="mdi-magnify"
+            label="Search"
+            single-line
+            hide-details
+        ></v-text-field>
+      </v-card-title>
+
+      <v-data-table
+        dense
+        hide-default-footer
+        multi-sort
+        :headers="headers"
+        :items="tableData"
+        :search="search"
+        :items-per-page="10000"
+        :loading="loading"
+        :sort-by="['ts', 'block', 'nonce']"
+        :sort-desc="[true, true, true]"
+      >
+        <template v-slot:item.ts="{ item }">
+          {{ getTime(item.ts) }}
+        </template>
+        <!-- Type -->
+        <template v-slot:item.type="{ item }">
+          <v-chip small v-if="item.o.type.indexOf('err') !== -1" outlined label color="grey">
+            <v-icon small left>mdi-filter-remove</v-icon>
+            {{ item.o.type.split(':')[1] }}
+          </v-chip>
+          <v-chip small v-else-if="item.o.type.indexOf('WARN') !== -1" outlined label color="red lighten-2">
+            <v-icon small left>mdi-alert-circle-outline</v-icon>
+            {{ item.o.type.split(':')[1] }}
+          </v-chip>
+          <v-chip small v-else-if="item.o.type.indexOf('DS:') !== -1" outlined label color="cyan darken-3">
+            <v-icon small left>mdi-horse-human</v-icon>
+            {{ item.o.type.split(':')[1] }}
+          </v-chip>
+          <v-chip small v-else-if="item.o.type.indexOf('CASH') !== -1" outlined label color="black">
+            <v-icon small left>mdi-currency-usd</v-icon>
+            {{ item.o.type.split(':')[1] }}
+          </v-chip>
+          <v-chip small v-else-if="item.o.type.indexOf('ZIJI') !== -1" outlined label color="blue">
+            <v-icon small left>mdi-panda</v-icon>
+            {{ item.o.type.split(':')[1] }}
+          </v-chip>
+        </template>
+        <!-- Hash -->
+        <template v-slot:item.hs="{ item }">
+          <v-tooltip right>
+            <template v-slot:activator="{ on, attrs }">
+              <v-btn small :href="'https://etherscan.io/tx/' + item.hs" target="_blank" text v-on="on" v-bind="attrs">{{ getSimpleStr(item.hs) }}</v-btn>
+            </template>
+            <span>{{ item.hs }}</span>
+          </v-tooltip>
+        </template>
+        <!-- From -->
+        <!--        <template v-slot:item.fm="{ item }">-->
+        <!--          <v-tooltip right>-->
+        <!--            <template v-slot:activator="{ on, attrs }">-->
+        <!--              <v-btn :href="'https://etherscan.io/address/' + item.fm" target="_blank" text v-on="on" v-bind="attrs">{{ getSimpleStr(item.fm) }}</v-btn>-->
+        <!--            </template>-->
+        <!--            <span>{{ item.fm }}</span>-->
+        <!--          </v-tooltip>-->
+        <!--        </template>-->
+        <!-- Address -->
+        <template v-slot:item.symbolAddress="{ item }">
+          <v-tooltip right>
+            <template v-slot:activator="{ on, attrs }">
+              <v-btn small :href="'https://etherscan.io/address/' + item.symbolAddress" target="_blank" text v-on="on" v-bind="attrs">{{ getSimpleStr(item.symbolAddress) }}</v-btn>
+            </template>
+            <span>{{ item.symbolAddress }}</span>
+          </v-tooltip>
+        </template>
+        <!-- 利润 -->
+        <template v-slot:item.profit="{ item }">
+          {{ item.o.profit }}
+        </template>
+        <!-- origin -->
+        <template v-slot:item.origin  >
+          {{ '' }}
+        </template>
+        <!-- State -->
+        <template v-slot:item.state="{ item }">
+          <v-chip small v-if="item.state === 'ok'" outlined label color="teal"> ok </v-chip>
+          <v-chip small v-else-if="item.state === 'pending'" outlined label color="orange"> {{ item.state }} </v-chip>
+          <v-chip small v-else-if="item.state === 'fail'" outlined label color="red lighten-3"> {{ item.state }} </v-chip>
+          <v-chip small v-else-if="item.state === 'cancel'" outlined label color="blue-grey"> {{ item.state }} </v-chip>
+        </template>
+        <!-- Amount -->
+        <template v-slot:item.amount="{ item }">
+          <div v-if="!item.o.intoken">
+            <v-chip small outlined color="red accent-1">-{{ item.o.inamount }}(TOKEN)</v-chip>
+            >
+            <v-chip small outlined color="teal lighten-1" @click="jump(item.o.pairAddress)">+{{ item.o.outamount }}({{ item.o.outtoken }})</v-chip>
+          </div>
+          <div v-else-if="!item.o.outtoken">
+            <v-chip small outlined color="red accent-1">-{{ item.o.inamount }}({{ item.o.intoken }})</v-chip>
+            >
+            <v-chip small outlined color="teal lighten-1" @click="jump(item.o.pairAddress)">+{{ item.o.outamount }}(TOKEN)</v-chip>
+          </div>
+          <div v-else>
+            <v-chip small outlined color="red accent-1">-{{ item.o.inamount }}({{ item.o.intoken }})</v-chip>
+            >
+            <v-chip small outlined color="teal lighten-1" @click="jump(item.o.pairAddress)">+{{ item.o.outamount }}({{ item.o.outtoken }})</v-chip>
+          </div>
+        </template>
+      </v-data-table>
+    </v-card>
+  </v-card>
+</template>
+
+<script>
+import axios from 'axios'
+
+export default {
+  name: 'History',
+
+  methods: {
+    // 时间戳转换chart时间
+    getTime (timestamp) {
+      // 组织日期格式并返回
+      return this.dateFormat('YYYY-mm-dd HH:MM:SS:sss', new Date(timestamp * 1000))
+    },
+    dateFormat(fmt, date) {
+      let ret;
+      const opt = {
+        "Y+": date.getFullYear().toString(),        // 年
+        "m+": (date.getMonth() + 1).toString(),     // 月
+        "d+": date.getDate().toString(),            // 日
+        "H+": date.getHours().toString(),           // 时
+        "M+": date.getMinutes().toString(),         // 分
+        "S+": date.getSeconds().toString(),         // 秒
+        "s+": date.getMilliseconds().toString()     // 毫秒
+        // 有其他格式化字符需求可以继续添加,必须转化成字符串
+      };
+      for (let k in opt) {
+        ret = new RegExp("(" + k + ")").exec(fmt);
+        if (ret) {
+          fmt = fmt.replace(ret[1], (ret[1].length === 1) ? (opt[k]) : (opt[k].padStart(ret[1].length, "0")))
+        }
+      }
+      return fmt;
+    },
+    // 最后四位
+    getSimpleStr (str) {
+      if (str && str.indexOf('x') !== -1) {
+        return str.substr(0, 7) + '...' + str.substr(-4)
+      } else {
+        return ''
+      }
+    },
+    async getRecord () {
+      let url = 'http://410eth.com/data/getRecord'
+      if (process.env.NODE_ENV === 'development') {
+        url = '/api/data/getRecord'
+      }
+      this.loading = true
+      const app = this
+      const data = await axios.get(url, { params: this.query })
+      this.tableData = []
+
+      data.data.data.map(function (one) {
+        if (one.state === 'cancel') {
+          return
+        }
+        one.origin = one.o
+        one.o = JSON.parse(one.o.replaceAll("'", '"'))
+        one.blockNumber = one.o.blockNumber
+        one.gas = one.o.gas
+        one.nonce = one.o.nonce
+        one.profit = one.o.profit
+        one.symbolAddress = one.o.symbolAddress
+
+        app.tableData.push(one)
+      })
+
+      this.loading = false
+    },
+    jump (pairAddress) {
+      window.open('https://etherscan.io/address/' + pairAddress)
+    }
+  },
+
+  data: () => ({
+    query: {
+      t1: '',
+      t2: '',
+      fm: '',
+      t: '',
+      state: '',
+      hs: ''
+    },
+    search: '',
+    loading: false,
+    headers: [
+      { text: 'Save Time', value: 'ts' },
+      // { text: 'Sender', value: 'fm' },
+      { text: '类型', value: 'type' },
+      { text: 'Symbol', value: 'symbolAddress' },
+      { text: '交易数量', value: 'amount' },
+      { text: '利润', value: 'profit' },
+      { text: 'Gas', value: 'gas' },
+      { text: '状态', value: 'state' },
+      { text: 'BlockNumber', value: 'blockNumber' },
+      { text: 'Nonce', value: 'nonce' },
+      { text: 'Hash', value: 'hs' },
+      { text: '  ', value: 'origin' }
+    ],
+    history: {"data":[{"t":"0x8a2a17e7adb6cbf6635380a35c04c01db473ed5ca919706f1c492d3aa1d9f1a9","fm":"0x6ff6f16a2459114fad74eed1604b402b97b02717","hs":"0x8a2a17e7adb6cbf6635380a35c04c01db473ed5ca919706f1c492d3aa1d9f1a9","state":"ok","ts":1602503469,"o":"{'type': 'NO TOKEN', 'intoken': 'WETH', 'outtoken': false, 'inamount': 0.314674952033383, 'outamount': 48953401866254456333, 'symbol': false, 'gas': 56.100000233, 'profit': 0, 'balance': 0, 'stocks': 0, 'decimals': 18, 'symbolAddress': '0x054f76beed60ab6dbeb23502178c52d6c5debe40', 'pairAddress': false, 'from': '0x6ff6f16a2459114fad74eed1604b402b97b02717', 'to': '0x8a2a17e7adb6cbf6635380a35c04c01db473ed5ca919706f1c492d3aa1d9f1a9', 'hs': '0x8a2a17e7adb6cbf6635380a35c04c01db473ed5ca919706f1c492d3aa1d9f1a9'}"}],"count":1,"state":"ok"},
+    tableData: []
+  }),
+
+  mounted() {
+    const app = this
+
+    app.query.t1 = parseInt(((new Date().getTime()) / 1000 - 2 * 60) + '')
+    app.getRecord()
+
+    setInterval(function () {
+      if (!app.loading) {
+        app.query.t1 = parseInt(((new Date().getTime()) / 1000) + '')
+        app.getRecord()
+      }
+    }, 5 * 1000)
+  }
+}
+</script>

+ 11 - 0
src/main.js

@@ -0,0 +1,11 @@
+import Vue from 'vue'
+import './plugins/axios'
+import App from './App.vue'
+import vuetify from './plugins/vuetify';
+
+Vue.config.productionTip = false
+
+new Vue({
+  vuetify,
+  render: h => h(App)
+}).$mount('#app')

+ 61 - 0
src/plugins/axios.js

@@ -0,0 +1,61 @@
+"use strict";
+
+import Vue from 'vue';
+import axios from "axios";
+
+// Full config:  https://github.com/axios/axios#request-config
+// axios.defaults.baseURL = process.env.baseURL || process.env.apiUrl || '';
+// axios.defaults.headers.common['Authorization'] = AUTH_TOKEN;
+// axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';
+
+let config = {
+  // baseURL: process.env.baseURL || process.env.apiUrl || ""
+  // timeout: 60 * 1000, // Timeout
+  // withCredentials: true, // Check cross-site Access-Control
+};
+
+const _axios = axios.create(config);
+
+_axios.interceptors.request.use(
+  function(config) {
+    // Do something before request is sent
+    return config;
+  },
+  function(error) {
+    // Do something with request error
+    return Promise.reject(error);
+  }
+);
+
+// Add a response interceptor
+_axios.interceptors.response.use(
+  function(response) {
+    // Do something with response data
+    return response;
+  },
+  function(error) {
+    // Do something with response error
+    return Promise.reject(error);
+  }
+);
+
+Plugin.install = function(Vue) {
+  Vue.axios = _axios;
+  window.axios = _axios;
+  Object.defineProperties(Vue.prototype, {
+    axios: {
+      get() {
+        return _axios;
+      }
+    },
+    $axios: {
+      get() {
+        return _axios;
+      }
+    },
+  });
+};
+
+Vue.use(Plugin)
+
+export default Plugin;

+ 7 - 0
src/plugins/vuetify.js

@@ -0,0 +1,7 @@
+import Vue from 'vue';
+import Vuetify from 'vuetify/lib';
+
+Vue.use(Vuetify);
+
+export default new Vuetify({
+});

+ 16 - 0
vue.config.js

@@ -0,0 +1,16 @@
+module.exports = {
+  "transpileDependencies": [
+    "vuetify"
+  ],
+  devServer: {
+    proxy: {
+      '/api': {
+        target:'http://localhost',
+        changeOrigin:true,
+        pathRewrite:{
+          '^/api': ''
+        }
+      }
+    },
+  }
+}

Nem az összes módosított fájl került megjelenítésre, mert túl sok fájl változott