From 0d73d0c63b183fcaf7d708aa20c9d32a4b1235f6 Mon Sep 17 00:00:00 2001
From: RUI <298977887@qq.com>
Date: Thu, 27 Nov 2025 22:43:24 +0800
Subject: [PATCH] 2025.11.27.22.40
---
.dockerignore | 9 +
.env.example | 4 +
.gitignore | 3 +-
Dockerfile | 51 +++
next.config.ts | 5 +
package.json | 3 +
pnpm-lock.yaml | 339 +++++++++++++++++-
src/components/admin/ArticleEditor.tsx | 48 ++-
src/components/home/HeroBanner.tsx | 13 +-
src/components/ui/checkbox.tsx | 30 ++
src/lib/auth.ts | 9 +-
src/models/index.ts | 30 +-
src/pages/admin/index.tsx | 40 ++-
src/pages/api/articles/[id].ts | 21 +-
src/pages/api/auth/login.ts | 5 +
src/pages/api/payment/notify.ts | 13 +-
src/pages/article/[id].tsx | 455 +++++++++++++++++--------
src/pages/auth/login.tsx | 124 +++++--
src/pages/auth/register.tsx | 116 +++++--
src/pages/index.tsx | 62 +++-
20 files changed, 1154 insertions(+), 226 deletions(-)
create mode 100644 .dockerignore
create mode 100644 .env.example
create mode 100644 Dockerfile
create mode 100644 src/components/ui/checkbox.tsx
diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..83892a0
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,9 @@
+Dockerfile
+.dockerignore
+node_modules
+npm-debug.log
+README.md
+.next
+.git
+.env*
+! .env.example
diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..8a6d619
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,4 @@
+# MONGODB数据库
+MONGODB_URI
+# JWT配置,用于生成token
+JWT_SECRET=
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 5ef6a52..b813b75 100644
--- a/.gitignore
+++ b/.gitignore
@@ -31,7 +31,8 @@ yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
-.env*
+# .env*
+.env.local
# vercel
.vercel
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..ad7a93e
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,51 @@
+# 🚀 优化版 Dockerfile for Next.js with pnpm (目标: <200MB)
+# 第一阶段:安装依赖
+FROM node:22-alpine AS deps
+WORKDIR /app
+# 查看 https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine 以了解为什么可能需要 libc6-compat
+RUN apk add --no-cache libc6-compat
+COPY package.json pnpm-lock.yaml ./
+RUN npm install -g pnpm && pnpm i --frozen-lockfile
+
+# 第二阶段:构建应用
+FROM node:22-alpine AS builder
+WORKDIR /app
+COPY --from=deps /app/node_modules ./node_modules
+COPY . .
+# 在构建期间禁用遥测
+ENV NEXT_TELEMETRY_DISABLED=1
+RUN npm install -g pnpm && pnpm run build
+
+# 第三阶段:生产镜像,复制所有文件并运行 next
+FROM node:22-alpine AS runner
+WORKDIR /app
+
+ENV NODE_ENV=production
+# 在运行时禁用遥测
+ENV NEXT_TELEMETRY_DISABLED=1
+
+RUN addgroup --system --gid 1001 nodejs
+RUN adduser --system --uid 1001 nextjs
+
+# 复制 public 文件夹
+COPY --from=builder /app/public ./public
+
+# 自动利用输出追踪来减小镜像大小
+# https://nextjs.org/docs/advanced-features/output-file-tracing
+COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
+COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
+
+USER nextjs
+
+EXPOSE 3000
+
+ENV PORT=3000
+# 设置主机名为 0.0.0.0 以便外部访问
+ENV HOSTNAME="0.0.0.0"
+
+CMD ["node", "server.js"]PORT=3000 \
+ HOSTNAME="0.0.0.0" \
+ NODE_ENV=production
+
+# 启动应用 (standalone模式直接运行server.js)
+CMD ["node", "server.js"]
\ No newline at end of file
diff --git a/next.config.ts b/next.config.ts
index dedb5d5..35bc5f4 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -2,6 +2,7 @@ import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
+ output: 'standalone',
reactStrictMode: true,
images: {
remotePatterns: [
@@ -13,6 +14,10 @@ const nextConfig: NextConfig = {
protocol: 'https',
hostname: 'api.dicebear.com',
},
+ {
+ protocol: 'https',
+ hostname: 'images.unsplash.com',
+ },
],
},
};
diff --git a/package.json b/package.json
index c3d6542..2b77b03 100644
--- a/package.json
+++ b/package.json
@@ -11,6 +11,7 @@
"dependencies": {
"@hookform/resolvers": "^5.2.2",
"@radix-ui/react-avatar": "^1.1.11",
+ "@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.8",
@@ -43,12 +44,14 @@
"react-dom": "19.2.0",
"react-hook-form": "^7.66.1",
"react-markdown": "^10.1.0",
+ "recharts": "^3.5.0",
"rehype-pretty-code": "^0.14.1",
"rehype-raw": "^7.0.0",
"rehype-sanitize": "^6.0.0",
"rehype-stringify": "^10.0.1",
"remark-parse": "^11.0.0",
"remark-rehype": "^11.1.2",
+ "sharp": "^0.34.5",
"shiki": "^3.15.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.4.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index e5cd994..e91e68d 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -14,6 +14,9 @@ importers:
'@radix-ui/react-avatar':
specifier: ^1.1.11
version: 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ '@radix-ui/react-checkbox':
+ specifier: ^1.3.3
+ version: 1.3.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@radix-ui/react-dialog':
specifier: ^1.1.15
version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
@@ -110,6 +113,9 @@ importers:
react-markdown:
specifier: ^10.1.0
version: 10.1.0(@types/react@19.2.7)(react@19.2.0)
+ recharts:
+ specifier: ^3.5.0
+ version: 3.5.0(@types/react@19.2.7)(eslint@9.39.1(jiti@2.6.1))(react-dom@19.2.0(react@19.2.0))(react-is@16.13.1)(react@19.2.0)(redux@5.0.1)
rehype-pretty-code:
specifier: ^0.14.1
version: 0.14.1(shiki@3.15.0)
@@ -128,6 +134,9 @@ importers:
remark-rehype:
specifier: ^11.1.2
version: 11.1.2
+ sharp:
+ specifier: ^0.34.5
+ version: 0.34.5
shiki:
specifier: ^3.15.0
version: 3.15.0
@@ -596,6 +605,19 @@ packages:
'@types/react-dom':
optional: true
+ '@radix-ui/react-checkbox@1.3.3':
+ resolution: {integrity: sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
'@radix-ui/react-collection@1.1.7':
resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==}
peerDependencies:
@@ -986,6 +1008,17 @@ packages:
'@radix-ui/rect@1.1.1':
resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==}
+ '@reduxjs/toolkit@2.11.0':
+ resolution: {integrity: sha512-hBjYg0aaRL1O2Z0IqWhnTLytnjDIxekmRxm1snsHjHaKVmIF1HiImWqsq+PuEbn6zdMlkIj9WofK1vR8jjx+Xw==}
+ peerDependencies:
+ react: ^16.9.0 || ^17.0.0 || ^18 || ^19
+ react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0
+ peerDependenciesMeta:
+ react:
+ optional: true
+ react-redux:
+ optional: true
+
'@remirror/core-constants@3.0.0':
resolution: {integrity: sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==}
@@ -1013,6 +1046,9 @@ packages:
'@shikijs/vscode-textmate@10.0.2':
resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==}
+ '@standard-schema/spec@1.0.0':
+ resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==}
+
'@standard-schema/utils@0.3.0':
resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==}
@@ -1274,6 +1310,33 @@ packages:
'@tybys/wasm-util@0.10.1':
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
+ '@types/d3-array@3.2.2':
+ resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==}
+
+ '@types/d3-color@3.1.3':
+ resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==}
+
+ '@types/d3-ease@3.0.2':
+ resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==}
+
+ '@types/d3-interpolate@3.0.4':
+ resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==}
+
+ '@types/d3-path@3.1.1':
+ resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==}
+
+ '@types/d3-scale@4.0.9':
+ resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==}
+
+ '@types/d3-shape@3.1.7':
+ resolution: {integrity: sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==}
+
+ '@types/d3-time@3.0.4':
+ resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==}
+
+ '@types/d3-timer@3.0.2':
+ resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==}
+
'@types/debug@4.1.12':
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
@@ -1726,6 +1789,50 @@ packages:
csstype@3.2.3:
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
+ d3-array@3.2.4:
+ resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==}
+ engines: {node: '>=12'}
+
+ d3-color@3.1.0:
+ resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==}
+ engines: {node: '>=12'}
+
+ d3-ease@3.0.1:
+ resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==}
+ engines: {node: '>=12'}
+
+ d3-format@3.1.0:
+ resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==}
+ engines: {node: '>=12'}
+
+ d3-interpolate@3.0.1:
+ resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==}
+ engines: {node: '>=12'}
+
+ d3-path@3.1.0:
+ resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==}
+ engines: {node: '>=12'}
+
+ d3-scale@4.0.2:
+ resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==}
+ engines: {node: '>=12'}
+
+ d3-shape@3.2.0:
+ resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==}
+ engines: {node: '>=12'}
+
+ d3-time-format@4.1.0:
+ resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==}
+ engines: {node: '>=12'}
+
+ d3-time@3.1.0:
+ resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==}
+ engines: {node: '>=12'}
+
+ d3-timer@3.0.1:
+ resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==}
+ engines: {node: '>=12'}
+
damerau-levenshtein@1.0.8:
resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==}
@@ -1761,6 +1868,9 @@ packages:
supports-color:
optional: true
+ decimal.js-light@2.5.1:
+ resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==}
+
decode-named-character-reference@1.2.0:
resolution: {integrity: sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==}
@@ -1867,6 +1977,9 @@ packages:
resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==}
engines: {node: '>= 0.4'}
+ es-toolkit@1.42.0:
+ resolution: {integrity: sha512-SLHIyY7VfDJBM8clz4+T2oquwTQxEzu263AyhVK4jREOAwJ+8eebaa4wM3nlvnAqhDrMm2EsA6hWHaQsMPQ1nA==}
+
escalade@3.2.0:
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
engines: {node: '>=6'}
@@ -1947,6 +2060,12 @@ packages:
peerDependencies:
eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0
+ eslint-plugin-react-perf@3.3.3:
+ resolution: {integrity: sha512-EzPdxsRJg5IllCAH9ny/3nK7sv9251tvKmi/d3Ouv5KzI8TB3zNhzScxL9wnh9Hvv8GYC5LEtzTauynfOEYiAw==}
+ engines: {node: '>=6.9.1'}
+ peerDependencies:
+ eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0
+
eslint-plugin-react@7.37.5:
resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==}
engines: {node: '>=4'}
@@ -1998,6 +2117,9 @@ packages:
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
engines: {node: '>=0.10.0'}
+ eventemitter3@5.0.1:
+ resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==}
+
eventsource-parser@3.0.6:
resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==}
engines: {node: '>=18.0.0'}
@@ -2221,6 +2343,12 @@ packages:
resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
engines: {node: '>= 4'}
+ immer@10.2.0:
+ resolution: {integrity: sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==}
+
+ immer@11.0.0:
+ resolution: {integrity: sha512-XtRG4SINt4dpqlnJvs70O2j6hH7H0X8fUzFsjMn1rwnETaxwp83HLNimXBjZ78MrKl3/d3/pkzDH0o0Lkxm37Q==}
+
import-fresh@3.3.1:
resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
engines: {node: '>=6'}
@@ -2236,6 +2364,10 @@ packages:
resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
engines: {node: '>= 0.4'}
+ internmap@2.0.3:
+ resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
+ engines: {node: '>=12'}
+
is-alphabetical@2.0.1:
resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==}
@@ -3028,6 +3160,18 @@ packages:
'@types/react': '>=18'
react: '>=18'
+ react-redux@9.2.0:
+ resolution: {integrity: sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==}
+ peerDependencies:
+ '@types/react': ^18.2.25 || ^19
+ react: ^18.0 || ^19
+ redux: ^5.0.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ redux:
+ optional: true
+
react-remove-scroll-bar@2.3.8:
resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==}
engines: {node: '>=10'}
@@ -3062,6 +3206,22 @@ packages:
resolution: {integrity: sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==}
engines: {node: '>=0.10.0'}
+ recharts@3.5.0:
+ resolution: {integrity: sha512-jWqBtu8L3VICXWa3g/y+bKjL8DDHSRme7DHD/70LQ/Tk0di1h11Y0kKC0nPh6YJ2oaa0k6anIFNhg6SfzHWdEA==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ react-is: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+
+ redux-thunk@3.1.0:
+ resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==}
+ peerDependencies:
+ redux: ^5.0.0
+
+ redux@5.0.1:
+ resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==}
+
reflect.getprototypeof@1.0.10:
resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==}
engines: {node: '>= 0.4'}
@@ -3143,6 +3303,9 @@ packages:
remark-stringify@11.0.0:
resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==}
+ reselect@5.1.1:
+ resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==}
+
resolve-from@4.0.0:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
engines: {node: '>=4'}
@@ -3336,6 +3499,9 @@ packages:
resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==}
engines: {node: '>=6'}
+ tiny-invariant@1.3.3:
+ resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
+
tinyglobby@0.2.15:
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
engines: {node: '>=12.0.0'}
@@ -3483,6 +3649,9 @@ packages:
vfile@6.0.3:
resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==}
+ victory-vendor@37.3.6:
+ resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==}
+
w3c-keyname@2.2.8:
resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
@@ -3742,8 +3911,7 @@ snapshots:
'@humanwhocodes/retry@0.4.3': {}
- '@img/colour@1.0.0':
- optional: true
+ '@img/colour@1.0.0': {}
'@img/sharp-darwin-arm64@0.34.5':
optionalDependencies:
@@ -3939,6 +4107,22 @@ snapshots:
'@types/react': 19.2.7
'@types/react-dom': 19.2.3(@types/react@19.2.7)
+ '@radix-ui/react-checkbox@1.3.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.0)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.0)
+ '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.7)(react@19.2.0)
+ '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.7)(react@19.2.0)
+ '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.7)(react@19.2.0)
+ react: 19.2.0
+ react-dom: 19.2.0(react@19.2.0)
+ optionalDependencies:
+ '@types/react': 19.2.7
+ '@types/react-dom': 19.2.3(@types/react@19.2.7)
+
'@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.0)
@@ -4326,6 +4510,18 @@ snapshots:
'@radix-ui/rect@1.1.1': {}
+ '@reduxjs/toolkit@2.11.0(react-redux@9.2.0(@types/react@19.2.7)(react@19.2.0)(redux@5.0.1))(react@19.2.0)':
+ dependencies:
+ '@standard-schema/spec': 1.0.0
+ '@standard-schema/utils': 0.3.0
+ immer: 11.0.0
+ redux: 5.0.1
+ redux-thunk: 3.1.0(redux@5.0.1)
+ reselect: 5.1.1
+ optionalDependencies:
+ react: 19.2.0
+ react-redux: 9.2.0(@types/react@19.2.7)(react@19.2.0)(redux@5.0.1)
+
'@remirror/core-constants@3.0.0': {}
'@rtsao/scc@1.1.0': {}
@@ -4363,6 +4559,8 @@ snapshots:
'@shikijs/vscode-textmate@10.0.2': {}
+ '@standard-schema/spec@1.0.0': {}
+
'@standard-schema/utils@0.3.0': {}
'@swc/helpers@0.5.15':
@@ -4631,6 +4829,30 @@ snapshots:
tslib: 2.8.1
optional: true
+ '@types/d3-array@3.2.2': {}
+
+ '@types/d3-color@3.1.3': {}
+
+ '@types/d3-ease@3.0.2': {}
+
+ '@types/d3-interpolate@3.0.4':
+ dependencies:
+ '@types/d3-color': 3.1.3
+
+ '@types/d3-path@3.1.1': {}
+
+ '@types/d3-scale@4.0.9':
+ dependencies:
+ '@types/d3-time': 3.0.4
+
+ '@types/d3-shape@3.1.7':
+ dependencies:
+ '@types/d3-path': 3.1.1
+
+ '@types/d3-time@3.0.4': {}
+
+ '@types/d3-timer@3.0.2': {}
+
'@types/debug@4.1.12':
dependencies:
'@types/ms': 2.1.0
@@ -5107,6 +5329,44 @@ snapshots:
csstype@3.2.3: {}
+ d3-array@3.2.4:
+ dependencies:
+ internmap: 2.0.3
+
+ d3-color@3.1.0: {}
+
+ d3-ease@3.0.1: {}
+
+ d3-format@3.1.0: {}
+
+ d3-interpolate@3.0.1:
+ dependencies:
+ d3-color: 3.1.0
+
+ d3-path@3.1.0: {}
+
+ d3-scale@4.0.2:
+ dependencies:
+ d3-array: 3.2.4
+ d3-format: 3.1.0
+ d3-interpolate: 3.0.1
+ d3-time: 3.1.0
+ d3-time-format: 4.1.0
+
+ d3-shape@3.2.0:
+ dependencies:
+ d3-path: 3.1.0
+
+ d3-time-format@4.1.0:
+ dependencies:
+ d3-time: 3.1.0
+
+ d3-time@3.1.0:
+ dependencies:
+ d3-array: 3.2.4
+
+ d3-timer@3.0.1: {}
+
damerau-levenshtein@1.0.8: {}
data-view-buffer@1.0.2:
@@ -5137,6 +5397,8 @@ snapshots:
dependencies:
ms: 2.1.3
+ decimal.js-light@2.5.1: {}
+
decode-named-character-reference@1.2.0:
dependencies:
character-entities: 2.0.2
@@ -5307,6 +5569,8 @@ snapshots:
is-date-object: 1.1.0
is-symbol: 1.1.1
+ es-toolkit@1.42.0: {}
+
escalade@3.2.0: {}
escape-string-regexp@4.0.0: {}
@@ -5426,6 +5690,10 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ eslint-plugin-react-perf@3.3.3(eslint@9.39.1(jiti@2.6.1)):
+ dependencies:
+ eslint: 9.39.1(jiti@2.6.1)
+
eslint-plugin-react@7.37.5(eslint@9.39.1(jiti@2.6.1)):
dependencies:
array-includes: 3.1.9
@@ -5518,6 +5786,8 @@ snapshots:
esutils@2.0.3: {}
+ eventemitter3@5.0.1: {}
+
eventsource-parser@3.0.6: {}
extend@3.0.2: {}
@@ -5824,6 +6094,10 @@ snapshots:
ignore@7.0.5: {}
+ immer@10.2.0: {}
+
+ immer@11.0.0: {}
+
import-fresh@3.3.1:
dependencies:
parent-module: 1.0.1
@@ -5839,6 +6113,8 @@ snapshots:
hasown: 2.0.2
side-channel: 1.1.0
+ internmap@2.0.3: {}
+
is-alphabetical@2.0.1: {}
is-alphanumerical@2.0.1:
@@ -6887,6 +7163,15 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ react-redux@9.2.0(@types/react@19.2.7)(react@19.2.0)(redux@5.0.1):
+ dependencies:
+ '@types/use-sync-external-store': 0.0.6
+ react: 19.2.0
+ use-sync-external-store: 1.6.0(react@19.2.0)
+ optionalDependencies:
+ '@types/react': 19.2.7
+ redux: 5.0.1
+
react-remove-scroll-bar@2.3.8(@types/react@19.2.7)(react@19.2.0):
dependencies:
react: 19.2.0
@@ -6916,6 +7201,34 @@ snapshots:
react@19.2.0: {}
+ recharts@3.5.0(@types/react@19.2.7)(eslint@9.39.1(jiti@2.6.1))(react-dom@19.2.0(react@19.2.0))(react-is@16.13.1)(react@19.2.0)(redux@5.0.1):
+ dependencies:
+ '@reduxjs/toolkit': 2.11.0(react-redux@9.2.0(@types/react@19.2.7)(react@19.2.0)(redux@5.0.1))(react@19.2.0)
+ clsx: 2.1.1
+ decimal.js-light: 2.5.1
+ es-toolkit: 1.42.0
+ eslint-plugin-react-perf: 3.3.3(eslint@9.39.1(jiti@2.6.1))
+ eventemitter3: 5.0.1
+ immer: 10.2.0
+ react: 19.2.0
+ react-dom: 19.2.0(react@19.2.0)
+ react-is: 16.13.1
+ react-redux: 9.2.0(@types/react@19.2.7)(react@19.2.0)(redux@5.0.1)
+ reselect: 5.1.1
+ tiny-invariant: 1.3.3
+ use-sync-external-store: 1.6.0(react@19.2.0)
+ victory-vendor: 37.3.6
+ transitivePeerDependencies:
+ - '@types/react'
+ - eslint
+ - redux
+
+ redux-thunk@3.1.0(redux@5.0.1):
+ dependencies:
+ redux: 5.0.1
+
+ redux@5.0.1: {}
+
reflect.getprototypeof@1.0.10:
dependencies:
call-bind: 1.0.8
@@ -7083,6 +7396,8 @@ snapshots:
mdast-util-to-markdown: 2.1.2
unified: 11.0.5
+ reselect@5.1.1: {}
+
resolve-from@4.0.0: {}
resolve-pkg-maps@1.0.0: {}
@@ -7186,7 +7501,6 @@ snapshots:
'@img/sharp-win32-arm64': 0.34.5
'@img/sharp-win32-ia32': 0.34.5
'@img/sharp-win32-x64': 0.34.5
- optional: true
shebang-command@2.0.0:
dependencies:
@@ -7341,6 +7655,8 @@ snapshots:
tapable@2.3.0: {}
+ tiny-invariant@1.3.3: {}
+
tinyglobby@0.2.15:
dependencies:
fdir: 6.5.0(picomatch@4.0.3)
@@ -7549,6 +7865,23 @@ snapshots:
'@types/unist': 3.0.3
vfile-message: 4.0.3
+ victory-vendor@37.3.6:
+ dependencies:
+ '@types/d3-array': 3.2.2
+ '@types/d3-ease': 3.0.2
+ '@types/d3-interpolate': 3.0.4
+ '@types/d3-scale': 4.0.9
+ '@types/d3-shape': 3.1.7
+ '@types/d3-time': 3.0.4
+ '@types/d3-timer': 3.0.2
+ d3-array: 3.2.4
+ d3-ease: 3.0.1
+ d3-interpolate: 3.0.1
+ d3-scale: 4.0.2
+ d3-shape: 3.2.0
+ d3-time: 3.1.0
+ d3-timer: 3.0.1
+
w3c-keyname@2.2.8: {}
web-namespaces@2.0.1: {}
diff --git a/src/components/admin/ArticleEditor.tsx b/src/components/admin/ArticleEditor.tsx
index f20b098..887234b 100644
--- a/src/components/admin/ArticleEditor.tsx
+++ b/src/components/admin/ArticleEditor.tsx
@@ -12,8 +12,8 @@ import {
SelectValue,
} from '@/components/ui/select';
import { Switch } from '@/components/ui/switch';
-import { Loader2, Save, ArrowLeft, Globe, Lock, Image as ImageIcon, FileText, DollarSign, Settings } from 'lucide-react';
-import { useForm, Controller } from 'react-hook-form';
+import { Loader2, Save, ArrowLeft, Globe, Lock, Image as ImageIcon, FileText, DollarSign, Settings, Plus, Trash2 } from 'lucide-react';
+import { useForm, Controller, useFieldArray } from 'react-hook-form';
import TiptapEditor from './TiptapEditor';
import { toast } from 'sonner';
@@ -57,12 +57,20 @@ export default function ArticleEditor({ mode, articleId }: ArticleEditorProps) {
下载链接: '',
提取码: '',
解压密码: '',
- 隐藏内容: ''
+ 隐藏内容: '',
+ 版本号: '',
+ 文件大小: '',
+ 扩展属性: [] as { 属性名: string; 属性值: string }[]
},
发布状态: 'published'
}
});
+ const { fields: extendedFields, append: appendExtended, remove: removeExtended } = useFieldArray({
+ control,
+ name: "资源属性.扩展属性"
+ });
+
useEffect(() => {
fetchDependencies();
if (mode === 'edit' && articleId) {
@@ -296,6 +304,16 @@ export default function ArticleEditor({ mode, articleId }: ArticleEditorProps) {
资源交付
+
+
+
+
+
+ {/* Extended Attributes */}
+
@@ -334,6 +375,7 @@ export default function ArticleEditor({ mode, articleId }: ArticleEditorProps) {
积分支付
现金支付 (CNY)
+ 会员免费
)}
diff --git a/src/components/home/HeroBanner.tsx b/src/components/home/HeroBanner.tsx
index 6c47237..bc61517 100644
--- a/src/components/home/HeroBanner.tsx
+++ b/src/components/home/HeroBanner.tsx
@@ -1,6 +1,7 @@
import React, { useState, useEffect, useCallback } from 'react';
import { Button } from '@/components/ui/button';
import Link from 'next/link';
+import Image from 'next/image';
import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious, type CarouselApi } from "@/components/ui/carousel";
import { cn } from "@/lib/utils";
@@ -62,10 +63,14 @@ export default function HeroBanner({ banners }: HeroBannerProps) {
{/* Background Image */}
-
+
+
{/* Overlay */}
{/* Gradient Overlay */}
diff --git a/src/components/ui/checkbox.tsx b/src/components/ui/checkbox.tsx
new file mode 100644
index 0000000..ab9df48
--- /dev/null
+++ b/src/components/ui/checkbox.tsx
@@ -0,0 +1,30 @@
+"use client"
+
+import * as React from "react"
+import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
+import { Check } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const Checkbox = React.forwardRef<
+ React.ElementRef
,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+
+))
+Checkbox.displayName = CheckboxPrimitive.Root.displayName
+
+export { Checkbox }
diff --git a/src/lib/auth.ts b/src/lib/auth.ts
index d55c217..31afd9c 100644
--- a/src/lib/auth.ts
+++ b/src/lib/auth.ts
@@ -2,9 +2,6 @@ import jwt from 'jsonwebtoken';
import { NextApiRequest, NextApiResponse } from 'next';
const JWT_SECRET = process.env.JWT_SECRET;
-if (!JWT_SECRET) {
- throw new Error('Please define the JWT_SECRET environment variable inside .env.local');
-}
export interface DecodedToken {
userId: string;
@@ -15,8 +12,12 @@ export interface DecodedToken {
}
export function verifyToken(token: string): DecodedToken | null {
+ if (!JWT_SECRET) {
+ console.error('JWT_SECRET is not defined');
+ return null;
+ }
try {
- return jwt.verify(token, JWT_SECRET as string) as unknown as DecodedToken;
+ return jwt.verify(token, JWT_SECRET) as unknown as DecodedToken;
} catch (error) {
return null;
}
diff --git a/src/models/index.ts b/src/models/index.ts
index 1e6558a..a761b04 100644
--- a/src/models/index.ts
+++ b/src/models/index.ts
@@ -43,6 +43,10 @@ const UserSchema = new Schema({
* ==================================================================
* 2. Article (文章/资源模型)
* ==================================================================
+ * 核心更新:
+ * - 支付方式增加了 'membership_free' (会员免费)。
+ * - 资源属性增加了 版本、大小、扩展属性。
+ * - 统计数据增加了 销量、最近售出时间。
*/
const ArticleSchema = new Schema({
// --- 基础信息 ---
@@ -52,7 +56,7 @@ const ArticleSchema = new Schema({
// --- 内容部分 ---
摘要: { type: String },
- 正文内容: { type: String, required: true },
+ 正文内容: { type: String, required: true }, // 公开预览内容 或 包含截断标识的完整内容
// --- SEO 专用优化 ---
SEO关键词: { type: [String], default: [] },
@@ -65,14 +69,28 @@ const ArticleSchema = new Schema({
// --- 售卖策略 ---
价格: { type: Number, default: 0 },
- 支付方式: { type: String, enum: ['points', 'cash'], default: 'points' },
+ // 【升级】新增 membership_free:会员免费,普通用户需购买
+ 支付方式: { type: String, enum: ['points', 'cash', 'membership_free'], default: 'points' },
// --- 资源交付 (付费后可见) ---
资源属性: {
下载链接: { type: String },
提取码: { type: String },
解压密码: { type: String },
- 隐藏内容: { type: String }
+
+ // 【升级】标准资源字段
+ 版本号: { type: String }, // e.g., "v2.0.1"
+ 文件大小: { type: String }, // e.g., "1.5GB"
+
+ // 【升级】扩展属性:支持自定义键值对,如 [{ 属性名: "运行环境", 属性值: "Win11" }]
+ 扩展属性: [{
+ 属性名: { type: String },
+ 属性值: { type: String }
+ }],
+
+ // 【升级】内容隐藏逻辑
+ // 直接存在这里,和正文完全分离(推荐,更安全)
+ 隐藏内容: { type: String },
},
// --- 数据统计 ---
@@ -81,7 +99,10 @@ const ArticleSchema = new Schema({
点赞数: { type: Number, default: 0 },
评论数: { type: Number, default: 0 },
收藏数: { type: Number, default: 0 },
- 分享数: { type: Number, default: 0 }
+ 分享数: { type: Number, default: 0 },
+ // 【升级】销售统计
+ 销量: { type: Number, default: 0 },
+ 最近售出时间: { type: Date }
},
// --- 状态控制 ---
@@ -91,6 +112,7 @@ const ArticleSchema = new Schema({
// 复合索引优化
ArticleSchema.index({ createdAt: -1 });
ArticleSchema.index({ 分类ID: 1, 发布状态: 1 });
+ArticleSchema.index({ '统计数据.销量': -1 }); // 新增:支持按销量排行
/**
diff --git a/src/pages/admin/index.tsx b/src/pages/admin/index.tsx
index 0ac8763..7885e6e 100644
--- a/src/pages/admin/index.tsx
+++ b/src/pages/admin/index.tsx
@@ -5,6 +5,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Users, FileText, ShoppingBag, DollarSign } from 'lucide-react';
import { verifyToken } from '@/lib/auth';
import { User, Article, Order } from '@/models';
+import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
// 定义统计数据接口
interface DashboardStats {
@@ -15,9 +16,26 @@ interface DashboardStats {
}
export default function AdminDashboard({ stats }: { stats: DashboardStats }) {
+ // 模拟图表数据 (实际应从 API 获取)
+ const data = [
+ { name: 'Jan', total: Math.floor(Math.random() * 5000) + 1000 },
+ { name: 'Feb', total: Math.floor(Math.random() * 5000) + 1000 },
+ { name: 'Mar', total: Math.floor(Math.random() * 5000) + 1000 },
+ { name: 'Apr', total: Math.floor(Math.random() * 5000) + 1000 },
+ { name: 'May', total: Math.floor(Math.random() * 5000) + 1000 },
+ { name: 'Jun', total: Math.floor(Math.random() * 5000) + 1000 },
+ { name: 'Jul', total: Math.floor(Math.random() * 5000) + 1000 },
+ { name: 'Aug', total: Math.floor(Math.random() * 5000) + 1000 },
+ { name: 'Sep', total: Math.floor(Math.random() * 5000) + 1000 },
+ { name: 'Oct', total: Math.floor(Math.random() * 5000) + 1000 },
+ { name: 'Nov', total: Math.floor(Math.random() * 5000) + 1000 },
+ { name: 'Dec', total: Math.floor(Math.random() * 5000) + 1000 },
+ ];
+
return (
+ {/* ... (existing header and stats cards) ... */}
仪表盘
@@ -82,9 +100,25 @@ export default function AdminDashboard({ stats }: { stats: DashboardStats }) {
近期概览
-
- 图表组件待集成 (Recharts)
-
+
+
+
+ `¥${value}`}
+ />
+
+
+
diff --git a/src/pages/api/articles/[id].ts b/src/pages/api/articles/[id].ts
index 8b99962..bb141c6 100644
--- a/src/pages/api/articles/[id].ts
+++ b/src/pages/api/articles/[id].ts
@@ -67,7 +67,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}
}
- // If article is free, everyone has access
+ // If article is free (price 0), everyone has access
if (article.价格 === 0) {
hasAccess = true;
}
@@ -75,12 +75,18 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
// If not yet accessible and user is logged in, check purchase/membership
if (!hasAccess && userId) {
const user = await User.findById(userId);
+ const isMember = user?.会员信息?.过期时间 && new Date(user.会员信息.过期时间) > new Date();
// Check Membership
- if (user?.会员信息?.过期时间 && new Date(user.会员信息.过期时间) > new Date()) {
+ // If payment method is 'membership_free', members get access for free
+ if (isMember && article.支付方式 === 'membership_free') {
hasAccess = true;
}
+ // If user is a member, they might have general download privileges (depending on your business logic)
+ // For now, let's assume 'membership_free' explicitly grants access to members.
+ // If you have a global "members can download everything" policy, you can add `if (isMember) hasAccess = true;` here.
+
// Check if purchased
if (!hasAccess) {
const order = await Order.findOne({
@@ -104,7 +110,16 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
if (!hasAccess) {
// Hide sensitive content
- delete articleData.资源属性;
+ if (articleData.资源属性) {
+ // Keep public attributes visible
+ const publicAttributes = {
+ 版本号: articleData.资源属性.版本号,
+ 文件大小: articleData.资源属性.文件大小,
+ 扩展属性: articleData.资源属性.扩展属性
+ };
+ articleData.资源属性 = publicAttributes;
+ }
+
// Optional: Truncate content for preview
articleData.正文内容 = await processMarkdown(article.正文内容.substring(0, 300) + '...');
}
diff --git a/src/pages/api/auth/login.ts b/src/pages/api/auth/login.ts
index c6d21bf..9218487 100644
--- a/src/pages/api/auth/login.ts
+++ b/src/pages/api/auth/login.ts
@@ -32,6 +32,11 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
return res.status(401).json({ message: '邮箱或密码错误' });
}
+ // 检查用户是否被封禁
+ if (user.是否被封禁) {
+ return res.status(403).json({ message: '该账号已被封禁,请联系管理员' });
+ }
+
// 生成 JWT
const token = jwt.sign(
{ userId: user._id, email: user.邮箱, role: user.角色 },
diff --git a/src/pages/api/payment/notify.ts b/src/pages/api/payment/notify.ts
index a3810f7..03f78ad 100644
--- a/src/pages/api/payment/notify.ts
+++ b/src/pages/api/payment/notify.ts
@@ -1,6 +1,6 @@
import { NextApiRequest, NextApiResponse } from 'next';
import dbConnect from '@/lib/dbConnect';
-import { Order, User, MembershipPlan } from '@/models';
+import { Order, User, MembershipPlan, Article } from '@/models';
import { alipayService } from '@/lib/alipay';
export const config = {
@@ -71,8 +71,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
order.支付方式 = 'alipay';
await order.save();
- // Update User Membership
+ // Handle different order types
if (order.订单类型 === 'buy_membership') {
+ // Update User Membership
const plan = await MembershipPlan.findById(order.商品ID);
if (plan) {
const user = await User.findById(order.用户ID);
@@ -89,6 +90,14 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
await user.save();
}
}
+ } else if (order.订单类型 === 'buy_resource') {
+ // Update Article Sales Stats
+ const article = await Article.findById(order.商品ID);
+ if (article) {
+ article.统计数据.销量 = (article.统计数据.销量 || 0) + 1;
+ article.统计数据.最近售出时间 = new Date();
+ await article.save();
+ }
}
console.log(`Order ${outTradeNo} marked as paid`);
diff --git a/src/pages/article/[id].tsx b/src/pages/article/[id].tsx
index 60b2ad5..090c3a8 100644
--- a/src/pages/article/[id].tsx
+++ b/src/pages/article/[id].tsx
@@ -1,12 +1,16 @@
import React, { useState, useEffect } from 'react';
import { useRouter } from 'next/router';
+import Image from 'next/image';
+import Link from 'next/link';
import { useAuth } from '@/hooks/useAuth';
import MainLayout from '@/components/layouts/MainLayout';
import CommentSection from '@/components/article/CommentSection';
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Skeleton } from "@/components/ui/skeleton";
-import { Loader2, Lock, Download, Calendar, User as UserIcon, Tag as TagIcon } from 'lucide-react';
+import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from "@/components/ui/card";
+import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
+import { Loader2, Lock, Download, Calendar, User as UserIcon, Tag as TagIcon, Share2, Crown, Sparkles, FileText, Eye, ShoppingCart, Copy, Check, Box, HardDrive } from 'lucide-react';
import { format } from 'date-fns';
import { zhCN } from 'date-fns/locale';
import { toast } from 'sonner';
@@ -22,22 +26,27 @@ interface Article {
头像: string;
};
分类ID: {
+ _id: string;
分类名称: string;
};
标签ID列表: {
标签名称: string;
}[];
价格: number;
- 支付方式: 'points' | 'cash';
+ 支付方式: 'points' | 'cash' | 'membership_free';
资源属性?: {
下载链接: string;
提取码: string;
解压密码: string;
隐藏内容: string;
+ 版本号?: string;
+ 文件大小?: string;
+ 扩展属性?: { 属性名: string; 属性值: string }[];
};
createdAt: string;
统计数据: {
阅读数: number;
+ 销量: number;
};
}
@@ -46,13 +55,16 @@ export default function ArticleDetail() {
const { id } = router.query;
const { user, loading: authLoading } = useAuth();
const [article, setArticle] = useState(null);
+ const [recentArticles, setRecentArticles] = useState([]);
const [loading, setLoading] = useState(true);
const [hasAccess, setHasAccess] = useState(false);
const [purchasing, setPurchasing] = useState(false);
+ const [copiedField, setCopiedField] = useState(null);
useEffect(() => {
if (id) {
fetchArticle();
+ fetchRecentArticles();
}
}, [id]);
@@ -74,6 +86,20 @@ export default function ArticleDetail() {
}
};
+ const fetchRecentArticles = async () => {
+ try {
+ const res = await fetch('/api/articles?limit=5');
+ if (res.ok) {
+ const data = await res.json();
+ // Filter out current article if present
+ const filtered = data.articles.filter((a: Article) => a._id !== id);
+ setRecentArticles(filtered.slice(0, 5));
+ }
+ } catch (error) {
+ console.error('Failed to fetch recent articles', error);
+ }
+ };
+
const handlePurchase = async () => {
if (!user) {
router.push(`/auth/login?redirect=${encodeURIComponent(router.asPath)}`);
@@ -113,22 +139,43 @@ export default function ArticleDetail() {
}
};
+ const handleShare = () => {
+ const url = window.location.href;
+ navigator.clipboard.writeText(url).then(() => {
+ toast.success('链接已复制到剪贴板');
+ }).catch(() => {
+ toast.error('复制失败');
+ });
+ };
+
+ const copyToClipboard = (text: string, field: string) => {
+ navigator.clipboard.writeText(text).then(() => {
+ setCopiedField(field);
+ toast.success('复制成功');
+ setTimeout(() => setCopiedField(null), 2000);
+ }).catch(() => {
+ toast.error('复制失败');
+ });
+ };
+
if (loading) {
return (
-
-
-
-
-
-
+
@@ -145,137 +192,271 @@ export default function ArticleDetail() {
keywords: article.标签ID列表?.map(t => t.标签名称).join(',')
}}
>
-
- {/* Article Header */}
-
-
-
- {article.分类ID?.分类名称 || '未分类'}
-
- {article.标签ID列表?.map((tag, index) => (
-
- {tag.标签名称}
-
- ))}
-
+
+
+ {/* Main Content - Left Column (75%) */}
+
+ {/* Article Header */}
+
+
+
+ {article.分类ID?.分类名称 || '未分类'}
+
+ {article.标签ID列表?.map((tag, index) => (
+
+ {tag.标签名称}
+
+ ))}
+
-
- {article.文章标题}
-
+
+ {article.文章标题}
+
-
-
-
- {article.作者ID?.用户名 || '匿名'}
-
-
-
- {format(new Date(article.createdAt), 'yyyy-MM-dd', { locale: zhCN })}
-
-
- 阅读 {article.统计数据?.阅读数 || 0}
-
-
-
-
- {/* Cover Image */}
- {article.封面图 && (
-
-

-
- )}
-
- {/* Article Content */}
-
-
-
-
- {/* Resource Download / Paywall Section */}
-
- {hasAccess ? (
-
-
-
- 资源下载
-
- {article.资源属性 ? (
-
-
-
下载链接
-
- {article.资源属性.下载链接}
-
+
+
+
+
+ {article.作者ID?.用户名 || '匿名'}
- {article.资源属性.提取码 && (
-
-
提取码
-
- {article.资源属性.提取码}
-
+
+
+ {format(new Date(article.createdAt), 'yyyy-MM-dd', { locale: zhCN })}
+
+
+
+ {article.统计数据?.阅读数 || 0} 阅读
+
+
+
+
+
+
+
+ {/* Cover Image */}
+ {article.封面图 && (
+
+
+
+ )}
+
+ {/* Article Content */}
+
+
+
+
+ {/* Comments Section */}
+
+
+
+ {/* Sidebar - Right Column (25%) */}
+
+ {/* Resource Card */}
+
+
+
+
+ 资源下载
+
+
+
+
+
资源价格
+
+
+ {article.价格 > 0 ? `¥${article.价格}` : '免费'}
+
+ {article.价格 > 0 && / 永久}
+
+
+
+ {/* Resource Attributes (Version, Size, etc.) */}
+ {article.资源属性 && (
+
+ {article.资源属性.版本号 && (
+
+
+ 版本: {article.资源属性.版本号}
+
+ )}
+ {article.资源属性.文件大小 && (
+
+
+ 大小: {article.资源属性.文件大小}
+
+ )}
+ {article.资源属性.扩展属性?.map((attr, idx) => (
+
+
+ {attr.属性名}: {attr.属性值}
+
+ ))}
+
+ )}
+
+ {hasAccess ? (
+
+
+
+ 您已拥有此资源权限
- )}
- {article.资源属性.解压密码 && (
-
-
解压密码
-
- {article.资源属性.解压密码}
-
+
+ {article.资源属性 && (
+
+
+
+ {article.资源属性.提取码 && (
+
+ 提取码: {article.资源属性.提取码}
+
+
+ )}
+
+ {article.资源属性.解压密码 && (
+
+ 解压密码: {article.资源属性.解压密码}
+
+
+ )}
+
+ {article.资源属性.隐藏内容 && (
+
+
+ 付费内容
+
+
{article.资源属性.隐藏内容}
+
+ )}
+
+ )}
+
+ ) : (
+
+
+
+ {article.支付方式 === 'membership_free' ? (
+
+
+
+ 会员免费下载 (点击开通)
+
+
+ ) : (
+
+
+
+ 开通会员享受折扣
+
+
+ )}
+
+ )}
+
+
+
+
{article.统计数据?.阅读数 || 0}
+
浏览次数
+
+
+
{article.统计数据?.销量 || 0}
+
最近售出
+
+
+
+
+
+ {/* Recent Posts Card */}
+
+
+
+
+ 近期推荐
+
+
+
+
+ {recentArticles.length > 0 ? (
+ recentArticles.map((item, index) => (
+
+
+ {item.封面图 ? (
+
+ ) : (
+
+
+
+ )}
+
+
+
+ {item.文章标题}
+
+
+ {format(new Date(item.createdAt), 'MM-dd')}
+
+
+
+ ))
+ ) : (
+
+ 暂无推荐
)}
- ) : (
- 此资源暂无下载信息
- )}
-
- ) : (
-
-
-
-
-
- 此内容需要付费查看
-
-
- 购买此资源或开通会员,即可解锁全文及下载链接,享受更多优质内容。
-
-
-
-
-
-
-
- )}
+
+
+
-
- {/* Comments Section */}
-
);
diff --git a/src/pages/auth/login.tsx b/src/pages/auth/login.tsx
index 6b0921f..56da160 100644
--- a/src/pages/auth/login.tsx
+++ b/src/pages/auth/login.tsx
@@ -4,7 +4,7 @@ import Link from 'next/link';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod';
-import { Loader2 } from 'lucide-react';
+import { Loader2, Mail, Lock, ArrowRight, Sparkles } from 'lucide-react';
import { useAuth } from '@/hooks/useAuth';
import { Button } from '@/components/ui/button';
@@ -17,7 +17,7 @@ import {
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
-import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
+import { Checkbox } from '@/components/ui/checkbox';
const formSchema = z.object({
email: z.string().email({ message: "请输入有效的邮箱地址" }),
@@ -69,25 +69,59 @@ export default function LoginPage() {
}
return (
-
-
-
- 登录账号
-
- 请输入您的邮箱和密码进行登录
-
-
-
+
+ {/* Left Panel - Visual & Branding */}
+
+ {/* Abstract Background */}
+
+
+
+ {/* Content */}
+
+
+
+
+ 释放您的
+ 无限创意潜能
+
+
+ 加入数万名创作者的行列,利用最先进的 AI 技术,将您的想法瞬间转化为现实。
+
+
+
+
+ © 2024 AOUN AI. All rights reserved.
+
+
+
+ {/* Right Panel - Login Form */}
+
+
+
+
-
-
-
- 还没有账号?{' '}
-
- 立即注册
+
+
+
+
+
+ 免费注册账号
-
-
-
+
+
+
);
}
diff --git a/src/pages/auth/register.tsx b/src/pages/auth/register.tsx
index c3a3cd2..659b8ba 100644
--- a/src/pages/auth/register.tsx
+++ b/src/pages/auth/register.tsx
@@ -4,7 +4,7 @@ import Link from 'next/link';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod';
-import { Loader2 } from 'lucide-react';
+import { Loader2, User, Mail, Lock, ArrowRight, Sparkles } from 'lucide-react';
import { Button } from '@/components/ui/button';
import {
@@ -16,7 +16,6 @@ import {
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
-import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
const formSchema = z.object({
username: z.string().min(2, { message: "用户名至少需要2个字符" }),
@@ -76,15 +75,46 @@ export default function RegisterPage() {
}
return (
-
-
-
- 注册账号
-
- 创建一个新账号以开始使用
-
-
-
+
+ {/* Left Panel - Visual & Branding */}
+
+ {/* Abstract Background */}
+
+
+
+ {/* Content */}
+
+
+
+
+ 开启您的
+ 智能创作之旅
+
+
+ 注册即可获得 AI 积分,体验 GPT-4、Claude 3 等顶级模型的强大能力。
+
+
+
+
+ © 2024 AOUN AI. All rights reserved.
+
+
+
+ {/* Right Panel - Register Form */}
+
+
+
+
-
-
-
- 已有账号?{' '}
-
- 立即登录
+
+
+
+
+
+ 直接登录
-
-
-
+
+
+
);
}
diff --git a/src/pages/index.tsx b/src/pages/index.tsx
index f0d404b..d7d7c16 100644
--- a/src/pages/index.tsx
+++ b/src/pages/index.tsx
@@ -6,13 +6,56 @@ import Sidebar from '@/components/home/Sidebar';
import { Button } from '@/components/ui/button';
import { Loader2 } from 'lucide-react';
+interface Article {
+ _id: string;
+ 文章标题: string;
+ 摘要?: string;
+ 封面图?: string;
+ 作者ID?: {
+ 用户名: string;
+ 头像: string;
+ };
+ 分类ID?: {
+ 分类名称: string;
+ };
+ 标签ID列表?: {
+ 标签名称: string;
+ }[];
+ 统计数据: {
+ 阅读数: number;
+ 点赞数: number;
+ 评论数: number;
+ };
+ 价格?: number;
+ createdAt: string;
+}
+
+interface Category {
+ _id: string;
+ 分类名称: string;
+ 别名: string;
+}
+
+interface Tag {
+ _id: string;
+ 标签名称: string;
+}
+
+interface Banner {
+ 标题: string;
+ 描述: string;
+ 图片地址: string;
+ 按钮文本: string;
+ 按钮链接: string;
+}
+
export default function Home() {
- const [articles, setArticles] = useState([]);
- const [categories, setCategories] = useState([]);
- const [tags, setTags] = useState([]);
+ const [articles, setArticles] = useState([]);
+ const [categories, setCategories] = useState([]);
+ const [tags, setTags] = useState([]);
const [loading, setLoading] = useState(true);
const [activeCategory, setActiveCategory] = useState('all');
- const [banners, setBanners] = useState([]);
+ const [banners, setBanners] = useState([]);
useEffect(() => {
fetchData();
@@ -108,7 +151,16 @@ export default function Home() {
) : (
{articles.map(article => (
-
+
))}
)}