This commit is contained in:
2026-03-04 00:47:41 +08:00
parent 54f83cd76c
commit f3196f3e87
9 changed files with 2929 additions and 650 deletions
+193
View File
@@ -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)
+769
View File
@@ -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
View File
@@ -21,35 +21,16 @@ logger = get_logger(__name__)
router = APIRouter()
# 服务实例
stp_parser = STPParser()
geometry_analyzer = GeometryAnalyzer()
file_handler = FileHandler()
html_generator = HTMLGenerator()
# 初始化模具生成器(可配置不同材料的收缩率)
mold_generator = MoldCavityGenerator(shrinkage_rate=0.005) # ABS材料
mold_generator = MoldCavityGenerator(shrinkage_rate=0.005)
mesh_generator = MeshGenerator(quality="medium")
# 内存中的任务存储
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.post("/health")
async def health():
+40 -17
View File
@@ -40,17 +40,17 @@ from fastapi.templating import Jinja2Templates
import asyncio
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 database.init_db import init_database
# 设置日志
setup_logging()
# 创建FastAPI应用
app = FastAPI(
title="模具几何分析服务",
description="基于PythonOCC的STP文件几何分析和模具设计建议服务",
version="3.0.0"
title="Gemold - 模具制造管理系统",
description="模具制造行业综合管理平台,包含模具分析、进销存管理等功能",
version="4.0.0"
)
# 启动时初始化数据库和RustFS
@@ -96,7 +96,8 @@ import os
static_dir = os.path.join(os.getcwd(), "static")
app.mount("/static", StaticFiles(directory=static_dir), name="static")
# 注册路由
app.include_router(auth_router)
app.include_router(inventory_router)
app.include_router(router)
@@ -106,25 +107,47 @@ async def health():
from database.database import db_manager
return {
"status": "healthy",
"service": "mold-geometry-analysis",
"service": "gemold",
"version": "4.0.0",
"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__":
import uvicorn
# 从配置文件获取端口配置
from config.settings import settings
print("启动模具几何分析服务 v3.0...")
print(f"访问 http://localhost:{settings.PORT} 使用网页界面")
print("新增功能:")
print(" - STP文件解析为JSON数据")
print(" - 数据存储到PostgreSQL数据库")
print(" - 自动生成3D可视化HTML页面")
print(" - 源文件、JSON数据、HTML文件统一管理")
print(f"调试接口: http://localhost:{settings.PORT}/debug/tasks")
print("启动 Gemold 模具制造管理系统 v4.0...")
print(f"访问 http://localhost:{settings.PORT}")
print("功能模块:")
print(" - 首页仪表盘")
print(" - 用户管理")
print(" - MoldInsight 模具分析")
print(" - 进销存管理")
uvicorn.run(
"main:app",
+230
View File
@@ -362,3 +362,233 @@ class SystemLog(Base):
def __repr__(self):
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})>"
+145
View File
@@ -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()
+24
View File
@@ -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>
+468
View File
@@ -1307,3 +1307,471 @@ a:hover {
white-space: nowrap;
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);
}
+1059 -613
View File
File diff suppressed because it is too large Load Diff