增加前端增删改查
This commit is contained in:
@@ -4,17 +4,19 @@
|
|||||||
提供客户信息的管理功能,包括:
|
提供客户信息的管理功能,包括:
|
||||||
- 客户列表查询(支持分页、搜索)
|
- 客户列表查询(支持分页、搜索)
|
||||||
- 创建新客户(自动生成客户编码)
|
- 创建新客户(自动生成客户编码)
|
||||||
|
- 更新客户信息
|
||||||
|
- 删除客户(软删除)
|
||||||
|
|
||||||
路由前缀: /api/customers
|
路由前缀: /api/customers
|
||||||
"""
|
"""
|
||||||
from fastapi import APIRouter, Depends, Query
|
from fastapi import APIRouter, Depends, Query, HTTPException
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from typing import Optional, List
|
from typing import Optional, List
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from database.database import get_db_session
|
from database.database import get_db_session
|
||||||
from services.auth_service import get_current_active_user
|
from services.auth_service import get_current_active_user, get_current_admin_user
|
||||||
from models.database import User, Customer
|
from models.database import User, Customer
|
||||||
from .schemas import CustomerCreate, CustomerResponse
|
from .schemas import CustomerCreate, CustomerResponse
|
||||||
|
|
||||||
@@ -52,3 +54,39 @@ async def create_customer(
|
|||||||
await db_session.commit()
|
await db_session.commit()
|
||||||
await db_session.refresh(customer)
|
await db_session.refresh(customer)
|
||||||
return CustomerResponse.from_orm(customer)
|
return CustomerResponse.from_orm(customer)
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/{customer_id}", response_model=CustomerResponse)
|
||||||
|
async def update_customer(
|
||||||
|
customer_id: int,
|
||||||
|
customer_data: CustomerCreate,
|
||||||
|
db_session: AsyncSession = Depends(get_db_session),
|
||||||
|
current_user: User = Depends(get_current_active_user)
|
||||||
|
):
|
||||||
|
result = await db_session.execute(select(Customer).where(Customer.id == customer_id))
|
||||||
|
customer = result.scalar_one_or_none()
|
||||||
|
if not customer:
|
||||||
|
raise HTTPException(status_code=404, detail="客户不存在")
|
||||||
|
|
||||||
|
for key, value in customer_data.dict().items():
|
||||||
|
setattr(customer, key, value)
|
||||||
|
|
||||||
|
await db_session.commit()
|
||||||
|
await db_session.refresh(customer)
|
||||||
|
return CustomerResponse.from_orm(customer)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{customer_id}")
|
||||||
|
async def delete_customer(
|
||||||
|
customer_id: int,
|
||||||
|
db_session: AsyncSession = Depends(get_db_session),
|
||||||
|
current_user: User = Depends(get_current_admin_user)
|
||||||
|
):
|
||||||
|
result = await db_session.execute(select(Customer).where(Customer.id == customer_id))
|
||||||
|
customer = result.scalar_one_or_none()
|
||||||
|
if not customer:
|
||||||
|
raise HTTPException(status_code=404, detail="客户不存在")
|
||||||
|
|
||||||
|
customer.is_active = False
|
||||||
|
await db_session.commit()
|
||||||
|
return {"message": "客户已删除"}
|
||||||
|
|||||||
@@ -4,17 +4,19 @@
|
|||||||
提供供应商信息的管理功能,包括:
|
提供供应商信息的管理功能,包括:
|
||||||
- 供应商列表查询(支持分页、搜索)
|
- 供应商列表查询(支持分页、搜索)
|
||||||
- 创建新供应商(自动生成供应商编码)
|
- 创建新供应商(自动生成供应商编码)
|
||||||
|
- 更新供应商信息
|
||||||
|
- 删除供应商(软删除)
|
||||||
|
|
||||||
路由前缀: /api/suppliers
|
路由前缀: /api/suppliers
|
||||||
"""
|
"""
|
||||||
from fastapi import APIRouter, Depends, Query
|
from fastapi import APIRouter, Depends, Query, HTTPException
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from typing import Optional, List
|
from typing import Optional, List
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from database.database import get_db_session
|
from database.database import get_db_session
|
||||||
from services.auth_service import get_current_active_user
|
from services.auth_service import get_current_active_user, get_current_admin_user
|
||||||
from models.database import User, Supplier
|
from models.database import User, Supplier
|
||||||
from .schemas import SupplierCreate, SupplierResponse
|
from .schemas import SupplierCreate, SupplierResponse
|
||||||
|
|
||||||
@@ -52,3 +54,39 @@ async def create_supplier(
|
|||||||
await db_session.commit()
|
await db_session.commit()
|
||||||
await db_session.refresh(supplier)
|
await db_session.refresh(supplier)
|
||||||
return SupplierResponse.from_orm(supplier)
|
return SupplierResponse.from_orm(supplier)
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/{supplier_id}", response_model=SupplierResponse)
|
||||||
|
async def update_supplier(
|
||||||
|
supplier_id: int,
|
||||||
|
supplier_data: SupplierCreate,
|
||||||
|
db_session: AsyncSession = Depends(get_db_session),
|
||||||
|
current_user: User = Depends(get_current_active_user)
|
||||||
|
):
|
||||||
|
result = await db_session.execute(select(Supplier).where(Supplier.id == supplier_id))
|
||||||
|
supplier = result.scalar_one_or_none()
|
||||||
|
if not supplier:
|
||||||
|
raise HTTPException(status_code=404, detail="供应商不存在")
|
||||||
|
|
||||||
|
for key, value in supplier_data.dict().items():
|
||||||
|
setattr(supplier, key, value)
|
||||||
|
|
||||||
|
await db_session.commit()
|
||||||
|
await db_session.refresh(supplier)
|
||||||
|
return SupplierResponse.from_orm(supplier)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{supplier_id}")
|
||||||
|
async def delete_supplier(
|
||||||
|
supplier_id: int,
|
||||||
|
db_session: AsyncSession = Depends(get_db_session),
|
||||||
|
current_user: User = Depends(get_current_admin_user)
|
||||||
|
):
|
||||||
|
result = await db_session.execute(select(Supplier).where(Supplier.id == supplier_id))
|
||||||
|
supplier = result.scalar_one_or_none()
|
||||||
|
if not supplier:
|
||||||
|
raise HTTPException(status_code=404, detail="供应商不存在")
|
||||||
|
|
||||||
|
supplier.is_active = False
|
||||||
|
await db_session.commit()
|
||||||
|
return {"message": "供应商已删除"}
|
||||||
|
|||||||
+245
-7
@@ -1477,13 +1477,40 @@ select.form-input {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ================================
|
/* ================================
|
||||||
标签页
|
标签页 - 现代风格
|
||||||
================================ */
|
================================ */
|
||||||
.tabs {
|
.tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--space-1);
|
gap: var(--space-2);
|
||||||
border-bottom: 1px solid var(--border-light);
|
background: var(--bg-secondary);
|
||||||
|
padding: var(--space-1);
|
||||||
|
border-radius: var(--radius-xl);
|
||||||
margin-bottom: var(--space-6);
|
margin-bottom: var(--space-6);
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
padding: var(--space-3) var(--space-5);
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
font-weight: var(--font-medium);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all var(--duration-fast) var(--ease-default);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab:hover {
|
||||||
|
color: var(--text-primary);
|
||||||
|
background: var(--bg-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab.active {
|
||||||
|
color: var(--primary-600);
|
||||||
|
background: var(--bg-primary);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-item {
|
.tab-item {
|
||||||
@@ -1534,7 +1561,7 @@ select.form-input {
|
|||||||
.hidden { display: none; }
|
.hidden { display: none; }
|
||||||
|
|
||||||
/* ================================
|
/* ================================
|
||||||
统计卡片
|
统计卡片 - 现代风格
|
||||||
================================ */
|
================================ */
|
||||||
.stats-grid {
|
.stats-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -1543,6 +1570,13 @@ select.form-input {
|
|||||||
margin-bottom: var(--space-6);
|
margin-bottom: var(--space-6);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dashboard-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||||
|
gap: var(--space-5);
|
||||||
|
margin-bottom: var(--space-8);
|
||||||
|
}
|
||||||
|
|
||||||
.stat-card {
|
.stat-card {
|
||||||
background: var(--bg-primary);
|
background: var(--bg-primary);
|
||||||
border: 1px solid var(--border-light);
|
border: 1px solid var(--border-light);
|
||||||
@@ -1550,21 +1584,33 @@ select.form-input {
|
|||||||
padding: var(--space-5);
|
padding: var(--space-5);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all var(--duration-fast) var(--ease-default);
|
transition: all var(--duration-fast) var(--ease-default);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-4);
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-card:hover {
|
.stat-card:hover {
|
||||||
border-color: var(--primary-300);
|
border-color: var(--primary-200);
|
||||||
box-shadow: var(--shadow-md);
|
box-shadow: var(--shadow-md);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card .stat-icon {
|
||||||
|
font-size: var(--text-3xl);
|
||||||
|
line-height: 1;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-content {
|
.stat-content {
|
||||||
text-align: center;
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-value {
|
.stat-value {
|
||||||
font-size: var(--text-3xl);
|
font-size: var(--text-2xl);
|
||||||
font-weight: var(--font-bold);
|
font-weight: var(--font-bold);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
|
line-height: 1.2;
|
||||||
margin-bottom: var(--space-1);
|
margin-bottom: var(--space-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1573,6 +1619,198 @@ select.form-input {
|
|||||||
color: var(--text-tertiary);
|
color: var(--text-tertiary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.stat-icon {
|
||||||
|
font-size: var(--text-2xl);
|
||||||
|
margin-bottom: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================
|
||||||
|
快速操作 - 现代风格
|
||||||
|
================================ */
|
||||||
|
.quick-actions {
|
||||||
|
margin-top: var(--space-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: var(--text-lg);
|
||||||
|
font-weight: var(--font-semibold);
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin-bottom: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||||
|
gap: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: var(--space-3);
|
||||||
|
padding: var(--space-6) var(--space-4);
|
||||||
|
background: linear-gradient(135deg, var(--bg-primary) 0%, var(--bg-secondary) 100%);
|
||||||
|
border: 1px solid var(--border-light);
|
||||||
|
border-radius: var(--radius-xl);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all var(--duration-normal) var(--ease-default);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-card::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 3px;
|
||||||
|
background: linear-gradient(90deg, var(--primary-400), var(--primary-600));
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity var(--duration-fast) var(--ease-default);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-card:hover {
|
||||||
|
border-color: var(--primary-200);
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
background: linear-gradient(135deg, var(--primary-50) 0%, var(--bg-primary) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-card:hover::before {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-card:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-icon {
|
||||||
|
font-size: var(--text-3xl);
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-label {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
font-weight: var(--font-medium);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 不同操作按钮的颜色变体 */
|
||||||
|
.action-card:nth-child(1):hover {
|
||||||
|
background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%);
|
||||||
|
border-color: var(--primary-200);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-card:nth-child(2):hover {
|
||||||
|
background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%);
|
||||||
|
border-color: #86efac;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-card:nth-child(3):hover {
|
||||||
|
background: linear-gradient(135deg, #fefce8 0%, #fef9c3 100%);
|
||||||
|
border-color: #fde047;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-card:nth-child(4):hover {
|
||||||
|
background: linear-gradient(135deg, #fdf2f8 0%, #fce7f3 100%);
|
||||||
|
border-color: #f9a8d4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================
|
||||||
|
表格操作区
|
||||||
|
================================ */
|
||||||
|
.table-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-bottom: var(--space-4);
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btns {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================
|
||||||
|
模态框
|
||||||
|
================================ */
|
||||||
|
.modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
padding: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border-radius: var(--radius-xl);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 480px;
|
||||||
|
max-height: 90vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
box-shadow: var(--shadow-xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: var(--space-5) var(--space-6);
|
||||||
|
border-bottom: 1px solid var(--border-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header h3 {
|
||||||
|
font-size: var(--text-lg);
|
||||||
|
font-weight: var(--font-semibold);
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
font-size: var(--text-xl);
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
transition: all var(--duration-fast) var(--ease-default);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close:hover {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
padding: var(--space-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: var(--space-3);
|
||||||
|
padding-top: var(--space-4);
|
||||||
|
margin-top: var(--space-4);
|
||||||
|
border-top: 1px solid var(--border-light);
|
||||||
|
}
|
||||||
|
|
||||||
/* ================================
|
/* ================================
|
||||||
用户区域
|
用户区域
|
||||||
================================ */
|
================================ */
|
||||||
|
|||||||
+370
-8
@@ -448,7 +448,7 @@ const HomeView = {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="stat-card" @click="$router.push('/moldinsight')">
|
<div class="stat-card" @click="$router.push('/moldinsight')">
|
||||||
<div class="stat-icon">◈</div>
|
<div class="stat-icon">⚙️</div>
|
||||||
<div class="stat-content">
|
<div class="stat-content">
|
||||||
<div class="stat-value">{{ state.stats?.health?.total_tasks || 0 }}</div>
|
<div class="stat-value">{{ state.stats?.health?.total_tasks || 0 }}</div>
|
||||||
<div class="stat-label">分析任务</div>
|
<div class="stat-label">分析任务</div>
|
||||||
@@ -500,7 +500,7 @@ const HomeView = {
|
|||||||
<h2 class="section-title">快速操作</h2>
|
<h2 class="section-title">快速操作</h2>
|
||||||
<div class="action-grid">
|
<div class="action-grid">
|
||||||
<button class="action-card" @click="$router.push('/moldinsight')">
|
<button class="action-card" @click="$router.push('/moldinsight')">
|
||||||
<span class="action-icon">◈</span>
|
<span class="action-icon">⚙️</span>
|
||||||
<span class="action-label">模具分析</span>
|
<span class="action-label">模具分析</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="action-card" @click="$router.push('/inventory')">
|
<button class="action-card" @click="$router.push('/inventory')">
|
||||||
@@ -1479,7 +1479,11 @@ const InventoryView = {
|
|||||||
warehouses: [],
|
warehouses: [],
|
||||||
inventory: [],
|
inventory: [],
|
||||||
movements: [],
|
movements: [],
|
||||||
loading: false
|
loading: false,
|
||||||
|
showModal: false,
|
||||||
|
modalType: '',
|
||||||
|
editingItem: null,
|
||||||
|
form: {}
|
||||||
});
|
});
|
||||||
|
|
||||||
const loadDashboard = async () => {
|
const loadDashboard = async () => {
|
||||||
@@ -1560,6 +1564,153 @@ const InventoryView = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openModal = (type, item = null) => {
|
||||||
|
state.modalType = type;
|
||||||
|
state.editingItem = item;
|
||||||
|
if (item) {
|
||||||
|
state.form = { ...item };
|
||||||
|
} else {
|
||||||
|
state.form = {};
|
||||||
|
}
|
||||||
|
state.showModal = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
state.showModal = false;
|
||||||
|
state.modalType = '';
|
||||||
|
state.editingItem = null;
|
||||||
|
state.form = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveProduct = async () => {
|
||||||
|
try {
|
||||||
|
if (state.editingItem) {
|
||||||
|
await apiRequest(`/api/products/${state.editingItem.id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify(state.form)
|
||||||
|
});
|
||||||
|
showNotification('产品更新成功', 'success');
|
||||||
|
} else {
|
||||||
|
await apiRequest('/api/products', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(state.form)
|
||||||
|
});
|
||||||
|
showNotification('产品创建成功', 'success');
|
||||||
|
}
|
||||||
|
closeModal();
|
||||||
|
loadProducts();
|
||||||
|
} catch (e) {
|
||||||
|
handleApiError(e, '保存产品');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteProduct = async (id) => {
|
||||||
|
if (!confirm('确定要删除这个产品吗?')) return;
|
||||||
|
try {
|
||||||
|
await apiRequest(`/api/products/${id}`, { method: 'DELETE' });
|
||||||
|
showNotification('产品已删除', 'success');
|
||||||
|
loadProducts();
|
||||||
|
} catch (e) {
|
||||||
|
handleApiError(e, '删除产品');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveSupplier = async () => {
|
||||||
|
try {
|
||||||
|
if (state.editingItem) {
|
||||||
|
await apiRequest(`/api/suppliers/${state.editingItem.id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify(state.form)
|
||||||
|
});
|
||||||
|
showNotification('供应商更新成功', 'success');
|
||||||
|
} else {
|
||||||
|
await apiRequest('/api/suppliers', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(state.form)
|
||||||
|
});
|
||||||
|
showNotification('供应商创建成功', 'success');
|
||||||
|
}
|
||||||
|
closeModal();
|
||||||
|
loadSuppliers();
|
||||||
|
} catch (e) {
|
||||||
|
handleApiError(e, '保存供应商');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteSupplier = async (id) => {
|
||||||
|
if (!confirm('确定要删除这个供应商吗?')) return;
|
||||||
|
try {
|
||||||
|
await apiRequest(`/api/suppliers/${id}`, { method: 'DELETE' });
|
||||||
|
showNotification('供应商已删除', 'success');
|
||||||
|
loadSuppliers();
|
||||||
|
} catch (e) {
|
||||||
|
handleApiError(e, '删除供应商');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveCustomer = async () => {
|
||||||
|
try {
|
||||||
|
if (state.editingItem) {
|
||||||
|
await apiRequest(`/api/customers/${state.editingItem.id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify(state.form)
|
||||||
|
});
|
||||||
|
showNotification('客户更新成功', 'success');
|
||||||
|
} else {
|
||||||
|
await apiRequest('/api/customers', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(state.form)
|
||||||
|
});
|
||||||
|
showNotification('客户创建成功', 'success');
|
||||||
|
}
|
||||||
|
closeModal();
|
||||||
|
loadCustomers();
|
||||||
|
} catch (e) {
|
||||||
|
handleApiError(e, '保存客户');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteCustomer = async (id) => {
|
||||||
|
if (!confirm('确定要删除这个客户吗?')) return;
|
||||||
|
try {
|
||||||
|
await apiRequest(`/api/customers/${id}`, { method: 'DELETE' });
|
||||||
|
showNotification('客户已删除', 'success');
|
||||||
|
loadCustomers();
|
||||||
|
} catch (e) {
|
||||||
|
handleApiError(e, '删除客户');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const stockIn = async () => {
|
||||||
|
try {
|
||||||
|
await apiRequest('/api/stock-movements', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ ...state.form, movement_type: 'in' })
|
||||||
|
});
|
||||||
|
showNotification('入库成功', 'success');
|
||||||
|
closeModal();
|
||||||
|
loadInventory();
|
||||||
|
loadMovements();
|
||||||
|
} catch (e) {
|
||||||
|
handleApiError(e, '入库操作');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const stockOut = async () => {
|
||||||
|
try {
|
||||||
|
await apiRequest('/api/stock-movements', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ ...state.form, movement_type: 'out' })
|
||||||
|
});
|
||||||
|
showNotification('出库成功', 'success');
|
||||||
|
closeModal();
|
||||||
|
loadInventory();
|
||||||
|
loadMovements();
|
||||||
|
} catch (e) {
|
||||||
|
handleApiError(e, '出库操作');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (!appState.user) {
|
if (!appState.user) {
|
||||||
router.push('/login');
|
router.push('/login');
|
||||||
@@ -1573,7 +1724,17 @@ const InventoryView = {
|
|||||||
switchTab,
|
switchTab,
|
||||||
formatNumber,
|
formatNumber,
|
||||||
formatCurrency,
|
formatCurrency,
|
||||||
formatDateTime
|
formatDateTime,
|
||||||
|
openModal,
|
||||||
|
closeModal,
|
||||||
|
saveProduct,
|
||||||
|
deleteProduct,
|
||||||
|
saveSupplier,
|
||||||
|
deleteSupplier,
|
||||||
|
saveCustomer,
|
||||||
|
deleteCustomer,
|
||||||
|
stockIn,
|
||||||
|
stockOut
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
template: `
|
template: `
|
||||||
@@ -1643,7 +1804,11 @@ const InventoryView = {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else-if="state.activeTab === 'products'" class="table-container">
|
<div v-else-if="state.activeTab === 'products'">
|
||||||
|
<div class="table-header">
|
||||||
|
<button class="btn btn-primary" @click="openModal('product')">+ 新增产品</button>
|
||||||
|
</div>
|
||||||
|
<div class="table-container">
|
||||||
<table class="data-table">
|
<table class="data-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -1653,6 +1818,7 @@ const InventoryView = {
|
|||||||
<th>单位</th>
|
<th>单位</th>
|
||||||
<th>成本价</th>
|
<th>成本价</th>
|
||||||
<th>销售价</th>
|
<th>销售价</th>
|
||||||
|
<th>操作</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -1663,12 +1829,24 @@ const InventoryView = {
|
|||||||
<td>{{ product.unit }}</td>
|
<td>{{ product.unit }}</td>
|
||||||
<td>{{ formatCurrency(product.cost_price) }}</td>
|
<td>{{ formatCurrency(product.cost_price) }}</td>
|
||||||
<td>{{ formatCurrency(product.sale_price) }}</td>
|
<td>{{ formatCurrency(product.sale_price) }}</td>
|
||||||
|
<td>
|
||||||
|
<div class="action-btns">
|
||||||
|
<button class="btn btn-sm btn-secondary" @click="openModal('product', product)">编辑</button>
|
||||||
|
<button class="btn btn-sm btn-danger" @click="deleteProduct(product.id)">删除</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-else-if="state.activeTab === 'inventory'" class="table-container">
|
<div v-else-if="state.activeTab === 'inventory'">
|
||||||
|
<div class="table-header">
|
||||||
|
<button class="btn btn-primary" @click="openModal('stockIn')">📥 入库</button>
|
||||||
|
<button class="btn btn-secondary" @click="openModal('stockOut')" style="margin-left: 8px;">📤 出库</button>
|
||||||
|
</div>
|
||||||
|
<div class="table-container">
|
||||||
<table class="data-table">
|
<table class="data-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -1690,8 +1868,13 @@ const InventoryView = {
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-else-if="state.activeTab === 'suppliers'" class="table-container">
|
<div v-else-if="state.activeTab === 'suppliers'">
|
||||||
|
<div class="table-header">
|
||||||
|
<button class="btn btn-primary" @click="openModal('supplier')">+ 新增供应商</button>
|
||||||
|
</div>
|
||||||
|
<div class="table-container">
|
||||||
<table class="data-table">
|
<table class="data-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -1700,6 +1883,7 @@ const InventoryView = {
|
|||||||
<th>联系人</th>
|
<th>联系人</th>
|
||||||
<th>电话</th>
|
<th>电话</th>
|
||||||
<th>邮箱</th>
|
<th>邮箱</th>
|
||||||
|
<th>操作</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -1709,12 +1893,23 @@ const InventoryView = {
|
|||||||
<td>{{ supplier.contact_person || '-' }}</td>
|
<td>{{ supplier.contact_person || '-' }}</td>
|
||||||
<td>{{ supplier.phone || '-' }}</td>
|
<td>{{ supplier.phone || '-' }}</td>
|
||||||
<td>{{ supplier.email || '-' }}</td>
|
<td>{{ supplier.email || '-' }}</td>
|
||||||
|
<td>
|
||||||
|
<div class="action-btns">
|
||||||
|
<button class="btn btn-sm btn-secondary" @click="openModal('supplier', supplier)">编辑</button>
|
||||||
|
<button class="btn btn-sm btn-danger" @click="deleteSupplier(supplier.id)">删除</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-else-if="state.activeTab === 'customers'" class="table-container">
|
<div v-else-if="state.activeTab === 'customers'">
|
||||||
|
<div class="table-header">
|
||||||
|
<button class="btn btn-primary" @click="openModal('customer')">+ 新增客户</button>
|
||||||
|
</div>
|
||||||
|
<div class="table-container">
|
||||||
<table class="data-table">
|
<table class="data-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -1723,6 +1918,7 @@ const InventoryView = {
|
|||||||
<th>联系人</th>
|
<th>联系人</th>
|
||||||
<th>电话</th>
|
<th>电话</th>
|
||||||
<th>邮箱</th>
|
<th>邮箱</th>
|
||||||
|
<th>操作</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -1732,10 +1928,17 @@ const InventoryView = {
|
|||||||
<td>{{ customer.contact_person || '-' }}</td>
|
<td>{{ customer.contact_person || '-' }}</td>
|
||||||
<td>{{ customer.phone || '-' }}</td>
|
<td>{{ customer.phone || '-' }}</td>
|
||||||
<td>{{ customer.email || '-' }}</td>
|
<td>{{ customer.email || '-' }}</td>
|
||||||
|
<td>
|
||||||
|
<div class="action-btns">
|
||||||
|
<button class="btn btn-sm btn-secondary" @click="openModal('customer', customer)">编辑</button>
|
||||||
|
<button class="btn btn-sm btn-danger" @click="deleteCustomer(customer.id)">删除</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-else-if="state.activeTab === 'movements'" class="table-container">
|
<div v-else-if="state.activeTab === 'movements'" class="table-container">
|
||||||
<table class="data-table">
|
<table class="data-table">
|
||||||
@@ -1766,6 +1969,165 @@ const InventoryView = {
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 模态框 -->
|
||||||
|
<div v-if="state.showModal" class="modal-overlay" @click.self="closeModal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3>{{ state.editingItem ? '编辑' : '新增' }}{{ state.modalType === 'product' ? '产品' : state.modalType === 'supplier' ? '供应商' : state.modalType === 'customer' ? '客户' : state.modalType === 'stockIn' ? '入库' : '出库' }}</h3>
|
||||||
|
<button class="modal-close" @click="closeModal">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<!-- 产品表单 -->
|
||||||
|
<form v-if="state.modalType === 'product'" @submit.prevent="saveProduct">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">SKU *</label>
|
||||||
|
<input v-model="state.form.sku" class="form-input" required placeholder="产品编码" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">名称 *</label>
|
||||||
|
<input v-model="state.form.name" class="form-input" required placeholder="产品名称" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">分类</label>
|
||||||
|
<input v-model="state.form.category" class="form-input" placeholder="产品分类" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">单位</label>
|
||||||
|
<input v-model="state.form.unit" class="form-input" placeholder="件/个/箱" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">成本价</label>
|
||||||
|
<input v-model.number="state.form.cost_price" type="number" step="0.01" class="form-input" placeholder="0.00" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">销售价</label>
|
||||||
|
<input v-model.number="state.form.sale_price" type="number" step="0.01" class="form-input" placeholder="0.00" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">最低库存</label>
|
||||||
|
<input v-model.number="state.form.min_stock" type="number" class="form-input" placeholder="0" />
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" @click="closeModal">取消</button>
|
||||||
|
<button type="submit" class="btn btn-primary">保存</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- 供应商表单 -->
|
||||||
|
<form v-else-if="state.modalType === 'supplier'" @submit.prevent="saveSupplier">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">名称 *</label>
|
||||||
|
<input v-model="state.form.name" class="form-input" required placeholder="供应商名称" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">联系人</label>
|
||||||
|
<input v-model="state.form.contact_person" class="form-input" placeholder="联系人姓名" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">电话</label>
|
||||||
|
<input v-model="state.form.phone" class="form-input" placeholder="联系电话" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">邮箱</label>
|
||||||
|
<input v-model="state.form.email" type="email" class="form-input" placeholder="email@example.com" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">地址</label>
|
||||||
|
<input v-model="state.form.address" class="form-input" placeholder="详细地址" />
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" @click="closeModal">取消</button>
|
||||||
|
<button type="submit" class="btn btn-primary">保存</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- 客户表单 -->
|
||||||
|
<form v-else-if="state.modalType === 'customer'" @submit.prevent="saveCustomer">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">名称 *</label>
|
||||||
|
<input v-model="state.form.name" class="form-input" required placeholder="客户名称" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">联系人</label>
|
||||||
|
<input v-model="state.form.contact_person" class="form-input" placeholder="联系人姓名" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">电话</label>
|
||||||
|
<input v-model="state.form.phone" class="form-input" placeholder="联系电话" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">邮箱</label>
|
||||||
|
<input v-model="state.form.email" type="email" class="form-input" placeholder="email@example.com" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">地址</label>
|
||||||
|
<input v-model="state.form.address" class="form-input" placeholder="详细地址" />
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" @click="closeModal">取消</button>
|
||||||
|
<button type="submit" class="btn btn-primary">保存</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- 入库表单 -->
|
||||||
|
<form v-else-if="state.modalType === 'stockIn'" @submit.prevent="stockIn">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">产品ID *</label>
|
||||||
|
<input v-model.number="state.form.product_id" type="number" class="form-input" required placeholder="产品ID" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">仓库ID *</label>
|
||||||
|
<input v-model.number="state.form.warehouse_id" type="number" class="form-input" required placeholder="仓库ID (默认1)" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">数量 *</label>
|
||||||
|
<input v-model.number="state.form.quantity" type="number" class="form-input" required placeholder="入库数量" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">单价</label>
|
||||||
|
<input v-model.number="state.form.unit_price" type="number" step="0.01" class="form-input" placeholder="采购单价" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">备注</label>
|
||||||
|
<input v-model="state.form.remark" class="form-input" placeholder="备注信息" />
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" @click="closeModal">取消</button>
|
||||||
|
<button type="submit" class="btn btn-primary">确认入库</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- 出库表单 -->
|
||||||
|
<form v-else-if="state.modalType === 'stockOut'" @submit.prevent="stockOut">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">产品ID *</label>
|
||||||
|
<input v-model.number="state.form.product_id" type="number" class="form-input" required placeholder="产品ID" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">仓库ID *</label>
|
||||||
|
<input v-model.number="state.form.warehouse_id" type="number" class="form-input" required placeholder="仓库ID (默认1)" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">数量 *</label>
|
||||||
|
<input v-model.number="state.form.quantity" type="number" class="form-input" required placeholder="出库数量" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">单价</label>
|
||||||
|
<input v-model.number="state.form.unit_price" type="number" step="0.01" class="form-input" placeholder="销售单价" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">备注</label>
|
||||||
|
<input v-model="state.form.remark" class="form-input" placeholder="备注信息" />
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" @click="closeModal">取消</button>
|
||||||
|
<button type="submit" class="btn btn-primary">确认出库</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user