修改代码

This commit is contained in:
lzm
2026-04-17 18:17:42 +08:00
parent a770170871
commit 8f9205a6a1
20 changed files with 5205 additions and 6 deletions

View File

@@ -0,0 +1,821 @@
<template>
<ContentWrap>
<el-form
ref="queryFormRef"
:inline="true"
:model="queryParams"
class="-mb-15px"
label-width="88px"
>
<el-form-item label="工程名称" prop="projectName">
<el-input
v-model="queryParams.projectName"
class="!w-240px"
clearable
placeholder="请输入工程名称"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="开始年度" prop="projectStartYear">
<el-date-picker
v-model="queryProjectStartYearValue"
class="!w-180px"
clearable
placeholder="请选择年度"
type="year"
value-format="YYYY"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery">
<Icon class="mr-5px" icon="ep:search" />
搜索
</el-button>
<el-button @click="resetQuery">
<Icon class="mr-5px" icon="ep:refresh" />
重置
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<ContentWrap>
<el-table
ref="projectTableRef"
v-loading="loading"
:data="projectList"
highlight-current-row
@current-change="handleCurrentProjectChange"
>
<el-table-column align="center" label="项目 ID" prop="id" width="88" />
<el-table-column align="center" label="工程名称" min-width="220" prop="projectName" />
<el-table-column align="center" label="项目经理" min-width="120" prop="projectManagerName" />
<el-table-column align="center" label="工程负责人" min-width="120" prop="engineeringPrincipalName" />
<el-table-column align="center" label="项目开始年度" prop="projectStartYear" width="120" />
</el-table>
<Pagination
v-model:limit="queryParams.pageSize"
v-model:page="queryParams.pageNo"
:total="total"
@pagination="getProjectList"
/>
</ContentWrap>
<el-row :gutter="16">
<el-col :span="8">
<ContentWrap>
<div class="mb-12px flex items-center justify-between">
<div class="text-14px font-600">
{{ currentProject?.projectName || '专业所规划列表' }}
</div>
<el-button v-if="currentProject" size="small" @click="getPlanningList">刷新</el-button>
</div>
<el-table
ref="planningTableRef"
v-loading="planningLoading"
:data="planningList"
highlight-current-row
@current-change="handleCurrentPlanningChange"
>
<el-table-column align="center" label="规划内容" min-width="180" prop="planningContent" />
<el-table-column align="center" label="开始年度" prop="planningStartYear" width="100" />
<el-table-column align="center" label="考核产值(元)" width="120">
<template #default="scope">
{{ formatAmountText(scope.row.assessmentOutputValue) }}
</template>
</el-table-column>
</el-table>
</ContentWrap>
</el-col>
<el-col :span="16">
<ContentWrap v-if="currentPlanning && currentSpecialtyGroup">
<div class="mb-16px flex items-center justify-between gap-12px">
<div>
<div class="text-16px font-600">{{ currentPlanning.planningContent }}</div>
</div>
<div class="flex items-center gap-12px">
<el-button
v-hasPermi="['tjt:specialty-role-split:update']"
plain
type="primary"
@click="openEditDialog"
>
<Icon class="mr-5px" icon="ep:edit" />
编辑人员分配
</el-button>
<el-button @click="refreshCurrentPlanning">
<Icon class="mr-5px" icon="ep:refresh" />
刷新结果
</el-button>
</div>
</div>
<div class="mb-16px flex flex-wrap items-start justify-between gap-12px">
<el-radio-group v-model="selectedSpecialtyCode" size="small">
<el-radio-button
v-for="item in specialtyTabOptions"
:key="item.value"
:label="item.value"
>
{{ item.label }}
</el-radio-button>
</el-radio-group>
</div>
<el-row :gutter="16" class="mb-16px">
<el-col v-for="item in currentSpecialtySummaryCards" :key="item.label" :span="6">
<div class="rounded-8px bg-[var(--el-fill-color-light)] px-16px py-12px">
<div class="text-12px text-[var(--el-text-color-secondary)]">
{{ item.label }}
</div>
<div class="mt-6px text-18px font-600">
{{ item.value }}
</div>
</div>
</el-col>
</el-row>
<el-table :data="currentSpecialtyGroup.rows" border>
<el-table-column align="center" label="角色" min-width="110" prop="roleName" />
<el-table-column align="center" label="角色比例" min-width="120">
<template #default="scope">
{{ formatPercentText(scope.row.roleRatio) }}
</template>
</el-table-column>
<el-table-column align="center" label="角色金额(元)" min-width="130">
<template #default="scope">
{{ formatAmountText(scope.row.roleAmount) }}
</template>
</el-table-column>
<el-table-column align="center" label="人员合计" min-width="120">
<template #default="scope">
{{ formatPercentText(getPersonTotalRatio(scope.row)) }}
</template>
</el-table-column>
<el-table-column align="center" label="人员信息" min-width="360">
<template #default="scope">
<div v-if="scope.row.persons?.length" class="person-display-list">
<div
v-for="(person, index) in scope.row.persons"
:key="`${scope.row.specialtyCode}-${scope.row.roleCode}-display-${index}`"
class="person-display-row"
>
<span class="person-display-name">{{ person.personName || '-' }}</span>
<span>{{ formatPercentText(person.personRatio) }}</span>
<span>{{ formatAmountText(person.personAmount) }} </span>
</div>
</div>
<div v-else class="person-empty">暂无人员</div>
</template>
</el-table-column>
</el-table>
</ContentWrap>
<ContentWrap v-else>
<el-empty description="请选择专业所规划后查看页面5结果" />
</ContentWrap>
</el-col>
</el-row>
<Dialog v-model="dialogVisible" title="编辑专业人员角色分配" width="1180">
<template v-if="currentEditSpecialtyGroup">
<div class="mb-16px flex flex-wrap items-start justify-between gap-12px">
<el-radio-group v-model="editSpecialtyCode" size="small">
<el-radio-button
v-for="item in specialtyTabOptions"
:key="`edit-${item.value}`"
:label="item.value"
>
{{ item.label }}
</el-radio-button>
</el-radio-group>
</div>
<el-row :gutter="16" class="mb-16px">
<el-col v-for="item in currentEditSpecialtySummaryCards" :key="item.label" :span="6">
<div class="rounded-8px bg-[var(--el-fill-color-light)] px-16px py-12px">
<div class="text-12px text-[var(--el-text-color-secondary)]">
{{ item.label }}
</div>
<div class="mt-6px text-18px font-600">
{{ item.value }}
</div>
</div>
</el-col>
</el-row>
<el-table :data="currentEditSpecialtyGroup.rows" border>
<el-table-column align="center" label="角色" min-width="110" prop="roleName" />
<el-table-column align="center" label="角色比例" min-width="120">
<template #default="scope">
<el-input-number
:model-value="toPercentValue(scope.row.roleRatio)"
:max="100"
:min="0"
:precision="2"
:step="0.1"
class="!w-1/1"
controls-position="right"
@update:model-value="(value) => updateRoleRatio(scope.row, value)"
/>
</template>
</el-table-column>
<el-table-column align="center" label="角色金额()" min-width="130">
<template #default="scope">
{{ formatAmountText(scope.row.roleAmount) }}
</template>
</el-table-column>
<el-table-column align="center" label="人员合计" min-width="120">
<template #default="scope">
<span :class="getPersonTotalClass(scope.row)">
{{ formatPercentText(getPersonTotalRatio(scope.row)) }}
</span>
</template>
</el-table-column>
<el-table-column label="人员配置" min-width="520">
<template #default="scope">
<div class="person-editor">
<div v-if="!scope.row.persons?.length" class="person-empty">
暂无人员,请点击“添加人员”
</div>
<div
v-for="(person, index) in scope.row.persons"
:key="`${scope.row.specialtyCode}-${scope.row.roleCode}-${index}`"
class="person-row"
>
<el-input
v-model="person.personName"
clearable
maxlength="64"
placeholder="请输入人员名称"
/>
<el-input-number
:model-value="toPercentValue(person.personRatio)"
:max="getPersonRatioMax(scope.row, person)"
:min="0"
:precision="2"
:step="0.1"
class="!w-1/1"
controls-position="right"
placeholder="比例(%)"
@update:model-value="(value) => updatePersonRatio(scope.row, person, value)"
/>
<div class="person-amount">{{ formatAmountText(person.personAmount) }} 元</div>
<el-button text type="danger" @click="removePerson(scope.row, index)">
删除
</el-button>
</div>
<div class="person-toolbar">
<el-button text type="primary" @click="addPerson(scope.row)">
<Icon class="mr-5px" icon="ep:plus" />
添加人员
</el-button>
</div>
</div>
</template>
</el-table-column>
</el-table>
</template>
<template #footer>
<el-button :loading="saveLoading" type="primary" @click="handleSave">保存</el-button>
<el-button @click="dialogVisible = false">取消</el-button>
</template>
</Dialog>
</template>
<script lang="ts" setup>
import * as ProjectApi from '@/api/tjt/project'
import * as PlanningApi from '@/api/tjt/planning'
import * as SpecialtyRoleSplitApi from '@/api/tjt/specialtyRoleSplit'
import {
formatAmountText,
formatPercentText,
fromPercentValue,
isMajorOwnership,
OUTPUT_SPLIT_SPECIALTY_OPTIONS,
toPercentValue
} from '@/views/tjt/shared/planning'
defineOptions({ name: 'TjtStaffAssignment' })
const DESIGN_ROLE_CODE = 'design'
const EPSILON = 0.0001
type SpecialtyRolePersonVO = SpecialtyRoleSplitApi.SpecialtyRolePersonVO
type SpecialtyRoleSplitVO = SpecialtyRoleSplitApi.SpecialtyRoleSplitVO
interface SpecialtyGroup {
specialtyCode: string
specialtyName: string
specialtyAmount: number
rows: SpecialtyRoleSplitVO[]
roleTotal: number
designRatio: number
personCount: number
}
const message = useMessage()
const loading = ref(false)
const planningLoading = ref(false)
const saveLoading = ref(false)
const total = ref(0)
const projectList = ref<ProjectApi.ProjectVO[]>([])
const planningList = ref<PlanningApi.ProjectPlanningVO[]>([])
const currentProject = ref<ProjectApi.ProjectVO>()
const currentPlanning = ref<PlanningApi.ProjectPlanningVO>()
const roleList = ref<SpecialtyRoleSplitVO[]>([])
const editRoleList = ref<SpecialtyRoleSplitVO[]>([])
const selectedSpecialtyCode = ref('')
const editSpecialtyCode = ref('')
const dialogVisible = ref(false)
const queryFormRef = ref()
const projectTableRef = ref()
const planningTableRef = ref()
const queryParams = reactive<ProjectApi.ProjectPageReqVO>({
pageNo: 1,
pageSize: 10,
projectName: undefined,
projectStartYear: undefined
})
const queryProjectStartYearValue = computed({
get: () => (queryParams.projectStartYear ? String(queryParams.projectStartYear) : undefined),
set: (value?: string) => {
queryParams.projectStartYear = value ? Number(value) : undefined
}
})
const roundRatio = (value?: number | string | null) => {
const numericValue = Number(value || 0)
if (Number.isNaN(numericValue)) {
return 0
}
return Number(numericValue.toFixed(4))
}
const roundAmount = (value?: number | string | null) => {
const numericValue = Number(value || 0)
if (Number.isNaN(numericValue)) {
return 0
}
return Number(numericValue.toFixed(2))
}
const multiplyAmount = (amount?: number | string | null, ratio?: number | string | null) => {
return roundAmount(Number(amount || 0) * Number(ratio || 0))
}
const normalizePersonName = (value?: string) => value?.trim() || ''
const buildEditablePerson = (person?: SpecialtyRolePersonVO): SpecialtyRolePersonVO => ({
personName: person?.personName || '',
personRatio:
person?.personRatio === undefined || person?.personRatio === null
? undefined
: roundRatio(person.personRatio),
personAmount: roundAmount(person?.personAmount)
})
const buildEditableRow = (row: SpecialtyRoleSplitVO): SpecialtyRoleSplitVO => ({
...row,
specialtyAmount: roundAmount(row.specialtyAmount),
roleRatio: roundRatio(row.roleRatio),
roleAmount: roundAmount(row.roleAmount),
persons: (row.persons || []).map((person) => buildEditablePerson(person))
})
const cloneRoleRows = (rows: SpecialtyRoleSplitVO[]) => rows.map((item) => buildEditableRow(item))
const sumPersonRatios = (persons?: SpecialtyRolePersonVO[]) =>
roundRatio((persons || []).reduce((sum, person) => sum + Number(person?.personRatio || 0), 0))
const getPersonTotalRatio = (row: SpecialtyRoleSplitVO) => sumPersonRatios(row.persons)
const getPersonTotalClass = (row: SpecialtyRoleSplitVO) =>
getPersonTotalRatio(row) > 1 + EPSILON ? 'text-[var(--el-color-danger)]' : ''
const getPersonRatioMax = (row: SpecialtyRoleSplitVO, currentPerson: SpecialtyRolePersonVO) => {
const otherTotalRatio = roundRatio(
(row.persons || []).reduce((sum, person) => {
if (person === currentPerson) {
return sum
}
return sum + Number(person?.personRatio || 0)
}, 0)
)
const currentPercent = toPercentValue(currentPerson.personRatio) ?? 0
const remainPercent = toPercentValue(Math.max(0, 1 - otherTotalRatio)) ?? 0
return Math.min(100, Math.max(currentPercent, remainPercent))
}
const countConfiguredPersons = (persons?: SpecialtyRolePersonVO[]) =>
(persons || []).filter(
(person) =>
normalizePersonName(person.personName) ||
(person.personRatio !== undefined && person.personRatio !== null)
).length
const syncSpecialtyRows = (source: SpecialtyRoleSplitVO[], specialtyCode: string) => {
const rows = source.filter((item) => item.specialtyCode === specialtyCode)
rows.forEach((row) => {
row.persons = (row.persons || []).map((person) => buildEditablePerson(person))
row.roleRatio = roundRatio(row.roleRatio)
row.roleAmount = multiplyAmount(row.specialtyAmount, row.roleRatio)
row.persons = (row.persons || []).map((person) => ({
...person,
personAmount: multiplyAmount(row.roleAmount, person.personRatio)
}))
})
}
const syncAllDerivedValues = (source: SpecialtyRoleSplitVO[]) => {
const specialtyCodeSet = new Set(source.map((item) => item.specialtyCode))
specialtyCodeSet.forEach((specialtyCode) => syncSpecialtyRows(source, specialtyCode))
}
const buildSpecialtyGroups = (source: SpecialtyRoleSplitVO[]): SpecialtyGroup[] => {
const map = new Map<string, SpecialtyRoleSplitVO[]>()
source.forEach((item) => {
const list = map.get(item.specialtyCode) || []
list.push(item)
map.set(item.specialtyCode, list)
})
return OUTPUT_SPLIT_SPECIALTY_OPTIONS.map((option) => {
const rows = map.get(option.value)
if (!rows?.length) {
return undefined
}
const sortedRows = [...rows].sort((a, b) => Number(a.sortNo || 0) - Number(b.sortNo || 0))
const specialtyName = sortedRows[0]?.specialtyName || option.label
const specialtyAmount = roundAmount(sortedRows[0]?.specialtyAmount)
const roleTotal = roundRatio(
sortedRows.reduce((sum, item) => sum + Number(item.roleRatio || 0), 0)
)
const designRow = sortedRows.find((item) => item.roleCode === DESIGN_ROLE_CODE)
const personCount = sortedRows.reduce(
(sum, row) => sum + countConfiguredPersons(row.persons),
0
)
return {
specialtyCode: option.value,
specialtyName,
specialtyAmount,
rows: sortedRows,
roleTotal,
designRatio: roundRatio(designRow?.roleRatio),
personCount
}
}).filter((item): item is SpecialtyGroup => !!item)
}
const specialtyGroups = computed(() => buildSpecialtyGroups(roleList.value))
const editSpecialtyGroups = computed(() => buildSpecialtyGroups(editRoleList.value))
const specialtyTabOptions = computed(() =>
specialtyGroups.value.map((item) => ({
label: item.specialtyName,
value: item.specialtyCode
}))
)
const currentSpecialtyGroup = computed(
() =>
specialtyGroups.value.find((item) => item.specialtyCode === selectedSpecialtyCode.value) ||
specialtyGroups.value[0]
)
const currentEditSpecialtyGroup = computed(
() =>
editSpecialtyGroups.value.find((item) => item.specialtyCode === editSpecialtyCode.value) ||
editSpecialtyGroups.value[0]
)
const buildSpecialtySummaryCards = (group?: SpecialtyGroup) => {
if (!group) {
return []
}
return [
{ label: '专业金额', value: `${formatAmountText(group.specialtyAmount)} 元` },
{ label: '角色合计', value: formatPercentText(group.roleTotal) },
{ label: '设计比例', value: formatPercentText(group.designRatio) },
{ label: '已配置人数', value: `${group.personCount} 人` }
]
}
const currentSpecialtySummaryCards = computed(() =>
buildSpecialtySummaryCards(currentSpecialtyGroup.value)
)
const currentEditSpecialtySummaryCards = computed(() =>
buildSpecialtySummaryCards(currentEditSpecialtyGroup.value)
)
const ensureSelectedCode = (codes: string[], target: { value: string }) => {
if (!codes.length) {
target.value = ''
return
}
if (!codes.includes(target.value)) {
target.value = codes[0]
}
}
watch(
specialtyGroups,
(groups) => {
ensureSelectedCode(
groups.map((item) => item.specialtyCode),
selectedSpecialtyCode
)
},
{ immediate: true }
)
watch(
editSpecialtyGroups,
(groups) => {
ensureSelectedCode(
groups.map((item) => item.specialtyCode),
editSpecialtyCode
)
},
{ immediate: true }
)
const addPerson = (row: SpecialtyRoleSplitVO) => {
if (!row.persons) {
row.persons = []
}
row.persons.push(buildEditablePerson())
syncSpecialtyRows(editRoleList.value, row.specialtyCode)
}
const removePerson = (row: SpecialtyRoleSplitVO, index: number) => {
row.persons?.splice(index, 1)
syncSpecialtyRows(editRoleList.value, row.specialtyCode)
}
const updatePersonRatio = (
row: SpecialtyRoleSplitVO,
person: SpecialtyRolePersonVO,
value?: number
) => {
const maxValue = getPersonRatioMax(row, person)
const nextValue =
value === undefined || value === null ? undefined : Math.min(Math.max(value, 0), maxValue)
person.personRatio = fromPercentValue(nextValue) ?? undefined
syncSpecialtyRows(editRoleList.value, row.specialtyCode)
}
const updateRoleRatio = (row: SpecialtyRoleSplitVO, value?: number) => {
row.roleRatio = fromPercentValue(value) ?? 0
syncSpecialtyRows(editRoleList.value, row.specialtyCode)
}
const buildSavePersons = (
row: SpecialtyRoleSplitVO
):
| { persons: SpecialtyRolePersonVO[]; error?: never }
| { persons?: never; error: string } => {
const persons: SpecialtyRolePersonVO[] = []
for (const person of row.persons || []) {
const personName = normalizePersonName(person.personName)
const hasName = !!personName
const hasRatio = person.personRatio !== undefined && person.personRatio !== null
if (!hasName && !hasRatio) {
continue
}
if (!hasName || !hasRatio) {
return {
error: `${row.specialtyName || row.specialtyCode}-${row.roleName || row.roleCode}的人员名称和比例必须同时填写`
}
}
persons.push({
personName,
personRatio: roundRatio(person.personRatio)
})
}
return { persons }
}
const validateAndBuildSaveItems = () => {
const items: SpecialtyRoleSplitApi.SpecialtyRoleSplitSaveItemVO[] = []
for (const group of editSpecialtyGroups.value) {
let roleTotal = 0
for (const row of group.rows) {
const result = buildSavePersons(row)
if (result.error) {
message.warning(result.error)
return undefined
}
const persons = result.persons || []
const personTotalRatio = sumPersonRatios(persons)
if (personTotalRatio > 1 + EPSILON) {
message.warning(
`${row.specialtyName || row.specialtyCode}-${row.roleName || row.roleCode}的人员比例合计不能大于 100%`
)
return undefined
}
if ((row.roleCode === DESIGN_ROLE_CODE && Number(row.roleAmount || 0) > EPSILON) && persons.length === 0) {
message.warning(`${group.specialtyName}的设计角色金额大于 0 时必须至少配置 1 个人员`)
return undefined
}
roleTotal = roundRatio(roleTotal + Number(row.roleRatio || 0))
items.push({
specialtyCode: row.specialtyCode,
roleCode: row.roleCode,
roleRatio: roundRatio(row.roleRatio),
persons
})
}
if (Math.abs(roleTotal - 1) > EPSILON) {
message.warning(`${group.specialtyName}五类角色比例合计必须等于 100%`)
return undefined
}
}
return items
}
const getProjectList = async () => {
loading.value = true
try {
const data = await ProjectApi.getProjectPage(queryParams)
projectList.value = data.list
total.value = data.total
if (!projectList.value.length) {
currentProject.value = undefined
planningList.value = []
currentPlanning.value = undefined
roleList.value = []
editRoleList.value = []
return
}
const targetProjectId = currentProject.value?.id || projectList.value[0].id
const targetProject =
projectList.value.find((item) => item.id === targetProjectId) || projectList.value[0]
await nextTick()
projectTableRef.value?.setCurrentRow(targetProject)
} finally {
loading.value = false
}
}
const getPlanningList = async () => {
if (!currentProject.value?.id) {
planningList.value = []
currentPlanning.value = undefined
roleList.value = []
editRoleList.value = []
return
}
planningLoading.value = true
try {
const list = await PlanningApi.getProjectPlanningListByProjectId(currentProject.value.id)
planningList.value = list.filter((item) => isMajorOwnership(item.ownershipType))
if (!planningList.value.length) {
currentPlanning.value = undefined
roleList.value = []
editRoleList.value = []
return
}
const targetPlanningId = currentPlanning.value?.id || planningList.value[0].id
const targetPlanning =
planningList.value.find((item) => item.id === targetPlanningId) || planningList.value[0]
await nextTick()
planningTableRef.value?.setCurrentRow(targetPlanning)
} finally {
planningLoading.value = false
}
}
const loadRoleList = async (planningId: number) => {
const data = await SpecialtyRoleSplitApi.getSpecialtyRoleSplitListByPlanningId(planningId)
roleList.value = cloneRoleRows(data)
syncAllDerivedValues(roleList.value)
}
const handleQuery = () => {
queryParams.pageNo = 1
getProjectList()
}
const resetQuery = () => {
queryFormRef.value?.resetFields()
handleQuery()
}
const handleCurrentProjectChange = async (row?: ProjectApi.ProjectVO) => {
currentProject.value = row || undefined
await getPlanningList()
}
const handleCurrentPlanningChange = async (row?: PlanningApi.ProjectPlanningVO) => {
currentPlanning.value = row || undefined
if (!row?.id) {
roleList.value = []
editRoleList.value = []
return
}
await loadRoleList(row.id)
}
const refreshCurrentPlanning = async () => {
if (!currentPlanning.value?.id) {
return
}
await loadRoleList(currentPlanning.value.id)
}
const openEditDialog = () => {
if (!roleList.value.length) {
return
}
editRoleList.value = cloneRoleRows(roleList.value)
syncAllDerivedValues(editRoleList.value)
editSpecialtyCode.value = selectedSpecialtyCode.value || specialtyTabOptions.value[0]?.value || ''
dialogVisible.value = true
}
const handleSave = async () => {
if (!currentPlanning.value?.id) {
return
}
const items = validateAndBuildSaveItems()
if (!items) {
return
}
saveLoading.value = true
try {
await SpecialtyRoleSplitApi.saveSpecialtyRoleSplitBatch({
planningId: currentPlanning.value.id,
items
})
message.success('保存成功')
dialogVisible.value = false
await loadRoleList(currentPlanning.value.id)
} finally {
saveLoading.value = false
}
}
onMounted(() => {
getProjectList()
})
onActivated(() => {
getProjectList()
})
</script>
<style lang="scss" scoped>
.person-empty {
font-size: 12px;
color: var(--el-text-color-secondary);
}
.person-display-list,
.person-editor {
display: flex;
flex-direction: column;
gap: 8px;
}
.person-display-row,
.person-row {
display: grid;
align-items: center;
gap: 8px;
}
.person-display-row {
grid-template-columns: minmax(120px, 1fr) 100px 120px;
font-size: 13px;
}
.person-display-name {
font-weight: 600;
}
.person-row {
grid-template-columns: minmax(160px, 1fr) 180px 120px 64px;
}
.person-amount {
color: var(--el-text-color-regular);
text-align: right;
white-space: nowrap;
}
.person-toolbar {
display: flex;
justify-content: flex-start;
}
@media (max-width: 1500px) {
.person-row {
grid-template-columns: minmax(140px, 1fr) 160px 110px 64px;
}
}
</style>