浏览代码

迁移洞察页和打赏页到vue

懒得勤快 3 周之前
父节点
当前提交
d1c177c54e

+ 1 - 0
.gitignore

@@ -277,3 +277,4 @@ Test
 /src/Masuit.MyBlogs.Core/App_Data/OneDrive.db
 /src/Masuit.MyBlogs.Core/.config
 static
+/front/public/test.html

+ 6 - 1
front/src/main.ts

@@ -25,6 +25,7 @@ import "animate.css";
 // vxe-table
 import VxeUIAll from "vxe-pc-ui";
 import "vxe-pc-ui/es/style.css";
+import XEUtils from 'xe-utils'
 
 import VxeUITable from "vxe-table";
 import "vxe-table/es/style.css";
@@ -66,7 +67,11 @@ app.config.globalProperties.$baseURL = globalConfig.baseURL;
 app.config.globalProperties.$timeOut = globalConfig.timeOut;
 app.config.globalProperties.$Max_KeepAlive = globalConfig.Max_KeepAlive;
 app.config.globalProperties.$dayjs = dayjs;
-
+VxeUIAll.formats.add('formatDate', {
+  cellFormatMethod({ cellValue }, format?: string) {
+    return XEUtils.toDateString(cellValue, format || 'yyyy-MM-dd HH:mm:ss')
+  }
+})
 // 注册全局组件
 app
   .use(Quasar, quasarUserOptions)

+ 1 - 31
front/src/router/asyncRoutes.ts

@@ -68,7 +68,6 @@ const asyncRoutesChildren: AsyncRouteRecord[] = [
           roles: [],
           title: "写文章",
           icon: "edit_note",
-          keepAlive: true,
         },
         component: () => import("@/views/posts/write.vue"),
       },
@@ -79,7 +78,6 @@ const asyncRoutesChildren: AsyncRouteRecord[] = [
           roles: [],
           title: "待审核文章",
           icon: "hourglass_empty",
-          keepAlive: true,
         },
         component: () => import("@/views/posts/post-pending.vue"),
       },
@@ -90,7 +88,6 @@ const asyncRoutesChildren: AsyncRouteRecord[] = [
           roles: [],
           title: "文章合并",
           icon: "extension",
-          keepAlive: true,
         },
         component: () => import("@/views/merge/list.vue"),
       },
@@ -101,7 +98,6 @@ const asyncRoutesChildren: AsyncRouteRecord[] = [
           roles: [],
           title: "文章合并编辑",
           icon: "extension",
-          keepAlive: true,
           isHidden: true,
         },
         component: () => import("@/views/merge/edit.vue"),
@@ -113,7 +109,6 @@ const asyncRoutesChildren: AsyncRouteRecord[] = [
           roles: [],
           title: "文章合并对比",
           icon: "extension",
-          keepAlive: true,
           isHidden: true,
         },
         component: () => import("@/views/merge/compare.vue"),
@@ -125,7 +120,6 @@ const asyncRoutesChildren: AsyncRouteRecord[] = [
           roles: [],
           title: "文章分类管理",
           icon: "category",
-          keepAlive: true,
         },
         component: () => import("@/views/posts/category.vue"),
       },
@@ -136,7 +130,6 @@ const asyncRoutesChildren: AsyncRouteRecord[] = [
           roles: [],
           title: "文章专题管理",
           icon: "topic",
-          keepAlive: true,
         },
         component: () => import("@/views/posts/seminar.vue"),
       },
@@ -147,7 +140,6 @@ const asyncRoutesChildren: AsyncRouteRecord[] = [
           roles: [],
           title: "快速分享",
           icon: "share",
-          keepAlive: true,
         },
         component: () => import("@/views/posts/share.vue"),
       },
@@ -162,7 +154,7 @@ const asyncRoutesChildren: AsyncRouteRecord[] = [
       title: "审核管理",
       icon: "gavel",
     },
-    component: LayoutComponent,children: [
+    component: LayoutComponent, children: [
       {
         path: "comments",
         name: "评论审核",
@@ -170,7 +162,6 @@ const asyncRoutesChildren: AsyncRouteRecord[] = [
           roles: [],
           title: "评论审核",
           icon: "comment",
-          keepAlive: true,
         },
         component: () => import("@/views/audit/comments.vue"),
       },
@@ -181,7 +172,6 @@ const asyncRoutesChildren: AsyncRouteRecord[] = [
           roles: [],
           title: "留言审核",
           icon: "message",
-          keepAlive: true,
         },
         component: () => import("@/views/audit/msgs.vue"),
       }
@@ -205,7 +195,6 @@ const asyncRoutesChildren: AsyncRouteRecord[] = [
           roles: ["admin", "editor"],
           title: "公告列表",
           icon: "list",
-          keepAlive: true,
         },
         component: () => import("@/views/notice/list.vue"),
       },
@@ -216,7 +205,6 @@ const asyncRoutesChildren: AsyncRouteRecord[] = [
           roles: ["admin", "editor"],
           title: "编辑公告",
           icon: "edit",
-          keepAlive: false,
         },
         component: () => import("@/views/notice/write.vue"),
       },
@@ -240,7 +228,6 @@ const asyncRoutesChildren: AsyncRouteRecord[] = [
           roles: ["admin", "editor"],
           title: "杂项列表",
           icon: "list",
-          keepAlive: true,
         },
         component: () => import("@/views/misc/list.vue"),
       },
@@ -251,7 +238,6 @@ const asyncRoutesChildren: AsyncRouteRecord[] = [
           roles: ["admin", "editor"],
           title: "新建杂项页",
           icon: "edit",
-          keepAlive: false,
         },
         component: () => import("@/views/misc/write.vue"),
       },
@@ -264,7 +250,6 @@ const asyncRoutesChildren: AsyncRouteRecord[] = [
       roles: [],
       title: "文件管理器",
       icon: "folder",
-      keepAlive: true,
     },
     component: () => import("@/views/home/files.vue"),
   },
@@ -286,7 +271,6 @@ const asyncRoutesChildren: AsyncRouteRecord[] = [
           roles: ["admin"],
           title: "系统设置",
           icon: "tune",
-          keepAlive: true,
         },
         component: () => import("@/views/system/settings.vue"),
       },
@@ -297,7 +281,6 @@ const asyncRoutesChildren: AsyncRouteRecord[] = [
           roles: ["admin"],
           title: "菜单管理",
           icon: "menu",
-          keepAlive: true,
         },
         component: () => import("@/views/menus/index.vue"),
       },
@@ -308,7 +291,6 @@ const asyncRoutesChildren: AsyncRouteRecord[] = [
           roles: ["admin"],
           title: "防火墙管理",
           icon: "security",
-          keepAlive: true,
         },
         component: () => import("@/views/system/firewall.vue"),
       },
@@ -319,7 +301,6 @@ const asyncRoutesChildren: AsyncRouteRecord[] = [
           roles: ["admin"],
           title: "友情链接",
           icon: "link",
-          keepAlive: true,
         },
         component: () => import("@/views/system/links.vue"),
       },
@@ -330,7 +311,6 @@ const asyncRoutesChildren: AsyncRouteRecord[] = [
           roles: ["admin"],
           title: "系统日志",
           icon: "description",
-          keepAlive: true,
         },
         component: () => import("@/views/system/logs.vue"),
       },
@@ -341,7 +321,6 @@ const asyncRoutesChildren: AsyncRouteRecord[] = [
           roles: ["admin"],
           title: "邮件/页面模板",
           icon: "email",
-          keepAlive: true,
         },
         component: () => import("@/views/system/email-templates.vue"),
       },
@@ -352,7 +331,6 @@ const asyncRoutesChildren: AsyncRouteRecord[] = [
           roles: ["admin"],
           title: "邮件记录",
           icon: "send",
-          keepAlive: true,
         },
         component: () => import("@/views/system/sendbox.vue"),
       },
@@ -363,7 +341,6 @@ const asyncRoutesChildren: AsyncRouteRecord[] = [
           roles: ["admin"],
           title: "模板变量",
           icon: "view_list",
-          keepAlive: true,
         },
         component: () => import("@/views/system/values.vue"),
       },
@@ -376,7 +353,6 @@ const asyncRoutesChildren: AsyncRouteRecord[] = [
       roles: [],
       title: "广告管理",
       icon: "campaign",
-      keepAlive: true,
     },
     component: () => import("@/views/advertisement/index.vue"),
   },
@@ -387,7 +363,6 @@ const asyncRoutesChildren: AsyncRouteRecord[] = [
       roles: [],
       title: "打赏管理",
       icon: "monetization_on",
-      keepAlive: true,
     },
     component: () => import("@/views/donate/index.vue"),
   },
@@ -398,7 +373,6 @@ const asyncRoutesChildren: AsyncRouteRecord[] = [
       roles: [],
       title: "用户管理",
       icon: "people",
-      keepAlive: true,
     },
     component: () => import("@/views/user/index.vue"),
   },
@@ -409,7 +383,6 @@ const asyncRoutesChildren: AsyncRouteRecord[] = [
       roles: [],
       title: "搜索记录分析",
       icon: "search",
-      keepAlive: true,
     },
     component: () => import("@/views/search/index.vue"),
   },
@@ -420,7 +393,6 @@ const asyncRoutesChildren: AsyncRouteRecord[] = [
       roles: [],
       title: "定时任务监控",
       icon: "schedule",
-      keepAlive: true,
     },
     component: () => import("@/views/jobs/index.vue"),
   },
@@ -431,7 +403,6 @@ const asyncRoutesChildren: AsyncRouteRecord[] = [
       roles: [],
       title: "站内消息",
       icon: "message",
-      keepAlive: true,
       isHidden: true,
     },
     component: () => import("@/views/msgs/index.vue"),
@@ -443,7 +414,6 @@ const asyncRoutesChildren: AsyncRouteRecord[] = [
       roles: [],
       title: "登录记录",
       icon: "history",
-      keepAlive: true,
       isHidden: true,
     },
     component: () => import("@/views/user/loginrecord.vue"),

+ 121 - 152
front/src/views/donate/index.vue

@@ -4,91 +4,68 @@
     <q-card-section>
       <!-- 顶部控制栏 -->
       <q-btn-group push>
-        <q-btn color="primary" icon="add" @click="openEditDialog()"> 添加打赏记录 </q-btn>
+        <q-btn color="primary" icon="add" @click="addDonateRow"> 添加打赏记录 </q-btn>
         <q-btn color="info" icon="refresh" @click="loadPageData" :loading="loading"> 刷新 </q-btn>
       </q-btn-group>
-      <!-- 主表格 -->
+      <!-- 主表格 行内编辑 -->
       <div class="q-mt-md">
-        <vxe-table :data="tableData" :loading="loading" stripe border>
+        <vxe-table ref="tableRef" :data="tableData" :loading="loading" stripe border show-header-overflow show-overflow :edit-config="{ trigger: 'manual', mode: 'row' }">
           <!-- 打赏时间列 -->
-          <vxe-column field="DonateTime" title="打赏时间" width="140" fixed="left">
-            <template #default="{ row }"> {{ dayjs(row.DonateTime).format('YYYY-MM-DD') }} </template>
-          </vxe-column>
+          <vxe-column field="DonateTime" title="打赏时间" width="160" fixed="left" :edit-render="{ name: 'VxeInput', props: { type: 'date' } }" formatter="formatDate"></vxe-column>
           <!-- 昵称列 -->
-          <vxe-column field="NickName" title="昵称" min-width="120" />
+          <vxe-column field="NickName" title="昵称" min-width="140" :edit-render="{ name: 'VxeInput' }"></vxe-column>
           <!-- 金额列 -->
-          <vxe-column field="Amount" title="金额" width="100" />
+          <vxe-column field="Amount" title="金额" width="110" :edit-render="{ name: 'VxeInput' }"></vxe-column>
           <!-- 打赏方式列 -->
-          <vxe-column field="Via" title="打赏方式" width="120" />
+          <vxe-column field="Via" title="打赏方式" width="140" :edit-render="{ name: 'VxeInput' }"></vxe-column>
           <!-- Email列 -->
-          <vxe-column field="Email" title="Email" min-width="180" />
+          <vxe-column field="Email" title="Email" min-width="200" :edit-render="{ name: 'VxeInput', props: { type: 'email' } }"></vxe-column>
           <!-- QQ或微信列 -->
-          <vxe-column field="QQorWechat" title="QQ或微信" width="140" />
+          <vxe-column field="QQorWechat" title="QQ或微信" width="160" :edit-render="{ name: 'VxeInput' }"></vxe-column>
           <!-- 操作列 -->
-          <vxe-column title="操作" width="110" fixed="right">
-            <template #default="{ row }">
-              <q-btn dense flat color="primary" icon="edit" @click="openEditDialog(row)" class="q-mr-xs">
-                <q-tooltip>编辑</q-tooltip>
-              </q-btn>
-              <q-btn dense flat color="negative" icon="delete">
-                <q-tooltip>删除</q-tooltip>
-                <q-popup-proxy transition-show="scale" transition-hide="scale">
-                  <q-card>
-                    <q-card-section class="row items-center">
-                      <q-icon name="warning" color="red" size="2rem" class="q-mr-sm" />
-                      <div>
-                        <div class="text-h6">确认删除</div>
-                        <div class="text-subtitle2">确认删除这条打赏记录吗?</div>
-                      </div>
-                    </q-card-section>
-                    <q-card-actions align="right">
-                      <q-btn flat label="确认" color="negative" v-close-popup @click="deleteRecord(row.Id)" />
-                      <q-btn flat label="取消" color="primary" v-close-popup />
-                    </q-card-actions>
-                  </q-card>
-                </q-popup-proxy>
-              </q-btn>
+          <vxe-column title="操作" width="130" fixed="right" align="center">
+            <template #default="{ row, $table }">
+              <div class="q-gutter-xs">
+                <template v-if="$table.isEditByRow(row)">
+                  <q-btn color="primary" icon="check" size="sm" dense flat :loading="loading" @click="saveRow(row, $table)">
+                    <q-tooltip>保存</q-tooltip>
+                  </q-btn>
+                  <q-btn color="grey" icon="close" size="sm" dense flat @click="cancelEdit(row, $table)">
+                    <q-tooltip>取消</q-tooltip>
+                  </q-btn>
+                </template>
+                <template v-else>
+                  <q-btn color="info" icon="edit" size="sm" dense flat @click="$table.setEditRow(row)">
+                    <q-tooltip>编辑</q-tooltip>
+                  </q-btn>
+                  <q-btn color="negative" icon="delete" size="sm" dense flat :disable="row.Id === 0 || row.Id == null">
+                    <q-tooltip>删除</q-tooltip>
+                    <q-popup-proxy transition-show="scale" transition-hide="scale">
+                      <q-card>
+                        <q-card-section class="row items-center">
+                          <q-icon name="warning" color="red" size="2rem" class="q-mr-sm" />
+                          <div>
+                            <div class="text-h6">确认删除</div>
+                            <div class="text-subtitle2">确认删除这条打赏记录吗?</div>
+                          </div>
+                        </q-card-section>
+                        <q-card-actions align="right">
+                          <q-btn flat label="确认" color="negative" v-close-popup @click="deleteRecord(row.Id)" />
+                          <q-btn flat label="取消" color="primary" v-close-popup />
+                        </q-card-actions>
+                      </q-card>
+                    </q-popup-proxy>
+                  </q-btn>
+                </template>
+              </div>
             </template>
           </vxe-column>
         </vxe-table>
         <!-- 分页组件 -->
-        <div class="q-mt-md flex justify-center items-center">
-          <q-pagination max-pages="6" v-model="pagination.page" :max="Math.ceil(pagination.total / pagination.rowsPerPage)" direction-links @update:model-value="loadPageData" />
-          <q-select v-model="pagination.rowsPerPage" :options="[10, 15, 20, 30, 50, 100]" dense outlined style="width: 80px" class="q-ml-md" @update:model-value="onPageSizeChange" />
-        </div>
+        <vxe-pager :current-page="pagination.page" :total="pagination.total" :page-size="pagination.rowsPerPage" @page-change="onPageSizeChange" />
       </div>
     </q-card-section>
   </q-card>
-  <!-- 编辑弹窗 -->
-  <q-dialog v-model="showEditDialog" persistent>
-    <q-card style="min-width: 500px">
-      <q-card-section class="row items-center q-pb-none">
-        <div class="text-h6">{{ editingItem?.Id ? '编辑打赏记录' : '添加打赏记录' }}</div>
-        <q-space />
-        <q-btn icon="close" flat round dense @click="closeEditDialog" />
-      </q-card-section>
-      <q-card-section>
-        <q-input v-model="editingItem.NickName" label="昵称" outlined :rules="[val => !!val || '请输入昵称']" />
-        <q-input v-model="editingItem.DonateTime" label="打赏时间" outlined readonly :rules="[val => !!val || '请选择打赏时间']">
-          <template v-slot:append>
-            <q-icon name="event" class="cursor-pointer">
-              <q-popup-proxy cover transition-show="scale" transition-hide="scale">
-                <q-date v-model="editingItem.DonateTime" mask="YYYY-MM-DD" v-close-popup></q-date>
-              </q-popup-proxy>
-            </q-icon>
-          </template>
-        </q-input>
-        <q-input v-model="editingItem.Amount" label="打赏金额" outlined type="number" step="0.01" :rules="[val => !!val || '请输入金额']" />
-        <q-input v-model="editingItem.Via" label="打赏方式" outlined :rules="[val => !!val || '请输入打赏方式']" />
-        <q-input v-model="editingItem.Email" label="Email" outlined type="email" />
-        <q-input v-model="editingItem.QQorWechat" label="QQ或微信" outlined />
-        <div class="row justify-end q-gutter-sm">
-          <q-btn label="取消" color="grey" @click="closeEditDialog" />
-          <q-btn label="保存" type="submit" color="primary" :loading="saving" @click="saveRecord" />
-        </div>
-      </q-card-section>
-    </q-card>
-  </q-dialog>
 </div>
 </template>
 <script setup lang="ts">
@@ -97,7 +74,16 @@ import { toast } from 'vue3-toastify'
 import dayjs from 'dayjs'
 import api from '@/axios/AxiosConfig'
 
-// 接口定义
+interface DonateItem {
+  Id: number | null
+  NickName: string
+  DonateTime: string
+  Amount: string | number
+  Via: string
+  Email: string
+  QQorWechat: string
+}
+
 interface ApiResponse<T = any> {
   Success: boolean
   Message?: string
@@ -105,133 +91,121 @@ interface ApiResponse<T = any> {
   TotalCount?: number
 }
 
-// 响应式数据
 const loading = ref(false)
-const saving = ref(false)
 const deleting = ref(false)
-const tableData = ref([])
+const tableData = ref<DonateItem[]>([])
+const tableRef = ref()
+const originalMap = ref(new Map<number, DonateItem>())
 
-// 分页数据
+// 服务端分页
 const pagination = ref({
   page: 1,
   rowsPerPage: 10,
   total: 0
 })
 
-// 弹窗控制
-const showEditDialog = ref(false)
-
-// 表单数据
-const editingItem = ref({
-  Id: 0,
-  NickName: '',
-  DonateTime: '',
-  Amount: '',
-  Via: '',
-  Email: '',
-  QQorWechat: ''
-})
+const formatDate = (val: string) => val ? dayjs(val).format('YYYY-MM-DD HH:mm:ss') : ''
+const autoFillTime = (row: DonateItem) => {
+  // 如果只有日期没有时间,补全当前时间
+  if (row.DonateTime && row.DonateTime.length === 10) {
+    row.DonateTime = dayjs(row.DonateTime + ' 00:00:00').format('YYYY-MM-DD HH:mm:ss')
+  }
+}
 
-// 加载数据
 const loadPageData = async () => {
   loading.value = true
   try {
-    const params = {
-      page: pagination.value.page,
-      size: pagination.value.rowsPerPage
-    }
-
+    const params = { page: pagination.value.page, size: pagination.value.rowsPerPage }
     const data = await api.post('/donate/getpagedata', null, { params }) as ApiResponse
-
     if (data) {
-      tableData.value = data.Data || []
+      tableData.value = (data.Data || []).map((r: any) => ({ ...r, DonateTime: dayjs(r.DonateTime).format('YYYY-MM-DD HH:mm:ss') }))
       pagination.value.total = data.TotalCount || 0
     }
-  } catch (error) {
+  } catch (e) {
     toast.error('加载数据失败', { autoClose: 2000, position: 'top-center' })
   } finally {
     loading.value = false
   }
 }
 
-// 分页变更
-const onPageSizeChange = () => {
-  pagination.value.page = 1
+const onPageSizeChange = ({ pageSize, currentPage }) => {
+  pagination.value.page = currentPage
+  pagination.value.rowsPerPage = pageSize
   loadPageData()
 }
 
-// 打开编辑弹窗
-const openEditDialog = (item = null) => {
-  if (item) {
-    // 编辑模式
-    editingItem.value = { ...item }
-    // 格式化日期为 YYYY-MM-DD 格式
-    if (item.DonateTime) {
-      editingItem.value.DonateTime = dayjs(item.DonateTime).format('YYYY-MM-DD HH:mm:ss')
-    }
-  } else {
-    // 新增模式
-    editingItem.value = {
-      Id: null,
-      NickName: '',
-      DonateTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
-      Amount: '',
-      Via: '',
-      Email: '',
-      QQorWechat: ''
-    }
+const addDonateRow = async () => {
+  const $table = tableRef.value
+  if (!$table) return
+  // 防止已有编辑行
+  if ($table.getEditRecords && $table.getEditRecords().length > 0) {
+    toast.info('请先保存或取消当前编辑行', { autoClose: 2000, position: 'top-center' })
+    return
   }
-  showEditDialog.value = true
-}
-
-// 关闭编辑弹窗
-const closeEditDialog = () => {
-  showEditDialog.value = false
-  editingItem.value = {
-    Id: null,
+  // 在首行插入
+  const record: DonateItem = {
+    Id: 0,
     NickName: '',
-    DonateTime: '',
+    DonateTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
     Amount: '',
     Via: '',
     Email: '',
     QQorWechat: ''
   }
+  const { row } = await $table.insertAt(record, 0)
+  $table.setEditRow(row)
+  originalMap.value.set(0, { ...record, Id: 0 })
 }
 
-// 保存记录
-const saveRecord = async () => {
-  saving.value = true
+const saveRow = async (row: DonateItem, table: any) => {
+  if (!row.NickName || !row.Amount || !row.Via) {
+    toast.warning('请填写必填字段:昵称/金额/打赏方式', { autoClose: 2000, position: 'top-center' })
+    return
+  }
   try {
-    const data = await api.post('/donate/save', editingItem.value) as ApiResponse
-    if (data?.Success) {
-      toast.success(data.Message || '保存成功', { autoClose: 2000, position: 'top-center' })
-      closeEditDialog()
+    const resp = await api.post('/donate/save', row) as ApiResponse
+    if (resp?.Success) {
+      toast.success(resp.Message || '保存成功', { autoClose: 2000, position: 'top-center' })
+      table.clearEdit()
       await loadPageData()
     } else {
-      toast.error(data?.Message || '保存失败', { autoClose: 2000, position: 'top-center' })
+      toast.error(resp?.Message || '保存失败', { autoClose: 2000, position: 'top-center' })
     }
-  } catch (error) {
+  } catch (e) {
     toast.error('保存失败', { autoClose: 2000, position: 'top-center' })
-  } finally {
-    saving.value = false
   }
 }
 
-// 执行删除
-const deleteRecord = async (id) => {
+const cancelEdit = (row: DonateItem, table: any) => {
+  if (row.Id == null) {
+    // 新增未保存直接移除
+    const idx = tableData.value.indexOf(row)
+    if (idx > -1) tableData.value.splice(idx, 1)
+  } else {
+    const origin = originalMap.value.get(row.Id)
+    if (origin) Object.assign(row, origin)
+  }
+  table.clearEdit()
+}
+
+const deleteRecord = async (id: number) => {
+  if (!id) return
   deleting.value = true
   try {
     const data = await api.post(`/donate/delete/${id}`) as ApiResponse
-    toast.success(data?.Message || '删除成功', { autoClose: 2000, position: 'top-center' })
-    await loadPageData()
-  } catch (error) {
+    if (data?.Success !== false) {
+      toast.success(data?.Message || '删除成功', { autoClose: 2000, position: 'top-center' })
+      await loadPageData()
+    } else {
+      toast.error(data?.Message || '删除失败', { autoClose: 2000, position: 'top-center' })
+    }
+  } catch (e) {
     toast.error('删除失败', { autoClose: 2000, position: 'top-center' })
   } finally {
     deleting.value = false
   }
 }
 
-// 生命周期
 onMounted(() => {
   loadPageData()
 })
@@ -299,14 +273,9 @@ onMounted(() => {
   }
 }
 
-/* 弹窗样式优化 */
-:deep(.q-dialog .q-card) {
-  max-width: 90vw;
-}
-
-/* 表单输入框样式 */
-:deep(.q-field) {
-  margin-bottom: 16px;
+/* 行内编辑输入间距优化 */
+:deep(.vxe-table .q-field) {
+  margin: 0;
 }
 
 /* 分页组件样式 */

+ 20 - 81
front/src/views/system/links.vue

@@ -32,7 +32,7 @@
         <div class="text-body2 q-mt-sm">点击上方"添加链接"按钮添加第一个友情链接</div>
       </div>
       <!-- 友情链接表格 -->
-      <vxe-table v-else ref="tableRef" :data="paginatedLinks" stripe border show-header-overflow show-overflow :loading="loading" :edit-config="{ trigger: 'click', mode: 'row' }">
+      <vxe-table v-else ref="tableRef" :data="paginatedLinks" stripe border show-header-overflow show-overflow :loading="loading" :edit-config="{ trigger: 'manual', mode: 'row' }">
         <!-- 名称列 -->
         <vxe-column field="Name" title="名称" width="150" sortable :edit-render="{}">
           <template #default="{ row }">
@@ -138,27 +138,6 @@
       </div>
     </q-card-section>
   </q-card>
-  <!-- 添加链接对话框 -->
-  <q-dialog v-model="showDialog" persistent>
-    <q-card style="min-width: 500px;">
-      <q-card-section class="row items-center q-pb-none">
-        <div class="text-h6">添加友情链接</div>
-        <q-space />
-        <q-btn icon="close" flat round dense @click="closeDialog" />
-      </q-card-section>
-      <q-card-section>
-        <div class="q-gutter-md">
-          <q-input v-model="newLink.Name" label="链接名称" placeholder="请输入链接名称" dense outlined required :rules="[val => !!val || '请输入链接名称']" />
-          <q-input v-model="newLink.Url" label="链接地址" placeholder="请输入完整的链接地址" dense outlined required :rules="[val => !!val || '请输入链接地址']" />
-          <q-input v-model="newLink.UrlBase" label="主页地址" placeholder="请输入主页地址" dense outlined required :rules="[val => !!val || '请输入主页地址']" />
-        </div>
-      </q-card-section>
-      <q-card-actions align="right">
-        <q-btn flat label="取消" @click="closeDialog" />
-        <q-btn color="primary" label="添加" @click="addLink" :loading="adding" :disable="!newLink.Name || !newLink.Url || !newLink.UrlBase" />
-      </q-card-actions>
-    </q-card>
-  </q-dialog>
 </div>
 </template>
 <script setup lang="ts">
@@ -189,29 +168,12 @@ interface ApiResponse {
 // 响应式数据
 const links = ref<LinkItem[]>([])
 const loading = ref(false)
-const adding = ref(false)
 
 // 分页相关数据
 const currentPage = ref(1)
 const pageSize = ref(15)
 const searchTerm = ref('')
 
-// 对话框状态
-const showDialog = ref(false)
-
-// 新建链接数据
-const newLink = ref<LinkItem>({
-  Id: 0,
-  Name: '',
-  Url: '',
-  UrlBase: '',
-  Loopbacks: 0,
-  UpdateTime: '',
-  Except: false,
-  Recommend: false,
-  Status: 0
-})
-
 // 表格引用
 const tableRef = ref()
 
@@ -257,54 +219,31 @@ const loadLinks = async () => {
 }
 
 // 显示添加对话框
-const showAddDialog = () => {
-  newLink.value = {
-    Name: '',
-    Url: '',
-    UrlBase: '',
-    Id: 0,
-    Loopbacks: 0,
-    UpdateTime: '',
-    Except: false,
-    Recommend: false,
-    Status: 0
-  }
-  showDialog.value = true
-}
-
-// 关闭对话框
-const closeDialog = () => {
-  showDialog.value = false
-}
-
-// 添加友情链接
-const addLink = async () => {
-  if (!newLink.value.Name || !newLink.value.Url || !newLink.value.UrlBase) {
-    toast.warning('请填写完整的链接信息', { autoClose: 2000, position: 'top-center' })
-    return
-  }
-
-  adding.value = true
-  try {
-    const response = await api.post('/links/save', newLink.value) as ApiResponse
-
-    if (response?.Success) {
-      toast.success(response.Message || '保存成功', { autoClose: 2000, position: 'top-center' })
-      closeDialog()
-      loadLinks()
-    } else {
-      toast.error(response?.Message || '保存失败', { autoClose: 2000, position: 'top-center' })
+const showAddDialog = async () => {
+  const $table = tableRef.value
+  if ($table) {
+    const record = {
+      Name: '',
+      Url: '',
+      UrlBase: '',
+      Id: 0,
+      Loopbacks: 0,
+      UpdateTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
+      Except: false,
+      Recommend: false,
+      Status: 0
     }
-  } catch (error) {
-    toast.error('保存失败', { autoClose: 2000, position: 'top-center' })
-    console.error('Error adding link:', error)
-  } finally {
-    adding.value = false
+    const { row: newRow } = await $table.insert(record)
+    $table.setEditCell(newRow, 'Name')
   }
 }
 
 // 保存编辑
 const saveLink = async (row: LinkItem, table: any) => {
+  if (!row.Name || !row.Url || !row.UrlBase) {
+    toast.warning('请填写完整的链接信息', { autoClose: 2000, position: 'top-center' })
+    return
+  }
   const response = await api.post('/links/save', row) as ApiResponse
 
   if (response?.Success) {

+ 1 - 1
src/Masuit.MyBlogs.Core/Controllers/LinksController.cs

@@ -99,7 +99,7 @@ public sealed class LinksController : BaseController
     /// <param name="dto"></param>
     /// <returns></returns>
     [MyAuthorize, DistributedLockFilter]
-    public async Task<ActionResult> Save([FromBody] LinksDto dto)
+    public async Task<ActionResult> Save([FromBodyOrDefault] LinksDto dto)
     {
         bool b = await LinksService.AddOrUpdateSavedAsync(l => l.Id, dto.ToLinks()) > 0;
         return b ? ResultData(null, message: "添加成功!") : ResultData(null, false, "添加失败!");

+ 162 - 171
src/Masuit.MyBlogs.Core/Views/Advertisement/ClickRecordsInsight.cshtml

@@ -1,208 +1,199 @@
 @model Masuit.MyBlogs.Core.Models.ViewModel.AdvertisementViewModel
-
 @{
-    Layout = null;
+  Layout = null;
 }
-
 <!DOCTYPE html>
 <html>
 <head>
-    <meta charset="utf-8">
-    <title>广告《@Model.Title》洞察分析</title>
-    <meta content="webkit" name="renderer">
-    <meta content="IE=edge,chrome=1" http-equiv="X-UA-Compatible">
-    <meta content="width=device-width, initial-scale=1, maximum-scale=1" name="viewport">
-    <link href="/Assets/layui/css/layui.min.css" media="all" rel="stylesheet">
-    <style>
+  <meta charset="utf-8">
+  <title>广告《@Model.Title》洞察分析</title>
+  <meta content="width=device-width, initial-scale=1.0" name="viewport">
+  <style>
         .mp-results.mp-bottomleft {
             top: unset !important;
             bottom: 0;
         }
     </style>
 </head>
-<body style="overflow-x: hidden">
-    <h3 align="center">广告《@Model.Title》洞察分析</h3>
-    <div class="searchTable">
-        <div class="layui-inline">
-            <input class="layui-input" name="kw" id="kw">
-        </div>
-        <button class="layui-btn" data-type="reload">搜索</button>
-        <a class="layui-btn" asp-controller="Advertisement" asp-action="ExportClickRecords" asp-route-id="@Model.Id">导出</a>
-    </div>
-    <table class="layui-hide" id="table" lay-filter="tableEvent"></table>
-    <form class="layui-form">
-        <label class="layui-form-label">对比最近</label>
-        <div class="layui-input-inline">
-            <select id="period" name="period" lay-filter="period">
-                <option value="0">不对比</option>
-                <option value="7">一周</option>
-                <option value="15">15天</option>
-                <option value="30" selected="selected">一个月</option>
-                <option value="60">两个月</option>
-                <option value="90">三个月</option>
-            </select>
-        </div>
-    </form>
-    <div id="chart" style="height: 500px"></div>
-    <mini-profiler max-traces="5" />
+<body id="app" style="overflow-x: hidden">
+<h3 align="center">广告《@Model.Title》洞察分析</h3>
+<vxe-toolbar>
+  <template #tools>
+    <vxe-input @@search-click="loadData" placeholder="搜索" type="search" v-model="kw"></vxe-input>
+    <a asp-action="ExportClickRecords" asp-controller="Advertisement" asp-route-id="@Model.Id" class="theme--primary type--button vxe-button">导出</a>
+  </template>
+</vxe-toolbar>
+<!-- 主表格 -->
+<vxe-table :data="tableData" :loading="loading" border class="limited-row-height" ref="tableRef" stripe>
+  <vxe-column field="IP" fixed="left" min-width="200" title="IP">
+    <template #default="{ row }">
+      <a :href="`/tools/ip/${row.IP}`" class="text-primary" target="_blank"> {{ row.IP }} </a>
+    </template>
+  </vxe-column>
+  <vxe-column field="Location" min-width="180" title="地理位置"></vxe-column>
+  <vxe-column field="Referer" min-width="250" title="请求来源"></vxe-column>
+  <vxe-column field="Time" min-width="250" title="访问时间"></vxe-column>
+</vxe-table>
+<!-- 分页组件 -->
+<div class="">
+  <vxe-pager :current-page.sync="pageConfig.page" :page-size.sync="pageConfig.size" :total="pageConfig.total" @@page-change="pageChange">
+  </vxe-pager>
+</div>
+<vxe-toolbar>
+  <template #tools>
+    <span>对比最近:</span>
+    <vxe-select @@change="showCharts" v-model="period">
+      <vxe-option :key="num" :label="`${num}天`" :value="num" v-for="num in [7,15,30,60,90,180,365]"></vxe-option>
+    </vxe-select>
+  </template>
+</vxe-toolbar>
+<div id="chart" style="height: 500px"></div>
+<mini-profiler max-traces="5"/>
 </body>
 </html>
-
-<script src="/Assets/layui/layui.js"></script>
-<script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js" type="text/javascript"></script>
+<!-- 引入vxe-table样式 -->
+<link href="https://cdn.jsdelivr.net/npm/[email protected]/lib/style.min.css" rel="stylesheet">
+<link href="https://cdn.jsdelivr.net/npm/[email protected]/lib/style.min.css" rel="stylesheet">
+<script src="https://unpkg.com/vue"></script>
+<!-- 引入vxe-table的JS文件 -->
+<script src="https://cdn.jsdelivr.net/npm/xe-utils/dist/xe-utils.umd.min.js"></script>
+<script src="https://cdn.jsdelivr.net/npm/[email protected]/lib/index.umd.min.js"></script>
+<script src="https://cdn.jsdelivr.net/npm/[email protected]/lib/index.umd.min.js"></script>
+<script src="https://cdn.jsdelivr.net/npm/echarts@6/dist/echarts.min.js" type="text/javascript"></script>
+<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
 <script>
-    layui.use('table', function() {
-        var table = layui.table;
-        table.render({
-            elem: '#table',
-            url: '/partner/@Model.Id/records',
-            cellMinWidth: 80, //全局定义常规单元格的最小宽度,layui 2.2.1 新增
-            cols: [
-                [
-                    { field: 'IP', title: 'IP', align: 'center', event: 'tool-ip', width:320 },
-                    { field: 'Location', title: '位置和网络', align: 'center'},
-                    { field: 'Referer', title: '页面来源', align: 'center', event: 'visit' },
-                    { field: 'Time', title: '访问时间', align: 'center',width:180 }
-                ]
-            ],
-            page: true,
-            limit:20,
-            request: {
-                limitName: 'size' //每页数据量的参数名,默认:limit
-            },
-            parseData: function(res) { //res 即为原始返回的数据
-                return {
-                    "code": res.TotalCount > 0 ? 0 : 1, //解析接口状态
-                    "msg": "暂无数据", //解析提示文本
-                    "count": res.TotalCount, //解析数据长度
-                    "data": res.Data //解析数据列表
-                };
-            }
-        });
-        table.on('tool(tableEvent)', function(obj){
-            var data = obj.data;
-            if(obj.event === 'tool-ip'){
-                window.open("/tools/ip/"+data.IP);
-            }
-            if(obj.event === 'visit'){
-                window.open(data.Referer);
-            }
-        });
+    const { createApp, ref, onMounted, watch, computed } = Vue;
+    createApp({
+        setup() {
+            // 表格数据
+            const tableData = ref([]);
+            const kw = ref('');
+            const period = ref(30);
+            // 分页配置
+            const pageConfig = ref({
+                page: 1,
+                size: 10,
+                total: 0
+            });
 
-        var $ = layui.$;
-        $('.searchTable .layui-btn').on('click', function () {
-            table.reload('table', {
-                page: {
-                    curr: 1
-                },
-                where: {
-                    kw: $('#kw').val()
-                }
+            // 加载状态
+            const loading = ref(false);
+            return {
+              kw,
+                tableData,
+                pageConfig,
+                loading,
+                period
+            };
+        },
+        methods: {
+          pageChange({ pageSize, currentPage }) {
+            this.pageConfig.page = currentPage;
+            this.pageConfig.size = pageSize;
+            this.loadData();
+          },
+          async loadData(){
+            this.loading = true;
+            const data = await axios.get(`/partner/@Model.Id/records?kw=${this.kw}&page=${this.pageConfig.page}&size=${this.pageConfig.size}`).then(function(response) {
+              return response.data;
             });
-        });
-    });
-    layui.use("form", function () {
-        var form = layui.form;
-        form.on("select(period)", function (data) {
-            var chartDom = document.getElementById('chart');
-            echarts.init(chartDom).dispose();
-            showCharts();
-        });
-    });
-    showCharts();
-    function showCharts() {
-        var period = document.getElementById("period").value;
-        window.fetch(`/partner/@Model.Id/records-chart?compare=${period > 0}&period=${period}`, {
-            credentials: 'include',
-            method: 'GET',
-            mode: 'cors'
-        }).then(function (response) {
-            return response.json();
-        }).then(function (res) {
-            var xSeries = [];
-            var ySeries = [];
-            for (let series of res) {
+            this.tableData = data.Data;
+            this.pageConfig.total = data.TotalCount;
+            this.loading = false;
+          },
+          showCharts() {
+            axios.get(`/partner/@Model.Id/records-chart?compare=${this.period > 0}&period=${this.period}`).then(function(response) {
+              const res = response.data;
+              var xSeries = [];
+              var ySeries = [];
+              for (let series of res) {
                 var x = [];
                 var y = [];
                 for (let item of series) {
-                    x.push(new Date(Date.parse(item.Date)).toLocaleDateString());
-                    y.push(item.Count);
+                  x.push(new Date(Date.parse(item.Date)).toLocaleDateString());
+                  y.push(item.Count);
                 }
                 xSeries.push(x);
                 ySeries.push(y);
-            }
-            var chartDom = document.getElementById('chart');
-            var myChart = echarts.init(chartDom);
-            const colors = ['#009688', '#ccc'];
-            var option = {
+              }
+              var chartDom = document.getElementById('chart');
+              var myChart = echarts.init(chartDom);
+              const colors = ['#009688', '#ccc'];
+              var option = {
                 color: colors,
                 tooltip: {
-                    trigger: 'none',
-                    axisPointer: {
-                        type: 'cross'
-                    }
+                  trigger: 'none',
+                  axisPointer: {
+                    type: 'cross'
+                  }
                 },
                 legend: {},
                 grid: {
-                    top: 70,
-                    bottom: 50
+                  top: 70,
+                  bottom: 50
                 },
                 title: {
-                    left: 'center',
-                    text: '广告《@Model.Title》最近访问趋势'
+                  left: 'center',
+                  text: '广告《@Model.Title》最近访问趋势'
                 },
-                xAxis: xSeries.map(function (item, index) {
-                    return {
-                        type: 'category',
-                        axisTick: {
-                            alignWithLabel: true
-                        },
-                        axisLine: {
-                            onZero: false,
-                            lineStyle: {
-                                color: colors[index]
-                            }
-                        },
-                        axisPointer: {
-                            label: {
-                                formatter: function (params) {
-                                    return params.value + (params.seriesData.length ? ':' + params.seriesData[0].data : '');
-                                }
-                            }
-                        },
-                        data: item
-                    }
+                xAxis: xSeries.map(function(item, index) {
+                  return {
+                    type: 'category',
+                    axisTick: {
+                      alignWithLabel: true
+                    },
+                    axisLine: {
+                      onZero: false,
+                      lineStyle: {
+                        color: colors[index]
+                      }
+                    },
+                    axisPointer: {
+                      label: {
+                        formatter: function(params) {
+                          return params.value + (params.seriesData.length ? ':' + params.seriesData[0].data : '');
+                        }
+                      }
+                    },
+                    data: item
+                  }
                 }),
                 yAxis: [
-                    {
-                        type: 'value'
-                    }
+                  {
+                    type: 'value'
+                  }
                 ],
-                series: ySeries.map(function (item, index) {
-                    return {
-                        type: 'line',
-                        symbol: 'none',
-                        xAxisIndex: index,
-                        areaStyle: {},
-                        data: item,
-                        lineStyle: {
-                            type: index === 1 ? 'dashed' : ""
-                        },
-                        markPoint: {
-                            data: [
-                                { type: 'max', name: '最大值' },
-                                { type: 'min', name: '最小值' }
-                            ]
-                        },
-                        markLine: {
-                            data: [
-                                { type: 'average', name: '平均值' }
-                            ]
-                        }
+                series: ySeries.map(function(item, index) {
+                  return {
+                    type: 'line',
+                    symbol: 'none',
+                    xAxisIndex: index,
+                    areaStyle: {},
+                    data: item,
+                    lineStyle: {
+                        type: index === 1 ? 'dashed' : ""
+                    },
+                    markPoint: {
+                        data: [
+                            { type: 'max', name: '最大值' },
+                            { type: 'min', name: '最小值' }
+                        ]
+                    },
+                    markLine: {
+                        data: [
+                            { type: 'average', name: '平均值' }
+                        ]
                     }
+                  }
                 })
-            };
-            myChart.setOption(option);
-        });
+              };
+              myChart.setOption(option);
+            });
+          }
+        },
+    created() {
+        this.loadData();
+        this.showCharts();
     }
-</script>
+    }).use(VxeUI).use(VXETable).mount('#app');
+</script>

+ 88 - 78
src/Masuit.MyBlogs.Core/Views/Misc/Donate.cshtml

@@ -1,6 +1,5 @@
 @model string
 @using Masuit.MyBlogs.Core.Models.DTO
-
 @{
     ViewBag.Title = "网站打赏";
     Layout = "~/Views/Shared/_Layout.cshtml";
@@ -12,10 +11,11 @@
         height: 50vh;
         max-height: 400px;
         position: relative;
-        background: url(/Content/images/@(r.Next(1,9)).jpg) no-repeat center;
-        background-size:cover;
+        background: url(/Content/images/@(r.Next(1, 9)).jpg) no-repeat center;
+        background-size: cover;
         background-attachment: fixed;
     }
+
     .flex-box {
         display: flex;
         flex-wrap: wrap;
@@ -23,22 +23,25 @@
         width: 100%;
         height: 100%;
     }
-    .flex-box>div{
-        margin: 0 15px;
-        width: 18%;
-    }
+
+        .flex-box > div {
+            margin: 0 15px;
+            width: 18%;
+        }
+
     @@media only screen and (max-width: 1080px) {
-        .flex-box>div{
+        .flex-box > div {
             width: 29.8%;
         }
     }
 </style>
-<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.7.7/angular.min.js"></script>
-<script src="~/Scripts/tm.pagination.js"></script>
-<script src="https://cdnjs.cloudflare.com/ajax/libs/ng-table/1.0.0/ng-table.js"></script>
 <ol class="cd-breadcrumb triangle">
-    <li><a asp-controller="Home" asp-action="Index">首页</a></li>
-    <li class="current"><em>@ViewBag.Title</em></li>
+    <li>
+        <a asp-action="Index" asp-controller="Home">首页</a>
+    </li>
+    <li class="current">
+        <em>@ViewBag.Title</em>
+    </li>
 </ol>
 <div class="bg-title">
     <div class="header-content text-center">
@@ -52,37 +55,33 @@
     </div>
 </div>
 @Html.Raw(Model)
-<div class="container-fluid" ng-app="myApp" ng-controller="home as list">
-    <div class="page-header margin-clear">
-        <h2 class="size24">
+<div class="container-fluid" id="donateApp" ng-controller="home as list">
+    <div class="margin-clear page-header">
+        <h2 class="size24" style="display: inline">
             打赏名单(排名不分先后):
         </h2>
     </div>
-    <table ng-cloak ng-table="list.tableParams" class="table table-bordered table-hover table-condensed margin-clear" ng-form="list.tableForm" disable-filter="list.isAdding" tracked-table="list.tableTracker" style="margin: 0">
-        <tr ng-repeat="row in $data" ng-form="rowForm" tracked-table-row="row">
-            <td title="'打赏时间'" sortable="'DonateTime'">
-                {{row.DonateTime|date:'yyyy-MM-dd'}}
-            </td>
-            <td title="'昵称'" sortable="'NickName'">
-                {{row.NickName}}
-            </td>
-            <td title="'邮箱'" sortable="'Email'">
-                {{row.Email}}
-            </td>
-            <td title="'QQ或微信'" sortable="'QQorWechat'">
-                {{row.QQorWechat}}
-            </td>
-            <td title="'金额'" sortable="'Amount'">
-                {{row.Amount}}
-            </td>
-        </tr>
-    </table>
-    <tm-pagination conf="paginationConf"></tm-pagination>
+    <vxe-table :data="tableData" :loading="loading" border stripe>
+        <!-- 打赏时间列 -->
+        <vxe-column field="DonateTime" formatter="formatDate" title="打赏时间" width="160"></vxe-column>
+        <!-- 昵称列 -->
+        <vxe-column field="NickName" min-width="140" title="昵称"></vxe-column>
+        <!-- 金额列 -->
+        <vxe-column field="Amount" title="金额" width="110"></vxe-column>
+        <!-- 打赏方式列 -->
+        <vxe-column field="Via" title="打赏方式" width="140"></vxe-column>
+        <!-- Email列 -->
+        <vxe-column field="Email" min-width="200" title="Email"></vxe-column>
+        <!-- QQ或微信列 -->
+        <vxe-column field="QQorWechat" title="QQ或微信" width="160"></vxe-column>
+    </vxe-table>
+    <!-- 分页组件 -->
+    <vxe-pager :current-page="pageConfig.page" :page-size="pageConfig.size" :total="pageConfig.total" @@page-change="pageChange" />
 </div>
 @if (ads.Count == 2)
 {
     <div class="container-fluid">
-        <div class="page-header margin-clear">
+        <div class="margin-clear page-header">
             <h2 class="size24" style="display: inline">
                 推广支持:
             </h2>
@@ -101,48 +100,59 @@
         </div>
     </div>
 }
+<link href="https://cdn.jsdelivr.net/npm/[email protected]/lib/style.min.css" rel="stylesheet">
+<link href="https://cdn.jsdelivr.net/npm/[email protected]/lib/style.min.css" rel="stylesheet">
+<script src="https://unpkg.com/vue"></script>
+<!-- 引入vxe-table的JS文件 -->
+<script src="https://cdn.jsdelivr.net/npm/xe-utils/dist/xe-utils.umd.min.js"></script>
+<script src="https://cdn.jsdelivr.net/npm/[email protected]/lib/index.umd.min.js"></script>
+<script src="https://cdn.jsdelivr.net/npm/[email protected]/lib/index.umd.min.js"></script>
+<script src="https://cdn.jsdelivr.net/npm/echarts@6/dist/echarts.min.js" type="text/javascript"></script>
+<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
+<script src="https://cdn.jsdelivr.net/npm/dayjs/dayjs.min.js"></script>
 <script>
-    var app = angular.module('myApp', ["ngTable", "tm.pagination"]);
-    app.config(["$httpProvider", function ($httpProvider) {
-        $httpProvider.defaults.transformRequest = function (obj) {
-            var str = [];
-            for (var p in obj) {
-                if (obj.hasOwnProperty(p)) {
-                    str.push(window.encodeURIComponent(p) + "=" + window.encodeURIComponent(obj[p]));
-                }
-            }
-            return str.join("&");
-        };
+    VxeUI.formats.add('formatDate', {
+      cellFormatMethod:function (data, format) {
+        return dayjs(data.cellValue).format(format || 'YYYY-MM-DD')
+      }
+    })
+    const { createApp, ref, onMounted, watch, computed } = Vue;
+    createApp({
+        setup() {
+            // 表格数据
+            const tableData = ref([]);
 
-        $httpProvider.defaults.headers.post = {
-            'Content-Type': 'application/x-www-form-urlencoded; charser=UTF-8'
+            // 分页配置
+            const pageConfig = ref({
+                page: 1,
+                size: 10,
+                total: 0
+            });
+
+            // 加载状态
+            const loading = ref(false);
+            return {
+                tableData,
+                pageConfig,
+                loading
+            };
+        },
+        methods: {
+          pageChange({ pageSize, currentPage }) {
+              this.pageConfig.page = currentPage;
+              this.pageConfig.size = pageSize;
+              this.loadData();
+          },
+          async loadData(){
+            this.loading = true;
+            const data = await axios.post(`/DonateList?page=${this.pageConfig.page}&size=${this.pageConfig.size}`).then(res=>res.data);
+            this.tableData = data.Data;
+            this.pageConfig.total = data.TotalCount;
+            this.loading = false;
+          }},
+        created() {
+            this.loadData();
         }
-    }]);
-    app.controller("home", ["$scope", "$http", "NgTableParams", function ($scope, $http, NgTableParams) {
-        var self = this;
-        $scope.paginationConf = {
-            currentPage: 1,
-            itemsPerPage: 10,
-            pagesLength: 15,
-            perPageOptions: [10, 15, 20, 30, 50, 100],
-            rememberPerPage: 'perPageItems',
-            onChange: function () {
-                window.loading();
-                $http.post("/DonateList", {
-                    page: $scope.paginationConf.currentPage,
-                    size: $scope.paginationConf.itemsPerPage
-                }).then(function (res) {
-                    $scope.paginationConf.totalItems = res.data.TotalCount;
-                    $("div[ng-table-pagination]").remove();
-                    self.tableParams = new NgTableParams({
-                        count: 50000
-                    }, {
-                        filterDelay: 0,
-                        dataset: res.data.Data
-                    });
-                    window.loadingDone();
-                });
-            }
-        };
-    }]);
+    }).use(VxeUI).use(VXETable).mount('#donateApp');
+
 </script>

+ 215 - 213
src/Masuit.MyBlogs.Core/Views/Misc/Donate_Admin.cshtml

@@ -1,11 +1,10 @@
 @model string
 @using Masuit.MyBlogs.Core.Models.DTO
-
 @{
-    ViewBag.Title = "网站打赏";
-    Layout = "~/Views/Shared/_Layout.cshtml";
-    Random r = new Random();
-    List<AdvertisementDto> ads = ViewBag.Ads;
+  ViewBag.Title = "网站打赏";
+  Layout = "~/Views/Shared/_Layout.cshtml";
+  Random r = new Random();
+  List<AdvertisementDto> ads = ViewBag.Ads;
 }
 <style>
     .bg-title {
@@ -33,226 +32,229 @@
         }
     }
 </style>
-<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.10.1/lodash.min.js"></script>
-<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.7.7/angular.min.js"></script>
-<script src="~/Scripts/tm.pagination.js"></script>
-<script src="https://cdnjs.cloudflare.com/ajax/libs/ng-table/1.0.0/ng-table.js"></script>
 <ol class="cd-breadcrumb triangle">
-    <li><a asp-controller="Home" asp-action="Index">首页</a></li>
-    <li class="current">
-        <em>@ViewBag.Title</em>
-    </li>
+  <li>
+    <a asp-action="Index" asp-controller="Home">首页</a>
+  </li>
+  <li class="current">
+    <em>@ViewBag.Title</em>
+  </li>
 </ol>
 <div class="bg-title">
-    <div class="header-content text-center">
-        <h2 class="size48">
-            喜欢我的作品和文章?
-        </h2>
-        <div class="divider"></div>
-        <p class="size24">
-            您的捐助就是给我最大的鼓励
-        </p>
-    </div>
+  <div class="header-content text-center">
+    <h2 class="size48">
+      喜欢我的作品和文章?
+    </h2>
+    <div class="divider"></div>
+    <p class="size24">
+      您的捐助就是给我最大的鼓励
+    </p>
+  </div>
 </div>
 @Html.Raw(Model)
-<div class="container-fluid" ng-app="myApp" ng-controller="home as list">
-    <div class="page-header margin-clear">
-        <h2 class="size24" style="display: inline">
-            打赏名单(排名不分先后):
-        </h2>
-        <button class="btn btn-info pull-right" ng-click="save()">添加打赏</button>
-    </div>
-    <table ng-table="list.tableParams" class="table table-bordered table-hover table-condensed margin-clear" ng-form="list.tableForm" disable-filter="list.isAdding" tracked-table="list.tableTracker">
-        <tr ng-repeat="row in $data" ng-form="rowForm" tracked-table-row="row">
-            <td title="'打赏时间'">
-                {{row.DonateTime|date:'yyyy-MM-dd'}}
-            </td>
-            <td title="'昵称'">
-                {{row.NickName}}
-            </td>
-            <td title="'金额'">
-                {{row.Amount}}
-            </td>
-            <td title="'打赏方式'">
-                {{row.Via}}
-            </td>
-            <td title="'Email'">
-                {{row.Email}}
-            </td>
-            <td title="'QQ或微信'">
-                {{row.QQorWechat}}
-            </td>
-            <td title="'操作'">
-                <button class="btn btn-default btn-sm" ng-click="save(row)">编辑</button>
-                <button class="btn btn-danger btn-sm" ng-click="list.del(row)">删除</button>
-            </td>
-        </tr>
-    </table>
-    <tm-pagination conf="paginationConf"></tm-pagination>
-</div>
-@if (ads.Count == 2)
-{
-    <div class="container-fluid">
-        <div class="page-header margin-clear">
-            <h2 class="size24" style="display: inline">
-                推广支持:
-            </h2>
-        </div>
-        <div class="row">
-            <div class="col-md-6">
-                @{
-                    await Html.RenderPartialAsync("_ArticleListAdvertisement", ads[0]);
-                }
-            </div>
-            <div class="col-md-6">
-                @{
-                    await Html.RenderPartialAsync("_ArticleListAdvertisement", ads[1]);
-                }
-            </div>
+<div class="container-fluid" id="donateApp" ng-controller="home as list">
+  <div class="margin-clear page-header">
+    <h2 class="size24" style="display: inline">
+      打赏名单(排名不分先后):
+    </h2>
+  </div>
+  <vxe-toolbar>
+    <template #tools>
+      <vxe-button @@click="add">添加打赏</vxe-button>
+    </template>
+  </vxe-toolbar>
+  <vxe-table :data="tableData" :edit-config="{ trigger: 'manual', mode: 'row' }" :loading="loading" border ref="tableRef" show-header-overflow show-overflow stripe>
+    <!-- 打赏时间列 -->
+    <vxe-column :edit-render="{ name: 'VxeInput', props: { type: 'date' } }" field="DonateTime" fixed="left" formatter="formatDate" title="打赏时间" width="160"></vxe-column>
+    <!-- 昵称列 -->
+    <vxe-column :edit-render="{ name: 'VxeInput' }" field="NickName" min-width="140" title="昵称"></vxe-column>
+    <!-- 金额列 -->
+    <vxe-column :edit-render="{ name: 'VxeInput' }" field="Amount" title="金额" width="110"></vxe-column>
+    <!-- 打赏方式列 -->
+    <vxe-column :edit-render="{ name: 'VxeInput' }" field="Via" title="打赏方式" width="140"></vxe-column>
+    <!-- Email列 -->
+    <vxe-column :edit-render="{ name: 'VxeInput', props: { type: 'email' } }" field="Email" min-width="200" title="Email"></vxe-column>
+    <!-- QQ或微信列 -->
+    <vxe-column :edit-render="{ name: 'VxeInput' }" field="QQorWechat" title="QQ或微信" width="160"></vxe-column>
+    <!-- 操作列 -->
+    <vxe-column align="center" fixed="right" title="操作" width="130">
+      <template #default="{ row, $table }">
+        <div class="q-gutter-xs">
+          <template v-if="$table.isEditByRow(row)">
+            <vxe-button @@click="saveRow(row, $table)" icon="vxe-icon-check"></vxe-button>
+            <vxe-button @@click="cancelEdit(row, $table)" icon="vxe-icon-close"> </vxe-button>
+          </template>
+          <template v-else>
+            <vxe-button @@click="$table.setEditRow(row)" color="info" icon="vxe-icon-edit"></vxe-button>
+            <vxe-button :disable="row.Id === 0 || row.Id == null" @@click="deleteRecord(row.Id)" icon="vxe-icon-delete"></vxe-button>
+          </template>
         </div>
+      </template>
+    </vxe-column>
+  </vxe-table>
+  <!-- 分页组件 -->
+  <vxe-pager :current-page="pageConfig.page" :page-size="pageConfig.size" :total="pageConfig.total" @@page-change="pageChange"/>
+</div>
+@if (ads.Count == 2) {
+  <div class="container-fluid">
+    <div class="margin-clear page-header">
+      <h2 class="size24" style="display: inline">
+        推广支持:
+      </h2>
     </div>
+    <div class="row">
+      <div class="col-md-6">
+        @{
+          await Html.RenderPartialAsync("_ArticleListAdvertisement", ads[0]);
+        }
+      </div>
+      <div class="col-md-6">
+        @{
+          await Html.RenderPartialAsync("_ArticleListAdvertisement", ads[1]);
+        }
+      </div>
+    </div>
+  </div>
 }
+<link href="https://cdn.jsdelivr.net/npm/[email protected]/lib/style.min.css" rel="stylesheet">
+<link href="https://cdn.jsdelivr.net/npm/[email protected]/lib/style.min.css" rel="stylesheet">
+<script src="https://unpkg.com/vue"></script>
+<!-- 引入vxe-table的JS文件 -->
+<script src="https://cdn.jsdelivr.net/npm/xe-utils/dist/xe-utils.umd.min.js"></script>
+<script src="https://cdn.jsdelivr.net/npm/[email protected]/lib/index.umd.min.js"></script>
+<script src="https://cdn.jsdelivr.net/npm/[email protected]/lib/index.umd.min.js"></script>
+<script src="https://cdn.jsdelivr.net/npm/echarts@6/dist/echarts.min.js" type="text/javascript"></script>
+<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
+<script src="https://cdn.jsdelivr.net/npm/dayjs/dayjs.min.js"></script>
 <script>
-    var app = angular.module('myApp', ["ngTable", "tm.pagination"]);
-    app.config(["$httpProvider", function ($httpProvider) {
-        $httpProvider.defaults.transformRequest = function (obj) {
-            var str = [];
-            for (var p in obj) {
-                if (obj.hasOwnProperty(p)) {
-                    str.push(window.encodeURIComponent(p) + "=" + window.encodeURIComponent(obj[p]));
-                }
-            }
-            return str.join("&");
-        };
+    VxeUI.formats.add('formatDate', {
+      cellFormatMethod:function (data, format) {
+        return dayjs(data.cellValue).format(format || 'YYYY-MM-DD')
+      }
+    })
+    const { createApp, ref, onMounted, watch, computed } = Vue;
+    createApp({
+        setup() {
+            // 表格数据
+            const tableData = ref([]);
+            const tableRef = ref(null)
+            const originalMap = ref(new Map())
 
-        $httpProvider.defaults.headers.post = {
-            'Content-Type': 'application/x-www-form-urlencoded; charser=UTF-8'
-        }
-    }]);
-    app.controller("home", ["$scope", "$http", "NgTableParams", function ($scope, $http, NgTableParams) {
-        var self = this;
-        $scope.paginationConf = {
-            currentPage: 1,
-            itemsPerPage: 10,
-            pagesLength: 15,
-            perPageOptions: [10, 15, 20, 30, 50, 100],
-            rememberPerPage: 'perPageItems',
-            onChange: function () {
-                window.loading();
-                $http.post("/donate/getpagedata", {
-                    page: $scope.paginationConf.currentPage,
-                    size: $scope.paginationConf.itemsPerPage
-                }).then(function (res) {
-                    $scope.paginationConf.totalItems = res.data.TotalCount;
-                    $("div[ng-table-pagination]").remove();
-                    self.tableParams = new NgTableParams({
-                        count: 50000
-                    }, {
-                        filterDelay: 0,
-                        dataset: res.data.Data
-                    });
-                    window.loadingDone();
-                });
-            }
-        };
-        self.del = function (row) {
-            swal({
-                title: "确认删除这条打赏记录吗?",
-                text: row.NickName,
-                showCancelButton: true,
-                confirmButtonColor: "#DD6B55",
-                confirmButtonText: "确定",
-                cancelButtonText: "取消",
-                showLoaderOnConfirm: true,
-                animation: true,
-                allowOutsideClick: false
-            }).then(function () {
-                $http.post("/donate/delete/"+row.Id).then(function (res) {
-                    window.notie.alert({
-                        type: 1,
-                        text: res.data.Message,
-                        time: 4
-                    });
-                    _.remove(self.tableParams.settings().dataset, function (item) {
-                        return row === item;
-                    });
-                    self.tableParams.reload().then(function (data) {
-                        if (data.length === 0 && self.tableParams.total() > 0) {
-                            self.tableParams.page(self.tableParams.page() - 1);
-                            self.tableParams.reload();
-                        }
-                    });
-                });
-            }, function () {
+            // 分页配置
+            const pageConfig = ref({
+                page: 1,
+                size: 10,
+                total: 0
             });
-        }
-        $scope.save = function (row) {
-            if (row == null) {
-                row = {
-                    NickName: "",
-                    DonateTime: "",
-                    Amount: "",
-                    Email: "",
-                    QQorWechat: "",
-                    Via: ""
-                };
+
+            // 加载状态
+            const loading = ref(false);
+            return {
+                tableData,
+                pageConfig,
+                loading,
+                originalMap,
+                tableRef
+            };
+        },
+        methods: {
+          pageChange({ pageSize, currentPage }) {
+              this.pageConfig.page = currentPage;
+              this.pageConfig.size = pageSize;
+              this.loadData();
+          },
+          async loadData(){
+            this.loading = true;
+            const data = await axios.post("/donate/getpagedata", {
+                page: this.pageConfig.page,
+                size: this.pageConfig.size
+            }).then(res=>res.data);
+            this.tableData = data.Data;
+            this.pageConfig.total = data.TotalCount;
+            this.loading = false;
+          },
+
+          async add(){
+              const $table = this.tableRef
+              if (!$table) return
+              // 防止已有编辑行
+              if ($table.getEditRecords && $table.getEditRecords().length > 0) {
+                  VxeUI.modal.notification({
+                      content: '请先保存或取消当前编辑行',
+                      status: 'error'
+                  });
+                  return
+              }
+              // 在首行插入
+              const record = {
+                  Id: 0,
+                  NickName: '',
+                  DonateTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
+                  Amount: '',
+                  Via: '',
+                  Email: '',
+                  QQorWechat: ''
+              }
+              const { row } = await $table.insertAt(record, 0)
+              $table.setEditRow(row)
+              this.originalMap.set(0, { ...record, Id: 0 })
+          },
+          
+          async saveRow(row, table){
+            if (!row.NickName || !row.Amount || !row.Via) {
+              VxeUI.modal.notification({
+                  content: '请填写必填字段:昵称/金额/打赏方式',
+                  status: 'warning'
+              });
+              return
             }
-            swal({
-                title: '添加打赏记录',
-                html:
-                    '<div class="input-group"><span class="input-group-addon">昵称: </span><input type="text" id="name" class="form-control input-lg" placeholder="请输入昵称" value="' + row.NickName + '"></div>' +
-                    '<div class="input-group"><span class="input-group-addon">打赏时间: </span><input id="date" type="text" class="form-control input-lg date datainp dateicon" readonly placeholder="请输入打赏时间" value="' + row.DonateTime + '"></div>	' +
-                    '<div class="input-group"><span class="input-group-addon">打赏金额: </span><input id="amount" type="text" class="form-control input-lg" placeholder="请输入金额" value="' + row.Amount + '"></div>' +
-                    '<div class="input-group"><span class="input-group-addon">打赏方式: </span><input id="via" type="text" class="form-control input-lg" placeholder="请输入打赏方式" value="' + row.Via + '"></div>' +
-                    '<div class="input-group"><span class="input-group-addon">Email: </span><input type="email" id="email" class="form-control input-lg" placeholder="请输入Email" value="' + row.Email + '"></div>' +
-                    '<div class="input-group"><span class="input-group-addon">QQ或微信: </span><input type="text" id="qq" class="form-control input-lg" placeholder="请输入QQ或微信" value="' + row.QQorWechat + '"></div>',
-                showCloseButton: true,
-                confirmButtonColor: "#DD6B55",
-                confirmButtonText: "确定",
-                cancelButtonText: "取消",
-                showLoaderOnConfirm: true,
-                animation: true,
-                allowOutsideClick: false,
-                preConfirm: function () {
-                    return new Promise(function (resolve, reject) {
-                        row.NickName = $("#name").val();
-                        row.DonateTime = $("#date").val();
-                        row.Amount = $("#amount").val();
-                        row.Via = $("#via").val();
-                        row.Email = $("#email").val();
-                        row.QQorWechat = $("#qq").val();
-                        $http.post("/donate/save", row).then(function (res) {
-                            if (res.data.Success) {
-                                resolve(res.data);
-                            } else {
-                                reject(res.data.Message);
-                            }
-                        }, function (error) {
-                            reject("服务请求失败!");
-                        });
-                    });
-                }
-            }).then(function (result) {
-                if (result) {
-                    if (result.Success) {
-                        swal(result.Message, "", "success");
-                        self.GetPageData($scope.paginationConf.currentPage, $scope.paginationConf.itemsPerPage);
-                    } else {
-                        swal(result.Message, "", "error");
-                    }
-                }
-            }).catch(swal.noop);
-            layui.use('laydate', function(){
-              var laydate = layui.laydate;
-              laydate.render({
-                elem: '.date',
-                calendar: true,
-                done: function(value, date, endDate) {
-                    $scope.partner.ExpireTime=value;
-                    $("#date").val(value);
-                }
+            const resp = await axios.post('/donate/save', row).then(res=>res.data)
+            if (resp?.Success) {
+              VxeUI.modal.notification({
+                  content: resp.Message || '保存成功',
+                  status: 'success'
               });
-            });
+              table.clearEdit()
+              await this.loadData()
+            } else {
+              VxeUI.modal.notification({
+                  content: resp?.Message || '保存失败',
+                  status: 'error'
+              });
+            }
+          },
+          cancelEdit(row, table){
+            if (row.Id == null) {
+              // 新增未保存直接移除
+              const idx = this.tableData.indexOf(row)
+              if (idx > -1) this.tableData.splice(idx, 1)
+            } else {
+              const origin = this.originalMap.get(row.Id)
+              if (origin) Object.assign(row, origin)
+            }
+            table.clearEdit()
+          },
+
+          async deleteRecord(id){
+            if (!id) return
+              const data = await axios.post(`/donate/delete/${id}`).then(res=>res.data)
+              if (data?.Success !== false) {
+                VxeUI.modal.notification({
+                    content: data?.Message || '删除成功',
+                    status: 'success'
+                });
+                await this.loadData()
+              } else {
+                VxeUI.modal.notification({
+                    content: data?.Message || '删除失败',
+                    status: 'error'
+                });
+              }
+          }
+        },
+        created() {
+            this.loadData();
         }
-    }]);
+    }).use(VxeUI).use(VXETable).mount('#donateApp');
+
 </script>

+ 89 - 101
src/Masuit.MyBlogs.Core/Views/Post/PostVisitRecordInsight.cshtml

@@ -12,116 +12,99 @@
     <meta content="webkit" name="renderer">
     <meta content="IE=edge,chrome=1" http-equiv="X-UA-Compatible">
     <meta content="width=device-width, initial-scale=1, maximum-scale=1" name="viewport">
-    <link href="/Assets/layui/css/layui.min.css" media="all" rel="stylesheet">
-    <style>
-        .mp-results.mp-bottomleft {
-            top: unset !important;
-            bottom: 0;
-        }
-    </style>
 </head>
-<body style="overflow-x: hidden">
+<body style="overflow-x: hidden" id="app">
     <h3 align="center">文章《@Model.Title》洞察分析</h3>
-    <div class="searchTable">
-        <div class="layui-inline">
-            <input class="layui-input" name="kw" id="kw">
-        </div>
-        <button class="layui-btn" data-type="reload">搜索</button>
-        <a class="layui-btn" asp-controller="Post" asp-action="ExportPostVisitRecords" asp-route-id="@Model.Id">导出</a>
+    <vxe-toolbar>
+        <template #tools>
+            <vxe-input @@search-click="loadData" placeholder="搜索" type="search" v-model="kw"></vxe-input>
+            <a asp-controller="Post" asp-action="ExportPostVisitRecords" asp-route-id="@Model.Id" class="theme--primary type--button vxe-button">导出</a>
+        </template>
+    </vxe-toolbar>
+    <!-- 主表格 -->
+    <vxe-table :data="tableData" :loading="loading" border class="limited-row-height" ref="tableRef" stripe>
+        <vxe-column field="IP" fixed="left" min-width="200" title="IP">
+            <template #default="{ row }">
+                <a :href="`/tools/ip/${row.IP}`" class="text-primary" target="_blank"> {{ row.IP }} </a>
+            </template>
+        </vxe-column>
+        <vxe-column field="RequestUrl" min-width="180" title="请求URL"></vxe-column>
+        <vxe-column field="Location" min-width="180" title="地理位置"></vxe-column>
+        <vxe-column field="Referer" min-width="250" title="请求来源"></vxe-column>
+        <vxe-column field="Time" min-width="250" title="访问时间"></vxe-column>
+    </vxe-table>
+    <!-- 分页组件 -->
+    <div class="">
+        <vxe-pager :current-page.sync="pageConfig.page" :page-size.sync="pageConfig.size" :total="pageConfig.total" @@page-change="pageChange">
+        </vxe-pager>
     </div>
-    <table class="layui-hide" id="table" lay-filter="tableEvent"></table>
-    <form class="layui-form">
-        <label class="layui-form-label">对比最近</label>
-        <div class="layui-input-inline">
-            <select id="period" name="period" lay-filter="period">
-                <option value="0">不对比</option>
-                <option value="7">一周</option>
-                <option value="15">15天</option>
-                <option value="30" selected="selected">一个月</option>
-                <option value="60">两个月</option>
-                <option value="90">三个月</option>
-                <option value="180">半年</option>
-            </select>
-        </div>
-    </form>
+    <vxe-toolbar>
+        <template #tools>
+            <span>对比最近:</span>
+            <vxe-select @@change="showCharts" v-model="period">
+                <vxe-option :key="num" :label="`${num}天`" :value="num" v-for="num in [7,15,30,60,90,180,365]"></vxe-option>
+            </vxe-select>
+        </template>
+    </vxe-toolbar>
     <div id="chart" style="height: 500px"></div>
     <mini-profiler max-traces="5" />
 </body>
 </html>
-<script src="/Assets/layui/layui.js"></script>
+<!-- 引入vxe-table样式 -->
+<link href="https://cdn.jsdelivr.net/npm/[email protected]/lib/style.min.css" rel="stylesheet">
+<link href="https://cdn.jsdelivr.net/npm/[email protected]/lib/style.min.css" rel="stylesheet">
+<script src="https://unpkg.com/vue"></script>
+<!-- 引入vxe-table的JS文件 -->
+<script src="https://cdn.jsdelivr.net/npm/xe-utils/dist/xe-utils.umd.min.js"></script>
+<script src="https://cdn.jsdelivr.net/npm/[email protected]/lib/index.umd.min.js"></script>
+<script src="https://cdn.jsdelivr.net/npm/[email protected]/lib/index.umd.min.js"></script>
+<script src="https://cdn.jsdelivr.net/npm/echarts@6/dist/echarts.min.js" type="text/javascript"></script>
+<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
 <script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js" type="text/javascript"></script>
 <script>
-    layui.use('table', function() {
-        var table = layui.table;
-        table.render({
-            elem: '#table',
-            url: '/@Model.Id/records',
-            cellMinWidth: 80, //全局定义常规单元格的最小宽度,layui 2.2.1 新增
-            cols: [
-                [
-                    { field: 'IP', title: 'IP', align: 'center', event: 'tool-ip', width:320 },
-                    { field: 'Location', title: '位置和网络', align: 'center'},
-                    { field: 'RequestUrl', title: '请求URL', align: 'center' },
-                    { field: 'Referer', title: '页面来源', align: 'center', event: 'visit' },
-                    { field: 'Time', title: '访问时间', align: 'center',width:180 }
-                ]
-            ],
-            page: true,
-            limit:20,
-            request: {
-                limitName: 'size' //每页数据量的参数名,默认:limit
-            },
-            parseData: function(res) { //res 即为原始返回的数据
-                return {
-                    "code": res.TotalCount > 0 ? 0 : 1, //解析接口状态
-                    "msg": "暂无数据", //解析提示文本
-                    "count": res.TotalCount, //解析数据长度
-                    "data": res.Data //解析数据列表
-                };
-            }
-        });
-        table.on('tool(tableEvent)', function(obj){
-            var data = obj.data;
-            if(obj.event === 'tool-ip'){
-                window.open("/tools/ip/"+data.IP);
-            }
-
-            if(obj.event === 'visit'){
-                window.open(data.Referer);
-            }
-        });
+    const { createApp, ref, onMounted, watch, computed } = Vue;
+    createApp({
+        setup() {
+            // 表格数据
+            const tableData = ref([]);
+            const kw = ref('');
+            const period = ref(30);
+            // 分页配置
+            const pageConfig = ref({
+                page: 1,
+                size: 10,
+                total: 0
+            });
 
-        var $ = layui.$;
-        $('.searchTable .layui-btn').on('click', function () {
-            table.reload('table', {
-                page: {
-                    curr: 1
-                },
-                where: {
-                    kw: $('#kw').val()
-                }
+            // 加载状态
+            const loading = ref(false);
+            return {
+              kw,
+                tableData,
+                pageConfig,
+                loading,
+                period
+            };
+        },
+        methods: {
+          pageChange({ pageSize, currentPage }) {
+            this.pageConfig.page = currentPage;
+            this.pageConfig.size = pageSize;
+            this.loadData();
+          },
+          async loadData(){
+            this.loading = true;
+            const data = await axios.get(`/@Model.Id/records?kw=${this.kw}&page=${this.pageConfig.page}&size=${this.pageConfig.size}`).then(function(response) {
+              return response.data;
             });
-        });
-    });
-    layui.use("form", function() {
-        var form = layui.form;
-        form.on("select(period)", function (data) {
-            var chartDom = document.getElementById('chart');
-            echarts.init(chartDom).dispose();
-            showCharts();
-        });
-    });
-    showCharts();
-    function showCharts() {
-        var period = document.getElementById("period").value;
-        window.fetch(`/@Model.Id/records-chart?compare=${period > 0}&period=${period}`, {
-            credentials: 'include',
-            method: 'GET',
-            mode: 'cors'
-        }).then(function (response) {
-            return response.json();
-        }).then(function (res) {
-            var xSeries = [];
+            this.tableData = data.Data;
+            this.pageConfig.total = data.TotalCount;
+            this.loading = false;
+          },
+          showCharts() {
+            axios.get(`/@Model.Id/records-chart?compare=${this.period > 0}&period=${this.period}`).then(function(response) {
+              const res = response.data;
+              var xSeries = [];
             var yCountSeries = [];
             var yUvSeries = [];
             for (let series of res) {
@@ -219,6 +202,11 @@
                 }))
             };
             myChart.setOption(option);
-        });
-    }
+            });
+          }
+        },
+    created() {
+        this.loadData();
+        this.showCharts();
+    }}).use(VxeUI).use(VXETable).mount('#app');
 </script>

+ 0 - 186
src/Masuit.MyBlogs.Core/wwwroot/Scripts/tm.pagination.js

@@ -1,186 +0,0 @@
-angular.module('tm.pagination', []).directive('tmPagination', [function () {
-	window.timeout = null;
-	return {
-		restrict: 'EA',
-		template: '<div class="page-list">' +
-			'<ul class="pagination" ng-show="conf.totalItems > 0">' +
-			'<li ng-class="{disabled: conf.currentPage == 1}" ng-click="prevPage()"><span>&laquo;</span></li>' +
-			'<li ng-repeat="item in pageList track by $index" ng-class="{active: item == conf.currentPage, separate: item == \'...\'}" ' +
-			'ng-click="changeCurrentPage(item)">' +
-			'<span>{{ item }}</span>' +
-			'</li>' +
-			'<li ng-class="{disabled: conf.currentPage == conf.numberOfPages}" ng-click="nextPage()"><span>&raquo;</span></li>' +
-			'</ul>' +
-			'<div class="page-total" ng-show="conf.totalItems > 0">' +
-			'第<input type="text" ng-model="jumpPageNum"  ng-keyup="jumpToPage($event)"/>页 / 共<strong>{{ totalPage }}</strong>页,' +
-			'每页<select ng-model="conf.itemsPerPage" ng-options="option for option in conf.perPageOptions " ng-change="changeItemsPerPage()"></select>' +
-			'条,合计<strong>{{ conf.totalItems }}</strong>条' +
-			'</div>' +
-			'<div class="no-items" ng-show="conf.totalItems <= 0">暂无数据</div>' +
-			'</div>',
-		replace: true,
-		scope: {
-			conf: '='
-		},
-		link: function (scope, element, attrs) {
-			// 变更当前页
-			scope.changeCurrentPage = function (item) {
-				if (item == '...') {
-					return;
-				} else {
-					scope.conf.currentPage = item;
-				}
-			};
-			// 定义分页的长度必须为奇数 (default:9)
-			scope.conf.pagesLength = parseInt(scope.conf.pagesLength) ? parseInt(scope.conf.pagesLength) : 9;
-			if (scope.conf.pagesLength % 2 === 0) {
-				// 如果不是奇数的时候处理一下
-				scope.conf.pagesLength = scope.conf.pagesLength - 1;
-			}
-			if (!scope.conf.perPageOptions) {
-				scope.conf.perPageOptions = [10, 15, 20, 30, 50];
-			}
-			// pageList数组
-			function getPagination() {
-			scope.totalPage = Math.ceil(scope.conf.totalItems / scope.conf.itemsPerPage);
-				// conf.currentPage
-				scope.conf.currentPage = parseInt(scope.conf.currentPage) ? parseInt(scope.conf.currentPage) : 1;
-				// conf.totalItems
-				scope.conf.totalItems = parseInt(scope.conf.totalItems);
-				// conf.itemsPerPage (default:15)
-				// 先判断一下本地存储中有没有这个值
-				if (scope.conf.rememberPerPage) {
-					if (!parseInt(localStorage[scope.conf.rememberPerPage])) {
-						localStorage[scope.conf.rememberPerPage] = parseInt(scope.conf.itemsPerPage) ? parseInt(scope.conf.itemsPerPage) : 15;
-					}
-					scope.conf.itemsPerPage = parseInt(localStorage[scope.conf.rememberPerPage]);
-				} else {
-					scope.conf.itemsPerPage = parseInt(scope.conf.itemsPerPage) ? parseInt(scope.conf.itemsPerPage) : 15;
-				}
-				// numberOfPages
-				scope.conf.numberOfPages = Math.ceil(scope.conf.totalItems / scope.conf.itemsPerPage);
-				// judge currentPage > scope.numberOfPages
-				if (scope.conf.currentPage < 1) {
-					scope.conf.currentPage = 1;
-				}
-				if (scope.conf.currentPage > scope.conf.numberOfPages) {
-					scope.conf.currentPage = scope.conf.numberOfPages;
-				}
-				// jumpPageNum
-				scope.jumpPageNum = scope.conf.currentPage;
-				// 如果itemsPerPage在不在perPageOptions数组中,就把itemsPerPage加入这个数组中
-				var perPageOptionsLength = scope.conf.perPageOptions.length;
-				// 定义状态
-				var perPageOptionsStatus;
-				for (var i = 0; i < perPageOptionsLength; i++) {
-					if (scope.conf.perPageOptions[i] == scope.conf.itemsPerPage) {
-						perPageOptionsStatus = true;
-					}
-				}
-				// 如果itemsPerPage在不在perPageOptions数组中,就把itemsPerPage加入这个数组中
-				if (!perPageOptionsStatus) {
-					scope.conf.perPageOptions.push(scope.conf.itemsPerPage);
-				}
-				// 对选项进行sort
-				scope.conf.perPageOptions.sort(function (a, b) { return a - b });
-				scope.pageList = [];
-				if (scope.conf.numberOfPages <= scope.conf.pagesLength) {
-					// 判断总页数如果小于等于分页的长度,若小于则直接显示
-					for (i = 1; i <= scope.conf.numberOfPages; i++) {
-						scope.pageList.push(i);
-					}
-				} else {
-					// 总页数大于分页长度(此时分为三种情况:1.左边没有...2.右边没有...3.左右都有...)
-					// 计算中心偏移量
-					var offset = (scope.conf.pagesLength - 1) / 2;
-					if (scope.conf.currentPage <= offset) {
-						// 左边没有...
-						for (i = 1; i <= offset + 1; i++) {
-							scope.pageList.push(i);
-						}
-						scope.pageList.push('...');
-						scope.pageList.push(scope.conf.numberOfPages);
-					} else if (scope.conf.currentPage > scope.conf.numberOfPages - offset) {
-						scope.pageList.push(1);
-						scope.pageList.push('...');
-						for (i = offset + 1; i >= 1; i--) {
-							scope.pageList.push(scope.conf.numberOfPages - i);
-						}
-						scope.pageList.push(scope.conf.numberOfPages);
-					} else {
-						// 最后一种情况,两边都有...
-						scope.pageList.push(1);
-						scope.pageList.push('...');
-
-						for (i = Math.ceil(offset / 2); i >= 1; i--) {
-							scope.pageList.push(scope.conf.currentPage - i);
-						}
-						scope.pageList.push(scope.conf.currentPage);
-						for (i = 1; i <= offset / 2; i++) {
-							scope.pageList.push(scope.conf.currentPage + i);
-						}
-
-						scope.pageList.push('...');
-						scope.pageList.push(scope.conf.numberOfPages);
-					}
-				}
-				if (scope.conf.onChange) {
-					if (window.timeout) {
-						clearTimeout(window.timeout);
-					}
-					window.timeout = setTimeout(function() {
-						if (scope.conf.currentPage > 0) {
-							scope.conf.onChange();
-							window.timeout = null;
-						}
-					}, 100);
-				}
-				scope.$parent.conf = scope.conf;
-			}
-			// prevPage
-			scope.prevPage = function () {
-				if (scope.conf.currentPage > 1) {
-					scope.conf.currentPage -= 1;
-				}
-			};
-			// nextPage
-			scope.nextPage = function () {
-				if (scope.conf.currentPage < scope.conf.numberOfPages) {
-					scope.conf.currentPage += 1;
-				}
-			};
-			// 跳转页
-			scope.jumpToPage = function () {
-				scope.jumpPageNum = scope.jumpPageNum.replace(/[^0-9]/g, '');
-				if (scope.jumpPageNum !== '') {
-					scope.conf.currentPage = scope.jumpPageNum;
-				}
-			};
-			// 修改每页显示的条数
-			scope.changeItemsPerPage = function () {
-				// 清除本地存储的值方便重新设置
-				if (scope.conf.rememberPerPage) {
-					localStorage.removeItem(scope.conf.rememberPerPage);
-				}
-			};
-			scope.$watch(function () {
-				var newValue = scope.conf.currentPage + ' ' + scope.conf.totalItems + ' ';
-				// 如果直接watch perPage变化的时候,因为记住功能的原因,所以一开始可能调用两次。
-				//所以用了如下方式处理
-				if (scope.conf.rememberPerPage) {
-					// 由于记住的时候需要特别处理一下,不然可能造成反复请求
-					// 之所以不监控localStorage[scope.conf.rememberPerPage]是因为在删除的时候会undefind
-					// 然后又一次请求
-					if (localStorage[scope.conf.rememberPerPage]) {
-						newValue += localStorage[scope.conf.rememberPerPage];
-					} else {
-						newValue += scope.conf.itemsPerPage;
-					}
-				} else {
-					newValue += scope.conf.itemsPerPage;
-				}
-				return newValue;
-			}, getPagination);
-		}
-	};
-}]);