feat(system): add Excel export for dept and menu tree lists
Export flattened filtered or selected rows via backend xlsx and ListExportDropdown. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
b3b6ec187a
commit
ee56dad875
4 files changed
+170
-1
No files matched your search
@@ -987,6 +987,52 @@ def list_depts(
|
||||
return success_response(tree)
|
||||
|
||||
|
||||
|
||||
DEPT_EXPORT_COLUMNS = [
|
||||
ExportColumn(title="组织名称", field="deptName"),
|
||||
ExportColumn(title="组织类别", field="deptCategoryCode"),
|
||||
ExportColumn(title="状态", field="status", dict_type="sys_status"),
|
||||
ExportColumn(title="负责人", field="leader"),
|
||||
ExportColumn(title="联系电话", field="leaderPhone"),
|
||||
ExportColumn(title="创建时间", field="createTime"),
|
||||
]
|
||||
|
||||
|
||||
class DeptExportBody(BaseModel):
|
||||
keyword: Optional[str] = None
|
||||
ids: Optional[List[str]] = None
|
||||
columns: Optional[List[str]] = None
|
||||
|
||||
|
||||
@router.post("/depts/export")
|
||||
def export_depts(
|
||||
body: DeptExportBody,
|
||||
user: dict = Depends(require_menu_code("system:dept:list")),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""导出组织列表(扁平行;当前筛选或已选)。"""
|
||||
try:
|
||||
tenant_no = _get_tenant_no(db, user)
|
||||
rows = dept_svc.list_depts(db, keyword=body.keyword, tenant_no=tenant_no, user_info=user)
|
||||
if body.ids:
|
||||
idset = set(body.ids)
|
||||
rows = [r for r in rows if r.get("id") in idset]
|
||||
cols = filter_export_columns(DEPT_EXPORT_COLUMNS, body.columns)
|
||||
date_str = datetime.now().strftime("%Y%m%d")
|
||||
return export_rows_to_xlsx_response(
|
||||
db,
|
||||
tenant_no=tenant_no,
|
||||
sheet_title="组织列表",
|
||||
columns=cols,
|
||||
rows=rows,
|
||||
filename_stem=f"组织列表_{date_str}",
|
||||
)
|
||||
except ExportValidationError as e:
|
||||
return error_response(400, str(e))
|
||||
except Exception as e:
|
||||
return error_response(500, str(e))
|
||||
|
||||
|
||||
@router.post("/depts")
|
||||
def create_dept(
|
||||
body: DeptCreateBody,
|
||||
@@ -1462,6 +1508,66 @@ def list_menus(
|
||||
return success_response(tree)
|
||||
|
||||
|
||||
|
||||
MENU_EXPORT_COLUMNS = [
|
||||
ExportColumn(title="菜单名称", field="menuName"),
|
||||
ExportColumn(title="菜单类型", field="menuType"),
|
||||
ExportColumn(title="路由", field="path"),
|
||||
ExportColumn(title="组件", field="component"),
|
||||
ExportColumn(title="权限标识", field="perms"),
|
||||
ExportColumn(title="状态", field="status", dict_type="sys_status"),
|
||||
ExportColumn(title="可见", field="visible"),
|
||||
ExportColumn(title="创建时间", field="createTime"),
|
||||
]
|
||||
|
||||
|
||||
class MenuExportBody(BaseModel):
|
||||
keyword: Optional[str] = None
|
||||
ids: Optional[List[str]] = None
|
||||
columns: Optional[List[str]] = None
|
||||
|
||||
|
||||
@router.post("/menus/export")
|
||||
def export_menus(
|
||||
body: MenuExportBody,
|
||||
user: dict = Depends(require_admin_or_super_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""导出菜单列表(扁平行;当前筛选或已选)。"""
|
||||
try:
|
||||
tenant_no = _get_tenant_no(db, user)
|
||||
# list flat via tree then flatten, or use underlying list if exists
|
||||
from app.crud import menu as menu_mod
|
||||
if hasattr(menu_mod, 'list_menus'):
|
||||
rows = menu_mod.list_menus(db, keyword=body.keyword, tenant_no=tenant_no, user_info=user)
|
||||
else:
|
||||
tree = menu_svc.list_menus_tree(db, keyword=body.keyword, tenant_no=tenant_no, user_info=user)
|
||||
rows = []
|
||||
def walk(nodes):
|
||||
for n in nodes or []:
|
||||
item = {k: v for k, v in n.items() if k != 'children'}
|
||||
rows.append(item)
|
||||
walk(n.get('children') or [])
|
||||
walk(tree)
|
||||
if body.ids:
|
||||
idset = set(body.ids)
|
||||
rows = [r for r in rows if r.get("id") in idset]
|
||||
cols = filter_export_columns(MENU_EXPORT_COLUMNS, body.columns)
|
||||
date_str = datetime.now().strftime("%Y%m%d")
|
||||
return export_rows_to_xlsx_response(
|
||||
db,
|
||||
tenant_no=tenant_no,
|
||||
sheet_title="菜单列表",
|
||||
columns=cols,
|
||||
rows=rows,
|
||||
filename_stem=f"菜单列表_{date_str}",
|
||||
)
|
||||
except ExportValidationError as e:
|
||||
return error_response(400, str(e))
|
||||
except Exception as e:
|
||||
return error_response(500, str(e))
|
||||
|
||||
|
||||
@router.post("/menus")
|
||||
def create_menu(
|
||||
body: MenuCreateBody,
|
||||
|
||||
@@ -346,6 +346,14 @@ export function fetchDeptTree(params?: { keyword?: string }) {
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchExportDepts(data: Record<string, any>) {
|
||||
return request.post({
|
||||
url: '/web/admin/depts/export',
|
||||
data,
|
||||
responseType: 'blob'
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchCreateDept(data: {
|
||||
parentId?: string
|
||||
deptName: string
|
||||
@@ -505,6 +513,14 @@ export function fetchMenuTree(params?: { keyword?: string }) {
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchExportMenus(data: Record<string, any>) {
|
||||
return request.post({
|
||||
url: '/web/admin/menus/export',
|
||||
data,
|
||||
responseType: 'blob'
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchCreateMenu(data: {
|
||||
menuName: string
|
||||
parentId?: string
|
||||
|
||||
@@ -66,6 +66,15 @@
|
||||
{{ isExpandAll ? '全部折叠' : '全部展开' }}
|
||||
</el-button>
|
||||
|
||||
<ListExportDropdown
|
||||
:export-fn="fetchExportDepts"
|
||||
:get-filter-params="getDeptExportFilterParams"
|
||||
:has-selection="true"
|
||||
:selected-ids="selectedRows.map((r) => r.id)"
|
||||
:visible-columns="deptExportColumns"
|
||||
filename="组织列表.xlsx"
|
||||
/>
|
||||
|
||||
<ListToolbarEndActions
|
||||
:loading="loading"
|
||||
:column-defs="listColumnDefs"
|
||||
@@ -304,6 +313,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||||
import ListExportDropdown from '@/components/business/list-export-dropdown/index.vue'
|
||||
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
|
||||
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
|
||||
import { ref, reactive, computed, watch, onMounted, nextTick } from 'vue'
|
||||
@@ -323,6 +333,7 @@ import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
|
||||
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
|
||||
import {
|
||||
fetchDeptTree,
|
||||
fetchExportDepts,
|
||||
fetchCreateDept,
|
||||
fetchUpdateDept,
|
||||
fetchDeleteDept,
|
||||
@@ -356,6 +367,19 @@ const dictStore = useDictStore()
|
||||
|
||||
const searchForm = reactive<{ keyword: string }>({ keyword: '' })
|
||||
const quickSearchKeyword = ref('')
|
||||
|
||||
function getDeptExportFilterParams() {
|
||||
const quickKeyword = quickSearchKeyword.value?.trim()
|
||||
return {
|
||||
keyword: quickKeyword || searchForm.keyword || undefined
|
||||
}
|
||||
}
|
||||
|
||||
const deptExportColumns = computed(() => {
|
||||
const cols = [...visibleColumns.value]
|
||||
if (!cols.includes('deptName')) cols.unshift('deptName')
|
||||
return cols
|
||||
})
|
||||
const tableData = ref<Api.Dept.DeptItem[]>([])
|
||||
/** 弹窗上级组织:全量树(不受列表关键词筛选影响) */
|
||||
const deptTreeForSelect = ref<Api.Dept.DeptItem[]>([])
|
||||
|
||||
@@ -65,6 +65,15 @@
|
||||
{{ isExpandAll ? '全部折叠' : '全部展开' }}
|
||||
</el-button>
|
||||
|
||||
<ListExportDropdown
|
||||
:export-fn="fetchExportMenus"
|
||||
:get-filter-params="getMenuExportFilterParams"
|
||||
:has-selection="true"
|
||||
:selected-ids="selectedRows.map((r) => r.id)"
|
||||
:visible-columns="menuExportColumns"
|
||||
filename="菜单列表.xlsx"
|
||||
/>
|
||||
|
||||
<ListToolbarEndActions
|
||||
:loading="loading"
|
||||
:column-defs="listColumnDefs"
|
||||
@@ -314,6 +323,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||||
import ListExportDropdown from '@/components/business/list-export-dropdown/index.vue'
|
||||
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
|
||||
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
|
||||
import { ref, reactive, computed, onMounted, nextTick } from 'vue'
|
||||
@@ -331,7 +341,7 @@ import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
|
||||
Search
|
||||
} from '@element-plus/icons-vue'
|
||||
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
|
||||
import { fetchMenuTree, fetchCreateMenu, fetchUpdateMenu, fetchDeleteMenu } from '@/api/admin'
|
||||
import { fetchMenuTree, fetchExportMenus, fetchCreateMenu, fetchUpdateMenu, fetchDeleteMenu } from '@/api/admin'
|
||||
import { useDictStore } from '@/store/modules/dict'
|
||||
|
||||
defineOptions({ name: 'SystemMenu' })
|
||||
@@ -363,6 +373,19 @@ import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
|
||||
const isExpandAll = ref(true)
|
||||
const tableRef = ref()
|
||||
|
||||
function getMenuExportFilterParams() {
|
||||
const quickKeyword = quickSearchKeyword.value?.trim()
|
||||
return {
|
||||
keyword: quickKeyword || searchForm.keyword || undefined
|
||||
}
|
||||
}
|
||||
|
||||
const menuExportColumns = computed(() => {
|
||||
const cols = [...visibleColumns.value]
|
||||
if (!cols.includes('menuName')) cols.unshift('menuName')
|
||||
return cols
|
||||
})
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('新增菜单')
|
||||
const isEdit = ref(false)
|
||||
|
||||
Reference in new issue
Block a user