Bläddra i källkod

✨ feat(policy-news): 集成富文本编辑器并优化统计功能

- 添加react-quill富文本编辑器依赖,支持政策资讯内容编辑
- 重构统计数据获取逻辑,使用useCallback优化性能
- 增加今日发布数量统计指标
- 完善错误处理机制,提升数据加载稳定性
- 优化表格排序和筛选功能,改进用户体验

🔧 chore(deps): 添加react-quill依赖

- 安装react-quill@^2.0.0及其类型定义
- 更新pnpm-lock.yaml文件,同步依赖版本信息
yourname 7 månader sedan
förälder
incheckning
81a1af5657
4 ändrade filer med 205 tillägg och 38 borttagningar
  1. 1 0
      package.json
  2. 148 0
      pnpm-lock.yaml
  3. 47 38
      src/client/admin/pages/PolicyNewsPage.tsx
  4. 9 0
      src/client/utils/minio.ts

+ 1 - 0
package.json

@@ -44,6 +44,7 @@
     "react-dom": "^19.1.0",
     "react-hook-form": "^7.57.0",
     "react-i18next": "^15.5.2",
+    "react-quill": "^2.0.0",
     "react-router": "^7.6.1",
     "react-router-dom": "^7.6.1",
     "react-toastify": "^11.0.5",

+ 148 - 0
pnpm-lock.yaml

@@ -110,6 +110,9 @@ importers:
       react-i18next:
         specifier: ^15.5.2
         version: 15.5.2(i18next@25.2.1(typescript@5.8.3))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.3)
+      react-quill:
+        specifier: ^2.0.0
+        version: 2.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
       react-router:
         specifier: ^7.6.1
         version: 7.6.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@@ -1551,6 +1554,9 @@ packages:
   '@types/parse-json@4.0.2':
     resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==}
 
+  '@types/quill@1.3.10':
+    resolution: {integrity: sha512-IhW3fPW+bkt9MLNlycw8u8fWb7oO7W5URC9MfZYHBlA24rex9rs23D5DETChu1zvgVdc5ka64ICjJOgQMr6Shw==}
+
   '@types/react-dom@19.1.6':
     resolution: {integrity: sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==}
     peerDependencies:
@@ -1735,6 +1741,10 @@ packages:
     resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
     engines: {node: '>=12'}
 
+  clone@2.1.2:
+    resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==}
+    engines: {node: '>=0.8'}
+
   clsx@2.1.1:
     resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
     engines: {node: '>=6'}
@@ -1945,10 +1955,18 @@ packages:
       babel-plugin-macros:
         optional: true
 
+  deep-equal@1.1.2:
+    resolution: {integrity: sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg==}
+    engines: {node: '>= 0.4'}
+
   define-data-property@1.1.4:
     resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==}
     engines: {node: '>= 0.4'}
 
+  define-properties@1.2.1:
+    resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==}
+    engines: {node: '>= 0.4'}
+
   defu@6.1.4:
     resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==}
 
@@ -2028,6 +2046,9 @@ packages:
     resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
     engines: {node: '>=10'}
 
+  eventemitter3@2.0.3:
+    resolution: {integrity: sha512-jLN68Dx5kyFHaePoXWPsCGW5qdyZQtLYHkxkg02/Mz6g0kYpDx4FyP6XfArhQdlOC4b8Mv+EMxPo/8La7Tzghg==}
+
   eventemitter3@5.0.1:
     resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==}
 
@@ -2038,12 +2059,18 @@ packages:
   exsolve@1.0.5:
     resolution: {integrity: sha512-pz5dvkYYKQ1AHVrgOzBKWeP4u4FRb3a6DNK2ucr0OoNwYIU4QWsJ+NM36LLzORT+z845MzKHHhpXiUF5nvQoJg==}
 
+  extend@3.0.2:
+    resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==}
+
   fast-decode-uri-component@1.0.1:
     resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==}
 
   fast-deep-equal@3.1.3:
     resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
 
+  fast-diff@1.1.2:
+    resolution: {integrity: sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig==}
+
   fast-querystring@1.1.2:
     resolution: {integrity: sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==}
 
@@ -2117,6 +2144,9 @@ packages:
   function-bind@1.1.2:
     resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
 
+  functions-have-names@1.2.3:
+    resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==}
+
   generate-function@2.3.1:
     resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==}
 
@@ -2245,6 +2275,10 @@ packages:
     resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==}
     engines: {node: '>= 0.4'}
 
+  is-date-object@1.1.0:
+    resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==}
+    engines: {node: '>= 0.4'}
+
   is-fullwidth-code-point@3.0.0:
     resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
     engines: {node: '>=8'}
@@ -2541,6 +2575,14 @@ packages:
   node-releases@2.0.19:
     resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==}
 
+  object-is@1.1.6:
+    resolution: {integrity: sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==}
+    engines: {node: '>= 0.4'}
+
+  object-keys@1.1.1:
+    resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==}
+    engines: {node: '>= 0.4'}
+
   ohash@2.0.11:
     resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==}
 
@@ -2566,6 +2608,9 @@ packages:
   package-json-from-dist@1.0.1:
     resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
 
+  parchment@1.1.4:
+    resolution: {integrity: sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg==}
+
   parent-module@1.0.1:
     resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
     engines: {node: '>=6'}
@@ -2626,6 +2671,13 @@ packages:
   quickselect@2.0.0:
     resolution: {integrity: sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==}
 
+  quill-delta@3.6.3:
+    resolution: {integrity: sha512-wdIGBlcX13tCHOXGMVnnTVFtGRLoP0imqxM696fIPwIf5ODIYUHIvHbZcyvGlZFiFhK5XzDC2lpjbxRhnM05Tg==}
+    engines: {node: '>=0.10'}
+
+  quill@1.3.7:
+    resolution: {integrity: sha512-hG/DVzh/TiknWtE6QmWAF/pxoZKYxfe3J/d/+ShUWkDvvkZQVTPeVmUJVu1uE6DDooC4fWTiCLh84ul89oNz5g==}
+
   rbush@3.0.1:
     resolution: {integrity: sha512-XRaVO0YecOpEuIvbhbpTrZgoiI6xBlz6hnlr6EHhd+0x9ase6EmeN+hdwwUaJvLcsFFQ8iWVF1GAK1yB0BWi0w==}
 
@@ -2890,6 +2942,12 @@ packages:
   react-is@18.3.1:
     resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
 
+  react-quill@2.0.0:
+    resolution: {integrity: sha512-4qQtv1FtCfLgoD3PXAur5RyxuUbPXQGOHgTlFie3jtxp43mXDtzCKaOgQ3mLyZfi1PUlyjycfivKelFhy13QUg==}
+    peerDependencies:
+      react: ^16 || ^17 || ^18
+      react-dom: ^16 || ^17 || ^18
+
   react-refresh@0.17.0:
     resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==}
     engines: {node: '>=0.10.0'}
@@ -2936,6 +2994,10 @@ packages:
   reflect-metadata@0.2.2:
     resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==}
 
+  regexp.prototype.flags@1.5.4:
+    resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==}
+    engines: {node: '>= 0.4'}
+
   require-directory@2.1.1:
     resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
     engines: {node: '>=0.10.0'}
@@ -2998,6 +3060,10 @@ packages:
     resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
     engines: {node: '>= 0.4'}
 
+  set-function-name@2.0.2:
+    resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==}
+    engines: {node: '>= 0.4'}
+
   sha.js@2.4.11:
     resolution: {integrity: sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==}
     hasBin: true
@@ -4684,6 +4750,10 @@ snapshots:
 
   '@types/parse-json@4.0.2': {}
 
+  '@types/quill@1.3.10':
+    dependencies:
+      parchment: 1.1.4
+
   '@types/react-dom@19.1.6(@types/react@19.1.8)':
     dependencies:
       '@types/react': 19.1.8
@@ -4923,6 +4993,8 @@ snapshots:
       strip-ansi: 6.0.1
       wrap-ansi: 7.0.0
 
+  clone@2.1.2: {}
+
   clsx@2.1.1: {}
 
   cluster-key-slot@1.1.2: {}
@@ -5108,12 +5180,27 @@ snapshots:
     optionalDependencies:
       babel-plugin-macros: 3.1.0
 
+  deep-equal@1.1.2:
+    dependencies:
+      is-arguments: 1.2.0
+      is-date-object: 1.1.0
+      is-regex: 1.2.1
+      object-is: 1.1.6
+      object-keys: 1.1.1
+      regexp.prototype.flags: 1.5.4
+
   define-data-property@1.1.4:
     dependencies:
       es-define-property: 1.0.1
       es-errors: 1.3.0
       gopd: 1.2.0
 
+  define-properties@1.2.1:
+    dependencies:
+      define-data-property: 1.1.4
+      has-property-descriptors: 1.0.2
+      object-keys: 1.1.1
+
   defu@6.1.4: {}
 
   delayed-stream@1.0.0: {}
@@ -5226,16 +5313,22 @@ snapshots:
 
   escape-string-regexp@4.0.0: {}
 
+  eventemitter3@2.0.3: {}
+
   eventemitter3@5.0.1: {}
 
   exit-hook@2.2.1: {}
 
   exsolve@1.0.5: {}
 
+  extend@3.0.2: {}
+
   fast-decode-uri-component@1.0.1: {}
 
   fast-deep-equal@3.1.3: {}
 
+  fast-diff@1.1.2: {}
+
   fast-querystring@1.1.2:
     dependencies:
       fast-decode-uri-component: 1.0.1
@@ -5295,6 +5388,8 @@ snapshots:
 
   function-bind@1.1.2: {}
 
+  functions-have-names@1.2.3: {}
+
   generate-function@2.3.1:
     dependencies:
       is-property: 1.0.2
@@ -5441,6 +5536,11 @@ snapshots:
     dependencies:
       hasown: 2.0.2
 
+  is-date-object@1.1.0:
+    dependencies:
+      call-bound: 1.0.4
+      has-tostringtag: 1.0.2
+
   is-fullwidth-code-point@3.0.0: {}
 
   is-generator-function@1.1.0:
@@ -5721,6 +5821,13 @@ snapshots:
 
   node-releases@2.0.19: {}
 
+  object-is@1.1.6:
+    dependencies:
+      call-bind: 1.0.8
+      define-properties: 1.2.1
+
+  object-keys@1.1.1: {}
+
   ohash@2.0.11: {}
 
   on-headers@1.1.0: {}
@@ -5736,6 +5843,8 @@ snapshots:
 
   package-json-from-dist@1.0.1: {}
 
+  parchment@1.1.4: {}
+
   parent-module@1.0.1:
     dependencies:
       callsites: 3.1.0
@@ -5789,6 +5898,21 @@ snapshots:
 
   quickselect@2.0.0: {}
 
+  quill-delta@3.6.3:
+    dependencies:
+      deep-equal: 1.1.2
+      extend: 3.0.2
+      fast-diff: 1.1.2
+
+  quill@1.3.7:
+    dependencies:
+      clone: 2.1.2
+      deep-equal: 1.1.2
+      eventemitter3: 2.0.3
+      extend: 3.0.2
+      parchment: 1.1.4
+      quill-delta: 3.6.3
+
   rbush@3.0.1:
     dependencies:
       quickselect: 2.0.0
@@ -6136,6 +6260,14 @@ snapshots:
 
   react-is@18.3.1: {}
 
+  react-quill@2.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
+    dependencies:
+      '@types/quill': 1.3.10
+      lodash: 4.17.21
+      quill: 1.3.7
+      react: 19.1.0
+      react-dom: 19.1.0(react@19.1.0)
+
   react-refresh@0.17.0: {}
 
   react-router-dom@7.6.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
@@ -6174,6 +6306,15 @@ snapshots:
 
   reflect-metadata@0.2.2: {}
 
+  regexp.prototype.flags@1.5.4:
+    dependencies:
+      call-bind: 1.0.8
+      define-properties: 1.2.1
+      es-errors: 1.3.0
+      get-proto: 1.0.1
+      gopd: 1.2.0
+      set-function-name: 2.0.2
+
   require-directory@2.1.1: {}
 
   resize-observer-polyfill@1.5.1: {}
@@ -6249,6 +6390,13 @@ snapshots:
       gopd: 1.2.0
       has-property-descriptors: 1.0.2
 
+  set-function-name@2.0.2:
+    dependencies:
+      define-data-property: 1.1.4
+      es-errors: 1.3.0
+      functions-have-names: 1.2.3
+      has-property-descriptors: 1.0.2
+
   sha.js@2.4.11:
     dependencies:
       inherits: 2.0.4

+ 47 - 38
src/client/admin/pages/PolicyNewsPage.tsx

@@ -1,4 +1,4 @@
-import React, { useState, useRef, useEffect } from 'react';
+import React, { useState, useRef, useEffect, useCallback } from 'react';
 import {
   Button,
   message,
@@ -29,7 +29,6 @@ import {
   StarFilled,
   UploadOutlined,
 } from '@ant-design/icons';
-import { useRequest } from 'ahooks';
 import dayjs from 'dayjs';
 import { policyNewsClient } from '@/client/api';
 import type { InferResponseType, InferRequestType } from 'hono/client';
@@ -58,6 +57,7 @@ const PolicyNewsPage: React.FC = () => {
   const [editingRecord, setEditingRecord] = useState<PolicyNewsItem | null>(null);
   const [content, setContent] = useState('');
   const [data, setData] = useState<PolicyNewsItem[]>([]);
+  const [stats, setStats] = useState({ total: 0, featured: 0, views: 0, today: 0 });
   const [pagination, setPagination] = useState({
     current: 1,
     pageSize: 10,
@@ -72,26 +72,31 @@ const PolicyNewsPage: React.FC = () => {
   const [form] = Form.useForm();
 
   // 获取统计数据
-  const { data: statsData } = useRequest(async () => {
-    const response = await policyNewsClient.$get({
-      query: { page: 1, pageSize: 1000 }
-    });
-    const result = await response.json();
-    return result;
-  });
-
-  // 计算统计数据
-  const stats = React.useMemo(() => {
-    if (!statsData?.data) return { total: 0, featured: 0, views: 0 };
-    return {
-      total: statsData.data.length,
-      featured: statsData.data.filter(item => item.isFeatured === 1).length,
-      views: statsData.data.reduce((sum, item) => sum + item.viewCount, 0)
-    };
-  }, [statsData]);
+  const fetchStats = useCallback(async () => {
+    try {
+      const response = await policyNewsClient.$get({
+        query: { page: 1, pageSize: 1000 }
+      });
+      const result = await response.json();
+      
+      const allData = result.data;
+      const today = allData.filter(
+        item => dayjs(item.createdAt).isSame(dayjs(), 'day')
+      ).length;
+      
+      setStats({
+        total: allData.length,
+        featured: allData.filter(item => item.isFeatured === 1).length,
+        views: allData.reduce((sum, item) => sum + item.viewCount, 0),
+        today,
+      });
+    } catch (error) {
+      console.error('获取统计数据失败:', error);
+    }
+  }, []);
 
   // 获取政策资讯列表
-  const fetchPolicyNews = async (params: any = {}) => {
+  const fetchPolicyNews = useCallback(async (params: any = {}) => {
     setLoading(true);
     try {
       const response = await policyNewsClient.$get({
@@ -120,22 +125,24 @@ const PolicyNewsPage: React.FC = () => {
     } finally {
       setLoading(false);
     }
-  };
+  }, [searchParams, pagination.current, pagination.pageSize]);
 
   useEffect(() => {
+    fetchStats();
     fetchPolicyNews();
+  }, [fetchStats, fetchPolicyNews]);
+
+  useEffect(() => {
+    fetchPolicyNews({ current: 1 });
   }, [searchParams]);
 
   // 处理表格分页、排序、筛选变化
   const handleTableChange = (newPagination: any, filters: any, sorter: any) => {
-    const params = {
+    fetchPolicyNews({
       current: newPagination.current,
       pageSize: newPagination.pageSize,
       ...filters,
-      sortField: sorter.field,
-      sortOrder: sorter.order,
-    };
-    fetchPolicyNews(params);
+    });
   };
 
   // 处理搜索
@@ -163,6 +170,7 @@ const PolicyNewsPage: React.FC = () => {
 
       if (response.ok) {
         message.success('删除成功');
+        fetchStats();
         fetchPolicyNews();
       } else {
         message.error('删除失败');
@@ -182,6 +190,7 @@ const PolicyNewsPage: React.FC = () => {
 
       if (response.ok) {
         message.success(record.isFeatured === 1 ? '取消精选成功' : '设为精选成功');
+        fetchStats();
         fetchPolicyNews();
       } else {
         message.error('操作失败');
@@ -219,6 +228,7 @@ const PolicyNewsPage: React.FC = () => {
         setEditingRecord(null);
         setContent('');
         form.resetFields();
+        fetchStats();
         fetchPolicyNews();
       } else {
         message.error(editingRecord ? '更新失败' : '创建失败');
@@ -261,6 +271,7 @@ const PolicyNewsPage: React.FC = () => {
     form.resetFields();
     form.setFieldsValue({
       publishTime: dayjs(),
+      images: [],
     });
     setModalVisible(true);
   };
@@ -380,7 +391,7 @@ const PolicyNewsPage: React.FC = () => {
       key: 'action',
       width: 150,
       fixed: 'right' as const,
-      render: (_, record: PolicyNewsItem) => (
+      render: (_: any, record: PolicyNewsItem) => (
         <Space>
           <Button
             type="link"
@@ -418,6 +429,14 @@ const PolicyNewsPage: React.FC = () => {
     },
   ];
 
+  // 自定义上传组件
+  const normFile = (e: any) => {
+    if (Array.isArray(e)) {
+      return e;
+    }
+    return e?.fileList;
+  };
+
   // 富文本编辑器配置
   const quillModules = {
     toolbar: [
@@ -438,14 +457,6 @@ const PolicyNewsPage: React.FC = () => {
     ],
   };
 
-  // 自定义上传组件
-  const normFile = (e: any) => {
-    if (Array.isArray(e)) {
-      return e;
-    }
-    return e?.fileList;
-  };
-
   return (
     <div>
       {/* 统计卡片 */}
@@ -481,9 +492,7 @@ const PolicyNewsPage: React.FC = () => {
           <Card>
             <Statistic
               title="今日发布"
-              value={statsData?.data?.filter(
-                item => dayjs(item.createdAt).isSame(dayjs(), 'day')
-              ).length || 0}
+              value={stats.today}
               valueStyle={{ color: '#722ed1' }}
             />
           </Card>

+ 9 - 0
src/client/utils/minio.ts

@@ -345,6 +345,15 @@ export async function getMultipartUploadPolicy(totalSize: number, fileKey: strin
   return await policyResponse.json();
 }
 
+export async function uploadFile(
+  file: File,
+  folder: string = 'uploads'
+): Promise<string> {
+  const fileKey = `${folder}/${Date.now()}-${file.name}`;
+  const result = await uploadMinIOWithPolicy('', file, fileKey);
+  return result.fileUrl;
+}
+
 export async function uploadMinIOWithPolicy(
   uploadPath: string,
   file: File | Blob,