工资发放
This commit is contained in:
parent
5e51b460f4
commit
710fe5406d
@ -15,13 +15,13 @@ import {
|
||||
pageManualSalary,
|
||||
} from '#/api/legacy/operate';
|
||||
import AccountInput from '#/components/account-input.vue';
|
||||
import AdminConfigTable from '#/views/_shared/admin-config-table.vue';
|
||||
import { getAllowedSysOrigins } from '#/views/system/shared';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Modal,
|
||||
Pagination,
|
||||
Select,
|
||||
Space,
|
||||
Table,
|
||||
@ -33,6 +33,12 @@ import UserProfileLink from './components/user-profile-link.vue';
|
||||
|
||||
defineOptions({ name: 'OperateManualSalaryPayment' });
|
||||
|
||||
type SalarySortKey =
|
||||
| 'currentSalaryBalance'
|
||||
| 'payableSalary'
|
||||
| 'totalIssuedSalary';
|
||||
type SalarySortOrder = '' | 'ascend' | 'descend';
|
||||
|
||||
const accessStore = useAccessStore();
|
||||
const sysOriginOptions = computed(() => {
|
||||
const options = getAllowedSysOrigins(accessStore.accessCodes || []);
|
||||
@ -41,6 +47,7 @@ const sysOriginOptions = computed(() => {
|
||||
|
||||
const loading = ref(false);
|
||||
const paying = ref(false);
|
||||
const settlingUserId = ref('');
|
||||
const total = ref(0);
|
||||
const list = ref<Array<Record<string, any>>>([]);
|
||||
const countries = ref<Array<Record<string, any>>>([]);
|
||||
@ -48,11 +55,15 @@ const countryKeyword = ref('');
|
||||
const resultOpen = ref(false);
|
||||
const resultRows = ref<Array<Record<string, any>>>([]);
|
||||
const selectedUserIds = ref<string[]>([]);
|
||||
const salarySortKey = ref<'' | SalarySortKey>('');
|
||||
const salarySortOrder = ref<SalarySortOrder>('');
|
||||
|
||||
const query = reactive({
|
||||
countryCode: '',
|
||||
cursor: 1,
|
||||
limit: 20,
|
||||
sortField: '',
|
||||
sortOrder: '' as SalarySortOrder,
|
||||
sysOrigin: '',
|
||||
userId: '',
|
||||
});
|
||||
@ -63,9 +74,11 @@ const columns = [
|
||||
{ dataIndex: 'lastSettlementTarget', key: 'lastSettlementTarget', title: '上次结算的积分', width: 150 },
|
||||
{ dataIndex: 'currentTarget', key: 'currentTarget', title: '本次积分', width: 130 },
|
||||
{ dataIndex: 'policyLevel', key: 'policyLevel', title: '本次积分档位', width: 130 },
|
||||
{ dataIndex: 'payableSalary', key: 'payableSalary', title: '应发工资', width: 120 },
|
||||
{ dataIndex: 'totalIssuedSalary', key: 'totalIssuedSalary', title: '总发放工资', width: 130 },
|
||||
{ dataIndex: 'currentSalaryBalance', key: 'currentSalaryBalance', title: '当前工资余额', width: 140 },
|
||||
{ dataIndex: 'expectedSalary', key: 'expectedSalary', title: '本月应发', width: 120 },
|
||||
{ dataIndex: 'payableSalary', key: 'payableSalary', title: '应发(应发-实收)', width: 160 },
|
||||
{ dataIndex: 'totalIssuedSalary', key: 'totalIssuedSalary', title: '总发工资', width: 130 },
|
||||
{ dataIndex: 'currentSalaryBalance', key: 'currentSalaryBalance', title: '当前工资', width: 140 },
|
||||
{ dataIndex: 'action', fixed: 'right', key: 'action', title: '操作', width: 110 },
|
||||
];
|
||||
|
||||
const resultColumns = [
|
||||
@ -77,6 +90,28 @@ const resultColumns = [
|
||||
{ dataIndex: 'currentTarget', key: 'currentTarget', title: '本次积分', width: 120 },
|
||||
];
|
||||
|
||||
const salarySortableKeys = new Set<SalarySortKey>([
|
||||
'payableSalary',
|
||||
'totalIssuedSalary',
|
||||
'currentSalaryBalance',
|
||||
]);
|
||||
|
||||
const hasMore = computed(() => list.value.length < total.value);
|
||||
|
||||
const tableRows = computed(() =>
|
||||
list.value.map((item) => ({
|
||||
...item,
|
||||
rowKey: getRowKey(item),
|
||||
})),
|
||||
);
|
||||
|
||||
const summaryText = computed(() => {
|
||||
if (total.value <= 0) {
|
||||
return list.value.length > 0 ? `已加载 ${list.value.length} 条` : '';
|
||||
}
|
||||
return `已加载 ${list.value.length}/${total.value} 条`;
|
||||
});
|
||||
|
||||
const selectedUserCount = computed(() => selectedUserIds.value.length);
|
||||
|
||||
const confirmButtonText = computed(() =>
|
||||
@ -137,7 +172,7 @@ async function loadCountries() {
|
||||
countries.value = (await getCountryAlls()) || [];
|
||||
}
|
||||
|
||||
async function loadData(reset = false) {
|
||||
async function loadData(reset = false, append = false) {
|
||||
if (!query.sysOrigin) {
|
||||
return;
|
||||
}
|
||||
@ -147,22 +182,34 @@ async function loadData(reset = false) {
|
||||
}
|
||||
if (reset) {
|
||||
query.cursor = 1;
|
||||
list.value = [];
|
||||
total.value = 0;
|
||||
selectedUserIds.value = [];
|
||||
}
|
||||
loading.value = true;
|
||||
try {
|
||||
const result = await pageManualSalary({ ...query });
|
||||
list.value = result.records || [];
|
||||
const records = result.records || [];
|
||||
list.value = append ? [...list.value, ...records] : records;
|
||||
total.value = Number(result.total || 0);
|
||||
sortLoadedRows();
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handlePageChange(page: number, pageSize: number) {
|
||||
query.cursor = page;
|
||||
query.limit = pageSize;
|
||||
void loadData();
|
||||
async function loadNextPage() {
|
||||
if (loading.value || !hasMore.value) {
|
||||
return;
|
||||
}
|
||||
const previousCursor = query.cursor;
|
||||
query.cursor = previousCursor + 1;
|
||||
try {
|
||||
await loadData(false, true);
|
||||
} catch (error) {
|
||||
query.cursor = previousCursor;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function formatMoney(value?: number | string) {
|
||||
@ -192,6 +239,74 @@ function getRowKey(record: Record<string, any>) {
|
||||
return String(record.userId || record.id || '');
|
||||
}
|
||||
|
||||
function getSalarySortValue(
|
||||
record: Record<string, any>,
|
||||
key: SalarySortKey,
|
||||
) {
|
||||
const value = Number(record?.[key] || 0);
|
||||
return Number.isFinite(value) ? value : 0;
|
||||
}
|
||||
|
||||
function isSalarySortableColumn(key?: number | string) {
|
||||
return salarySortableKeys.has(String(key || '') as SalarySortKey);
|
||||
}
|
||||
|
||||
function salarySortIndicator(key?: number | string) {
|
||||
if (salarySortKey.value !== String(key || '') || !salarySortOrder.value) {
|
||||
return '排序';
|
||||
}
|
||||
return salarySortOrder.value === 'descend' ? '降序' : '升序';
|
||||
}
|
||||
|
||||
function sortLoadedRows() {
|
||||
if (!salarySortKey.value || !salarySortOrder.value) {
|
||||
return;
|
||||
}
|
||||
const sortKey = salarySortKey.value;
|
||||
const direction = salarySortOrder.value === 'ascend' ? 1 : -1;
|
||||
list.value = [...list.value].sort((left, right) => {
|
||||
const diff =
|
||||
getSalarySortValue(left, sortKey) - getSalarySortValue(right, sortKey);
|
||||
if (diff !== 0) {
|
||||
return diff * direction;
|
||||
}
|
||||
return getRowKey(left).localeCompare(getRowKey(right));
|
||||
});
|
||||
}
|
||||
|
||||
function toggleSalarySort(key?: number | string) {
|
||||
if (!isSalarySortableColumn(key)) {
|
||||
return;
|
||||
}
|
||||
const nextKey = String(key) as SalarySortKey;
|
||||
if (salarySortKey.value !== nextKey) {
|
||||
salarySortKey.value = nextKey;
|
||||
salarySortOrder.value = 'descend';
|
||||
} else if (salarySortOrder.value === 'descend') {
|
||||
salarySortOrder.value = 'ascend';
|
||||
} else if (salarySortOrder.value === 'ascend') {
|
||||
salarySortKey.value = '';
|
||||
salarySortOrder.value = '';
|
||||
} else {
|
||||
salarySortOrder.value = 'descend';
|
||||
}
|
||||
query.sortField = salarySortKey.value;
|
||||
query.sortOrder = salarySortOrder.value;
|
||||
void loadData(true);
|
||||
}
|
||||
|
||||
async function submitManualSalary(userIds?: string[]) {
|
||||
const hasSelectedUsers = !!userIds?.length;
|
||||
resultRows.value = await confirmManualSalary({
|
||||
countryCode: query.countryCode,
|
||||
sysOrigin: query.sysOrigin,
|
||||
...(hasSelectedUsers ? { userIds } : {}),
|
||||
});
|
||||
resultOpen.value = true;
|
||||
selectedUserIds.value = [];
|
||||
await loadData(true);
|
||||
}
|
||||
|
||||
function confirmPay() {
|
||||
if (!query.countryCode) {
|
||||
message.warning('请选择国家');
|
||||
@ -211,15 +326,8 @@ function confirmPay() {
|
||||
async onOk() {
|
||||
paying.value = true;
|
||||
try {
|
||||
resultRows.value = await confirmManualSalary({
|
||||
countryCode: query.countryCode,
|
||||
sysOrigin: query.sysOrigin,
|
||||
...(hasSelectedUsers ? { userIds } : {}),
|
||||
});
|
||||
resultOpen.value = true;
|
||||
selectedUserIds.value = [];
|
||||
await submitManualSalary(hasSelectedUsers ? userIds : undefined);
|
||||
message.success('发放成功');
|
||||
await loadData();
|
||||
} finally {
|
||||
paying.value = false;
|
||||
}
|
||||
@ -227,13 +335,45 @@ function confirmPay() {
|
||||
});
|
||||
}
|
||||
|
||||
function settleUser(record: Record<string, any>) {
|
||||
if (!query.countryCode) {
|
||||
message.warning('请选择国家');
|
||||
return;
|
||||
}
|
||||
const userId = String(record.userId || '');
|
||||
if (!userId) {
|
||||
message.warning('用户ID为空');
|
||||
return;
|
||||
}
|
||||
const payableSalary = Number(record.payableSalary || 0);
|
||||
Modal.confirm({
|
||||
content: payableSalary < 0
|
||||
? '该用户应发小于实收,本次发放0并置为已结算,不会对用户扣款。'
|
||||
: '本次只结算当前这一行用户的工资。',
|
||||
title: `确认结算用户 ${record.userProfile?.account || userId} 的当前账期工资?`,
|
||||
async onOk() {
|
||||
settlingUserId.value = userId;
|
||||
try {
|
||||
await submitManualSalary([userId]);
|
||||
message.success('结算成功');
|
||||
} finally {
|
||||
settlingUserId.value = '';
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
void loadCountries();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page title="工资发放">
|
||||
<Card>
|
||||
<div class="toolbar">
|
||||
<Page
|
||||
auto-content-height
|
||||
content-class="manual-salary-page-content"
|
||||
title="工资发放"
|
||||
>
|
||||
<div class="manual-salary-page">
|
||||
<Card class="manual-salary-filter-card">
|
||||
<Space wrap>
|
||||
<SysOriginSelect
|
||||
v-model:value="query.sysOrigin"
|
||||
@ -273,17 +413,42 @@ void loadCountries();
|
||||
{{ confirmButtonText }}
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Table
|
||||
<AdminConfigTable
|
||||
auto-height
|
||||
:columns="columns"
|
||||
:data-source="list"
|
||||
:current="query.cursor"
|
||||
:data-source="tableRows"
|
||||
:has-more="hasMore"
|
||||
infinite
|
||||
:loading="loading"
|
||||
:pagination="false"
|
||||
:row-key="getRowKey"
|
||||
:page-size="query.limit"
|
||||
row-key="rowKey"
|
||||
:row-selection="rowSelection"
|
||||
:scroll="{ x: 1400 }"
|
||||
:scroll-x="1620"
|
||||
:show-pager="false"
|
||||
:summary-text="summaryText"
|
||||
:total="total"
|
||||
@load-more="loadNextPage"
|
||||
>
|
||||
<template #headerCell="{ column }">
|
||||
<button
|
||||
v-if="isSalarySortableColumn(column.key)"
|
||||
class="salary-sort-header"
|
||||
:class="{ 'salary-sort-header--active': salarySortKey === column.key }"
|
||||
type="button"
|
||||
@click="toggleSalarySort(column.key)"
|
||||
>
|
||||
<span>{{ column.title }}</span>
|
||||
<span class="salary-sort-state">
|
||||
{{ salarySortIndicator(column.key) }}
|
||||
</span>
|
||||
</button>
|
||||
<template v-else>
|
||||
{{ column.title }}
|
||||
</template>
|
||||
</template>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'userProfile'">
|
||||
<UserProfileLink :profile="record.userProfile" />
|
||||
@ -303,32 +468,45 @@ void loadCountries();
|
||||
</Tag>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'expectedSalary'">
|
||||
{{ formatMoney(record.expectedSalary) }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'payableSalary'">
|
||||
<Space size="small">
|
||||
<span>{{ formatMoney(record.payableSalary) }}</span>
|
||||
<Tag v-if="record.paid" color="green">已发</Tag>
|
||||
<span
|
||||
:class="{ 'payable-salary-negative': Number(record.payableSalary || 0) < 0 }"
|
||||
>
|
||||
{{ formatMoney(record.payableSalary) }}
|
||||
</span>
|
||||
<Tag v-if="record.paid" color="green">已结算</Tag>
|
||||
</Space>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'totalIssuedSalary'">
|
||||
{{ formatMoney(record.totalIssuedSalary) }}
|
||||
<span>{{ formatMoney(record.totalIssuedSalary) }}</span>
|
||||
<span
|
||||
v-if="Number(record.overIssuedSalary || 0) > 0"
|
||||
class="over-issued-salary"
|
||||
>
|
||||
({{ formatMoney(record.overIssuedSalary) }})
|
||||
</span>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'currentSalaryBalance'">
|
||||
{{ formatMoney(record.currentSalaryBalance) }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<Button
|
||||
:disabled="!record.userId || record.paid"
|
||||
:loading="settlingUserId === String(record.userId || '')"
|
||||
size="small"
|
||||
type="primary"
|
||||
@click="settleUser(record)"
|
||||
>
|
||||
结算
|
||||
</Button>
|
||||
</template>
|
||||
</template>
|
||||
</Table>
|
||||
|
||||
<div class="pager">
|
||||
<Pagination
|
||||
:current="query.cursor"
|
||||
:page-size="query.limit"
|
||||
:total="total"
|
||||
show-size-changer
|
||||
@change="handlePageChange"
|
||||
@showSizeChange="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</AdminConfigTable>
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
v-model:open="resultOpen"
|
||||
@ -367,13 +545,55 @@ void loadCountries();
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.toolbar {
|
||||
margin-bottom: 16px;
|
||||
.manual-salary-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.pager {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 16px;
|
||||
.manual-salary-filter-card {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.salary-sort-header {
|
||||
align-items: center;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
font: inherit;
|
||||
gap: 6px;
|
||||
max-width: 100%;
|
||||
padding: 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.salary-sort-header:hover,
|
||||
.salary-sort-header--active {
|
||||
color: rgb(22 119 255);
|
||||
}
|
||||
|
||||
.salary-sort-state {
|
||||
color: rgb(102 112 133);
|
||||
flex: 0 0 auto;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.salary-sort-header--active .salary-sort-state {
|
||||
color: rgb(22 119 255);
|
||||
}
|
||||
|
||||
.over-issued-salary {
|
||||
color: rgb(217 45 32);
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.payable-salary-negative {
|
||||
color: rgb(217 45 32);
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user