2025.11.27.22.40
This commit is contained in:
9
.dockerignore
Normal file
9
.dockerignore
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
Dockerfile
|
||||||
|
.dockerignore
|
||||||
|
node_modules
|
||||||
|
npm-debug.log
|
||||||
|
README.md
|
||||||
|
.next
|
||||||
|
.git
|
||||||
|
.env*
|
||||||
|
! .env.example
|
||||||
4
.env.example
Normal file
4
.env.example
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# MONGODB数据库
|
||||||
|
MONGODB_URI
|
||||||
|
# JWT配置,用于生成token
|
||||||
|
JWT_SECRET=
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -31,7 +31,8 @@ yarn-error.log*
|
|||||||
.pnpm-debug.log*
|
.pnpm-debug.log*
|
||||||
|
|
||||||
# env files (can opt-in for committing if needed)
|
# env files (can opt-in for committing if needed)
|
||||||
.env*
|
# .env*
|
||||||
|
.env.local
|
||||||
|
|
||||||
# vercel
|
# vercel
|
||||||
.vercel
|
.vercel
|
||||||
|
|||||||
51
Dockerfile
Normal file
51
Dockerfile
Normal file
@@ -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"]
|
||||||
@@ -2,6 +2,7 @@ import type { NextConfig } from "next";
|
|||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
/* config options here */
|
/* config options here */
|
||||||
|
output: 'standalone',
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
images: {
|
images: {
|
||||||
remotePatterns: [
|
remotePatterns: [
|
||||||
@@ -13,6 +14,10 @@ const nextConfig: NextConfig = {
|
|||||||
protocol: 'https',
|
protocol: 'https',
|
||||||
hostname: 'api.dicebear.com',
|
hostname: 'api.dicebear.com',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
protocol: 'https',
|
||||||
|
hostname: 'images.unsplash.com',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^5.2.2",
|
"@hookform/resolvers": "^5.2.2",
|
||||||
"@radix-ui/react-avatar": "^1.1.11",
|
"@radix-ui/react-avatar": "^1.1.11",
|
||||||
|
"@radix-ui/react-checkbox": "^1.3.3",
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
"@radix-ui/react-label": "^2.1.8",
|
"@radix-ui/react-label": "^2.1.8",
|
||||||
@@ -43,12 +44,14 @@
|
|||||||
"react-dom": "19.2.0",
|
"react-dom": "19.2.0",
|
||||||
"react-hook-form": "^7.66.1",
|
"react-hook-form": "^7.66.1",
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
|
"recharts": "^3.5.0",
|
||||||
"rehype-pretty-code": "^0.14.1",
|
"rehype-pretty-code": "^0.14.1",
|
||||||
"rehype-raw": "^7.0.0",
|
"rehype-raw": "^7.0.0",
|
||||||
"rehype-sanitize": "^6.0.0",
|
"rehype-sanitize": "^6.0.0",
|
||||||
"rehype-stringify": "^10.0.1",
|
"rehype-stringify": "^10.0.1",
|
||||||
"remark-parse": "^11.0.0",
|
"remark-parse": "^11.0.0",
|
||||||
"remark-rehype": "^11.1.2",
|
"remark-rehype": "^11.1.2",
|
||||||
|
"sharp": "^0.34.5",
|
||||||
"shiki": "^3.15.0",
|
"shiki": "^3.15.0",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
|
|||||||
339
pnpm-lock.yaml
generated
339
pnpm-lock.yaml
generated
@@ -14,6 +14,9 @@ importers:
|
|||||||
'@radix-ui/react-avatar':
|
'@radix-ui/react-avatar':
|
||||||
specifier: ^1.1.11
|
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)
|
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':
|
'@radix-ui/react-dialog':
|
||||||
specifier: ^1.1.15
|
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)
|
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:
|
react-markdown:
|
||||||
specifier: ^10.1.0
|
specifier: ^10.1.0
|
||||||
version: 10.1.0(@types/react@19.2.7)(react@19.2.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:
|
rehype-pretty-code:
|
||||||
specifier: ^0.14.1
|
specifier: ^0.14.1
|
||||||
version: 0.14.1(shiki@3.15.0)
|
version: 0.14.1(shiki@3.15.0)
|
||||||
@@ -128,6 +134,9 @@ importers:
|
|||||||
remark-rehype:
|
remark-rehype:
|
||||||
specifier: ^11.1.2
|
specifier: ^11.1.2
|
||||||
version: 11.1.2
|
version: 11.1.2
|
||||||
|
sharp:
|
||||||
|
specifier: ^0.34.5
|
||||||
|
version: 0.34.5
|
||||||
shiki:
|
shiki:
|
||||||
specifier: ^3.15.0
|
specifier: ^3.15.0
|
||||||
version: 3.15.0
|
version: 3.15.0
|
||||||
@@ -596,6 +605,19 @@ packages:
|
|||||||
'@types/react-dom':
|
'@types/react-dom':
|
||||||
optional: true
|
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':
|
'@radix-ui/react-collection@1.1.7':
|
||||||
resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==}
|
resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -986,6 +1008,17 @@ packages:
|
|||||||
'@radix-ui/rect@1.1.1':
|
'@radix-ui/rect@1.1.1':
|
||||||
resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==}
|
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':
|
'@remirror/core-constants@3.0.0':
|
||||||
resolution: {integrity: sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==}
|
resolution: {integrity: sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==}
|
||||||
|
|
||||||
@@ -1013,6 +1046,9 @@ packages:
|
|||||||
'@shikijs/vscode-textmate@10.0.2':
|
'@shikijs/vscode-textmate@10.0.2':
|
||||||
resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==}
|
resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==}
|
||||||
|
|
||||||
|
'@standard-schema/spec@1.0.0':
|
||||||
|
resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==}
|
||||||
|
|
||||||
'@standard-schema/utils@0.3.0':
|
'@standard-schema/utils@0.3.0':
|
||||||
resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==}
|
resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==}
|
||||||
|
|
||||||
@@ -1274,6 +1310,33 @@ packages:
|
|||||||
'@tybys/wasm-util@0.10.1':
|
'@tybys/wasm-util@0.10.1':
|
||||||
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
|
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':
|
'@types/debug@4.1.12':
|
||||||
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
|
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
|
||||||
|
|
||||||
@@ -1726,6 +1789,50 @@ packages:
|
|||||||
csstype@3.2.3:
|
csstype@3.2.3:
|
||||||
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
|
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:
|
damerau-levenshtein@1.0.8:
|
||||||
resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==}
|
resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==}
|
||||||
|
|
||||||
@@ -1761,6 +1868,9 @@ packages:
|
|||||||
supports-color:
|
supports-color:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
decimal.js-light@2.5.1:
|
||||||
|
resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==}
|
||||||
|
|
||||||
decode-named-character-reference@1.2.0:
|
decode-named-character-reference@1.2.0:
|
||||||
resolution: {integrity: sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==}
|
resolution: {integrity: sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==}
|
||||||
|
|
||||||
@@ -1867,6 +1977,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==}
|
resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
es-toolkit@1.42.0:
|
||||||
|
resolution: {integrity: sha512-SLHIyY7VfDJBM8clz4+T2oquwTQxEzu263AyhVK4jREOAwJ+8eebaa4wM3nlvnAqhDrMm2EsA6hWHaQsMPQ1nA==}
|
||||||
|
|
||||||
escalade@3.2.0:
|
escalade@3.2.0:
|
||||||
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
|
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -1947,6 +2060,12 @@ packages:
|
|||||||
peerDependencies:
|
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: ^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:
|
eslint-plugin-react@7.37.5:
|
||||||
resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==}
|
resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
@@ -1998,6 +2117,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
|
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
eventemitter3@5.0.1:
|
||||||
|
resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==}
|
||||||
|
|
||||||
eventsource-parser@3.0.6:
|
eventsource-parser@3.0.6:
|
||||||
resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==}
|
resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
@@ -2221,6 +2343,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
|
resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
|
||||||
engines: {node: '>= 4'}
|
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:
|
import-fresh@3.3.1:
|
||||||
resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
|
resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -2236,6 +2364,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
|
resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
|
||||||
engines: {node: '>= 0.4'}
|
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:
|
is-alphabetical@2.0.1:
|
||||||
resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==}
|
resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==}
|
||||||
|
|
||||||
@@ -3028,6 +3160,18 @@ packages:
|
|||||||
'@types/react': '>=18'
|
'@types/react': '>=18'
|
||||||
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:
|
react-remove-scroll-bar@2.3.8:
|
||||||
resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==}
|
resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -3062,6 +3206,22 @@ packages:
|
|||||||
resolution: {integrity: sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==}
|
resolution: {integrity: sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==}
|
||||||
engines: {node: '>=0.10.0'}
|
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:
|
reflect.getprototypeof@1.0.10:
|
||||||
resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==}
|
resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -3143,6 +3303,9 @@ packages:
|
|||||||
remark-stringify@11.0.0:
|
remark-stringify@11.0.0:
|
||||||
resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==}
|
resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==}
|
||||||
|
|
||||||
|
reselect@5.1.1:
|
||||||
|
resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==}
|
||||||
|
|
||||||
resolve-from@4.0.0:
|
resolve-from@4.0.0:
|
||||||
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
|
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
@@ -3336,6 +3499,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==}
|
resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
tiny-invariant@1.3.3:
|
||||||
|
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
|
||||||
|
|
||||||
tinyglobby@0.2.15:
|
tinyglobby@0.2.15:
|
||||||
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
|
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
|
||||||
engines: {node: '>=12.0.0'}
|
engines: {node: '>=12.0.0'}
|
||||||
@@ -3483,6 +3649,9 @@ packages:
|
|||||||
vfile@6.0.3:
|
vfile@6.0.3:
|
||||||
resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==}
|
resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==}
|
||||||
|
|
||||||
|
victory-vendor@37.3.6:
|
||||||
|
resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==}
|
||||||
|
|
||||||
w3c-keyname@2.2.8:
|
w3c-keyname@2.2.8:
|
||||||
resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
|
resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
|
||||||
|
|
||||||
@@ -3742,8 +3911,7 @@ snapshots:
|
|||||||
|
|
||||||
'@humanwhocodes/retry@0.4.3': {}
|
'@humanwhocodes/retry@0.4.3': {}
|
||||||
|
|
||||||
'@img/colour@1.0.0':
|
'@img/colour@1.0.0': {}
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@img/sharp-darwin-arm64@0.34.5':
|
'@img/sharp-darwin-arm64@0.34.5':
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
@@ -3939,6 +4107,22 @@ snapshots:
|
|||||||
'@types/react': 19.2.7
|
'@types/react': 19.2.7
|
||||||
'@types/react-dom': 19.2.3(@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)':
|
'@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:
|
dependencies:
|
||||||
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.0)
|
'@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': {}
|
'@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': {}
|
'@remirror/core-constants@3.0.0': {}
|
||||||
|
|
||||||
'@rtsao/scc@1.1.0': {}
|
'@rtsao/scc@1.1.0': {}
|
||||||
@@ -4363,6 +4559,8 @@ snapshots:
|
|||||||
|
|
||||||
'@shikijs/vscode-textmate@10.0.2': {}
|
'@shikijs/vscode-textmate@10.0.2': {}
|
||||||
|
|
||||||
|
'@standard-schema/spec@1.0.0': {}
|
||||||
|
|
||||||
'@standard-schema/utils@0.3.0': {}
|
'@standard-schema/utils@0.3.0': {}
|
||||||
|
|
||||||
'@swc/helpers@0.5.15':
|
'@swc/helpers@0.5.15':
|
||||||
@@ -4631,6 +4829,30 @@ snapshots:
|
|||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
optional: true
|
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':
|
'@types/debug@4.1.12':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/ms': 2.1.0
|
'@types/ms': 2.1.0
|
||||||
@@ -5107,6 +5329,44 @@ snapshots:
|
|||||||
|
|
||||||
csstype@3.2.3: {}
|
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: {}
|
damerau-levenshtein@1.0.8: {}
|
||||||
|
|
||||||
data-view-buffer@1.0.2:
|
data-view-buffer@1.0.2:
|
||||||
@@ -5137,6 +5397,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.3
|
ms: 2.1.3
|
||||||
|
|
||||||
|
decimal.js-light@2.5.1: {}
|
||||||
|
|
||||||
decode-named-character-reference@1.2.0:
|
decode-named-character-reference@1.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
character-entities: 2.0.2
|
character-entities: 2.0.2
|
||||||
@@ -5307,6 +5569,8 @@ snapshots:
|
|||||||
is-date-object: 1.1.0
|
is-date-object: 1.1.0
|
||||||
is-symbol: 1.1.1
|
is-symbol: 1.1.1
|
||||||
|
|
||||||
|
es-toolkit@1.42.0: {}
|
||||||
|
|
||||||
escalade@3.2.0: {}
|
escalade@3.2.0: {}
|
||||||
|
|
||||||
escape-string-regexp@4.0.0: {}
|
escape-string-regexp@4.0.0: {}
|
||||||
@@ -5426,6 +5690,10 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- 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)):
|
eslint-plugin-react@7.37.5(eslint@9.39.1(jiti@2.6.1)):
|
||||||
dependencies:
|
dependencies:
|
||||||
array-includes: 3.1.9
|
array-includes: 3.1.9
|
||||||
@@ -5518,6 +5786,8 @@ snapshots:
|
|||||||
|
|
||||||
esutils@2.0.3: {}
|
esutils@2.0.3: {}
|
||||||
|
|
||||||
|
eventemitter3@5.0.1: {}
|
||||||
|
|
||||||
eventsource-parser@3.0.6: {}
|
eventsource-parser@3.0.6: {}
|
||||||
|
|
||||||
extend@3.0.2: {}
|
extend@3.0.2: {}
|
||||||
@@ -5824,6 +6094,10 @@ snapshots:
|
|||||||
|
|
||||||
ignore@7.0.5: {}
|
ignore@7.0.5: {}
|
||||||
|
|
||||||
|
immer@10.2.0: {}
|
||||||
|
|
||||||
|
immer@11.0.0: {}
|
||||||
|
|
||||||
import-fresh@3.3.1:
|
import-fresh@3.3.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
parent-module: 1.0.1
|
parent-module: 1.0.1
|
||||||
@@ -5839,6 +6113,8 @@ snapshots:
|
|||||||
hasown: 2.0.2
|
hasown: 2.0.2
|
||||||
side-channel: 1.1.0
|
side-channel: 1.1.0
|
||||||
|
|
||||||
|
internmap@2.0.3: {}
|
||||||
|
|
||||||
is-alphabetical@2.0.1: {}
|
is-alphabetical@2.0.1: {}
|
||||||
|
|
||||||
is-alphanumerical@2.0.1:
|
is-alphanumerical@2.0.1:
|
||||||
@@ -6887,6 +7163,15 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- 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):
|
react-remove-scroll-bar@2.3.8(@types/react@19.2.7)(react@19.2.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.2.0
|
react: 19.2.0
|
||||||
@@ -6916,6 +7201,34 @@ snapshots:
|
|||||||
|
|
||||||
react@19.2.0: {}
|
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:
|
reflect.getprototypeof@1.0.10:
|
||||||
dependencies:
|
dependencies:
|
||||||
call-bind: 1.0.8
|
call-bind: 1.0.8
|
||||||
@@ -7083,6 +7396,8 @@ snapshots:
|
|||||||
mdast-util-to-markdown: 2.1.2
|
mdast-util-to-markdown: 2.1.2
|
||||||
unified: 11.0.5
|
unified: 11.0.5
|
||||||
|
|
||||||
|
reselect@5.1.1: {}
|
||||||
|
|
||||||
resolve-from@4.0.0: {}
|
resolve-from@4.0.0: {}
|
||||||
|
|
||||||
resolve-pkg-maps@1.0.0: {}
|
resolve-pkg-maps@1.0.0: {}
|
||||||
@@ -7186,7 +7501,6 @@ snapshots:
|
|||||||
'@img/sharp-win32-arm64': 0.34.5
|
'@img/sharp-win32-arm64': 0.34.5
|
||||||
'@img/sharp-win32-ia32': 0.34.5
|
'@img/sharp-win32-ia32': 0.34.5
|
||||||
'@img/sharp-win32-x64': 0.34.5
|
'@img/sharp-win32-x64': 0.34.5
|
||||||
optional: true
|
|
||||||
|
|
||||||
shebang-command@2.0.0:
|
shebang-command@2.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -7341,6 +7655,8 @@ snapshots:
|
|||||||
|
|
||||||
tapable@2.3.0: {}
|
tapable@2.3.0: {}
|
||||||
|
|
||||||
|
tiny-invariant@1.3.3: {}
|
||||||
|
|
||||||
tinyglobby@0.2.15:
|
tinyglobby@0.2.15:
|
||||||
dependencies:
|
dependencies:
|
||||||
fdir: 6.5.0(picomatch@4.0.3)
|
fdir: 6.5.0(picomatch@4.0.3)
|
||||||
@@ -7549,6 +7865,23 @@ snapshots:
|
|||||||
'@types/unist': 3.0.3
|
'@types/unist': 3.0.3
|
||||||
vfile-message: 4.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: {}
|
w3c-keyname@2.2.8: {}
|
||||||
|
|
||||||
web-namespaces@2.0.1: {}
|
web-namespaces@2.0.1: {}
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select';
|
||||||
import { Switch } from '@/components/ui/switch';
|
import { Switch } from '@/components/ui/switch';
|
||||||
import { Loader2, Save, ArrowLeft, Globe, Lock, Image as ImageIcon, FileText, DollarSign, Settings } from 'lucide-react';
|
import { Loader2, Save, ArrowLeft, Globe, Lock, Image as ImageIcon, FileText, DollarSign, Settings, Plus, Trash2 } from 'lucide-react';
|
||||||
import { useForm, Controller } from 'react-hook-form';
|
import { useForm, Controller, useFieldArray } from 'react-hook-form';
|
||||||
import TiptapEditor from './TiptapEditor';
|
import TiptapEditor from './TiptapEditor';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
@@ -57,12 +57,20 @@ export default function ArticleEditor({ mode, articleId }: ArticleEditorProps) {
|
|||||||
下载链接: '',
|
下载链接: '',
|
||||||
提取码: '',
|
提取码: '',
|
||||||
解压密码: '',
|
解压密码: '',
|
||||||
隐藏内容: ''
|
隐藏内容: '',
|
||||||
|
版本号: '',
|
||||||
|
文件大小: '',
|
||||||
|
扩展属性: [] as { 属性名: string; 属性值: string }[]
|
||||||
},
|
},
|
||||||
发布状态: 'published'
|
发布状态: 'published'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { fields: extendedFields, append: appendExtended, remove: removeExtended } = useFieldArray({
|
||||||
|
control,
|
||||||
|
name: "资源属性.扩展属性"
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchDependencies();
|
fetchDependencies();
|
||||||
if (mode === 'edit' && articleId) {
|
if (mode === 'edit' && articleId) {
|
||||||
@@ -296,6 +304,16 @@ export default function ArticleEditor({ mode, articleId }: ArticleEditorProps) {
|
|||||||
<Lock className="w-4 h-4" /> 资源交付
|
<Lock className="w-4 h-4" /> 资源交付
|
||||||
</h3>
|
</h3>
|
||||||
<div className="p-4 bg-gray-50 rounded-lg space-y-4 border border-gray-100">
|
<div className="p-4 bg-gray-50 rounded-lg space-y-4 border border-gray-100">
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label className="text-xs text-muted-foreground">版本号</Label>
|
||||||
|
<Input {...register('资源属性.版本号')} className="h-8 text-sm bg-white" placeholder="v1.0.0" />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label className="text-xs text-muted-foreground">文件大小</Label>
|
||||||
|
<Input {...register('资源属性.文件大小')} className="h-8 text-sm bg-white" placeholder="100MB" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<Label className="text-xs text-muted-foreground">下载链接</Label>
|
<Label className="text-xs text-muted-foreground">下载链接</Label>
|
||||||
<Input {...register('资源属性.下载链接')} className="h-8 text-sm bg-white" />
|
<Input {...register('资源属性.下载链接')} className="h-8 text-sm bg-white" />
|
||||||
@@ -310,6 +328,29 @@ export default function ArticleEditor({ mode, articleId }: ArticleEditorProps) {
|
|||||||
<Input {...register('资源属性.解压密码')} className="h-8 text-sm bg-white" />
|
<Input {...register('资源属性.解压密码')} className="h-8 text-sm bg-white" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label className="text-xs text-muted-foreground">隐藏内容 (付费后可见)</Label>
|
||||||
|
<Textarea {...register('资源属性.隐藏内容')} className="text-sm bg-white min-h-[60px]" placeholder="仅付费用户可见的内容..." />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Extended Attributes */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-xs text-muted-foreground flex justify-between items-center">
|
||||||
|
<span>扩展属性</span>
|
||||||
|
<Button type="button" variant="ghost" size="sm" className="h-5 px-2 text-xs" onClick={() => appendExtended({ 属性名: '', 属性值: '' })}>
|
||||||
|
<Plus className="w-3 h-3 mr-1" /> 添加
|
||||||
|
</Button>
|
||||||
|
</Label>
|
||||||
|
{extendedFields.map((field, index) => (
|
||||||
|
<div key={field.id} className="flex gap-2 items-center">
|
||||||
|
<Input {...register(`资源属性.扩展属性.${index}.属性名` as const)} className="h-7 text-xs bg-white flex-1" placeholder="属性名" />
|
||||||
|
<Input {...register(`资源属性.扩展属性.${index}.属性值` as const)} className="h-7 text-xs bg-white flex-1" placeholder="属性值" />
|
||||||
|
<Button type="button" variant="ghost" size="icon" className="h-7 w-7 text-red-500 hover:text-red-600 hover:bg-red-50" onClick={() => removeExtended(index)}>
|
||||||
|
<Trash2 className="w-3 h-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -334,6 +375,7 @@ export default function ArticleEditor({ mode, articleId }: ArticleEditorProps) {
|
|||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="points">积分支付</SelectItem>
|
<SelectItem value="points">积分支付</SelectItem>
|
||||||
<SelectItem value="cash">现金支付 (CNY)</SelectItem>
|
<SelectItem value="cash">现金支付 (CNY)</SelectItem>
|
||||||
|
<SelectItem value="membership_free">会员免费</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { useState, useEffect, useCallback } from 'react';
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import Image from 'next/image';
|
||||||
import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious, type CarouselApi } from "@/components/ui/carousel";
|
import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious, type CarouselApi } from "@/components/ui/carousel";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
@@ -62,10 +63,14 @@ export default function HeroBanner({ banners }: HeroBannerProps) {
|
|||||||
<CarouselItem key={index}>
|
<CarouselItem key={index}>
|
||||||
<div className="relative h-[500px] w-full overflow-hidden">
|
<div className="relative h-[500px] w-full overflow-hidden">
|
||||||
{/* Background Image */}
|
{/* Background Image */}
|
||||||
<div
|
<div className="absolute inset-0">
|
||||||
className="absolute inset-0 bg-cover bg-center transition-transform duration-700 hover:scale-105"
|
<Image
|
||||||
style={{ backgroundImage: `url(${banner.图片地址})` }}
|
src={banner.图片地址}
|
||||||
>
|
alt={banner.标题}
|
||||||
|
fill
|
||||||
|
className="object-cover transition-transform duration-700 hover:scale-105"
|
||||||
|
priority={index === 0}
|
||||||
|
/>
|
||||||
<div className="absolute inset-0 bg-black/40" /> {/* Overlay */}
|
<div className="absolute inset-0 bg-black/40" /> {/* Overlay */}
|
||||||
<div className="absolute inset-0 bg-linear-to-t from-black/60 via-transparent to-transparent" /> {/* Gradient Overlay */}
|
<div className="absolute inset-0 bg-linear-to-t from-black/60 via-transparent to-transparent" /> {/* Gradient Overlay */}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
30
src/components/ui/checkbox.tsx
Normal file
30
src/components/ui/checkbox.tsx
Normal file
@@ -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<typeof CheckboxPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<CheckboxPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<CheckboxPrimitive.Indicator
|
||||||
|
className={cn("flex items-center justify-center text-current")}
|
||||||
|
>
|
||||||
|
<Check className="h-4 w-4" />
|
||||||
|
</CheckboxPrimitive.Indicator>
|
||||||
|
</CheckboxPrimitive.Root>
|
||||||
|
))
|
||||||
|
Checkbox.displayName = CheckboxPrimitive.Root.displayName
|
||||||
|
|
||||||
|
export { Checkbox }
|
||||||
@@ -2,9 +2,6 @@ import jwt from 'jsonwebtoken';
|
|||||||
import { NextApiRequest, NextApiResponse } from 'next';
|
import { NextApiRequest, NextApiResponse } from 'next';
|
||||||
|
|
||||||
const JWT_SECRET = process.env.JWT_SECRET;
|
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 {
|
export interface DecodedToken {
|
||||||
userId: string;
|
userId: string;
|
||||||
@@ -15,8 +12,12 @@ export interface DecodedToken {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function verifyToken(token: string): DecodedToken | null {
|
export function verifyToken(token: string): DecodedToken | null {
|
||||||
|
if (!JWT_SECRET) {
|
||||||
|
console.error('JWT_SECRET is not defined');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
try {
|
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) {
|
} catch (error) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,10 @@ const UserSchema = new Schema({
|
|||||||
* ==================================================================
|
* ==================================================================
|
||||||
* 2. Article (文章/资源模型)
|
* 2. Article (文章/资源模型)
|
||||||
* ==================================================================
|
* ==================================================================
|
||||||
|
* 核心更新:
|
||||||
|
* - 支付方式增加了 'membership_free' (会员免费)。
|
||||||
|
* - 资源属性增加了 版本、大小、扩展属性。
|
||||||
|
* - 统计数据增加了 销量、最近售出时间。
|
||||||
*/
|
*/
|
||||||
const ArticleSchema = new Schema({
|
const ArticleSchema = new Schema({
|
||||||
// --- 基础信息 ---
|
// --- 基础信息 ---
|
||||||
@@ -52,7 +56,7 @@ const ArticleSchema = new Schema({
|
|||||||
|
|
||||||
// --- 内容部分 ---
|
// --- 内容部分 ---
|
||||||
摘要: { type: String },
|
摘要: { type: String },
|
||||||
正文内容: { type: String, required: true },
|
正文内容: { type: String, required: true }, // 公开预览内容 或 包含截断标识的完整内容
|
||||||
|
|
||||||
// --- SEO 专用优化 ---
|
// --- SEO 专用优化 ---
|
||||||
SEO关键词: { type: [String], default: [] },
|
SEO关键词: { type: [String], default: [] },
|
||||||
@@ -65,14 +69,28 @@ const ArticleSchema = new Schema({
|
|||||||
|
|
||||||
// --- 售卖策略 ---
|
// --- 售卖策略 ---
|
||||||
价格: { type: Number, default: 0 },
|
价格: { 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 },
|
解压密码: { 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: 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({ createdAt: -1 });
|
||||||
ArticleSchema.index({ 分类ID: 1, 发布状态: 1 });
|
ArticleSchema.index({ 分类ID: 1, 发布状态: 1 });
|
||||||
|
ArticleSchema.index({ '统计数据.销量': -1 }); // 新增:支持按销量排行
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|||||||
import { Users, FileText, ShoppingBag, DollarSign } from 'lucide-react';
|
import { Users, FileText, ShoppingBag, DollarSign } from 'lucide-react';
|
||||||
import { verifyToken } from '@/lib/auth';
|
import { verifyToken } from '@/lib/auth';
|
||||||
import { User, Article, Order } from '@/models';
|
import { User, Article, Order } from '@/models';
|
||||||
|
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
|
||||||
|
|
||||||
// 定义统计数据接口
|
// 定义统计数据接口
|
||||||
interface DashboardStats {
|
interface DashboardStats {
|
||||||
@@ -15,9 +16,26 @@ interface DashboardStats {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function AdminDashboard({ stats }: { stats: 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 (
|
return (
|
||||||
<AdminLayout>
|
<AdminLayout>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
{/* ... (existing header and stats cards) ... */}
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-3xl font-bold tracking-tight">仪表盘</h2>
|
<h2 className="text-3xl font-bold tracking-tight">仪表盘</h2>
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
@@ -82,9 +100,25 @@ export default function AdminDashboard({ stats }: { stats: DashboardStats }) {
|
|||||||
<CardTitle>近期概览</CardTitle>
|
<CardTitle>近期概览</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="pl-2">
|
<CardContent className="pl-2">
|
||||||
<div className="h-[200px] flex items-center justify-center text-muted-foreground">
|
<ResponsiveContainer width="100%" height={350}>
|
||||||
图表组件待集成 (Recharts)
|
<BarChart data={data}>
|
||||||
</div>
|
<XAxis
|
||||||
|
dataKey="name"
|
||||||
|
stroke="#888888"
|
||||||
|
fontSize={12}
|
||||||
|
tickLine={false}
|
||||||
|
axisLine={false}
|
||||||
|
/>
|
||||||
|
<YAxis
|
||||||
|
stroke="#888888"
|
||||||
|
fontSize={12}
|
||||||
|
tickLine={false}
|
||||||
|
axisLine={false}
|
||||||
|
tickFormatter={(value) => `¥${value}`}
|
||||||
|
/>
|
||||||
|
<Bar dataKey="total" fill="#000000" radius={[4, 4, 0, 0]} />
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<Card className="col-span-3">
|
<Card className="col-span-3">
|
||||||
|
|||||||
@@ -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) {
|
if (article.价格 === 0) {
|
||||||
hasAccess = true;
|
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 not yet accessible and user is logged in, check purchase/membership
|
||||||
if (!hasAccess && userId) {
|
if (!hasAccess && userId) {
|
||||||
const user = await User.findById(userId);
|
const user = await User.findById(userId);
|
||||||
|
const isMember = user?.会员信息?.过期时间 && new Date(user.会员信息.过期时间) > new Date();
|
||||||
|
|
||||||
// Check Membership
|
// 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;
|
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
|
// Check if purchased
|
||||||
if (!hasAccess) {
|
if (!hasAccess) {
|
||||||
const order = await Order.findOne({
|
const order = await Order.findOne({
|
||||||
@@ -104,7 +110,16 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
|
|
||||||
if (!hasAccess) {
|
if (!hasAccess) {
|
||||||
// Hide sensitive content
|
// Hide sensitive content
|
||||||
delete articleData.资源属性;
|
if (articleData.资源属性) {
|
||||||
|
// Keep public attributes visible
|
||||||
|
const publicAttributes = {
|
||||||
|
版本号: articleData.资源属性.版本号,
|
||||||
|
文件大小: articleData.资源属性.文件大小,
|
||||||
|
扩展属性: articleData.资源属性.扩展属性
|
||||||
|
};
|
||||||
|
articleData.资源属性 = publicAttributes;
|
||||||
|
}
|
||||||
|
|
||||||
// Optional: Truncate content for preview
|
// Optional: Truncate content for preview
|
||||||
articleData.正文内容 = await processMarkdown(article.正文内容.substring(0, 300) + '...');
|
articleData.正文内容 = await processMarkdown(article.正文内容.substring(0, 300) + '...');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,11 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
return res.status(401).json({ message: '邮箱或密码错误' });
|
return res.status(401).json({ message: '邮箱或密码错误' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查用户是否被封禁
|
||||||
|
if (user.是否被封禁) {
|
||||||
|
return res.status(403).json({ message: '该账号已被封禁,请联系管理员' });
|
||||||
|
}
|
||||||
|
|
||||||
// 生成 JWT
|
// 生成 JWT
|
||||||
const token = jwt.sign(
|
const token = jwt.sign(
|
||||||
{ userId: user._id, email: user.邮箱, role: user.角色 },
|
{ userId: user._id, email: user.邮箱, role: user.角色 },
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { NextApiRequest, NextApiResponse } from 'next';
|
import { NextApiRequest, NextApiResponse } from 'next';
|
||||||
import dbConnect from '@/lib/dbConnect';
|
import dbConnect from '@/lib/dbConnect';
|
||||||
import { Order, User, MembershipPlan } from '@/models';
|
import { Order, User, MembershipPlan, Article } from '@/models';
|
||||||
import { alipayService } from '@/lib/alipay';
|
import { alipayService } from '@/lib/alipay';
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
@@ -71,8 +71,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
order.支付方式 = 'alipay';
|
order.支付方式 = 'alipay';
|
||||||
await order.save();
|
await order.save();
|
||||||
|
|
||||||
// Update User Membership
|
// Handle different order types
|
||||||
if (order.订单类型 === 'buy_membership') {
|
if (order.订单类型 === 'buy_membership') {
|
||||||
|
// Update User Membership
|
||||||
const plan = await MembershipPlan.findById(order.商品ID);
|
const plan = await MembershipPlan.findById(order.商品ID);
|
||||||
if (plan) {
|
if (plan) {
|
||||||
const user = await User.findById(order.用户ID);
|
const user = await User.findById(order.用户ID);
|
||||||
@@ -89,6 +90,14 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
await user.save();
|
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`);
|
console.log(`Order ${outTradeNo} marked as paid`);
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
|
import Image from 'next/image';
|
||||||
|
import Link from 'next/link';
|
||||||
import { useAuth } from '@/hooks/useAuth';
|
import { useAuth } from '@/hooks/useAuth';
|
||||||
import MainLayout from '@/components/layouts/MainLayout';
|
import MainLayout from '@/components/layouts/MainLayout';
|
||||||
import CommentSection from '@/components/article/CommentSection';
|
import CommentSection from '@/components/article/CommentSection';
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
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 { format } from 'date-fns';
|
||||||
import { zhCN } from 'date-fns/locale';
|
import { zhCN } from 'date-fns/locale';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
@@ -22,22 +26,27 @@ interface Article {
|
|||||||
头像: string;
|
头像: string;
|
||||||
};
|
};
|
||||||
分类ID: {
|
分类ID: {
|
||||||
|
_id: string;
|
||||||
分类名称: string;
|
分类名称: string;
|
||||||
};
|
};
|
||||||
标签ID列表: {
|
标签ID列表: {
|
||||||
标签名称: string;
|
标签名称: string;
|
||||||
}[];
|
}[];
|
||||||
价格: number;
|
价格: number;
|
||||||
支付方式: 'points' | 'cash';
|
支付方式: 'points' | 'cash' | 'membership_free';
|
||||||
资源属性?: {
|
资源属性?: {
|
||||||
下载链接: string;
|
下载链接: string;
|
||||||
提取码: string;
|
提取码: string;
|
||||||
解压密码: string;
|
解压密码: string;
|
||||||
隐藏内容: string;
|
隐藏内容: string;
|
||||||
|
版本号?: string;
|
||||||
|
文件大小?: string;
|
||||||
|
扩展属性?: { 属性名: string; 属性值: string }[];
|
||||||
};
|
};
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
统计数据: {
|
统计数据: {
|
||||||
阅读数: number;
|
阅读数: number;
|
||||||
|
销量: number;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,13 +55,16 @@ export default function ArticleDetail() {
|
|||||||
const { id } = router.query;
|
const { id } = router.query;
|
||||||
const { user, loading: authLoading } = useAuth();
|
const { user, loading: authLoading } = useAuth();
|
||||||
const [article, setArticle] = useState<Article | null>(null);
|
const [article, setArticle] = useState<Article | null>(null);
|
||||||
|
const [recentArticles, setRecentArticles] = useState<Article[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [hasAccess, setHasAccess] = useState(false);
|
const [hasAccess, setHasAccess] = useState(false);
|
||||||
const [purchasing, setPurchasing] = useState(false);
|
const [purchasing, setPurchasing] = useState(false);
|
||||||
|
const [copiedField, setCopiedField] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (id) {
|
if (id) {
|
||||||
fetchArticle();
|
fetchArticle();
|
||||||
|
fetchRecentArticles();
|
||||||
}
|
}
|
||||||
}, [id]);
|
}, [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 () => {
|
const handlePurchase = async () => {
|
||||||
if (!user) {
|
if (!user) {
|
||||||
router.push(`/auth/login?redirect=${encodeURIComponent(router.asPath)}`);
|
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) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<MainLayout>
|
<MainLayout>
|
||||||
<div className="container mx-auto px-4 py-8 max-w-4xl">
|
<div className="container mx-auto px-4 py-8 max-w-6xl">
|
||||||
<div className="space-y-4 mb-8">
|
<div className="grid grid-cols-1 lg:grid-cols-4 gap-8">
|
||||||
<Skeleton className="h-12 w-3/4" />
|
<div className="lg:col-span-3 space-y-8">
|
||||||
<div className="flex gap-4">
|
<Skeleton className="h-12 w-3/4" />
|
||||||
<Skeleton className="h-4 w-24" />
|
<Skeleton className="h-[400px] w-full rounded-xl" />
|
||||||
<Skeleton className="h-4 w-24" />
|
<div className="space-y-4">
|
||||||
|
<Skeleton className="h-4 w-full" />
|
||||||
|
<Skeleton className="h-4 w-full" />
|
||||||
|
<Skeleton className="h-4 w-5/6" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="lg:col-span-1 space-y-6">
|
||||||
|
<Skeleton className="h-[300px] w-full rounded-xl" />
|
||||||
|
<Skeleton className="h-[200px] w-full rounded-xl" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<Skeleton className="h-[400px] w-full rounded-xl mb-8" />
|
|
||||||
<div className="space-y-4">
|
|
||||||
<Skeleton className="h-4 w-full" />
|
|
||||||
<Skeleton className="h-4 w-full" />
|
|
||||||
<Skeleton className="h-4 w-5/6" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
@@ -145,137 +192,271 @@ export default function ArticleDetail() {
|
|||||||
keywords: article.标签ID列表?.map(t => t.标签名称).join(',')
|
keywords: article.标签ID列表?.map(t => t.标签名称).join(',')
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="max-w-4xl mx-auto px-4 py-8">
|
<div className="max-w-7xl mx-auto px-4 py-8">
|
||||||
{/* Article Header */}
|
<div className="grid grid-cols-1 lg:grid-cols-4 gap-8">
|
||||||
<div className="mb-8">
|
{/* Main Content - Left Column (75%) */}
|
||||||
<div className="flex items-center gap-2 mb-4">
|
<div className="lg:col-span-3">
|
||||||
<Badge variant="secondary" className="text-primary bg-primary/10 hover:bg-primary/20">
|
{/* Article Header */}
|
||||||
{article.分类ID?.分类名称 || '未分类'}
|
<div className="mb-8">
|
||||||
</Badge>
|
<div className="flex items-center gap-2 mb-4">
|
||||||
{article.标签ID列表?.map((tag, index) => (
|
<Badge variant="secondary" className="text-primary bg-primary/10 hover:bg-primary/20">
|
||||||
<Badge key={index} variant="outline" className="text-gray-500">
|
{article.分类ID?.分类名称 || '未分类'}
|
||||||
{tag.标签名称}
|
</Badge>
|
||||||
</Badge>
|
{article.标签ID列表?.map((tag, index) => (
|
||||||
))}
|
<Badge key={index} variant="outline" className="text-gray-500">
|
||||||
</div>
|
{tag.标签名称}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
<h1 className="text-3xl md:text-4xl font-bold text-gray-900 mb-6 leading-tight">
|
<h1 className="text-3xl md:text-4xl font-bold text-gray-900 mb-6 leading-tight">
|
||||||
{article.文章标题}
|
{article.文章标题}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<div className="flex items-center gap-6 text-sm text-gray-500 border-b border-gray-100 pb-8">
|
<div className="flex flex-wrap items-center justify-between gap-4 border-b border-gray-100 pb-8">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-6 text-sm text-gray-500">
|
||||||
<UserIcon className="w-4 h-4" />
|
<div className="flex items-center gap-2">
|
||||||
<span>{article.作者ID?.用户名 || '匿名'}</span>
|
<UserIcon className="w-4 h-4" />
|
||||||
</div>
|
<span>{article.作者ID?.用户名 || '匿名'}</span>
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Calendar className="w-4 h-4" />
|
|
||||||
<span>{format(new Date(article.createdAt), 'yyyy-MM-dd', { locale: zhCN })}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
阅读 {article.统计数据?.阅读数 || 0}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Cover Image */}
|
|
||||||
{article.封面图 && (
|
|
||||||
<div className="mb-10 rounded-xl overflow-hidden shadow-lg">
|
|
||||||
<img
|
|
||||||
src={article.封面图}
|
|
||||||
alt={article.文章标题}
|
|
||||||
className="w-full h-auto object-cover max-h-[500px]"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Article Content */}
|
|
||||||
<article className="prose prose-lg max-w-none mb-12 prose-headings:text-gray-900 prose-p:text-gray-700 prose-a:text-primary hover:prose-a:text-primary/80 prose-img:rounded-xl dark:prose-invert">
|
|
||||||
<div dangerouslySetInnerHTML={{ __html: article.正文内容 }} />
|
|
||||||
</article>
|
|
||||||
|
|
||||||
{/* Resource Download / Paywall Section */}
|
|
||||||
<div className="bg-gray-50 rounded-xl p-8 mb-12 border border-gray-100">
|
|
||||||
{hasAccess ? (
|
|
||||||
<div className="space-y-6">
|
|
||||||
<h3 className="text-xl font-bold flex items-center gap-2 text-green-700">
|
|
||||||
<Download className="w-6 h-6" />
|
|
||||||
资源下载
|
|
||||||
</h3>
|
|
||||||
{article.资源属性 ? (
|
|
||||||
<div className="grid gap-4 md:grid-cols-2">
|
|
||||||
<div className="bg-white p-4 rounded-lg border border-gray-200">
|
|
||||||
<span className="text-gray-500 text-sm block mb-1">下载链接</span>
|
|
||||||
<a
|
|
||||||
href={article.资源属性.下载链接}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="text-primary font-medium hover:underline break-all"
|
|
||||||
>
|
|
||||||
{article.资源属性.下载链接}
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
{article.资源属性.提取码 && (
|
<div className="flex items-center gap-2">
|
||||||
<div className="bg-white p-4 rounded-lg border border-gray-200">
|
<Calendar className="w-4 h-4" />
|
||||||
<span className="text-gray-500 text-sm block mb-1">提取码</span>
|
<span>{format(new Date(article.createdAt), 'yyyy-MM-dd', { locale: zhCN })}</span>
|
||||||
<code className="bg-gray-100 px-2 py-1 rounded text-gray-900 font-mono">
|
</div>
|
||||||
{article.资源属性.提取码}
|
<div className="flex items-center gap-2">
|
||||||
</code>
|
<Eye className="w-4 h-4" />
|
||||||
|
<span>{article.统计数据?.阅读数 || 0} 阅读</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button variant="ghost" size="sm" onClick={handleShare} className="text-gray-500 hover:text-primary">
|
||||||
|
<Share2 className="w-4 h-4 mr-2" />
|
||||||
|
分享
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Cover Image */}
|
||||||
|
{article.封面图 && (
|
||||||
|
<div className="mb-10 rounded-xl overflow-hidden shadow-lg relative aspect-video bg-gray-100">
|
||||||
|
<Image
|
||||||
|
src={article.封面图}
|
||||||
|
alt={article.文章标题}
|
||||||
|
fill
|
||||||
|
className="object-cover"
|
||||||
|
priority
|
||||||
|
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 80vw, 800px"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Article Content */}
|
||||||
|
<article className="prose prose-lg max-w-none mb-12 prose-headings:text-gray-900 prose-p:text-gray-700 prose-a:text-primary hover:prose-a:text-primary/80 prose-img:rounded-xl dark:prose-invert">
|
||||||
|
<div dangerouslySetInnerHTML={{ __html: article.正文内容 }} />
|
||||||
|
</article>
|
||||||
|
|
||||||
|
{/* Comments Section */}
|
||||||
|
<CommentSection articleId={article._id} isLoggedIn={!!user} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Sidebar - Right Column (25%) */}
|
||||||
|
<div className="lg:col-span-1 space-y-6">
|
||||||
|
{/* Resource Card */}
|
||||||
|
<Card className="border-primary/20 shadow-md overflow-hidden">
|
||||||
|
<div className="bg-linear-to-r from-primary/10 to-primary/5 p-4 border-b border-primary/10">
|
||||||
|
<h3 className="font-bold text-lg flex items-center gap-2 text-gray-900">
|
||||||
|
<Download className="w-5 h-5 text-primary" />
|
||||||
|
资源下载
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<CardContent className="p-6 space-y-6">
|
||||||
|
<div className="flex items-baseline justify-between">
|
||||||
|
<span className="text-sm text-gray-500">资源价格</span>
|
||||||
|
<div className="flex items-baseline gap-1">
|
||||||
|
<span className="text-2xl font-bold text-primary">
|
||||||
|
{article.价格 > 0 ? `¥${article.价格}` : '免费'}
|
||||||
|
</span>
|
||||||
|
{article.价格 > 0 && <span className="text-xs text-gray-400">/ 永久</span>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Resource Attributes (Version, Size, etc.) */}
|
||||||
|
{article.资源属性 && (
|
||||||
|
<div className="grid grid-cols-2 gap-2 text-xs text-gray-600 bg-gray-50 p-3 rounded-lg border border-gray-100">
|
||||||
|
{article.资源属性.版本号 && (
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<Box className="w-3.5 h-3.5 text-gray-400" />
|
||||||
|
<span>版本: {article.资源属性.版本号}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{article.资源属性.文件大小 && (
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<HardDrive className="w-3.5 h-3.5 text-gray-400" />
|
||||||
|
<span>大小: {article.资源属性.文件大小}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{article.资源属性.扩展属性?.map((attr, idx) => (
|
||||||
|
<div key={idx} className="flex items-center gap-1.5 col-span-2">
|
||||||
|
<TagIcon className="w-3.5 h-3.5 text-gray-400" />
|
||||||
|
<span>{attr.属性名}: {attr.属性值}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{hasAccess ? (
|
||||||
|
<div className="space-y-4 animate-in fade-in duration-500">
|
||||||
|
<div className="bg-green-50 text-green-700 px-4 py-3 rounded-lg text-sm flex items-center gap-2 border border-green-100">
|
||||||
|
<Check className="w-4 h-4" />
|
||||||
|
您已拥有此资源权限
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
{article.资源属性.解压密码 && (
|
{article.资源属性 && (
|
||||||
<div className="bg-white p-4 rounded-lg border border-gray-200">
|
<div className="space-y-3">
|
||||||
<span className="text-gray-500 text-sm block mb-1">解压密码</span>
|
<Button className="w-full" asChild>
|
||||||
<code className="bg-gray-100 px-2 py-1 rounded text-gray-900 font-mono">
|
<a href={article.资源属性.下载链接} target="_blank" rel="noopener noreferrer">
|
||||||
{article.资源属性.解压密码}
|
<Download className="w-4 h-4 mr-2" />
|
||||||
</code>
|
立即下载
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{article.资源属性.提取码 && (
|
||||||
|
<div className="flex items-center justify-between bg-gray-50 p-2 rounded text-sm border border-gray-100">
|
||||||
|
<span className="text-gray-500 pl-1">提取码: {article.资源属性.提取码}</span>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-6 w-6"
|
||||||
|
onClick={() => copyToClipboard(article.资源属性!.提取码, 'code')}
|
||||||
|
>
|
||||||
|
{copiedField === 'code' ? <Check className="w-3 h-3 text-green-600" /> : <Copy className="w-3 h-3" />}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{article.资源属性.解压密码 && (
|
||||||
|
<div className="flex items-center justify-between bg-gray-50 p-2 rounded text-sm border border-gray-100">
|
||||||
|
<span className="text-gray-500 pl-1">解压密码: {article.资源属性.解压密码}</span>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-6 w-6"
|
||||||
|
onClick={() => copyToClipboard(article.资源属性!.解压密码, 'pwd')}
|
||||||
|
>
|
||||||
|
{copiedField === 'pwd' ? <Check className="w-3 h-3 text-green-600" /> : <Copy className="w-3 h-3" />}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{article.资源属性.隐藏内容 && (
|
||||||
|
<div className="bg-amber-50 p-3 rounded text-sm border border-amber-100 text-amber-800">
|
||||||
|
<div className="font-medium mb-1 flex items-center gap-1">
|
||||||
|
<Lock className="w-3 h-3" /> 付费内容
|
||||||
|
</div>
|
||||||
|
<div className="whitespace-pre-wrap text-xs opacity-90">{article.资源属性.隐藏内容}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Button
|
||||||
|
className="w-full bg-linear-to-r from-primary to-purple-600 hover:from-primary/90 hover:to-purple-600/90 text-white shadow-lg shadow-primary/20"
|
||||||
|
onClick={handlePurchase}
|
||||||
|
disabled={purchasing}
|
||||||
|
>
|
||||||
|
{purchasing ? (
|
||||||
|
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<ShoppingCart className="w-4 h-4 mr-2" />
|
||||||
|
)}
|
||||||
|
立即购买
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{article.支付方式 === 'membership_free' ? (
|
||||||
|
<div className="text-center">
|
||||||
|
<Link href="/membership" className="text-xs text-primary font-medium hover:underline flex items-center justify-center gap-1">
|
||||||
|
<Crown className="w-3 h-3 text-yellow-500" />
|
||||||
|
会员免费下载 (点击开通)
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-center">
|
||||||
|
<Link href="/membership" className="text-xs text-gray-500 hover:text-primary flex items-center justify-center gap-1">
|
||||||
|
<Crown className="w-3 h-3 text-yellow-500" />
|
||||||
|
开通会员享受折扣
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="pt-4 border-t border-gray-100 grid grid-cols-2 gap-4 text-center text-sm text-gray-500">
|
||||||
|
<div>
|
||||||
|
<div className="text-gray-900 font-medium">{article.统计数据?.阅读数 || 0}</div>
|
||||||
|
<div className="text-xs mt-1">浏览次数</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-gray-900 font-medium">{article.统计数据?.销量 || 0}</div>
|
||||||
|
<div className="text-xs mt-1">最近售出</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Recent Posts Card */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<CardTitle className="text-base flex items-center gap-2">
|
||||||
|
<FileText className="w-4 h-4 text-primary" />
|
||||||
|
近期推荐
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="px-0 pb-2">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
{recentArticles.length > 0 ? (
|
||||||
|
recentArticles.map((item, index) => (
|
||||||
|
<Link
|
||||||
|
key={item._id}
|
||||||
|
href={`/article/${item._id}`}
|
||||||
|
className="group flex items-start gap-3 px-6 py-3 hover:bg-gray-50 transition-colors border-b border-gray-50 last:border-0"
|
||||||
|
>
|
||||||
|
<div className="relative w-16 h-12 shrink-0 rounded overflow-hidden bg-gray-100">
|
||||||
|
{item.封面图 ? (
|
||||||
|
<Image
|
||||||
|
src={item.封面图}
|
||||||
|
alt={item.文章标题}
|
||||||
|
fill
|
||||||
|
className="object-cover group-hover:scale-105 transition-transform duration-300"
|
||||||
|
sizes="64px"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="w-full h-full flex items-center justify-center text-gray-300">
|
||||||
|
<FileText className="w-6 h-6" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<h4 className="text-sm font-medium text-gray-900 line-clamp-2 group-hover:text-primary transition-colors">
|
||||||
|
{item.文章标题}
|
||||||
|
</h4>
|
||||||
|
<div className="flex items-center gap-2 mt-1 text-xs text-gray-400">
|
||||||
|
<span>{format(new Date(item.createdAt), 'MM-dd')}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="px-6 py-4 text-sm text-gray-500 text-center">
|
||||||
|
暂无推荐
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
</CardContent>
|
||||||
<p className="text-gray-500">此资源暂无下载信息</p>
|
</Card>
|
||||||
)}
|
</div>
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="text-center py-8">
|
|
||||||
<div className="w-16 h-16 bg-blue-100 rounded-full flex items-center justify-center mx-auto mb-6">
|
|
||||||
<Lock className="w-8 h-8 text-primary" />
|
|
||||||
</div>
|
|
||||||
<h3 className="text-2xl font-bold text-gray-900 mb-2">
|
|
||||||
此内容需要付费查看
|
|
||||||
</h3>
|
|
||||||
<p className="text-gray-600 mb-8 max-w-md mx-auto">
|
|
||||||
购买此资源或开通会员,即可解锁全文及下载链接,享受更多优质内容。
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
|
||||||
<Button
|
|
||||||
size="lg"
|
|
||||||
className="px-8"
|
|
||||||
onClick={handlePurchase}
|
|
||||||
disabled={purchasing}
|
|
||||||
>
|
|
||||||
{purchasing ? (
|
|
||||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<span className="mr-2">¥{article.价格}</span>
|
|
||||||
)}
|
|
||||||
立即购买
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="lg"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => router.push('/membership')}
|
|
||||||
>
|
|
||||||
开通会员 (免费下载)
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Comments Section */}
|
|
||||||
<CommentSection articleId={article._id} isLoggedIn={!!user} />
|
|
||||||
</div>
|
</div>
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import Link from 'next/link';
|
|||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import * as z from '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 { useAuth } from '@/hooks/useAuth';
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from '@/components/ui/form';
|
} from '@/components/ui/form';
|
||||||
import { Input } from '@/components/ui/input';
|
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({
|
const formSchema = z.object({
|
||||||
email: z.string().email({ message: "请输入有效的邮箱地址" }),
|
email: z.string().email({ message: "请输入有效的邮箱地址" }),
|
||||||
@@ -69,25 +69,59 @@ export default function LoginPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen items-center justify-center bg-gray-50 px-4 py-12 sm:px-6 lg:px-8">
|
<div className="min-h-screen grid lg:grid-cols-2">
|
||||||
<Card className="w-full max-w-md">
|
{/* Left Panel - Visual & Branding */}
|
||||||
<CardHeader className="space-y-1">
|
<div className="hidden lg:flex flex-col justify-between bg-black text-white p-12 relative overflow-hidden">
|
||||||
<CardTitle className="text-2xl font-bold text-center">登录账号</CardTitle>
|
{/* Abstract Background */}
|
||||||
<CardDescription className="text-center">
|
<div className="absolute inset-0 bg-[url('https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?q=80&w=2564&auto=format&fit=crop')] bg-cover bg-center opacity-40"></div>
|
||||||
请输入您的邮箱和密码进行登录
|
<div className="absolute inset-0 bg-linear-to-br from-black via-black/80 to-transparent"></div>
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
{/* Content */}
|
||||||
<CardContent>
|
<div className="relative z-10">
|
||||||
|
<div className="flex items-center gap-2 text-2xl font-bold">
|
||||||
|
<div className="w-8 h-8 rounded-lg bg-white text-black flex items-center justify-center">
|
||||||
|
<Sparkles className="w-5 h-5" />
|
||||||
|
</div>
|
||||||
|
AOUN AI
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative z-10 space-y-6 max-w-lg">
|
||||||
|
<h1 className="text-5xl font-bold leading-tight tracking-tight">
|
||||||
|
释放您的<br />
|
||||||
|
<span className="text-transparent bg-clip-text bg-linear-to-r from-blue-400 to-purple-400">无限创意潜能</span>
|
||||||
|
</h1>
|
||||||
|
<p className="text-lg text-gray-300 leading-relaxed">
|
||||||
|
加入数万名创作者的行列,利用最先进的 AI 技术,将您的想法瞬间转化为现实。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative z-10 text-sm text-gray-400">
|
||||||
|
© 2024 AOUN AI. All rights reserved.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right Panel - Login Form */}
|
||||||
|
<div className="flex items-center justify-center p-8 bg-white">
|
||||||
|
<div className="w-full max-w-md space-y-8">
|
||||||
|
<div className="text-center space-y-2">
|
||||||
|
<h2 className="text-3xl font-bold tracking-tight text-gray-900">欢迎回来</h2>
|
||||||
|
<p className="text-gray-500">请输入您的账号信息以继续</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="email"
|
name="email"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>邮箱</FormLabel>
|
<FormLabel>邮箱地址</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="name@example.com" {...field} />
|
<div className="relative">
|
||||||
|
<Mail className="absolute left-3 top-3 h-4 w-4 text-gray-400" />
|
||||||
|
<Input placeholder="name@example.com" className="pl-10 h-11" {...field} />
|
||||||
|
</div>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -100,33 +134,67 @@ export default function LoginPage() {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>密码</FormLabel>
|
<FormLabel>密码</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input type="password" placeholder="******" {...field} />
|
<div className="relative">
|
||||||
|
<Lock className="absolute left-3 top-3 h-4 w-4 text-gray-400" />
|
||||||
|
<Input type="password" placeholder="******" className="pl-10 h-11" {...field} />
|
||||||
|
</div>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Checkbox id="remember" />
|
||||||
|
<label
|
||||||
|
htmlFor="remember"
|
||||||
|
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-gray-500"
|
||||||
|
>
|
||||||
|
记住我
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<Link href="#" className="text-sm font-medium text-primary hover:underline">
|
||||||
|
忘记密码?
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="text-sm text-destructive text-center">{error}</div>
|
<div className="p-3 rounded-md bg-red-50 text-red-500 text-sm text-center font-medium">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Button type="submit" className="w-full" disabled={isLoading}>
|
<Button type="submit" className="w-full h-11 text-base" disabled={isLoading}>
|
||||||
{isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
{isLoading ? (
|
||||||
登录
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
立即登录 <ArrowRight className="w-4 h-4" />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</CardContent>
|
|
||||||
<CardFooter className="flex justify-center">
|
<div className="relative">
|
||||||
<p className="text-sm text-muted-foreground">
|
<div className="absolute inset-0 flex items-center">
|
||||||
还没有账号?{' '}
|
<span className="w-full border-t border-gray-100" />
|
||||||
<Link href="/auth/register" className="text-primary hover:underline">
|
</div>
|
||||||
立即注册
|
<div className="relative flex justify-center text-xs uppercase">
|
||||||
|
<span className="bg-white px-2 text-gray-500">
|
||||||
|
还没有账号?
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-center">
|
||||||
|
<Link href="/auth/register" className="text-primary font-semibold hover:underline">
|
||||||
|
免费注册账号
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</div>
|
||||||
</CardFooter>
|
</div>
|
||||||
</Card>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import Link from 'next/link';
|
|||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import * as z from '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 { Button } from '@/components/ui/button';
|
||||||
import {
|
import {
|
||||||
@@ -16,7 +16,6 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from '@/components/ui/form';
|
} from '@/components/ui/form';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
|
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
username: z.string().min(2, { message: "用户名至少需要2个字符" }),
|
username: z.string().min(2, { message: "用户名至少需要2个字符" }),
|
||||||
@@ -76,15 +75,46 @@ export default function RegisterPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen items-center justify-center bg-gray-50 px-4 py-12 sm:px-6 lg:px-8">
|
<div className="min-h-screen grid lg:grid-cols-2">
|
||||||
<Card className="w-full max-w-md">
|
{/* Left Panel - Visual & Branding */}
|
||||||
<CardHeader className="space-y-1">
|
<div className="hidden lg:flex flex-col justify-between bg-black text-white p-12 relative overflow-hidden">
|
||||||
<CardTitle className="text-2xl font-bold text-center">注册账号</CardTitle>
|
{/* Abstract Background */}
|
||||||
<CardDescription className="text-center">
|
<div className="absolute inset-0 bg-[url('https://images.unsplash.com/photo-1620641788421-7a1c342ea42e?q=80&w=2574&auto=format&fit=crop')] bg-cover bg-center opacity-40"></div>
|
||||||
创建一个新账号以开始使用
|
<div className="absolute inset-0 bg-linear-to-br from-black via-black/80 to-transparent"></div>
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
{/* Content */}
|
||||||
<CardContent>
|
<div className="relative z-10">
|
||||||
|
<div className="flex items-center gap-2 text-2xl font-bold">
|
||||||
|
<div className="w-8 h-8 rounded-lg bg-white text-black flex items-center justify-center">
|
||||||
|
<Sparkles className="w-5 h-5" />
|
||||||
|
</div>
|
||||||
|
AOUN AI
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative z-10 space-y-6 max-w-lg">
|
||||||
|
<h1 className="text-5xl font-bold leading-tight tracking-tight">
|
||||||
|
开启您的<br />
|
||||||
|
<span className="text-transparent bg-clip-text bg-linear-to-r from-purple-400 to-pink-400">智能创作之旅</span>
|
||||||
|
</h1>
|
||||||
|
<p className="text-lg text-gray-300 leading-relaxed">
|
||||||
|
注册即可获得 AI 积分,体验 GPT-4、Claude 3 等顶级模型的强大能力。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative z-10 text-sm text-gray-400">
|
||||||
|
© 2024 AOUN AI. All rights reserved.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right Panel - Register Form */}
|
||||||
|
<div className="flex items-center justify-center p-8 bg-white">
|
||||||
|
<div className="w-full max-w-md space-y-8">
|
||||||
|
<div className="text-center space-y-2">
|
||||||
|
<h2 className="text-3xl font-bold tracking-tight text-gray-900">创建账号</h2>
|
||||||
|
<p className="text-gray-500">填写以下信息以完成注册</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||||
<FormField
|
<FormField
|
||||||
@@ -94,7 +124,10 @@ export default function RegisterPage() {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>用户名</FormLabel>
|
<FormLabel>用户名</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="johndoe" {...field} />
|
<div className="relative">
|
||||||
|
<User className="absolute left-3 top-3 h-4 w-4 text-gray-400" />
|
||||||
|
<Input placeholder="johndoe" className="pl-10 h-11" {...field} />
|
||||||
|
</div>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -105,9 +138,12 @@ export default function RegisterPage() {
|
|||||||
name="email"
|
name="email"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>邮箱</FormLabel>
|
<FormLabel>邮箱地址</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="name@example.com" {...field} />
|
<div className="relative">
|
||||||
|
<Mail className="absolute left-3 top-3 h-4 w-4 text-gray-400" />
|
||||||
|
<Input placeholder="name@example.com" className="pl-10 h-11" {...field} />
|
||||||
|
</div>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -120,7 +156,10 @@ export default function RegisterPage() {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>密码</FormLabel>
|
<FormLabel>密码</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input type="password" placeholder="******" {...field} />
|
<div className="relative">
|
||||||
|
<Lock className="absolute left-3 top-3 h-4 w-4 text-gray-400" />
|
||||||
|
<Input type="password" placeholder="******" className="pl-10 h-11" {...field} />
|
||||||
|
</div>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -133,7 +172,10 @@ export default function RegisterPage() {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>确认密码</FormLabel>
|
<FormLabel>确认密码</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input type="password" placeholder="******" {...field} />
|
<div className="relative">
|
||||||
|
<Lock className="absolute left-3 top-3 h-4 w-4 text-gray-400" />
|
||||||
|
<Input type="password" placeholder="******" className="pl-10 h-11" {...field} />
|
||||||
|
</div>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -141,25 +183,41 @@ export default function RegisterPage() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="text-sm text-destructive text-center">{error}</div>
|
<div className="p-3 rounded-md bg-red-50 text-red-500 text-sm text-center font-medium">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Button type="submit" className="w-full" disabled={isLoading}>
|
<Button type="submit" className="w-full h-11 text-base mt-2" disabled={isLoading}>
|
||||||
{isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
{isLoading ? (
|
||||||
注册
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
立即注册 <ArrowRight className="w-4 h-4" />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</CardContent>
|
|
||||||
<CardFooter className="flex justify-center">
|
<div className="relative">
|
||||||
<p className="text-sm text-muted-foreground">
|
<div className="absolute inset-0 flex items-center">
|
||||||
已有账号?{' '}
|
<span className="w-full border-t border-gray-100" />
|
||||||
<Link href="/auth/login" className="text-primary hover:underline">
|
</div>
|
||||||
立即登录
|
<div className="relative flex justify-center text-xs uppercase">
|
||||||
|
<span className="bg-white px-2 text-gray-500">
|
||||||
|
已有账号?
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-center">
|
||||||
|
<Link href="/auth/login" className="text-primary font-semibold hover:underline">
|
||||||
|
直接登录
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</div>
|
||||||
</CardFooter>
|
</div>
|
||||||
</Card>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,13 +6,56 @@ import Sidebar from '@/components/home/Sidebar';
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Loader2 } from 'lucide-react';
|
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() {
|
export default function Home() {
|
||||||
const [articles, setArticles] = useState<any[]>([]);
|
const [articles, setArticles] = useState<Article[]>([]);
|
||||||
const [categories, setCategories] = useState<any[]>([]);
|
const [categories, setCategories] = useState<Category[]>([]);
|
||||||
const [tags, setTags] = useState<any[]>([]);
|
const [tags, setTags] = useState<Tag[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [activeCategory, setActiveCategory] = useState('all');
|
const [activeCategory, setActiveCategory] = useState('all');
|
||||||
const [banners, setBanners] = useState<any[]>([]);
|
const [banners, setBanners] = useState<Banner[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchData();
|
fetchData();
|
||||||
@@ -108,7 +151,16 @@ export default function Home() {
|
|||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||||
{articles.map(article => (
|
{articles.map(article => (
|
||||||
<ArticleCard key={article._id} article={article} />
|
<ArticleCard
|
||||||
|
key={article._id}
|
||||||
|
article={{
|
||||||
|
...article,
|
||||||
|
摘要: article.摘要 || '',
|
||||||
|
封面图: article.封面图 || '',
|
||||||
|
价格: article.价格 || 0,
|
||||||
|
分类ID: article.分类ID || { 分类名称: '未分类' }
|
||||||
|
}}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user