feat: add member URL pool and login tracking

This commit is contained in:
richjohnrambo
2026-10-15 10:41:29 +08:00
parent ca0d115e36
commit f8b5e4270b
46 changed files with 3733 additions and 66 deletions
@@ -0,0 +1,63 @@
<template>
<div>
<div class="n-layout-page-header"><n-card :bordered="false" title="会员登录日志">记录C端会员每次登录成功或失败,敏感信息均脱敏展示。</n-card></div>
<n-card :bordered="false" class="proCard">
<BasicForm ref="searchFormRef" @register="register" @submit="reloadTable" @reset="reloadTable" />
<BasicTable ref="actionRef" :columns="columns" :request="loadDataTable" :row-key="row => row.id" :action-column="actionColumn" :scroll-x="scrollX" :resize-height-offset="-10000">
<template #tableTitle><n-button v-if="hasPermission(['/appMemberLoginLog/export'])" type="primary" @click="exportRows">导出</n-button></template>
</BasicTable>
</n-card>
<n-modal v-model:show="showDetail" preset="card" title="登录日志详情" class="detail-modal">
<n-spin :show="detailLoading">
<n-descriptions v-if="detail.id" bordered :column="2" label-placement="left">
<n-descriptions-item label="日志ID">{{ detail.id }}</n-descriptions-item>
<n-descriptions-item label="请求ID">{{ detail.reqId }}</n-descriptions-item>
<n-descriptions-item label="会员编号">{{ detail.memberNo || '-' }}</n-descriptions-item>
<n-descriptions-item label="用户名">{{ detail.username || '-' }}</n-descriptions-item>
<n-descriptions-item label="手机号">{{ detail.mobileMask || '-' }}</n-descriptions-item>
<n-descriptions-item label="登录方式">{{ dict.getLabel('appMemberLoginTypeOptions', detail.loginType) }}</n-descriptions-item>
<n-descriptions-item label="登录状态">{{ dict.getLabel('sys_login_status', detail.status) }}</n-descriptions-item>
<n-descriptions-item label="登录时间">{{ detail.loginAt }}</n-descriptions-item>
<n-descriptions-item label="登录IP">{{ detail.loginIpMask || '-' }}</n-descriptions-item>
<n-descriptions-item label="登录地区">{{ detail.loginRegion || '-' }}</n-descriptions-item>
<n-descriptions-item label="登录域名">{{ detail.loginDomain || '-' }}</n-descriptions-item>
<n-descriptions-item label="操作系统">{{ detail.os }}</n-descriptions-item>
<n-descriptions-item label="浏览器">{{ detail.browser }}</n-descriptions-item>
<n-descriptions-item label="失败原因">{{ detail.errMsg || '-' }}</n-descriptions-item>
<n-descriptions-item label="User-Agent" :span="2">{{ detail.userAgent || '-' }}</n-descriptions-item>
</n-descriptions>
</n-spin>
</n-modal>
</div>
</template>
<script lang="ts" setup>
import { computed, h, onMounted, reactive, ref } from 'vue';
import { BasicTable, TableAction } from '@/components/Table';
import { BasicForm, useForm } from '@/components/Form';
import { usePermission } from '@/hooks/web/usePermission';
import { useDictStore } from '@/store/modules/dict';
import { adaTableScrollX } from '@/utils/hotgo';
import { List, View, Export } from '@/api/appMemberLoginLog';
import { columns, schemas, loadOptions } from './model';
const dict = useDictStore();
const { hasPermission } = usePermission();
const actionRef = ref();
const searchFormRef = ref<any>({});
const showDetail = ref(false);
const detailLoading = ref(false);
const detail = reactive<any>({});
const [register] = useForm({ gridProps: { cols: '1 s:1 m:2 l:3' }, labelWidth: 80, schemas });
const loadDataTable = (res) => List({ ...searchFormRef.value?.formModel, ...res });
const reloadTable = () => actionRef.value?.reload();
const actionColumn = reactive({ width: 100, title: '操作', key: 'action', fixed: 'right', render: (row) => h(TableAction as any, { style: 'button', actions: [{ label: '详情', onClick: () => openDetail(row), auth: ['/appMemberLoginLog/view'] }] }) });
const scrollX = computed(() => adaTableScrollX(columns, actionColumn.width));
async function openDetail(row) { showDetail.value = true; detailLoading.value = true; try { Object.assign(detail, await View({ id: row.id })); } finally { detailLoading.value = false; } }
function exportRows() { Export({ ...searchFormRef.value?.formModel }); }
onMounted(loadOptions);
</script>
<style lang="less" scoped>
:deep(.detail-modal) { width: min(900px, 94vw); }
</style>