jerry před 4 měsíci
rodič
revize
5a93762410

+ 28 - 0
Dockerfile

@@ -0,0 +1,28 @@
+FROM node:18-alpine AS deps
+RUN apk add --no-cache libc6-compat
+WORKDIR /app
+COPY package.json package-lock.json* ./
+RUN npm ci
+
+FROM node:18-alpine AS builder
+WORKDIR /app
+COPY --from=deps /app/node_modules ./node_modules
+COPY . .
+ENV NEXT_TELEMETRY_DISABLED 1
+RUN npm run build
+
+FROM node:18-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
+
+COPY --from=builder /app/public ./public
+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
+CMD ["node", "server.js"]

+ 16 - 0
docker-compose.yml

@@ -0,0 +1,16 @@
+version: '3.8'
+
+services:
+  next-web-ui:
+    container_name: next-web-ui
+    build:
+      context: .
+      dockerfile: Dockerfile
+    restart: always
+    ports:
+      # 格式: "宿主机IP:宿主机端口:容器端口"
+      # 将容器的 3000 映射到宿主机的 3000,且仅限本机访问
+      - "127.0.0.1:3000:3000"
+    environment:
+      - NODE_ENV=production
+      # 如果需要连接同在宿主机上的后端(非Docker),可以使用 host.docker.internal (需配置) 或直接填公网/局域网IP

+ 1 - 1
next.config.js

@@ -1,6 +1,6 @@
 /** @type {import('next').NextConfig} */
 const nextConfig = {
-
+  output: "standalone",
   async rewrites() {
     return [
       {

+ 7 - 2
src/app/dashboard/page.tsx

@@ -258,7 +258,7 @@ export default function DashboardPage() {
         title={t('nav.logout')}
         message={t('dashboard.logout_confirm')}
         onConfirm={performLogout}
-        onClose={() => setIsLogoutConfirmOpen(false)}
+        onCancel={() => setIsLogoutConfirmOpen(false)}
       />
 
       {/* 3. 通用消息提示框 (新增) */}
@@ -273,7 +273,12 @@ export default function DashboardPage() {
       {/* 4. 业务弹窗 */}
       <TicketModal isOpen={isTicketModalOpen} onClose={() => setIsTicketModalOpen(false)} defaultOrderId={ticketDefaultOrderId} />
       <UserOrderDetailModal isOpen={isOrderDetailOpen} onClose={() => setIsOrderDetailOpen(false)} order={selectedOrder} />
-      <UserTicketDetailModal isOpen={isTicketDetailOpen} onClose={() => setIsTicketDetailOpen(false)} ticket={selectedTicket} onUpdate={handleTicketUpdate} />
+      <UserTicketDetailModal 
+        isOpen={isTicketDetailOpen}
+        onClose={() => setIsTicketDetailOpen(false)}
+        ticket={selectedTicket}
+        // onUpdate={handleTicketUpdate}
+      />
     </div>
   );
 }

+ 16 - 3
src/app/dashboard/settings/page.tsx

@@ -4,12 +4,10 @@ import ProfileSettings from '@/components/dashboard/ProfileSettings';
 import Sidebar from '@/components/dashboard/Sidebar';
 import { useRouter } from 'next/navigation';
 import { LogOut, Plus } from 'lucide-react';
-// 1. 引入 useLanguage Hook
 import { useLanguage } from '@/lib/i18n/LanguageContext';
 
 export default function SettingsPage() {
   const router = useRouter();
-  // 2. 获取翻译函数
   const { t } = useLanguage();
 
   const handleLogout = () => {
@@ -18,6 +16,17 @@ export default function SettingsPage() {
     router.push('/login');
   };
 
+  // 新增:处理侧边栏点击导航
+  // 如果你的 Sidebar 是纯状态控制的,需要在这里处理跳转
+  const handleTabChange = (tab: string) => {
+    if (tab === 'overview') {
+      router.push('/dashboard');
+    } else if (tab === 'orders') {
+      router.push('/dashboard/orders');
+    }
+    // 既然当前已经在 settings 页面,点击 settings 不需要跳转
+  };
+
   return (
     <div className="min-h-screen bg-slate-50">
       <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-10">
@@ -48,7 +57,11 @@ export default function SettingsPage() {
           
           {/* 左侧侧边栏 */}
           <div className="lg:col-span-1">
-            <Sidebar />
+            {/* 🔴 修复部分:传入必要的 props */}
+            <Sidebar 
+              activeTab="settings" 
+              setActiveTab={handleTabChange} 
+            />
           </div>
 
           {/* 右侧内容区 */}

+ 2 - 1
src/app/knowledge/page.tsx

@@ -6,10 +6,11 @@ import { Search, BookOpen, Loader2 } from 'lucide-react';
 import KnowledgeCard from '@/components/knowledge/KnowledgeCard';
 import Pagination from '@/components/common/Pagination';
 import { useLanguage } from '@/lib/i18n/LanguageContext';
+import { CardData } from '@/types/card';
 
 export default function KnowledgePage() {
   const [loading, setLoading] = useState(true);
-  const [cards, setCards] = useState<any[]>([]);
+  const [cards, setCards] = useState<CardData[]>([]);
   
   const { t, lang } = useLanguage();
 

+ 81 - 67
src/app/payment/confirm/page.tsx

@@ -1,15 +1,15 @@
 'use client';
 
-import { useEffect, useState, useRef } from 'react';
+import { useEffect, useState, useRef, Suspense } from 'react'; // 1. 引入 Suspense
 import { useSearchParams, useRouter } from 'next/navigation';
 import api from '@/lib/api';
 import { Loader2, CheckCircle, XCircle, ArrowLeft } from 'lucide-react';
 
-export default function PaymentConfirmPage() {
+// 2. 将原来的主要逻辑提取到一个单独的组件中 (非 default export)
+function PaymentConfirmContent() {
   const searchParams = useSearchParams();
   const router = useRouter();
   
-  // 防止 React StrictMode 下执行两次
   const hasFetched = useRef(false);
 
   const [status, setStatus] = useState<'loading' | 'success' | 'error'>('loading');
@@ -28,13 +28,11 @@ export default function PaymentConfirmPage() {
 
     setPaymentId(pid);
 
-    // 核心逻辑:调用确认接口
     const confirmPayment = async () => {
       if (hasFetched.current) return;
       hasFetched.current = true;
 
       try {
-        // API: GET /api/vas/payment/confirm?payment_id=1&token=1
         await api.get('/api/vas/payment/confirm', {
           params: {
             payment_id: pid,
@@ -47,7 +45,6 @@ export default function PaymentConfirmPage() {
       } catch (error: any) {
         console.error(error);
         setStatus('error');
-        // 获取后端返回的错误信息
         const errorMsg = error.response?.data?.message || error.message || '未知错误';
         setMessage(`确认失败: ${errorMsg}`);
       }
@@ -56,76 +53,93 @@ export default function PaymentConfirmPage() {
     confirmPayment();
   }, [searchParams]);
 
-  // 渲染不同的状态 UI
   return (
-    <div className="min-h-screen bg-slate-50 flex flex-col items-center justify-center p-4">
-      <div className="bg-white rounded-2xl shadow-xl w-full max-w-md overflow-hidden border border-slate-100">
-        
-        {/* 顶部装饰条 */}
-        <div className={`h-2 w-full ${
-          status === 'loading' ? 'bg-blue-500 animate-pulse' :
-          status === 'success' ? 'bg-green-500' : 'bg-red-500'
-        }`} />
-
-        <div className="p-8 text-center">
-          {/* 图标状态 */}
-          <div className="flex justify-center mb-6">
-            {status === 'loading' && (
-              <div className="w-20 h-20 bg-blue-50 rounded-full flex items-center justify-center">
-                <Loader2 className="w-10 h-10 text-blue-600 animate-spin" />
-              </div>
-            )}
-            {status === 'success' && (
-              <div className="w-20 h-20 bg-green-50 rounded-full flex items-center justify-center animate-in zoom-in duration-300">
-                <CheckCircle className="w-10 h-10 text-green-600" />
-              </div>
-            )}
-            {status === 'error' && (
-              <div className="w-20 h-20 bg-red-50 rounded-full flex items-center justify-center animate-in zoom-in duration-300">
-                <XCircle className="w-10 h-10 text-red-600" />
-              </div>
-            )}
-          </div>
-
-          {/* 标题与描述 */}
-          <h1 className="text-xl font-bold text-slate-900 mb-2">
-            {status === 'loading' ? '处理中...' : 
-             status === 'success' ? '操作成功' : '操作失败'}
-          </h1>
-          
-          <p className={`text-sm mb-6 leading-relaxed ${
-            status === 'error' ? 'text-red-500' : 'text-slate-500'
-          }`}>
-            {message}
-          </p>
-
-          {/* 详情信息 (仅在非加载状态显示) */}
-          {status !== 'loading' && paymentId && (
-            <div className="bg-slate-50 rounded-lg p-3 mb-6 text-xs text-slate-400 font-mono">
-              Payment ID: {paymentId}
+    <div className="bg-white rounded-2xl shadow-xl w-full max-w-md overflow-hidden border border-slate-100">
+      {/* 顶部装饰条 */}
+      <div className={`h-2 w-full ${
+        status === 'loading' ? 'bg-blue-500 animate-pulse' :
+        status === 'success' ? 'bg-green-500' : 'bg-red-500'
+      }`} />
+
+      <div className="p-8 text-center">
+        {/* 图标状态 */}
+        <div className="flex justify-center mb-6">
+          {status === 'loading' && (
+            <div className="w-20 h-20 bg-blue-50 rounded-full flex items-center justify-center">
+              <Loader2 className="w-10 h-10 text-blue-600 animate-spin" />
             </div>
           )}
+          {status === 'success' && (
+            <div className="w-20 h-20 bg-green-50 rounded-full flex items-center justify-center animate-in zoom-in duration-300">
+              <CheckCircle className="w-10 h-10 text-green-600" />
+            </div>
+          )}
+          {status === 'error' && (
+            <div className="w-20 h-20 bg-red-50 rounded-full flex items-center justify-center animate-in zoom-in duration-300">
+              <XCircle className="w-10 h-10 text-red-600" />
+            </div>
+          )}
+        </div>
 
-          {/* 底部按钮 */}
-          <div className="space-y-3">
+        {/* 标题与描述 */}
+        <h1 className="text-xl font-bold text-slate-900 mb-2">
+          {status === 'loading' ? '处理中...' : 
+           status === 'success' ? '操作成功' : '操作失败'}
+        </h1>
+        
+        <p className={`text-sm mb-6 leading-relaxed ${
+          status === 'error' ? 'text-red-500' : 'text-slate-500'
+        }`}>
+          {message}
+        </p>
+
+        {/* 详情信息 */}
+        {status !== 'loading' && paymentId && (
+          <div className="bg-slate-50 rounded-lg p-3 mb-6 text-xs text-slate-400 font-mono">
+            Payment ID: {paymentId}
+          </div>
+        )}
+
+        {/* 底部按钮 */}
+        <div className="space-y-3">
+          <button 
+            onClick={() => router.push('/admin/orders')}
+            className="w-full flex items-center justify-center gap-2 bg-slate-900 text-white py-2.5 rounded-lg text-sm font-medium hover:bg-slate-800 transition shadow-sm"
+          >
+            <ArrowLeft size={16} /> 返回订单管理
+          </button>
+          
+          {status === 'error' && (
             <button 
-              onClick={() => router.push('/admin/orders')}
-              className="w-full flex items-center justify-center gap-2 bg-slate-900 text-white py-2.5 rounded-lg text-sm font-medium hover:bg-slate-800 transition shadow-sm"
+              onClick={() => window.location.reload()}
+              className="w-full py-2.5 text-slate-600 text-sm hover:bg-slate-50 rounded-lg transition"
             >
-              <ArrowLeft size={16} /> 返回订单管理
+              重试
             </button>
-            
-            {status === 'error' && (
-              <button 
-                onClick={() => window.location.reload()}
-                className="w-full py-2.5 text-slate-600 text-sm hover:bg-slate-50 rounded-lg transition"
-              >
-                重试
-              </button>
-            )}
-          </div>
+          )}
         </div>
       </div>
+    </div>
+  );
+}
+
+// 3. 定义一个 Loading Fallback 组件 (在 Suspense 加载时显示)
+function PageFallback() {
+  return (
+    <div className="bg-white rounded-2xl shadow-xl w-full max-w-md p-12 flex flex-col items-center justify-center">
+      <Loader2 className="w-10 h-10 text-blue-600 animate-spin mb-4" />
+      <p className="text-slate-500 text-sm">正在加载支付信息...</p>
+    </div>
+  );
+}
+
+// 4. 默认导出主页面组件,包裹 Suspense
+export default function PaymentConfirmPage() {
+  return (
+    <div className="min-h-screen bg-slate-50 flex flex-col items-center justify-center p-4">
+      <Suspense fallback={<PageFallback />}>
+        <PaymentConfirmContent />
+      </Suspense>
 
       <p className="mt-8 text-xs text-slate-400">
         Visafly Admin System &copy; {new Date().getFullYear()}

+ 1 - 1
src/components/CreateOrderForm.tsx

@@ -490,7 +490,7 @@ export default function CreateOrderForm({ productId, productName }: CreateOrderF
         title={t('common.confirm_title') || "请确认您的操作"}
         message={t('order.confirm_submit_msg') || "请核对信息无误后,点击确认提交。"}
         onConfirm={handleConfirmSubmit}
-        onClose={() => {
+        onCancel={() => {
             setShowConfirmModal(false);
             setSubmitting(false); 
         }}

+ 3 - 13
src/components/admin/cards/CardModal.tsx

@@ -3,19 +3,7 @@
 import { useState, useEffect, useRef } from 'react';
 import api from '@/lib/api';
 import { X, Loader2, Upload, Image as ImageIcon } from 'lucide-react';
-
-// 直接定义在文件内部
-export interface CardData {
-  id?: number;
-  title: string;
-  content: string;
-  image: string; // 存储图片 ID 或 URL
-  label: string;
-  country: string;
-  additional_info?: string;
-  culture: 'english' | 'chinese';
-  created_at?: string;
-}
+import { CardData } from '@/types/card';
 
 interface CardModalProps {
   isOpen: boolean;
@@ -41,6 +29,7 @@ export default function CardModal({ isOpen, onClose, onSuccess, card }: CardModa
   
   // 表单状态
   const [form, setForm] = useState<CardData>({
+    id: 0,
     title: '',
     content: '',
     image: '',
@@ -62,6 +51,7 @@ export default function CardModal({ isOpen, onClose, onSuccess, card }: CardModa
       } else {
         // 重置为默认值
         setForm({
+          id: 0,
           title: '',
           content: '',
           image: '',

+ 3 - 14
src/components/admin/cards/CardTable.tsx

@@ -1,24 +1,13 @@
 'use client';
 
 import { Edit, Image as ImageIcon, MapPin, Tag, Globe } from 'lucide-react';
+import { CardData } from '@/types/card';
 
-export interface KnowledgeCard {
-  id: number;
-  title: string;
-  content: string;
-  image: string;
-  label: string;
-  country: string;
-  additional_info: string;
-  culture: string;
-  created_at?: string;
-  updated_at?: string;
-}
 
 interface CardTableProps {
-  cards: KnowledgeCard[];
+  cards: CardData[];
   loading: boolean;
-  onEdit: (card: KnowledgeCard) => void;
+  onEdit: (card: CardData) => void;
 }
 
 const getImageUrl = (fidString: string | null) => {

+ 4 - 4
src/components/common/ConfirmModal.tsx

@@ -10,7 +10,7 @@ interface ConfirmModalProps {
   onCancel: () => void;
 }
 
-export default function ConfirmModal({ isOpen, onClose, onConfirm, message, title }: ConfirmModalProps) {
+export default function ConfirmModal({ isOpen, onCancel, onConfirm, message, title }: ConfirmModalProps) {
   if (!isOpen) return null;
 
   return (
@@ -20,7 +20,7 @@ export default function ConfirmModal({ isOpen, onClose, onConfirm, message, titl
         {/* Header */}
         <div className="px-6 py-4 border-b flex justify-between items-center bg-gray-50">
           <h3 className="font-bold text-gray-900 text-lg">{title}</h3>
-          <button onClick={onClose} className="text-gray-400 hover:text-gray-600 p-1 rounded-full hover:bg-gray-100 transition">
+          <button onClick={onCancel} className="text-gray-400 hover:text-gray-600 p-1 rounded-full hover:bg-gray-100 transition">
             <X size={24} />
           </button>
         </div>
@@ -33,7 +33,7 @@ export default function ConfirmModal({ isOpen, onClose, onConfirm, message, titl
         {/* Footer Buttons */}
         <div className="px-6 py-4 border-t bg-gray-50 flex justify-end gap-3">
           <button 
-            onClick={onClose}
+            onClick={onCancel}
             className="px-4 py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 text-sm font-medium transition"
           >
             取消
@@ -41,7 +41,7 @@ export default function ConfirmModal({ isOpen, onClose, onConfirm, message, titl
           <button 
             onClick={() => {
               onConfirm(); // 调用确认回调
-              onClose();   // 关闭弹窗
+              onCancel();   // 关闭弹窗
             }}
             className="px-6 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 text-sm font-bold transition"
           >

+ 1 - 9
src/components/knowledge/KnowledgeCard.tsx

@@ -4,16 +4,8 @@ import { useState } from 'react';
 import { MapPin, ChevronDown, ChevronUp, Calendar } from 'lucide-react';
 // 1. 引入 Hook
 import { useLanguage } from '@/lib/i18n/LanguageContext';
+import { CardData } from '@/types/card';
 
-interface CardData {
-  id: number;
-  title: string;
-  content: string; 
-  image: string | null;
-  label: string;
-  country: string;
-  created_at: string;
-}
 
 const getImageUrl = (fidString: string | null) => {
   if (!fidString) return null;

+ 1 - 0
src/lib/auth.ts

@@ -2,6 +2,7 @@
 export interface User {
     id: string;
     email: string;
+    phone?: string; 
     nickname?: string;
     role?: string; // 关键字段: 'admin' | 'user'
     avatar_url?: string;

+ 11 - 0
src/types/card.ts

@@ -0,0 +1,11 @@
+export interface CardData {
+    id: number; 
+    title: string;
+    content: string;
+    image: string; // 存储图片 ID 或 URL
+    label: string;
+    country: string;
+    additional_info?: string;
+    culture: 'english' | 'chinese';
+    created_at?: string;
+  }

+ 50 - 0
src/types/payment.ts

@@ -0,0 +1,50 @@
+// src/types/payment.ts
+
+/**
+ * 支付单/确认单状态类型
+ */
+export type PaymentStatus = 'pending' | 'approved' | 'rejected' | 'expired' | 'cancelled';
+
+/**
+ * 支付确认记录 (PaymentConfirmation)
+ * 对应数据库中的 payment_confirmations 表
+ */
+export interface PaymentConfirmation {
+  id: string;              // 唯一标识 ID
+  payment_id: string;      // 关联的支付单 ID (PID)
+  user_id: string;         // 用户 ID
+  
+  amount: number;          // 确认金额 (通常为整数,单位:分/cent)
+  currency: string;        // 货币代码 (如 'CNY', 'USD')
+  random_offset: number;   // 随机立减/偏移金额 (单位:分/cent)
+  
+  status: 'pending' | 'approved' | 'rejected'; // 状态: 待审核 | 已通过 | 已驳回
+  
+  created_at: string;      // 创建/提交时间 (ISO 8601 字符串)
+  confirmed_at: string;    // 用户点击"我已支付"的时间 / 确认时间 (ISO 8601 字符串)
+  
+  // 以下字段组件中虽未直接显示,但在 "详情 (onViewDetail)" 中可能需要
+  proof_url?: string;      // 支付凭证图片链接 (如果有)
+  transaction_id?: string; // 链上交易哈希或第三方流水号 (如果有)
+  remarks?: string;        // 用户或系统备注
+  updated_at?: string;     // 更新时间
+}
+
+/**
+ * 支付主单 (Payment)
+ * 对应数据库中的 payments 表
+ * (为了完整性补充在此,ConfirmationTable 主要用的是上面的 Interface)
+ */
+export interface Payment {
+  id: string;
+  order_id: string;
+  user_id: string;
+  amount: number;
+  currency: string;
+  status: PaymentStatus;
+  provider: string;        // 支付提供商 (e.g., 'wechat', 'alipay', 'usdt')
+  qr_code?: string;        // 二维码内容
+  expires_at: string;      // 过期时间
+  created_at: string;
+  updated_at: string;
+}

+ 11 - 10
src/types/products.ts

@@ -14,13 +14,14 @@ export interface Product {
     // ... 其他字段
   }
   
-  // 商品路由信息 (One Product -> Many Routings)
-  export interface ProductRouting {
-    id: number;
-    product_id: number;
-    routing_key: string;    // e.g., "fr_tls_london_bot" (队列名/标识符)
-    script_version: string; // e.g., "v2.1" (脚本版本)
-    priority: number;       // e.g., 10 (优先级)
-    meta?: string;          // JSON string, 额外配置
-    enabled: boolean;
-  }
+// 商品路由信息 (One Product -> Many Routings)
+export interface ProductRouting {
+  id: number;
+  product_id: number;
+  routing_key: string;    // e.g., "fr_tls_london_bot" (队列名/标识符)
+  script_version: string; // e.g., "v2.1" (脚本版本)
+  priority: number;       // e.g., 10 (优先级)
+  meta?: string;          // JSON string, 额外配置
+  enabled: boolean;
+}
+