diff --git a/backend/app/api/crm.py b/backend/app/api/crm.py
index cd54ab29b..a4cc92b76 100644
--- a/backend/app/api/crm.py
+++ b/backend/app/api/crm.py
@@ -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:
diff --git a/backend/app/crud/crm/product.py b/backend/app/crud/crm/product.py
index e019d7623..e965db174 100644
--- a/backend/app/crud/crm/product.py
+++ b/backend/app/crud/crm/product.py
@@ -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())
diff --git a/backend/app/schemas/crm/product.py b/backend/app/schemas/crm/product.py
index 8a93919a8..c2f698958 100644
--- a/backend/app/schemas/crm/product.py
+++ b/backend/app/schemas/crm/product.py
@@ -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):
diff --git a/backend/app/utils/crm_export_columns.py b/backend/app/utils/crm_export_columns.py
index 863f34b1e..34d35f753 100644
--- a/backend/app/utils/crm_export_columns.py
+++ b/backend/app/utils/crm_export_columns.py
@@ -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"),
diff --git a/web-admin/src/views/crm/product/index.vue b/web-admin/src/views/crm/product/index.vue
index 80187882b..a8caccbb4 100644
--- a/web-admin/src/views/crm/product/index.vue
+++ b/web-admin/src/views/crm/product/index.vue
@@ -101,10 +101,14 @@
导入
-
-
- 导出
-
+
产品分类
@@ -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([])
const pagination = reactive({ page: 1, pageSize: 20, total: 0 })
@@ -781,32 +802,6 @@
}
}
- async function handleExport() {
- try {
- const params: Record = {}
- 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,
() => {