增加工资发放
This commit is contained in:
parent
c7f7a7047f
commit
5e51b460f4
@ -138,6 +138,22 @@ export async function pageSalary(params: Record<string, any>) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function pageManualSalary(params: Record<string, any>) {
|
||||||
|
return requestClient.get<LegacyPageResult<Record<string, any>>>(
|
||||||
|
'/team/salary/manual/page',
|
||||||
|
{
|
||||||
|
params,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function confirmManualSalary(data: Record<string, any>) {
|
||||||
|
return requestClient.post<Array<Record<string, any>>>(
|
||||||
|
'/team/salary/manual/pay',
|
||||||
|
data,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export async function pageCpApply(params: Record<string, any>) {
|
export async function pageCpApply(params: Record<string, any>) {
|
||||||
return requestClient.get<LegacyPageResult<Record<string, any>>>(
|
return requestClient.get<LegacyPageResult<Record<string, any>>>(
|
||||||
'/user-cp-apply/page',
|
'/user-cp-apply/page',
|
||||||
|
|||||||
@ -25,6 +25,13 @@ const routes: RouteRecordRaw[] = [
|
|||||||
component: () => import('#/views/operate/auto-salary-pay-record.vue'),
|
component: () => import('#/views/operate/auto-salary-pay-record.vue'),
|
||||||
meta: { title: '自动发放工资凭据' },
|
meta: { title: '自动发放工资凭据' },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'ManualSalaryPayment',
|
||||||
|
path: 'manual-salary-payment',
|
||||||
|
alias: '/manual-salary-payment',
|
||||||
|
component: () => import('#/views/operate/manual-salary-payment.vue'),
|
||||||
|
meta: { title: '工资发放' },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'UserBankWithdrawMoneyApply',
|
name: 'UserBankWithdrawMoneyApply',
|
||||||
path: 'withdraw-money-apply-page',
|
path: 'withdraw-money-apply-page',
|
||||||
|
|||||||
379
apps/src/views/operate/manual-salary-payment.vue
Normal file
379
apps/src/views/operate/manual-salary-payment.vue
Normal file
@ -0,0 +1,379 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import {
|
||||||
|
computed,
|
||||||
|
reactive,
|
||||||
|
ref,
|
||||||
|
watch,
|
||||||
|
} from 'vue';
|
||||||
|
|
||||||
|
import { Page } from '@vben/common-ui';
|
||||||
|
import { useAccessStore } from '@vben/stores';
|
||||||
|
|
||||||
|
import { getCountryAlls } from '#/api/legacy/system';
|
||||||
|
import {
|
||||||
|
confirmManualSalary,
|
||||||
|
pageManualSalary,
|
||||||
|
} from '#/api/legacy/operate';
|
||||||
|
import AccountInput from '#/components/account-input.vue';
|
||||||
|
import { getAllowedSysOrigins } from '#/views/system/shared';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Modal,
|
||||||
|
Pagination,
|
||||||
|
Select,
|
||||||
|
Space,
|
||||||
|
Table,
|
||||||
|
Tag,
|
||||||
|
message,
|
||||||
|
} from 'antdv-next';
|
||||||
|
|
||||||
|
import UserProfileLink from './components/user-profile-link.vue';
|
||||||
|
|
||||||
|
defineOptions({ name: 'OperateManualSalaryPayment' });
|
||||||
|
|
||||||
|
const accessStore = useAccessStore();
|
||||||
|
const sysOriginOptions = computed(() => {
|
||||||
|
const options = getAllowedSysOrigins(accessStore.accessCodes || []);
|
||||||
|
return options.length > 0 ? options : getAllowedSysOrigins([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const paying = ref(false);
|
||||||
|
const total = ref(0);
|
||||||
|
const list = ref<Array<Record<string, any>>>([]);
|
||||||
|
const countries = ref<Array<Record<string, any>>>([]);
|
||||||
|
const countryKeyword = ref('');
|
||||||
|
const resultOpen = ref(false);
|
||||||
|
const resultRows = ref<Array<Record<string, any>>>([]);
|
||||||
|
const selectedUserIds = ref<string[]>([]);
|
||||||
|
|
||||||
|
const query = reactive({
|
||||||
|
countryCode: '',
|
||||||
|
cursor: 1,
|
||||||
|
limit: 20,
|
||||||
|
sysOrigin: '',
|
||||||
|
userId: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ dataIndex: 'userProfile', key: 'userProfile', title: '用户信息', width: 300 },
|
||||||
|
{ dataIndex: 'countryCode', key: 'countryCode', title: '国家', width: 90 },
|
||||||
|
{ 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 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const resultColumns = [
|
||||||
|
{ dataIndex: 'userProfile', key: 'userProfile', title: '用户信息', width: 280 },
|
||||||
|
{ dataIndex: 'countryCode', key: 'countryCode', title: '国家', width: 90 },
|
||||||
|
{ dataIndex: 'salaryChange', key: 'salaryChange', title: '工资变动', width: 180 },
|
||||||
|
{ dataIndex: 'issuedSalary', key: 'issuedSalary', title: '发放工资', width: 120 },
|
||||||
|
{ dataIndex: 'policyLevel', key: 'policyLevel', title: '档位', width: 90 },
|
||||||
|
{ dataIndex: 'currentTarget', key: 'currentTarget', title: '本次积分', width: 120 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const selectedUserCount = computed(() => selectedUserIds.value.length);
|
||||||
|
|
||||||
|
const confirmButtonText = computed(() =>
|
||||||
|
selectedUserCount.value > 0
|
||||||
|
? `确认发放(${selectedUserCount.value}人)`
|
||||||
|
: '确认发放',
|
||||||
|
);
|
||||||
|
|
||||||
|
const rowSelection = computed(() => ({
|
||||||
|
getCheckboxProps: (record: Record<string, any>) => ({
|
||||||
|
disabled: !record.userId,
|
||||||
|
}),
|
||||||
|
onChange: (keys: Array<number | string>) => {
|
||||||
|
selectedUserIds.value = keys.map(String).filter(Boolean);
|
||||||
|
},
|
||||||
|
preserveSelectedRowKeys: true,
|
||||||
|
selectedRowKeys: selectedUserIds.value,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const filteredCountries = computed(() => {
|
||||||
|
const keyword = countryKeyword.value.trim().toLowerCase();
|
||||||
|
if (!keyword) {
|
||||||
|
return countries.value;
|
||||||
|
}
|
||||||
|
return countries.value.filter((item) =>
|
||||||
|
[
|
||||||
|
item.phonePrefix,
|
||||||
|
item.countryName,
|
||||||
|
item.aliasName,
|
||||||
|
item.alphaTwo,
|
||||||
|
item.alphaThree,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ')
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(keyword),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
sysOriginOptions,
|
||||||
|
(options) => {
|
||||||
|
if (!query.sysOrigin && options.length > 0) {
|
||||||
|
query.sysOrigin = String(options[0]?.value || '');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
[() => query.sysOrigin, () => query.countryCode, () => query.userId],
|
||||||
|
() => {
|
||||||
|
selectedUserIds.value = [];
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
async function loadCountries() {
|
||||||
|
countries.value = (await getCountryAlls()) || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadData(reset = false) {
|
||||||
|
if (!query.sysOrigin) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!query.countryCode) {
|
||||||
|
message.warning('请选择国家');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (reset) {
|
||||||
|
query.cursor = 1;
|
||||||
|
selectedUserIds.value = [];
|
||||||
|
}
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const result = await pageManualSalary({ ...query });
|
||||||
|
list.value = result.records || [];
|
||||||
|
total.value = Number(result.total || 0);
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePageChange(page: number, pageSize: number) {
|
||||||
|
query.cursor = page;
|
||||||
|
query.limit = pageSize;
|
||||||
|
void loadData();
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatMoney(value?: number | string) {
|
||||||
|
const amount = Number(value || 0);
|
||||||
|
if (Number.isNaN(amount)) {
|
||||||
|
return '$0.00';
|
||||||
|
}
|
||||||
|
return `$${amount.toFixed(2)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatInteger(value?: number | string) {
|
||||||
|
if (value === undefined || value === null || value === '') {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
return Number(value).toLocaleString('en-US');
|
||||||
|
}
|
||||||
|
|
||||||
|
function countryLabel(code?: string) {
|
||||||
|
const country = countries.value.find((item) => item.alphaTwo === code);
|
||||||
|
if (!country) {
|
||||||
|
return code || '-';
|
||||||
|
}
|
||||||
|
return `${country.phonePrefix || ''} ${country.countryName || country.aliasName || code}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRowKey(record: Record<string, any>) {
|
||||||
|
return String(record.userId || record.id || '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmPay() {
|
||||||
|
if (!query.countryCode) {
|
||||||
|
message.warning('请选择国家');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userIds = [...selectedUserIds.value];
|
||||||
|
const hasSelectedUsers = userIds.length > 0;
|
||||||
|
|
||||||
|
Modal.confirm({
|
||||||
|
content: hasSelectedUsers
|
||||||
|
? '本次只发放已勾选的用户。'
|
||||||
|
: '未勾选用户,本次将发放当前筛选国家的所有可发工资用户。',
|
||||||
|
title: hasSelectedUsers
|
||||||
|
? `确认发放已选择的 ${userIds.length} 人当前账期工资?`
|
||||||
|
: `确认发放 ${countryLabel(query.countryCode)} 当前账期工资?`,
|
||||||
|
async onOk() {
|
||||||
|
paying.value = true;
|
||||||
|
try {
|
||||||
|
resultRows.value = await confirmManualSalary({
|
||||||
|
countryCode: query.countryCode,
|
||||||
|
sysOrigin: query.sysOrigin,
|
||||||
|
...(hasSelectedUsers ? { userIds } : {}),
|
||||||
|
});
|
||||||
|
resultOpen.value = true;
|
||||||
|
selectedUserIds.value = [];
|
||||||
|
message.success('发放成功');
|
||||||
|
await loadData();
|
||||||
|
} finally {
|
||||||
|
paying.value = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void loadCountries();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page title="工资发放">
|
||||||
|
<Card>
|
||||||
|
<div class="toolbar">
|
||||||
|
<Space wrap>
|
||||||
|
<SysOriginSelect
|
||||||
|
v-model:value="query.sysOrigin"
|
||||||
|
:options="sysOriginOptions"
|
||||||
|
style="width: 140px"
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
v-model:value="query.countryCode"
|
||||||
|
allow-clear
|
||||||
|
show-search
|
||||||
|
:filter-option="false"
|
||||||
|
option-label-prop="label"
|
||||||
|
placeholder="国家"
|
||||||
|
style="width: 240px"
|
||||||
|
:options="filteredCountries.map((item) => ({
|
||||||
|
label: `${item.phonePrefix || ''} ${item.countryName || item.aliasName || ''}`,
|
||||||
|
value: item.alphaTwo as any,
|
||||||
|
}))"
|
||||||
|
@change="loadData(true)"
|
||||||
|
@search="countryKeyword = $event"
|
||||||
|
/>
|
||||||
|
<AccountInput
|
||||||
|
v-model:value="query.userId"
|
||||||
|
:sys-origin="query.sysOrigin"
|
||||||
|
placeholder="用户ID"
|
||||||
|
style="width: 260px"
|
||||||
|
/>
|
||||||
|
<Button :loading="loading" type="primary" @click="loadData(true)">
|
||||||
|
搜索
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
danger
|
||||||
|
:disabled="!query.countryCode"
|
||||||
|
:loading="paying"
|
||||||
|
@click="confirmPay"
|
||||||
|
>
|
||||||
|
{{ confirmButtonText }}
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Table
|
||||||
|
:columns="columns"
|
||||||
|
:data-source="list"
|
||||||
|
:loading="loading"
|
||||||
|
:pagination="false"
|
||||||
|
:row-key="getRowKey"
|
||||||
|
:row-selection="rowSelection"
|
||||||
|
:scroll="{ x: 1400 }"
|
||||||
|
>
|
||||||
|
<template #bodyCell="{ column, record }">
|
||||||
|
<template v-if="column.key === 'userProfile'">
|
||||||
|
<UserProfileLink :profile="record.userProfile" />
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'countryCode'">
|
||||||
|
{{ record.countryCode || '-' }}
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'lastSettlementTarget'">
|
||||||
|
{{ formatInteger(record.lastSettlementTarget) }}
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'currentTarget'">
|
||||||
|
{{ formatInteger(record.currentTarget) }}
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'policyLevel'">
|
||||||
|
<Tag v-if="record.policyLevel" color="blue">
|
||||||
|
{{ record.policyLevel }}
|
||||||
|
</Tag>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'payableSalary'">
|
||||||
|
<Space size="small">
|
||||||
|
<span>{{ formatMoney(record.payableSalary) }}</span>
|
||||||
|
<Tag v-if="record.paid" color="green">已发</Tag>
|
||||||
|
</Space>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'totalIssuedSalary'">
|
||||||
|
{{ formatMoney(record.totalIssuedSalary) }}
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'currentSalaryBalance'">
|
||||||
|
{{ formatMoney(record.currentSalaryBalance) }}
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
v-model:open="resultOpen"
|
||||||
|
:footer="null"
|
||||||
|
title="发放结果"
|
||||||
|
width="980px"
|
||||||
|
>
|
||||||
|
<Table
|
||||||
|
:columns="resultColumns"
|
||||||
|
:data-source="resultRows"
|
||||||
|
:pagination="false"
|
||||||
|
row-key="id"
|
||||||
|
size="small"
|
||||||
|
:scroll="{ x: 900, y: 520 }"
|
||||||
|
>
|
||||||
|
<template #bodyCell="{ column, record }">
|
||||||
|
<template v-if="column.key === 'userProfile'">
|
||||||
|
<UserProfileLink :profile="record.userProfile" />
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'salaryChange'">
|
||||||
|
{{ formatMoney(record.beforeSalaryBalance) }} -> {{ formatMoney(record.afterSalaryBalance) }}
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'issuedSalary'">
|
||||||
|
{{ formatMoney(record.issuedSalary) }}
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'policyLevel'">
|
||||||
|
<Tag color="blue">{{ record.policyLevel || '-' }}</Tag>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="column.key === 'currentTarget'">
|
||||||
|
{{ formatInteger(record.currentTarget) }}
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</Table>
|
||||||
|
</Modal>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.toolbar {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pager {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
76
scripts/sql/sync_manual_salary_payment_menu.sql
Normal file
76
scripts/sql/sync_manual_salary_payment_menu.sql
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
SET NAMES utf8mb4;
|
||||||
|
|
||||||
|
SET @app_user_manager_id := (
|
||||||
|
SELECT id
|
||||||
|
FROM sys_menu
|
||||||
|
WHERE alias IN ('AppUserManager', 'OperateManager')
|
||||||
|
ORDER BY FIELD(alias, 'AppUserManager', 'OperateManager')
|
||||||
|
LIMIT 1
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO sys_menu (
|
||||||
|
id,
|
||||||
|
parent_id,
|
||||||
|
menu_name,
|
||||||
|
path,
|
||||||
|
menu_type,
|
||||||
|
icon,
|
||||||
|
create_user,
|
||||||
|
update_user,
|
||||||
|
sort,
|
||||||
|
status,
|
||||||
|
router,
|
||||||
|
alias
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
menu_id.next_id,
|
||||||
|
@app_user_manager_id,
|
||||||
|
'工资发放',
|
||||||
|
'operate/manual-salary-payment',
|
||||||
|
2,
|
||||||
|
'form',
|
||||||
|
'0',
|
||||||
|
'0',
|
||||||
|
49,
|
||||||
|
0,
|
||||||
|
'/manual-salary-payment',
|
||||||
|
'ManualSalaryPayment'
|
||||||
|
FROM (
|
||||||
|
SELECT COALESCE(MAX(id), 1700000000) + 1 AS next_id
|
||||||
|
FROM sys_menu
|
||||||
|
) AS menu_id
|
||||||
|
WHERE @app_user_manager_id IS NOT NULL
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM sys_menu WHERE alias = 'ManualSalaryPayment'
|
||||||
|
);
|
||||||
|
|
||||||
|
UPDATE sys_menu
|
||||||
|
SET
|
||||||
|
parent_id = @app_user_manager_id,
|
||||||
|
menu_name = '工资发放',
|
||||||
|
path = 'operate/manual-salary-payment',
|
||||||
|
menu_type = 2,
|
||||||
|
icon = 'form',
|
||||||
|
update_user = '0',
|
||||||
|
sort = 49,
|
||||||
|
status = 0,
|
||||||
|
router = '/manual-salary-payment'
|
||||||
|
WHERE alias = 'ManualSalaryPayment'
|
||||||
|
AND @app_user_manager_id IS NOT NULL;
|
||||||
|
|
||||||
|
INSERT INTO sys_role_menu (role_id, menu_id)
|
||||||
|
SELECT DISTINCT
|
||||||
|
role_menu.role_id,
|
||||||
|
target_menu.id
|
||||||
|
FROM sys_role_menu AS role_menu
|
||||||
|
JOIN sys_menu AS source_menu
|
||||||
|
ON source_menu.id = role_menu.menu_id
|
||||||
|
AND source_menu.alias IN ('AutoSalaryPayRecord', 'AppUserManager', 'OperateManager')
|
||||||
|
JOIN sys_menu AS target_menu
|
||||||
|
ON target_menu.alias = 'ManualSalaryPayment'
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM sys_role_menu AS existing_role_menu
|
||||||
|
WHERE existing_role_menu.role_id = role_menu.role_id
|
||||||
|
AND existing_role_menu.menu_id = target_menu.id
|
||||||
|
);
|
||||||
Loading…
x
Reference in New Issue
Block a user