init
This commit is contained in:
@@ -0,0 +1,193 @@
|
|||||||
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
|
from fastapi.security import OAuth2PasswordRequestForm
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from pydantic import BaseModel, EmailStr
|
||||||
|
from typing import Optional
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from database.database import get_db_session
|
||||||
|
from services.auth_service import (
|
||||||
|
authenticate_user,
|
||||||
|
create_access_token,
|
||||||
|
create_user,
|
||||||
|
get_user_by_username,
|
||||||
|
get_user_by_email,
|
||||||
|
get_current_active_user,
|
||||||
|
get_current_admin_user
|
||||||
|
)
|
||||||
|
from models.database import User
|
||||||
|
from config.settings import settings
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/api/auth", tags=["认证"])
|
||||||
|
|
||||||
|
|
||||||
|
class UserCreate(BaseModel):
|
||||||
|
username: str
|
||||||
|
email: EmailStr
|
||||||
|
password: str
|
||||||
|
full_name: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class UserResponse(BaseModel):
|
||||||
|
id: int
|
||||||
|
username: str
|
||||||
|
email: str
|
||||||
|
full_name: Optional[str]
|
||||||
|
is_active: bool
|
||||||
|
is_superuser: bool
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
class Token(BaseModel):
|
||||||
|
access_token: str
|
||||||
|
token_type: str
|
||||||
|
user: UserResponse
|
||||||
|
|
||||||
|
|
||||||
|
class LoginRequest(BaseModel):
|
||||||
|
username: str
|
||||||
|
password: str
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/login", response_model=Token)
|
||||||
|
async def login(
|
||||||
|
form_data: OAuth2PasswordRequestForm = Depends(),
|
||||||
|
db_session: AsyncSession = Depends(get_db_session)
|
||||||
|
):
|
||||||
|
user = await authenticate_user(db_session, form_data.username, form_data.password)
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="用户名或密码错误",
|
||||||
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
|
)
|
||||||
|
|
||||||
|
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||||
|
access_token = create_access_token(
|
||||||
|
data={"sub": user.username}, expires_delta=access_token_expires
|
||||||
|
)
|
||||||
|
|
||||||
|
return Token(
|
||||||
|
access_token=access_token,
|
||||||
|
token_type="bearer",
|
||||||
|
user=UserResponse.from_orm(user)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/login/json", response_model=Token)
|
||||||
|
async def login_json(
|
||||||
|
login_data: LoginRequest,
|
||||||
|
db_session: AsyncSession = Depends(get_db_session)
|
||||||
|
):
|
||||||
|
user = await authenticate_user(db_session, login_data.username, login_data.password)
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="用户名或密码错误",
|
||||||
|
)
|
||||||
|
|
||||||
|
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||||
|
access_token = create_access_token(
|
||||||
|
data={"sub": user.username}, expires_delta=access_token_expires
|
||||||
|
)
|
||||||
|
|
||||||
|
return Token(
|
||||||
|
access_token=access_token,
|
||||||
|
token_type="bearer",
|
||||||
|
user=UserResponse.from_orm(user)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/register", response_model=UserResponse, status_code=201)
|
||||||
|
async def register(
|
||||||
|
user_data: UserCreate,
|
||||||
|
db_session: AsyncSession = Depends(get_db_session)
|
||||||
|
):
|
||||||
|
existing_user = await get_user_by_username(db_session, user_data.username)
|
||||||
|
if existing_user:
|
||||||
|
raise HTTPException(status_code=400, detail="用户名已存在")
|
||||||
|
|
||||||
|
existing_email = await get_user_by_email(db_session, user_data.email)
|
||||||
|
if existing_email:
|
||||||
|
raise HTTPException(status_code=400, detail="邮箱已被注册")
|
||||||
|
|
||||||
|
user = await create_user(
|
||||||
|
db_session=db_session,
|
||||||
|
username=user_data.username,
|
||||||
|
email=user_data.email,
|
||||||
|
password=user_data.password,
|
||||||
|
full_name=user_data.full_name
|
||||||
|
)
|
||||||
|
|
||||||
|
return UserResponse.from_orm(user)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/me", response_model=UserResponse)
|
||||||
|
async def get_current_user_info(
|
||||||
|
current_user: User = Depends(get_current_active_user)
|
||||||
|
):
|
||||||
|
return UserResponse.from_orm(current_user)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/logout")
|
||||||
|
async def logout():
|
||||||
|
return {"message": "已登出"}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/users", response_model=list[UserResponse])
|
||||||
|
async def list_users(
|
||||||
|
db_session: AsyncSession = Depends(get_db_session),
|
||||||
|
admin_user: User = Depends(get_current_admin_user)
|
||||||
|
):
|
||||||
|
from sqlalchemy import select
|
||||||
|
result = await db_session.execute(select(User))
|
||||||
|
users = result.scalars().all()
|
||||||
|
return [UserResponse.from_orm(u) for u in users]
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/users/{user_id}/toggle-active", response_model=UserResponse)
|
||||||
|
async def toggle_user_active(
|
||||||
|
user_id: int,
|
||||||
|
db_session: AsyncSession = Depends(get_db_session),
|
||||||
|
admin_user: User = Depends(get_current_admin_user)
|
||||||
|
):
|
||||||
|
from sqlalchemy import select
|
||||||
|
result = await db_session.execute(select(User).where(User.id == user_id))
|
||||||
|
user = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(status_code=404, detail="用户不存在")
|
||||||
|
|
||||||
|
if user.id == admin_user.id:
|
||||||
|
raise HTTPException(status_code=400, detail="不能禁用自己的账户")
|
||||||
|
|
||||||
|
user.is_active = not user.is_active
|
||||||
|
await db_session.commit()
|
||||||
|
await db_session.refresh(user)
|
||||||
|
|
||||||
|
return UserResponse.from_orm(user)
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/users/{user_id}/toggle-admin", response_model=UserResponse)
|
||||||
|
async def toggle_user_admin(
|
||||||
|
user_id: int,
|
||||||
|
db_session: AsyncSession = Depends(get_db_session),
|
||||||
|
admin_user: User = Depends(get_current_admin_user)
|
||||||
|
):
|
||||||
|
from sqlalchemy import select
|
||||||
|
result = await db_session.execute(select(User).where(User.id == user_id))
|
||||||
|
user = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(status_code=404, detail="用户不存在")
|
||||||
|
|
||||||
|
if user.id == admin_user.id:
|
||||||
|
raise HTTPException(status_code=400, detail="不能修改自己的管理员权限")
|
||||||
|
|
||||||
|
user.is_superuser = not user.is_superuser
|
||||||
|
await db_session.commit()
|
||||||
|
await db_session.refresh(user)
|
||||||
|
|
||||||
|
return UserResponse.from_orm(user)
|
||||||
@@ -0,0 +1,769 @@
|
|||||||
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy import select, func, and_, or_
|
||||||
|
from sqlalchemy.orm import selectinload
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from typing import Optional, List
|
||||||
|
from datetime import datetime
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from database.database import get_db_session
|
||||||
|
from services.auth_service import get_current_active_user, get_current_admin_user
|
||||||
|
from models.database import (
|
||||||
|
User, Product, Supplier, Customer, Warehouse, Inventory,
|
||||||
|
StockMovement, PurchaseOrder, PurchaseOrderItem,
|
||||||
|
SalesOrder, SalesOrderItem
|
||||||
|
)
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/api/inventory", tags=["进销存"])
|
||||||
|
|
||||||
|
|
||||||
|
def generate_order_no(prefix: str) -> str:
|
||||||
|
date_str = datetime.now().strftime("%Y%m%d%H%M%S")
|
||||||
|
random_str = uuid.uuid4().hex[:4].upper()
|
||||||
|
return f"{prefix}{date_str}{random_str}"
|
||||||
|
|
||||||
|
|
||||||
|
class ProductCreate(BaseModel):
|
||||||
|
sku: str
|
||||||
|
name: str
|
||||||
|
description: Optional[str] = None
|
||||||
|
category: Optional[str] = None
|
||||||
|
unit: str = "件"
|
||||||
|
cost_price: float = 0
|
||||||
|
sale_price: float = 0
|
||||||
|
min_stock: int = 0
|
||||||
|
max_stock: int = 1000
|
||||||
|
|
||||||
|
|
||||||
|
class ProductResponse(BaseModel):
|
||||||
|
id: int
|
||||||
|
sku: str
|
||||||
|
name: str
|
||||||
|
description: Optional[str]
|
||||||
|
category: Optional[str]
|
||||||
|
unit: str
|
||||||
|
cost_price: float
|
||||||
|
sale_price: float
|
||||||
|
min_stock: int
|
||||||
|
max_stock: int
|
||||||
|
is_active: bool
|
||||||
|
created_at: datetime
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
class SupplierCreate(BaseModel):
|
||||||
|
code: Optional[str] = None
|
||||||
|
name: str
|
||||||
|
contact_person: Optional[str] = None
|
||||||
|
phone: Optional[str] = None
|
||||||
|
email: Optional[str] = None
|
||||||
|
address: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class SupplierResponse(BaseModel):
|
||||||
|
id: int
|
||||||
|
code: Optional[str]
|
||||||
|
name: str
|
||||||
|
contact_person: Optional[str]
|
||||||
|
phone: Optional[str]
|
||||||
|
email: Optional[str]
|
||||||
|
is_active: bool
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
class CustomerCreate(BaseModel):
|
||||||
|
code: Optional[str] = None
|
||||||
|
name: str
|
||||||
|
contact_person: Optional[str] = None
|
||||||
|
phone: Optional[str] = None
|
||||||
|
email: Optional[str] = None
|
||||||
|
address: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class CustomerResponse(BaseModel):
|
||||||
|
id: int
|
||||||
|
code: Optional[str]
|
||||||
|
name: str
|
||||||
|
contact_person: Optional[str]
|
||||||
|
phone: Optional[str]
|
||||||
|
email: Optional[str]
|
||||||
|
is_active: bool
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
class WarehouseCreate(BaseModel):
|
||||||
|
code: Optional[str] = None
|
||||||
|
name: str
|
||||||
|
address: Optional[str] = None
|
||||||
|
manager: Optional[str] = None
|
||||||
|
phone: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class WarehouseResponse(BaseModel):
|
||||||
|
id: int
|
||||||
|
code: Optional[str]
|
||||||
|
name: str
|
||||||
|
address: Optional[str]
|
||||||
|
manager: Optional[str]
|
||||||
|
is_active: bool
|
||||||
|
is_default: bool
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
class InventoryResponse(BaseModel):
|
||||||
|
id: int
|
||||||
|
product_id: int
|
||||||
|
product_name: str
|
||||||
|
product_sku: str
|
||||||
|
warehouse_id: int
|
||||||
|
warehouse_name: str
|
||||||
|
quantity: int
|
||||||
|
locked_quantity: int
|
||||||
|
available_quantity: int
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
class StockMovementCreate(BaseModel):
|
||||||
|
product_id: int
|
||||||
|
warehouse_id: int
|
||||||
|
movement_type: str
|
||||||
|
quantity: int
|
||||||
|
unit_price: Optional[float] = None
|
||||||
|
remark: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class StockMovementResponse(BaseModel):
|
||||||
|
id: int
|
||||||
|
product_name: str
|
||||||
|
movement_type: str
|
||||||
|
quantity: int
|
||||||
|
before_quantity: int
|
||||||
|
after_quantity: int
|
||||||
|
reference_no: Optional[str]
|
||||||
|
remark: Optional[str]
|
||||||
|
created_at: datetime
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
class PurchaseOrderItemCreate(BaseModel):
|
||||||
|
product_id: int
|
||||||
|
quantity: int
|
||||||
|
unit_price: float
|
||||||
|
remark: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class PurchaseOrderCreate(BaseModel):
|
||||||
|
supplier_id: int
|
||||||
|
expected_date: Optional[datetime] = None
|
||||||
|
remark: Optional[str] = None
|
||||||
|
items: List[PurchaseOrderItemCreate]
|
||||||
|
|
||||||
|
|
||||||
|
class PurchaseOrderResponse(BaseModel):
|
||||||
|
id: int
|
||||||
|
order_no: str
|
||||||
|
supplier_name: str
|
||||||
|
order_date: datetime
|
||||||
|
expected_date: Optional[datetime]
|
||||||
|
status: str
|
||||||
|
total_amount: float
|
||||||
|
paid_amount: float
|
||||||
|
remark: Optional[str]
|
||||||
|
created_at: datetime
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
class SalesOrderItemCreate(BaseModel):
|
||||||
|
product_id: int
|
||||||
|
quantity: int
|
||||||
|
unit_price: float
|
||||||
|
remark: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class SalesOrderCreate(BaseModel):
|
||||||
|
customer_id: int
|
||||||
|
delivery_date: Optional[datetime] = None
|
||||||
|
remark: Optional[str] = None
|
||||||
|
items: List[SalesOrderItemCreate]
|
||||||
|
|
||||||
|
|
||||||
|
class SalesOrderResponse(BaseModel):
|
||||||
|
id: int
|
||||||
|
order_no: str
|
||||||
|
customer_name: str
|
||||||
|
order_date: datetime
|
||||||
|
delivery_date: Optional[datetime]
|
||||||
|
status: str
|
||||||
|
total_amount: float
|
||||||
|
received_amount: float
|
||||||
|
remark: Optional[str]
|
||||||
|
created_at: datetime
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/dashboard")
|
||||||
|
async def get_dashboard(
|
||||||
|
db_session: AsyncSession = Depends(get_db_session),
|
||||||
|
current_user: User = Depends(get_current_active_user)
|
||||||
|
):
|
||||||
|
product_count = await db_session.scalar(select(func.count(Product.id)).where(Product.is_active == True))
|
||||||
|
supplier_count = await db_session.scalar(select(func.count(Supplier.id)).where(Supplier.is_active == True))
|
||||||
|
customer_count = await db_session.scalar(select(func.count(Customer.id)).where(Customer.is_active == True))
|
||||||
|
warehouse_count = await db_session.scalar(select(func.count(Warehouse.id)).where(Warehouse.is_active == True))
|
||||||
|
|
||||||
|
total_stock = await db_session.scalar(select(func.sum(Inventory.quantity))) or 0
|
||||||
|
total_value = await db_session.scalar(
|
||||||
|
select(func.sum(Inventory.quantity * Product.cost_price))
|
||||||
|
.join(Product, Inventory.product_id == Product.id)
|
||||||
|
) or 0
|
||||||
|
|
||||||
|
pending_purchase = await db_session.scalar(
|
||||||
|
select(func.count(PurchaseOrder.id)).where(PurchaseOrder.status == "pending")
|
||||||
|
)
|
||||||
|
pending_sales = await db_session.scalar(
|
||||||
|
select(func.count(SalesOrder.id)).where(SalesOrder.status == "pending")
|
||||||
|
)
|
||||||
|
|
||||||
|
low_stock_products = await db_session.execute(
|
||||||
|
select(Product, Inventory)
|
||||||
|
.join(Inventory, Product.id == Inventory.product_id)
|
||||||
|
.where(Inventory.quantity <= Product.min_stock)
|
||||||
|
.limit(10)
|
||||||
|
)
|
||||||
|
low_stock = [
|
||||||
|
{"id": p.id, "name": p.name, "sku": p.sku, "quantity": i.quantity, "min_stock": p.min_stock}
|
||||||
|
for p, i in low_stock_products.all()
|
||||||
|
]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"product_count": product_count,
|
||||||
|
"supplier_count": supplier_count,
|
||||||
|
"customer_count": customer_count,
|
||||||
|
"warehouse_count": warehouse_count,
|
||||||
|
"total_stock": total_stock,
|
||||||
|
"total_value": round(total_value, 2),
|
||||||
|
"pending_purchase": pending_purchase,
|
||||||
|
"pending_sales": pending_sales,
|
||||||
|
"low_stock_products": low_stock
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/products", response_model=List[ProductResponse])
|
||||||
|
async def list_products(
|
||||||
|
skip: int = Query(0, ge=0),
|
||||||
|
limit: int = Query(20, ge=1, le=100),
|
||||||
|
search: Optional[str] = None,
|
||||||
|
category: Optional[str] = None,
|
||||||
|
db_session: AsyncSession = Depends(get_db_session),
|
||||||
|
current_user: User = Depends(get_current_active_user)
|
||||||
|
):
|
||||||
|
query = select(Product).where(Product.is_active == True)
|
||||||
|
|
||||||
|
if search:
|
||||||
|
query = query.where(or_(Product.name.ilike(f"%{search}%"), Product.sku.ilike(f"%{search}%")))
|
||||||
|
if category:
|
||||||
|
query = query.where(Product.category == category)
|
||||||
|
|
||||||
|
query = query.offset(skip).limit(limit).order_by(Product.created_at.desc())
|
||||||
|
result = await db_session.execute(query)
|
||||||
|
products = result.scalars().all()
|
||||||
|
return [ProductResponse.from_orm(p) for p in products]
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/products", response_model=ProductResponse, status_code=201)
|
||||||
|
async def create_product(
|
||||||
|
product_data: ProductCreate,
|
||||||
|
db_session: AsyncSession = Depends(get_db_session),
|
||||||
|
current_user: User = Depends(get_current_active_user)
|
||||||
|
):
|
||||||
|
existing = await db_session.execute(select(Product).where(Product.sku == product_data.sku))
|
||||||
|
if existing.scalar_one_or_none():
|
||||||
|
raise HTTPException(status_code=400, detail="SKU已存在")
|
||||||
|
|
||||||
|
product = Product(**product_data.dict())
|
||||||
|
db_session.add(product)
|
||||||
|
await db_session.commit()
|
||||||
|
await db_session.refresh(product)
|
||||||
|
return ProductResponse.from_orm(product)
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/products/{product_id}", response_model=ProductResponse)
|
||||||
|
async def update_product(
|
||||||
|
product_id: int,
|
||||||
|
product_data: ProductCreate,
|
||||||
|
db_session: AsyncSession = Depends(get_db_session),
|
||||||
|
current_user: User = Depends(get_current_active_user)
|
||||||
|
):
|
||||||
|
result = await db_session.execute(select(Product).where(Product.id == product_id))
|
||||||
|
product = result.scalar_one_or_none()
|
||||||
|
if not product:
|
||||||
|
raise HTTPException(status_code=404, detail="产品不存在")
|
||||||
|
|
||||||
|
for key, value in product_data.dict().items():
|
||||||
|
setattr(product, key, value)
|
||||||
|
|
||||||
|
await db_session.commit()
|
||||||
|
await db_session.refresh(product)
|
||||||
|
return ProductResponse.from_orm(product)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/products/{product_id}")
|
||||||
|
async def delete_product(
|
||||||
|
product_id: int,
|
||||||
|
db_session: AsyncSession = Depends(get_db_session),
|
||||||
|
current_user: User = Depends(get_current_admin_user)
|
||||||
|
):
|
||||||
|
result = await db_session.execute(select(Product).where(Product.id == product_id))
|
||||||
|
product = result.scalar_one_or_none()
|
||||||
|
if not product:
|
||||||
|
raise HTTPException(status_code=404, detail="产品不存在")
|
||||||
|
|
||||||
|
product.is_active = False
|
||||||
|
await db_session.commit()
|
||||||
|
return {"message": "产品已删除"}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/suppliers", response_model=List[SupplierResponse])
|
||||||
|
async def list_suppliers(
|
||||||
|
skip: int = Query(0, ge=0),
|
||||||
|
limit: int = Query(20, ge=1, le=100),
|
||||||
|
search: Optional[str] = None,
|
||||||
|
db_session: AsyncSession = Depends(get_db_session),
|
||||||
|
current_user: User = Depends(get_current_active_user)
|
||||||
|
):
|
||||||
|
query = select(Supplier).where(Supplier.is_active == True)
|
||||||
|
if search:
|
||||||
|
query = query.where(Supplier.name.ilike(f"%{search}%"))
|
||||||
|
query = query.offset(skip).limit(limit).order_by(Supplier.created_at.desc())
|
||||||
|
result = await db_session.execute(query)
|
||||||
|
return [SupplierResponse.from_orm(s) for s in result.scalars().all()]
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/suppliers", response_model=SupplierResponse, status_code=201)
|
||||||
|
async def create_supplier(
|
||||||
|
supplier_data: SupplierCreate,
|
||||||
|
db_session: AsyncSession = Depends(get_db_session),
|
||||||
|
current_user: User = Depends(get_current_active_user)
|
||||||
|
):
|
||||||
|
data = supplier_data.dict()
|
||||||
|
if not data.get("code"):
|
||||||
|
data["code"] = f"S{datetime.now().strftime('%Y%m%d%H%M%S')}"
|
||||||
|
|
||||||
|
supplier = Supplier(**data)
|
||||||
|
db_session.add(supplier)
|
||||||
|
await db_session.commit()
|
||||||
|
await db_session.refresh(supplier)
|
||||||
|
return SupplierResponse.from_orm(supplier)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/customers", response_model=List[CustomerResponse])
|
||||||
|
async def list_customers(
|
||||||
|
skip: int = Query(0, ge=0),
|
||||||
|
limit: int = Query(20, ge=1, le=100),
|
||||||
|
search: Optional[str] = None,
|
||||||
|
db_session: AsyncSession = Depends(get_db_session),
|
||||||
|
current_user: User = Depends(get_current_active_user)
|
||||||
|
):
|
||||||
|
query = select(Customer).where(Customer.is_active == True)
|
||||||
|
if search:
|
||||||
|
query = query.where(Customer.name.ilike(f"%{search}%"))
|
||||||
|
query = query.offset(skip).limit(limit).order_by(Customer.created_at.desc())
|
||||||
|
result = await db_session.execute(query)
|
||||||
|
return [CustomerResponse.from_orm(c) for c in result.scalars().all()]
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/customers", response_model=CustomerResponse, status_code=201)
|
||||||
|
async def create_customer(
|
||||||
|
customer_data: CustomerCreate,
|
||||||
|
db_session: AsyncSession = Depends(get_db_session),
|
||||||
|
current_user: User = Depends(get_current_active_user)
|
||||||
|
):
|
||||||
|
data = customer_data.dict()
|
||||||
|
if not data.get("code"):
|
||||||
|
data["code"] = f"C{datetime.now().strftime('%Y%m%d%H%M%S')}"
|
||||||
|
|
||||||
|
customer = Customer(**data)
|
||||||
|
db_session.add(customer)
|
||||||
|
await db_session.commit()
|
||||||
|
await db_session.refresh(customer)
|
||||||
|
return CustomerResponse.from_orm(customer)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/warehouses", response_model=List[WarehouseResponse])
|
||||||
|
async def list_warehouses(
|
||||||
|
db_session: AsyncSession = Depends(get_db_session),
|
||||||
|
current_user: User = Depends(get_current_active_user)
|
||||||
|
):
|
||||||
|
result = await db_session.execute(
|
||||||
|
select(Warehouse).where(Warehouse.is_active == True).order_by(Warehouse.is_default.desc())
|
||||||
|
)
|
||||||
|
return [WarehouseResponse.from_orm(w) for w in result.scalars().all()]
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/warehouses", response_model=WarehouseResponse, status_code=201)
|
||||||
|
async def create_warehouse(
|
||||||
|
warehouse_data: WarehouseCreate,
|
||||||
|
db_session: AsyncSession = Depends(get_db_session),
|
||||||
|
current_user: User = Depends(get_current_active_user)
|
||||||
|
):
|
||||||
|
data = warehouse_data.dict()
|
||||||
|
if not data.get("code"):
|
||||||
|
data["code"] = f"W{datetime.now().strftime('%Y%m%d%H%M%S')}"
|
||||||
|
|
||||||
|
warehouse = Warehouse(**data)
|
||||||
|
db_session.add(warehouse)
|
||||||
|
await db_session.commit()
|
||||||
|
await db_session.refresh(warehouse)
|
||||||
|
return WarehouseResponse.from_orm(warehouse)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/inventory", response_model=List[InventoryResponse])
|
||||||
|
async def list_inventory(
|
||||||
|
warehouse_id: Optional[int] = None,
|
||||||
|
product_id: Optional[int] = None,
|
||||||
|
low_stock: bool = False,
|
||||||
|
skip: int = Query(0, ge=0),
|
||||||
|
limit: int = Query(20, ge=1, le=100),
|
||||||
|
db_session: AsyncSession = Depends(get_db_session),
|
||||||
|
current_user: User = Depends(get_current_active_user)
|
||||||
|
):
|
||||||
|
query = (
|
||||||
|
select(Inventory, Product, Warehouse)
|
||||||
|
.join(Product, Inventory.product_id == Product.id)
|
||||||
|
.join(Warehouse, Inventory.warehouse_id == Warehouse.id)
|
||||||
|
.where(Product.is_active == True)
|
||||||
|
.where(Warehouse.is_active == True)
|
||||||
|
)
|
||||||
|
|
||||||
|
if warehouse_id:
|
||||||
|
query = query.where(Inventory.warehouse_id == warehouse_id)
|
||||||
|
if product_id:
|
||||||
|
query = query.where(Inventory.product_id == product_id)
|
||||||
|
if low_stock:
|
||||||
|
query = query.where(Inventory.quantity <= Product.min_stock)
|
||||||
|
|
||||||
|
query = query.offset(skip).limit(limit)
|
||||||
|
result = await db_session.execute(query)
|
||||||
|
|
||||||
|
inventory_list = []
|
||||||
|
for inv, product, warehouse in result.all():
|
||||||
|
inventory_list.append(InventoryResponse(
|
||||||
|
id=inv.id,
|
||||||
|
product_id=inv.product_id,
|
||||||
|
product_name=product.name,
|
||||||
|
product_sku=product.sku,
|
||||||
|
warehouse_id=inv.warehouse_id,
|
||||||
|
warehouse_name=warehouse.name,
|
||||||
|
quantity=inv.quantity,
|
||||||
|
locked_quantity=inv.locked_quantity,
|
||||||
|
available_quantity=inv.available_quantity
|
||||||
|
))
|
||||||
|
|
||||||
|
return inventory_list
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/stock-movements", response_model=StockMovementResponse, status_code=201)
|
||||||
|
async def create_stock_movement(
|
||||||
|
movement_data: StockMovementCreate,
|
||||||
|
db_session: AsyncSession = Depends(get_db_session),
|
||||||
|
current_user: User = Depends(get_current_active_user)
|
||||||
|
):
|
||||||
|
if movement_data.movement_type not in ["in", "out", "adjust"]:
|
||||||
|
raise HTTPException(status_code=400, detail="无效的变动类型")
|
||||||
|
|
||||||
|
result = await db_session.execute(
|
||||||
|
select(Inventory)
|
||||||
|
.where(Inventory.product_id == movement_data.product_id)
|
||||||
|
.where(Inventory.warehouse_id == movement_data.warehouse_id)
|
||||||
|
)
|
||||||
|
inventory = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if not inventory:
|
||||||
|
if movement_data.movement_type == "out":
|
||||||
|
raise HTTPException(status_code=400, detail="库存不足")
|
||||||
|
inventory = Inventory(
|
||||||
|
product_id=movement_data.product_id,
|
||||||
|
warehouse_id=movement_data.warehouse_id,
|
||||||
|
quantity=0
|
||||||
|
)
|
||||||
|
db_session.add(inventory)
|
||||||
|
await db_session.flush()
|
||||||
|
|
||||||
|
before_qty = inventory.quantity
|
||||||
|
|
||||||
|
if movement_data.movement_type == "in":
|
||||||
|
inventory.quantity += movement_data.quantity
|
||||||
|
elif movement_data.movement_type == "out":
|
||||||
|
if inventory.quantity < movement_data.quantity:
|
||||||
|
raise HTTPException(status_code=400, detail="库存不足")
|
||||||
|
inventory.quantity -= movement_data.quantity
|
||||||
|
else:
|
||||||
|
inventory.quantity = movement_data.quantity
|
||||||
|
|
||||||
|
after_qty = inventory.quantity
|
||||||
|
|
||||||
|
movement = StockMovement(
|
||||||
|
product_id=movement_data.product_id,
|
||||||
|
warehouse_id=movement_data.warehouse_id,
|
||||||
|
movement_type=movement_data.movement_type,
|
||||||
|
quantity=movement_data.quantity,
|
||||||
|
before_quantity=before_qty,
|
||||||
|
after_quantity=after_qty,
|
||||||
|
reference_no=generate_order_no("SM"),
|
||||||
|
unit_price=movement_data.unit_price,
|
||||||
|
total_amount=movement_data.unit_price * movement_data.quantity if movement_data.unit_price else None,
|
||||||
|
remark=movement_data.remark,
|
||||||
|
operator_id=current_user.id
|
||||||
|
)
|
||||||
|
db_session.add(movement)
|
||||||
|
await db_session.commit()
|
||||||
|
|
||||||
|
product = await db_session.execute(select(Product).where(Product.id == movement_data.product_id))
|
||||||
|
product = product.scalar_one()
|
||||||
|
|
||||||
|
return StockMovementResponse(
|
||||||
|
id=movement.id,
|
||||||
|
product_name=product.name,
|
||||||
|
movement_type=movement.movement_type,
|
||||||
|
quantity=movement.quantity,
|
||||||
|
before_quantity=movement.before_quantity,
|
||||||
|
after_quantity=movement.after_quantity,
|
||||||
|
reference_no=movement.reference_no,
|
||||||
|
remark=movement.remark,
|
||||||
|
created_at=movement.created_at
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/stock-movements", response_model=List[StockMovementResponse])
|
||||||
|
async def list_stock_movements(
|
||||||
|
product_id: Optional[int] = None,
|
||||||
|
movement_type: Optional[str] = None,
|
||||||
|
skip: int = Query(0, ge=0),
|
||||||
|
limit: int = Query(20, ge=1, le=100),
|
||||||
|
db_session: AsyncSession = Depends(get_db_session),
|
||||||
|
current_user: User = Depends(get_current_active_user)
|
||||||
|
):
|
||||||
|
query = (
|
||||||
|
select(StockMovement, Product)
|
||||||
|
.join(Product, StockMovement.product_id == Product.id)
|
||||||
|
.order_by(StockMovement.created_at.desc())
|
||||||
|
)
|
||||||
|
|
||||||
|
if product_id:
|
||||||
|
query = query.where(StockMovement.product_id == product_id)
|
||||||
|
if movement_type:
|
||||||
|
query = query.where(StockMovement.movement_type == movement_type)
|
||||||
|
|
||||||
|
query = query.offset(skip).limit(limit)
|
||||||
|
result = await db_session.execute(query)
|
||||||
|
|
||||||
|
movements = []
|
||||||
|
for movement, product in result.all():
|
||||||
|
movements.append(StockMovementResponse(
|
||||||
|
id=movement.id,
|
||||||
|
product_name=product.name,
|
||||||
|
movement_type=movement.movement_type,
|
||||||
|
quantity=movement.quantity,
|
||||||
|
before_quantity=movement.before_quantity,
|
||||||
|
after_quantity=movement.after_quantity,
|
||||||
|
reference_no=movement.reference_no,
|
||||||
|
remark=movement.remark,
|
||||||
|
created_at=movement.created_at
|
||||||
|
))
|
||||||
|
|
||||||
|
return movements
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/purchase-orders", response_model=List[PurchaseOrderResponse])
|
||||||
|
async def list_purchase_orders(
|
||||||
|
status: Optional[str] = None,
|
||||||
|
skip: int = Query(0, ge=0),
|
||||||
|
limit: int = Query(20, ge=1, le=100),
|
||||||
|
db_session: AsyncSession = Depends(get_db_session),
|
||||||
|
current_user: User = Depends(get_current_active_user)
|
||||||
|
):
|
||||||
|
query = (
|
||||||
|
select(PurchaseOrder, Supplier)
|
||||||
|
.join(Supplier, PurchaseOrder.supplier_id == Supplier.id)
|
||||||
|
.order_by(PurchaseOrder.created_at.desc())
|
||||||
|
)
|
||||||
|
|
||||||
|
if status:
|
||||||
|
query = query.where(PurchaseOrder.status == status)
|
||||||
|
|
||||||
|
query = query.offset(skip).limit(limit)
|
||||||
|
result = await db_session.execute(query)
|
||||||
|
|
||||||
|
orders = []
|
||||||
|
for order, supplier in result.all():
|
||||||
|
orders.append(PurchaseOrderResponse(
|
||||||
|
id=order.id,
|
||||||
|
order_no=order.order_no,
|
||||||
|
supplier_name=supplier.name,
|
||||||
|
order_date=order.order_date,
|
||||||
|
expected_date=order.expected_date,
|
||||||
|
status=order.status,
|
||||||
|
total_amount=order.total_amount,
|
||||||
|
paid_amount=order.paid_amount,
|
||||||
|
remark=order.remark,
|
||||||
|
created_at=order.created_at
|
||||||
|
))
|
||||||
|
|
||||||
|
return orders
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/purchase-orders", response_model=PurchaseOrderResponse, status_code=201)
|
||||||
|
async def create_purchase_order(
|
||||||
|
order_data: PurchaseOrderCreate,
|
||||||
|
db_session: AsyncSession = Depends(get_db_session),
|
||||||
|
current_user: User = Depends(get_current_active_user)
|
||||||
|
):
|
||||||
|
order = PurchaseOrder(
|
||||||
|
order_no=generate_order_no("PO"),
|
||||||
|
supplier_id=order_data.supplier_id,
|
||||||
|
expected_date=order_data.expected_date,
|
||||||
|
remark=order_data.remark,
|
||||||
|
operator_id=current_user.id,
|
||||||
|
status="draft"
|
||||||
|
)
|
||||||
|
db_session.add(order)
|
||||||
|
await db_session.flush()
|
||||||
|
|
||||||
|
total_amount = 0
|
||||||
|
for item_data in order_data.items:
|
||||||
|
item = PurchaseOrderItem(
|
||||||
|
order_id=order.id,
|
||||||
|
product_id=item_data.product_id,
|
||||||
|
quantity=item_data.quantity,
|
||||||
|
unit_price=item_data.unit_price,
|
||||||
|
amount=item_data.quantity * item_data.unit_price,
|
||||||
|
remark=item_data.remark
|
||||||
|
)
|
||||||
|
db_session.add(item)
|
||||||
|
total_amount += item.amount
|
||||||
|
|
||||||
|
order.total_amount = total_amount
|
||||||
|
await db_session.commit()
|
||||||
|
await db_session.refresh(order)
|
||||||
|
|
||||||
|
supplier = await db_session.execute(select(Supplier).where(Supplier.id == order.supplier_id))
|
||||||
|
supplier = supplier.scalar_one()
|
||||||
|
|
||||||
|
return PurchaseOrderResponse(
|
||||||
|
id=order.id,
|
||||||
|
order_no=order.order_no,
|
||||||
|
supplier_name=supplier.name,
|
||||||
|
order_date=order.order_date,
|
||||||
|
expected_date=order.expected_date,
|
||||||
|
status=order.status,
|
||||||
|
total_amount=order.total_amount,
|
||||||
|
paid_amount=order.paid_amount,
|
||||||
|
remark=order.remark,
|
||||||
|
created_at=order.created_at
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/sales-orders", response_model=List[SalesOrderResponse])
|
||||||
|
async def list_sales_orders(
|
||||||
|
status: Optional[str] = None,
|
||||||
|
skip: int = Query(0, ge=0),
|
||||||
|
limit: int = Query(20, ge=1, le=100),
|
||||||
|
db_session: AsyncSession = Depends(get_db_session),
|
||||||
|
current_user: User = Depends(get_current_active_user)
|
||||||
|
):
|
||||||
|
query = (
|
||||||
|
select(SalesOrder, Customer)
|
||||||
|
.join(Customer, SalesOrder.customer_id == Customer.id)
|
||||||
|
.order_by(SalesOrder.created_at.desc())
|
||||||
|
)
|
||||||
|
|
||||||
|
if status:
|
||||||
|
query = query.where(SalesOrder.status == status)
|
||||||
|
|
||||||
|
query = query.offset(skip).limit(limit)
|
||||||
|
result = await db_session.execute(query)
|
||||||
|
|
||||||
|
orders = []
|
||||||
|
for order, customer in result.all():
|
||||||
|
orders.append(SalesOrderResponse(
|
||||||
|
id=order.id,
|
||||||
|
order_no=order.order_no,
|
||||||
|
customer_name=customer.name,
|
||||||
|
order_date=order.order_date,
|
||||||
|
delivery_date=order.delivery_date,
|
||||||
|
status=order.status,
|
||||||
|
total_amount=order.total_amount,
|
||||||
|
received_amount=order.received_amount,
|
||||||
|
remark=order.remark,
|
||||||
|
created_at=order.created_at
|
||||||
|
))
|
||||||
|
|
||||||
|
return orders
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/sales-orders", response_model=SalesOrderResponse, status_code=201)
|
||||||
|
async def create_sales_order(
|
||||||
|
order_data: SalesOrderCreate,
|
||||||
|
db_session: AsyncSession = Depends(get_db_session),
|
||||||
|
current_user: User = Depends(get_current_active_user)
|
||||||
|
):
|
||||||
|
order = SalesOrder(
|
||||||
|
order_no=generate_order_no("SO"),
|
||||||
|
customer_id=order_data.customer_id,
|
||||||
|
delivery_date=order_data.delivery_date,
|
||||||
|
remark=order_data.remark,
|
||||||
|
operator_id=current_user.id,
|
||||||
|
status="draft"
|
||||||
|
)
|
||||||
|
db_session.add(order)
|
||||||
|
await db_session.flush()
|
||||||
|
|
||||||
|
total_amount = 0
|
||||||
|
for item_data in order_data.items:
|
||||||
|
item = SalesOrderItem(
|
||||||
|
order_id=order.id,
|
||||||
|
product_id=item_data.product_id,
|
||||||
|
quantity=item_data.quantity,
|
||||||
|
unit_price=item_data.unit_price,
|
||||||
|
amount=item_data.quantity * item_data.unit_price,
|
||||||
|
remark=item_data.remark
|
||||||
|
)
|
||||||
|
db_session.add(item)
|
||||||
|
total_amount += item.amount
|
||||||
|
|
||||||
|
order.total_amount = total_amount
|
||||||
|
await db_session.commit()
|
||||||
|
await db_session.refresh(order)
|
||||||
|
|
||||||
|
customer = await db_session.execute(select(Customer).where(Customer.id == order.customer_id))
|
||||||
|
customer = customer.scalar_one()
|
||||||
|
|
||||||
|
return SalesOrderResponse(
|
||||||
|
id=order.id,
|
||||||
|
order_no=order.order_no,
|
||||||
|
customer_name=customer.name,
|
||||||
|
order_date=order.order_date,
|
||||||
|
delivery_date=order.delivery_date,
|
||||||
|
status=order.status,
|
||||||
|
total_amount=order.total_amount,
|
||||||
|
received_amount=order.received_amount,
|
||||||
|
remark=order.remark,
|
||||||
|
created_at=order.created_at
|
||||||
|
)
|
||||||
+1
-20
@@ -21,35 +21,16 @@ logger = get_logger(__name__)
|
|||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
# 服务实例
|
|
||||||
stp_parser = STPParser()
|
stp_parser = STPParser()
|
||||||
geometry_analyzer = GeometryAnalyzer()
|
geometry_analyzer = GeometryAnalyzer()
|
||||||
file_handler = FileHandler()
|
file_handler = FileHandler()
|
||||||
html_generator = HTMLGenerator()
|
html_generator = HTMLGenerator()
|
||||||
# 初始化模具生成器(可配置不同材料的收缩率)
|
mold_generator = MoldCavityGenerator(shrinkage_rate=0.005)
|
||||||
mold_generator = MoldCavityGenerator(shrinkage_rate=0.005) # ABS材料
|
|
||||||
mesh_generator = MeshGenerator(quality="medium")
|
mesh_generator = MeshGenerator(quality="medium")
|
||||||
|
|
||||||
# 内存中的任务存储
|
|
||||||
tasks = {}
|
tasks = {}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/")
|
|
||||||
@router.post("/")
|
|
||||||
async def read_root(request: Request):
|
|
||||||
"""主页面"""
|
|
||||||
from fastapi.templating import Jinja2Templates
|
|
||||||
import os
|
|
||||||
# 简化路径配置,直接使用当前工作目录下的templates文件夹
|
|
||||||
templates_dir = os.path.join(os.getcwd(), "templates")
|
|
||||||
templates = Jinja2Templates(directory=templates_dir)
|
|
||||||
return templates.TemplateResponse("index.html", {
|
|
||||||
"request": request,
|
|
||||||
"pythonocc_available": True,
|
|
||||||
"version": "3.0.0"
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/health")
|
@router.get("/health")
|
||||||
@router.post("/health")
|
@router.post("/health")
|
||||||
async def health():
|
async def health():
|
||||||
|
|||||||
+40
-17
@@ -40,17 +40,17 @@ from fastapi.templating import Jinja2Templates
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
from api.routes import router
|
from api.routes import router
|
||||||
|
from api.auth_routes import router as auth_router
|
||||||
|
from api.inventory_routes import router as inventory_router
|
||||||
from utils.logger import setup_logging
|
from utils.logger import setup_logging
|
||||||
from database.init_db import init_database
|
from database.init_db import init_database
|
||||||
|
|
||||||
# 设置日志
|
|
||||||
setup_logging()
|
setup_logging()
|
||||||
|
|
||||||
# 创建FastAPI应用
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="模具几何分析服务",
|
title="Gemold - 模具制造管理系统",
|
||||||
description="基于PythonOCC的STP文件几何分析和模具设计建议服务",
|
description="模具制造行业综合管理平台,包含模具分析、进销存管理等功能",
|
||||||
version="3.0.0"
|
version="4.0.0"
|
||||||
)
|
)
|
||||||
|
|
||||||
# 启动时初始化数据库和RustFS
|
# 启动时初始化数据库和RustFS
|
||||||
@@ -96,7 +96,8 @@ import os
|
|||||||
static_dir = os.path.join(os.getcwd(), "static")
|
static_dir = os.path.join(os.getcwd(), "static")
|
||||||
app.mount("/static", StaticFiles(directory=static_dir), name="static")
|
app.mount("/static", StaticFiles(directory=static_dir), name="static")
|
||||||
|
|
||||||
# 注册路由
|
app.include_router(auth_router)
|
||||||
|
app.include_router(inventory_router)
|
||||||
app.include_router(router)
|
app.include_router(router)
|
||||||
|
|
||||||
|
|
||||||
@@ -106,25 +107,47 @@ async def health():
|
|||||||
from database.database import db_manager
|
from database.database import db_manager
|
||||||
return {
|
return {
|
||||||
"status": "healthy",
|
"status": "healthy",
|
||||||
"service": "mold-geometry-analysis",
|
"service": "gemold",
|
||||||
|
"version": "4.0.0",
|
||||||
"database_connected": db_manager.is_connected
|
"database_connected": db_manager.is_connected
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
async def root():
|
||||||
|
from fastapi.responses import FileResponse
|
||||||
|
return FileResponse(os.path.join(os.getcwd(), "static", "index.html"))
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/moldinsight")
|
||||||
|
async def moldinsight():
|
||||||
|
from fastapi.responses import FileResponse
|
||||||
|
return FileResponse(os.path.join(os.getcwd(), "static", "index.html"))
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/inventory")
|
||||||
|
async def inventory():
|
||||||
|
from fastapi.responses import FileResponse
|
||||||
|
return FileResponse(os.path.join(os.getcwd(), "static", "index.html"))
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/users")
|
||||||
|
async def users():
|
||||||
|
from fastapi.responses import FileResponse
|
||||||
|
return FileResponse(os.path.join(os.getcwd(), "static", "index.html"))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import uvicorn
|
import uvicorn
|
||||||
|
|
||||||
# 从配置文件获取端口配置
|
|
||||||
from config.settings import settings
|
from config.settings import settings
|
||||||
|
|
||||||
print("启动模具几何分析服务 v3.0...")
|
print("启动 Gemold 模具制造管理系统 v4.0...")
|
||||||
print(f"访问 http://localhost:{settings.PORT} 使用网页界面")
|
print(f"访问 http://localhost:{settings.PORT}")
|
||||||
print("新增功能:")
|
print("功能模块:")
|
||||||
print(" - STP文件解析为JSON数据")
|
print(" - 首页仪表盘")
|
||||||
print(" - 数据存储到PostgreSQL数据库")
|
print(" - 用户管理")
|
||||||
print(" - 自动生成3D可视化HTML页面")
|
print(" - MoldInsight 模具分析")
|
||||||
print(" - 源文件、JSON数据、HTML文件统一管理")
|
print(" - 进销存管理")
|
||||||
print(f"调试接口: http://localhost:{settings.PORT}/debug/tasks")
|
|
||||||
|
|
||||||
uvicorn.run(
|
uvicorn.run(
|
||||||
"main:app",
|
"main:app",
|
||||||
|
|||||||
@@ -362,3 +362,233 @@ class SystemLog(Base):
|
|||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<SystemLog(id={self.id}, level='{self.level}', module='{self.module}')>"
|
return f"<SystemLog(id={self.id}, level='{self.level}', module='{self.module}')>"
|
||||||
|
|
||||||
|
|
||||||
|
class Product(Base):
|
||||||
|
"""产品表"""
|
||||||
|
__tablename__ = "products"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
sku = Column(String(50), unique=True, index=True, nullable=False)
|
||||||
|
name = Column(String(200), nullable=False)
|
||||||
|
description = Column(Text, nullable=True)
|
||||||
|
category = Column(String(100), nullable=True)
|
||||||
|
unit = Column(String(20), default="件")
|
||||||
|
cost_price = Column(Float, default=0)
|
||||||
|
sale_price = Column(Float, default=0)
|
||||||
|
min_stock = Column(Integer, default=0)
|
||||||
|
max_stock = Column(Integer, default=1000)
|
||||||
|
is_active = Column(Boolean, default=True)
|
||||||
|
created_at = Column(DateTime, default=func.now())
|
||||||
|
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
|
inventory = relationship("Inventory", back_populates="product", uselist=False)
|
||||||
|
stock_movements = relationship("StockMovement", back_populates="product")
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<Product(id={self.id}, sku='{self.sku}', name='{self.name}')>"
|
||||||
|
|
||||||
|
|
||||||
|
class Supplier(Base):
|
||||||
|
"""供应商表"""
|
||||||
|
__tablename__ = "suppliers"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
code = Column(String(50), unique=True, index=True)
|
||||||
|
name = Column(String(200), nullable=False)
|
||||||
|
contact_person = Column(String(100), nullable=True)
|
||||||
|
phone = Column(String(50), nullable=True)
|
||||||
|
email = Column(String(100), nullable=True)
|
||||||
|
address = Column(Text, nullable=True)
|
||||||
|
bank_name = Column(String(100), nullable=True)
|
||||||
|
bank_account = Column(String(50), nullable=True)
|
||||||
|
tax_number = Column(String(50), nullable=True)
|
||||||
|
is_active = Column(Boolean, default=True)
|
||||||
|
created_at = Column(DateTime, default=func.now())
|
||||||
|
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
|
purchase_orders = relationship("PurchaseOrder", back_populates="supplier")
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<Supplier(id={self.id}, name='{self.name}')>"
|
||||||
|
|
||||||
|
|
||||||
|
class Customer(Base):
|
||||||
|
"""客户表"""
|
||||||
|
__tablename__ = "customers"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
code = Column(String(50), unique=True, index=True)
|
||||||
|
name = Column(String(200), nullable=False)
|
||||||
|
contact_person = Column(String(100), nullable=True)
|
||||||
|
phone = Column(String(50), nullable=True)
|
||||||
|
email = Column(String(100), nullable=True)
|
||||||
|
address = Column(Text, nullable=True)
|
||||||
|
bank_name = Column(String(100), nullable=True)
|
||||||
|
bank_account = Column(String(50), nullable=True)
|
||||||
|
tax_number = Column(String(50), nullable=True)
|
||||||
|
credit_limit = Column(Float, default=0)
|
||||||
|
is_active = Column(Boolean, default=True)
|
||||||
|
created_at = Column(DateTime, default=func.now())
|
||||||
|
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
|
sales_orders = relationship("SalesOrder", back_populates="customer")
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<Customer(id={self.id}, name='{self.name}')>"
|
||||||
|
|
||||||
|
|
||||||
|
class Warehouse(Base):
|
||||||
|
"""仓库表"""
|
||||||
|
__tablename__ = "warehouses"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
code = Column(String(50), unique=True, index=True)
|
||||||
|
name = Column(String(200), nullable=False)
|
||||||
|
address = Column(Text, nullable=True)
|
||||||
|
manager = Column(String(100), nullable=True)
|
||||||
|
phone = Column(String(50), nullable=True)
|
||||||
|
is_active = Column(Boolean, default=True)
|
||||||
|
is_default = Column(Boolean, default=False)
|
||||||
|
created_at = Column(DateTime, default=func.now())
|
||||||
|
|
||||||
|
inventories = relationship("Inventory", back_populates="warehouse")
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<Warehouse(id={self.id}, name='{self.name}')>"
|
||||||
|
|
||||||
|
|
||||||
|
class Inventory(Base):
|
||||||
|
"""库存表"""
|
||||||
|
__tablename__ = "inventory"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
product_id = Column(Integer, ForeignKey("products.id"), nullable=False, index=True)
|
||||||
|
warehouse_id = Column(Integer, ForeignKey("warehouses.id"), nullable=False, index=True)
|
||||||
|
quantity = Column(Integer, default=0)
|
||||||
|
locked_quantity = Column(Integer, default=0)
|
||||||
|
batch_number = Column(String(50), nullable=True)
|
||||||
|
location = Column(String(100), nullable=True)
|
||||||
|
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
|
product = relationship("Product", back_populates="inventory")
|
||||||
|
warehouse = relationship("Warehouse", back_populates="inventories")
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<Inventory(product_id={self.product_id}, quantity={self.quantity})>"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available_quantity(self):
|
||||||
|
return self.quantity - self.locked_quantity
|
||||||
|
|
||||||
|
|
||||||
|
class StockMovement(Base):
|
||||||
|
"""库存变动记录表"""
|
||||||
|
__tablename__ = "stock_movements"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
product_id = Column(Integer, ForeignKey("products.id"), nullable=False, index=True)
|
||||||
|
warehouse_id = Column(Integer, ForeignKey("warehouses.id"), nullable=False)
|
||||||
|
movement_type = Column(String(20), nullable=False)
|
||||||
|
quantity = Column(Integer, nullable=False)
|
||||||
|
before_quantity = Column(Integer, default=0)
|
||||||
|
after_quantity = Column(Integer, default=0)
|
||||||
|
reference_type = Column(String(50), nullable=True)
|
||||||
|
reference_id = Column(Integer, nullable=True)
|
||||||
|
reference_no = Column(String(50), nullable=True)
|
||||||
|
unit_price = Column(Float, nullable=True)
|
||||||
|
total_amount = Column(Float, nullable=True)
|
||||||
|
remark = Column(Text, nullable=True)
|
||||||
|
operator_id = Column(Integer, ForeignKey("users.id"), nullable=True)
|
||||||
|
created_at = Column(DateTime, default=func.now(), index=True)
|
||||||
|
|
||||||
|
product = relationship("Product", back_populates="stock_movements")
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<StockMovement(id={self.id}, type='{self.movement_type}', qty={self.quantity})>"
|
||||||
|
|
||||||
|
|
||||||
|
class PurchaseOrder(Base):
|
||||||
|
"""采购订单表"""
|
||||||
|
__tablename__ = "purchase_orders"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
order_no = Column(String(50), unique=True, index=True, nullable=False)
|
||||||
|
supplier_id = Column(Integer, ForeignKey("suppliers.id"), nullable=False, index=True)
|
||||||
|
order_date = Column(DateTime, default=func.now())
|
||||||
|
expected_date = Column(DateTime, nullable=True)
|
||||||
|
status = Column(String(20), default="draft")
|
||||||
|
total_amount = Column(Float, default=0)
|
||||||
|
paid_amount = Column(Float, default=0)
|
||||||
|
remark = Column(Text, nullable=True)
|
||||||
|
operator_id = Column(Integer, ForeignKey("users.id"), nullable=True)
|
||||||
|
created_at = Column(DateTime, default=func.now())
|
||||||
|
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
|
supplier = relationship("Supplier", back_populates="purchase_orders")
|
||||||
|
items = relationship("PurchaseOrderItem", back_populates="order", cascade="all, delete-orphan")
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<PurchaseOrder(order_no='{self.order_no}', status='{self.status}')>"
|
||||||
|
|
||||||
|
|
||||||
|
class PurchaseOrderItem(Base):
|
||||||
|
"""采购订单明细表"""
|
||||||
|
__tablename__ = "purchase_order_items"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
order_id = Column(Integer, ForeignKey("purchase_orders.id"), nullable=False)
|
||||||
|
product_id = Column(Integer, ForeignKey("products.id"), nullable=False)
|
||||||
|
quantity = Column(Integer, nullable=False)
|
||||||
|
received_quantity = Column(Integer, default=0)
|
||||||
|
unit_price = Column(Float, nullable=False)
|
||||||
|
amount = Column(Float, nullable=False)
|
||||||
|
remark = Column(Text, nullable=True)
|
||||||
|
|
||||||
|
order = relationship("PurchaseOrder", back_populates="items")
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<PurchaseOrderItem(order_id={self.order_id}, product_id={self.product_id})>"
|
||||||
|
|
||||||
|
|
||||||
|
class SalesOrder(Base):
|
||||||
|
"""销售订单表"""
|
||||||
|
__tablename__ = "sales_orders"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
order_no = Column(String(50), unique=True, index=True, nullable=False)
|
||||||
|
customer_id = Column(Integer, ForeignKey("customers.id"), nullable=False, index=True)
|
||||||
|
order_date = Column(DateTime, default=func.now())
|
||||||
|
delivery_date = Column(DateTime, nullable=True)
|
||||||
|
status = Column(String(20), default="draft")
|
||||||
|
total_amount = Column(Float, default=0)
|
||||||
|
received_amount = Column(Float, default=0)
|
||||||
|
remark = Column(Text, nullable=True)
|
||||||
|
operator_id = Column(Integer, ForeignKey("users.id"), nullable=True)
|
||||||
|
created_at = Column(DateTime, default=func.now())
|
||||||
|
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
||||||
|
|
||||||
|
customer = relationship("Customer", back_populates="sales_orders")
|
||||||
|
items = relationship("SalesOrderItem", back_populates="order", cascade="all, delete-orphan")
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<SalesOrder(order_no='{self.order_no}', status='{self.status}')>"
|
||||||
|
|
||||||
|
|
||||||
|
class SalesOrderItem(Base):
|
||||||
|
"""销售订单明细表"""
|
||||||
|
__tablename__ = "sales_order_items"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
order_id = Column(Integer, ForeignKey("sales_orders.id"), nullable=False)
|
||||||
|
product_id = Column(Integer, ForeignKey("products.id"), nullable=False)
|
||||||
|
quantity = Column(Integer, nullable=False)
|
||||||
|
delivered_quantity = Column(Integer, default=0)
|
||||||
|
unit_price = Column(Float, nullable=False)
|
||||||
|
amount = Column(Float, nullable=False)
|
||||||
|
remark = Column(Text, nullable=True)
|
||||||
|
|
||||||
|
order = relationship("SalesOrder", back_populates="items")
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<SalesOrderItem(order_id={self.order_id}, product_id={self.product_id})>"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,145 @@
|
|||||||
|
from datetime import datetime, timedelta
|
||||||
|
from typing import Optional
|
||||||
|
from jose import JWTError, jwt
|
||||||
|
from passlib.context import CryptContext
|
||||||
|
from fastapi import Depends, HTTPException, status
|
||||||
|
from fastapi.security import OAuth2PasswordBearer
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.orm import selectinload
|
||||||
|
|
||||||
|
from config.settings import settings
|
||||||
|
from database.database import get_db_session
|
||||||
|
from models.database import User
|
||||||
|
|
||||||
|
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||||
|
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login", auto_error=False)
|
||||||
|
|
||||||
|
|
||||||
|
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||||||
|
return pwd_context.verify(plain_password, hashed_password)
|
||||||
|
|
||||||
|
|
||||||
|
def get_password_hash(password: str) -> str:
|
||||||
|
return pwd_context.hash(password)
|
||||||
|
|
||||||
|
|
||||||
|
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
|
||||||
|
to_encode = data.copy()
|
||||||
|
if expires_delta:
|
||||||
|
expire = datetime.utcnow() + expires_delta
|
||||||
|
else:
|
||||||
|
expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||||
|
to_encode.update({"exp": expire})
|
||||||
|
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
|
||||||
|
return encoded_jwt
|
||||||
|
|
||||||
|
|
||||||
|
async def get_current_user(
|
||||||
|
token: Optional[str] = Depends(oauth2_scheme),
|
||||||
|
db_session: AsyncSession = Depends(get_db_session)
|
||||||
|
) -> Optional[User]:
|
||||||
|
if not token:
|
||||||
|
return None
|
||||||
|
|
||||||
|
credentials_exception = HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="无法验证凭据",
|
||||||
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
|
||||||
|
username: str = payload.get("sub")
|
||||||
|
if username is None:
|
||||||
|
raise credentials_exception
|
||||||
|
except JWTError:
|
||||||
|
raise credentials_exception
|
||||||
|
|
||||||
|
result = await db_session.execute(
|
||||||
|
select(User).where(User.username == username)
|
||||||
|
)
|
||||||
|
user = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if user is None:
|
||||||
|
raise credentials_exception
|
||||||
|
if not user.is_active:
|
||||||
|
raise HTTPException(status_code=400, detail="用户已被禁用")
|
||||||
|
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
async def get_current_active_user(
|
||||||
|
current_user: Optional[User] = Depends(get_current_user)
|
||||||
|
) -> User:
|
||||||
|
if not current_user:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="请先登录",
|
||||||
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
|
)
|
||||||
|
return current_user
|
||||||
|
|
||||||
|
|
||||||
|
async def get_current_admin_user(
|
||||||
|
current_user: User = Depends(get_current_active_user)
|
||||||
|
) -> User:
|
||||||
|
if not current_user.is_superuser:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="需要管理员权限"
|
||||||
|
)
|
||||||
|
return current_user
|
||||||
|
|
||||||
|
|
||||||
|
async def authenticate_user(db_session: AsyncSession, username: str, password: str) -> Optional[User]:
|
||||||
|
result = await db_session.execute(
|
||||||
|
select(User).where(User.username == username)
|
||||||
|
)
|
||||||
|
user = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
return None
|
||||||
|
if not verify_password(password, user.hashed_password):
|
||||||
|
return None
|
||||||
|
|
||||||
|
user.last_login = datetime.utcnow()
|
||||||
|
await db_session.commit()
|
||||||
|
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
async def create_user(
|
||||||
|
db_session: AsyncSession,
|
||||||
|
username: str,
|
||||||
|
email: str,
|
||||||
|
password: str,
|
||||||
|
full_name: Optional[str] = None,
|
||||||
|
is_superuser: bool = False
|
||||||
|
) -> User:
|
||||||
|
hashed_password = get_password_hash(password)
|
||||||
|
user = User(
|
||||||
|
username=username,
|
||||||
|
email=email,
|
||||||
|
hashed_password=hashed_password,
|
||||||
|
full_name=full_name,
|
||||||
|
is_superuser=is_superuser,
|
||||||
|
is_active=True
|
||||||
|
)
|
||||||
|
db_session.add(user)
|
||||||
|
await db_session.commit()
|
||||||
|
await db_session.refresh(user)
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
async def get_user_by_username(db_session: AsyncSession, username: str) -> Optional[User]:
|
||||||
|
result = await db_session.execute(
|
||||||
|
select(User).where(User.username == username)
|
||||||
|
)
|
||||||
|
return result.scalar_one_or_none()
|
||||||
|
|
||||||
|
|
||||||
|
async def get_user_by_email(db_session: AsyncSession, email: str) -> Optional[User]:
|
||||||
|
result = await db_session.execute(
|
||||||
|
select(User).where(User.email == email)
|
||||||
|
)
|
||||||
|
return result.scalar_one_or_none()
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Gemold - 模具制造管理系统</title>
|
||||||
|
<link rel="stylesheet" href="/static/style.css">
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app">
|
||||||
|
<div class="loading-state" style="height: 100vh;">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
<span>加载中...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
|
||||||
|
<script src="https://unpkg.com/vue-router@4/dist/vue-router.global.prod.js"></script>
|
||||||
|
<script src="/static/vue-app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1307,3 +1307,471 @@ a:hover {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
border: 0;
|
border: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.badge-primary {
|
||||||
|
background: var(--primary-100);
|
||||||
|
color: var(--primary-700);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-warning {
|
||||||
|
color: var(--warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-section {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-name {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
font-weight: var(--font-medium);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-badge {
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
padding: var(--space-1) var(--space-2);
|
||||||
|
background: var(--primary-100);
|
||||||
|
color: var(--primary-700);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-logout {
|
||||||
|
padding: var(--space-2) var(--space-4);
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
font-weight: var(--font-medium);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--border-default);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all var(--duration-fast) var(--ease-default);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-logout:hover {
|
||||||
|
background: var(--gray-100);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-login {
|
||||||
|
padding: var(--space-2) var(--space-4);
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
font-weight: var(--font-medium);
|
||||||
|
color: white;
|
||||||
|
background: var(--gradient-primary);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all var(--duration-fast) var(--ease-default);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-login:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-page {
|
||||||
|
min-height: calc(100vh - 200px);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: var(--space-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-card {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border-radius: var(--radius-xl);
|
||||||
|
box-shadow: var(--shadow-xl);
|
||||||
|
padding: var(--space-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: var(--space-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-logo {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
background: var(--gradient-primary);
|
||||||
|
border-radius: var(--radius-xl);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: white;
|
||||||
|
margin: 0 auto var(--space-4);
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-header h1 {
|
||||||
|
font-size: var(--text-2xl);
|
||||||
|
margin-bottom: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-header p {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
font-weight: var(--font-medium);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input {
|
||||||
|
padding: var(--space-3) var(--space-4);
|
||||||
|
font-size: var(--text-base);
|
||||||
|
border: 1px solid var(--border-default);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background: var(--bg-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
transition: all var(--duration-fast) var(--ease-default);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--primary-500);
|
||||||
|
box-shadow: 0 0 0 3px var(--primary-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input::placeholder {
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
padding: var(--space-3);
|
||||||
|
background: var(--error-bg);
|
||||||
|
color: var(--error);
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-full {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-footer {
|
||||||
|
margin-top: var(--space-6);
|
||||||
|
text-align: center;
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-footer a {
|
||||||
|
color: var(--primary-600);
|
||||||
|
font-weight: var(--font-medium);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-footer a:hover {
|
||||||
|
color: var(--primary-700);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
margin-bottom: var(--space-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header h1 {
|
||||||
|
font-size: var(--text-3xl);
|
||||||
|
margin-bottom: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header p {
|
||||||
|
font-size: var(--text-base);
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
margin-top: var(--space-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: var(--text-xl);
|
||||||
|
margin-bottom: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: var(--space-12);
|
||||||
|
gap: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border: 3px solid var(--gray-200);
|
||||||
|
border-top-color: var(--primary-500);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: var(--space-12);
|
||||||
|
color: var(--error);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-2);
|
||||||
|
margin-bottom: var(--space-6);
|
||||||
|
border-bottom: 1px solid var(--border-light);
|
||||||
|
padding-bottom: var(--space-2);
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
padding: var(--space-2) var(--space-4);
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
font-weight: var(--font-medium);
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all var(--duration-fast) var(--ease-default);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab:hover {
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab.active {
|
||||||
|
color: var(--primary-600);
|
||||||
|
border-bottom-color: var(--primary-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-container {
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm {
|
||||||
|
padding: var(--space-1) var(--space-3);
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
font-weight: var(--font-medium);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all var(--duration-fast) var(--ease-default);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success {
|
||||||
|
background: var(--success);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success:hover {
|
||||||
|
background: var(--accent-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-warning {
|
||||||
|
background: var(--warning-bg);
|
||||||
|
color: var(--warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-warning:hover {
|
||||||
|
background: var(--warning);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-back {
|
||||||
|
padding: var(--space-2) var(--space-4);
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
font-weight: var(--font-medium);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-bottom: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-back:hover {
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-header h2 {
|
||||||
|
font-size: var(--text-2xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
|
gap: var(--space-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-card {
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: var(--space-6);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-card h3 {
|
||||||
|
font-size: var(--text-lg);
|
||||||
|
margin-bottom: var(--space-4);
|
||||||
|
padding-bottom: var(--space-3);
|
||||||
|
border-bottom: 1px solid var(--border-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-label {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
font-weight: var(--font-medium);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.viewer-section {
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: var(--space-6);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.viewer-section h3 {
|
||||||
|
font-size: var(--text-lg);
|
||||||
|
margin-bottom: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.viewer-frame {
|
||||||
|
width: 100%;
|
||||||
|
height: 500px;
|
||||||
|
border: 1px solid var(--border-light);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-actions {
|
||||||
|
margin-top: var(--space-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||||
|
gap: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-3);
|
||||||
|
padding: var(--space-6);
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
border: 1px solid var(--border-light);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all var(--duration-fast) var(--ease-default);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-card:hover {
|
||||||
|
border-color: var(--primary-300);
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-icon {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-label {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
font-weight: var(--font-medium);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-icon {
|
||||||
|
margin-right: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-4);
|
||||||
|
margin-bottom: var(--space-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-4);
|
||||||
|
padding: var(--space-3) var(--space-4);
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
border: 1px solid var(--border-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-name {
|
||||||
|
font-weight: var(--font-medium);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-size {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|||||||
+1034
-588
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user