修复核心流程并完成固定资产化改造

This commit is contained in:
WuYongJie 2026-03-10 18:34:25 +08:00
parent 80cbf3fbf5
commit c0ce7a6e3e
30 changed files with 1996 additions and 756 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
{
"name": "backend",
"version": "1.0.0",
"description": "摄影设备借还系统后端",
"description": "固定资产管理系统后端",
"main": "dist/index.js",
"scripts": {
"dev": "nodemon --exec ts-node src/index.ts",

View File

@ -0,0 +1,5 @@
ALTER TABLE `asset_type`
ADD COLUMN `short_name` VARCHAR(20) NULL;
CREATE UNIQUE INDEX `asset_type_short_name_key`
ON `asset_type`(`short_name`);

View File

@ -0,0 +1 @@
provider = "mysql"

View File

@ -13,6 +13,7 @@ datasource db {
model AssetType {
id Int @id @default(autoincrement())
name String @unique @db.VarChar(50)
short_name String? @unique @db.VarChar(20)
status String @default("active") @db.VarChar(20)
can_be_parent Boolean @default(false) // 可作为父资产
can_be_child Boolean @default(false) // 可作为子资产

View File

@ -5,6 +5,7 @@ import { successResponse, errorResponse } from '../utils/response'
const baseSelect = {
id: true,
name: true,
short_name: true,
status: true,
can_be_parent: true,
can_be_child: true,
@ -12,6 +13,16 @@ const baseSelect = {
updated_at: true
}
const normalizeShortName = (shortName?: string | null) => {
if (shortName === undefined) return undefined
const value = shortName?.trim().toUpperCase() || ''
if (!value) return null
if (!/^[A-Z0-9]{2,20}$/.test(value)) {
throw new Error('简称只能包含 2-20 位大写字母或数字')
}
return value
}
// 获取启用的资产类型列表(下拉选项)
export const getAssetTypeOptions = async (_req: Request, res: Response) => {
try {
@ -37,7 +48,10 @@ export const getAssetTypeList = async (req: Request, res: Response) => {
where.status = status
}
if (keyword) {
where.name = { contains: keyword as string }
where.OR = [
{ name: { contains: keyword as string } },
{ short_name: { contains: keyword as string } }
]
}
const skip = (Number(page) - 1) * Number(pageSize)
@ -76,8 +90,9 @@ export const getAssetTypeList = async (req: Request, res: Response) => {
// 创建资产类型
export const createAssetType = async (req: Request, res: Response) => {
try {
const { name, can_be_parent, can_be_child } = req.body as {
const { name, short_name, can_be_parent, can_be_child } = req.body as {
name?: string
short_name?: string
can_be_parent?: boolean
can_be_child?: boolean
}
@ -86,9 +101,12 @@ export const createAssetType = async (req: Request, res: Response) => {
return errorResponse(res, '类型名称不能为空', 422)
}
const normalizedShortName = normalizeShortName(short_name)
const assetType = await prisma.assetType.create({
data: {
name: name.trim(),
short_name: normalizedShortName,
can_be_parent: can_be_parent ?? false,
can_be_child: can_be_child ?? false
},
@ -98,7 +116,7 @@ export const createAssetType = async (req: Request, res: Response) => {
successResponse(res, assetType, '新增资产类型成功')
} catch (error: any) {
if (error.code === 'P2002') {
return errorResponse(res, '类型名称已存在')
return errorResponse(res, '类型名称或简称已存在')
}
errorResponse(res, error.message || '新增资产类型失败')
}
@ -108,8 +126,9 @@ export const createAssetType = async (req: Request, res: Response) => {
export const updateAssetType = async (req: Request, res: Response) => {
try {
const { id } = req.params
const { name, status, can_be_parent, can_be_child } = req.body as {
const { name, short_name, status, can_be_parent, can_be_child } = req.body as {
name?: string
short_name?: string
status?: string
can_be_parent?: boolean
can_be_child?: boolean
@ -127,10 +146,18 @@ export const updateAssetType = async (req: Request, res: Response) => {
return errorResponse(res, '类型名称不能为空', 422)
}
let normalizedShortName: string | null | undefined
try {
normalizedShortName = normalizeShortName(short_name)
} catch (error: any) {
return errorResponse(res, error.message || '简称格式不正确', 422)
}
const updated = await prisma.assetType.update({
where: { id: Number(id) },
data: {
name: name?.trim() ?? existing.name,
short_name: normalizedShortName !== undefined ? normalizedShortName : existing.short_name,
status: status ?? existing.status,
can_be_parent: can_be_parent ?? existing.can_be_parent,
can_be_child: can_be_child ?? existing.can_be_child
@ -141,7 +168,7 @@ export const updateAssetType = async (req: Request, res: Response) => {
successResponse(res, updated, '更新资产类型成功')
} catch (error: any) {
if (error.code === 'P2002') {
return errorResponse(res, '类型名称已存在')
return errorResponse(res, '类型名称或简称已存在')
}
errorResponse(res, error.message || '更新资产类型失败')
}

View File

@ -3,6 +3,122 @@ import prisma from '../utils/prisma'
import { successResponse, errorResponse } from '../utils/response'
import { logOperation } from './operationLogController'
const buildBorrowNotes = (notes?: string, isLongTerm?: boolean) => {
const longTerm = Boolean(isLongTerm)
return longTerm && notes
? `${notes};在职长期`
: longTerm
? '在职长期'
: notes || undefined
}
const getChildrenSnapshot = async (equipmentId: number) => {
const children = await prisma.equipment.findMany({
where: { parent_id: equipmentId },
select: {
id: true,
equipment_code: true,
equipment_name: true,
equipment_model: true,
equipment_type: true,
unit_price: true
}
})
return children.length > 0 ? JSON.stringify(children) : undefined
}
const batchBorrowTransaction = async (
equipment: any,
person: any,
expectedReturnTime?: string,
isLongTerm?: boolean,
notes?: string
) => {
const childrenSnapshot = await getChildrenSnapshot(equipment.id)
const finalNotes = buildBorrowNotes(notes, isLongTerm)
return prisma.$transaction(async (tx) => {
await tx.borrowRecord.create({
data: {
equipment_id: equipment.id,
equipment_code: equipment.equipment_code,
person_id: person.id,
person_name: person.real_name,
person_nickname: person.nickname,
person_identifier: person.id_number,
expected_return_time: isLongTerm ? null : (expectedReturnTime ? new Date(expectedReturnTime) : null),
is_long_term: Boolean(isLongTerm),
notes: finalNotes,
status: '已借出',
children_snapshot: childrenSnapshot
}
})
const nextStatus = equipment.remaining_quantity === 1 ? '已借出' : '可用'
const updated = await tx.equipment.updateMany({
where: {
id: equipment.id,
status: '可用',
remaining_quantity: equipment.remaining_quantity
},
data: {
remaining_quantity: { decrement: 1 },
borrowed_value: { increment: Number(equipment.unit_price) },
available_value: { decrement: Number(equipment.unit_price) },
status: nextStatus
}
})
if (updated.count !== 1) {
throw new Error('资产库存已变化,请刷新后重试')
}
})
}
const batchReturnTransaction = async (
equipment: any,
borrowRecord: any,
returnCondition: string,
notes?: string
) => {
return prisma.$transaction(async (tx) => {
const updatedRecord = await tx.borrowRecord.updateMany({
where: {
id: borrowRecord.id,
status: '已借出'
},
data: {
return_time: new Date(),
status: '已归还',
return_condition: returnCondition,
notes
}
})
if (updatedRecord.count !== 1) {
throw new Error('该借用记录已归还,请刷新后重试')
}
const updatedEquipment = await tx.equipment.updateMany({
where: {
id: equipment.id,
remaining_quantity: { lt: equipment.total_quantity }
},
data: {
remaining_quantity: { increment: 1 },
borrowed_value: { decrement: Number(equipment.unit_price) },
available_value: { increment: Number(equipment.unit_price) },
status: '可用'
}
})
if (updatedEquipment.count !== 1) {
throw new Error('资产库存已达到上限,请检查归还记录')
}
})
}
// 批量借出
export const batchBorrow = async (req: Request, res: Response) => {
try {
@ -37,32 +153,13 @@ export const batchBorrow = async (req: Request, res: Response) => {
continue
}
const longTerm = Boolean(is_long_term)
await prisma.borrowRecord.create({
data: {
equipment_id: equipment.id,
equipment_code: equipment.equipment_code,
person_id: person.id,
person_name: person.real_name,
person_nickname: person.nickname,
person_identifier: person.id_number,
expected_return_time: longTerm ? null : (expected_return_time ? new Date(expected_return_time) : null),
is_long_term: longTerm,
notes: longTerm ? `${notes || ''};在职长期` : notes,
status: '已借出'
}
})
const newRemaining = equipment.remaining_quantity - 1
await prisma.equipment.update({
where: { id: equipment.id },
data: {
remaining_quantity: newRemaining,
status: newRemaining === 0 ? '已借出' : '可用',
borrowed_value: Number(equipment.unit_price) * (equipment.total_quantity - newRemaining),
available_value: Number(equipment.unit_price) * newRemaining
}
})
await batchBorrowTransaction(
equipment,
person,
expected_return_time,
is_long_term,
notes
)
results.push({ equipment_id: equipmentId, equipment_code: equipment.equipment_code, status: 'success', message: '借出成功' })
successCount++
@ -117,10 +214,12 @@ export const batchReturn = async (req: Request, res: Response) => {
continue
}
const borrowRecord = await prisma.borrowRecord.findFirst({
const activeBorrowRecords = await prisma.borrowRecord.findMany({
where: { equipment_id: equipment.id, status: '已借出' },
orderBy: { borrow_time: 'desc' }
orderBy: { borrow_time: 'desc' },
take: 2
})
const borrowRecord = activeBorrowRecords[0]
if (!borrowRecord) {
results.push({ equipment_id: equipmentId, equipment_code: equipment.equipment_code, status: 'failed', message: '未找到借用记录' })
@ -128,21 +227,13 @@ export const batchReturn = async (req: Request, res: Response) => {
continue
}
await prisma.borrowRecord.update({
where: { id: borrowRecord.id },
data: { return_time: new Date(), status: '已归还', return_condition, notes }
})
if (activeBorrowRecords.length > 1) {
results.push({ equipment_id: equipmentId, equipment_code: equipment.equipment_code, status: 'failed', message: '存在多条未归还记录,请使用单个归还' })
failedCount++
continue
}
const newRemaining = equipment.remaining_quantity + 1
await prisma.equipment.update({
where: { id: equipment.id },
data: {
remaining_quantity: newRemaining,
status: '可用',
borrowed_value: Number(equipment.unit_price) * (equipment.total_quantity - newRemaining),
available_value: Number(equipment.unit_price) * newRemaining
}
})
await batchReturnTransaction(equipment, borrowRecord, return_condition, notes)
results.push({ equipment_id: equipmentId, equipment_code: equipment.equipment_code, status: 'success', message: '归还成功' })
successCount++

View File

@ -81,7 +81,7 @@ export const batchImportPersons = async (req: Request, res: Response) => {
}
// 记录操作日志
const user = (req as any).user
const user = res.locals.user
await prisma.operationLog.create({
data: {
user_id: user?.id || null,
@ -201,7 +201,7 @@ export const batchImportEquipments = async (req: Request, res: Response) => {
}
// 记录操作日志
const user = (req as any).user
const user = res.locals.user
await prisma.operationLog.create({
data: {
user_id: user?.id || null,

View File

@ -62,33 +62,206 @@ const ensureEquipmentBorrowable = (equipment: any) => {
}
}
const updateEquipmentAfterBorrow = async (equipment: any) => {
const newRemainingQuantity = equipment.remaining_quantity - 1
if (newRemainingQuantity < 0) {
throw new Error('资产库存不足')
}
const borrowedQuantity = equipment.total_quantity - newRemainingQuantity
await prisma.equipment.update({
where: { id: equipment.id },
data: {
remaining_quantity: newRemainingQuantity,
status: newRemainingQuantity === 0 ? '已借出' : '可用',
borrowed_value: Number(equipment.unit_price) * borrowedQuantity,
available_value: Number(equipment.unit_price) * newRemainingQuantity
const appendLongTermNotes = (notes?: string, isLongTerm?: boolean) => {
const longTerm = Boolean(isLongTerm)
return longTerm && notes
? `${notes};在职长期`
: longTerm
? '在职长期'
: notes || undefined
}
const getChildrenSnapshot = async (equipmentId: number) => {
const children = await prisma.equipment.findMany({
where: { parent_id: equipmentId },
select: {
id: true,
equipment_code: true,
equipment_name: true,
equipment_model: true,
equipment_type: true,
unit_price: true
}
})
return {
children,
snapshot: children.length > 0 ? JSON.stringify(children) : undefined
}
}
const borrowEquipmentInTransaction = async (
equipment: any,
person: any,
expectedReturn: Date | null,
combinedNotes?: string,
isLongTerm?: boolean,
childrenSnapshot?: string
) => {
return prisma.$transaction(async (tx) => {
const record = await tx.borrowRecord.create({
data: {
equipment_id: equipment.id,
equipment_code: equipment.equipment_code,
person_id: person.id,
person_name: person.real_name,
person_nickname: person.nickname,
person_identifier: person.id_number,
expected_return_time: expectedReturn,
is_long_term: Boolean(isLongTerm),
notes: combinedNotes,
status: '已借出',
children_snapshot: childrenSnapshot
}
})
const nextStatus = equipment.remaining_quantity === 1 ? '已借出' : '可用'
const updateResult = await tx.equipment.updateMany({
where: {
id: equipment.id,
status: '可用',
remaining_quantity: equipment.remaining_quantity
},
data: {
remaining_quantity: { decrement: 1 },
borrowed_value: { increment: Number(equipment.unit_price) },
available_value: { decrement: Number(equipment.unit_price) },
status: nextStatus
}
})
if (updateResult.count !== 1) {
throw new Error('资产库存已变化,请刷新后重试')
}
const updatedEquipment = await tx.equipment.findUnique({
where: { id: equipment.id },
include: equipmentInclude
})
return { record, updatedEquipment }
})
}
const updateEquipmentAfterReturn = async (equipment: any) => {
const newRemainingQuantity = equipment.remaining_quantity + 1
const borrowedQuantity = equipment.total_quantity - newRemainingQuantity
await prisma.equipment.update({
where: { id: equipment.id },
data: {
remaining_quantity: newRemainingQuantity,
status: '可用',
borrowed_value: Number(equipment.unit_price) * borrowedQuantity,
available_value: Number(equipment.unit_price) * newRemainingQuantity
const findActiveBorrowRecord = async (
equipmentId: number,
borrowRecordId?: number,
personId?: number
) => {
if (borrowRecordId) {
return prisma.borrowRecord.findFirst({
where: {
id: borrowRecordId,
equipment_id: equipmentId,
status: '已借出'
}
})
}
if (personId) {
return prisma.borrowRecord.findFirst({
where: {
equipment_id: equipmentId,
person_id: personId,
status: '已借出'
},
orderBy: { borrow_time: 'desc' }
})
}
const activeRecords = await prisma.borrowRecord.findMany({
where: {
equipment_id: equipmentId,
status: '已借出'
},
orderBy: { borrow_time: 'desc' },
take: 2
})
if (activeRecords.length > 1) {
throw new Error('该资产存在多条未归还记录,请明确选择归还使用者')
}
return activeRecords[0] || null
}
const getChildrenDiff = async (equipmentId: number, borrowRecord: any) => {
if (!borrowRecord?.children_snapshot) {
return null
}
const borrowedChildren = JSON.parse(borrowRecord.children_snapshot) as any[]
const currentChildren = await prisma.equipment.findMany({
where: { parent_id: equipmentId },
select: {
id: true,
equipment_code: true,
equipment_name: true,
equipment_model: true,
equipment_type: true,
unit_price: true
}
})
const borrowedIds = new Set(borrowedChildren.map((child) => child.id))
const currentIds = new Set(currentChildren.map((child) => child.id))
const removed = borrowedChildren.filter((child) => !currentIds.has(child.id))
const added = currentChildren.filter((child) => !borrowedIds.has(child.id))
if (removed.length === 0 && added.length === 0) {
return null
}
return { removed, added }
}
const returnEquipmentInTransaction = async (
equipment: any,
borrowRecord: any,
returnCondition: string,
notes?: string
) => {
return prisma.$transaction(async (tx) => {
const returnTime = new Date()
const updatedBorrowRecord = await tx.borrowRecord.updateMany({
where: {
id: borrowRecord.id,
status: '已借出'
},
data: {
return_time: returnTime,
status: '已归还',
return_condition: returnCondition,
notes
}
})
if (updatedBorrowRecord.count !== 1) {
throw new Error('该借用记录已归还,请刷新后重试')
}
const equipmentUpdate = await tx.equipment.updateMany({
where: {
id: equipment.id,
remaining_quantity: { lt: equipment.total_quantity }
},
data: {
remaining_quantity: { increment: 1 },
borrowed_value: { decrement: Number(equipment.unit_price) },
available_value: { increment: Number(equipment.unit_price) },
status: '可用'
}
})
if (equipmentUpdate.count !== 1) {
throw new Error('资产库存已达到上限,请检查归还记录')
}
return {
id: borrowRecord.id,
person_id: borrowRecord.person_id,
return_time: returnTime,
status: '已归还'
}
})
}
@ -123,7 +296,8 @@ const mapBorrowRecord = (record: any) => ({
return_time: record.return_time,
status: record.status,
return_condition: record.return_condition,
notes: record.notes
notes: record.notes,
children_snapshot: record.children_snapshot
})
// 借用资产
@ -158,48 +332,16 @@ export const borrowEquipment = async (req: Request, res: Response) => {
const longTerm = Boolean(is_long_term)
const expectedReturn = longTerm ? null : parseDate(expected_return_time)
const combinedNotes =
longTerm && notes
? `${notes};在职长期`
: longTerm
? '在职长期'
: notes || undefined
// 获取子资产快照(如果是父资产)
let childrenSnapshot: string | undefined
const children = await prisma.equipment.findMany({
where: { parent_id: equipment.id },
select: {
id: true,
equipment_code: true,
equipment_name: true,
equipment_model: true,
equipment_type: true,
unit_price: true
}
})
if (children.length > 0) {
childrenSnapshot = JSON.stringify(children)
}
const borrowRecord = await prisma.borrowRecord.create({
data: {
equipment_id: equipment.id,
equipment_code: equipment.equipment_code,
person_id: person.id,
person_name: person.real_name,
person_nickname: person.nickname,
person_identifier: person.id_number,
expected_return_time: expectedReturn,
is_long_term: longTerm,
notes: combinedNotes,
status: '已借出',
children_snapshot: childrenSnapshot
}
})
await updateEquipmentAfterBorrow(equipment)
const combinedNotes = appendLongTermNotes(notes, longTerm)
const { children, snapshot } = await getChildrenSnapshot(equipment.id)
const { record: borrowRecord } = await borrowEquipmentInTransaction(
equipment,
person,
expectedReturn,
combinedNotes,
longTerm,
snapshot
)
successResponse(
res,
@ -246,51 +388,16 @@ export const scanBorrow = async (req: Request, res: Response) => {
const longTerm = Boolean(is_long_term)
const expectedReturn = longTerm ? null : parseDate(expected_return_time)
const combinedNotes =
longTerm && notes
? `${notes};在职长期`
: longTerm
? '在职长期'
: notes || undefined
// 获取子资产快照(如果是父资产)
let childrenSnapshot: string | undefined
const children = await prisma.equipment.findMany({
where: { parent_id: equipment.id },
select: {
id: true,
equipment_code: true,
equipment_name: true,
equipment_model: true,
equipment_type: true,
unit_price: true
}
})
if (children.length > 0) {
childrenSnapshot = JSON.stringify(children)
}
const borrowRecord = await prisma.borrowRecord.create({
data: {
equipment_id: equipment.id,
equipment_code: equipment.equipment_code,
person_id: person.id,
person_name: person.real_name,
person_nickname: person.nickname,
person_identifier: person.id_number,
expected_return_time: expectedReturn,
is_long_term: longTerm,
notes: combinedNotes,
status: '已借出',
children_snapshot: childrenSnapshot
}
})
await updateEquipmentAfterBorrow(equipment)
const updatedEquipment = await prisma.equipment.findUnique({
where: { id: equipment.id },
include: equipmentInclude
})
const combinedNotes = appendLongTermNotes(notes, longTerm)
const { children, snapshot } = await getChildrenSnapshot(equipment.id)
const { record: borrowRecord, updatedEquipment } = await borrowEquipmentInTransaction(
equipment,
person,
expectedReturn,
combinedNotes,
longTerm,
snapshot
)
successResponse(
res,
@ -343,12 +450,7 @@ export const scanBorrowBatch = async (req: Request, res: Response) => {
const longTerm = Boolean(is_long_term)
const expectedReturn = longTerm ? null : parseDate(expected_return_time)
const combinedNotes =
longTerm && notes
? `${notes};在职长期`
: longTerm
? '在职长期'
: notes || undefined
const combinedNotes = appendLongTermNotes(notes, longTerm)
const results: any[] = []
let successCount = 0
let failedCount = 0
@ -370,22 +472,15 @@ export const scanBorrowBatch = async (req: Request, res: Response) => {
continue
}
await prisma.borrowRecord.create({
data: {
equipment_id: equipment.id,
equipment_code: equipment.equipment_code,
person_id: person.id,
person_name: person.real_name,
person_nickname: person.nickname,
person_identifier: person.id_number,
expected_return_time: expectedReturn,
is_long_term: longTerm,
notes: combinedNotes,
status: '已借出'
}
})
await updateEquipmentAfterBorrow(equipment)
const { snapshot } = await getChildrenSnapshot(equipment.id)
await borrowEquipmentInTransaction(
equipment,
person,
expectedReturn,
combinedNotes,
longTerm,
snapshot
)
results.push({ equipment_code: code, status: 'success', message: '出库成功' })
successCount++
@ -425,6 +520,8 @@ export const returnEquipment = async (req: Request, res: Response) => {
const {
equipment_id,
equipment_code,
borrow_record_id,
person_id,
return_condition = '正常',
notes,
confirm_children_diff
@ -437,67 +534,37 @@ export const returnEquipment = async (req: Request, res: Response) => {
return errorResponse(res, '资产不存在', 404)
}
const borrowRecord = await prisma.borrowRecord.findFirst({
where: {
equipment_id: equipment.id,
status: '已借出'
},
orderBy: { borrow_time: 'desc' }
})
let borrowRecord
try {
borrowRecord = await findActiveBorrowRecord(
equipment.id,
parseNumber(borrow_record_id),
parseNumber(person_id)
)
} catch (error: any) {
return errorResponse(res, error.message || '未找到对应的借用记录', 422)
}
if (!borrowRecord) {
return errorResponse(res, '未找到对应的借用记录')
}
// 检查配件差异
let childrenDiff: any = null
if (borrowRecord.children_snapshot) {
const borrowedChildren = JSON.parse(borrowRecord.children_snapshot) as any[]
const currentChildren = await prisma.equipment.findMany({
where: { parent_id: equipment.id },
select: {
id: true,
equipment_code: true,
equipment_name: true,
equipment_model: true,
equipment_type: true,
unit_price: true
}
})
const borrowedIds = new Set(borrowedChildren.map(c => c.id))
const currentIds = new Set(currentChildren.map(c => c.id))
const removed = borrowedChildren.filter(c => !currentIds.has(c.id))
const added = currentChildren.filter(c => !borrowedIds.has(c.id))
if (removed.length > 0 || added.length > 0) {
childrenDiff = { removed, added }
// 如果有差异且未确认,返回差异信息要求确认
if (!confirm_children_diff) {
return successResponse(res, {
need_confirm: true,
children_diff: childrenDiff,
message: '配件与借出时不一致,请确认后继续'
}, '配件差异提醒')
}
}
const childrenDiff = await getChildrenDiff(equipment.id, borrowRecord)
if (childrenDiff && !confirm_children_diff) {
return successResponse(res, {
need_confirm: true,
children_diff: childrenDiff,
message: '配件与借出时不一致,请确认后继续'
}, '配件差异提醒')
}
await prisma.borrowRecord.update({
where: { id: borrowRecord.id },
data: {
return_time: new Date(),
status: '已归还',
return_condition,
notes: childrenDiff
? `${notes || ''};配件变更:移除${childrenDiff.removed.length}个,新增${childrenDiff.added.length}个`
: notes
}
})
await updateEquipmentAfterReturn(equipment)
const finalNotes = childrenDiff
? [notes, `配件变更:移除${childrenDiff.removed.length}个,新增${childrenDiff.added.length}个`]
.filter(Boolean)
.join(';')
: notes
await returnEquipmentInTransaction(equipment, borrowRecord, return_condition, finalNotes)
successResponse(
res,
@ -527,7 +594,8 @@ export const scanReturn = async (req: Request, res: Response) => {
return_condition = '正常',
notes,
person_id,
borrow_record_id
borrow_record_id,
confirm_children_diff
} = req.body as Record<string, any>
const equipment = await getEquipmentByIdentifier(undefined, equipment_code)
@ -536,57 +604,42 @@ export const scanReturn = async (req: Request, res: Response) => {
return errorResponse(res, '资产不存在', 404)
}
const personIdNum = parseNumber(person_id)
const borrowRecordIdNum = parseNumber(borrow_record_id)
let borrowRecord = null
if (borrowRecordIdNum) {
borrowRecord = await prisma.borrowRecord.findFirst({
where: {
id: borrowRecordIdNum,
equipment_id: equipment.id,
status: '已借出'
}
})
}
if (!borrowRecord && personIdNum) {
borrowRecord = await prisma.borrowRecord.findFirst({
where: {
equipment_id: equipment.id,
person_id: personIdNum,
status: '已借出'
},
orderBy: { borrow_time: 'desc' }
})
}
if (!borrowRecord) {
borrowRecord = await prisma.borrowRecord.findFirst({
where: {
equipment_id: equipment.id,
status: '已借出'
},
orderBy: { borrow_time: 'desc' }
})
try {
borrowRecord = await findActiveBorrowRecord(
equipment.id,
parseNumber(borrow_record_id),
parseNumber(person_id)
)
} catch (error: any) {
return errorResponse(res, error.message || '未找到对应的借用记录,请选择正确的使用者', 422)
}
if (!borrowRecord) {
return errorResponse(res, '未找到对应的借用记录,请选择正确的使用者', 404)
}
const updatedRecord = await prisma.borrowRecord.update({
where: { id: borrowRecord.id },
data: {
return_time: new Date(),
status: '已归还',
return_condition,
notes
}
})
await updateEquipmentAfterReturn(equipment)
const childrenDiff = await getChildrenDiff(equipment.id, borrowRecord)
if (childrenDiff && !confirm_children_diff) {
return successResponse(res, {
need_confirm: true,
children_diff: childrenDiff,
message: '配件与借出时不一致,请确认后继续'
}, '配件差异提醒')
}
const finalNotes = childrenDiff
? [notes, `配件变更:移除${childrenDiff.removed.length}个,新增${childrenDiff.added.length}个`]
.filter(Boolean)
.join(';')
: notes
const updatedRecord = await returnEquipmentInTransaction(
equipment,
borrowRecord,
return_condition,
finalNotes
)
successResponse(
res,
@ -638,31 +691,36 @@ export const scanReturnBatch = async (req: Request, res: Response) => {
continue
}
const borrowRecord = await prisma.borrowRecord.findFirst({
const activeBorrowRecords = await prisma.borrowRecord.findMany({
where: {
equipment_id: equipment.id,
status: '已借出'
},
orderBy: { borrow_time: 'desc' }
orderBy: { borrow_time: 'desc' },
take: 2
})
const borrowRecord = activeBorrowRecords[0]
if (!borrowRecord) {
results.push({ equipment_code: code, status: 'failed', message: '未找到对应的借用记录' })
failedCount++
continue
}
await prisma.borrowRecord.update({
where: { id: borrowRecord.id },
data: {
return_time: new Date(),
status: '已归还',
return_condition,
notes
}
})
await updateEquipmentAfterReturn(equipment)
if (activeBorrowRecords.length > 1) {
results.push({ equipment_code: code, status: 'failed', message: '存在多条未归还记录,请使用单个扫码归还' })
failedCount++
continue
}
const childrenDiff = await getChildrenDiff(equipment.id, borrowRecord)
if (childrenDiff) {
results.push({ equipment_code: code, status: 'failed', message: '配件与借出时不一致,请使用单个扫码归还确认' })
failedCount++
continue
}
await returnEquipmentInTransaction(equipment, borrowRecord, return_condition, notes)
results.push({ equipment_code: code, status: 'success', message: '入库成功' })
successCount++
@ -827,4 +885,3 @@ export const getBorrowRecordList = async (req: Request, res: Response) => {
errorResponse(res, error.message || '获取借用记录列表失败')
}
}

View File

@ -37,6 +37,7 @@ const includeStorageLocation = {
select: {
id: true,
name: true,
short_name: true,
status: true
}
}
@ -453,6 +454,25 @@ export const deleteEquipment = async (req: Request, res: Response) => {
const equipmentId = Number(id)
const equipment = await prisma.equipment.findUnique({
where: { id: equipmentId },
include: {
_count: {
select: {
borrow_records: true,
children: true,
mountHistories: true,
disposalRecords: true,
maintenanceRecords: true
}
}
}
})
if (!equipment) {
return errorResponse(res, '资产不存在', 404)
}
// 检查是否有未归还的借用记录
const activeBorrowCount = await prisma.borrowRecord.count({
where: {
@ -465,15 +485,28 @@ export const deleteEquipment = async (req: Request, res: Response) => {
return errorResponse(res, '该资产存在未归还的借用记录,无法删除')
}
// 清理历史借用记录,避免外键约束阻止删除
await prisma.borrowRecord.deleteMany({
where: {
equipment_id: equipmentId
}
})
await prisma.equipment.delete({
where: { id: equipmentId }
if (equipment.parent_id) {
return errorResponse(res, '该资产当前作为子资产挂载中,无法删除')
}
if (equipment._count.children > 0) {
return errorResponse(res, '该资产存在已挂载的子资产,无法删除')
}
if (equipment._count.mountHistories > 0 || equipment._count.disposalRecords > 0 || equipment._count.maintenanceRecords > 0) {
return errorResponse(res, '该资产已存在维修、报废或挂载历史,无法删除')
}
await prisma.$transaction(async (tx) => {
await tx.borrowRecord.deleteMany({
where: {
equipment_id: equipmentId
}
})
await tx.equipment.delete({
where: { id: equipmentId }
})
})
successResponse(res, null, '删除成功')
@ -495,7 +528,7 @@ export const generateCode = async (req: Request, res: Response) => {
const code = await generateEquipmentCode(equipment_type, prisma, locationId)
successResponse(res, {
equipment_code: code,
code_format: '{类型数字}{地址ID(4位)}{序号(5位)}'
code_format: '{类型简称}-{地址ID(4位)}-{序号(5位)}'
}, '生成成功')
} else {
errorResponse(res, '自定义录入模式不需要生成编码')
@ -565,4 +598,3 @@ export const scanEquipment = async (req: Request, res: Response) => {
errorResponse(res, error.message || '查询资产失败')
}
}

View File

@ -38,7 +38,10 @@ export const mountChild = async (req: Request, res: Response) => {
try {
const { parentId } = req.params
const { child_id, notes } = req.body as { child_id: number; notes?: string }
const userId = (req as any).user?.id || 1
const userId = Number(res.locals.user?.id)
if (!userId) {
return errorResponse(res, '用户未登录', 401)
}
// 验证父资产存在
const parent = await prisma.equipment.findUnique({
@ -131,7 +134,10 @@ export const unmountChild = async (req: Request, res: Response) => {
try {
const { parentId } = req.params
const { child_id, notes } = req.body as { child_id: number; notes?: string }
const userId = (req as any).user?.id || 1
const userId = Number(res.locals.user?.id)
if (!userId) {
return errorResponse(res, '用户未登录', 401)
}
// 验证子资产存在且挂载在该父资产上
const child = await prisma.equipment.findUnique({
@ -185,7 +191,10 @@ export const replaceChild = async (req: Request, res: Response) => {
new_child_id: number
notes?: string
}
const userId = (req as any).user?.id || 1
const userId = Number(res.locals.user?.id)
if (!userId) {
return errorResponse(res, '用户未登录', 401)
}
// 验证旧配件挂载在该父资产上
const oldChild = await prisma.equipment.findUnique({

View File

@ -222,12 +222,13 @@ export const deletePerson = async (req: Request, res: Response) => {
return errorResponse(res, '该使用者存在未归还的借用记录,无法删除')
}
// 删除历史借用记录
await prisma.borrowRecord.deleteMany({
where: { person_id: personId }
})
await prisma.$transaction(async (tx) => {
await tx.borrowRecord.deleteMany({
where: { person_id: personId }
})
await prisma.person.delete({ where: { id: personId } })
await tx.person.delete({ where: { id: personId } })
})
successResponse(res, null, '删除成功')
} catch (error: any) {

View File

@ -2,9 +2,9 @@ import type { Request, Response } from 'express'
import prisma from '../utils/prisma'
import { successResponse, errorResponse } from '../utils/response'
const DEFAULT_SITE_NAME = '摄影资产借还系统'
const DEFAULT_SITE_NAME = '固定资产管理系统'
const DEFAULT_THEME_COLOR = '#409EFF'
const DEFAULT_LOGO_URL = 'https://s3.1080p.hk/shuzhou/2025/05/18/8ebadf0a7cc34eaea00e8619b8efb827.png'
const DEFAULT_LOGO_URL = ''
const mapConfigResponse = (config: any) => {
if (!config) {
@ -20,8 +20,8 @@ const mapConfigResponse = (config: any) => {
return {
site_name: config.site_name || DEFAULT_SITE_NAME,
logo_url: DEFAULT_LOGO_URL,
favicon_url: '',
logo_url: config.logo_url || DEFAULT_LOGO_URL,
favicon_url: config.favicon_url || '',
theme_color: config.theme_color || DEFAULT_THEME_COLOR,
updated_at: config.updated_at,
updated_by: config.updatedBy
@ -72,8 +72,8 @@ export const updateSiteConfig = async (req: Request, res: Response) => {
theme_color: theme_color || DEFAULT_THEME_COLOR,
updated_at: new Date(),
updated_by: res.locals.user?.id ?? null,
logo_url: DEFAULT_LOGO_URL,
favicon_url: null
logo_url: existing?.logo_url || DEFAULT_LOGO_URL,
favicon_url: existing?.favicon_url || null
}
const result = existing
@ -105,4 +105,3 @@ export const updateSiteConfig = async (req: Request, res: Response) => {
errorResponse(res, error.message || '更新站点配置失败')
}
}

View File

@ -164,16 +164,32 @@ export const getValueStatistics = async (req: Request, res: Response) => {
.filter(eq => eq.status === '维护中')
.reduce((sum, eq) => sum + Number(eq.available_value), 0)
// 按资产类型统计
const valueByType = ['相机', '镜头', '三脚架', '闪光灯', '摄影配饰品'].map(type => {
const typeEquipment = equipmentList.filter(eq => eq.equipment_type === type)
return {
equipment_type: type,
total_value: typeEquipment.reduce((sum, eq) => sum + Number(eq.total_value), 0),
borrowed_value: typeEquipment.reduce((sum, eq) => sum + Number(eq.borrowed_value), 0),
available_value: typeEquipment.reduce((sum, eq) => sum + Number(eq.available_value), 0)
// 按资产类型统计,避免沿用旧项目中的固定类型名单
const valueByTypeMap = equipmentList.reduce((acc, eq) => {
const type = eq.equipment_type || '未分类'
if (!acc[type]) {
acc[type] = {
equipment_type: type,
total_value: 0,
borrowed_value: 0,
available_value: 0
}
}
}).filter(item => item.total_value > 0)
acc[type].total_value += Number(eq.total_value)
acc[type].borrowed_value += Number(eq.borrowed_value)
acc[type].available_value += Number(eq.available_value)
return acc
}, {} as Record<string, {
equipment_type: string
total_value: number
borrowed_value: number
available_value: number
}>)
const valueByType = Object.values(valueByTypeMap)
.filter((item) => item.total_value > 0)
.sort((a, b) => b.total_value - a.total_value)
// 价值最高的资产(Top 10)
const topEquipment = equipmentList
@ -199,4 +215,3 @@ export const getValueStatistics = async (req: Request, res: Response) => {
errorResponse(res, error.message || '获取价值统计失败')
}
}

View File

@ -201,7 +201,20 @@ export const deleteUser = async (req: Request, res: Response) => {
const { id } = req.params
const userId = Number(id)
const user = await prisma.user.findUnique({ where: { id: userId } })
const user = await prisma.user.findUnique({
where: { id: userId },
include: {
_count: {
select: {
site_configs: true,
mount_histories: true,
disposal_applied: true,
disposal_approved: true,
maintenance_records: true
}
}
}
})
if (!user) {
return errorResponse(res, '用户不存在', 404)
}
@ -211,12 +224,23 @@ export const deleteUser = async (req: Request, res: Response) => {
return errorResponse(res, '不能删除管理员账户')
}
// 删除关联的操作日志
await prisma.operationLog.deleteMany({
where: { user_id: userId }
})
if (
user._count.site_configs > 0 ||
user._count.mount_histories > 0 ||
user._count.disposal_applied > 0 ||
user._count.disposal_approved > 0 ||
user._count.maintenance_records > 0
) {
return errorResponse(res, '该用户已存在业务历史记录,无法删除')
}
await prisma.user.delete({ where: { id: userId } })
await prisma.$transaction(async (tx) => {
await tx.operationLog.deleteMany({
where: { user_id: userId }
})
await tx.user.delete({ where: { id: userId } })
})
successResponse(res, null, '删除成功')
} catch (error: any) {

View File

@ -62,7 +62,7 @@ app.use('/api/return', protect(), returnRouter)
app.use('/api/statistics', protect(), statisticsRoutes)
app.use('/api/barcode', protect(), barcodeRoutes)
app.use('/api/auth', authRoutes)
app.use('/api/settings', protect(), requireAdmin(), settingsRoutes)
app.use('/api/settings', settingsRoutes)
app.use('/api/users', protect(), requireAdmin(), usersRoutes)
app.use('/api/persons', personRoutes)
app.use('/api/storage-locations', storageLocationRoutes)
@ -95,4 +95,3 @@ app.use((err: any, req: express.Request, res: express.Response, next: express.Ne
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`)
})

View File

@ -7,9 +7,13 @@ import {
completeDisposal,
getEquipmentDisposalHistory
} from '../controllers/disposalController'
import { protect, requireAdmin } from '../middleware/auth'
const router = Router()
router.use(protect())
router.use(requireAdmin())
router.post('/', createDisposal)
router.get('/list', getDisposalList)
router.get('/:id', getDisposalDetail)

View File

@ -8,9 +8,13 @@ import {
cancelMaintenance,
getEquipmentMaintenanceHistory
} from '../controllers/maintenanceController'
import { protect, requireAdmin } from '../middleware/auth'
const router = Router()
router.use(protect())
router.use(requireAdmin())
router.post('/', createMaintenance)
router.get('/list', getMaintenanceList)
router.get('/:id', getMaintenanceDetail)

View File

@ -7,11 +7,14 @@ import {
getMountHistory,
getAvailableChildren
} from '../controllers/mountController'
import { protect, requireAdmin } from '../middleware/auth'
const router = Router()
router.use(protect())
// 获取可挂载的子资产列表
router.get('/available-children', getAvailableChildren)
router.get('/available-children', requireAdmin(), getAvailableChildren)
// 获取资产的挂载历史
router.get('/:id/mount-history', getMountHistory)
@ -20,12 +23,12 @@ router.get('/:id/mount-history', getMountHistory)
router.get('/:parentId/children', getChildren)
// 挂载子资产到父资产
router.post('/:parentId/mount', mountChild)
router.post('/:parentId/mount', requireAdmin(), mountChild)
// 从父资产卸载子资产
router.post('/:parentId/unmount', unmountChild)
router.post('/:parentId/unmount', requireAdmin(), unmountChild)
// 更换配件
router.post('/:parentId/replace', replaceChild)
router.post('/:parentId/replace', requireAdmin(), replaceChild)
export default router

View File

@ -1,44 +1,44 @@
// 资产类型数字代码映射(纯数字编码)
const EQUIPMENT_TYPE_NUMBERS: Record<string, number> = {
'相机': 1,
'镜头': 2,
'三脚架': 3,
'闪光灯': 4,
'摄影配饰品': 5
const EQUIPMENT_CODE_PATTERN = /^[A-Z0-9]{2,20}-\d{4}-\d{5}$/
const normalizeShortName = (value?: string | null) => {
const normalized = value?.trim().toUpperCase() || ''
if (!normalized) return null
return normalized.replace(/[^A-Z0-9]/g, '')
}
const resolveTypeShortName = async (equipmentType: string, prisma: any) => {
const assetType = await prisma.assetType.findFirst({
where: { name: equipmentType },
select: {
id: true,
short_name: true
}
})
if (!assetType) {
throw new Error(`资产类型 "${equipmentType}" 不存在`)
}
const shortName = normalizeShortName(assetType.short_name)
return shortName || `AT${assetType.id}`
}
/**
* 生成资产编码(纯数字格式)
* 格式: {类型数字}{地址ID(4位)}{序号(5位)}
* 例如: 1000100001 (类型1,地址1,序号00001)
* 如果没有地址,地址ID为0000
*
* 确保唯一性:
* 1. 查找该类型和地址的最大序号,避免并发问题
* 2. 检查生成的编码是否已存在,如果存在则递增序号重试
* 3. 最多重试10次,防止无限循环
* 生成资产编码
* 格式: {类型简称}-{地址ID(4位)}-{序号(5位)}
* 例如: PC-0001-00001
*/
export const generateEquipmentCode = async (
equipmentType: string,
prisma: any,
storageLocationId?: number | null
): Promise<string> => {
const typeNumber = EQUIPMENT_TYPE_NUMBERS[equipmentType] || 0
// 获取地址ID,如果没有地址则为0
const locationId = storageLocationId || 0
// 格式前缀:{类型数字}{地址ID(4位)}
const typeCode = String(typeNumber)
const locationCode = String(locationId).padStart(4, '0')
const prefix = `${typeCode}${locationCode}`
// 查找该类型和地址的所有资产,提取最大序号
// 使用这种方式比count更安全,可以处理并发情况
const typeCode = await resolveTypeShortName(equipmentType, prisma)
const locationCode = String(storageLocationId || 0).padStart(4, '0')
const prefix = `${typeCode}-${locationCode}-`
const existingEquipment = await prisma.equipment.findMany({
where: {
equipment_type: equipmentType,
storage_location_id: storageLocationId || null,
equipment_code: {
startsWith: prefix
}
@ -49,58 +49,42 @@ export const generateEquipmentCode = async (
orderBy: {
equipment_code: 'desc'
},
take: 1 // 只取最大的一条
take: 1
})
// 计算起始序号
let startSequence = 1
if (existingEquipment.length > 0) {
// 从最大编码中提取序号
const maxCode = existingEquipment[0].equipment_code
if (maxCode.startsWith(prefix)) {
const sequencePart = maxCode.substring(prefix.length)
const maxSequence = parseInt(sequencePart, 10)
if (!isNaN(maxSequence)) {
if (!Number.isNaN(maxSequence)) {
startSequence = maxSequence + 1
}
}
}
// 检查序号是否溢出(5位数字最大99999)
if (startSequence > 99999) {
throw new Error(`资产编码序号已溢出,该类型和地址的资产数量已超过99999`)
throw new Error(`资产编码序号已溢出,该类型和地址的资产数量已超过 99999`)
}
// 生成编码并检查唯一性,最多重试10次
const maxRetries = 10
const maxRetries = 20
for (let attempt = 0; attempt < maxRetries; attempt++) {
const sequence = String(startSequence + attempt).padStart(5, '0')
const code = `${prefix}${sequence}`
// 检查编码是否已存在
const existing = await prisma.equipment.findUnique({
where: { equipment_code: code }
})
if (!existing) {
// 编码唯一,返回
return code
}
// 如果编码已存在,继续尝试下一个序号
console.warn(`资产编码 ${code} 已存在,尝试下一个序号`)
}
// 如果重试10次后仍然冲突,抛出错误
throw new Error(`无法生成唯一的资产编码,已重试${maxRetries}次`)
throw new Error(`无法生成唯一的资产编码,已重试 ${maxRetries} 次`)
}
/**
* 验证资产编码格式(纯数字)
*/
export const validateEquipmentCode = (code: string): boolean => {
// 纯数字格式,长度10位:{类型1位}{地址4位}{序号5位}
const pattern = /^\d{10}$/
return pattern.test(code)
return EQUIPMENT_CODE_PATTERN.test(code.trim().toUpperCase())
}

View File

@ -9,18 +9,22 @@
"version": "0.0.0",
"dependencies": {
"@heroicons/vue": "^2.2.0",
"@types/qrcode": "^1.5.6",
"axios": "^1.13.2",
"echarts": "^6.0.0",
"html2canvas": "^1.4.1",
"jsbarcode": "^3.12.1",
"keycloak-js": "^23.0.7",
"pinia": "^3.0.4",
"qrcode": "^1.5.4",
"vue": "^3.5.24",
"vue-router": "^4.6.3"
"vue-router": "^4.6.3",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@tailwindcss/forms": "^0.5.10",
"@types/node": "^24.10.0",
"@types/xlsx": "^0.0.35",
"@vitejs/plugin-vue": "^6.0.1",
"@vue/tsconfig": "^0.8.1",
"autoprefixer": "^10.4.22",
@ -985,12 +989,27 @@
"version": "24.10.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz",
"integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~7.16.0"
}
},
"node_modules/@types/qrcode": {
"version": "1.5.6",
"resolved": "https://registry.npmmirror.com/@types/qrcode/-/qrcode-1.5.6.tgz",
"integrity": "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/xlsx": {
"version": "0.0.35",
"resolved": "https://registry.npmmirror.com/@types/xlsx/-/xlsx-0.0.35.tgz",
"integrity": "sha512-s0x3DYHZzOkxtjqOk/Nv1ezGzpbN7I8WX+lzlV/nFfTDOv7x4d8ZwGHcnaiB8UCx89omPsftQhS5II3jeWePxQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@vitejs/plugin-vue": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.1.tgz",
@ -1213,6 +1232,15 @@
}
}
},
"node_modules/adler-32": {
"version": "1.3.1",
"resolved": "https://registry.npmmirror.com/adler-32/-/adler-32-1.3.1.tgz",
"integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/alien-signals": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.1.0.tgz",
@ -1453,6 +1481,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.8.25",
"caniuse-lite": "^1.0.30001754",
@ -1480,6 +1509,15 @@
"node": ">= 0.4"
}
},
"node_modules/camelcase": {
"version": "5.3.1",
"resolved": "https://registry.npmmirror.com/camelcase/-/camelcase-5.3.1.tgz",
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/camelcase-css": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
@ -1511,6 +1549,19 @@
],
"license": "CC-BY-4.0"
},
"node_modules/cfb": {
"version": "1.2.2",
"resolved": "https://registry.npmmirror.com/cfb/-/cfb-1.2.2.tgz",
"integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
"license": "Apache-2.0",
"dependencies": {
"adler-32": "~1.3.0",
"crc-32": "~1.2.0"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
@ -1549,11 +1600,100 @@
"node": ">= 6"
}
},
"node_modules/cliui": {
"version": "6.0.0",
"resolved": "https://registry.npmmirror.com/cliui/-/cliui-6.0.0.tgz",
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
"license": "ISC",
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.0",
"wrap-ansi": "^6.2.0"
}
},
"node_modules/cliui/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/cliui/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/cliui/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"license": "MIT"
},
"node_modules/cliui/node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/cliui/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/cliui/node_modules/wrap-ansi": {
"version": "6.2.0",
"resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/codepage": {
"version": "1.15.0",
"resolved": "https://registry.npmmirror.com/codepage/-/codepage-1.15.0.tgz",
"integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
@ -1566,7 +1706,6 @@
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true,
"license": "MIT"
},
"node_modules/combined-stream": {
@ -1606,6 +1745,18 @@
"url": "https://github.com/sponsors/mesqueeb"
}
},
"node_modules/crc-32": {
"version": "1.2.2",
"resolved": "https://registry.npmmirror.com/crc-32/-/crc-32-1.2.2.tgz",
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
"license": "Apache-2.0",
"bin": {
"crc32": "bin/crc32.njs"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@ -1649,6 +1800,15 @@
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"license": "MIT"
},
"node_modules/decamelize": {
"version": "1.2.0",
"resolved": "https://registry.npmmirror.com/decamelize/-/decamelize-1.2.0.tgz",
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@ -1658,18 +1818,6 @@
"node": ">=0.4.0"
}
},
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"dev": true,
"license": "Apache-2.0",
"optional": true,
"peer": true,
"engines": {
"node": ">=8"
}
},
"node_modules/didyoumean": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
@ -1677,6 +1825,12 @@
"dev": true,
"license": "Apache-2.0"
},
"node_modules/dijkstrajs": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
"license": "MIT"
},
"node_modules/dlv": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
@ -1915,6 +2069,19 @@
"node": ">=8"
}
},
"node_modules/find-up": {
"version": "4.1.0",
"resolved": "https://registry.npmmirror.com/find-up/-/find-up-4.1.0.tgz",
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
"license": "MIT",
"dependencies": {
"locate-path": "^5.0.0",
"path-exists": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
@ -1968,6 +2135,15 @@
"node": ">= 6"
}
},
"node_modules/frac": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/frac/-/frac-1.1.2.tgz",
"integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/fraction.js": {
"version": "5.3.4",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
@ -2006,6 +2182,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmmirror.com/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"license": "ISC",
"engines": {
"node": "6.* || 8.* || >= 10.*"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
@ -2190,7 +2375,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@ -2254,18 +2438,6 @@
"@pkgjs/parseargs": "^0.11.0"
}
},
"node_modules/jiti": {
"version": "2.6.1",
"resolved": "https://registry.npmmirror.com/jiti/-/jiti-2.6.1.tgz",
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"bin": {
"jiti": "lib/jiti-cli.mjs"
}
},
"node_modules/js-sha256": {
"version": "0.10.1",
"resolved": "https://registry.npmmirror.com/js-sha256/-/js-sha256-0.10.1.tgz",
@ -2298,280 +2470,6 @@
"jwt-decode": "^4.0.0"
}
},
"node_modules/lightningcss": {
"version": "1.30.2",
"resolved": "https://registry.npmmirror.com/lightningcss/-/lightningcss-1.30.2.tgz",
"integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==",
"dev": true,
"license": "MPL-2.0",
"optional": true,
"peer": true,
"dependencies": {
"detect-libc": "^2.0.3"
},
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
},
"optionalDependencies": {
"lightningcss-android-arm64": "1.30.2",
"lightningcss-darwin-arm64": "1.30.2",
"lightningcss-darwin-x64": "1.30.2",
"lightningcss-freebsd-x64": "1.30.2",
"lightningcss-linux-arm-gnueabihf": "1.30.2",
"lightningcss-linux-arm64-gnu": "1.30.2",
"lightningcss-linux-arm64-musl": "1.30.2",
"lightningcss-linux-x64-gnu": "1.30.2",
"lightningcss-linux-x64-musl": "1.30.2",
"lightningcss-win32-arm64-msvc": "1.30.2",
"lightningcss-win32-x64-msvc": "1.30.2"
}
},
"node_modules/lightningcss-android-arm64": {
"version": "1.30.2",
"resolved": "https://registry.npmmirror.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz",
"integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"android"
],
"peer": true,
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-darwin-arm64": {
"version": "1.30.2",
"resolved": "https://registry.npmmirror.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz",
"integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"darwin"
],
"peer": true,
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-darwin-x64": {
"version": "1.30.2",
"resolved": "https://registry.npmmirror.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz",
"integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"darwin"
],
"peer": true,
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-freebsd-x64": {
"version": "1.30.2",
"resolved": "https://registry.npmmirror.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz",
"integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"freebsd"
],
"peer": true,
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-arm-gnueabihf": {
"version": "1.30.2",
"resolved": "https://registry.npmmirror.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz",
"integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==",
"cpu": [
"arm"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-arm64-gnu": {
"version": "1.30.2",
"resolved": "https://registry.npmmirror.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz",
"integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-arm64-musl": {
"version": "1.30.2",
"resolved": "https://registry.npmmirror.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz",
"integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-x64-gnu": {
"version": "1.30.2",
"resolved": "https://registry.npmmirror.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz",
"integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==",
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-x64-musl": {
"version": "1.30.2",
"resolved": "https://registry.npmmirror.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz",
"integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-win32-arm64-msvc": {
"version": "1.30.2",
"resolved": "https://registry.npmmirror.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz",
"integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"win32"
],
"peer": true,
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-win32-x64-msvc": {
"version": "1.30.2",
"resolved": "https://registry.npmmirror.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz",
"integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"win32"
],
"peer": true,
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lilconfig": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
@ -2592,6 +2490,18 @@
"dev": true,
"license": "MIT"
},
"node_modules/locate-path": {
"version": "5.0.0",
"resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-5.0.0.tgz",
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
"license": "MIT",
"dependencies": {
"p-locate": "^4.1.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/lru-cache": {
"version": "10.4.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
@ -2801,6 +2711,42 @@
"node": ">= 6"
}
},
"node_modules/p-limit": {
"version": "2.3.0",
"resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-2.3.0.tgz",
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
"license": "MIT",
"dependencies": {
"p-try": "^2.0.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/p-locate": {
"version": "4.1.0",
"resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-4.1.0.tgz",
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
"license": "MIT",
"dependencies": {
"p-limit": "^2.2.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/p-try": {
"version": "2.2.0",
"resolved": "https://registry.npmmirror.com/p-try/-/p-try-2.2.0.tgz",
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/package-json-from-dist": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
@ -2815,6 +2761,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz",
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
@ -2867,6 +2822,7 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@ -2915,6 +2871,15 @@
"node": ">= 6"
}
},
"node_modules/pngjs": {
"version": "5.0.0",
"resolved": "https://registry.npmmirror.com/pngjs/-/pngjs-5.0.0.tgz",
"integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
"license": "MIT",
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/postcss": {
"version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
@ -2934,6 +2899,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@ -3083,6 +3049,23 @@
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/qrcode": {
"version": "1.5.4",
"resolved": "https://registry.npmmirror.com/qrcode/-/qrcode-1.5.4.tgz",
"integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
"license": "MIT",
"dependencies": {
"dijkstrajs": "^1.0.1",
"pngjs": "^5.0.0",
"yargs": "^15.3.1"
},
"bin": {
"qrcode": "bin/qrcode"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@ -3140,6 +3123,21 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmmirror.com/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/require-main-filename": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/require-main-filename/-/require-main-filename-2.0.0.tgz",
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
"license": "ISC"
},
"node_modules/resolve": {
"version": "1.22.11",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
@ -3244,6 +3242,12 @@
"queue-microtask": "^1.2.2"
}
},
"node_modules/set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
"license": "ISC"
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@ -3298,6 +3302,18 @@
"node": ">=0.10.0"
}
},
"node_modules/ssf": {
"version": "0.11.2",
"resolved": "https://registry.npmmirror.com/ssf/-/ssf-0.11.2.tgz",
"integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
"license": "Apache-2.0",
"dependencies": {
"frac": "~1.1.2"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/string-width": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
@ -3456,6 +3472,7 @@
"integrity": "sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
"arg": "^5.0.2",
@ -3579,6 +3596,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"devOptional": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@ -3591,7 +3609,6 @@
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
"dev": true,
"license": "MIT"
},
"node_modules/update-browserslist-db": {
@ -3647,6 +3664,7 @@
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
@ -3728,6 +3746,7 @@
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.24.tgz",
"integrity": "sha512-uTHDOpVQTMjcGgrqFPSb8iO2m1DUvo+WbGqoXQz8Y1CeBYQ0FXf2z1gLRaBtHjlRz7zZUBHxjVB5VTLzYkvftg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/compiler-dom": "3.5.24",
"@vue/compiler-sfc": "3.5.24",
@ -3798,6 +3817,30 @@
"node": ">= 8"
}
},
"node_modules/which-module": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/which-module/-/which-module-2.0.1.tgz",
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
"license": "ISC"
},
"node_modules/wmf": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/wmf/-/wmf-1.0.2.tgz",
"integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/word": {
"version": "0.3.0",
"resolved": "https://registry.npmmirror.com/word/-/word-0.3.0.tgz",
"integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/wrap-ansi": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
@ -3896,6 +3939,109 @@
"node": ">=8"
}
},
"node_modules/xlsx": {
"version": "0.18.5",
"resolved": "https://registry.npmmirror.com/xlsx/-/xlsx-0.18.5.tgz",
"integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
"license": "Apache-2.0",
"dependencies": {
"adler-32": "~1.3.0",
"cfb": "~1.2.1",
"codepage": "~1.15.0",
"crc-32": "~1.2.1",
"ssf": "~0.11.2",
"wmf": "~1.0.1",
"word": "~0.3.0"
},
"bin": {
"xlsx": "bin/xlsx.njs"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/y18n": {
"version": "4.0.3",
"resolved": "https://registry.npmmirror.com/y18n/-/y18n-4.0.3.tgz",
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
"license": "ISC"
},
"node_modules/yargs": {
"version": "15.4.1",
"resolved": "https://registry.npmmirror.com/yargs/-/yargs-15.4.1.tgz",
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
"license": "MIT",
"dependencies": {
"cliui": "^6.0.0",
"decamelize": "^1.2.0",
"find-up": "^4.1.0",
"get-caller-file": "^2.0.1",
"require-directory": "^2.1.1",
"require-main-filename": "^2.0.0",
"set-blocking": "^2.0.0",
"string-width": "^4.2.0",
"which-module": "^2.0.0",
"y18n": "^4.0.0",
"yargs-parser": "^18.1.2"
},
"engines": {
"node": ">=8"
}
},
"node_modules/yargs-parser": {
"version": "18.1.3",
"resolved": "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-18.1.3.tgz",
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
"license": "ISC",
"dependencies": {
"camelcase": "^5.0.0",
"decamelize": "^1.2.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/yargs/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/yargs/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"license": "MIT"
},
"node_modules/yargs/node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/yargs/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/zrender": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/zrender/-/zrender-6.0.0.tgz",

View File

@ -68,8 +68,11 @@ export const scanBorrowBatch = async (data: {
export const returnEquipment = async (data: {
equipment_id: number
equipment_code: string
person_id?: number
borrow_record_id?: number
return_condition?: string
notes?: string
confirm_children_diff?: boolean
}) => {
const response = await api.post<ApiResponse<BorrowRecord>>('/return', data)
return response.data
@ -82,6 +85,7 @@ export const scanReturn = async (data: {
notes?: string
person_id?: number
borrow_record_id?: number
confirm_children_diff?: boolean
}) => {
const response = await api.post<
ApiResponse<{
@ -91,6 +95,11 @@ export const scanReturn = async (data: {
return_time: string
status: string
person_id?: number
need_confirm?: boolean
children_diff?: {
removed: Array<Record<string, any>>
added: Array<Record<string, any>>
}
}>
>('/return/scan', data)
return response.data
@ -143,4 +152,3 @@ export const getBorrowRecordList = async (params: {
})
return response.data
}

View File

@ -19,12 +19,6 @@ const bootstrap = async () => {
const authMode = import.meta.env.VITE_AUTH_MODE || 'keycloak'
if (authMode === 'local') {
localStorage.removeItem('token')
authStore.$patch({
token: null,
user: null,
isAuthenticated: false
})
try {
const resp = await api.get('/auth/user')
if (resp?.data?.data) {
@ -39,9 +33,21 @@ const bootstrap = async () => {
}
authStore.setUser(normalizedUser)
authStore.setToken('session')
} else {
authStore.$patch({
token: null,
user: null,
isAuthenticated: false
})
localStorage.removeItem('token')
}
} catch {
// ignore, user not logged in
authStore.$patch({
token: null,
user: null,
isAuthenticated: false
})
localStorage.removeItem('token')
}
app.use(router)

View File

@ -92,7 +92,8 @@ router.beforeEach((to, _from, next) => {
if (authMode === 'local') {
next('/login')
} else {
keycloak.login()
keycloak.login()
next(false)
}
return
}
@ -106,4 +107,3 @@ router.beforeEach((to, _from, next) => {
})
export default router

View File

@ -4,7 +4,7 @@ import type { SiteConfigResponse } from '@/api/settings'
const DEFAULT_SITE_NAME = '固定资产管理系统'
const DEFAULT_THEME_COLOR = '#409EFF'
const DEFAULT_LOGO_URL = 'https://s3.1080p.hk/shuzhou/2025/05/18/8ebadf0a7cc34eaea00e8619b8efb827.png'
const DEFAULT_LOGO_URL = ''
const normalizeHex = (color: string) => {
if (!color) return null
@ -139,4 +139,3 @@ export const useSiteStore = defineStore('site', {
}
}
})

View File

@ -154,6 +154,7 @@ export interface BorrowRecord {
status: '已借出' | '已归还' | '逾期'
return_condition?: '正常' | '损坏' | '丢失'
notes?: string
children_snapshot?: string
created_at: string
updated_at: string
}
@ -228,4 +229,3 @@ export interface ValueStatistics {
available_value: number
}>
}

View File

@ -228,6 +228,7 @@ const personKeyword = ref('')
const selectedPersonId = ref<number | null>(null)
const persons = ref<Person[]>([])
let searchTimer: number | null = null
let latestPersonRequestId = 0
const canSubmit = computed(() => {
const hasEquipment = Boolean(equipmentInfo.value)
@ -273,6 +274,13 @@ const handleScan = async () => {
successInfo.value = null
mountedChildren.value = []
mountedChildrenValue.value = 0
selectedPersonId.value = null
personKeyword.value = ''
borrowForm.value = {
expected_return_time: '',
notes: '',
is_long_term: false
}
playBeepSound()
// 获取已挂载的子资产
@ -336,11 +344,20 @@ const handleSubmit = async () => {
} else if (equipmentInfo.value.remaining_quantity === 0) {
equipmentInfo.value.status = '已借出'
}
equipmentInfo.value = null
mountedChildren.value = []
mountedChildrenValue.value = 0
selectedPersonId.value = null
personKeyword.value = ''
equipmentConfirmed.value = false
borrowForm.value.notes = ''
if (!borrowForm.value.is_long_term) {
borrowForm.value.expected_return_time = ''
borrowForm.value = {
expected_return_time: '',
notes: '',
is_long_term: false
}
nextTick(() => {
scanInputRef.value?.focus()
})
}
} catch (error: any) {
alert(error.message || '出库失败')
@ -350,6 +367,7 @@ const handleSubmit = async () => {
}
const fetchPersons = async (keyword = '') => {
const requestId = ++latestPersonRequestId
try {
const response = await getPersonList({
page: 1,
@ -357,7 +375,7 @@ const fetchPersons = async (keyword = '') => {
keyword: keyword || undefined,
status: 'active'
})
if (response.data) {
if (response.data && requestId === latestPersonRequestId) {
persons.value = response.data.list
if (selectedPersonId.value) {
const exists = persons.value.some((person) => person.id === selectedPersonId.value)
@ -412,4 +430,3 @@ onUnmounted(() => {
}
})
</script>

View File

@ -348,13 +348,40 @@ const handleSubmit = async () => {
loading.value = true
try {
await scanReturn({
let confirmChildrenDiff = false
if (hasChildrenDiff.value) {
confirmChildrenDiff = window.confirm('检测到配件与借出时不一致,确认继续入库吗?')
if (!confirmChildrenDiff) {
loading.value = false
return
}
}
const response = await scanReturn({
equipment_code: borrowRecordInfo.value.equipment_code,
return_condition: returnInfo.value.return_condition,
notes: returnInfo.value.notes || undefined,
person_id: record.person_id,
borrow_record_id: record.id
borrow_record_id: record.id,
confirm_children_diff: confirmChildrenDiff
})
if (response.data?.need_confirm) {
const confirmed = window.confirm('后端检测到配件与借出时不一致,确认继续入库吗?')
if (!confirmed) {
loading.value = false
return
}
await scanReturn({
equipment_code: borrowRecordInfo.value.equipment_code,
return_condition: returnInfo.value.return_condition,
notes: returnInfo.value.notes || undefined,
person_id: record.person_id,
borrow_record_id: record.id,
confirm_children_diff: true
})
}
alert('入库成功!')
router.push('/borrow')
@ -371,4 +398,3 @@ onMounted(() => {
})
})
</script>

View File

@ -212,11 +212,11 @@ const updateCategoryChart = () => {
radius: ['45%', '70%'],
avoidLabelOverlap: false,
data: categories.length
? categories.map((item) => ({
? categories.map((item, index) => ({
value: item.total_quantity,
name: item.equipment_type,
itemStyle: {
color: categoryColorPalette[item.equipment_type] || undefined
color: getCategoryColor(index)
}
}))
: [{ value: 1, name: '暂无数据', itemStyle: { color: '#d1d1d6' } }],
@ -234,13 +234,18 @@ const updateCategoryChart = () => {
})
}
const categoryColorPalette: Record<string, string> = {
相机: '#0071e3',
镜头: '#34c759',
三脚架: '#ff9500',
闪光灯: '#af52de',
摄影配饰品: '#ff2d55'
}
const categoryColorPalette = [
'#0071e3',
'#34c759',
'#ff9500',
'#af52de',
'#ff2d55',
'#5ac8fa',
'#ff3b30',
'#30b0c7'
]
const getCategoryColor = (index: number) => categoryColorPalette[index % categoryColorPalette.length]
const getStatusClass = (status: string) => {
const classes: Record<string, string> = {
@ -279,4 +284,3 @@ onUnmounted(() => {
opacity: 0;
}
</style>

View File

@ -32,16 +32,16 @@
v-model.trim="createForm.name"
class="input-apple"
type="text"
placeholder="例如:相机、镜头"
placeholder="例如:办公电脑、显示器"
/>
</div>
<div>
<label class="block text-apple-body text-system-label-primary mb-2">简称(用于标签编码)</label>
<label class="block text-apple-body text-system-label-primary mb-2">简称(用于自动编码) *</label>
<input
v-model.trim="createForm.short_name"
class="input-apple"
type="text"
placeholder="例如:CAM、LENS"
placeholder="例如:PC、MON"
maxlength="20"
/>
</div>
@ -57,6 +57,9 @@
</div>
</div>
<p class="text-apple-footnote text-system-label-tertiary mt-2">
简称将用于自动生成资产编码,例如 `PC-0001-00001`。
</p>
<p class="text-apple-footnote text-system-label-tertiary mt-1">
父资产(如主机)可挂载子资产;子资产(如显卡、内存)可被挂载到父资产上
</p>
<button
@ -161,7 +164,7 @@
</div>
<div>
<label class="block text-apple-body text-system-label-primary mb-2">简称(用于标签编码)</label>
<input v-model.trim="editDraft.short_name" class="input-apple" type="text" placeholder="例如:CAM、LENS" maxlength="20" />
<input v-model.trim="editDraft.short_name" class="input-apple" type="text" placeholder="例如:PC、MON" maxlength="20" />
</div>
</div>
<div>
@ -208,6 +211,8 @@ const loading = ref(false)
const createLoading = ref(false)
const updateLoading = ref(false)
const normalizeShortName = (value: string) => value.trim().toUpperCase().replace(/[^A-Z0-9]/g, '')
const filters = reactive({
keyword: '',
status: ''
@ -232,7 +237,11 @@ const editDraft = reactive({
can_be_child: false
})
const canCreate = computed(() => createForm.name.trim().length > 0)
const canCreate = computed(() => {
const nameOk = createForm.name.trim().length > 0
const shortName = normalizeShortName(createForm.short_name)
return nameOk && shortName.length >= 2 && shortName.length <= 20
})
const resetCreateForm = () => {
createForm.name = ''
@ -264,9 +273,10 @@ const handleCreate = async () => {
if (!canCreate.value) return
createLoading.value = true
try {
const shortName = normalizeShortName(createForm.short_name)
await createAssetType({
name: createForm.name,
short_name: createForm.short_name || undefined,
short_name: shortName,
can_be_parent: createForm.can_be_parent,
can_be_child: createForm.can_be_child
})
@ -297,9 +307,10 @@ const submitEdit = async () => {
if (!editingType.value) return
updateLoading.value = true
try {
const shortName = normalizeShortName(editDraft.short_name)
await updateAssetType(editDraft.id, {
name: editDraft.name,
short_name: editDraft.short_name || undefined,
short_name: shortName || undefined,
status: editDraft.status,
can_be_parent: editDraft.can_be_parent,
can_be_child: editDraft.can_be_child