Compare commits
No commits in common. "1db5607cb8fa55178bcb346e1b1ac83c1507df32" and "bd3dde8bac36d73a772bfc3311ef89080bad2ce8" have entirely different histories.
1db5607cb8
...
bd3dde8bac
20
README.zh-CN.md
Normal file
20
README.zh-CN.md
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
## 安装使用
|
||||||
|
|
||||||
|
2. 安装依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm i -g corepack
|
||||||
|
pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
|
3. 运行
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
4. 打包
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm build
|
||||||
|
```
|
||||||
@ -33,10 +33,6 @@ export async function updateGift(data: Record<string, any>) {
|
|||||||
return requestClient.put('/sys/gift/config', data);
|
return requestClient.put('/sys/gift/config', data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateGiftSort(data: Record<string, any>) {
|
|
||||||
return requestClient.put('/sys/gift/config/sort', data);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function addGift(data: Record<string, any>) {
|
export async function addGift(data: Record<string, any>) {
|
||||||
return requestClient.post('/sys/gift/config', data);
|
return requestClient.post('/sys/gift/config', data);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -37,12 +37,6 @@ export async function pageBank(params: Record<string, any>) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getBankStatistics(params: Record<string, any>) {
|
|
||||||
return requestClient.get<Record<string, any>>('/user-bank-balance/statistics', {
|
|
||||||
params,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function exprotBank(params: Record<string, any>) {
|
export async function exprotBank(params: Record<string, any>) {
|
||||||
return downloadLegacyExcel(
|
return downloadLegacyExcel(
|
||||||
'/user-bank-balance/export',
|
'/user-bank-balance/export',
|
||||||
@ -138,22 +132,6 @@ 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',
|
||||||
|
|||||||
@ -125,12 +125,6 @@ export async function pageResidentWheelDrawRecords(params: Record<string, any>)
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function retryResidentWheelDrawRecord(id: number | string) {
|
|
||||||
return requestClient.post<Record<string, any>>(`${WHEEL_BASE}/record/retry`, {
|
|
||||||
id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getResidentSmashGoldenEggConfig(sysOrigin: string) {
|
export async function getResidentSmashGoldenEggConfig(sysOrigin: string) {
|
||||||
return requestClient.get<Record<string, any>>(`${SMASH_GOLDEN_EGG_BASE}/config`, {
|
return requestClient.get<Record<string, any>>(`${SMASH_GOLDEN_EGG_BASE}/config`, {
|
||||||
params: { sysOrigin },
|
params: { sysOrigin },
|
||||||
|
|||||||
@ -1,13 +1,19 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, watch } from 'vue';
|
import { computed, h, useAttrs } from 'vue';
|
||||||
|
|
||||||
import { useAccessStore } from '@vben/stores';
|
import { useAccessStore } from '@vben/stores';
|
||||||
|
|
||||||
|
import { Select } from 'antdv-next';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
SYS_ORIGIN_OPTIONS,
|
SYS_ORIGIN_OPTIONS,
|
||||||
|
getDisplayableSysOriginOptions,
|
||||||
getDisplayableSysOrigins,
|
getDisplayableSysOrigins,
|
||||||
|
isHiddenSysOrigin,
|
||||||
} from '#/views/system/shared';
|
} from '#/views/system/shared';
|
||||||
|
|
||||||
|
import SysOriginIcon from './sys-origin-icon.vue';
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
inheritAttrs: false,
|
inheritAttrs: false,
|
||||||
name: 'SysOriginSelect',
|
name: 'SysOriginSelect',
|
||||||
@ -32,43 +38,129 @@ const emit = defineEmits<{
|
|||||||
(event: 'update:value', value: SelectValue): void;
|
(event: 'update:value', value: SelectValue): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const attrs = useAttrs();
|
||||||
const accessStore = useAccessStore();
|
const accessStore = useAccessStore();
|
||||||
|
|
||||||
const fallbackOptions = computed(() => {
|
const fallbackOptions = computed(() => {
|
||||||
const options = getDisplayableSysOrigins(accessStore.accessCodes || []);
|
const options = getDisplayableSysOrigins(accessStore.accessCodes || []);
|
||||||
return (options.length > 0 ? options : SYS_ORIGIN_OPTIONS) as AppOriginOption[];
|
return (options.length > 0 ? options : getDisplayableSysOriginOptions(SYS_ORIGIN_OPTIONS)) as AppOriginOption[];
|
||||||
});
|
});
|
||||||
|
|
||||||
const rawOptions = computed(() =>
|
const normalizedOptions = computed(() =>
|
||||||
props.options?.length ? props.options : fallbackOptions.value,
|
getDisplayableSysOriginOptions(props.options?.length ? props.options : fallbackOptions.value).map(
|
||||||
);
|
(item) => {
|
||||||
|
const value = String(item.value ?? '');
|
||||||
const hiddenDefaultValue = computed(() =>
|
const label = String(item.label ?? ('name' in item ? item.name : '') ?? value);
|
||||||
String(rawOptions.value[0]?.value ?? SYS_ORIGIN_OPTIONS[0]?.value ?? 'LIKEI'),
|
return {
|
||||||
);
|
...item,
|
||||||
|
icon: String(item.icon || value).toLowerCase(),
|
||||||
watch(
|
label: label || value,
|
||||||
() => [props.value, hiddenDefaultValue.value] as const,
|
value,
|
||||||
([value, defaultValue]) => {
|
};
|
||||||
if (Array.isArray(value) || value !== undefined && value !== null && value !== '') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!defaultValue) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
emit('update:value', defaultValue);
|
|
||||||
emit('change', defaultValue, rawOptions.value[0] || null);
|
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const optionMap = computed(
|
||||||
|
() => new Map(normalizedOptions.value.map((item) => [String(item.value), item])),
|
||||||
|
);
|
||||||
|
|
||||||
|
const displayValue = computed<SelectValue>(() => {
|
||||||
|
if (Array.isArray(props.value)) {
|
||||||
|
return props.value.filter((item) => !isHiddenSysOrigin(item));
|
||||||
|
}
|
||||||
|
return isHiddenSysOrigin(props.value) ? undefined : props.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectAttrs = computed(() => {
|
||||||
|
const nextAttrs = { ...attrs } as Record<string, any>;
|
||||||
|
delete nextAttrs.options;
|
||||||
|
delete nextAttrs.value;
|
||||||
|
delete nextAttrs.onChange;
|
||||||
|
delete nextAttrs['onUpdate:value'];
|
||||||
|
delete nextAttrs.labelRender;
|
||||||
|
delete nextAttrs.optionRender;
|
||||||
|
delete nextAttrs.optionLabelProp;
|
||||||
|
delete nextAttrs.optionFilterProp;
|
||||||
|
return nextAttrs;
|
||||||
|
});
|
||||||
|
|
||||||
|
function renderContent(option?: Record<string, any> | null) {
|
||||||
|
if (!option || isHiddenSysOrigin(option.value ?? option.label ?? option.name)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const label = String(option.label || option.name || option.value || '-');
|
||||||
|
const code = String(option.icon || option.value || label);
|
||||||
|
return h('span', { class: 'sys-origin-select__content' }, [
|
||||||
|
h(SysOriginIcon, {
|
||||||
|
code,
|
||||||
|
label,
|
||||||
|
size: 18,
|
||||||
|
}),
|
||||||
|
h('span', { class: 'sys-origin-select__text' }, label),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveOption(option?: Record<string, any> | null): null | Record<string, any> {
|
||||||
|
if (!option) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const nextOption = option.option || option.data || option.originOption || option.item;
|
||||||
|
if (nextOption && nextOption !== option) {
|
||||||
|
return resolveOption(nextOption);
|
||||||
|
}
|
||||||
|
if (option.value !== undefined) {
|
||||||
|
return optionMap.value.get(String(option.value)) || option;
|
||||||
|
}
|
||||||
|
return option;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderOption(option: Record<string, any>) {
|
||||||
|
return renderContent(resolveOption(option));
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderLabel(item: Record<string, any>) {
|
||||||
|
if (isHiddenSysOrigin(item?.value ?? item?.label)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return renderContent(
|
||||||
|
resolveOption(item) || {
|
||||||
|
icon: item?.value,
|
||||||
|
label: item?.label || item?.value || '',
|
||||||
|
value: item?.value || '',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleChange(value: SelectValue, option: any) {
|
||||||
|
emit('update:value', value);
|
||||||
|
emit('change', value, option);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<span class="sys-origin-select--hidden" aria-hidden="true" />
|
<Select
|
||||||
|
v-bind="selectAttrs"
|
||||||
|
:value="displayValue"
|
||||||
|
:options="normalizedOptions"
|
||||||
|
option-filter-prop="label"
|
||||||
|
option-label-prop="label"
|
||||||
|
:label-render="renderLabel"
|
||||||
|
:option-render="renderOption"
|
||||||
|
@change="handleChange"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.sys-origin-select--hidden {
|
.sys-origin-select__content {
|
||||||
display: none !important;
|
align-items: center;
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 8px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sys-origin-select__text {
|
||||||
|
line-height: 1;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -25,13 +25,6 @@ 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',
|
||||||
|
|||||||
@ -1,13 +1,5 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import {
|
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue';
|
||||||
computed,
|
|
||||||
defineComponent,
|
|
||||||
nextTick,
|
|
||||||
onBeforeUnmount,
|
|
||||||
onMounted,
|
|
||||||
onUpdated,
|
|
||||||
ref,
|
|
||||||
} from 'vue';
|
|
||||||
|
|
||||||
import { Pagination, Table } from 'antdv-next';
|
import { Pagination, Table } from 'antdv-next';
|
||||||
|
|
||||||
@ -19,59 +11,32 @@ const props = withDefaults(
|
|||||||
columns: Array<Record<string, any>>;
|
columns: Array<Record<string, any>>;
|
||||||
current: number;
|
current: number;
|
||||||
dataSource: Array<Record<string, any>>;
|
dataSource: Array<Record<string, any>>;
|
||||||
hasMore?: boolean;
|
|
||||||
infinite?: boolean;
|
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
loadMoreText?: string;
|
|
||||||
pageSize: number;
|
pageSize: number;
|
||||||
plain?: boolean;
|
plain?: boolean;
|
||||||
rowKey?: string;
|
rowKey?: string;
|
||||||
rowSelection?: Record<string, any>;
|
|
||||||
scrollX?: number | string;
|
scrollX?: number | string;
|
||||||
showPager?: boolean;
|
|
||||||
showSizeChanger?: boolean;
|
showSizeChanger?: boolean;
|
||||||
summaryText?: string;
|
|
||||||
total: number;
|
total: number;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
autoHeight: false,
|
autoHeight: false,
|
||||||
hasMore: false,
|
|
||||||
infinite: false,
|
|
||||||
loadMoreText: '下滑加载下一页',
|
|
||||||
loading: false,
|
loading: false,
|
||||||
plain: false,
|
plain: false,
|
||||||
rowKey: 'id',
|
rowKey: 'id',
|
||||||
scrollX: 1000,
|
scrollX: 1000,
|
||||||
showPager: true,
|
|
||||||
showSizeChanger: true,
|
showSizeChanger: true,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
loadMore: [];
|
|
||||||
pageChange: [page: number, pageSize: number];
|
pageChange: [page: number, pageSize: number];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const RenderNode = defineComponent({
|
|
||||||
name: 'AdminConfigTableRenderNode',
|
|
||||||
props: {
|
|
||||||
node: {
|
|
||||||
default: null,
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
setup(props) {
|
|
||||||
return () => props.node as any;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const tableAreaRef = ref<HTMLDivElement | null>(null);
|
const tableAreaRef = ref<HTMLDivElement | null>(null);
|
||||||
const pagerRef = ref<HTMLDivElement | null>(null);
|
const pagerRef = ref<HTMLDivElement | null>(null);
|
||||||
const summaryRef = ref<HTMLDivElement | null>(null);
|
|
||||||
const tableScrollY = ref(320);
|
const tableScrollY = ref(320);
|
||||||
let tableResizeObserver: null | ResizeObserver = null;
|
let tableResizeObserver: null | ResizeObserver = null;
|
||||||
let tableBodyElement: HTMLElement | null = null;
|
|
||||||
let loadMoreLocked = false;
|
|
||||||
|
|
||||||
const tableScroll = computed(() => {
|
const tableScroll = computed(() => {
|
||||||
const scroll: Record<string, any> = { x: props.scrollX };
|
const scroll: Record<string, any> = { x: props.scrollX };
|
||||||
@ -81,19 +46,6 @@ const tableScroll = computed(() => {
|
|||||||
return scroll;
|
return scroll;
|
||||||
});
|
});
|
||||||
|
|
||||||
const infiniteStatusText = computed(() => {
|
|
||||||
if (!props.infinite) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
if (props.loading && props.dataSource.length > 0) {
|
|
||||||
return '加载中...';
|
|
||||||
}
|
|
||||||
if (props.hasMore) {
|
|
||||||
return props.loadMoreText;
|
|
||||||
}
|
|
||||||
return props.dataSource.length > 0 ? '已加载全部' : '';
|
|
||||||
});
|
|
||||||
|
|
||||||
function stringifyCellValue(value: any) {
|
function stringifyCellValue(value: any) {
|
||||||
if (value === undefined || value === null || value === '') {
|
if (value === undefined || value === null || value === '') {
|
||||||
return '-';
|
return '-';
|
||||||
@ -116,10 +68,6 @@ function getColumnValue(record: Record<string, any>, column: Record<string, any>
|
|||||||
return record?.[String(dataIndex)];
|
return record?.[String(dataIndex)];
|
||||||
}
|
}
|
||||||
|
|
||||||
function isVNodeLike(value: any) {
|
|
||||||
return Boolean(value && typeof value === 'object' && value.__v_isVNode);
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateTableScrollY() {
|
function updateTableScrollY() {
|
||||||
if (!props.autoHeight) {
|
if (!props.autoHeight) {
|
||||||
return;
|
return;
|
||||||
@ -137,45 +85,9 @@ function updateTableScrollY() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function emitLoadMore() {
|
|
||||||
if (loadMoreLocked) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
loadMoreLocked = true;
|
|
||||||
emit('loadMore');
|
|
||||||
window.setTimeout(() => {
|
|
||||||
loadMoreLocked = false;
|
|
||||||
}, 300);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleTableBodyScroll(event: Event) {
|
|
||||||
if (!props.infinite || props.loading || !props.hasMore) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const target = event.currentTarget as HTMLElement;
|
|
||||||
const distanceToBottom =
|
|
||||||
target.scrollHeight - target.scrollTop - target.clientHeight;
|
|
||||||
if (distanceToBottom <= 80) {
|
|
||||||
emitLoadMore();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function bindTableBodyScroll() {
|
|
||||||
const nextBody = props.infinite
|
|
||||||
? (tableAreaRef.value?.querySelector('.ant-table-body') as HTMLElement | null)
|
|
||||||
: null;
|
|
||||||
if (nextBody === tableBodyElement) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
tableBodyElement?.removeEventListener('scroll', handleTableBodyScroll);
|
|
||||||
tableBodyElement = nextBody;
|
|
||||||
tableBodyElement?.addEventListener('scroll', handleTableBodyScroll);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function refreshTableScroll() {
|
async function refreshTableScroll() {
|
||||||
await nextTick();
|
await nextTick();
|
||||||
updateTableScrollY();
|
updateTableScrollY();
|
||||||
bindTableBodyScroll();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
@ -192,18 +104,10 @@ onMounted(async () => {
|
|||||||
if (pagerRef.value) {
|
if (pagerRef.value) {
|
||||||
tableResizeObserver.observe(pagerRef.value);
|
tableResizeObserver.observe(pagerRef.value);
|
||||||
}
|
}
|
||||||
if (summaryRef.value) {
|
|
||||||
tableResizeObserver.observe(summaryRef.value);
|
|
||||||
}
|
|
||||||
window.addEventListener('resize', updateTableScrollY);
|
window.addEventListener('resize', updateTableScrollY);
|
||||||
});
|
});
|
||||||
|
|
||||||
onUpdated(() => {
|
|
||||||
void refreshTableScroll();
|
|
||||||
});
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
tableBodyElement?.removeEventListener('scroll', handleTableBodyScroll);
|
|
||||||
tableResizeObserver?.disconnect();
|
tableResizeObserver?.disconnect();
|
||||||
window.removeEventListener('resize', updateTableScrollY);
|
window.removeEventListener('resize', updateTableScrollY);
|
||||||
});
|
});
|
||||||
@ -214,10 +118,6 @@ onBeforeUnmount(() => {
|
|||||||
class="admin-config-table-card"
|
class="admin-config-table-card"
|
||||||
:class="{ 'admin-config-table-card--plain': plain }"
|
:class="{ 'admin-config-table-card--plain': plain }"
|
||||||
>
|
>
|
||||||
<div v-if="summaryText" ref="summaryRef" class="admin-config-table-summary">
|
|
||||||
{{ summaryText }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div ref="tableAreaRef" class="admin-config-table-area">
|
<div ref="tableAreaRef" class="admin-config-table-area">
|
||||||
<Table
|
<Table
|
||||||
class="admin-config-table"
|
class="admin-config-table"
|
||||||
@ -226,32 +126,22 @@ onBeforeUnmount(() => {
|
|||||||
:loading="loading"
|
:loading="loading"
|
||||||
:pagination="false"
|
:pagination="false"
|
||||||
:row-key="rowKey"
|
:row-key="rowKey"
|
||||||
:row-selection="rowSelection"
|
|
||||||
:scroll="tableScroll"
|
:scroll="tableScroll"
|
||||||
>
|
>
|
||||||
<template #headerCell="slotProps">
|
<template #headerCell="slotProps">
|
||||||
<RenderNode
|
<slot name="headerCell" v-bind="slotProps">
|
||||||
v-if="isVNodeLike(slotProps.column?.title)"
|
|
||||||
:node="slotProps.column.title"
|
|
||||||
/>
|
|
||||||
<slot v-else name="headerCell" v-bind="slotProps">
|
|
||||||
{{ slotProps.column.title }}
|
{{ slotProps.column.title }}
|
||||||
</slot>
|
</slot>
|
||||||
</template>
|
</template>
|
||||||
<template #bodyCell="slotProps">
|
<template #bodyCell="slotProps">
|
||||||
<RenderNode v-if="isVNodeLike(slotProps.text)" :node="slotProps.text" />
|
<slot name="bodyCell" v-bind="slotProps">
|
||||||
<slot v-else name="bodyCell" v-bind="slotProps">
|
|
||||||
{{ stringifyCellValue(getColumnValue(slotProps.record, slotProps.column)) }}
|
{{ stringifyCellValue(getColumnValue(slotProps.record, slotProps.column)) }}
|
||||||
</slot>
|
</slot>
|
||||||
</template>
|
</template>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="infiniteStatusText" class="admin-config-table-load-status">
|
<div ref="pagerRef" class="admin-config-table-pager">
|
||||||
{{ infiniteStatusText }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="showPager" ref="pagerRef" class="admin-config-table-pager">
|
|
||||||
<Pagination
|
<Pagination
|
||||||
:current="current"
|
:current="current"
|
||||||
:page-size="pageSize"
|
:page-size="pageSize"
|
||||||
@ -288,15 +178,6 @@ onBeforeUnmount(() => {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-config-table-summary {
|
|
||||||
border-bottom: 1px solid rgb(237 240 244);
|
|
||||||
color: rgb(71 84 103);
|
|
||||||
flex: 0 0 auto;
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 18px;
|
|
||||||
padding: 8px 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-config-table {
|
.admin-config-table {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -377,16 +258,6 @@ onBeforeUnmount(() => {
|
|||||||
background: rgb(250 252 255);
|
background: rgb(250 252 255);
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-config-table-load-status {
|
|
||||||
border-top: 1px solid rgb(237 240 244);
|
|
||||||
color: rgb(100 116 139);
|
|
||||||
flex: 0 0 auto;
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 18px;
|
|
||||||
padding: 8px 12px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-config-table-pager {
|
.admin-config-table-pager {
|
||||||
border-top: 1px solid rgb(237 240 244);
|
border-top: 1px solid rgb(237 240 244);
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@ -38,7 +38,6 @@ const accountHandleOpen = ref(false);
|
|||||||
const query = reactive({
|
const query = reactive({
|
||||||
cursor: 1,
|
cursor: 1,
|
||||||
deviceId: '',
|
deviceId: '',
|
||||||
ip: '',
|
|
||||||
limit: 20,
|
limit: 20,
|
||||||
userId: '',
|
userId: '',
|
||||||
});
|
});
|
||||||
@ -162,14 +161,6 @@ loadData(true);
|
|||||||
style="width: 260px"
|
style="width: 260px"
|
||||||
/>
|
/>
|
||||||
</InlineFilterField>
|
</InlineFilterField>
|
||||||
<InlineFilterField label="IP">
|
|
||||||
<Input
|
|
||||||
v-model:value="query.ip"
|
|
||||||
allow-clear
|
|
||||||
placeholder="IP"
|
|
||||||
style="width: 200px"
|
|
||||||
/>
|
|
||||||
</InlineFilterField>
|
|
||||||
<Button :loading="loading" type="primary" @click="loadData(true)">
|
<Button :loading="loading" type="primary" @click="loadData(true)">
|
||||||
搜索
|
搜索
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -21,11 +21,7 @@ import { getAccessImgUrl, simpleUploadFile } from '#/api/legacy/oss';
|
|||||||
import { regionConfigTable } from '#/api/legacy/system';
|
import { regionConfigTable } from '#/api/legacy/system';
|
||||||
import { normalizeResourceCode } from '#/views/_shared/resource-code';
|
import { normalizeResourceCode } from '#/views/_shared/resource-code';
|
||||||
|
|
||||||
import {
|
import { GIFT_CONFIG_TAB_OPTIONS } from '../constants';
|
||||||
CP_RELATION_TYPE_OPTIONS,
|
|
||||||
GIFT_CONFIG_TAB_OPTIONS,
|
|
||||||
GIFT_SPECIAL_OPTIONS,
|
|
||||||
} from '../constants';
|
|
||||||
|
|
||||||
type AssetKind = 'cover' | 'source';
|
type AssetKind = 'cover' | 'source';
|
||||||
type RowStatus = 'error' | 'ready' | 'saved' | 'saving' | 'uploading';
|
type RowStatus = 'error' | 'ready' | 'saved' | 'saving' | 'uploading';
|
||||||
@ -58,13 +54,6 @@ interface ParsedGiftFile {
|
|||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UploadTask {
|
|
||||||
file: File;
|
|
||||||
fileName: string;
|
|
||||||
kind: AssetKind;
|
|
||||||
row: BatchGiftRow;
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
initialSysOrigin?: number | string;
|
initialSysOrigin?: number | string;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@ -76,13 +65,9 @@ const emit = defineEmits<{
|
|||||||
success: [];
|
success: [];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const DEFAULT_GIFT_SPECIAL_LIST = ['MUSIC', 'ANIMATION'];
|
|
||||||
|
|
||||||
const batchForm = reactive({
|
const batchForm = reactive({
|
||||||
cpRelationType: 'CP',
|
|
||||||
giftTab: 'ORDINARY',
|
giftTab: 'ORDINARY',
|
||||||
regionList: [] as Array<number | string>,
|
regionList: [] as Array<number | string>,
|
||||||
specialList: [...DEFAULT_GIFT_SPECIAL_LIST],
|
|
||||||
sysOrigin: '',
|
sysOrigin: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -97,25 +82,11 @@ const sequence = ref(0);
|
|||||||
const showOnlyErrors = ref(false);
|
const showOnlyErrors = ref(false);
|
||||||
const submitting = ref(false);
|
const submitting = ref(false);
|
||||||
const droppedFilePathMap = new WeakMap<File, string>();
|
const droppedFilePathMap = new WeakMap<File, string>();
|
||||||
const uploadQueue: UploadTask[] = [];
|
|
||||||
const MAX_UPLOAD_CONCURRENCY = 1;
|
|
||||||
let activeUploadCount = 0;
|
|
||||||
|
|
||||||
const giftTabOptions = GIFT_CONFIG_TAB_OPTIONS.map((item) => ({
|
const giftTabOptions = GIFT_CONFIG_TAB_OPTIONS.map((item) => ({
|
||||||
label: item.name,
|
label: item.name,
|
||||||
value: item.value as any,
|
value: item.value as any,
|
||||||
}));
|
}));
|
||||||
const cpRelationTypeOptions = CP_RELATION_TYPE_OPTIONS.map((item) => ({
|
|
||||||
label: item.name,
|
|
||||||
value: item.value as any,
|
|
||||||
}));
|
|
||||||
const defaultSpecialText = computed(() =>
|
|
||||||
GIFT_SPECIAL_OPTIONS.filter((item) =>
|
|
||||||
DEFAULT_GIFT_SPECIAL_LIST.includes(item.value),
|
|
||||||
)
|
|
||||||
.map((item) => item.name)
|
|
||||||
.join(' + '),
|
|
||||||
);
|
|
||||||
const regionOptions = computed(() =>
|
const regionOptions = computed(() =>
|
||||||
regions.value.map((item) => ({
|
regions.value.map((item) => ({
|
||||||
label: String(item.regionName || item.name || item.id || '-'),
|
label: String(item.regionName || item.name || item.id || '-'),
|
||||||
@ -190,28 +161,16 @@ const rowSelection = computed(() => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
watch(
|
|
||||||
() => batchForm.giftTab,
|
|
||||||
(giftTab) => {
|
|
||||||
if (giftTab === 'CP' && !batchForm.cpRelationType) {
|
|
||||||
batchForm.cpRelationType = 'CP';
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
function resetBatch() {
|
function resetBatch() {
|
||||||
batchForm.sysOrigin =
|
batchForm.sysOrigin =
|
||||||
String(props.initialSysOrigin || props.sysOriginOptions[0]?.value || 'LIKEI');
|
String(props.initialSysOrigin || props.sysOriginOptions[0]?.value || 'LIKEI');
|
||||||
batchForm.giftTab = 'ORDINARY';
|
batchForm.giftTab = 'ORDINARY';
|
||||||
batchForm.cpRelationType = 'CP';
|
|
||||||
batchForm.regionList = [];
|
batchForm.regionList = [];
|
||||||
batchForm.specialList = [...DEFAULT_GIFT_SPECIAL_LIST];
|
|
||||||
rows.value = [];
|
rows.value = [];
|
||||||
selectedRowKeys.value = [];
|
selectedRowKeys.value = [];
|
||||||
activeRowKey.value = '';
|
activeRowKey.value = '';
|
||||||
showOnlyErrors.value = false;
|
showOnlyErrors.value = false;
|
||||||
sequence.value = 0;
|
sequence.value = 0;
|
||||||
uploadQueue.length = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
@ -383,26 +342,7 @@ function setRowAsset(row: BatchGiftRow, kind: AssetKind, file: File) {
|
|||||||
}
|
}
|
||||||
row.saved = false;
|
row.saved = false;
|
||||||
refreshAllRows();
|
refreshAllRows();
|
||||||
enqueueUpload({ file, fileName, kind, row });
|
void uploadAsset(row, kind, file, fileName);
|
||||||
}
|
|
||||||
|
|
||||||
function enqueueUpload(task: UploadTask) {
|
|
||||||
uploadQueue.push(task);
|
|
||||||
runUploadQueue();
|
|
||||||
}
|
|
||||||
|
|
||||||
function runUploadQueue() {
|
|
||||||
while (activeUploadCount < MAX_UPLOAD_CONCURRENCY && uploadQueue.length > 0) {
|
|
||||||
const task = uploadQueue.shift();
|
|
||||||
if (!task) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
activeUploadCount += 1;
|
|
||||||
void uploadAsset(task.row, task.kind, task.file, task.fileName).finally(() => {
|
|
||||||
activeUploadCount -= 1;
|
|
||||||
runUploadQueue();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function uploadAsset(
|
async function uploadAsset(
|
||||||
@ -688,7 +628,6 @@ function customRow(record: Record<string, any>) {
|
|||||||
function buildPayload(row: BatchGiftRow) {
|
function buildPayload(row: BatchGiftRow) {
|
||||||
return {
|
return {
|
||||||
account: '',
|
account: '',
|
||||||
cpRelationType: batchForm.giftTab === 'CP' ? batchForm.cpRelationType : '',
|
|
||||||
explanationGift: false,
|
explanationGift: false,
|
||||||
expiredTime: '',
|
expiredTime: '',
|
||||||
giftCandy: Number(row.giftCandy || 0),
|
giftCandy: Number(row.giftCandy || 0),
|
||||||
@ -700,7 +639,7 @@ function buildPayload(row: BatchGiftRow) {
|
|||||||
giftTab: batchForm.giftTab,
|
giftTab: batchForm.giftTab,
|
||||||
regionList: [...batchForm.regionList],
|
regionList: [...batchForm.regionList],
|
||||||
sort: Number(row.sort || 0),
|
sort: Number(row.sort || 0),
|
||||||
specialList: [...batchForm.specialList],
|
specialList: [],
|
||||||
standardId: '',
|
standardId: '',
|
||||||
sysOrigin: batchForm.sysOrigin,
|
sysOrigin: batchForm.sysOrigin,
|
||||||
type: 'GOLD',
|
type: 'GOLD',
|
||||||
@ -780,14 +719,6 @@ async function submitBatch() {
|
|||||||
:options="giftTabOptions"
|
:options="giftTabOptions"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="batchForm.giftTab === 'CP'" class="common-field">
|
|
||||||
<span class="common-label">CP礼物类型</span>
|
|
||||||
<Select
|
|
||||||
v-model:value="batchForm.cpRelationType"
|
|
||||||
option-label-prop="label"
|
|
||||||
:options="cpRelationTypeOptions"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="common-field common-field--wide">
|
<div class="common-field common-field--wide">
|
||||||
<span class="common-label">区域</span>
|
<span class="common-label">区域</span>
|
||||||
<Select
|
<Select
|
||||||
@ -798,7 +729,6 @@ async function submitBatch() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="common-meta">收费类型:金币</div>
|
<div class="common-meta">收费类型:金币</div>
|
||||||
<div class="common-meta">特效:{{ defaultSpecialText }}</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="batch-workspace">
|
<div class="batch-workspace">
|
||||||
@ -1201,18 +1131,6 @@ async function submitBatch() {
|
|||||||
width: 52px;
|
width: 52px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.asset-cover {
|
|
||||||
border-radius: 6px;
|
|
||||||
display: block;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.asset-cover :deep(.ant-image-img) {
|
|
||||||
height: 52px;
|
|
||||||
object-fit: contain;
|
|
||||||
width: 52px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.asset-empty {
|
.asset-empty {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: #f8fafc;
|
background: #f8fafc;
|
||||||
|
|||||||
@ -1,28 +1,13 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, reactive, ref, watch } from 'vue';
|
import { computed, reactive, ref, watch } from 'vue';
|
||||||
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
DatePicker,
|
|
||||||
Drawer,
|
|
||||||
Form,
|
|
||||||
FormItem,
|
|
||||||
Image,
|
|
||||||
Input,
|
|
||||||
message,
|
|
||||||
Select,
|
|
||||||
Space,
|
|
||||||
Spin,
|
|
||||||
Switch,
|
|
||||||
} from 'antdv-next';
|
|
||||||
|
|
||||||
import {
|
|
||||||
mapLuckyGiftStandard,
|
|
||||||
} from '#/api/legacy/game-lucky-gift';
|
|
||||||
import {
|
import {
|
||||||
addGift,
|
addGift,
|
||||||
updateGift,
|
updateGift,
|
||||||
} from '#/api/legacy/gift';
|
} from '#/api/legacy/gift';
|
||||||
|
import {
|
||||||
|
mapLuckyGiftStandard,
|
||||||
|
} from '#/api/legacy/game-lucky-gift';
|
||||||
import {
|
import {
|
||||||
getAccessImgUrl,
|
getAccessImgUrl,
|
||||||
simpleUploadFile,
|
simpleUploadFile,
|
||||||
@ -32,24 +17,28 @@ import {
|
|||||||
} from '#/api/legacy/system';
|
} from '#/api/legacy/system';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CP_RELATION_TYPE_OPTIONS,
|
Button,
|
||||||
|
DatePicker,
|
||||||
|
Drawer,
|
||||||
|
Form,
|
||||||
|
FormItem,
|
||||||
|
Image,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
Space,
|
||||||
|
Spin,
|
||||||
|
Switch,
|
||||||
|
TextArea,
|
||||||
|
message,
|
||||||
|
} from 'antdv-next';
|
||||||
|
|
||||||
|
import {
|
||||||
GIFT_CONFIG_TAB_OPTIONS,
|
GIFT_CONFIG_TAB_OPTIONS,
|
||||||
GIFT_SPECIAL_OPTIONS,
|
GIFT_SPECIAL_OPTIONS,
|
||||||
} from '../constants';
|
} from '../constants';
|
||||||
|
|
||||||
defineOptions({ name: 'OperateGiftFormModal' });
|
defineOptions({ name: 'OperateGiftFormModal' });
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
open: boolean;
|
|
||||||
row?: null | Record<string, any>;
|
|
||||||
sysOrigin: string;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
close: [];
|
|
||||||
success: [];
|
|
||||||
}>();
|
|
||||||
|
|
||||||
function createForm(sysOrigin = '') {
|
function createForm(sysOrigin = '') {
|
||||||
return {
|
return {
|
||||||
account: '',
|
account: '',
|
||||||
@ -63,7 +52,6 @@ function createForm(sysOrigin = '') {
|
|||||||
giftSourceUrl: '',
|
giftSourceUrl: '',
|
||||||
giftTab: 'ORDINARY',
|
giftTab: 'ORDINARY',
|
||||||
id: '',
|
id: '',
|
||||||
cpRelationType: 'CP',
|
|
||||||
regionList: [] as Array<number | string>,
|
regionList: [] as Array<number | string>,
|
||||||
sort: '',
|
sort: '',
|
||||||
specialList: [] as string[],
|
specialList: [] as string[],
|
||||||
@ -73,6 +61,17 @@ function createForm(sysOrigin = '') {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
open: boolean;
|
||||||
|
row?: null | Record<string, any>;
|
||||||
|
sysOrigin: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
close: [];
|
||||||
|
success: [];
|
||||||
|
}>();
|
||||||
|
|
||||||
const form = reactive(createForm());
|
const form = reactive(createForm());
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const saving = ref(false);
|
const saving = ref(false);
|
||||||
@ -97,10 +96,6 @@ const giftTabOptions = GIFT_CONFIG_TAB_OPTIONS.map((item) => ({
|
|||||||
label: item.name,
|
label: item.name,
|
||||||
value: item.value as any,
|
value: item.value as any,
|
||||||
}));
|
}));
|
||||||
const cpRelationTypeOptions = CP_RELATION_TYPE_OPTIONS.map((item) => ({
|
|
||||||
label: item.name,
|
|
||||||
value: item.value as any,
|
|
||||||
}));
|
|
||||||
const chargeTypeOptions = [
|
const chargeTypeOptions = [
|
||||||
{ label: '金币', value: 'GOLD' },
|
{ label: '金币', value: 'GOLD' },
|
||||||
{ label: '钻石', value: 'DIAMOND' },
|
{ label: '钻石', value: 'DIAMOND' },
|
||||||
@ -133,13 +128,6 @@ function normalizeMultiValue(value: any) {
|
|||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeTimestampValue(value: any) {
|
|
||||||
if (value === '' || value === null || value === undefined) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
return String(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetForm() {
|
function resetForm() {
|
||||||
Object.assign(form, createForm(props.sysOrigin));
|
Object.assign(form, createForm(props.sysOrigin));
|
||||||
previousStandardId.value = '';
|
previousStandardId.value = '';
|
||||||
@ -169,8 +157,6 @@ watch(
|
|||||||
Object.assign(form, {
|
Object.assign(form, {
|
||||||
...props.row,
|
...props.row,
|
||||||
explanationGift: Boolean(props.row.explanationGift),
|
explanationGift: Boolean(props.row.explanationGift),
|
||||||
expiredTime: normalizeTimestampValue(props.row.expiredTime),
|
|
||||||
cpRelationType: props.row.cpRelationType || 'CP',
|
|
||||||
regionList: normalizeMultiValue(props.row.regions || props.row.regionList),
|
regionList: normalizeMultiValue(props.row.regions || props.row.regionList),
|
||||||
specialList: normalizeMultiValue(props.row.special || props.row.specialList),
|
specialList: normalizeMultiValue(props.row.special || props.row.specialList),
|
||||||
sysOrigin: props.row.sysOrigin || props.sysOrigin,
|
sysOrigin: props.row.sysOrigin || props.sysOrigin,
|
||||||
@ -193,15 +179,6 @@ watch(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
watch(
|
|
||||||
() => form.giftTab,
|
|
||||||
(giftTab) => {
|
|
||||||
if (giftTab === 'CP' && !form.cpRelationType) {
|
|
||||||
form.cpRelationType = 'CP';
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
function toggleAllRegions() {
|
function toggleAllRegions() {
|
||||||
if (form.regionList.length === regions.value.length) {
|
if (form.regionList.length === regions.value.length) {
|
||||||
form.regionList = [];
|
form.regionList = [];
|
||||||
@ -289,10 +266,6 @@ async function handleSubmit() {
|
|||||||
message.warning('请输入归属人账号');
|
message.warning('请输入归属人账号');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (form.giftTab === 'CP' && !form.cpRelationType) {
|
|
||||||
message.warning('请选择CP礼物类型');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (isLuckyOrMagic.value && !form.standardId) {
|
if (isLuckyOrMagic.value && !form.standardId) {
|
||||||
message.warning('幸运/魔法礼物必须选择规格');
|
message.warning('幸运/魔法礼物必须选择规格');
|
||||||
return;
|
return;
|
||||||
@ -314,11 +287,14 @@ async function handleSubmit() {
|
|||||||
try {
|
try {
|
||||||
const payload = {
|
const payload = {
|
||||||
...form,
|
...form,
|
||||||
cpRelationType: form.giftTab === 'CP' ? form.cpRelationType : '',
|
|
||||||
giftCandy: form.type === 'FREE' ? '' : form.giftCandy,
|
giftCandy: form.type === 'FREE' ? '' : form.giftCandy,
|
||||||
giftIntegral: form.type === 'FREE' ? '' : form.giftIntegral,
|
giftIntegral: form.type === 'FREE' ? '' : form.giftIntegral,
|
||||||
};
|
};
|
||||||
await (payload.id ? updateGift(payload) : addGift(payload));
|
if (payload.id) {
|
||||||
|
await updateGift(payload);
|
||||||
|
} else {
|
||||||
|
await addGift(payload);
|
||||||
|
}
|
||||||
message.success('保存成功');
|
message.success('保存成功');
|
||||||
emit('success');
|
emit('success');
|
||||||
emit('close');
|
emit('close');
|
||||||
@ -333,69 +309,51 @@ async function handleSubmit() {
|
|||||||
:open="open"
|
:open="open"
|
||||||
:title="title"
|
:title="title"
|
||||||
destroy-on-close
|
destroy-on-close
|
||||||
root-class="gift-form-drawer-root"
|
width="980"
|
||||||
width="1120"
|
|
||||||
:styles="{
|
|
||||||
body: { padding: '0', overflow: 'hidden' },
|
|
||||||
footer: { padding: '10px 16px' },
|
|
||||||
}"
|
|
||||||
@close="emit('close')"
|
@close="emit('close')"
|
||||||
>
|
>
|
||||||
<Spin :spinning="loading" wrapper-class-name="gift-editor-spin">
|
<Spin :spinning="loading">
|
||||||
<div class="gift-editor">
|
<Form layout="vertical">
|
||||||
<aside class="media-panel">
|
<div class="grid">
|
||||||
<div class="media-block">
|
<FormItem label="平台">
|
||||||
<div class="media-head">
|
<Input :value="form.sysOrigin" disabled />
|
||||||
<span>封面</span>
|
</FormItem>
|
||||||
<span class="required-mark">必填</span>
|
<FormItem label="区域">
|
||||||
</div>
|
<Space direction="vertical" style="width: 100%">
|
||||||
<Image
|
<Button size="small" @click="toggleAllRegions">
|
||||||
v-if="previewCover"
|
{{ form.regionList.length === regions.length ? '取消全选' : '全选' }}
|
||||||
:src="previewCover"
|
</Button>
|
||||||
class="preview-image"
|
<Select option-label-prop="label"
|
||||||
|
v-model:value="form.regionList"
|
||||||
|
:options="regionOptions"
|
||||||
|
mode="multiple"
|
||||||
|
placeholder="请选择区域"
|
||||||
|
show-search
|
||||||
|
style="width: 100%"
|
||||||
/>
|
/>
|
||||||
<div v-else class="preview-image preview-image--empty">未上传</div>
|
</Space>
|
||||||
<div class="media-actions">
|
</FormItem>
|
||||||
<Button :loading="coverUploading" size="small" @click="openCoverUpload">
|
|
||||||
上传封面
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
v-if="form.giftPhoto"
|
|
||||||
danger
|
|
||||||
size="small"
|
|
||||||
@click="form.giftPhoto = ''"
|
|
||||||
>
|
|
||||||
清空
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="media-block">
|
<div class="grid">
|
||||||
<div class="media-head">
|
<FormItem label="名称">
|
||||||
<span>资源</span>
|
<Input v-model:value="form.giftName" />
|
||||||
<span class="optional-mark">可选</span>
|
</FormItem>
|
||||||
|
<FormItem label="Code">
|
||||||
|
<Input v-model:value="form.giftCode" />
|
||||||
|
</FormItem>
|
||||||
</div>
|
</div>
|
||||||
<div class="source-value">{{ previewSource || '未上传' }}</div>
|
|
||||||
<div class="media-actions">
|
|
||||||
<Button :loading="sourceUploading" size="small" @click="openSourceUpload">
|
|
||||||
上传资源
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
v-if="form.giftSourceUrl"
|
|
||||||
danger
|
|
||||||
size="small"
|
|
||||||
@click="form.giftSourceUrl = ''"
|
|
||||||
>
|
|
||||||
清空
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
<main class="editor-panel">
|
<div class="grid">
|
||||||
<Form class="editor-form" layout="vertical">
|
<FormItem label="金币">
|
||||||
<div class="section-title">基础信息</div>
|
<Input v-model:value="form.giftCandy" :disabled="form.type === 'FREE'" />
|
||||||
<div class="form-grid">
|
</FormItem>
|
||||||
|
<FormItem label="积分">
|
||||||
|
<Input v-model:value="form.giftIntegral" :disabled="form.type === 'FREE'" />
|
||||||
|
</FormItem>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid">
|
||||||
<FormItem label="礼物类型">
|
<FormItem label="礼物类型">
|
||||||
<Select
|
<Select
|
||||||
v-model:value="form.giftTab"
|
v-model:value="form.giftTab"
|
||||||
@ -412,15 +370,56 @@ async function handleSubmit() {
|
|||||||
placeholder="请选择收费类型"
|
placeholder="请选择收费类型"
|
||||||
/>
|
/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid">
|
||||||
|
<FormItem label="特效">
|
||||||
|
<Select
|
||||||
|
v-model:value="form.specialList"
|
||||||
|
:options="specialOptions"
|
||||||
|
mode="multiple"
|
||||||
|
option-label-prop="label"
|
||||||
|
placeholder="请选择特效"
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
<FormItem label="排序">
|
<FormItem label="排序">
|
||||||
<Input v-model:value="form.sort" />
|
<Input v-model:value="form.sort" />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
<FormItem class="span-2" label="名称">
|
</div>
|
||||||
<Input v-model:value="form.giftName" />
|
|
||||||
|
<div class="grid">
|
||||||
|
<FormItem label="封面">
|
||||||
|
<div class="upload-field">
|
||||||
|
<Image
|
||||||
|
v-if="previewCover"
|
||||||
|
:src="previewCover"
|
||||||
|
class="preview-image"
|
||||||
|
/>
|
||||||
|
<div v-else class="preview-image preview-image--empty">未上传</div>
|
||||||
|
<Space>
|
||||||
|
<Button :loading="coverUploading" @click="openCoverUpload">上传封面</Button>
|
||||||
|
<Button v-if="form.giftPhoto" danger @click="form.giftPhoto = ''">清空</Button>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
<FormItem label="Code">
|
<FormItem label="资源">
|
||||||
<Input v-model:value="form.giftCode" />
|
<div class="upload-field">
|
||||||
|
<div class="source-value">{{ previewSource || '未上传' }}</div>
|
||||||
|
<Space>
|
||||||
|
<Button :loading="sourceUploading" @click="openSourceUpload">上传资源</Button>
|
||||||
|
<Button
|
||||||
|
v-if="form.giftSourceUrl"
|
||||||
|
danger
|
||||||
|
@click="form.giftSourceUrl = ''"
|
||||||
|
>
|
||||||
|
清空
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid">
|
||||||
<FormItem label="有效期">
|
<FormItem label="有效期">
|
||||||
<DatePicker
|
<DatePicker
|
||||||
v-model:value="form.expiredTime"
|
v-model:value="form.expiredTime"
|
||||||
@ -428,48 +427,13 @@ async function handleSubmit() {
|
|||||||
value-format="x"
|
value-format="x"
|
||||||
/>
|
/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
<FormItem label="金币">
|
|
||||||
<Input v-model:value="form.giftCandy" :disabled="form.type === 'FREE'" />
|
|
||||||
</FormItem>
|
|
||||||
<FormItem label="积分">
|
|
||||||
<Input v-model:value="form.giftIntegral" :disabled="form.type === 'FREE'" />
|
|
||||||
</FormItem>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="section-title section-title--spaced">范围与效果</div>
|
|
||||||
<div class="form-grid">
|
|
||||||
<FormItem class="span-3" label="区域">
|
|
||||||
<div class="inline-field">
|
|
||||||
<Select
|
|
||||||
v-model:value="form.regionList"
|
|
||||||
:max-tag-count="4"
|
|
||||||
:options="regionOptions"
|
|
||||||
class="compact-multiple-select"
|
|
||||||
mode="multiple"
|
|
||||||
option-label-prop="label"
|
|
||||||
placeholder="请选择区域"
|
|
||||||
show-search
|
|
||||||
/>
|
|
||||||
<Button size="small" @click="toggleAllRegions">
|
|
||||||
{{ form.regionList.length === regions.length ? '取消全选' : '全选' }}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</FormItem>
|
|
||||||
<FormItem class="span-3" label="特效">
|
|
||||||
<Select
|
|
||||||
v-model:value="form.specialList"
|
|
||||||
:max-tag-count="4"
|
|
||||||
:options="specialOptions"
|
|
||||||
class="compact-multiple-select"
|
|
||||||
mode="multiple"
|
|
||||||
option-label-prop="label"
|
|
||||||
placeholder="请选择特效"
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
<FormItem v-if="form.giftTab === 'EXCLUSIVE'" label="归属人账号">
|
<FormItem v-if="form.giftTab === 'EXCLUSIVE'" label="归属人账号">
|
||||||
<Input v-model:value="form.account" />
|
<Input v-model:value="form.account" />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
<FormItem v-if="isLuckyOrMagic" label="幸运规格">
|
</div>
|
||||||
|
|
||||||
|
<div class="grid" v-if="isLuckyOrMagic">
|
||||||
|
<FormItem label="幸运规格">
|
||||||
<Select
|
<Select
|
||||||
v-model:value="form.standardId"
|
v-model:value="form.standardId"
|
||||||
:options="standardSelectOptions"
|
:options="standardSelectOptions"
|
||||||
@ -477,21 +441,18 @@ async function handleSubmit() {
|
|||||||
placeholder="请选择幸运规格"
|
placeholder="请选择幸运规格"
|
||||||
/>
|
/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
<FormItem v-if="form.giftTab === 'CP'" label="CP礼物类型">
|
</div>
|
||||||
<Select
|
|
||||||
v-model:value="form.cpRelationType"
|
<div class="grid" v-if="form.giftTab === 'CP'">
|
||||||
:options="cpRelationTypeOptions"
|
<FormItem label="是否告白礼物">
|
||||||
option-label-prop="label"
|
|
||||||
placeholder="请选择CP礼物类型"
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
<FormItem v-if="form.giftTab === 'CP'" label="告白礼物">
|
|
||||||
<Switch v-model:checked="form.explanationGift" />
|
<Switch v-model:checked="form.explanationGift" />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<FormItem label="备注">
|
||||||
|
<TextArea :value="previewSource" :rows="2" disabled />
|
||||||
|
</FormItem>
|
||||||
</Form>
|
</Form>
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
</Spin>
|
</Spin>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
@ -500,17 +461,17 @@ async function handleSubmit() {
|
|||||||
hidden
|
hidden
|
||||||
type="file"
|
type="file"
|
||||||
@change="handleCoverUpload"
|
@change="handleCoverUpload"
|
||||||
/>
|
>
|
||||||
<input
|
<input
|
||||||
ref="sourceInputRef"
|
ref="sourceInputRef"
|
||||||
accept=".svga,.pag,.mp4"
|
accept=".svga,.pag,.mp4"
|
||||||
hidden
|
hidden
|
||||||
type="file"
|
type="file"
|
||||||
@change="handleSourceUpload"
|
@change="handleSourceUpload"
|
||||||
/>
|
>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<Space class="drawer-footer-actions">
|
<Space>
|
||||||
<Button @click="emit('close')">取消</Button>
|
<Button @click="emit('close')">取消</Button>
|
||||||
<Button :loading="saving" type="primary" @click="handleSubmit">
|
<Button :loading="saving" type="primary" @click="handleSubmit">
|
||||||
保存
|
保存
|
||||||
@ -521,72 +482,22 @@ async function handleSubmit() {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.gift-editor {
|
.grid {
|
||||||
background: #f6f8fb;
|
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 12px;
|
gap: 16px;
|
||||||
grid-template-columns: 260px minmax(0, 1fr);
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
height: calc(100vh - 106px);
|
|
||||||
min-height: 0;
|
|
||||||
padding: 12px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.media-panel,
|
.upload-field {
|
||||||
.editor-panel {
|
|
||||||
background: #fff;
|
|
||||||
border: 1px solid #e5e7eb;
|
|
||||||
border-radius: 8px;
|
|
||||||
min-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-panel {
|
|
||||||
display: grid;
|
display: grid;
|
||||||
align-content: start;
|
gap: 10px;
|
||||||
gap: 12px;
|
|
||||||
overflow: auto;
|
|
||||||
padding: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-block {
|
|
||||||
display: grid;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-head {
|
|
||||||
align-items: center;
|
|
||||||
color: #111827;
|
|
||||||
display: flex;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 600;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.required-mark,
|
|
||||||
.optional-mark {
|
|
||||||
border-radius: 999px;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 18px;
|
|
||||||
padding: 0 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.required-mark {
|
|
||||||
background: #fff1f2;
|
|
||||||
color: #e11d48;
|
|
||||||
}
|
|
||||||
|
|
||||||
.optional-mark {
|
|
||||||
background: #f1f5f9;
|
|
||||||
color: #64748b;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-image {
|
.preview-image {
|
||||||
background: #f8fafc;
|
border-radius: 16px;
|
||||||
border: 1px solid #eef2f7;
|
height: 120px;
|
||||||
border-radius: 8px;
|
|
||||||
height: 180px;
|
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
width: 100%;
|
width: 120px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-image--empty,
|
.preview-image--empty,
|
||||||
@ -594,104 +505,17 @@ async function handleSubmit() {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
background: #f8fafc;
|
background: #f8fafc;
|
||||||
border: 1px dashed #cbd5e1;
|
border: 1px dashed #cbd5e1;
|
||||||
border-radius: 8px;
|
border-radius: 12px;
|
||||||
color: #94a3b8;
|
color: #94a3b8;
|
||||||
display: flex;
|
display: flex;
|
||||||
font-size: 12px;
|
|
||||||
min-height: 72px;
|
min-height: 72px;
|
||||||
padding: 10px;
|
padding: 12px;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
.media-actions,
|
|
||||||
.drawer-footer-actions {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-panel {
|
|
||||||
overflow: auto;
|
|
||||||
padding: 14px 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-form :deep(.ant-form-item) {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-form :deep(.ant-form-item-label) {
|
|
||||||
padding-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-form :deep(.ant-form-item-label > label) {
|
|
||||||
color: #475569;
|
|
||||||
font-size: 12px;
|
|
||||||
height: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-form :deep(.ant-input),
|
|
||||||
.editor-form :deep(.ant-picker),
|
|
||||||
.editor-form :deep(.ant-select-selector) {
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-title {
|
|
||||||
color: #111827;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 20px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-title--spaced {
|
|
||||||
border-top: 1px solid #eef2f7;
|
|
||||||
margin-top: 4px;
|
|
||||||
padding-top: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-grid {
|
|
||||||
display: grid;
|
|
||||||
column-gap: 12px;
|
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
|
|
||||||
.span-2 {
|
|
||||||
grid-column: span 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.span-3 {
|
|
||||||
grid-column: 1 / -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inline-field {
|
|
||||||
align-items: center;
|
|
||||||
display: grid;
|
|
||||||
gap: 8px;
|
|
||||||
grid-template-columns: minmax(0, 1fr) auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.compact-multiple-select :deep(.ant-select-selection-overflow) {
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.compact-multiple-select :deep(.ant-select-selection-item) {
|
|
||||||
max-width: 140px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
.gift-editor {
|
.grid {
|
||||||
grid-template-columns: minmax(0, 1fr);
|
grid-template-columns: minmax(0, 1fr);
|
||||||
height: auto;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-grid {
|
|
||||||
grid-template-columns: minmax(0, 1fr);
|
|
||||||
}
|
|
||||||
|
|
||||||
.span-2,
|
|
||||||
.span-3 {
|
|
||||||
grid-column: auto;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -20,21 +20,14 @@ import {
|
|||||||
|
|
||||||
defineOptions({ name: 'OperatePropsSourceSelectDrawer' });
|
defineOptions({ name: 'OperatePropsSourceSelectDrawer' });
|
||||||
|
|
||||||
type SourceTypeOption = {
|
|
||||||
label: string;
|
|
||||||
value: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
defaultType?: string;
|
defaultType?: string;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
sysOrigin: string;
|
sysOrigin: string;
|
||||||
typeOptions?: SourceTypeOption[];
|
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
defaultType: 'AVATAR_FRAME',
|
defaultType: 'AVATAR_FRAME',
|
||||||
typeOptions: undefined,
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -43,6 +36,11 @@ const emit = defineEmits<{
|
|||||||
select: [item: SourceItem];
|
select: [item: SourceItem];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
type SourceTypeOption = {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
|
||||||
type SourceItem = {
|
type SourceItem = {
|
||||||
amount?: number | string;
|
amount?: number | string;
|
||||||
cover?: string;
|
cover?: string;
|
||||||
@ -64,21 +62,12 @@ const TYPE_OPTIONS: SourceTypeOption[] = [
|
|||||||
{ label: '聊天气泡', value: 'CHAT_BUBBLE' },
|
{ label: '聊天气泡', value: 'CHAT_BUBBLE' },
|
||||||
{ label: '飘窗', value: 'FLOAT_PICTURE' },
|
{ label: '飘窗', value: 'FLOAT_PICTURE' },
|
||||||
{ label: '碎片', value: 'FRAGMENTS' },
|
{ label: '碎片', value: 'FRAGMENTS' },
|
||||||
{ label: '资料卡', value: 'DATA_CARD' },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const effectiveTypeOptions = computed(() =>
|
const typeSelectOptions = TYPE_OPTIONS.map((item) => ({
|
||||||
(props.typeOptions && props.typeOptions.length > 0 ? props.typeOptions : TYPE_OPTIONS).map(
|
|
||||||
(item) => ({
|
|
||||||
label: item.label,
|
label: item.label,
|
||||||
value: item.value as any,
|
value: item.value as any,
|
||||||
}),
|
}));
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
const allowedTypeSet = computed(
|
|
||||||
() => new Set(effectiveTypeOptions.value.map((item) => String(item.value))),
|
|
||||||
);
|
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const keyword = ref('');
|
const keyword = ref('');
|
||||||
@ -208,21 +197,13 @@ function formatSourcePrice(amount: number | string | undefined) {
|
|||||||
return `${amount} 金币`;
|
return `${amount} 金币`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveSourceType(type?: string) {
|
|
||||||
const value = String(type || '').trim();
|
|
||||||
if (value && allowedTypeSet.value.has(value)) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
return String(effectiveTypeOptions.value[0]?.value || 'AVATAR_FRAME');
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.open,
|
() => props.open,
|
||||||
(open) => {
|
(open) => {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
query.propsType = resolveSourceType(props.defaultType);
|
query.propsType = props.defaultType || 'AVATAR_FRAME';
|
||||||
keyword.value = '';
|
keyword.value = '';
|
||||||
void loadSource(query.propsType);
|
void loadSource(query.propsType);
|
||||||
},
|
},
|
||||||
@ -232,26 +213,14 @@ watch(
|
|||||||
watch(
|
watch(
|
||||||
() => props.defaultType,
|
() => props.defaultType,
|
||||||
(value) => {
|
(value) => {
|
||||||
if (!props.open) {
|
if (!props.open || !value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
query.propsType = resolveSourceType(value);
|
query.propsType = value;
|
||||||
void loadSource(query.propsType);
|
void loadSource(value);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.typeOptions,
|
|
||||||
() => {
|
|
||||||
if (!props.open) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
query.propsType = resolveSourceType(query.propsType || props.defaultType);
|
|
||||||
void loadSource(query.propsType);
|
|
||||||
},
|
|
||||||
{ deep: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.sysOrigin,
|
() => props.sysOrigin,
|
||||||
() => {
|
() => {
|
||||||
@ -358,7 +327,7 @@ function handleSelect(item: SourceItem) {
|
|||||||
<Space class="toolbar" wrap>
|
<Space class="toolbar" wrap>
|
||||||
<Select
|
<Select
|
||||||
v-model:value="query.propsType"
|
v-model:value="query.propsType"
|
||||||
:options="effectiveTypeOptions"
|
:options="typeSelectOptions"
|
||||||
option-label-prop="label"
|
option-label-prop="label"
|
||||||
placeholder="请选择道具类型"
|
placeholder="请选择道具类型"
|
||||||
style="width: 220px"
|
style="width: 220px"
|
||||||
|
|||||||
@ -105,12 +105,6 @@ export const GIFT_CONFIG_TAB_OPTIONS = [
|
|||||||
{ value: 'CUSTOMIZED', name: '定制礼物' },
|
{ value: 'CUSTOMIZED', name: '定制礼物' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const CP_RELATION_TYPE_OPTIONS = [
|
|
||||||
{ value: 'CP', name: 'CP' },
|
|
||||||
{ value: 'BROTHER', name: '兄弟' },
|
|
||||||
{ value: 'SISTERS', name: '姐妹' },
|
|
||||||
];
|
|
||||||
|
|
||||||
export const GIFT_SPECIAL_OPTIONS = [
|
export const GIFT_SPECIAL_OPTIONS = [
|
||||||
{ value: 'ANIMATION', name: '动画' },
|
{ value: 'ANIMATION', name: '动画' },
|
||||||
{ value: 'MUSIC', name: '音乐' },
|
{ value: 'MUSIC', name: '音乐' },
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1,643 +0,0 @@
|
|||||||
<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 AdminConfigTable from '#/views/_shared/admin-config-table.vue';
|
|
||||||
import { getAllowedSysOrigins } from '#/views/system/shared';
|
|
||||||
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Card,
|
|
||||||
Modal,
|
|
||||||
Select,
|
|
||||||
Space,
|
|
||||||
Table,
|
|
||||||
Tag,
|
|
||||||
message,
|
|
||||||
} from 'antdv-next';
|
|
||||||
|
|
||||||
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 || []);
|
|
||||||
return options.length > 0 ? options : getAllowedSysOrigins([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
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>>>([]);
|
|
||||||
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: '',
|
|
||||||
});
|
|
||||||
|
|
||||||
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: 'expectedMemberSalary', key: 'expectedMemberSalary', title: '主播应发', width: 120 },
|
|
||||||
{ dataIndex: 'expectedAgentSalary', key: 'expectedAgentSalary', title: '代理应发', width: 120 },
|
|
||||||
{ dataIndex: 'expectedSalary', key: 'expectedSalary', title: '应发合计', width: 120 },
|
|
||||||
{ dataIndex: 'payableMemberSalary', key: 'payableMemberSalary', title: '主播差额', width: 120 },
|
|
||||||
{ dataIndex: 'payableAgentSalary', key: 'payableAgentSalary', title: '代理差额', width: 120 },
|
|
||||||
{ dataIndex: 'payableSalary', key: 'payableSalary', title: '合计差额', width: 140 },
|
|
||||||
{ dataIndex: 'memberIssuedSalary', key: 'memberIssuedSalary', title: '主播实收', width: 140 },
|
|
||||||
{ dataIndex: 'agentIssuedSalary', key: 'agentIssuedSalary', title: '代理实收', width: 140 },
|
|
||||||
{ dataIndex: 'totalIssuedSalary', key: 'totalIssuedSalary', title: '总发工资', width: 140 },
|
|
||||||
{ dataIndex: 'currentSalaryBalance', key: 'currentSalaryBalance', title: '当前工资', width: 140 },
|
|
||||||
{ dataIndex: 'action', fixed: 'right', key: 'action', title: '操作', width: 110 },
|
|
||||||
];
|
|
||||||
|
|
||||||
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 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(() =>
|
|
||||||
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, append = false) {
|
|
||||||
if (!query.sysOrigin) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!query.countryCode) {
|
|
||||||
message.warning('请选择国家');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (reset) {
|
|
||||||
query.cursor = 1;
|
|
||||||
list.value = [];
|
|
||||||
total.value = 0;
|
|
||||||
selectedUserIds.value = [];
|
|
||||||
}
|
|
||||||
loading.value = true;
|
|
||||||
try {
|
|
||||||
const result = await pageManualSalary({ ...query });
|
|
||||||
const records = result.records || [];
|
|
||||||
list.value = append ? [...list.value, ...records] : records;
|
|
||||||
total.value = Number(result.total || 0);
|
|
||||||
sortLoadedRows();
|
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
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 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('请选择国家');
|
|
||||||
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 {
|
|
||||||
await submitManualSalary(hasSelectedUsers ? userIds : undefined);
|
|
||||||
message.success('发放成功');
|
|
||||||
} finally {
|
|
||||||
paying.value = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
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"
|
|
||||||
: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>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<AdminConfigTable
|
|
||||||
auto-height
|
|
||||||
:columns="columns"
|
|
||||||
:current="query.cursor"
|
|
||||||
:data-source="tableRows"
|
|
||||||
:has-more="hasMore"
|
|
||||||
infinite
|
|
||||||
:loading="loading"
|
|
||||||
:page-size="query.limit"
|
|
||||||
row-key="rowKey"
|
|
||||||
:row-selection="rowSelection"
|
|
||||||
:scroll-x="2340"
|
|
||||||
: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" />
|
|
||||||
</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 === 'expectedSalary'">
|
|
||||||
{{ formatMoney(record.expectedSalary) }}
|
|
||||||
</template>
|
|
||||||
<template v-else-if="column.key === 'expectedMemberSalary'">
|
|
||||||
{{ formatMoney(record.expectedMemberSalary) }}
|
|
||||||
</template>
|
|
||||||
<template v-else-if="column.key === 'expectedAgentSalary'">
|
|
||||||
{{ formatMoney(record.expectedAgentSalary) }}
|
|
||||||
</template>
|
|
||||||
<template v-else-if="column.key === 'payableMemberSalary'">
|
|
||||||
<span
|
|
||||||
:class="{ 'payable-salary-negative': Number(record.payableMemberSalary || 0) < 0 }"
|
|
||||||
>
|
|
||||||
{{ formatMoney(record.payableMemberSalary) }}
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
<template v-else-if="column.key === 'payableAgentSalary'">
|
|
||||||
<span
|
|
||||||
:class="{ 'payable-salary-negative': Number(record.payableAgentSalary || 0) < 0 }"
|
|
||||||
>
|
|
||||||
{{ formatMoney(record.payableAgentSalary) }}
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
<template v-else-if="column.key === 'payableSalary'">
|
|
||||||
<Space size="small">
|
|
||||||
<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 === 'memberIssuedSalary'">
|
|
||||||
<span>{{ formatMoney(record.memberIssuedSalary) }}</span>
|
|
||||||
<span
|
|
||||||
v-if="Number(record.memberOverIssuedSalary || 0) > 0"
|
|
||||||
class="over-issued-salary"
|
|
||||||
>
|
|
||||||
({{ formatMoney(record.memberOverIssuedSalary) }})
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
<template v-else-if="column.key === 'agentIssuedSalary'">
|
|
||||||
<span>{{ formatMoney(record.agentIssuedSalary) }}</span>
|
|
||||||
<span
|
|
||||||
v-if="Number(record.agentOverIssuedSalary || 0) > 0"
|
|
||||||
class="over-issued-salary"
|
|
||||||
>
|
|
||||||
({{ formatMoney(record.agentOverIssuedSalary) }})
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
<template v-else-if="column.key === '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>
|
|
||||||
</AdminConfigTable>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
.manual-salary-page {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
height: 100%;
|
|
||||||
min-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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>
|
|
||||||
@ -8,21 +8,10 @@ import {
|
|||||||
import { Page } from '@vben/common-ui';
|
import { Page } from '@vben/common-ui';
|
||||||
import { useAccessStore } from '@vben/stores';
|
import { useAccessStore } from '@vben/stores';
|
||||||
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Card,
|
|
||||||
Input,
|
|
||||||
message,
|
|
||||||
Modal,
|
|
||||||
Space,
|
|
||||||
Table,
|
|
||||||
} from 'antdv-next';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
confirmImport,
|
confirmImport,
|
||||||
deductMoney,
|
deductMoney,
|
||||||
exprotBank,
|
exprotBank,
|
||||||
getBankStatistics,
|
|
||||||
pageBank,
|
pageBank,
|
||||||
sendMoney,
|
sendMoney,
|
||||||
transferMoney,
|
transferMoney,
|
||||||
@ -31,6 +20,16 @@ import AccountInput from '#/components/account-input.vue';
|
|||||||
import { formatDate,
|
import { formatDate,
|
||||||
getAllowedSysOrigins } from '#/views/system/shared';
|
getAllowedSysOrigins } from '#/views/system/shared';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
Space,
|
||||||
|
Table,
|
||||||
|
message,
|
||||||
|
} from 'antdv-next';
|
||||||
|
|
||||||
import UserBankCreateModal from './components/user-bank-create-modal.vue';
|
import UserBankCreateModal from './components/user-bank-create-modal.vue';
|
||||||
import UserBankRunningWaterModal from './components/user-bank-running-water-modal.vue';
|
import UserBankRunningWaterModal from './components/user-bank-running-water-modal.vue';
|
||||||
import UserProfileLink from './components/user-profile-link.vue';
|
import UserProfileLink from './components/user-profile-link.vue';
|
||||||
@ -46,7 +45,6 @@ const sysOriginOptions = computed(() => {
|
|||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const loadMoreLoading = ref(false);
|
const loadMoreLoading = ref(false);
|
||||||
const exportLoading = ref(false);
|
const exportLoading = ref(false);
|
||||||
const statisticsLoading = ref(false);
|
|
||||||
const notData = ref(false);
|
const notData = ref(false);
|
||||||
const list = ref<Array<Record<string, any>>>([]);
|
const list = ref<Array<Record<string, any>>>([]);
|
||||||
const createOpen = ref(false);
|
const createOpen = ref(false);
|
||||||
@ -56,33 +54,16 @@ const importLoading = ref(false);
|
|||||||
const actionModalOpen = ref(false);
|
const actionModalOpen = ref(false);
|
||||||
const transferOpen = ref(false);
|
const transferOpen = ref(false);
|
||||||
const actionType = ref<'deduct' | 'send'>('send');
|
const actionType = ref<'deduct' | 'send'>('send');
|
||||||
const activeRow = ref<null | Record<string, any>>(null);
|
const activeRow = ref<Record<string, any> | null>(null);
|
||||||
const importInput = ref<HTMLInputElement | null>(null);
|
const importInput = ref<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
const query = reactive({
|
const query = reactive({
|
||||||
lastId: '',
|
lastId: '',
|
||||||
limit: 20,
|
limit: 20,
|
||||||
pageNo: 1,
|
|
||||||
sortField: '',
|
|
||||||
sortOrder: '',
|
|
||||||
sysOrigin: '',
|
sysOrigin: '',
|
||||||
userId: '',
|
userId: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
const statistics = reactive({
|
|
||||||
totalBalance: 0,
|
|
||||||
totalConsumptionPoints: 0,
|
|
||||||
totalEarnPoints: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
const sortableAmountKeys = new Set(['balance', 'consumptionPoints', 'earnPoints']);
|
|
||||||
|
|
||||||
type BankSortOrder = 'ascend' | 'descend';
|
|
||||||
|
|
||||||
const isSortActive = computed(() => {
|
|
||||||
return Boolean(query.sortField && query.sortOrder);
|
|
||||||
});
|
|
||||||
|
|
||||||
const actionForm = reactive({
|
const actionForm = reactive({
|
||||||
quantity: '',
|
quantity: '',
|
||||||
remark: '',
|
remark: '',
|
||||||
@ -98,96 +79,17 @@ const transferForm = reactive({
|
|||||||
userId: '',
|
userId: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
const columns = computed(() => [
|
const columns = [
|
||||||
{ dataIndex: 'index', key: 'index', title: 'No', width: 70 },
|
{ dataIndex: 'index', key: 'index', title: 'No', width: 70 },
|
||||||
{ dataIndex: 'sysOrigin', key: 'sysOrigin', title: '系统', width: 90 },
|
{ dataIndex: 'sysOrigin', key: 'sysOrigin', title: '系统', width: 90 },
|
||||||
{ dataIndex: 'userProfile', key: 'userProfile', title: '用户', width: 300 },
|
{ dataIndex: 'userProfile', key: 'userProfile', title: '用户', width: 300 },
|
||||||
{
|
{ dataIndex: 'earnPoints', key: 'earnPoints', title: '获得总额', width: 120 },
|
||||||
align: 'right' as const,
|
{ dataIndex: 'consumptionPoints', key: 'consumptionPoints', title: '消费总额', width: 120 },
|
||||||
dataIndex: 'earnPoints',
|
{ dataIndex: 'balance', key: 'balance', title: '余额', width: 120 },
|
||||||
key: 'earnPoints',
|
|
||||||
sorter: true,
|
|
||||||
sortOrder: getSortOrder('earnPoints'),
|
|
||||||
title: '获得总额',
|
|
||||||
width: 120,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
align: 'right' as const,
|
|
||||||
dataIndex: 'consumptionPoints',
|
|
||||||
key: 'consumptionPoints',
|
|
||||||
sorter: true,
|
|
||||||
sortOrder: getSortOrder('consumptionPoints'),
|
|
||||||
title: '消费总额',
|
|
||||||
width: 120,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
align: 'right' as const,
|
|
||||||
dataIndex: 'balance',
|
|
||||||
key: 'balance',
|
|
||||||
sorter: true,
|
|
||||||
sortOrder: getSortOrder('balance'),
|
|
||||||
title: '余额',
|
|
||||||
width: 120,
|
|
||||||
},
|
|
||||||
{ dataIndex: 'createTime', key: 'createTime', title: '创建时间', width: 180 },
|
{ dataIndex: 'createTime', key: 'createTime', title: '创建时间', width: 180 },
|
||||||
{ dataIndex: 'updateTime', key: 'updateTime', title: '修改时间', width: 180 },
|
{ dataIndex: 'updateTime', key: 'updateTime', title: '修改时间', width: 180 },
|
||||||
{ dataIndex: 'actions', key: 'actions', title: '操作', width: 260, fixed: 'right' as const },
|
{ dataIndex: 'actions', key: 'actions', title: '操作', width: 260, fixed: 'right' as const },
|
||||||
]);
|
];
|
||||||
|
|
||||||
function isBankSortOrder(value: string): value is BankSortOrder {
|
|
||||||
return value === 'ascend' || value === 'descend';
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSortOrder(field: string) {
|
|
||||||
return query.sortField === field && isBankSortOrder(query.sortOrder)
|
|
||||||
? query.sortOrder
|
|
||||||
: undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatAmount(value: any) {
|
|
||||||
const rawValue = String(value ?? '0').replaceAll(',', '');
|
|
||||||
if (!rawValue || rawValue === 'null' || rawValue === 'undefined') {
|
|
||||||
return '0';
|
|
||||||
}
|
|
||||||
const [integerPart = '0', decimalPart = ''] = rawValue.split('.');
|
|
||||||
const isNegative = integerPart.startsWith('-');
|
|
||||||
const unsignedInteger = isNegative ? integerPart.slice(1) : integerPart;
|
|
||||||
const formattedInteger =
|
|
||||||
unsignedInteger.replaceAll(/\B(?=(\d{3})+(?!\d))/g, ',') || '0';
|
|
||||||
const trimmedDecimal = decimalPart.replaceAll(/0+$/g, '');
|
|
||||||
return `${isNegative ? '-' : ''}${formattedInteger}${trimmedDecimal ? `.${trimmedDecimal}` : ''}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildListParams() {
|
|
||||||
return {
|
|
||||||
...query,
|
|
||||||
lastId: isSortActive.value ? undefined : query.lastId,
|
|
||||||
pageNo: isSortActive.value ? query.pageNo : undefined,
|
|
||||||
sortField: isSortActive.value ? query.sortField : undefined,
|
|
||||||
sortOrder: isSortActive.value ? query.sortOrder : undefined,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildStatisticsParams() {
|
|
||||||
return {
|
|
||||||
sysOrigin: query.sysOrigin,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadStatistics() {
|
|
||||||
if (!query.sysOrigin) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
statisticsLoading.value = true;
|
|
||||||
try {
|
|
||||||
const result = await getBankStatistics(buildStatisticsParams());
|
|
||||||
statistics.totalEarnPoints = result?.totalEarnPoints ?? 0;
|
|
||||||
statistics.totalConsumptionPoints = result?.totalConsumptionPoints ?? 0;
|
|
||||||
statistics.totalBalance = result?.totalBalance ?? 0;
|
|
||||||
} finally {
|
|
||||||
statisticsLoading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
sysOriginOptions,
|
sysOriginOptions,
|
||||||
@ -205,7 +107,6 @@ async function loadData(reset = false) {
|
|||||||
}
|
}
|
||||||
if (reset) {
|
if (reset) {
|
||||||
query.lastId = '';
|
query.lastId = '';
|
||||||
query.pageNo = 1;
|
|
||||||
list.value = [];
|
list.value = [];
|
||||||
notData.value = false;
|
notData.value = false;
|
||||||
}
|
}
|
||||||
@ -215,21 +116,13 @@ async function loadData(reset = false) {
|
|||||||
loadMoreLoading.value = true;
|
loadMoreLoading.value = true;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const result = reset
|
const result = await pageBank({ ...query });
|
||||||
? await Promise.all([pageBank(buildListParams()), loadStatistics()]).then(
|
|
||||||
([rows]) => rows,
|
|
||||||
)
|
|
||||||
: await pageBank(buildListParams());
|
|
||||||
const current = result || [];
|
const current = result || [];
|
||||||
notData.value = current.length < Number(query.limit || 20);
|
notData.value = current.length <= 0;
|
||||||
if (current.length > 0) {
|
if (!notData.value) {
|
||||||
list.value = [...list.value, ...current];
|
list.value = [...list.value, ...current];
|
||||||
if (isSortActive.value) {
|
|
||||||
query.pageNo += 1;
|
|
||||||
} else {
|
|
||||||
query.lastId = String(list.value[list.value.length - 1]?.id || '');
|
query.lastId = String(list.value[list.value.length - 1]?.id || '');
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
loadMoreLoading.value = false;
|
loadMoreLoading.value = false;
|
||||||
@ -240,20 +133,6 @@ function handleSearch() {
|
|||||||
void loadData(true);
|
void loadData(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleTableChange(_: any, __: any, sorter: any) {
|
|
||||||
const activeSorter = Array.isArray(sorter) ? sorter[0] : sorter;
|
|
||||||
const field = String(activeSorter?.columnKey || '');
|
|
||||||
const order = String(activeSorter?.order || '');
|
|
||||||
if (isBankSortOrder(order) && sortableAmountKeys.has(field)) {
|
|
||||||
query.sortField = field;
|
|
||||||
query.sortOrder = order;
|
|
||||||
} else {
|
|
||||||
query.sortField = '';
|
|
||||||
query.sortOrder = '';
|
|
||||||
}
|
|
||||||
void loadData(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
function openAction(record: Record<string, any>, type: 'deduct' | 'send') {
|
function openAction(record: Record<string, any>, type: 'deduct' | 'send') {
|
||||||
activeRow.value = record;
|
activeRow.value = record;
|
||||||
actionType.value = type;
|
actionType.value = type;
|
||||||
@ -286,7 +165,11 @@ async function submitAction() {
|
|||||||
}
|
}
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
await (actionType.value === 'send' ? sendMoney : deductMoney)({ ...actionForm });
|
if (actionType.value === 'send') {
|
||||||
|
await sendMoney({ ...actionForm });
|
||||||
|
} else {
|
||||||
|
await deductMoney({ ...actionForm });
|
||||||
|
}
|
||||||
message.success('操作成功');
|
message.success('操作成功');
|
||||||
actionModalOpen.value = false;
|
actionModalOpen.value = false;
|
||||||
await loadData(true);
|
await loadData(true);
|
||||||
@ -369,28 +252,11 @@ watch(
|
|||||||
<template>
|
<template>
|
||||||
<Page title="用户银行账户">
|
<Page title="用户银行账户">
|
||||||
<Card>
|
<Card>
|
||||||
<div class="statistics-card" :class="{ 'is-loading': statisticsLoading }">
|
|
||||||
<div class="statistics-item">
|
|
||||||
<span class="statistics-label">所有用户获得总额</span>
|
|
||||||
<strong>{{ formatAmount(statistics.totalEarnPoints) }}</strong>
|
|
||||||
</div>
|
|
||||||
<div class="statistics-item">
|
|
||||||
<span class="statistics-label">所有用户消费总额</span>
|
|
||||||
<strong>{{ formatAmount(statistics.totalConsumptionPoints) }}</strong>
|
|
||||||
</div>
|
|
||||||
<div class="statistics-item">
|
|
||||||
<span class="statistics-label">所有用户余额</span>
|
|
||||||
<strong>{{ formatAmount(statistics.totalBalance) }}</strong>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
<SysOriginSelect
|
<SysOriginSelect v-model:value="query.sysOrigin" style="width: 140px"
|
||||||
v-model:value="query.sysOrigin"
|
|
||||||
:options="sysOriginOptions"
|
:options="sysOriginOptions"
|
||||||
style="width: 140px"
|
></SysOriginSelect>
|
||||||
/>
|
|
||||||
<AccountInput
|
<AccountInput
|
||||||
v-model:value="query.userId"
|
v-model:value="query.userId"
|
||||||
:sys-origin="query.sysOrigin"
|
:sys-origin="query.sysOrigin"
|
||||||
@ -413,7 +279,6 @@ watch(
|
|||||||
:pagination="false"
|
:pagination="false"
|
||||||
row-key="id"
|
row-key="id"
|
||||||
:scroll="{ x: 1500 }"
|
:scroll="{ x: 1500 }"
|
||||||
@change="handleTableChange"
|
|
||||||
>
|
>
|
||||||
<template #bodyCell="{ column, record, index }">
|
<template #bodyCell="{ column, record, index }">
|
||||||
<template v-if="column.key === 'index'">
|
<template v-if="column.key === 'index'">
|
||||||
@ -422,15 +287,6 @@ watch(
|
|||||||
<template v-else-if="column.key === 'userProfile'">
|
<template v-else-if="column.key === 'userProfile'">
|
||||||
<UserProfileLink :profile="record.userProfile" />
|
<UserProfileLink :profile="record.userProfile" />
|
||||||
</template>
|
</template>
|
||||||
<template
|
|
||||||
v-else-if="
|
|
||||||
column.key === 'earnPoints' ||
|
|
||||||
column.key === 'consumptionPoints' ||
|
|
||||||
column.key === 'balance'
|
|
||||||
"
|
|
||||||
>
|
|
||||||
{{ formatAmount(record[column.key]) }}
|
|
||||||
</template>
|
|
||||||
<template v-else-if="column.key === 'createTime'">
|
<template v-else-if="column.key === 'createTime'">
|
||||||
{{ formatDate(record.createTime) }}
|
{{ formatDate(record.createTime) }}
|
||||||
</template>
|
</template>
|
||||||
@ -582,7 +438,7 @@ watch(
|
|||||||
class="hidden-input"
|
class="hidden-input"
|
||||||
type="file"
|
type="file"
|
||||||
@change="handleImportChange"
|
@change="handleImportChange"
|
||||||
/>
|
>
|
||||||
<Button :loading="importLoading" block @click="chooseImportFile">
|
<Button :loading="importLoading" block @click="chooseImportFile">
|
||||||
选择文件并导入
|
选择文件并导入
|
||||||
</Button>
|
</Button>
|
||||||
@ -592,39 +448,6 @@ watch(
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.statistics-card {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
||||||
gap: 12px;
|
|
||||||
padding: 14px 16px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
background: #f8fafc;
|
|
||||||
border: 1px solid #e5e7eb;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.statistics-card.is-loading {
|
|
||||||
opacity: 0.68;
|
|
||||||
}
|
|
||||||
|
|
||||||
.statistics-item {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.statistics-label {
|
|
||||||
color: #64748b;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.statistics-item strong {
|
|
||||||
color: #0f172a;
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 1.25;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar {
|
.toolbar {
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -79,13 +79,6 @@ interface StructuredResourceFile {
|
|||||||
type: string;
|
type: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UploadTask {
|
|
||||||
file: File;
|
|
||||||
fileName: string;
|
|
||||||
kind: AssetKind;
|
|
||||||
row: BatchRow;
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
initialSysOrigin?: number | string;
|
initialSysOrigin?: number | string;
|
||||||
initialType?: number | string;
|
initialType?: number | string;
|
||||||
@ -116,9 +109,6 @@ const sequence = ref(0);
|
|||||||
const showOnlyErrors = ref(false);
|
const showOnlyErrors = ref(false);
|
||||||
const submitting = ref(false);
|
const submitting = ref(false);
|
||||||
const droppedFilePathMap = new WeakMap<File, string>();
|
const droppedFilePathMap = new WeakMap<File, string>();
|
||||||
const uploadQueue: UploadTask[] = [];
|
|
||||||
const MAX_UPLOAD_CONCURRENCY = 1;
|
|
||||||
let activeUploadCount = 0;
|
|
||||||
|
|
||||||
const propsTypeOptions = PROPS_RESOURCE_CONFIG_TYPES.map((item) => ({
|
const propsTypeOptions = PROPS_RESOURCE_CONFIG_TYPES.map((item) => ({
|
||||||
label: item.name,
|
label: item.name,
|
||||||
@ -253,7 +243,6 @@ function resetBatch() {
|
|||||||
activeRowKey.value = '';
|
activeRowKey.value = '';
|
||||||
showOnlyErrors.value = false;
|
showOnlyErrors.value = false;
|
||||||
sequence.value = 0;
|
sequence.value = 0;
|
||||||
uploadQueue.length = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
@ -467,26 +456,7 @@ function setRowAsset(row: BatchRow, kind: AssetKind, file: File) {
|
|||||||
}
|
}
|
||||||
row.saved = false;
|
row.saved = false;
|
||||||
refreshAllRows();
|
refreshAllRows();
|
||||||
enqueueUpload({ file, fileName, kind, row });
|
void uploadAsset(row, kind, file, fileName);
|
||||||
}
|
|
||||||
|
|
||||||
function enqueueUpload(task: UploadTask) {
|
|
||||||
uploadQueue.push(task);
|
|
||||||
runUploadQueue();
|
|
||||||
}
|
|
||||||
|
|
||||||
function runUploadQueue() {
|
|
||||||
while (activeUploadCount < MAX_UPLOAD_CONCURRENCY && uploadQueue.length > 0) {
|
|
||||||
const task = uploadQueue.shift();
|
|
||||||
if (!task) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
activeUploadCount += 1;
|
|
||||||
void uploadAsset(task.row, task.kind, task.file, task.fileName).finally(() => {
|
|
||||||
activeUploadCount -= 1;
|
|
||||||
runUploadQueue();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function uploadAsset(
|
async function uploadAsset(
|
||||||
@ -1440,18 +1410,6 @@ function customRow(record: Record<string, any>) {
|
|||||||
width: 52px;
|
width: 52px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.asset-cover {
|
|
||||||
border-radius: 6px;
|
|
||||||
display: block;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.asset-cover :deep(.ant-image-img) {
|
|
||||||
height: 52px;
|
|
||||||
object-fit: contain;
|
|
||||||
width: 52px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.asset-empty {
|
.asset-empty {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: #f8fafc;
|
background: #f8fafc;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -85,8 +85,6 @@ const adminFreeOptions = [
|
|||||||
];
|
];
|
||||||
const DEFAULT_STORE_CURRENCY_TYPES = 'GOLD';
|
const DEFAULT_STORE_CURRENCY_TYPES = 'GOLD';
|
||||||
const DEFAULT_STORE_VALID_DAYS = '7';
|
const DEFAULT_STORE_VALID_DAYS = '7';
|
||||||
const DEFAULT_RESOURCE_CONFIG_TYPE =
|
|
||||||
(PROPS_RESOURCE_CONFIG_TYPES[0]?.value as string | undefined) ?? 'AVATAR_FRAME';
|
|
||||||
|
|
||||||
const query = reactive({
|
const query = reactive({
|
||||||
cursor: 1,
|
cursor: 1,
|
||||||
@ -99,7 +97,9 @@ const query = reactive({
|
|||||||
limit: 20,
|
limit: 20,
|
||||||
name: '',
|
name: '',
|
||||||
sysOrigin: sysOriginOptions.value[0]?.value ?? 'LIKEI',
|
sysOrigin: sysOriginOptions.value[0]?.value ?? 'LIKEI',
|
||||||
type: DEFAULT_RESOURCE_CONFIG_TYPE as string | undefined,
|
type: (PROPS_RESOURCE_CONFIG_TYPES[0]?.value ?? 'AVATAR_FRAME') as
|
||||||
|
| string
|
||||||
|
| undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
@ -203,7 +203,7 @@ function resetColumnFilter(key: unknown) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'typeName': {
|
case 'typeName': {
|
||||||
query.type = DEFAULT_RESOURCE_CONFIG_TYPE;
|
query.type = undefined;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -243,7 +243,7 @@ function resetAllColumnFilters() {
|
|||||||
query.del = undefined;
|
query.del = undefined;
|
||||||
query.id = '';
|
query.id = '';
|
||||||
query.name = '';
|
query.name = '';
|
||||||
query.type = DEFAULT_RESOURCE_CONFIG_TYPE;
|
query.type = undefined;
|
||||||
void loadData(true);
|
void loadData(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -598,6 +598,7 @@ loadData(true);
|
|||||||
<template v-else-if="column.key === 'typeName'">
|
<template v-else-if="column.key === 'typeName'">
|
||||||
<Select
|
<Select
|
||||||
v-model:value="query.type"
|
v-model:value="query.type"
|
||||||
|
allow-clear
|
||||||
option-label-prop="label"
|
option-label-prop="label"
|
||||||
placeholder="选择类型"
|
placeholder="选择类型"
|
||||||
:options="PROPS_RESOURCE_CONFIG_TYPES.map((item) => ({ label: `${item.name}`, value: item.value as any }))"
|
:options="PROPS_RESOURCE_CONFIG_TYPES.map((item) => ({ label: `${item.name}`, value: item.value as any }))"
|
||||||
|
|||||||
@ -13,12 +13,12 @@ import {
|
|||||||
InputNumber,
|
InputNumber,
|
||||||
message,
|
message,
|
||||||
Pagination,
|
Pagination,
|
||||||
|
Select,
|
||||||
Spin,
|
Spin,
|
||||||
Tag,
|
Tag,
|
||||||
} from 'antdv-next';
|
} from 'antdv-next';
|
||||||
|
|
||||||
import { listBadgePictureBySysOrigin } from '#/api/legacy/badge';
|
import { listBadgePictureBySysOrigin } from '#/api/legacy/badge';
|
||||||
import { listGiftBySysOrigin } from '#/api/legacy/gift';
|
|
||||||
import {
|
import {
|
||||||
listNotFamilyBySysOriginType,
|
listNotFamilyBySysOriginType,
|
||||||
sendPropsGiveUser,
|
sendPropsGiveUser,
|
||||||
@ -31,7 +31,7 @@ import { BADGE_TYPE_OPTIONS, PROPS_TYPES } from './shared';
|
|||||||
|
|
||||||
defineOptions({ name: 'PropsSendTool' });
|
defineOptions({ name: 'PropsSendTool' });
|
||||||
|
|
||||||
type ResourceKind = 'badge' | 'gift' | 'props';
|
type ResourceKind = 'badge' | 'props';
|
||||||
|
|
||||||
type TypeOption = {
|
type TypeOption = {
|
||||||
icon: string;
|
icon: string;
|
||||||
@ -50,7 +50,6 @@ type DisplayResource = {
|
|||||||
name: string;
|
name: string;
|
||||||
raw: Record<string, any>;
|
raw: Record<string, any>;
|
||||||
secondaryType: string;
|
secondaryType: string;
|
||||||
type: string;
|
|
||||||
typeLabel: string;
|
typeLabel: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -63,11 +62,9 @@ type RecipientProfile = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const BADGE_TYPE_VALUE = '__BADGE__';
|
const BADGE_TYPE_VALUE = '__BADGE__';
|
||||||
const GIFT_TYPE_VALUE = '__GIFT__';
|
|
||||||
const MAX_BATCH_ACCOUNTS = 15;
|
const MAX_BATCH_ACCOUNTS = 15;
|
||||||
const RESOURCE_PAGE_SIZE = 72;
|
const RESOURCE_PAGE_SIZE = 72;
|
||||||
const QUICK_DAYS = [1, 7, 15, 30, -1, -7];
|
const QUICK_DAYS = [1, 7, 15, 30, -1, -7];
|
||||||
const QUICK_GIFT_QUANTITIES = [1, 5, 10, 50, 100];
|
|
||||||
const USER_AVATAR_FALLBACK =
|
const USER_AVATAR_FALLBACK =
|
||||||
'https://dummyimage.com/40x40/e2e8f0/64748b&text=U';
|
'https://dummyimage.com/40x40/e2e8f0/64748b&text=U';
|
||||||
|
|
||||||
@ -79,7 +76,14 @@ const sysOriginOptions = computed(() => {
|
|||||||
return options.length > 0 ? options : getAllowedSysOrigins([]);
|
return options.length > 0 ? options : getAllowedSysOrigins([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
const propsTypeOptions = PROPS_TYPES.filter((item) => item.value !== 'CUSTOMIZE');
|
const propsTypeOptions = PROPS_TYPES.filter(
|
||||||
|
(item) => item.value !== 'CUSTOMIZE' && item.value !== 'FRAGMENTS',
|
||||||
|
);
|
||||||
|
const vipOriginOptions = [
|
||||||
|
{ label: '系统赠送', value: 'SYSTEM_GIVE' as any },
|
||||||
|
{ label: '购买或朋友赠送', value: 'BUY_OR_GIVE' as any },
|
||||||
|
{ label: '活动奖励', value: 'ACTIVITY_AWARD' as any },
|
||||||
|
];
|
||||||
|
|
||||||
const propsFormLoading = ref(false);
|
const propsFormLoading = ref(false);
|
||||||
const sourceLoading = ref(false);
|
const sourceLoading = ref(false);
|
||||||
@ -94,10 +98,10 @@ let recipientSearchTimer: number | undefined;
|
|||||||
|
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
exchangeDays: '',
|
exchangeDays: '',
|
||||||
giftQuantity: 1,
|
|
||||||
receiverAccounts: '',
|
receiverAccounts: '',
|
||||||
secondaryType: '',
|
secondaryType: '',
|
||||||
sysOrigin: sysOriginOptions.value[0]?.value ?? '',
|
sysOrigin: sysOriginOptions.value[0]?.value ?? '',
|
||||||
|
vipOrigin: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
function hasPermission(code: string) {
|
function hasPermission(code: string) {
|
||||||
@ -109,12 +113,7 @@ const canSendProps = computed(() => hasPermission('send:props'));
|
|||||||
const canSendBadge = computed(
|
const canSendBadge = computed(
|
||||||
() => canSendProps.value || hasPermission('send:props:badge'),
|
() => canSendProps.value || hasPermission('send:props:badge'),
|
||||||
);
|
);
|
||||||
const canSendGift = computed(
|
const canUsePage = computed(() => canSendProps.value || canSendBadge.value);
|
||||||
() => canSendProps.value || hasPermission('send:props:GIFT'),
|
|
||||||
);
|
|
||||||
const canUsePage = computed(
|
|
||||||
() => canSendProps.value || canSendBadge.value || canSendGift.value,
|
|
||||||
);
|
|
||||||
|
|
||||||
const typeOptions = computed<TypeOption[]>(() => {
|
const typeOptions = computed<TypeOption[]>(() => {
|
||||||
const options: TypeOption[] = [];
|
const options: TypeOption[] = [];
|
||||||
@ -136,14 +135,6 @@ const typeOptions = computed<TypeOption[]>(() => {
|
|||||||
value: BADGE_TYPE_VALUE,
|
value: BADGE_TYPE_VALUE,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (canSendGift.value) {
|
|
||||||
options.push({
|
|
||||||
icon: 'lucide:gift',
|
|
||||||
kind: 'gift',
|
|
||||||
label: '礼物',
|
|
||||||
value: GIFT_TYPE_VALUE,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return options;
|
return options;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -165,27 +156,14 @@ const displayedRecipients = computed(() =>
|
|||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
const sendActionText = computed(() => (Number(form.exchangeDays) < 0 ? '回收' : '赠送'));
|
||||||
const selectedCount = computed(() => selectedResources.value.length);
|
const selectedCount = computed(() => selectedResources.value.length);
|
||||||
const selectedHasGift = computed(() =>
|
const needsVipOrigin = computed(
|
||||||
selectedResources.value.some((item) => item.kind === 'gift'),
|
() =>
|
||||||
|
selectedResources.value.some(
|
||||||
|
(item) => item.kind === 'props' && item.secondaryType === 'NOBLE_VIP',
|
||||||
|
),
|
||||||
);
|
);
|
||||||
const selectedHasNonGift = computed(() =>
|
|
||||||
selectedResources.value.some((item) => item.kind !== 'gift'),
|
|
||||||
);
|
|
||||||
const currentTypeIsGift = computed(() => currentTypeOption.value?.kind === 'gift');
|
|
||||||
const showGiftQuantity = computed(() => selectedHasGift.value || currentTypeIsGift.value);
|
|
||||||
const showExchangeDays = computed(
|
|
||||||
() => selectedHasNonGift.value || (!selectedHasGift.value && !currentTypeIsGift.value),
|
|
||||||
);
|
|
||||||
const sendActionText = computed(() => {
|
|
||||||
if (selectedHasGift.value && selectedHasNonGift.value) {
|
|
||||||
return Number(form.exchangeDays) < 0 ? '礼物赠送 / 道具回收' : '赠送';
|
|
||||||
}
|
|
||||||
if (selectedHasGift.value) {
|
|
||||||
return '赠送';
|
|
||||||
}
|
|
||||||
return Number(form.exchangeDays) < 0 ? '回收' : '赠送';
|
|
||||||
});
|
|
||||||
const selectedTypeSummary = computed(() => {
|
const selectedTypeSummary = computed(() => {
|
||||||
const labels = [
|
const labels = [
|
||||||
...new Set(selectedResources.value.map((item) => item.typeLabel).filter(Boolean)),
|
...new Set(selectedResources.value.map((item) => item.typeLabel).filter(Boolean)),
|
||||||
@ -353,19 +331,17 @@ function normalizeDisplayResource(
|
|||||||
secondaryType: string,
|
secondaryType: string,
|
||||||
typeLabel: string,
|
typeLabel: string,
|
||||||
): DisplayResource {
|
): DisplayResource {
|
||||||
let id = firstPresent(item, ['id', 'resourceId', 'propsSourceId', 'propsId']);
|
const id =
|
||||||
let name = firstPresent(item, ['name', 'resourceName', 'sourceName']);
|
kind === 'badge'
|
||||||
if (kind === 'badge') {
|
? firstPresent(item, ['badgeConfigId', 'id', 'badgeId'])
|
||||||
id = firstPresent(item, ['badgeConfigId', 'id', 'badgeId']);
|
: firstPresent(item, ['id', 'resourceId', 'propsSourceId', 'propsId']);
|
||||||
name = firstPresent(item, ['badgeName', 'name']);
|
const name =
|
||||||
} else if (kind === 'gift') {
|
kind === 'badge'
|
||||||
id = firstPresent(item, ['id', 'giftId']);
|
? firstPresent(item, ['badgeName', 'name'])
|
||||||
name = firstPresent(item, ['giftName', 'name']);
|
: firstPresent(item, ['name', 'resourceName', 'sourceName']);
|
||||||
}
|
|
||||||
const cover = firstPresent(item, [
|
const cover = firstPresent(item, [
|
||||||
'cover',
|
'cover',
|
||||||
'coverUrl',
|
'coverUrl',
|
||||||
'giftPhoto',
|
|
||||||
'resourceCover',
|
'resourceCover',
|
||||||
'resourceCoverUrl',
|
'resourceCoverUrl',
|
||||||
'selectUrl',
|
'selectUrl',
|
||||||
@ -373,7 +349,7 @@ function normalizeDisplayResource(
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
amount: firstPresent(item, ['amount', 'giftCandy', 'price', 'goldAmount']),
|
amount: firstPresent(item, ['amount', 'price', 'goldAmount']),
|
||||||
badgeTypeLabel: kind === 'badge' ? typeLabel : '',
|
badgeTypeLabel: kind === 'badge' ? typeLabel : '',
|
||||||
cover: String(cover || ''),
|
cover: String(cover || ''),
|
||||||
id: id as number | string,
|
id: id as number | string,
|
||||||
@ -382,7 +358,6 @@ function normalizeDisplayResource(
|
|||||||
name: String(name || (id ? `资源 ${id}` : '-')),
|
name: String(name || (id ? `资源 ${id}` : '-')),
|
||||||
raw: item,
|
raw: item,
|
||||||
secondaryType,
|
secondaryType,
|
||||||
type: secondaryType,
|
|
||||||
typeLabel,
|
typeLabel,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -412,15 +387,12 @@ function setQuickDays(value: number) {
|
|||||||
form.exchangeDays = String(value);
|
form.exchangeDays = String(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setQuickGiftQuantity(value: number) {
|
|
||||||
form.giftQuantity = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectType(option: TypeOption) {
|
function selectType(option: TypeOption) {
|
||||||
if (form.secondaryType === option.value) {
|
if (form.secondaryType === option.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
form.secondaryType = option.value;
|
form.secondaryType = option.value;
|
||||||
|
form.vipOrigin = option.value === 'NOBLE_VIP' ? form.vipOrigin : '';
|
||||||
resourceKeyword.value = '';
|
resourceKeyword.value = '';
|
||||||
resourcePage.value = 1;
|
resourcePage.value = 1;
|
||||||
sourceOptions.value = [];
|
sourceOptions.value = [];
|
||||||
@ -457,14 +429,6 @@ async function loadSources() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type.kind === 'gift') {
|
|
||||||
const rows = await listGiftBySysOrigin(form.sysOrigin);
|
|
||||||
sourceOptions.value = (rows || []).map((item) =>
|
|
||||||
normalizeDisplayResource(item, 'gift', 'GIFT', '礼物'),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rows = await listNotFamilyBySysOriginType(form.sysOrigin, type.value);
|
const rows = await listNotFamilyBySysOriginType(form.sysOrigin, type.value);
|
||||||
sourceOptions.value = (rows || []).map((item) =>
|
sourceOptions.value = (rows || []).map((item) =>
|
||||||
normalizeDisplayResource(item, 'props', type.value, type.label),
|
normalizeDisplayResource(item, 'props', type.value, type.label),
|
||||||
@ -519,37 +483,18 @@ function validateBeforeSend() {
|
|||||||
message.warning('请选择资源');
|
message.warning('请选择资源');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (
|
if (form.exchangeDays === '' || Number.isNaN(Number(form.exchangeDays))) {
|
||||||
selectedHasNonGift.value &&
|
|
||||||
(form.exchangeDays === '' || Number.isNaN(Number(form.exchangeDays)))
|
|
||||||
) {
|
|
||||||
message.warning('请输入有效天数');
|
message.warning('请输入有效天数');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (
|
if (needsVipOrigin.value && !form.vipOrigin) {
|
||||||
selectedHasGift.value &&
|
message.warning('请选择贵族来源');
|
||||||
(!Number.isFinite(Number(form.giftQuantity)) || Number(form.giftQuantity) <= 0)
|
|
||||||
) {
|
|
||||||
message.warning('请输入礼物数量');
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return accounts;
|
return accounts;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildSendPayload(resource: DisplayResource, accounts: string[]) {
|
function buildSendPayload(resource: DisplayResource, accounts: string[]) {
|
||||||
if (resource.kind === 'gift') {
|
|
||||||
const quantity = Number(form.giftQuantity);
|
|
||||||
return {
|
|
||||||
content: resource.id,
|
|
||||||
exchangeDays: quantity,
|
|
||||||
quantity,
|
|
||||||
receiverAccounts: accounts.join(','),
|
|
||||||
secondaryType: 'GIFT',
|
|
||||||
sysOrigin: form.sysOrigin,
|
|
||||||
type: 'GIFT',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload: Record<string, any> = {
|
const payload: Record<string, any> = {
|
||||||
content: resource.id,
|
content: resource.id,
|
||||||
exchangeDays: Number(form.exchangeDays),
|
exchangeDays: Number(form.exchangeDays),
|
||||||
@ -559,6 +504,11 @@ function buildSendPayload(resource: DisplayResource, accounts: string[]) {
|
|||||||
type: resource.kind === 'badge' ? 'BADGE' : 'PROPS',
|
type: resource.kind === 'badge' ? 'BADGE' : 'PROPS',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (resource.kind === 'props') {
|
||||||
|
payload.vipOrigin =
|
||||||
|
resource.secondaryType === 'NOBLE_VIP' ? form.vipOrigin : null;
|
||||||
|
}
|
||||||
|
|
||||||
return payload;
|
return payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -592,7 +542,7 @@ function resetCurrentForm() {
|
|||||||
form.receiverAccounts = '';
|
form.receiverAccounts = '';
|
||||||
form.secondaryType = '';
|
form.secondaryType = '';
|
||||||
form.exchangeDays = '';
|
form.exchangeDays = '';
|
||||||
form.giftQuantity = 1;
|
form.vipOrigin = '';
|
||||||
resourceKeyword.value = '';
|
resourceKeyword.value = '';
|
||||||
resourcePage.value = 1;
|
resourcePage.value = 1;
|
||||||
sourceOptions.value = [];
|
sourceOptions.value = [];
|
||||||
@ -601,7 +551,7 @@ function resetCurrentForm() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Page title="资源赠送">
|
<Page title="道具赠送">
|
||||||
<div v-if="canUsePage" class="send-tool">
|
<div v-if="canUsePage" class="send-tool">
|
||||||
<div class="send-layout">
|
<div class="send-layout">
|
||||||
<section class="send-main">
|
<section class="send-main">
|
||||||
@ -737,7 +687,7 @@ function resetCurrentForm() {
|
|||||||
<aside class="send-summary">
|
<aside class="send-summary">
|
||||||
<div class="summary-title">
|
<div class="summary-title">
|
||||||
<IconifyIcon icon="lucide:package-check" />
|
<IconifyIcon icon="lucide:package-check" />
|
||||||
<span>赠送资源</span>
|
<span>赠送道具</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="selected-panel">
|
<div class="selected-panel">
|
||||||
@ -782,7 +732,7 @@ function resetCurrentForm() {
|
|||||||
<div v-else class="summary-empty">未选择资源</div>
|
<div v-else class="summary-empty">未选择资源</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="showExchangeDays" class="summary-control summary-control--days">
|
<div class="summary-control summary-control--days">
|
||||||
<div class="summary-control__row">
|
<div class="summary-control__row">
|
||||||
<label class="summary-control__label" for="props-send-days">
|
<label class="summary-control__label" for="props-send-days">
|
||||||
有效天数
|
有效天数
|
||||||
@ -810,34 +760,16 @@ function resetCurrentForm() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="showGiftQuantity" class="summary-control summary-control--days">
|
<div v-if="needsVipOrigin" class="summary-control">
|
||||||
<div class="summary-control__row">
|
<label class="summary-control__label">贵族来源</label>
|
||||||
<label class="summary-control__label" for="gift-send-quantity">
|
<Select
|
||||||
礼物数量
|
v-model:value="form.vipOrigin"
|
||||||
</label>
|
:options="vipOriginOptions"
|
||||||
<InputNumber
|
class="summary-vip-origin"
|
||||||
id="gift-send-quantity"
|
option-label-prop="label"
|
||||||
v-model:value="form.giftQuantity"
|
placeholder="请选择贵族来源"
|
||||||
:controls="false"
|
|
||||||
:min="1"
|
|
||||||
:precision="0"
|
|
||||||
class="summary-days-input"
|
|
||||||
placeholder="请输入数量"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="quick-days summary-quick-days">
|
|
||||||
<button
|
|
||||||
v-for="quantity in QUICK_GIFT_QUANTITIES"
|
|
||||||
:key="quantity"
|
|
||||||
:class="{ 'quick-day--active': Number(form.giftQuantity) === quantity }"
|
|
||||||
class="quick-day"
|
|
||||||
type="button"
|
|
||||||
@click="setQuickGiftQuantity(quantity)"
|
|
||||||
>
|
|
||||||
{{ quantity }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<dl class="summary-list">
|
<dl class="summary-list">
|
||||||
<div>
|
<div>
|
||||||
@ -1324,7 +1256,8 @@ function resetCurrentForm() {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary-days-input {
|
.summary-days-input,
|
||||||
|
.summary-vip-origin {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -8,8 +8,12 @@ export interface OptionItem {
|
|||||||
export const PROPS_TYPES: OptionItem[] = [
|
export const PROPS_TYPES: OptionItem[] = [
|
||||||
{ value: 'AVATAR_FRAME', name: '头像框' },
|
{ value: 'AVATAR_FRAME', name: '头像框' },
|
||||||
{ value: 'RIDE', name: '座驾' },
|
{ value: 'RIDE', name: '座驾' },
|
||||||
|
{ value: 'NOBLE_VIP', name: '贵族' },
|
||||||
|
{ value: 'THEME', name: '主题' },
|
||||||
|
{ value: 'LAYOUT', name: '装扮' },
|
||||||
{ value: 'CHAT_BUBBLE', name: '聊天气泡' },
|
{ value: 'CHAT_BUBBLE', name: '聊天气泡' },
|
||||||
{ value: 'FLOAT_PICTURE', name: '飘窗' },
|
{ value: 'FLOAT_PICTURE', name: '飘窗' },
|
||||||
|
{ value: 'FRAGMENTS', name: '碎片' },
|
||||||
{ value: 'DATA_CARD', name: '资料卡' },
|
{ value: 'DATA_CARD', name: '资料卡' },
|
||||||
{ value: 'SPECIAL_ID', name: '靓号' },
|
{ value: 'SPECIAL_ID', name: '靓号' },
|
||||||
{ value: 'CUSTOMIZE', name: '自定义(只读)' },
|
{ value: 'CUSTOMIZE', name: '自定义(只读)' },
|
||||||
@ -116,7 +120,7 @@ export const PROPS_SOURCE_GROUP_TYPE_MAP: Record<
|
|||||||
> = {
|
> = {
|
||||||
AVATAR_FRAME: { value: 'PROPS', name: '头像框' },
|
AVATAR_FRAME: { value: 'PROPS', name: '头像框' },
|
||||||
RIDE: { value: 'PROPS', name: '座驾' },
|
RIDE: { value: 'PROPS', name: '座驾' },
|
||||||
NOBLE_VIP: { value: 'PROPS', name: 'VIP' },
|
NOBLE_VIP: { value: 'PROPS', name: '贵族' },
|
||||||
THEME: { value: 'PROPS', name: '房间背景主题' },
|
THEME: { value: 'PROPS', name: '房间背景主题' },
|
||||||
GIFT: { value: 'GIFT', name: '礼物' },
|
GIFT: { value: 'GIFT', name: '礼物' },
|
||||||
SPECIAL_ID: { value: 'SPECIAL_ID', name: '靓号' },
|
SPECIAL_ID: { value: 'SPECIAL_ID', name: '靓号' },
|
||||||
@ -248,7 +252,6 @@ export function getPropsTypeName(value?: null | string) {
|
|||||||
return (
|
return (
|
||||||
PROPS_TYPES.find((item) => item.value === value)?.name ||
|
PROPS_TYPES.find((item) => item.value === value)?.name ||
|
||||||
PROPS_STORE_TYPES.find((item) => item.value === value)?.name ||
|
PROPS_STORE_TYPES.find((item) => item.value === value)?.name ||
|
||||||
BADGE_TYPE_OPTIONS.find((item) => item.value === value)?.name ||
|
|
||||||
PROPS_SOURCE_GROUP_TYPE_MAP[value]?.name ||
|
PROPS_SOURCE_GROUP_TYPE_MAP[value]?.name ||
|
||||||
value
|
value
|
||||||
);
|
);
|
||||||
@ -280,12 +283,7 @@ export function isPropsCouponType(type?: null | string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function isBadgeRewardType(type?: null | string) {
|
export function isBadgeRewardType(type?: null | string) {
|
||||||
return (
|
return type === 'BADGE' || type === 'ROOM_BADGE' || type === 'HONOR_ACTIVITY';
|
||||||
type === 'BADGE' ||
|
|
||||||
type === 'ROOM_BADGE' ||
|
|
||||||
type === 'HONOR_ACTIVITY' ||
|
|
||||||
BADGE_TYPE_OPTIONS.some((item) => item.value === type)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isPropsSourceType(type?: null | string) {
|
export function isPropsSourceType(type?: null | string) {
|
||||||
@ -324,12 +322,6 @@ export function formatRewardText(item: Record<string, any>) {
|
|||||||
quantity <= 0 ? '永久' : `${quantity}天`
|
quantity <= 0 ? '永久' : `${quantity}天`
|
||||||
}`;
|
}`;
|
||||||
}
|
}
|
||||||
if (detailType === 'NOBLE_VIP') {
|
|
||||||
const quantity = Number(item.quantity || 0);
|
|
||||||
return `${item.name || item.remark || item.content || '-'} ${
|
|
||||||
quantity <= 0 ? '' : `${quantity}天`
|
|
||||||
}`.trim();
|
|
||||||
}
|
|
||||||
if (detailType === 'GIFT' || detailType === 'FRAGMENTS') {
|
if (detailType === 'GIFT' || detailType === 'FRAGMENTS') {
|
||||||
return `数量:${item.quantity || 0}`;
|
return `数量:${item.quantity || 0}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -96,8 +96,6 @@ const rankQuery = reactive({
|
|||||||
limit: 20,
|
limit: 20,
|
||||||
});
|
});
|
||||||
|
|
||||||
const autoRewardEnabledCache = new Map<string, boolean>();
|
|
||||||
|
|
||||||
const statusOptions = [
|
const statusOptions = [
|
||||||
{ label: '全部状态', value: '' },
|
{ label: '全部状态', value: '' },
|
||||||
{ label: '成功', value: 'SUCCESS' },
|
{ label: '成功', value: 'SUCCESS' },
|
||||||
@ -143,19 +141,11 @@ const enabledLevelCount = computed(() =>
|
|||||||
|
|
||||||
function normalizeConfig(result: Record<string, any> | null | undefined) {
|
function normalizeConfig(result: Record<string, any> | null | undefined) {
|
||||||
const value = result || {};
|
const value = result || {};
|
||||||
const sysOrigin = String(value.sysOrigin || selectedSysOrigin.value || '');
|
|
||||||
if (Object.prototype.hasOwnProperty.call(value, 'autoRewardEnabled')) {
|
|
||||||
form.autoRewardEnabled = value.autoRewardEnabled !== false;
|
form.autoRewardEnabled = value.autoRewardEnabled !== false;
|
||||||
autoRewardEnabledCache.set(sysOrigin, form.autoRewardEnabled);
|
|
||||||
} else if (autoRewardEnabledCache.has(sysOrigin)) {
|
|
||||||
form.autoRewardEnabled = autoRewardEnabledCache.get(sysOrigin);
|
|
||||||
} else {
|
|
||||||
form.autoRewardEnabled = true;
|
|
||||||
}
|
|
||||||
form.configured = Boolean(value.configured);
|
form.configured = Boolean(value.configured);
|
||||||
form.enabled = Boolean(value.enabled);
|
form.enabled = Boolean(value.enabled);
|
||||||
form.id = value.id || 0;
|
form.id = value.id || 0;
|
||||||
form.sysOrigin = sysOrigin;
|
form.sysOrigin = String(value.sysOrigin || selectedSysOrigin.value || '');
|
||||||
form.timezone = String(value.timezone || 'Asia/Riyadh');
|
form.timezone = String(value.timezone || 'Asia/Riyadh');
|
||||||
form.updateTime = String(value.updateTime || '');
|
form.updateTime = String(value.updateTime || '');
|
||||||
const levels = Array.isArray(value.levelConfigs) && value.levelConfigs.length > 0
|
const levels = Array.isArray(value.levelConfigs) && value.levelConfigs.length > 0
|
||||||
@ -293,9 +283,8 @@ async function submitForm() {
|
|||||||
}
|
}
|
||||||
saving.value = true;
|
saving.value = true;
|
||||||
try {
|
try {
|
||||||
const autoRewardEnabled = Boolean(form.autoRewardEnabled);
|
|
||||||
await saveRoomTurnoverRewardConfig({
|
await saveRoomTurnoverRewardConfig({
|
||||||
autoRewardEnabled,
|
autoRewardEnabled: Boolean(form.autoRewardEnabled),
|
||||||
enabled: Boolean(form.enabled),
|
enabled: Boolean(form.enabled),
|
||||||
id: form.id || 0,
|
id: form.id || 0,
|
||||||
levelConfigs: (form.levelConfigs || []).map((item: Record<string, any>) => ({
|
levelConfigs: (form.levelConfigs || []).map((item: Record<string, any>) => ({
|
||||||
@ -308,7 +297,6 @@ async function submitForm() {
|
|||||||
sysOrigin: selectedSysOrigin.value,
|
sysOrigin: selectedSysOrigin.value,
|
||||||
timezone: String(form.timezone || '').trim(),
|
timezone: String(form.timezone || '').trim(),
|
||||||
});
|
});
|
||||||
autoRewardEnabledCache.set(selectedSysOrigin.value, autoRewardEnabled);
|
|
||||||
message.success('保存成功');
|
message.success('保存成功');
|
||||||
await reloadAll();
|
await reloadAll();
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@ -4,14 +4,9 @@ import { computed, nextTick, reactive, ref, watch } from 'vue';
|
|||||||
import { Page } from '@vben/common-ui';
|
import { Page } from '@vben/common-ui';
|
||||||
import { useAccessStore } from '@vben/stores';
|
import { useAccessStore } from '@vben/stores';
|
||||||
|
|
||||||
import type { Dayjs } from 'dayjs';
|
|
||||||
import dayjs from 'dayjs';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Avatar,
|
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
DatePicker,
|
|
||||||
Input,
|
Input,
|
||||||
InputNumber,
|
InputNumber,
|
||||||
message,
|
message,
|
||||||
@ -28,7 +23,6 @@ import {
|
|||||||
import {
|
import {
|
||||||
getResidentWheelConfig,
|
getResidentWheelConfig,
|
||||||
pageResidentWheelDrawRecords,
|
pageResidentWheelDrawRecords,
|
||||||
retryResidentWheelDrawRecord,
|
|
||||||
saveResidentWheelCategoryConfig,
|
saveResidentWheelCategoryConfig,
|
||||||
} from '#/api/legacy/resident-activity';
|
} from '#/api/legacy/resident-activity';
|
||||||
import PropsSourceSelectDrawer from '#/views/operate/components/props-source-select-drawer.vue';
|
import PropsSourceSelectDrawer from '#/views/operate/components/props-source-select-drawer.vue';
|
||||||
@ -36,9 +30,7 @@ import { getAllowedSysOrigins } from '#/views/system/shared';
|
|||||||
|
|
||||||
defineOptions({ name: 'ResidentWheelConfigPage' });
|
defineOptions({ name: 'ResidentWheelConfigPage' });
|
||||||
|
|
||||||
const { RangePicker } = DatePicker;
|
const PROBABILITY_TOTAL = 10_000;
|
||||||
|
|
||||||
const PROBABILITY_TOTAL = 1_000_000;
|
|
||||||
const WHEEL_CATEGORIES = [
|
const WHEEL_CATEGORIES = [
|
||||||
{
|
{
|
||||||
category: 'CLASSIC',
|
category: 'CLASSIC',
|
||||||
@ -77,18 +69,6 @@ const REWARD_TYPE_OPTIONS = [
|
|||||||
{ label: '金币', value: 'GOLD' },
|
{ label: '金币', value: 'GOLD' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const WHEEL_RESOURCE_TYPE_OPTIONS = [
|
|
||||||
{ label: '头像框', value: 'AVATAR_FRAME' },
|
|
||||||
{ label: '座驾', value: 'RIDE' },
|
|
||||||
{ label: '礼物', value: 'GIFT' },
|
|
||||||
{ label: '用户徽章', value: 'BADGE' },
|
|
||||||
{ label: '房间徽章', value: 'ROOM_BADGE' },
|
|
||||||
{ label: '表情包', value: 'EMOJI' },
|
|
||||||
{ label: '聊天气泡', value: 'CHAT_BUBBLE' },
|
|
||||||
{ label: '飘窗', value: 'FLOAT_PICTURE' },
|
|
||||||
{ label: '资料卡', value: 'DATA_CARD' },
|
|
||||||
];
|
|
||||||
|
|
||||||
let localRowId = 0;
|
let localRowId = 0;
|
||||||
|
|
||||||
function normalizeRewardTypeValue(value: any) {
|
function normalizeRewardTypeValue(value: any) {
|
||||||
@ -115,7 +95,6 @@ function createReward(type = 'RESOURCE', sort = 1) {
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
goldAmount: type === 'GOLD' ? 100 : 0,
|
goldAmount: type === 'GOLD' ? 100 : 0,
|
||||||
id: 0,
|
id: 0,
|
||||||
issuedCount: 0,
|
|
||||||
probability: 0,
|
probability: 0,
|
||||||
rewardType: type,
|
rewardType: type,
|
||||||
resourceId: null as null | number | string,
|
resourceId: null as null | number | string,
|
||||||
@ -124,8 +103,6 @@ function createReward(type = 'RESOURCE', sort = 1) {
|
|||||||
resourceUrl: '',
|
resourceUrl: '',
|
||||||
rowKey: createRowKey(sort),
|
rowKey: createRowKey(sort),
|
||||||
sort,
|
sort,
|
||||||
totalLimit: 0,
|
|
||||||
userLimit: 0,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -169,7 +146,6 @@ const loading = ref(false);
|
|||||||
const savingCategory = ref('');
|
const savingCategory = ref('');
|
||||||
const recordLoading = ref(false);
|
const recordLoading = ref(false);
|
||||||
const resourcePickerOpen = ref(false);
|
const resourcePickerOpen = ref(false);
|
||||||
const retryingRecordId = ref<null | number | string>(null);
|
|
||||||
const pickerCategory = ref('CLASSIC');
|
const pickerCategory = ref('CLASSIC');
|
||||||
const pickerRewardIndex = ref(-1);
|
const pickerRewardIndex = ref(-1);
|
||||||
const pickerRewardRecord = ref<null | Record<string, any>>(null);
|
const pickerRewardRecord = ref<null | Record<string, any>>(null);
|
||||||
@ -182,40 +158,26 @@ const recordQuery = reactive<Record<string, any>>({
|
|||||||
status: '',
|
status: '',
|
||||||
userId: '',
|
userId: '',
|
||||||
});
|
});
|
||||||
const recordRangeDate = ref<[Dayjs, Dayjs] | null>([
|
|
||||||
dayjs().startOf('day'),
|
|
||||||
dayjs().endOf('day'),
|
|
||||||
]);
|
|
||||||
const recordPage = reactive<Record<string, any>>({
|
const recordPage = reactive<Record<string, any>>({
|
||||||
current: 1,
|
current: 1,
|
||||||
records: [],
|
records: [],
|
||||||
size: 20,
|
size: 20,
|
||||||
total: 0,
|
total: 0,
|
||||||
});
|
});
|
||||||
const recordSummary = reactive<Record<string, any>>({
|
|
||||||
endTime: '',
|
|
||||||
participantCount: 0,
|
|
||||||
startTime: '',
|
|
||||||
totalDrawTimes: 0,
|
|
||||||
totalPaidGold: 0,
|
|
||||||
totalRewardGold: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
const rewardColumns = [
|
const rewardColumns = [
|
||||||
{ dataIndex: 'enabled', key: 'enabled', title: '启用', width: 80 },
|
{ dataIndex: 'enabled', key: 'enabled', title: '启用', width: 80 },
|
||||||
{ dataIndex: 'sort', key: 'sort', title: '位置', width: 90 },
|
{ dataIndex: 'sort', key: 'sort', title: '位置', width: 90 },
|
||||||
{ dataIndex: 'rewardType', key: 'rewardType', title: '类型', width: 130 },
|
{ dataIndex: 'rewardType', key: 'rewardType', title: '类型', width: 130 },
|
||||||
{ dataIndex: 'reward', key: 'reward', title: '奖励', width: 560 },
|
{ dataIndex: 'reward', key: 'reward', title: '奖励', width: 620 },
|
||||||
{ dataIndex: 'probability', key: 'probability', title: '概率', width: 180 },
|
{ dataIndex: 'probability', key: 'probability', title: '概率', width: 180 },
|
||||||
{ dataIndex: 'limit', key: 'limit', title: '止损线', width: 280 },
|
|
||||||
{ dataIndex: 'actions', key: 'actions', title: '操作', width: 90 },
|
{ dataIndex: 'actions', key: 'actions', title: '操作', width: 90 },
|
||||||
];
|
];
|
||||||
|
|
||||||
const recordColumns = [
|
const recordColumns = [
|
||||||
{ dataIndex: 'drawNo', key: 'drawNo', title: '抽奖单号', width: 190 },
|
{ dataIndex: 'drawNo', key: 'drawNo', title: '抽奖单号', width: 190 },
|
||||||
{ dataIndex: 'category', key: 'category', title: '转盘', width: 120 },
|
{ dataIndex: 'category', key: 'category', title: '转盘', width: 120 },
|
||||||
{ dataIndex: 'user', key: 'user', title: '用户信息', width: 280 },
|
{ dataIndex: 'userId', key: 'userId', title: '用户ID', width: 140 },
|
||||||
{ dataIndex: 'userTotalDrawTimes', key: 'userTotalDrawTimes', title: '用户总次数', width: 130 },
|
|
||||||
{ dataIndex: 'drawTimes', key: 'drawTimes', title: '次数', width: 90 },
|
{ dataIndex: 'drawTimes', key: 'drawTimes', title: '次数', width: 90 },
|
||||||
{ dataIndex: 'paidGold', key: 'paidGold', title: '消耗金币', width: 120 },
|
{ dataIndex: 'paidGold', key: 'paidGold', title: '消耗金币', width: 120 },
|
||||||
{ dataIndex: 'reward', key: 'reward', title: '中奖奖励', width: 360 },
|
{ dataIndex: 'reward', key: 'reward', title: '中奖奖励', width: 360 },
|
||||||
@ -223,7 +185,6 @@ const recordColumns = [
|
|||||||
{ dataIndex: 'status', key: 'status', title: '状态', width: 110 },
|
{ dataIndex: 'status', key: 'status', title: '状态', width: 110 },
|
||||||
{ dataIndex: 'createTime', key: 'createTime', title: '时间', width: 180 },
|
{ dataIndex: 'createTime', key: 'createTime', title: '时间', width: 180 },
|
||||||
{ dataIndex: 'errorMessage', key: 'errorMessage', title: '失败原因', width: 260 },
|
{ dataIndex: 'errorMessage', key: 'errorMessage', title: '失败原因', width: 260 },
|
||||||
{ dataIndex: 'actions', key: 'actions', title: '操作', width: 110 },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const statusMeta = computed(() => {
|
const statusMeta = computed(() => {
|
||||||
@ -261,14 +222,6 @@ function categoryProbabilityColor(category: Record<string, any>) {
|
|||||||
return categoryProbability(category) === PROBABILITY_TOTAL ? 'success' : 'error';
|
return categoryProbability(category) === PROBABILITY_TOTAL ? 'success' : 'error';
|
||||||
}
|
}
|
||||||
|
|
||||||
function probabilityPercent(value: any) {
|
|
||||||
return ((Number(value || 0) * 100) / PROBABILITY_TOTAL).toFixed(4);
|
|
||||||
}
|
|
||||||
|
|
||||||
function numberText(value: any) {
|
|
||||||
return Number(value || 0).toLocaleString();
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeReward(item: Record<string, any> | undefined, index: number) {
|
function normalizeReward(item: Record<string, any> | undefined, index: number) {
|
||||||
const value = item || {};
|
const value = item || {};
|
||||||
const rewardType = normalizeRewardTypeValue(value.rewardType);
|
const rewardType = normalizeRewardTypeValue(value.rewardType);
|
||||||
@ -290,7 +243,6 @@ function normalizeReward(item: Record<string, any> | undefined, index: number) {
|
|||||||
enabled: value.enabled !== false,
|
enabled: value.enabled !== false,
|
||||||
goldAmount: Number(value.goldAmount || 0),
|
goldAmount: Number(value.goldAmount || 0),
|
||||||
id: Number(value.id || 0),
|
id: Number(value.id || 0),
|
||||||
issuedCount: Number(value.issuedCount || 0),
|
|
||||||
probability: Number(value.probability || 0),
|
probability: Number(value.probability || 0),
|
||||||
rewardType,
|
rewardType,
|
||||||
resourceId: resourceId || null,
|
resourceId: resourceId || null,
|
||||||
@ -299,8 +251,6 @@ function normalizeReward(item: Record<string, any> | undefined, index: number) {
|
|||||||
resourceUrl: String(value.resourceUrl || ''),
|
resourceUrl: String(value.resourceUrl || ''),
|
||||||
rowKey: createRowKey(value.id || index + 1),
|
rowKey: createRowKey(value.id || index + 1),
|
||||||
sort: Number(value.sort || index + 1),
|
sort: Number(value.sort || index + 1),
|
||||||
totalLimit: Number(value.totalLimit || 0),
|
|
||||||
userLimit: Number(value.userLimit || 0),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -414,25 +364,6 @@ function normalizeRecordPage(result: null | Record<string, any> | undefined) {
|
|||||||
recordPage.total = Number(value.total || 0);
|
recordPage.total = Number(value.total || 0);
|
||||||
recordPage.current = Number(value.current || 1);
|
recordPage.current = Number(value.current || 1);
|
||||||
recordPage.size = Number(value.size || 20);
|
recordPage.size = Number(value.size || 20);
|
||||||
Object.assign(recordSummary, {
|
|
||||||
endTime: String(value.endTime || ''),
|
|
||||||
participantCount: Number(value.participantCount || 0),
|
|
||||||
startTime: String(value.startTime || ''),
|
|
||||||
totalDrawTimes: Number(value.totalDrawTimes || 0),
|
|
||||||
totalPaidGold: Number(value.totalPaidGold || 0),
|
|
||||||
totalRewardGold: Number(value.totalRewardGold || 0),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function recordTimeParams() {
|
|
||||||
const range = recordRangeDate.value;
|
|
||||||
if (!range?.length) {
|
|
||||||
return { endTime: '', startTime: '' };
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
endTime: range[1]?.format('YYYY-MM-DD HH:mm:ss') || '',
|
|
||||||
startTime: range[0]?.format('YYYY-MM-DD HH:mm:ss') || '',
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadRecords(reset = false) {
|
async function loadRecords(reset = false) {
|
||||||
@ -446,7 +377,6 @@ async function loadRecords(reset = false) {
|
|||||||
try {
|
try {
|
||||||
const result = await pageResidentWheelDrawRecords({
|
const result = await pageResidentWheelDrawRecords({
|
||||||
...recordQuery,
|
...recordQuery,
|
||||||
...recordTimeParams(),
|
|
||||||
sysOrigin: selectedSysOrigin.value,
|
sysOrigin: selectedSysOrigin.value,
|
||||||
});
|
});
|
||||||
normalizeRecordPage(result);
|
normalizeRecordPage(result);
|
||||||
@ -656,14 +586,6 @@ function validateReward(category: Record<string, any>, item: Record<string, any>
|
|||||||
message.warning(`${label} 概率不能小于 0`);
|
message.warning(`${label} 概率不能小于 0`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (Number(item.totalLimit || 0) < 0) {
|
|
||||||
message.warning(`${label} 总量上限不能小于 0`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (Number(item.userLimit || 0) < 0) {
|
|
||||||
message.warning(`${label} 单用户上限不能小于 0`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (
|
if (
|
||||||
category.enabled !== false &&
|
category.enabled !== false &&
|
||||||
item.enabled !== false &&
|
item.enabled !== false &&
|
||||||
@ -702,7 +624,7 @@ function validateCategory(category: Record<string, any>) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (categoryProbability(category) !== PROBABILITY_TOTAL) {
|
if (categoryProbability(category) !== PROBABILITY_TOTAL) {
|
||||||
message.warning(`${category.label} 启用时概率总和必须等于 ${PROBABILITY_TOTAL}`);
|
message.warning(`${category.label} 启用时概率总和必须等于 10000`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -747,8 +669,6 @@ function categoryPayload(category: Record<string, any>) {
|
|||||||
resourceType: rewardType === 'RESOURCE' ? String(item.resourceType || '') : '',
|
resourceType: rewardType === 'RESOURCE' ? String(item.resourceType || '') : '',
|
||||||
resourceUrl: rewardType === 'RESOURCE' ? String(item.resourceUrl || '') : '',
|
resourceUrl: rewardType === 'RESOURCE' ? String(item.resourceUrl || '') : '',
|
||||||
sort: Number(item.sort || 0),
|
sort: Number(item.sort || 0),
|
||||||
totalLimit: Number(item.totalLimit || 0),
|
|
||||||
userLimit: Number(item.userLimit || 0),
|
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
@ -795,21 +715,6 @@ function handleRecordTableChange(pagination: Record<string, any>) {
|
|||||||
void loadRecords();
|
void loadRecords();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function retryRecord(record: Record<string, any>) {
|
|
||||||
const id = record?.id;
|
|
||||||
if (!id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
retryingRecordId.value = id;
|
|
||||||
try {
|
|
||||||
await retryResidentWheelDrawRecord(id);
|
|
||||||
message.success('补发成功');
|
|
||||||
await loadRecords();
|
|
||||||
} finally {
|
|
||||||
retryingRecordId.value = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function rewardRecordName(record: Record<string, any>) {
|
function rewardRecordName(record: Record<string, any>) {
|
||||||
if (record.resourceName) {
|
if (record.resourceName) {
|
||||||
return record.resourceName;
|
return record.resourceName;
|
||||||
@ -835,9 +740,6 @@ function statusColor(status: string) {
|
|||||||
case 'FAILED': {
|
case 'FAILED': {
|
||||||
return 'error';
|
return 'error';
|
||||||
}
|
}
|
||||||
case 'PAY_FAILED': {
|
|
||||||
return 'warning';
|
|
||||||
}
|
|
||||||
case 'SUCCESS': {
|
case 'SUCCESS': {
|
||||||
return 'success';
|
return 'success';
|
||||||
}
|
}
|
||||||
@ -847,15 +749,6 @@ function statusColor(status: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function userAvatarFallback(record: Record<string, any>) {
|
|
||||||
const nickname = String(record.userNickname || '').trim();
|
|
||||||
if (nickname) {
|
|
||||||
return nickname.slice(0, 1).toUpperCase();
|
|
||||||
}
|
|
||||||
const shortId = String(record.userShortId || '').trim();
|
|
||||||
return shortId ? shortId.slice(0, 1).toUpperCase() : 'U';
|
|
||||||
}
|
|
||||||
|
|
||||||
function coverStyle(url: string) {
|
function coverStyle(url: string) {
|
||||||
return {
|
return {
|
||||||
backgroundImage: `url("${String(url || '').replaceAll('"', '%22')}")`,
|
backgroundImage: `url("${String(url || '').replaceAll('"', '%22')}")`,
|
||||||
@ -982,7 +875,7 @@ watch(activePageTab, (value) => {
|
|||||||
:loading="loading"
|
:loading="loading"
|
||||||
:pagination="false"
|
:pagination="false"
|
||||||
row-key="rowKey"
|
row-key="rowKey"
|
||||||
:scroll="{ x: 1460 }"
|
:scroll="{ x: 1180 }"
|
||||||
>
|
>
|
||||||
<template #bodyCell="{ column, index, record }">
|
<template #bodyCell="{ column, index, record }">
|
||||||
<template v-if="column.key === 'enabled'">
|
<template v-if="column.key === 'enabled'">
|
||||||
@ -1087,35 +980,12 @@ watch(activePageTab, (value) => {
|
|||||||
style="width: 120px"
|
style="width: 120px"
|
||||||
/>
|
/>
|
||||||
<span class="sub-text">
|
<span class="sub-text">
|
||||||
{{ probabilityPercent(record.probability) }}%
|
{{
|
||||||
|
(Number(record.probability || 0) / 100).toFixed(2)
|
||||||
|
}}%
|
||||||
</span>
|
</span>
|
||||||
</Space>
|
</Space>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="column.key === 'limit'">
|
|
||||||
<Space direction="vertical" size="small">
|
|
||||||
<Space align="center">
|
|
||||||
<span class="sub-text">总量</span>
|
|
||||||
<InputNumber
|
|
||||||
v-model:value="record.totalLimit"
|
|
||||||
:min="0"
|
|
||||||
:precision="0"
|
|
||||||
placeholder="不限"
|
|
||||||
style="width: 110px"
|
|
||||||
/>
|
|
||||||
<span class="sub-text">已占 {{ Number(record.issuedCount || 0) }}</span>
|
|
||||||
</Space>
|
|
||||||
<Space align="center">
|
|
||||||
<span class="sub-text">单用户</span>
|
|
||||||
<InputNumber
|
|
||||||
v-model:value="record.userLimit"
|
|
||||||
:min="0"
|
|
||||||
:precision="0"
|
|
||||||
placeholder="不限"
|
|
||||||
style="width: 110px"
|
|
||||||
/>
|
|
||||||
</Space>
|
|
||||||
</Space>
|
|
||||||
</template>
|
|
||||||
<template v-else-if="column.key === 'actions'">
|
<template v-else-if="column.key === 'actions'">
|
||||||
<Button danger type="link" @click="clearResource(record)">
|
<Button danger type="link" @click="clearResource(record)">
|
||||||
清空
|
清空
|
||||||
@ -1148,44 +1018,18 @@ watch(activePageTab, (value) => {
|
|||||||
allow-clear
|
allow-clear
|
||||||
:options="[
|
:options="[
|
||||||
{ label: '成功', value: 'SUCCESS' },
|
{ label: '成功', value: 'SUCCESS' },
|
||||||
{ label: '发奖失败', value: 'FAILED' },
|
{ label: '失败', value: 'FAILED' },
|
||||||
{ label: '扣款失败', value: 'PAY_FAILED' },
|
|
||||||
{ label: '发奖中', value: 'GRANTING' },
|
|
||||||
{ label: '处理中', value: 'PENDING' },
|
{ label: '处理中', value: 'PENDING' },
|
||||||
]"
|
]"
|
||||||
placeholder="状态"
|
placeholder="状态"
|
||||||
style="width: 130px"
|
style="width: 130px"
|
||||||
/>
|
/>
|
||||||
<RangePicker
|
|
||||||
v-model:value="recordRangeDate"
|
|
||||||
show-time
|
|
||||||
style="width: 380px"
|
|
||||||
/>
|
|
||||||
<Button :loading="recordLoading" type="primary" @click="handleRecordSearch">
|
<Button :loading="recordLoading" type="primary" @click="handleRecordSearch">
|
||||||
查询
|
查询
|
||||||
</Button>
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="record-summary">
|
|
||||||
<div class="record-summary-item">
|
|
||||||
<span>用户总抽奖消耗金币</span>
|
|
||||||
<strong>{{ numberText(recordSummary.totalPaidGold) }}</strong>
|
|
||||||
</div>
|
|
||||||
<div class="record-summary-item">
|
|
||||||
<span>礼物发放金币总价值</span>
|
|
||||||
<strong>{{ numberText(recordSummary.totalRewardGold) }}</strong>
|
|
||||||
</div>
|
|
||||||
<div class="record-summary-item">
|
|
||||||
<span>用户总抽奖次数</span>
|
|
||||||
<strong>{{ numberText(recordSummary.totalDrawTimes) }}</strong>
|
|
||||||
</div>
|
|
||||||
<div class="record-summary-item">
|
|
||||||
<span>用户参与量</span>
|
|
||||||
<strong>{{ numberText(recordSummary.participantCount) }}</strong>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Table
|
<Table
|
||||||
:columns="recordColumns"
|
:columns="recordColumns"
|
||||||
:data-source="recordPage.records"
|
:data-source="recordPage.records"
|
||||||
@ -1197,28 +1041,13 @@ watch(activePageTab, (value) => {
|
|||||||
total: recordPage.total,
|
total: recordPage.total,
|
||||||
}"
|
}"
|
||||||
row-key="id"
|
row-key="id"
|
||||||
:scroll="{ x: 2060 }"
|
:scroll="{ x: 1680 }"
|
||||||
@change="handleRecordTableChange"
|
@change="handleRecordTableChange"
|
||||||
>
|
>
|
||||||
<template #bodyCell="{ column, record }">
|
<template #bodyCell="{ column, record }">
|
||||||
<template v-if="column.key === 'category'">
|
<template v-if="column.key === 'category'">
|
||||||
<Tag>{{ categoryLabel(record.category) }}</Tag>
|
<Tag>{{ categoryLabel(record.category) }}</Tag>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="column.key === 'user'">
|
|
||||||
<div class="record-user">
|
|
||||||
<Avatar :size="36" :src="record.userAvatar || undefined">
|
|
||||||
{{ userAvatarFallback(record) }}
|
|
||||||
</Avatar>
|
|
||||||
<div class="record-user-info">
|
|
||||||
<div class="record-user-name">{{ record.userNickname || '-' }}</div>
|
|
||||||
<div class="sub-text">长ID:{{ record.userId || '-' }}</div>
|
|
||||||
<div class="sub-text">短ID:{{ record.userShortId || '-' }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template v-else-if="column.key === 'userTotalDrawTimes'">
|
|
||||||
{{ numberText(record.userTotalDrawTimes) }}
|
|
||||||
</template>
|
|
||||||
<template v-else-if="column.key === 'reward'">
|
<template v-else-if="column.key === 'reward'">
|
||||||
<div class="record-reward">
|
<div class="record-reward">
|
||||||
<div
|
<div
|
||||||
@ -1236,7 +1065,7 @@ watch(activePageTab, (value) => {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="column.key === 'probability'">
|
<template v-else-if="column.key === 'probability'">
|
||||||
{{ probabilityPercent(record.probability) }}%
|
{{ (Number(record.probability || 0) / 100).toFixed(2) }}%
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="column.key === 'status'">
|
<template v-else-if="column.key === 'status'">
|
||||||
<Tag :color="statusColor(record.status)">{{ record.status || '-' }}</Tag>
|
<Tag :color="statusColor(record.status)">{{ record.status || '-' }}</Tag>
|
||||||
@ -1244,18 +1073,6 @@ watch(activePageTab, (value) => {
|
|||||||
<template v-else-if="column.key === 'errorMessage'">
|
<template v-else-if="column.key === 'errorMessage'">
|
||||||
{{ record.errorMessage || '-' }}
|
{{ record.errorMessage || '-' }}
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="column.key === 'actions'">
|
|
||||||
<Button
|
|
||||||
v-if="String(record.status || '').toUpperCase() === 'FAILED'"
|
|
||||||
:loading="retryingRecordId === record.id"
|
|
||||||
size="small"
|
|
||||||
type="link"
|
|
||||||
@click="retryRecord(record)"
|
|
||||||
>
|
|
||||||
补发
|
|
||||||
</Button>
|
|
||||||
<span v-else class="sub-text">-</span>
|
|
||||||
</template>
|
|
||||||
</template>
|
</template>
|
||||||
</Table>
|
</Table>
|
||||||
</TabPane>
|
</TabPane>
|
||||||
@ -1270,7 +1087,6 @@ watch(activePageTab, (value) => {
|
|||||||
"
|
"
|
||||||
:open="resourcePickerOpen"
|
:open="resourcePickerOpen"
|
||||||
:sys-origin="selectedSysOrigin"
|
:sys-origin="selectedSysOrigin"
|
||||||
:type-options="WHEEL_RESOURCE_TYPE_OPTIONS"
|
|
||||||
@close="closeResourcePicker"
|
@close="closeResourcePicker"
|
||||||
@select="handleResourceSelect"
|
@select="handleResourceSelect"
|
||||||
/>
|
/>
|
||||||
@ -1308,50 +1124,12 @@ watch(activePageTab, (value) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.record-reward,
|
.record-reward,
|
||||||
.record-user,
|
|
||||||
.resource-summary {
|
.resource-summary {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.record-summary {
|
|
||||||
display: grid;
|
|
||||||
gap: 10px;
|
|
||||||
grid-template-columns: repeat(4, minmax(160px, 1fr));
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.record-summary-item {
|
|
||||||
background: rgb(248 250 252);
|
|
||||||
border: 1px solid rgb(226 232 240);
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 12px 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.record-summary-item span {
|
|
||||||
color: rgb(100 116 139);
|
|
||||||
display: block;
|
|
||||||
font-size: 12px;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.record-summary-item strong {
|
|
||||||
color: rgb(15 23 42);
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.record-user-info {
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.record-user-name {
|
|
||||||
color: rgb(15 23 42);
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.resource-summary {
|
.resource-summary {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,76 +0,0 @@
|
|||||||
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