工资发放
This commit is contained in:
parent
5e51b460f4
commit
710fe5406d
@ -15,13 +15,13 @@ import {
|
|||||||
pageManualSalary,
|
pageManualSalary,
|
||||||
} from '#/api/legacy/operate';
|
} from '#/api/legacy/operate';
|
||||||
import AccountInput from '#/components/account-input.vue';
|
import AccountInput from '#/components/account-input.vue';
|
||||||
|
import AdminConfigTable from '#/views/_shared/admin-config-table.vue';
|
||||||
import { getAllowedSysOrigins } from '#/views/system/shared';
|
import { getAllowedSysOrigins } from '#/views/system/shared';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
Modal,
|
Modal,
|
||||||
Pagination,
|
|
||||||
Select,
|
Select,
|
||||||
Space,
|
Space,
|
||||||
Table,
|
Table,
|
||||||
@ -33,6 +33,12 @@ import UserProfileLink from './components/user-profile-link.vue';
|
|||||||
|
|
||||||
defineOptions({ name: 'OperateManualSalaryPayment' });
|
defineOptions({ name: 'OperateManualSalaryPayment' });
|
||||||
|
|
||||||
|
type SalarySortKey =
|
||||||
|
| 'currentSalaryBalance'
|
||||||
|
| 'payableSalary'
|
||||||
|
| 'totalIssuedSalary';
|
||||||
|
type SalarySortOrder = '' | 'ascend' | 'descend';
|
||||||
|
|
||||||
const accessStore = useAccessStore();
|
const accessStore = useAccessStore();
|
||||||
const sysOriginOptions = computed(() => {
|
const sysOriginOptions = computed(() => {
|
||||||
const options = getAllowedSysOrigins(accessStore.accessCodes || []);
|
const options = getAllowedSysOrigins(accessStore.accessCodes || []);
|
||||||
@ -41,6 +47,7 @@ const sysOriginOptions = computed(() => {
|
|||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const paying = ref(false);
|
const paying = ref(false);
|
||||||
|
const settlingUserId = ref('');
|
||||||
const total = ref(0);
|
const total = ref(0);
|
||||||
const list = ref<Array<Record<string, any>>>([]);
|
const list = ref<Array<Record<string, any>>>([]);
|
||||||
const countries = ref<Array<Record<string, any>>>([]);
|
const countries = ref<Array<Record<string, any>>>([]);
|
||||||
@ -48,11 +55,15 @@ const countryKeyword = ref('');
|
|||||||
const resultOpen = ref(false);
|
const resultOpen = ref(false);
|
||||||
const resultRows = ref<Array<Record<string, any>>>([]);
|
const resultRows = ref<Array<Record<string, any>>>([]);
|
||||||
const selectedUserIds = ref<string[]>([]);
|
const selectedUserIds = ref<string[]>([]);
|
||||||
|
const salarySortKey = ref<'' | SalarySortKey>('');
|
||||||
|
const salarySortOrder = ref<SalarySortOrder>('');
|
||||||
|
|
||||||
const query = reactive({
|
const query = reactive({
|
||||||
countryCode: '',
|
countryCode: '',
|
||||||
cursor: 1,
|
cursor: 1,
|
||||||
limit: 20,
|
limit: 20,
|
||||||
|
sortField: '',
|
||||||
|
sortOrder: '' as SalarySortOrder,
|
||||||
sysOrigin: '',
|
sysOrigin: '',
|
||||||
userId: '',
|
userId: '',
|
||||||
});
|
});
|
||||||
@ -63,9 +74,11 @@ const columns = [
|
|||||||
{ dataIndex: 'lastSettlementTarget', key: 'lastSettlementTarget', title: '上次结算的积分', width: 150 },
|
{ dataIndex: 'lastSettlementTarget', key: 'lastSettlementTarget', title: '上次结算的积分', width: 150 },
|
||||||
{ dataIndex: 'currentTarget', key: 'currentTarget', title: '本次积分', width: 130 },
|
{ dataIndex: 'currentTarget', key: 'currentTarget', title: '本次积分', width: 130 },
|
||||||
{ dataIndex: 'policyLevel', key: 'policyLevel', title: '本次积分档位', width: 130 },
|
{ dataIndex: 'policyLevel', key: 'policyLevel', title: '本次积分档位', width: 130 },
|
||||||
{ dataIndex: 'payableSalary', key: 'payableSalary', title: '应发工资', width: 120 },
|
{ dataIndex: 'expectedSalary', key: 'expectedSalary', title: '本月应发', width: 120 },
|
||||||
{ dataIndex: 'totalIssuedSalary', key: 'totalIssuedSalary', title: '总发放工资', width: 130 },
|
{ dataIndex: 'payableSalary', key: 'payableSalary', title: '应发(应发-实收)', width: 160 },
|
||||||
{ dataIndex: 'currentSalaryBalance', key: 'currentSalaryBalance', title: '当前工资余额', width: 140 },
|
{ 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 = [
|
const resultColumns = [
|
||||||
@ -77,6 +90,28 @@ const resultColumns = [
|
|||||||
{ dataIndex: 'currentTarget', key: 'currentTarget', title: '本次积分', width: 120 },
|
{ 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 selectedUserCount = computed(() => selectedUserIds.value.length);
|
||||||
|
|
||||||
const confirmButtonText = computed(() =>
|
const confirmButtonText = computed(() =>
|
||||||
@ -137,7 +172,7 @@ async function loadCountries() {
|
|||||||
countries.value = (await getCountryAlls()) || [];
|
countries.value = (await getCountryAlls()) || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadData(reset = false) {
|
async function loadData(reset = false, append = false) {
|
||||||
if (!query.sysOrigin) {
|
if (!query.sysOrigin) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -147,22 +182,34 @@ async function loadData(reset = false) {
|
|||||||
}
|
}
|
||||||
if (reset) {
|
if (reset) {
|
||||||
query.cursor = 1;
|
query.cursor = 1;
|
||||||
|
list.value = [];
|
||||||
|
total.value = 0;
|
||||||
selectedUserIds.value = [];
|
selectedUserIds.value = [];
|
||||||
}
|
}
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
const result = await pageManualSalary({ ...query });
|
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);
|
total.value = Number(result.total || 0);
|
||||||
|
sortLoadedRows();
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handlePageChange(page: number, pageSize: number) {
|
async function loadNextPage() {
|
||||||
query.cursor = page;
|
if (loading.value || !hasMore.value) {
|
||||||
query.limit = pageSize;
|
return;
|
||||||
void loadData();
|
}
|
||||||
|
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) {
|
function formatMoney(value?: number | string) {
|
||||||
@ -192,6 +239,74 @@ function getRowKey(record: Record<string, any>) {
|
|||||||
return String(record.userId || record.id || '');
|
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() {
|
function confirmPay() {
|
||||||
if (!query.countryCode) {
|
if (!query.countryCode) {
|
||||||
message.warning('请选择国家');
|
message.warning('请选择国家');
|
||||||
@ -211,15 +326,8 @@ function confirmPay() {
|
|||||||
async onOk() {
|
async onOk() {
|
||||||
paying.value = true;
|
paying.value = true;
|
||||||
try {
|
try {
|
||||||
resultRows.value = await confirmManualSalary({
|
await submitManualSalary(hasSelectedUsers ? userIds : undefined);
|
||||||
countryCode: query.countryCode,
|
|
||||||
sysOrigin: query.sysOrigin,
|
|
||||||
...(hasSelectedUsers ? { userIds } : {}),
|
|
||||||
});
|
|
||||||
resultOpen.value = true;
|
|
||||||
selectedUserIds.value = [];
|
|
||||||
message.success('发放成功');
|
message.success('发放成功');
|
||||||
await loadData();
|
|
||||||
} finally {
|
} finally {
|
||||||
paying.value = false;
|
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();
|
void loadCountries();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Page title="工资发放">
|
<Page
|
||||||
<Card>
|
auto-content-height
|
||||||
<div class="toolbar">
|
content-class="manual-salary-page-content"
|
||||||
|
title="工资发放"
|
||||||
|
>
|
||||||
|
<div class="manual-salary-page">
|
||||||
|
<Card class="manual-salary-filter-card">
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
<SysOriginSelect
|
<SysOriginSelect
|
||||||
v-model:value="query.sysOrigin"
|
v-model:value="query.sysOrigin"
|
||||||
@ -273,17 +413,42 @@ void loadCountries();
|
|||||||
{{ confirmButtonText }}
|
{{ confirmButtonText }}
|
||||||
</Button>
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</Card>
|
||||||
|
|
||||||
<Table
|
<AdminConfigTable
|
||||||
|
auto-height
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
:data-source="list"
|
:current="query.cursor"
|
||||||
|
:data-source="tableRows"
|
||||||
|
:has-more="hasMore"
|
||||||
|
infinite
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:pagination="false"
|
:page-size="query.limit"
|
||||||
:row-key="getRowKey"
|
row-key="rowKey"
|
||||||
:row-selection="rowSelection"
|
: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 #bodyCell="{ column, record }">
|
||||||
<template v-if="column.key === 'userProfile'">
|
<template v-if="column.key === 'userProfile'">
|
||||||
<UserProfileLink :profile="record.userProfile" />
|
<UserProfileLink :profile="record.userProfile" />
|
||||||
@ -303,32 +468,45 @@ void loadCountries();
|
|||||||
</Tag>
|
</Tag>
|
||||||
<span v-else>-</span>
|
<span v-else>-</span>
|
||||||
</template>
|
</template>
|
||||||
|
<template v-else-if="column.key === 'expectedSalary'">
|
||||||
|
{{ formatMoney(record.expectedSalary) }}
|
||||||
|
</template>
|
||||||
<template v-else-if="column.key === 'payableSalary'">
|
<template v-else-if="column.key === 'payableSalary'">
|
||||||
<Space size="small">
|
<Space size="small">
|
||||||
<span>{{ formatMoney(record.payableSalary) }}</span>
|
<span
|
||||||
<Tag v-if="record.paid" color="green">已发</Tag>
|
:class="{ 'payable-salary-negative': Number(record.payableSalary || 0) < 0 }"
|
||||||
|
>
|
||||||
|
{{ formatMoney(record.payableSalary) }}
|
||||||
|
</span>
|
||||||
|
<Tag v-if="record.paid" color="green">已结算</Tag>
|
||||||
</Space>
|
</Space>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="column.key === 'totalIssuedSalary'">
|
<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>
|
||||||
<template v-else-if="column.key === 'currentSalaryBalance'">
|
<template v-else-if="column.key === 'currentSalaryBalance'">
|
||||||
{{ formatMoney(record.currentSalaryBalance) }}
|
{{ formatMoney(record.currentSalaryBalance) }}
|
||||||
</template>
|
</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>
|
</template>
|
||||||
</Table>
|
</AdminConfigTable>
|
||||||
|
</div>
|
||||||
<div class="pager">
|
|
||||||
<Pagination
|
|
||||||
:current="query.cursor"
|
|
||||||
:page-size="query.limit"
|
|
||||||
:total="total"
|
|
||||||
show-size-changer
|
|
||||||
@change="handlePageChange"
|
|
||||||
@showSizeChange="handlePageChange"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
v-model:open="resultOpen"
|
v-model:open="resultOpen"
|
||||||
@ -367,13 +545,55 @@ void loadCountries();
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.toolbar {
|
.manual-salary-page {
|
||||||
margin-bottom: 16px;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pager {
|
.manual-salary-filter-card {
|
||||||
display: flex;
|
flex: 0 0 auto;
|
||||||
justify-content: flex-end;
|
}
|
||||||
margin-top: 16px;
|
|
||||||
|
.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>
|
</style>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user