feat: add admin query emails (#39)

* feat: add NPagination

* feat: admin query emails
This commit is contained in:
Dream Hunter
2023-12-12 16:33:54 +08:00
committed by GitHub
parent 795f256bde
commit bf6374af52
5 changed files with 290 additions and 32 deletions
+1 -10
View File
@@ -14,6 +14,7 @@ const apiFetch = async (path, options = {}) => {
try { try {
const response = await instance.request(path, { const response = await instance.request(path, {
method: options.method || 'GET', method: options.method || 'GET',
data: options.body || null,
headers: { headers: {
'x-custom-auth': auth.value, 'x-custom-auth': auth.value,
'x-admin-auth': adminAuth.value, 'x-admin-auth': adminAuth.value,
@@ -88,20 +89,10 @@ const adminDeleteAddress = async (id) => {
} }
} }
const adminGetAddress = async () => {
try {
return await apiFetch("/admin/addresss");
} catch (error) {
throw error;
}
}
export const api = { export const api = {
fetch: apiFetch, fetch: apiFetch,
getSettings: getSettings, getSettings: getSettings,
getOpenSettings: getOpenSettings, getOpenSettings: getOpenSettings,
adminShowPassword: adminShowPassword, adminShowPassword: adminShowPassword,
adminDeleteAddress: adminDeleteAddress, adminDeleteAddress: adminDeleteAddress,
adminGetAddress: adminGetAddress,
} }
+162 -9
View File
@@ -1,8 +1,9 @@
<script setup> <script setup>
import { NSpace, NLayoutHeader, NInput, c } from 'naive-ui' import { NSpace, NLayoutHeader, NInput, NPagination } from 'naive-ui'
import { NButton, NSelect, NModal } from 'naive-ui' import { NButton, NModal, NTabs, NTabPane, NInputGroup } from 'naive-ui'
import { NList, NListItem, NThing, NTag } from 'naive-ui'
import { NDataTable, NPopconfirm } from 'naive-ui' import { NDataTable, NPopconfirm } from 'naive-ui'
import { ref, h, onMounted } from 'vue'; import { ref, h, onMounted, watch } from 'vue';
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useMessage } from 'naive-ui' import { useMessage } from 'naive-ui'
@@ -41,6 +42,7 @@ const { t } = useI18n({
delete: 'Delete', delete: 'Delete',
deleteTip: 'Are you sure to delete this email?', deleteTip: 'Are you sure to delete this email?',
refresh: 'Refresh', refresh: 'Refresh',
emails: 'Emails',
}, },
zh: { zh: {
title: '临时邮件 Admin', title: '临时邮件 Admin',
@@ -55,10 +57,14 @@ const { t } = useI18n({
delete: '删除', delete: '删除',
deleteTip: '确定要删除这个邮箱吗?', deleteTip: '确定要删除这个邮箱吗?',
refresh: '刷新', refresh: '刷新',
emails: '邮件',
} }
} }
}); });
const data = ref([]) const data = ref([])
const count = ref(0)
const page = ref(1)
const pageSize = ref(20)
const showPassword = async (id) => { const showPassword = async (id) => {
try { try {
@@ -83,7 +89,15 @@ const deleteEmail = async (id) => {
const fetchData = async () => { const fetchData = async () => {
try { try {
data.value = await api.adminGetAddress() const { results, count: addressCount } = await api.fetch(
`/admin/address`
+ `?limit=${pageSize.value}`
+ `&offset=${(page.value - 1) * pageSize.value}`
);
data.value = results;
if (addressCount > 0) {
count.value = addressCount;
}
} catch (error) { } catch (error) {
console.log(error) console.log(error)
message.error(error.message || "error"); message.error(error.message || "error");
@@ -115,6 +129,16 @@ const columns = [
}, },
{ default: () => t('showPass') } { default: () => t('showPass') }
), ),
h(NButton,
{
type: 'success',
onClick: () => {
mailAddress.value = row.name
tab.value = "mails"
}
},
{ default: () => t('emails') }
),
h(NPopconfirm, h(NPopconfirm,
{ {
onPositiveClick: () => deleteEmail(row.id) onPositiveClick: () => deleteEmail(row.id)
@@ -129,6 +153,10 @@ const columns = [
} }
] ]
watch([page, pageSize], async () => {
await fetchData()
})
onMounted(async () => { onMounted(async () => {
if (!adminAuth.value) { if (!adminAuth.value) {
@@ -137,11 +165,69 @@ onMounted(async () => {
await fetchData() await fetchData()
} }
}) })
const tab = ref("account")
const mailAddress = ref("")
const mailData = ref([])
const mailCount = ref(0)
const mailPage = ref(1)
const mailPageSize = ref(20)
watch([mailPage, mailPageSize, mailAddress], async () => {
await fetchMailData()
})
const fetchMailData = async () => {
if (!mailAddress.value) {
return
}
try {
const { results, count } = await api.fetch(
`/admin/mails`
+ `?address=${mailAddress.value}`
+ `&limit=${mailPageSize.value}`
+ `&offset=${(mailPage.value - 1) * mailPageSize.value}`
);
mailData.value = results;
if (count > 0) {
mailCount.value = count;
}
} catch (error) {
console.log(error)
message.error(error.message || "error");
}
}
const mailUnknowData = ref([])
const mailUnknowCount = ref(0)
const mailUnknowPage = ref(1)
const mailUnknowPageSize = ref(20)
watch([mailUnknowPage, mailUnknowPageSize], async () => {
await fetchMailUnknowData()
})
const fetchMailUnknowData = async () => {
try {
const { results, count } = await api.fetch(
`/admin/mails_unknow`
+ `?limit=${mailPageSize.value}`
+ `&offset=${(mailPage.value - 1) * mailPageSize.value}`
);
mailUnknowData.value = results;
if (count > 0) {
mailUnknowCount.value = count;
}
} catch (error) {
console.log(error)
message.error(error.message || "error");
}
}
</script> </script>
<template> <template>
<n-space vertical> <n-space vertical>
<n-layout-header> <n-layout-header>
<div> <div>
<h2>{{ t('title') }}</h2> <h2>{{ t('title') }}</h2>
@@ -150,9 +236,6 @@ onMounted(async () => {
<n-button tertiary @click="() => router.push('/')" type="primary"> <n-button tertiary @click="() => router.push('/')" type="primary">
{{ t('home') }} {{ t('home') }}
</n-button> </n-button>
<n-button tertiary @click="fetchData" type="primary">
{{ t('refresh') }}
</n-button>
</div> </div>
<n-modal v-model:show="showAdminAuth" :closable="false" :closeOnEsc="false" :maskClosable="false" preset="dialog" <n-modal v-model:show="showAdminAuth" :closable="false" :closeOnEsc="false" :maskClosable="false" preset="dialog"
title="Dialog"> title="Dialog">
@@ -183,7 +266,72 @@ onMounted(async () => {
</template> </template>
</n-modal> </n-modal>
</n-layout-header> </n-layout-header>
<n-data-table :columns="columns" :data="data" :bordered="false" /> <n-tabs type="segment" v-model:value="tab">
<n-tab-pane name="account" tab="account">
<div style="display: inline-block;">
<n-pagination v-model:page="page" v-model:page-size="pageSize" :item-count="count" :page-sizes="[20, 50, 100]"
show-size-picker />
</div>
<n-button tertiary @click="fetchData" type="primary">
{{ t('refresh') }}
</n-button>
<n-data-table :columns="columns" :data="data" :bordered="false" />
</n-tab-pane>
<n-tab-pane name="mails" tab="mails">
<n-input-group>
<n-input v-model:value="mailAddress" />
<n-button @click="fetchMailData" type="primary" ghost>
{{ t('refresh') }}
</n-button>
</n-input-group>
<n-list hoverable clickable>
<div style="display: inline-block; margin-bottom: 10px;">
<n-pagination v-model:page="mailPage" v-model:page-size="mailPageSize" :item-count="mailCount" simple />
</div>
<n-list-item v-for="row in mailData" v-bind:key="row.id">
<n-thing class="center" :title="row.subject">
<template #description>
<n-space>
<n-tag type="info">
FROM: {{ row.source }}
</n-tag>
<n-tag type="info">
ID: {{ row.id }}
</n-tag>
</n-space>
</template>
<div v-html="row.message"></div>
</n-thing>
</n-list-item>
</n-list>
</n-tab-pane>
<n-tab-pane name="unknow" tab="unknown">
<n-button @click="fetchMailUnknowData" type="primary" ghost>
{{ t('refresh') }}
</n-button>
<n-list hoverable clickable>
<div style="display: inline-block; margin-bottom: 10px;">
<n-pagination v-model:page="mailUnknowPage" v-model:page-size="mailUnknowPageSize"
:item-count="mailUnknowCount" simple />
</div>
<n-list-item v-for="row in mailUnknowData" v-bind:key="row.id">
<n-thing class="center" :title="row.subject">
<template #description>
<n-space>
<n-tag type="info">
FROM: {{ row.source }}
</n-tag>
<n-tag type="info">
ID: {{ row.id }}
</n-tag>
</n-space>
</template>
<div v-html="row.message"></div>
</n-thing>
</n-list-item>
</n-list>
</n-tab-pane>
</n-tabs>
</n-space> </n-space>
</template> </template>
@@ -193,4 +341,9 @@ onMounted(async () => {
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
} }
.n-pagination {
margin-top: 10px;
margin-bottom: 10px;
}
</style> </style>
+23 -2
View File
@@ -1,6 +1,6 @@
<script setup> <script setup>
import { NSpace, NAlert, NSwitch, NCard, NInput, NInputGroupLabel } from 'naive-ui' import { NSpace, NAlert, NSwitch, NCard, NInput, NInputGroupLabel } from 'naive-ui'
import { NButton, NLayout, NInputGroup, NModal, NSelect } from 'naive-ui' import { NButton, NLayout, NInputGroup, NModal, NSelect, NPagination } from 'naive-ui'
import { NList, NListItem, NThing, NTag } from 'naive-ui' import { NList, NListItem, NThing, NTag } from 'naive-ui'
import { watch, onMounted, ref } from "vue"; import { watch, onMounted, ref } from "vue";
import useClipboard from 'vue-clipboard3' import useClipboard from 'vue-clipboard3'
@@ -22,6 +22,10 @@ const showNewEmail = ref(false)
const emailName = ref("") const emailName = ref("")
const emailDomain = ref("") const emailDomain = ref("")
const count = ref(0)
const page = ref(1)
const pageSize = ref(20)
const { t } = useI18n({ const { t } = useI18n({
locale: 'zh', locale: 'zh',
messages: { messages: {
@@ -73,12 +77,26 @@ watch(autoRefresh, async (autoRefresh, old) => {
setupAutoRefresh(autoRefresh) setupAutoRefresh(autoRefresh)
}) })
watch([page, pageSize], async ([page, pageSize], [oldPage, oldPageSize]) => {
if (page !== oldPage || pageSize !== oldPageSize) {
await refresh();
}
})
const refresh = async () => { const refresh = async () => {
if (typeof address.value != 'string' || address.value.trim() === '') { if (typeof address.value != 'string' || address.value.trim() === '') {
return; return;
} }
try { try {
data.value = await api.fetch("/api/mails"); const { results, count: totalCount } = await api.fetch(
`/api/mails`
+ `?limit=${pageSize.value}`
+ `&offset=${(page.value - 1) * pageSize.value}`
);
data.value = results;
if (totalCount > 0) {
count.value = totalCount;
}
} catch (error) { } catch (error) {
message.error(error.message || "error"); message.error(error.message || "error");
console.error(error); console.error(error);
@@ -150,6 +168,9 @@ onMounted(async () => {
{{ t('refresh') }} {{ t('refresh') }}
</n-button> </n-button>
<n-list hoverable clickable> <n-list hoverable clickable>
<div style="display: inline-block; margin-bottom: 10px;">
<n-pagination v-model:page="page" v-model:page-size="pageSize" :item-count="count" simple />
</div>
<n-list-item v-for="row in data" v-bind:key="row.id"> <n-list-item v-for="row in data" v-bind:key="row.id">
<n-thing class="center" :title="row.subject"> <n-thing class="center" :title="row.subject">
<template #description> <template #description>
+6 -1
View File
@@ -4,11 +4,13 @@ import { NButton, NSelect, NModal } from 'naive-ui'
import { NSwitch, NPopconfirm } from 'naive-ui' import { NSwitch, NPopconfirm } from 'naive-ui'
import { ref } from 'vue' import { ref } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import { useGlobalState } from '../store' import { useGlobalState } from '../store'
import { api } from '../api' import { api } from '../api'
const { jwt, localeCache, themeSwitch, showAuth, auth } = useGlobalState() const { jwt, localeCache, themeSwitch, showAuth, adminAuth, auth } = useGlobalState()
const router = useRouter()
const showLogin = ref(false) const showLogin = ref(false)
const password = ref('') const password = ref('')
@@ -75,6 +77,9 @@ const { t } = useI18n({
<div> <div>
<n-button v-if="localeCache == 'zh'" @click="changeLocale('en')">English</n-button> <n-button v-if="localeCache == 'zh'" @click="changeLocale('en')">English</n-button>
<n-button v-else @click="changeLocale('zh')">中文</n-button> <n-button v-else @click="changeLocale('zh')">中文</n-button>
<n-button v-if="adminAuth" tertiary @click="() => router.push('/admin')" type="primary">
Admin
</n-button>
<n-switch v-model:value="themeSwitch"> <n-switch v-model:value="themeSwitch">
<template #checked> <template #checked>
{{ t('dark') }} {{ t('dark') }}
+98 -10
View File
@@ -8,10 +8,27 @@ api.get('/api/mails', async (c) => {
if (!address) { if (!address) {
return c.json({ "error": "No address" }, 400) return c.json({ "error": "No address" }, 400)
} }
const { limit, offset } = c.req.query();
if (!limit || limit < 0 || limit > 100) {
return c.text("Invalid limit", 400)
}
if (!offset || offset < 0) {
return c.text("Invalid offset", 400)
}
const { results } = await c.env.DB.prepare( const { results } = await c.env.DB.prepare(
`SELECT id, source, subject, message FROM mails where address = ? order by id desc limit 100` `SELECT id, source, subject, message FROM mails where address = ? order by id desc limit ? offset ?`
).bind(address).all(); ).bind(address, limit, offset).all();
return c.json(results); let count = 0;
if (offset == 0) {
const { count: mailCount } = await c.env.DB.prepare(
`SELECT count(*) as count FROM mails where address = ?`
).bind(address).first();
count = mailCount;
}
return c.json({
results: results,
count: count
})
}) })
api.get('/api/settings', async (c) => { api.get('/api/settings', async (c) => {
@@ -66,14 +83,31 @@ api.get('/api/new_address', async (c) => {
}) })
}) })
api.get('/admin/addresss', async (c) => { api.get('/admin/address', async (c) => {
const { limit, offset } = c.req.query();
if (!limit || limit < 0 || limit > 100) {
return c.text("Invalid limit", 400)
}
if (!offset || offset < 0) {
return c.text("Invalid offset", 400)
}
const { results } = await c.env.DB.prepare( const { results } = await c.env.DB.prepare(
`SELECT * FROM address order by id desc` `SELECT * FROM address order by id desc limit ? offset ?`
).all(); ).bind(limit, offset).all();
return c.json(results.map((r) => { let count = 0;
r.name = c.env.PREFIX + r.name; if (offset == 0) {
return r; const { count: addressCount } = await c.env.DB.prepare(
})); `SELECT count(*) as count FROM address`
).first();
count = addressCount;
}
return c.json({
results: results.map((r) => {
r.name = c.env.PREFIX + r.name;
return r;
}),
count: count
})
}) })
api.delete('/admin/delete_address/:id', async (c) => { api.delete('/admin/delete_address/:id', async (c) => {
@@ -104,4 +138,58 @@ api.get('/admin/show_password/:id', async (c) => {
}) })
}) })
api.get('/admin/mails', async (c) => {
const { address, limit, offset } = c.req.query();
if (!limit || limit < 0 || limit > 100) {
return c.text("Invalid limit", 400)
}
if (!offset || offset < 0) {
return c.text("Invalid offset", 400)
}
const { results } = await c.env.DB.prepare(
`SELECT id, source, subject, message FROM mails where address = ? order by id desc limit ? offset ?`
).bind(address, limit, offset).all();
let count = 0;
if (offset == 0) {
const { count: mailCount } = await c.env.DB.prepare(
`SELECT count(*) as count FROM mails where address = ?`
).bind(address).first();
count = mailCount;
}
return c.json({
results: results,
count: count
})
});
api.get('/admin/mails_unknow', async (c) => {
const { limit, offset } = c.req.query();
if (!limit || limit < 0 || limit > 100) {
return c.text("Invalid limit", 400)
}
if (!offset || offset < 0) {
return c.text("Invalid offset", 400)
}
const { results } = await c.env.DB.prepare(`
SELECT id, source, subject, message FROM mails
where address NOT IN (select concat('${c.env.PREFIX}', name) from address)
order by id desc limit ? offset ? `
).bind(limit, offset).all();
let count = 0;
if (offset == 0) {
const { count: mailCount } = await c.env.DB.prepare(`
SELECT count(*) FROM mails
where address NOT IN
(select concat('${c.env.PREFIX}', name) from address)`
).first();
count = mailCount;
}
return c.json({
results: results,
count: count
})
});
export { api } export { api }