xiongzhu před 2 roky
rodič
revize
70dff77f77

+ 7 - 5
src/components/PagingTable.vue

@@ -3,7 +3,12 @@
         <slot name="filter"></slot>
     </div>
     <ElConfigProvider :size="isMobile ? '' : 'small'">
-        <ElTable :data="tableData" :height="height || tableHeight" stripe v-loading="loading">
+        <ElTable
+            :data="tableData"
+            :height="height === 'auto' ? null : height || tableHeight"
+            stripe
+            v-loading="loading"
+        >
             <slot></slot>
         </ElTable>
     </ElConfigProvider>
@@ -37,10 +42,7 @@ const props = defineProps({
         type: Object,
         default: () => ({ createdAt: 'DESC' })
     },
-    height: {
-        type: Number,
-        default: 0
-    }
+    height: {}
 })
 const search = computed(() => {
     const where = { ...(props.where || {}) }

+ 5 - 1
src/components/WalletAddress.vue

@@ -3,7 +3,8 @@
         <ElIcon class="cursor-pointer mr-1" @click.stop="copyAddress">
             <Copy />
         </ElIcon>
-        <span>{{ truncatedAddress }}</span>
+        <ElLink class="!text-xs" v-if="url" :href="url" target="_blank">{{ truncatedAddress }}</ElLink>
+        <span v-else>{{ truncatedAddress }}</span>
     </div>
 </template>
 <script setup>
@@ -15,6 +16,9 @@ const props = defineProps({
     address: {
         type: String,
         required: true
+    },
+    url: {
+        type: String
     }
 })
 function truncateAddress(address) {

+ 6 - 0
src/styles/main.less

@@ -11,4 +11,10 @@ body,
 @font-face {
     font-family: 'sh';
     src: url(https://cdn.raex.vip/font/2023-03-24-10-09-25HtghnVXP.ttf);
+}
+.el-card__header {
+    padding: 10px !important;
+}
+.el-card__body{
+    padding: 10px !important;
 }

+ 175 - 0
src/views/AccountsView.vue

@@ -0,0 +1,175 @@
+<template>
+    <ElCard shadow="never" ref="el">
+        <template #header>
+            <div class="flex items-center">
+                <div class="flex-1 mr-4">账号</div>
+                <ElButton @click="onAddAccount">添加账号</ElButton>
+                <ElButton @click="importAccounts">批量导入</ElButton>
+            </div>
+        </template>
+        <ElTable :data="accounts" size="small" ref="table" :height="tableHeight">
+            <el-table-column type="selection" width="55" />
+            <ElTableColumn prop="name" label="名称"></ElTableColumn>
+            <ElTableColumn prop="address" label="地址" min-width="150">
+                <template #default="{ row }">
+                    <WalletAddress :address="row.address" />
+                </template>
+            </ElTableColumn>
+            <ElTableColumn prop="ethBalance" label="ETH余额" min-width="160" />
+            <ElTableColumn prop="zkBalance" label="ETH余额(ZK)" min-width="160" />
+            <ElTableColumn prop="zkUsdcBalance" label="USDC余额(ZK)" min-width="160" />
+            <ElTableColumn width="80" fixed="right">
+                <template #default="{ row }">
+                    <ElButton type="danger" size="small" @click="deleteAccount(row)">删除</ElButton>
+                </template>
+            </ElTableColumn>
+        </ElTable>
+        <ElDialog v-model="showAddDialog" title="添加账号" class="!w-10/12 max-w-md">
+            <ElForm :model="addForm" :rules="addRules" ref="addFormRef">
+                <ElFormItem label="名称" prop="name">
+                    <ElInput v-model="addForm.name"></ElInput>
+                </ElFormItem>
+                <ElFormItem label="私钥" prop="privateKey">
+                    <ElInput v-model="addForm.privateKey" type="password"></ElInput>
+                </ElFormItem>
+            </ElForm>
+            <template #footer>
+                <ElButton @click="showAddDialog = false">取消</ElButton>
+                <ElButton type="primary" @click="saveAccount" :loading="savingAccount">确定</ElButton>
+            </template>
+        </ElDialog>
+    </ElCard>
+</template>
+<script setup>
+import { inject, watch, ref, computed } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { useFileDialog, useIntervalFn, useElementSize } from '@vueuse/core'
+import { http } from '@/plugins/http'
+import { ethers, Wallet } from 'ethers'
+
+const el = ref(null)
+const { width, height } = useElementSize(el)
+const tableHeight = computed(() => {
+    return height.value - 70
+})
+const network = inject('network')
+const accounts = ref([])
+const table = ref(null)
+const addForm = ref({})
+const addFormRef = ref(null)
+const addRules = {
+    name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
+    privateKey: [{ required: true, message: '请输入私钥', trigger: 'blur' }]
+}
+const showAddDialog = ref(false)
+const savingAccount = ref(false)
+
+watch(network, () => {
+    refresh()
+})
+
+function onAddAccount() {
+    if (addFormRef.value) {
+        addFormRef.value.clearValidate()
+    }
+    showAddDialog.value = true
+}
+
+async function importAccounts() {
+    await ElMessageBox.confirm(
+        '请上传TXT文件,每行一个账号,账号名称和私钥用逗号隔开,如:<br/><p class="italic font-bold text-sm">账号1,0x123456...<br/>账号2,0x123456...<br/>账号3,0x123456...<br/>账号4,0x123456...</p>',
+        '导入账号',
+        {
+            confirmButtonText: '确定',
+            showCancelButton: false,
+            type: 'warning',
+            dangerouslyUseHTMLString: true
+        }
+    )
+    const { files, open, reset, onChange } = useFileDialog({
+        accept: '.txt'
+    })
+    onChange((files) => {
+        /** do something with files */
+        const reader = new FileReader()
+        reader.addEventListener('load', (event) => {
+            http.put(
+                '/accounts',
+                event.target.result
+                    .split(/\r?\n/)
+                    .filter((i) => {
+                        return !!i && i.split(/[,,]/).length === 2
+                    })
+                    .map((e) => {
+                        let [name, privateKey] = e.split(/[,,]/)
+                        const provider = new ethers.providers.JsonRpcProvider('https://zksync2-testnet.zksync.dev/')
+                        const wallet = new Wallet(privateKey, provider)
+                        return {
+                            name,
+                            address: wallet.address,
+                            privateKey,
+                            network: network.value
+                        }
+                    })
+            )
+                .then(() => {
+                    ElMessage.success('导入成功')
+                    refresh()
+                })
+                .catch((e) => {
+                    ElMessage.error(e.message)
+                })
+        })
+        reader.readAsText(files[0])
+    })
+    open()
+}
+
+async function saveAccount() {
+    await addFormRef.value.validate()
+    savingAccount.value = true
+    try {
+        const provider = new ethers.providers.JsonRpcProvider('https://zksync2-testnet.zksync.dev/')
+        const wallet = new Wallet(addForm.value.privateKey, provider)
+        addForm.value.address = wallet.address
+        await http.put('/accounts', {
+            ...addForm.value,
+            network: network.value
+        })
+        savingAccount.value = false
+        showAddDialog.value = false
+        ElMessage.success('添加成功')
+        refresh()
+    } catch (e) {
+        console.log(e)
+        savingAccount.value = false
+        ElMessage.error(e.message)
+    }
+}
+
+async function deleteAccount(account) {
+    try {
+        await ElMessageBox.confirm(`确认删除?`, '确认', {
+            type: 'warning'
+        })
+        await http.delete(`/accounts/${account.id}`)
+        ElMessage.success('删除成功')
+        refresh()
+    } catch (error) {
+        if ('cancel' !== error) ElMessage.error(error.message)
+    }
+}
+
+async function refresh() {
+    const res = await http.get('/accounts/my', { network: network.value })
+    accounts.value = res
+}
+function selected() {
+    return table.value?.getSelectionRows() || []
+}
+refresh()
+defineExpose({
+    refresh,
+    selected
+})
+</script>

+ 81 - 389
src/views/HomeView.vue

@@ -1,121 +1,43 @@
 <template>
     <ElConfigProvider size="small">
-        <ElRow :gutter="20">
-            <ElCol :span="12">
-                <ElCard shadow="never">
-                    <div class="text-sm flex flex-col items-center">
-                        <div class="flex items-center">
-                            Mainnet
-                            <ElIcon class="ml-2 mr-2">
-                                <ArrowNarrowRight />
-                            </ElIcon>
-                            zkSync Era
-                            <ElInput class="ml-2 !w-20" v-model="depositAmount" />
-                            <ElButton type="primary" class="ml-2" @click="deposit(0)" :loading="depositing">
-                                官方
-                            </ElButton>
-                            <ElButton type="primary" class="ml-2" @click="deposit(1)" :loading="depositing">
-                                Orbiter
-                            </ElButton>
-                        </div>
-
-                        <div class="flex items-center mt-4">
-                            zkSync Era
-                            <ElIcon class="ml-2 mr-2">
-                                <ArrowNarrowRight />
-                            </ElIcon>
-                            Mainnet
-                            <ElInput class="ml-2 !w-20" v-model="widthdrawAmount" />
-                            <ElButton type="primary" class="ml-2" @click="widthdraw(0)" :loading="widthdrawing"
-                                >官方</ElButton
-                            >
-                            <ElButton type="primary" class="ml-2" @click="widthdraw(1)" :loading="widthdrawing">
-                                Orbiter
-                            </ElButton>
-                        </div>
-                        <div class="flex items-center mt-4">
-                            ETH
-                            <ElIcon class="ml-2 mr-2">
-                                <ArrowsRightLeft />
-                            </ElIcon>
-                            USDC
-                            <ElInput class="ml-2 !w-20" v-model="liquidityAmount" />
-                            <ElButton class="ml-2" type="primary" @click="addLiquidity" :loading="addingLiquidity">
-                                增加流动性
-                            </ElButton>
-                            <ElButton class="ml-2" type="primary" @click="removeLiquidity" :loading="addingLiquidity">
-                                移除流动性
-                            </ElButton>
-                        </div>
-                        <div class="flex items-center mt-4">
-                            ETH
-                            <ElIcon class="ml-2 mr-2">
-                                <ArrowNarrowRight />
-                            </ElIcon>
-                            <ElInput class="!w-20 mr-2" v-model="exactOutAmount" />
-                            USDC
-                            <ElButton class="ml-2" type="primary" @click="swapExactOut" :loading="swaping">
-                                SWAP
-                            </ElButton>
-                        </div>
-                        <div class="flex items-center mt-4">
-                            <ElInput class="!w-20 mr-2" v-model="exactInAmount" />
-                            USDC
-                            <ElIcon class="ml-2 mr-2">
-                                <ArrowNarrowRight />
-                            </ElIcon>
-                            ETH
-                            <ElButton class="ml-2" type="primary" @click="swapExactIn" :loading="swaping">
-                                SWAP
-                            </ElButton>
-                        </div>
-                        <div class="flex items-center mt-4">
-                            <ElButton type="primary" @click="mint" :loading="minting">mint</ElButton>
-                        </div>
-                    </div>
-                </ElCard>
-
-                <div class="mb-3 mt-3">
-                    <ElButton @click="onAddAccount">添加账号</ElButton>
-                    <ElButton @click="importAccounts">批量导入</ElButton>
-                </div>
-                <ElCard shadow="never">
-                    <ElTable :data="accounts" size="small" ref="table" @row-click="rowClick">
-                        <el-table-column type="selection" width="55" />
-                        <ElTableColumn prop="name" label="名称"></ElTableColumn>
-                        <ElTableColumn prop="address" label="地址" min-width="150">
-                            <template #default="{ row }">
-                                <WalletAddress :address="row.address" />
-                            </template>
-                        </ElTableColumn>
-                        <ElTableColumn prop="ethBalance" label="ETH余额" min-width="160" />
-                        <ElTableColumn prop="zkBalance" label="ETH余额(ZK)" min-width="160" />
-                        <ElTableColumn prop="zkUsdcBalance" label="USDC余额(ZK)" min-width="160" />
-                        <ElTableColumn width="80" fixed="right">
-                            <template #default="{ row }">
-                                <ElButton type="danger" size="small" @click="deleteAccount(row)">删除</ElButton>
-                            </template>
-                        </ElTableColumn>
-                    </ElTable>
-                </ElCard>
+        <ElRow :gutter="20" class="h-full">
+            <ElCol :span="12" class="h-full">
+                <AccountsView ref="accountsView" class="h-full" />
             </ElCol>
-            <ElCol :span="12">
-                <TaskView />
+            <ElCol :span="12" class="!flex flex-col h-full">
+                <TaskView class=" flex-1" @createTask="onCreateTask" />
+                <LogsView class="flex-1 mt-4" />
             </ElCol>
         </ElRow>
     </ElConfigProvider>
-    <ElDialog v-model="showAddDialog" title="添加账号" class="!w-10/12 max-w-md">
-        <ElForm :model="addForm" :rules="addRules" ref="addFormRef">
-            <ElFormItem label="名称" prop="name">
-                <ElInput v-model="addForm.name"></ElInput>
+    <ElDialog v-model="showCreateTaskDialog" title="新建任务" width="500px">
+        <ElForm
+            :model="createTaskForm"
+            :rules="createTaskRule"
+            ref="createTaskFormRef"
+            label-position="right"
+            label-width="100px"
+        >
+            <ElFormItem prop="type" label="任务类型">
+                <ElSelect v-model="createTaskForm.type">
+                    <ElOption v-for="key in TaskType" :key="key" :label="TaskType[key]" :value="key"></ElOption>
+                </ElSelect>
             </ElFormItem>
-            <ElFormItem label="私钥" prop="privateKey">
-                <ElInput v-model="addForm.privateKey" type="password"></ElInput>
+            <ElFormItem prop="amount" label="金额">
+                <ElInput v-model="createTaskForm.minAmount" class="!w-36">
+                    <template #prepend>MIN</template>
+                </ElInput>
+                <ElInput v-model="createTaskForm.maxAmount" class="ml-6 !w-36">
+                    <template #prepend>MAX</template>
+                </ElInput>
+            </ElFormItem>
+            <ElFormItem prop="startTime" label="执行时间">
+                <ElDatePicker type="datetime" v-model="createTaskForm.startTime"></ElDatePicker>
             </ElFormItem>
         </ElForm>
         <template #footer>
-            <ElButton @click="showAddDialog = false">取消</ElButton>
-            <ElButton type="primary" @click="saveAccount" :loading="savingAccount">确定</ElButton>
+            <ElButton @click="showCreateTaskDialog = false">取消</ElButton>
+            <ElButton type="primary" @click="createTask" :loading="creatingTask">确定</ElButton>
         </template>
     </ElDialog>
 </template>
@@ -124,300 +46,70 @@
 import { ref, onMounted, inject, watch } from 'vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
 import { http } from '@/plugins/http'
-import { ethers, Wallet } from 'ethers'
 import { ArrowNarrowRight, ArrowsRightLeft } from '@vicons/tabler'
 import { useFileDialog, useIntervalFn } from '@vueuse/core'
 import WalletAddress from '@/components/WalletAddress.vue'
 import { TaskType } from '@/enums'
+import AccountsView from './AccountsView.vue'
 import TaskView from '@/views/TaskView.vue'
-const network = inject('network')
-watch(network, () => {
-    getAccounts()
-})
-const accounts = ref([])
-const table = ref(null)
-const addForm = ref({})
-const addFormRef = ref(null)
-const addRules = {
-    name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
-    privateKey: [{ required: true, message: '请输入私钥', trigger: 'blur' }]
-}
-const showAddDialog = ref(false)
-const savingAccount = ref(false)
-function rowClick(row, column, event) {
-    table.value.clearSelection()
-    table.value.toggleRowSelection(row, true)
-}
-function onAddAccount() {
-    if (addFormRef.value) {
-        addFormRef.value.clearValidate()
-    }
-    showAddDialog.value = true
-}
-
-async function importAccounts() {
-    await ElMessageBox.confirm(
-        '请上传TXT文件,每行一个账号,账号名称和私钥用逗号隔开,如:<br/><p class="italic font-bold text-sm">账号1,0x123456...<br/>账号2,0x123456...<br/>账号3,0x123456...<br/>账号4,0x123456...</p>',
-        '导入账号',
-        {
-            confirmButtonText: '确定',
-            showCancelButton: false,
-            type: 'warning',
-            dangerouslyUseHTMLString: true
-        }
-    )
-    const { files, open, reset, onChange } = useFileDialog({
-        accept: '.txt'
-    })
-    onChange((files) => {
-        /** do something with files */
-        const reader = new FileReader()
-        reader.addEventListener('load', (event) => {
-            http.put(
-                '/accounts',
-                event.target.result
-                    .split(/\r?\n/)
-                    .filter((i) => {
-                        return !!i && i.split(/[,,]/).length === 2
-                    })
-                    .map((e) => {
-                        let [name, privateKey] = e.split(/[,,]/)
-                        const provider = new ethers.providers.JsonRpcProvider('https://zksync2-testnet.zksync.dev/')
-                        const wallet = new Wallet(privateKey, provider)
-                        return {
-                            name,
-                            address: wallet.address,
-                            privateKey,
-                            network: network.value
-                        }
-                    })
-            )
-                .then(() => {
-                    ElMessage.success('导入成功')
-                    getAccounts()
-                })
-                .catch((e) => {
-                    ElMessage.error(e.message)
-                })
-        })
-        reader.readAsText(files[0])
-    })
-    open()
-}
+import LogsView from './LogsView.vue'
 
-async function saveAccount() {
-    await addFormRef.value.validate()
-    savingAccount.value = true
-    try {
-        const provider = new ethers.providers.JsonRpcProvider('https://zksync2-testnet.zksync.dev/')
-        const wallet = new Wallet(addForm.value.privateKey, provider)
-        addForm.value.address = wallet.address
-        await http.put('/accounts', {
-            ...addForm.value,
-            network: network.value
-        })
-        savingAccount.value = false
-        showAddDialog.value = false
-        ElMessage.success('添加成功')
-        getAccounts()
-    } catch (e) {
-        console.log(e)
-        savingAccount.value = false
-        ElMessage.error(e.message)
-    }
-}
-
-async function getAccounts() {
-    const res = await http.get('/accounts/my', { network: network.value })
-    accounts.value = res
+const accountsView = ref(null)
+function selected() {
+    return accountsView.value && accountsView.value.selected()
 }
-useIntervalFn(getAccounts, 10000)
-getAccounts()
 
-async function deleteAccount(account) {
-    try {
-        await ElMessageBox.confirm(`确认删除?`, '确认', {
-            type: 'warning'
-        })
-        await http.delete(`/accounts/${account.id}`)
-        ElMessage.success('删除成功')
-        getAccounts()
-    } catch (error) {
-        if ('cancel' !== error) ElMessage.error(error.message)
-    }
-}
-const depositAmount = ref('')
-const widthdrawAmount = ref('')
-const depositing = ref(false)
-const widthdrawing = ref(false)
-async function deposit(type) {
-    if (table.value.getSelectionRows().length === 0) {
-        ElMessage.error('请选择账号')
-        return
-    }
-    if (!depositAmount.value) {
-        ElMessage.error('请输入金额')
-        return
-    }
-    depositing.value = true
-    try {
-        console.log(table.value.getSelectionRows())
-        await http.put('/tasks', {
-            accounts: table.value.getSelectionRows().map((i) => i.id),
-            params: {
-                amount: depositAmount.value
+const showCreateTaskDialog = ref(false)
+const createTaskFormRef = ref(null)
+const createTaskForm = ref({})
+const createTaskRule = {
+    type: [{ required: true, message: '请选择类型', trigger: 'change' }],
+    amount: [
+        {
+            validator: (rule, value, callback) => {
+                if (createTaskForm.value.type !== 'min') {
+                    if (!(createTaskForm.value.minAmount && createTaskForm.value.maxAmount)) {
+                        callback(new Error('请输入金额'))
+                    } else {
+                        callback()
+                    }
+                } else {
+                    callback()
+                }
             },
-            type: type === 0 ? TaskType.bridge2zk : TaskType.orbiter2zk
-        })
-        ElMessage.success('任务添加成功')
-        depositing.value = false
-    } catch (error) {
-        depositing.value = false
-        ElMessage.error(error.message)
-    }
-}
-async function widthdraw(type) {
-    if (table.value.getSelectionRows().length === 0) {
-        ElMessage.error('请选择账号')
-        return
-    }
-    if (!widthdrawAmount.value) {
-        ElMessage.error('请输入金额')
-        return
-    }
-    widthdrawing.value = true
-    try {
-        console.log(table.value.getSelectionRows())
-        await http.post(type === 0 ? '/web3/zk-withdraw' : '/web3/orbiter-withdraw', {
-            amount: widthdrawAmount.value,
-            accountId: table.value.getSelectionRows()[0].id,
-            network: network.value
-        })
-        ElMessage.success('成功')
-        widthdrawing.value = false
-    } catch (error) {
-        widthdrawing.value = false
-        ElMessage.error(error.message)
-    }
-}
-const addingLiquidity = ref(false)
-const liquidityAmount = ref('')
-async function addLiquidity() {
-    if (table.value.getSelectionRows().length === 0) {
-        ElMessage.error('请选择账号')
-        return
-    }
-    if (!liquidityAmount.value) {
-        ElMessage.error('请输入金额')
-        return
-    }
-    addingLiquidity.value = true
-    try {
-        console.log(table.value.getSelectionRows())
-        await http.post('/web3/add-liquidity', {
-            accountId: table.value.getSelectionRows()[0].id,
-            amount: liquidityAmount.value,
-            network: network.value
-        })
-        ElMessage.success('成功')
-        addingLiquidity.value = false
-    } catch (error) {
-        addingLiquidity.value = false
-        ElMessage.error(error.message)
-    }
-}
-
-async function removeLiquidity() {
-    if (table.value.getSelectionRows().length === 0) {
-        ElMessage.error('请选择账号')
-        return
-    }
-    addingLiquidity.value = true
-    try {
-        console.log(table.value.getSelectionRows())
-        await http.post('/web3/remove-liquidity', {
-            accountId: table.value.getSelectionRows()[0].id,
-            network: network.value
-        })
-        ElMessage.success('成功')
-        addingLiquidity.value = false
-    } catch (error) {
-        addingLiquidity.value = false
-        ElMessage.error(error.message)
-    }
+            trigger: 'change'
+        }
+    ]
 }
-
-const exactOutAmount = ref('')
-const swaping = ref(false)
-function swapExactOut() {
-    if (table.value.getSelectionRows().length === 0) {
-        ElMessage.error('请选择账号')
+const creatingTask = ref(false)
+function onCreateTask() {
+    if (selected().length === 0) {
+        ElMessage.warning('请先选择账号')
         return
     }
-    if (!exactOutAmount.value) {
-        ElMessage.error('请输入金额')
-        return
-    }
-    swaping.value = true
-    http.post('/web3/swap-exact-out', {
-        accountId: table.value.getSelectionRows()[0].id,
-        amount: exactOutAmount.value,
-        network: network.value
-    })
-        .then(() => {
-            ElMessage.success('成功')
-            swaping.value = false
-        })
-        .catch((e) => {
-            swaping.value = false
-            ElMessage.error(e.message)
-        })
+    showCreateTaskDialog.value = true
 }
-
-const exactInAmount = ref('')
-function swapExactIn() {
-    if (table.value.getSelectionRows().length === 0) {
-        ElMessage.error('请选择账号')
-        return
-    }
-    if (!exactInAmount.value) {
-        ElMessage.error('请输入金额')
-        return
-    }
-    swaping.value = true
-    http.post('/web3/swap-exact-in', {
-        accountId: table.value.getSelectionRows()[0].id,
-        amount: exactInAmount.value,
-        network: network.value
+function createTask() {
+    createTaskFormRef.value.validate(async (valid) => {
+        if (valid) {
+            creatingTask.value = true
+            try {
+                await http.put('/tasks', {
+                    accounts: selected().map((i) => i.id),
+                    params: {
+                        mintAmount: createTaskForm.value.mintAmount,
+                        maxAmount: createTaskForm.value.maxAmount
+                    },
+                    type: createTaskForm.value.type
+                })
+                ElMessage.success('任务添加成功')
+                creatingTask.value = false
+                showCreateTaskDialog.value = false
+            } catch (error) {
+                creatingTask.value = false
+                ElMessage.error(error.message)
+            }
+        }
     })
-        .then(() => {
-            ElMessage.success('成功')
-            swaping.value = false
-        })
-        .catch((e) => {
-            swaping.value = false
-            ElMessage.error(e.message)
-        })
-}
-
-const minting = ref(false)
-async function mint() {
-    if (table.value.getSelectionRows().length === 0) {
-        ElMessage.error('请选择账号')
-        return
-    }
-    minting.value = true
-    try {
-        console.log(table.value.getSelectionRows())
-        await http.post('/web3/mint', {
-            accountId: table.value.getSelectionRows()[0].id,
-            network: network.value
-        })
-        ElMessage.success('成功')
-        minting.value = false
-    } catch (error) {
-        minting.value = false
-        ElMessage.error(error.message)
-    }
 }
 </script>

+ 81 - 0
src/views/LogsView.vue

@@ -0,0 +1,81 @@
+<template>
+    <ElCard shadow="never" ref="el">
+        <template #header>
+            <div class="flex items-center">
+                <span class="flex-1">日志</span>
+                <ElIcon @click="refresh" class="cursor-pointer" :class="{ loading: loading }">
+                    <RotateClockwise />
+                </ElIcon>
+            </div>
+        </template>
+        <PagingTable url="/tasks/logs" ref="pt" :height="tableHeight">
+            <ElTableColumn prop="id" label="ID" width="50" />
+            <ElTableColumn prop="createdAt" label="创建时间" :formatter="timeFormatter" width="150" />
+            <ElTableColumn prop="taskId" label="任务ID" min-width="80"></ElTableColumn>
+            <ElTableColumn prop="type" label="任务类型" min-width="120" :formatter="taskTypeFormatter" />
+            <ElTableColumn prop="address" label="账号" width="130">
+                <template #default="{ row }">
+                    <WalletAddress :address="row.address" />
+                </template>
+            </ElTableColumn>
+            <ElTableColumn prop="status" label="状态" align="center" width="80">
+                <template #default="{ row }">
+                    <ElTag :type="row.status === 'success' ? 'success' : 'danger'">
+                        {{ row.status === 'success' ? '成功' : '失败' }}
+                    </ElTag>
+                </template>
+            </ElTableColumn>
+            <ElTableColumn prop="txHash" label="txHash" width="130">
+                <template #default="{ row }">
+                    <WalletAddress v-if="row.txHash" :address="row.txHash" :url="row.url" />
+                </template>
+            </ElTableColumn>
+        </PagingTable>
+    </ElCard>
+</template>
+<script setup>
+import { ref, inject, computed } from 'vue'
+import PagingTable from '@/components/PagingTable.vue'
+import { useTimeFormatter, useEnumFormatter } from '@/utils/formatter'
+import { TaskType } from '@/enums'
+import { useIntervalFn, useElementSize } from '@vueuse/core'
+import WalletAddress from '@/components/WalletAddress.vue'
+import { RotateClockwise } from '@vicons/tabler'
+
+const timeFormatter = useTimeFormatter()
+const taskTypeFormatter = useEnumFormatter(TaskType)
+const pt = ref(null)
+// useIntervalFn(() => {
+//     pt.value && pt.value.refresh()
+// }, 2000)
+function progressFormatter(row, column, value, index) {
+    return `${value + 1}/${row.accounts.length}`
+}
+const loading = ref(false)
+function refresh() {
+    pt.value && pt.value.refresh()
+    loading.value = true
+    setTimeout(() => {
+        loading.value = false
+    }, 1000)
+}
+
+const el = ref(null)
+const { width, height } = useElementSize(el)
+const tableHeight = computed(() => {
+    return height.value - 105
+})
+</script>
+<style lang="less" scoped>
+.loading {
+    animation: loading 1s linear infinite;
+}
+@keyframes loading {
+    0% {
+        transform: rotate(0deg);
+    }
+    100% {
+        transform: rotate(360deg);
+    }
+}
+</style>

+ 43 - 4
src/views/TaskView.vue

@@ -1,6 +1,16 @@
 <template>
-    <ElCard shadow="never" header="任务">
-        <PagingTable url="/tasks" :height="300" ref="pt">
+    <ElCard shadow="never" ref="el">
+        <template #header>
+            <div class="flex items-center">
+                <span>任务</span>
+                <ElButton class="ml-4" type="primary" @click="emit('createTask')">新建任务</ElButton>
+                <div class="flex-1"></div>
+                <ElIcon @click="refresh" class="cursor-pointer" :class="{ loading: loading }">
+                    <RotateClockwise />
+                </ElIcon>
+            </div>
+        </template>
+        <PagingTable url="/tasks" ref="pt" :height="tableHeight">
             <ElTableColumn prop="id" label="ID" width="50" />
             <ElTableColumn prop="createdAt" label="创建时间" :formatter="timeFormatter" width="150" />
             <ElTableColumn prop="startTime" label="执行时间" :formatter="timeFormatter" width="150" />
@@ -11,11 +21,14 @@
     </ElCard>
 </template>
 <script setup>
-import { ref } from 'vue'
+import { ref, inject, computed } from 'vue'
 import PagingTable from '@/components/PagingTable.vue'
 import { useTimeFormatter, useEnumFormatter } from '@/utils/formatter'
 import { TaskType } from '@/enums'
-import { useIntervalFn } from '@vueuse/core'
+import { useIntervalFn, useElementSize } from '@vueuse/core'
+import { RotateClockwise } from '@vicons/tabler'
+
+const emit = defineEmits(['createTask'])
 const timeFormatter = useTimeFormatter()
 const taskTypeFormatter = useEnumFormatter(TaskType)
 const pt = ref(null)
@@ -25,4 +38,30 @@ useIntervalFn(() => {
 function progressFormatter(row, column, value, index) {
     return `${value + 1}/${row.accounts.length}`
 }
+const loading = ref(false)
+function refresh() {
+    pt.value && pt.value.refresh()
+    loading.value = true
+    setTimeout(() => {
+        loading.value = false
+    }, 1000)
+}
+const el = ref(null)
+const { width, height } = useElementSize(el)
+const tableHeight = computed(() => {
+    return height.value - 105
+})
 </script>
+<style lang="less" scoped>
+.loading {
+    animation: loading 1s linear infinite;
+}
+@keyframes loading {
+    0% {
+        transform: rotate(0deg);
+    }
+    100% {
+        transform: rotate(360deg);
+    }
+}
+</style>