feat(crm): align product export with ListExportDropdown
Add ids/columns support and unify toolbar export UX. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
09827fdbf3
commit
5c7210facc
5 files changed
+77
-34
No files matched your search
+23
-3
@@ -31,7 +31,12 @@ from app.utils.crm_export_columns import (
|
||||
PRODUCT_EXPORT_COLUMNS,
|
||||
SUPPLIER_INFO_EXPORT_COLUMNS,
|
||||
)
|
||||
from app.utils.excel_export import export_rows_to_xlsx_response
|
||||
from app.utils.excel_export import (
|
||||
EXPORT_MAX_ROWS,
|
||||
ExportValidationError,
|
||||
export_rows_to_xlsx_response,
|
||||
filter_export_columns,
|
||||
)
|
||||
from app.crud.crm.product import (
|
||||
_category_row_to_dict,
|
||||
create_category,
|
||||
@@ -871,6 +876,12 @@ def product_import(
|
||||
return error_response(500, str(e))
|
||||
|
||||
|
||||
def _map_product_export_column_keys(requested: List[str]) -> List[str]:
|
||||
"""前端列表 prop → 导出列 field(如 cost → costPrice)。"""
|
||||
alias = {"cost": "costPrice"}
|
||||
return [alias.get(str(key), str(key)) for key in requested]
|
||||
|
||||
|
||||
@router.post("/products/export")
|
||||
def product_export(
|
||||
body: ProductListBody,
|
||||
@@ -885,23 +896,32 @@ def product_export(
|
||||
db,
|
||||
tenant_no,
|
||||
page=1,
|
||||
page_size=10000,
|
||||
page_size=EXPORT_MAX_ROWS,
|
||||
max_page_size=EXPORT_MAX_ROWS,
|
||||
keyword=body.keyword,
|
||||
category_id=body.category_id,
|
||||
status=body.status,
|
||||
supplier_id=body.supplier_id,
|
||||
ids=body.ids,
|
||||
user_info=user_info,
|
||||
match_product_no=True if body.match_product_no is None else bool(body.match_product_no),
|
||||
match_model=bool(body.match_model) if body.match_model is not None else False,
|
||||
)
|
||||
export_cols = filter_export_columns(
|
||||
PRODUCT_EXPORT_COLUMNS,
|
||||
_map_product_export_column_keys(body.columns) if body.columns else None,
|
||||
)
|
||||
date_str = datetime.now().strftime("%Y%m%d")
|
||||
return export_rows_to_xlsx_response(
|
||||
db,
|
||||
tenant_no=tenant_no,
|
||||
sheet_title="产品列表",
|
||||
columns=PRODUCT_EXPORT_COLUMNS,
|
||||
columns=export_cols,
|
||||
rows=rows,
|
||||
filename_stem=f"产品列表_{date_str}",
|
||||
)
|
||||
except ExportValidationError as e:
|
||||
return error_response(400, str(e))
|
||||
except ValueError as e:
|
||||
return error_response(400, str(e))
|
||||
except Exception as e:
|
||||
|
||||
@@ -570,9 +570,11 @@ def list_products(
|
||||
category_id: str = None,
|
||||
status: str = None,
|
||||
supplier_id: str = None,
|
||||
ids: Optional[List[str]] = None,
|
||||
user_info=None,
|
||||
match_product_no: bool = True,
|
||||
match_model: bool = False,
|
||||
max_page_size: Optional[int] = None,
|
||||
) -> Tuple[List[Dict[str, Any]], int]:
|
||||
"""分页查询产品列表
|
||||
|
||||
@@ -586,6 +588,9 @@ def list_products(
|
||||
if user_info:
|
||||
q = apply_crm_create_by_scope(q, CrmProduct, user_info, db, tenant_no)
|
||||
|
||||
if ids:
|
||||
q = q.filter(CrmProduct.id.in_(ids))
|
||||
|
||||
kw = (keyword or "").strip()
|
||||
if kw:
|
||||
like = f"%{kw}%"
|
||||
@@ -628,7 +633,8 @@ def list_products(
|
||||
|
||||
total = q.order_by(None).with_entities(func.count(CrmProduct.id)).scalar() or 0
|
||||
page = max(1, page)
|
||||
page_size = min(max(1, page_size), 500)
|
||||
cap = max_page_size if max_page_size is not None else 500
|
||||
page_size = min(max(1, page_size), cap)
|
||||
|
||||
rows = (
|
||||
q.order_by(CrmProduct.product_no.desc())
|
||||
|
||||
@@ -88,6 +88,8 @@ class ProductListBody(BaseModel):
|
||||
alias="matchModel",
|
||||
description="keyword 是否匹配规格型号(选择器建议 true)",
|
||||
)
|
||||
ids: Optional[List[str]] = Field(default=None, description="导出时指定产品ID列表")
|
||||
columns: Optional[List[str]] = Field(default=None, description="导出列(前端 visibleColumns prop)")
|
||||
|
||||
|
||||
class ProductBrandItem(BaseModel):
|
||||
|
||||
@@ -12,6 +12,24 @@ def _yes_no(value) -> str:
|
||||
return "否"
|
||||
|
||||
|
||||
def _join_brand_names(value) -> str:
|
||||
if not value:
|
||||
return ""
|
||||
if isinstance(value, list):
|
||||
names = [str(x.get("brandName", "")) for x in value if isinstance(x, dict)]
|
||||
return "、".join(n for n in names if n)
|
||||
return str(value)
|
||||
|
||||
|
||||
def _join_alias_names(value) -> str:
|
||||
if not value:
|
||||
return ""
|
||||
if isinstance(value, list):
|
||||
names = [str(x.get("aliasName", "")) for x in value if isinstance(x, dict)]
|
||||
return "、".join(n for n in names if n)
|
||||
return str(value)
|
||||
|
||||
|
||||
CLUE_EXPORT_COLUMNS = (
|
||||
ExportColumn("线索姓名", "clueName"),
|
||||
ExportColumn("手机号", "mobile"),
|
||||
@@ -52,6 +70,8 @@ PRODUCT_EXPORT_COLUMNS = (
|
||||
ExportColumn("产品编号", "productNo"),
|
||||
ExportColumn("产品名称", "productName"),
|
||||
ExportColumn("分类名称", "categoryName"),
|
||||
ExportColumn("品牌", "brands", formatter=_join_brand_names),
|
||||
ExportColumn("别名", "aliases", formatter=_join_alias_names),
|
||||
ExportColumn("规格型号", "model"),
|
||||
ExportColumn("单位", "unit"),
|
||||
ExportColumn("标准单价", "price"),
|
||||
|
||||
@@ -101,10 +101,14 @@
|
||||
<el-icon><Upload /></el-icon>
|
||||
导入
|
||||
</el-button>
|
||||
<el-button class="btn-ghost" @click="handleExport">
|
||||
<el-icon><Download /></el-icon>
|
||||
导出
|
||||
</el-button>
|
||||
<ListExportDropdown
|
||||
:export-fn="fetchExportProducts"
|
||||
:get-filter-params="getProductExportFilterParams"
|
||||
:has-selection="true"
|
||||
:selected-ids="selectedRows.map((r) => r.id)"
|
||||
:visible-columns="productExportColumns"
|
||||
filename="产品列表.xlsx"
|
||||
/>
|
||||
<el-button class="btn-ghost" @click="categoryMaintainVisible = true">
|
||||
<el-icon><Folder /></el-icon>
|
||||
产品分类
|
||||
@@ -421,6 +425,7 @@
|
||||
allRowsDeletableByCreator,
|
||||
CREATOR_DELETE_TIP
|
||||
} from '@/utils/creator-delete'
|
||||
import ListExportDropdown from '@/components/business/list-export-dropdown/index.vue'
|
||||
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||||
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
|
||||
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
|
||||
@@ -482,6 +487,22 @@
|
||||
const { visibleColumns, checkAll, isIndeterminate, isColumnVisible, resetToDefault } =
|
||||
usePersistedListColumns('crm-product-list-columns-v2', listColumnDefs)
|
||||
|
||||
function getProductExportFilterParams() {
|
||||
const quickKeyword = quickSearchKeyword.value?.trim()
|
||||
return {
|
||||
keyword: quickKeyword || searchForm.keyword || undefined,
|
||||
categoryId: searchForm.categoryId || undefined,
|
||||
status: searchForm.status || undefined
|
||||
}
|
||||
}
|
||||
|
||||
/** 产品名称列始终展示,需并入导出 columns;cost 映射后端 costPrice */
|
||||
const productExportColumns = computed(() => {
|
||||
const cols = [...visibleColumns.value]
|
||||
if (!cols.includes('productName')) cols.unshift('productName')
|
||||
return cols
|
||||
})
|
||||
|
||||
const selectedRows = ref<any[]>([])
|
||||
const pagination = reactive({ page: 1, pageSize: 20, total: 0 })
|
||||
|
||||
@@ -781,32 +802,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function handleExport() {
|
||||
try {
|
||||
const params: Record<string, any> = {}
|
||||
const quickKeyword = quickSearchKeyword.value?.trim()
|
||||
if (quickKeyword) {
|
||||
params.keyword = quickKeyword
|
||||
} else if (searchForm.keyword) {
|
||||
params.keyword = searchForm.keyword
|
||||
}
|
||||
if (searchForm.categoryId) params.categoryId = searchForm.categoryId
|
||||
if (searchForm.status) params.status = searchForm.status
|
||||
const res = await fetchExportProducts(params)
|
||||
const blob = new Blob([res as any], {
|
||||
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
})
|
||||
const url = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = `产品列表_${new Date().getTime()}.xlsx`
|
||||
link.click()
|
||||
URL.revokeObjectURL(url)
|
||||
} catch {
|
||||
ElMessage.error('导出失败')
|
||||
}
|
||||
}
|
||||
|
||||
watchDebounced(
|
||||
searchForm,
|
||||
() => {
|
||||
|
||||
Reference in new issue
Block a user