feat(导出): 支持资产父子关系快照

This commit is contained in:
WuYongJie 2026-07-10 13:37:40 +08:00
parent 94a5e8a639
commit 879d74f668
11 changed files with 725 additions and 35 deletions

View File

@ -1,15 +1,21 @@
import { Request, Response } from 'express'
import { Prisma } from '@prisma/client'
import prisma from '../utils/prisma'
import { sumSnapshotChildrenValue } from '../utils/assetValue'
import { flattenEquipmentExportHierarchy } from '../utils/equipmentExportHierarchy'
import {
getEquipmentDisplayAvailableValue,
getEquipmentDisplayBorrowedValue,
getEquipmentDisplayTotalValue,
getEquipmentMountedChildrenValue,
sumSnapshotChildrenValue
} from '../utils/assetValue'
buildEquipmentExportDescriptionRows,
formatShanghaiExportFilenameTimestamp
} from '../utils/equipmentExportMetadata'
import { calculateEquipmentExportValues } from '../utils/equipmentExportValues'
import { buildEquipmentKeywordSearchConditions } from '../utils/equipmentKeywordSearch'
import { buildSyncedBusinessDepartmentWhere } from '../utils/organization'
// eslint-disable-next-line @typescript-eslint/no-var-requires
const ExcelJS = require('exceljs')
const loadExcelJS = (): typeof import('exceljs') => {
// 低频导出按需加载,避免后台常驻流程在启动时就加载 Excel 组件。
// eslint-disable-next-line @typescript-eslint/no-var-requires
return require('exceljs')
}
// 生成带下拉选项的导入模板
export const generateImportTemplate = async (req: Request, res: Response) => {
@ -39,6 +45,7 @@ export const generateImportTemplate = async (req: Request, res: Response) => {
const assetTypeNames = assetTypes.map(t => t.name)
const locationNames = storageLocations.map(l => l.name)
const ExcelJS = loadExcelJS()
const workbook = new ExcelJS.Workbook()
// 创建主数据表
@ -194,34 +201,85 @@ export const generateImportTemplate = async (req: Request, res: Response) => {
// 导出资产列表
export const exportEquipment = async (req: Request, res: Response) => {
const exportedAt = new Date()
try {
const { status, equipment_type, department_id, keyword } = req.query
const {
status,
equipment_type,
department_id,
storage_location_id,
mount_status,
keyword
} = req.query
const where: any = {}
const where: any = { parent_id: null }
if (status) where.status = status
if (equipment_type) where.equipment_type = equipment_type
if (department_id) where.department_id = Number(department_id)
const departmentId = Number(department_id)
if (department_id && !Number.isNaN(departmentId)) where.department_id = departmentId
const storageLocationId = Number(storage_location_id)
if (storage_location_id && !Number.isNaN(storageLocationId)) {
where.storage_location_id = storageLocationId
}
if (mount_status === 'mounted') {
where.children = { some: {} }
} else if (mount_status === 'unmounted') {
where.children = { none: {} }
}
if (keyword) {
where.OR = [
{ equipment_code: { contains: String(keyword) } },
{ equipment_name: { contains: String(keyword) } }
]
const normalizedKeyword = String(keyword).trim()
where.OR = buildEquipmentKeywordSearchConditions(normalizedKeyword, 2)
}
const equipments = await prisma.equipment.findMany({
where,
include: {
department: { select: { name: true } },
storageLocation: { select: { name: true } },
assetType: { select: { name: true } }
},
orderBy: { created_at: 'desc' }
})
const equipments = await prisma.$transaction(
async (tx) =>
tx.equipment.findMany({
where,
include: {
department: { select: { name: true } },
storageLocation: { select: { name: true } },
assetType: { select: { name: true } },
children: {
include: {
department: { select: { name: true } },
storageLocation: { select: { name: true } },
assetType: { select: { name: true } },
children: {
include: {
department: { select: { name: true } },
storageLocation: { select: { name: true } },
assetType: { select: { name: true } }
},
orderBy: [{ equipment_code: 'asc' }, { id: 'asc' }]
}
},
orderBy: [{ equipment_code: 'asc' }, { id: 'asc' }]
}
},
orderBy: [{ created_at: 'desc' }, { id: 'desc' }]
}),
{
isolationLevel: Prisma.TransactionIsolationLevel.RepeatableRead,
maxWait: 5000,
timeout: 30000
}
)
const exportRows = flattenEquipmentExportHierarchy(equipments)
const exportValues = calculateEquipmentExportValues(exportRows)
const ExcelJS = loadExcelJS()
const workbook = new ExcelJS.Workbook()
workbook.created = exportedAt
const worksheet = workbook.addWorksheet('资产列表')
worksheet.properties.outlineProperties = { summaryBelow: false, summaryRight: false }
worksheet.columns = [
{ header: '关系组编码', key: 'relationship_group_code', width: 20 },
{ header: '资产层级', key: 'asset_level', width: 12 },
{ header: '所属主资产编码', key: 'parent_equipment_code', width: 20 },
{ header: '所属主资产名称', key: 'parent_equipment_name', width: 25 },
{ header: '配件序号', key: 'accessory_sequence', width: 10 },
{ header: '关系状态', key: 'relationship_status', width: 18 },
{ header: '资产编码', key: 'equipment_code', width: 20 },
{ header: '资产名称', key: 'equipment_name', width: 25 },
{ header: '资产类型', key: 'equipment_type', width: 15 },
@ -229,6 +287,7 @@ export const exportEquipment = async (req: Request, res: Response) => {
{ header: '使用部门', key: 'department', width: 15 },
{ header: '存放地址', key: 'storage_location', width: 20 },
{ header: '本体单价', key: 'unit_price', width: 12 },
{ header: '本体价值', key: 'own_total_value', width: 14 },
{ header: '当前价值', key: 'display_total_value', width: 14 },
{ header: '挂载配件价值', key: 'mounted_children_value', width: 16 },
{ header: '借出价值', key: 'borrowed_value', width: 14 },
@ -243,9 +302,38 @@ export const exportEquipment = async (req: Request, res: Response) => {
// 设置表头样式
worksheet.getRow(1).font = { bold: true }
worksheet.getRow(1).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFE0E0E0' } }
const currentValueHeader = worksheet.getCell(
1,
worksheet.getColumn('display_total_value').number
)
currentValueHeader.note =
'仅主资产和独立资产填写,可安全求和;主资产当前价值已包含配件。配件自身价值请查看“本体价值”。'
worksheet.getCell(1, worksheet.getColumn('own_total_value').number).note =
'每行资产自身价值,不包含所挂载配件;配件行请以此列查看价值。'
for (const columnKey of [
'mounted_children_value',
'borrowed_value',
'available_value'
]) {
worksheet.getCell(1, worksheet.getColumn(columnKey).number).note =
'仅主资产和独立资产填写,可直接求和;配件行留空以避免与主资产重复计算。'
}
equipments.forEach(eq => {
worksheet.addRow({
exportRows.forEach((hierarchyRow, index) => {
const eq: any = hierarchyRow.equipment
const calculatedValues = exportValues[index]
const row = worksheet.addRow({
relationship_group_code: hierarchyRow.relationship_group_code,
asset_level: hierarchyRow.asset_level,
parent_equipment_code: hierarchyRow.parent_equipment_code || '',
parent_equipment_name: hierarchyRow.parent_equipment_name || '',
accessory_sequence: hierarchyRow.accessory_sequence || '',
relationship_status:
hierarchyRow.asset_level === '主资产'
? `包含 ${hierarchyRow.group_accessory_count} 个配件`
: hierarchyRow.asset_level === '配件'
? '已挂载'
: '无配件',
equipment_code: eq.equipment_code,
equipment_name: eq.equipment_name,
equipment_type: eq.assetType?.name || eq.equipment_type,
@ -253,19 +341,53 @@ export const exportEquipment = async (req: Request, res: Response) => {
department: eq.department?.name || '',
storage_location: eq.storageLocation?.name || '',
unit_price: Number(eq.unit_price),
display_total_value: getEquipmentDisplayTotalValue(eq),
mounted_children_value: getEquipmentMountedChildrenValue(eq),
borrowed_value: getEquipmentDisplayBorrowedValue(eq),
available_value: getEquipmentDisplayAvailableValue(eq),
own_total_value: calculatedValues.own_total_value,
display_total_value: calculatedValues.display_total_value,
mounted_children_value: calculatedValues.mounted_children_value,
borrowed_value: calculatedValues.borrowed_value,
available_value: calculatedValues.available_value,
total_quantity: eq.total_quantity,
remaining_quantity: eq.remaining_quantity,
status: eq.status,
purchase_date: eq.purchase_date ? new Date(eq.purchase_date).toLocaleDateString('zh-CN') : '',
supplier: eq.supplier || ''
})
if (hierarchyRow.asset_level === '配件') {
row.outlineLevel = 1
row.getCell('equipment_name').alignment = {
...row.getCell('equipment_name').alignment,
indent: 1
}
} else if (hierarchyRow.asset_level === '主资产') {
row.font = { bold: true }
row.fill = {
type: 'pattern',
pattern: 'solid',
fgColor: { argb: 'FFF3F6F8' }
}
}
})
const timestamp = new Date().toISOString().slice(0, 10).replace(/-/g, '')
const descriptionWorksheet = workbook.addWorksheet('导出说明')
descriptionWorksheet.columns = [
{ header: '项目', key: 'item', width: 16 },
{ header: '说明', key: 'description', width: 100 }
]
descriptionWorksheet.addRows(buildEquipmentExportDescriptionRows(exportedAt))
descriptionWorksheet.getRow(1).font = { bold: true }
descriptionWorksheet.getRow(1).fill = {
type: 'pattern',
pattern: 'solid',
fgColor: { argb: 'FFE0E0E0' }
}
descriptionWorksheet.getColumn('description').alignment = {
vertical: 'top',
wrapText: true
}
descriptionWorksheet.views = [{ state: 'frozen', ySplit: 1 }]
const timestamp = formatShanghaiExportFilenameTimestamp(exportedAt)
const filename = `资产列表_${timestamp}.xlsx`
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
@ -318,6 +440,7 @@ export const exportBorrowRecords = async (req: Request, res: Response) => {
orderBy: { borrow_time: 'desc' }
})
const ExcelJS = loadExcelJS()
const workbook = new ExcelJS.Workbook()
const worksheet = workbook.addWorksheet('借用记录')
@ -389,6 +512,7 @@ export const generatePersonTemplate = async (req: Request, res: Response) => {
const departmentNames = departments.map(d => d.name)
const ExcelJS = loadExcelJS()
const workbook = new ExcelJS.Workbook()
// 创建主数据表

View File

@ -0,0 +1,83 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { flattenEquipmentExportHierarchy } from '../equipmentExportHierarchy'
test('flattenEquipmentExportHierarchy 应让父资产后紧跟配件,并保留独立资产', () => {
const rows = flattenEquipmentExportHierarchy([
{
id: 10,
equipment_code: 'JS-PC-0010',
equipment_name: '设计主机',
children: [
{ id: 12, equipment_code: 'JS-PJ-0012', equipment_name: '显卡' },
{ id: 11, equipment_code: 'JS-PJ-0011', equipment_name: '内存' }
]
},
{
id: 20,
equipment_code: 'JS-DY-0020',
equipment_name: '独立打印机',
children: []
}
])
assert.deepEqual(rows.map((row) => row.equipment.id), [10, 11, 12, 20])
assert.deepEqual(rows.map((row) => row.asset_level), ['主资产', '配件', '配件', '独立资产'])
assert.deepEqual(rows.map((row) => row.relationship_group_code), [
'JS-PC-0010',
'JS-PC-0010',
'JS-PC-0010',
'JS-DY-0020'
])
assert.deepEqual(rows.map((row) => row.accessory_sequence), [null, 1, 2, null])
assert.deepEqual(rows.map((row) => row.group_accessory_count), [2, 2, 2, 0])
assert.equal(rows[1].parent_equipment_code, 'JS-PC-0010')
assert.equal(rows[1].parent_equipment_name, '设计主机')
})
test('flattenEquipmentExportHierarchy 应稳定排序配件且同一资产只导出一次', () => {
const duplicatedChild = { id: 32, equipment_code: 'JS-PJ-0032', equipment_name: '硬盘' }
const rows = flattenEquipmentExportHierarchy([
{
id: 30,
equipment_code: 'JS-PC-0030',
equipment_name: '研发主机',
children: [
duplicatedChild,
{ id: 31, equipment_code: 'JS-PJ-0031', equipment_name: '内存' },
duplicatedChild
]
}
])
assert.deepEqual(rows.map((row) => row.equipment.id), [30, 31, 32])
assert.equal(new Set(rows.map((row) => row.equipment.id)).size, rows.length)
assert.deepEqual(rows.map((row) => row.accessory_sequence), [null, 1, 2])
})
test('flattenEquipmentExportHierarchy 应递归展平孙级配件且保持深度优先连续', () => {
const rows = flattenEquipmentExportHierarchy([
{
id: 40,
equipment_code: 'JS-PC-0040',
equipment_name: '多级主机',
children: [
{
id: 42,
equipment_code: 'JS-PJ-0042',
equipment_name: '扩展坞',
children: [
{ id: 43, equipment_code: 'JS-PJ-0043', equipment_name: '扩展卡' }
]
},
{ id: 41, equipment_code: 'JS-PJ-0041', equipment_name: '内存' }
]
}
])
assert.deepEqual(rows.map((row) => row.equipment.id), [40, 41, 42, 43])
assert.deepEqual(rows.map((row) => row.asset_level), ['主资产', '配件', '配件', '配件'])
assert.deepEqual(rows.map((row) => row.accessory_sequence), [null, 1, 2, 3])
assert.deepEqual(rows.map((row) => row.group_accessory_count), [3, 3, 3, 3])
})

View File

@ -0,0 +1,27 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import {
buildEquipmentExportDescriptionRows,
formatShanghaiExportDateTime,
formatShanghaiExportFilenameTimestamp
} from '../equipmentExportMetadata'
test('资产导出时间应固定按 Asia/Shanghai 精确格式化到秒', () => {
const exportedAt = new Date('2026-07-10T16:02:03.000Z')
assert.equal(formatShanghaiExportDateTime(exportedAt), '2026-07-11 00:02:03')
assert.equal(formatShanghaiExportFilenameTimestamp(exportedAt), '20260711_000203')
})
test('导出说明应完整声明快照、当前关系、时效与金额口径', () => {
const rows = buildEquipmentExportDescriptionRows(new Date('2026-07-10T01:02:03.000Z'))
const descriptionByItem = new Map(rows.map((row) => [row.item, row.description]))
assert.equal(descriptionByItem.get('文件性质'), '导出时点快照')
assert.match(descriptionByItem.get('导出时间') || '', /2026-07-10 09:02:03(Asia\/Shanghai)/)
assert.match(descriptionByItem.get('关系口径') || '', /当前 parent_id \/ children/)
assert.match(descriptionByItem.get('关系口径') || '', /不包含历史挂载/)
assert.match(descriptionByItem.get('时效提醒') || '', /最新导出/)
assert.match(descriptionByItem.get('金额口径') || '', /可安全求和/)
})

View File

@ -0,0 +1,85 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { flattenEquipmentExportHierarchy } from '../equipmentExportHierarchy'
import { calculateEquipmentExportValues } from '../equipmentExportValues'
test('主资产组合金额应现场汇总全部孙级本体价值并忽略根预存组合值', () => {
const rows = flattenEquipmentExportHierarchy([
{
id: 1,
equipment_code: 'JS-PC-0001',
equipment_name: '主机',
total_value: 100,
total_value_with_children: 150,
status: '已借出',
children: [
{
id: 2,
equipment_code: 'JS-PJ-0002',
equipment_name: '扩展坞',
total_value: 50,
children: [
{
id: 3,
equipment_code: 'JS-PJ-0003',
equipment_name: '扩展卡',
total_value: 30
}
]
}
]
}
])
const values = calculateEquipmentExportValues(rows)
assert.deepEqual(values[0], {
own_total_value: 100,
display_total_value: 180,
mounted_children_value: 80,
borrowed_value: 180,
available_value: 0
})
assert.deepEqual(values.slice(1), [
{
own_total_value: 50,
display_total_value: '',
mounted_children_value: '',
borrowed_value: '',
available_value: ''
},
{
own_total_value: 30,
display_total_value: '',
mounted_children_value: '',
borrowed_value: '',
available_value: ''
}
])
})
test('可用主资产应把现场组合总额计入可用价值', () => {
const rows = flattenEquipmentExportHierarchy([
{
id: 10,
equipment_code: 'JS-PC-0010',
equipment_name: '可用主机',
total_value: 200,
total_value_with_children: 0,
status: '可用',
children: [
{
id: 11,
equipment_code: 'JS-PJ-0011',
equipment_name: '内存',
total_value: 60
}
]
}
])
const values = calculateEquipmentExportValues(rows)
assert.equal(values[0].display_total_value, 260)
assert.equal(values[0].borrowed_value, 0)
assert.equal(values[0].available_value, 260)
})

View File

@ -0,0 +1,34 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { buildEquipmentKeywordSearchConditions } from '../equipmentKeywordSearch'
test('buildEquipmentKeywordSearchConditions 应覆盖设备、部门、地址和当前借用人字段', () => {
const serialized = JSON.stringify(buildEquipmentKeywordSearchConditions('A1001', 2))
for (const field of [
'equipment_code',
'equipment_name',
'equipment_model',
'department',
'storageLocation',
'borrow_records',
'person_nickname',
'person_name',
'nickname',
'real_name',
'job_number'
]) {
assert.equal(serialized.includes(field), true, `应包含 ${field}`)
}
assert.equal(serialized.includes('person_identifier'), false)
assert.equal(serialized.includes('id_number'), false)
})
test('buildEquipmentKeywordSearchConditions 深度 2 应允许父资产、配件和孙级配件命中', () => {
const serialized = JSON.stringify(buildEquipmentKeywordSearchConditions('显卡', 2))
const childRelationCount = (serialized.match(/"children"/g) || []).length
assert.equal(childRelationCount, 2)
})

View File

@ -0,0 +1,110 @@
export type EquipmentExportHierarchyLevel = '主资产' | '配件' | '独立资产'
export interface EquipmentExportHierarchyNode {
id: number
equipment_code: string
equipment_name: string
total_value?: unknown
total_value_with_children?: unknown
borrowed_value?: unknown
available_value?: unknown
status?: unknown
children?: EquipmentExportHierarchyNode[]
}
export interface FlattenedEquipmentExportRow<
TEquipment extends EquipmentExportHierarchyNode = EquipmentExportHierarchyNode
> {
equipment: TEquipment
relationship_group_code: string
asset_level: EquipmentExportHierarchyLevel
parent_equipment_code: string | null
parent_equipment_name: string | null
accessory_sequence: number | null
group_accessory_count: number
}
const compareAccessories = (
left: EquipmentExportHierarchyNode,
right: EquipmentExportHierarchyNode
) => {
const codeComparison = left.equipment_code.localeCompare(right.equipment_code)
return codeComparison || left.id - right.id
}
const collectDescendants = (
parent: EquipmentExportHierarchyNode,
exportedEquipmentIds: Set<number>,
ancestorIds: Set<number>
) => {
const descendants: EquipmentExportHierarchyNode[] = []
const childIdsInCurrentGroup = new Set<number>()
const children = [...(parent.children || [])]
.filter((child) => {
if (
exportedEquipmentIds.has(child.id) ||
ancestorIds.has(child.id) ||
childIdsInCurrentGroup.has(child.id)
) {
return false
}
childIdsInCurrentGroup.add(child.id)
return true
})
.sort(compareAccessories)
for (const child of children) {
exportedEquipmentIds.add(child.id)
descendants.push(child)
descendants.push(
...collectDescendants(
child,
exportedEquipmentIds,
new Set([...ancestorIds, child.id])
)
)
}
return descendants
}
export const flattenEquipmentExportHierarchy = (
parents: EquipmentExportHierarchyNode[]
): FlattenedEquipmentExportRow[] => {
const rows: FlattenedEquipmentExportRow[] = []
const exportedEquipmentIds = new Set<number>()
for (const parent of parents) {
if (exportedEquipmentIds.has(parent.id)) {
continue
}
exportedEquipmentIds.add(parent.id)
const descendants = collectDescendants(parent, exportedEquipmentIds, new Set([parent.id]))
const assetLevel: EquipmentExportHierarchyLevel = descendants.length > 0 ? '主资产' : '独立资产'
rows.push({
equipment: parent,
relationship_group_code: parent.equipment_code,
asset_level: assetLevel,
parent_equipment_code: null,
parent_equipment_name: null,
accessory_sequence: null,
group_accessory_count: descendants.length
})
descendants.forEach((child, index) => {
rows.push({
equipment: child,
relationship_group_code: parent.equipment_code,
asset_level: '配件',
parent_equipment_code: parent.equipment_code,
parent_equipment_name: parent.equipment_name,
accessory_sequence: index + 1,
group_accessory_count: descendants.length
})
})
}
return rows
}

View File

@ -0,0 +1,69 @@
const SHANGHAI_TIME_ZONE = 'Asia/Shanghai'
type ShanghaiDateTimeParts = {
year: string
month: string
day: string
hour: string
minute: string
second: string
}
const getShanghaiDateTimeParts = (date: Date): ShanghaiDateTimeParts => {
if (Number.isNaN(date.getTime())) {
throw new Error('导出时间无效')
}
const parts = new Intl.DateTimeFormat('en-CA', {
timeZone: SHANGHAI_TIME_ZONE,
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hourCycle: 'h23'
}).formatToParts(date)
const partMap = new Map(parts.map((part) => [part.type, part.value]))
return {
year: partMap.get('year') || '',
month: partMap.get('month') || '',
day: partMap.get('day') || '',
hour: partMap.get('hour') || '',
minute: partMap.get('minute') || '',
second: partMap.get('second') || ''
}
}
export const formatShanghaiExportDateTime = (date: Date) => {
const parts = getShanghaiDateTimeParts(date)
return `${parts.year}-${parts.month}-${parts.day} ${parts.hour}:${parts.minute}:${parts.second}`
}
export const formatShanghaiExportFilenameTimestamp = (date: Date) => {
const parts = getShanghaiDateTimeParts(date)
return `${parts.year}${parts.month}${parts.day}_${parts.hour}${parts.minute}${parts.second}`
}
export const buildEquipmentExportDescriptionRows = (exportedAt: Date) => [
{ item: '文件性质', description: '导出时点快照' },
{
item: '导出时间',
description: `${formatShanghaiExportDateTime(exportedAt)}(Asia/Shanghai)`
},
{
item: '关系口径',
description:
'按导出时同一次只读事务快照中的当前 parent_id / children 父子关系生成,不包含历史挂载记录。'
},
{
item: '时效提醒',
description: '父子关系会随挂载、卸载而变化,请以系统最新导出为准。'
},
{
item: '金额口径',
description:
'本体价值为每项资产自身价值;主资产组合金额现场汇总组内全部本体价值。当前价值、借出价值、可用价值和挂载配件价值仅在主资产或独立资产行填写,可安全求和;配件价值请查看本体价值。'
}
]

View File

@ -0,0 +1,70 @@
import {
getEquipmentDisplayAvailableValue,
getEquipmentDisplayBorrowedValue,
getEquipmentDisplayTotalValue,
getEquipmentMountedChildrenValue
} from './assetValue'
import type { FlattenedEquipmentExportRow } from './equipmentExportHierarchy'
export interface EquipmentExportCalculatedValues {
own_total_value: number
display_total_value: number | ''
mounted_children_value: number | ''
borrowed_value: number | ''
available_value: number | ''
}
const toSafeNumber = (value: unknown) => {
const numericValue = Number(value || 0)
return Number.isFinite(numericValue) ? numericValue : 0
}
export const calculateEquipmentExportValues = (
rows: FlattenedEquipmentExportRow[]
): EquipmentExportCalculatedValues[] => {
const groupTotalValues = new Map<string, number>()
for (const row of rows) {
const ownTotalValue = toSafeNumber(row.equipment.total_value)
groupTotalValues.set(
row.relationship_group_code,
(groupTotalValues.get(row.relationship_group_code) || 0) + ownTotalValue
)
}
return rows.map((row) => {
const equipment = row.equipment
const ownTotalValue = toSafeNumber(equipment.total_value)
if (row.asset_level === '配件') {
return {
own_total_value: ownTotalValue,
display_total_value: '',
mounted_children_value: '',
borrowed_value: '',
available_value: ''
}
}
if (row.asset_level === '独立资产') {
return {
own_total_value: ownTotalValue,
display_total_value: getEquipmentDisplayTotalValue(equipment),
mounted_children_value: getEquipmentMountedChildrenValue(equipment),
borrowed_value: getEquipmentDisplayBorrowedValue(equipment),
available_value: getEquipmentDisplayAvailableValue(equipment)
}
}
const combinedTotalValue = groupTotalValues.get(row.relationship_group_code) || ownTotalValue
const status = String(equipment.status || '')
return {
own_total_value: ownTotalValue,
display_total_value: combinedTotalValue,
mounted_children_value: Math.max(combinedTotalValue - ownTotalValue, 0),
borrowed_value: status === '已借出' ? combinedTotalValue : 0,
available_value: status === '可用' ? combinedTotalValue : 0
}
})
}

View File

@ -0,0 +1,74 @@
type EquipmentKeywordCondition = Record<string, unknown>
const buildOwnEquipmentKeywordConditions = (keyword: string): EquipmentKeywordCondition[] => [
{ equipment_name: { contains: keyword } },
{ equipment_code: { contains: keyword } },
{ equipment_model: { contains: keyword } },
{
department: {
is: {
name: { contains: keyword }
}
}
},
{
storageLocation: {
is: {
name: { contains: keyword }
}
}
},
{
borrow_records: {
some: {
status: '已借出',
OR: [
{ person_nickname: { contains: keyword } },
{ person_name: { contains: keyword } },
{
person: {
is: {
nickname: { contains: keyword }
}
}
},
{
person: {
is: {
real_name: { contains: keyword }
}
}
},
{
person: {
is: {
job_number: { contains: keyword }
}
}
}
]
}
}
}
]
export const buildEquipmentKeywordSearchConditions = (
keyword: string,
descendantDepth = 0
): EquipmentKeywordCondition[] => {
const ownConditions = buildOwnEquipmentKeywordConditions(keyword)
if (descendantDepth <= 0) {
return ownConditions
}
return [
...ownConditions,
{
children: {
some: {
OR: buildEquipmentKeywordSearchConditions(keyword, descendantDepth - 1)
}
}
}
]
}

View File

@ -3,12 +3,21 @@ import request from './index'
// 导出资产列表
export const exportEquipment = (params?: {
status?: string
equipment_type?: string
department_id?: number
equipmentType?: string
departmentId?: number | string
storageLocationId?: number | string
mountStatus?: string
keyword?: string
}) => {
return request.get('/export/equipment', {
params,
params: {
status: params?.status,
equipment_type: params?.equipmentType,
department_id: params?.departmentId,
storage_location_id: params?.storageLocationId,
mount_status: params?.mountStatus,
keyword: params?.keyword
},
responseType: 'blob'
})
}

View File

@ -1424,9 +1424,14 @@ const toggleSelectAll = () => {
const handlePrevPage = () => { if (currentPage.value > 1) { currentPage.value--; fetchData() } }
const handleNextPage = () => { if (currentPage.value < totalPages.value) { currentPage.value++; fetchData() } }
const formatExportTimestamp = (date = new Date()) => {
const pad = (value: number) => String(value).padStart(2, '0')
return `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}_${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}`
}
const handleExport = async () => {
const res = await exportEquipment(filters.value)
downloadFile(res.data as Blob, '资产列表.xlsx')
downloadFile(res.data as Blob, `资产列表_${formatExportTimestamp()}.xlsx`)
}
const handleExportAndClose = async () => {