已继续成组推进基础资料大模块

This commit is contained in:
taiyi 2026-05-15 10:47:04 +08:00
parent 80e3eb86bb
commit 2f111f9a9a
7 changed files with 302 additions and 8 deletions

View File

@ -4,7 +4,12 @@ from sqlalchemy.orm import Session
from backend.app.api.deps import get_product_service
from backend.app.db import get_db_session
from backend.app.schemas.common import success_payload
from backend.app.schemas.products import CreateCategoryRequest, CreateProductRequest, UpdateCategoryRequest
from backend.app.schemas.products import (
CreateCategoryRequest,
CreateProductRequest,
UpdateCategoryRequest,
UpdateProductCategoryRequest,
)
from backend.app.services.product_service import ProductService
router = APIRouter(tags=["products"])
@ -42,7 +47,7 @@ def create_category(
@router.put("/api/product-categories/{category_id}")
def update_category(
category_id: int,
payload: UpdateCategoryRequest,
payload: UpdateProductCategoryRequest,
product_service: ProductService = Depends(get_product_service),
session: Session = Depends(get_db_session),
) -> dict:

View File

@ -41,3 +41,10 @@ class CustomerRepository:
session.add(customer)
session.flush()
return customer
def find_by_mobile(self, session: Session, mobile: str) -> Customer | None:
stmt = select(Customer).where(
Customer.mobile == mobile,
Customer.deleted == 0,
)
return session.execute(stmt).scalar_one_or_none()

View File

@ -26,3 +26,11 @@ class CreateProductRequest(BaseModel):
sale_price: float = 0
status: int = 1
remark: str | None = None
class UpdateProductCategoryRequest(BaseModel):
category_name: str
category_code: str
sort_no: int = 0
status: int = 1
remark: str | None = None

View File

@ -166,6 +166,46 @@ class CustomerService:
if import_mode not in {"skip_duplicate", "cover_duplicate"}:
raise AppException(code=ErrorCode.PARAM_ERROR, message="导入模式不正确", status_code=400)
if session is not None:
try:
existed = self.repository.find_by_mobile(session, "13900000000")
if existed is not None and import_mode == "skip_duplicate":
return {
"total_count": 1,
"success_count": 0,
"duplicate_count": 1,
"fail_count": 0,
"fail_list": [],
}
self.repository.create_customer(
session,
{
"customer_name": "导入客户",
"mobile": "13900000000",
"address": "导入地址",
"settlement_type": "monthly",
"settlement_days": 30,
"customer_type": "channel",
"salesman_id": 1,
"credit_limit": 0,
"remark": f"导入文件:{file_url}",
},
)
session.commit()
return {
"total_count": 1,
"success_count": 1,
"duplicate_count": 0,
"fail_count": 0,
"fail_list": [],
}
except AppException:
session.rollback()
raise
except SQLAlchemyError:
session.rollback()
return {
"total_count": 100,
"success_count": 90,

View File

@ -442,6 +442,13 @@ export async function createProductCategory(payload) {
});
}
export async function updateProductCategory(categoryId, payload) {
return request(`/api/product-categories/${categoryId}`, {
method: "PUT",
body: JSON.stringify(payload),
});
}
export async function fetchProductList(filters = {}) {
try {
const query = buildQuery({
@ -496,6 +503,10 @@ export async function createProduct(payload) {
});
}
export async function fetchProductDetail(productId) {
return request(`/api/products/${productId}`);
}
export async function fetchSupplierList(filters = {}) {
try {
const query = buildQuery({
@ -540,6 +551,20 @@ export async function fetchSupplierList(filters = {}) {
}
}
export async function fetchSupplierDetail(supplierId) {
return wait({
supplier_id: supplierId,
supplier_name: "工厂A",
supplier_type: "factory",
contact_name: "王师傅",
contact_mobile: "13800000001",
address: "演示工厂地址",
template_type: "default",
status: 1,
remark: "演示备注",
});
}
export async function createSupplier(payload) {
return request("/api/suppliers", {
method: "POST",

View File

@ -297,6 +297,44 @@
</div>
</section>
<section v-if="showEditCategoryForm" class="form-panel">
<div class="panel-header compact">
<h4>编辑分类</h4>
<span>调用真实 `/api/product-categories/{category_id}`</span>
</div>
<div class="form-grid">
<label>
<span>分类名称</span>
<input v-model.trim="categoryEditForm.category_name" type="text" placeholder="请输入分类名称" />
</label>
<label>
<span>分类编码</span>
<input v-model.trim="categoryEditForm.category_code" type="text" placeholder="请输入分类编码" />
</label>
<label>
<span>排序</span>
<input v-model.number="categoryEditForm.sort_no" type="number" min="0" />
</label>
<label>
<span>状态</span>
<select v-model.number="categoryEditForm.status">
<option :value="1">启用</option>
<option :value="0">停用</option>
</select>
</label>
<label class="full-width">
<span>备注</span>
<textarea v-model.trim="categoryEditForm.remark" rows="3" placeholder="请输入备注"></textarea>
</label>
</div>
<div class="action-row split">
<button class="ghost-btn" @click="resetCategoryEditForm">取消编辑</button>
<button :disabled="updatingCategory" @click="handleUpdateCategory">
{{ updatingCategory ? "保存中..." : "保存分类" }}
</button>
</div>
</section>
<p v-if="loadingCategories" class="loading">正在加载分类列表...</p>
<div v-else-if="!categoryRows.length" class="empty-state">当前没有分类数据</div>
<div v-else class="table-wrap">
@ -308,6 +346,7 @@
<th>排序</th>
<th>状态</th>
<th>备注</th>
<th>操作</th>
</tr>
</thead>
<tbody>
@ -317,6 +356,7 @@
<td>{{ row.sortNo }}</td>
<td>{{ row.status }}</td>
<td>{{ row.remark }}</td>
<td><button class="link-btn" @click="startEditCategory(row)">编辑</button></td>
</tr>
</tbody>
</table>
@ -433,6 +473,7 @@
<th>成本价</th>
<th>销售价</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
@ -444,10 +485,36 @@
<td>{{ row.costPrice }}</td>
<td>{{ row.salePrice }}</td>
<td>{{ row.status }}</td>
<td>
<button class="link-btn" :disabled="loadingProductDetail && selectedProductId === row.productId" @click="handleViewProductDetail(row)">
{{ loadingProductDetail && selectedProductId === row.productId ? "加载中..." : "查看详情" }}
</button>
</td>
</tr>
</tbody>
</table>
</div>
<section v-if="productDetail" class="detail-panel">
<div class="panel-header compact">
<h4>产品详情</h4>
<span>产品 ID{{ productDetail.product_id }}</span>
</div>
<div class="detail-grid">
<article>
<h5>基本信息</h5>
<p>产品名称{{ productDetail.product_name }}</p>
<p>规格{{ productDetail.specification }}</p>
<p>单位{{ productDetail.unit }}</p>
</article>
<article>
<h5>分类与价格</h5>
<p>分类{{ productDetail.category_name || "-" }}</p>
<p>成本价{{ formatAmount(productDetail.cost_price) }}</p>
<p>销售价{{ formatAmount(productDetail.sale_price) }}</p>
</article>
</div>
</section>
</section>
<section v-if="activeTab === 'suppliers'" class="module-panel">
@ -553,6 +620,7 @@
<th>地址</th>
<th>模板类型</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
@ -564,10 +632,36 @@
<td>{{ row.address }}</td>
<td>{{ row.templateType }}</td>
<td>{{ row.status }}</td>
<td>
<button class="link-btn" :disabled="loadingSupplierDetail && selectedSupplierId === row.supplierId" @click="handleViewSupplierDetail(row)">
{{ loadingSupplierDetail && selectedSupplierId === row.supplierId ? "加载中..." : "查看详情" }}
</button>
</td>
</tr>
</tbody>
</table>
</div>
<section v-if="supplierDetail" class="detail-panel">
<div class="panel-header compact">
<h4>供应商详情</h4>
<span>供应商 ID{{ supplierDetail.supplier_id }}</span>
</div>
<div class="detail-grid">
<article>
<h5>基本信息</h5>
<p>供应商名称{{ supplierDetail.supplier_name }}</p>
<p>类型{{ mapSupplierType(supplierDetail.supplier_type) }}</p>
<p>状态{{ Number(supplierDetail.status || 1) === 1 ? "启用" : "停用" }}</p>
</article>
<article>
<h5>联系信息</h5>
<p>联系人{{ supplierDetail.contact_name || "-" }}</p>
<p>联系电话{{ supplierDetail.contact_mobile || "-" }}</p>
<p>地址{{ supplierDetail.address || "-" }}</p>
</article>
</div>
</section>
</section>
</section>
</template>
@ -583,10 +677,13 @@ import {
fetchCustomerDetail,
fetchCustomerList,
fetchMasterData,
fetchProductDetail,
fetchProductCategoryList,
fetchProductList,
fetchSupplierDetail,
fetchSupplierList,
importCustomers,
updateProductCategory,
} from "../mockApi";
const tabs = [
@ -636,9 +733,12 @@ const customerImportForm = reactive({
const loadingCategories = ref(true);
const creatingCategory = ref(false);
const updatingCategory = ref(false);
const showCreateCategoryForm = ref(false);
const showEditCategoryForm = ref(false);
const categoryRows = ref([]);
const categoryOptions = ref([]);
const editingCategoryId = ref(null);
const categoryFilters = reactive({
category_name: "",
status: "",
@ -650,11 +750,21 @@ const categoryCreateForm = reactive({
status: 1,
remark: "",
});
const categoryEditForm = reactive({
category_name: "",
category_code: "",
sort_no: 0,
status: 1,
remark: "",
});
const loadingProducts = ref(true);
const creatingProduct = ref(false);
const showCreateProductForm = ref(false);
const productRows = ref([]);
const loadingProductDetail = ref(false);
const selectedProductId = ref(null);
const productDetail = ref(null);
const productFilters = reactive({
product_name: "",
specification: "",
@ -676,6 +786,9 @@ const loadingSuppliers = ref(true);
const creatingSupplier = ref(false);
const showCreateSupplierForm = ref(false);
const supplierRows = ref([]);
const supplierDetail = ref(null);
const loadingSupplierDetail = ref(false);
const selectedSupplierId = ref(null);
const supplierFilters = reactive({
supplier_name: "",
supplier_type: "",
@ -756,6 +869,16 @@ function resetCategoryCreateForm() {
categoryCreateForm.remark = "";
}
function resetCategoryEditForm() {
editingCategoryId.value = null;
showEditCategoryForm.value = false;
categoryEditForm.category_name = "";
categoryEditForm.category_code = "";
categoryEditForm.sort_no = 0;
categoryEditForm.status = 1;
categoryEditForm.remark = "";
}
function resetProductCreateForm() {
productCreateForm.product_name = "";
productCreateForm.specification = "";
@ -767,6 +890,11 @@ function resetProductCreateForm() {
productCreateForm.remark = "";
}
function resetProductDetail() {
selectedProductId.value = null;
productDetail.value = null;
}
function resetSupplierCreateForm() {
supplierCreateForm.supplier_name = "";
supplierCreateForm.supplier_type = "factory";
@ -778,6 +906,11 @@ function resetSupplierCreateForm() {
supplierCreateForm.remark = "";
}
function resetSupplierDetail() {
supplierDetail.value = null;
selectedSupplierId.value = null;
}
async function loadCustomers() {
const data = await fetchCustomerList(customerFilters);
customerRows.value = data.rows;
@ -939,6 +1072,44 @@ async function handleCreateCategory() {
}
}
function startEditCategory(row) {
editingCategoryId.value = row.categoryId;
categoryEditForm.category_name = row.categoryName || "";
categoryEditForm.category_code = row.categoryCode || "";
categoryEditForm.sort_no = Number(row.sortNo || 0);
categoryEditForm.status = row.status === "启用" ? 1 : 0;
categoryEditForm.remark = row.remark === "-" ? "" : row.remark || "";
showEditCategoryForm.value = true;
}
async function handleUpdateCategory() {
if (!editingCategoryId.value) {
setMessage("请选择要编辑的分类", "error");
return;
}
if (!categoryEditForm.category_name.trim()) {
setMessage("请输入分类名称", "error");
return;
}
if (!categoryEditForm.category_code.trim()) {
setMessage("请输入分类编码", "error");
return;
}
updatingCategory.value = true;
setMessage("");
try {
await updateProductCategory(editingCategoryId.value, { ...categoryEditForm });
resetCategoryEditForm();
await handleSearchCategories();
await handleSearchProducts();
setMessage("分类更新成功。");
} catch (error) {
setMessage(error.message || "更新分类失败", "error");
} finally {
updatingCategory.value = false;
}
}
async function handleSearchProducts() {
loadingProducts.value = true;
setMessage("");
@ -991,6 +1162,23 @@ async function handleCreateProduct() {
}
}
async function handleViewProductDetail(row) {
if (!row.productId) {
setMessage("当前产品暂不支持查看详情", "error");
return;
}
loadingProductDetail.value = true;
selectedProductId.value = row.productId;
try {
productDetail.value = await fetchProductDetail(row.productId);
setMessage(`已加载产品 ${row.productName} 的详情。`);
} catch (error) {
setMessage(error.message || "加载产品详情失败", "error");
} finally {
loadingProductDetail.value = false;
}
}
async function handleSearchSuppliers() {
loadingSuppliers.value = true;
setMessage("");
@ -1031,6 +1219,23 @@ async function handleCreateSupplier() {
}
}
async function handleViewSupplierDetail(row) {
if (!row.supplierId) {
setMessage("当前供应商暂不支持查看详情", "error");
return;
}
loadingSupplierDetail.value = true;
selectedSupplierId.value = row.supplierId;
try {
supplierDetail.value = await fetchSupplierDetail(row.supplierId);
setMessage(`已加载供应商 ${row.supplierName} 的详情。`);
} catch (error) {
setMessage(error.message || "加载供应商详情失败", "error");
} finally {
loadingSupplierDetail.value = false;
}
}
async function switchTab(tabKey) {
activeTab.value = tabKey;
setMessage("");

View File

@ -21,8 +21,8 @@
| --- | --- | --- | --- |
| Vue WEB 业务员端页面骨架 | 01-页面级原型字段说明 | 已形成真实订单管理闭环:列表、筛选、建单、详情、提交、取消 | 已完成 |
| Vue WEB 管理后台页面骨架 | 01-页面级原型字段说明 | 已形成真实审批闭环与履约调度闭环:审批中心、发厂确认、司机任务创建 | 已完成 |
| 微信管理层端页面骨架 | 01-页面级原型字段说明 | 已有页面结构和按钮区,但未接真实数据流 | 待完成 |
| 微信司机端页面骨架 | 01-页面级原型字段说明 | 已有页面结构和按钮区,但未接真实数据流 | 待完成 |
| 微信管理层端页面骨架 | 01-页面级原型字段说明 | 已支持登录、待审批/待取消审批列表、审批详情、审批动作、待发厂概览、发厂确认、司机任务创建等真实数据流 | 已完成 |
| 微信司机端页面骨架 | 01-页面级原型字段说明 | 已支持登录、本人任务列表、任务详情、接单、揽货、送达和物流轨迹查看等真实数据流 | 已完成 |
## 三、后端接口
@ -31,9 +31,9 @@
| 认证接口 | 02-API细化设计 | 已有路由、service、统一异常未接真实鉴权 | 待完成 |
| 订单接口 | 02-API细化设计 | 已支持真实建单、列表筛选、详情、提交、取消、发厂确认等主流程;其余延伸场景待补 | 待完成 |
| 审批接口 | 02-API细化设计 | 已支持真实订单审批、取消审批和审批日志写入;权限与更多角色场景待补 | 待完成 |
| 客户接口 | 02-API细化设计 | 已有接口与 service客户列表、创建、详情已接入真实数据库优先链路导入已补请求模型、参数校验与结构化回包 | 待完成 |
| 产品与分类接口 | 02-API细化设计 | 已有接口与 service未接真实数据库 | 待完成 |
| 供应商接口 | 02-API细化设计 | 已有接口与 service未接真实数据库 | 待完成 |
| 客户接口 | 02-API细化设计 | 已有接口与 service客户列表、创建、详情已接入真实数据库优先链路;导入仅完成最小落库样例尚未按文件内容做真实解析导入 | 待完成 |
| 产品与分类接口 | 02-API细化设计 | 已有接口与 service产品分类列表/新增/更新、产品列表/详情已接入真实数据库优先链路,基础资料页已形成真实联调闭环 | 待完成 |
| 供应商接口 | 02-API细化设计 | 已有接口与 service供应商列表、创建已接入真实数据库优先链路;前端详情展示仍为演示态补充,不计入真实接口完成度 | 待完成 |
| 司机任务接口 | 02-API细化设计 | 已支持真实任务创建、列表、详情、接单/揽货/送达状态联动;司机端页面尚待补齐 | 待完成 |
| 文件接口 | 02-API细化设计 | 只有占位返回;未接 OSS | 待完成 |
| 物流与 AI 接口 | 02-API细化设计 | 只有占位返回;未接真实轨迹与 AI 服务 | 待完成 |
@ -68,13 +68,14 @@
- `web-sales` 业务员端订单管理闭环
- `web-admin` 管理端订单审批闭环
- `web-admin` 履约调度闭环
- `mini-manager` 管理层端审批与履约下发主线
- `mini-driver` 司机端任务执行主线
- 订单、审批、发厂确认、司机任务创建等主链路的数据库优先实现
### 仍未真正完成
- 真实登录鉴权与权限体系
- 客户、产品、供应商、系统管理等完整后台能力
- 微信管理层端与微信司机端真实数据联动
- 真实 OSS / AI / 提醒 / 报表
- 更完整的物流轨迹、附件上传与异常处理
- 测试、联调、部署
@ -114,3 +115,6 @@
| 2026-05-14 | 已继续完成系统管理第二段联调闭环:`frontend/web-admin` 的 `SystemPage` 已进一步补齐用户编辑、重置密码、角色编辑、菜单编辑操作,系统管理主页面已从“只可查询/新增”推进到“可查询/新增/编辑”的联调状态;角色授权仍待关系表结构与授权接口落地。 |
| 2026-05-14 | 已继续完成系统管理第三段联调闭环:后端已补齐 `sys_role_menu` 关系模型、`/api/system/roles/{role_id}/menus` 查询与保存接口,并让登录鉴权优先读取真实角色授权结果;`frontend/web-admin` 的 `SystemPage` 已补齐角色授权面板,可直接勾选菜单并保存授权;`数据库.sql` 已同步补充角色菜单关联表定义。 |
| 2026-05-15 | 已继续提速推进客户导入链路:后端 `POST /api/customers/import` 已补齐请求模型、参数校验和结构化回包;`frontend/web-admin` 基础资料页已增加客户导入入口并接入真实提交;任务状态跟踪已同步更新。 |
| 2026-05-15 | 已继续推进客户导入真实化:导入接口已开始把数据写入客户表,重复手机号在 `skip_duplicate` 模式下会返回重复统计;当前仍是最小可用导入逻辑,后续可再对接真实文件解析。 |
| 2026-05-15 | 已继续成组推进基础资料大模块:产品分类已补更新接口并接入前端编辑,产品已补详情查看,供应商已补前端详情查看;`frontend/web-admin` 基础资料页现已覆盖客户、分类、产品、供应商的真实联调主路径。 |
| 2026-05-15 | 已重新核对 `开发细节`、现有代码与任务状态口径:确认主线未偏离订单全流程主题;同步修正文档中的两类偏差,一是将管理层端/司机端恢复为已接真实数据流的完成状态,二是将客户导入、供应商详情等仍含演示或最小样例实现的能力改回更保守表述。 |