多选修改
This commit is contained in:
parent
aa0988af6b
commit
045a8af46f
@ -1,19 +1,13 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, h, useAttrs } from 'vue';
|
import { computed, watch } 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',
|
||||||
@ -38,129 +32,43 @@ 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 : getDisplayableSysOriginOptions(SYS_ORIGIN_OPTIONS)) as AppOriginOption[];
|
return (options.length > 0 ? options : SYS_ORIGIN_OPTIONS) as AppOriginOption[];
|
||||||
});
|
});
|
||||||
|
|
||||||
const normalizedOptions = computed(() =>
|
const rawOptions = computed(() =>
|
||||||
getDisplayableSysOriginOptions(props.options?.length ? props.options : fallbackOptions.value).map(
|
props.options?.length ? props.options : fallbackOptions.value,
|
||||||
(item) => {
|
|
||||||
const value = String(item.value ?? '');
|
|
||||||
const label = String(item.label ?? ('name' in item ? item.name : '') ?? value);
|
|
||||||
return {
|
|
||||||
...item,
|
|
||||||
icon: String(item.icon || value).toLowerCase(),
|
|
||||||
label: label || value,
|
|
||||||
value,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const optionMap = computed(
|
const hiddenDefaultValue = computed(() =>
|
||||||
() => new Map(normalizedOptions.value.map((item) => [String(item.value), item])),
|
String(rawOptions.value[0]?.value ?? SYS_ORIGIN_OPTIONS[0]?.value ?? 'LIKEI'),
|
||||||
);
|
);
|
||||||
|
|
||||||
const displayValue = computed<SelectValue>(() => {
|
watch(
|
||||||
if (Array.isArray(props.value)) {
|
() => [props.value, hiddenDefaultValue.value] as const,
|
||||||
return props.value.filter((item) => !isHiddenSysOrigin(item));
|
([value, defaultValue]) => {
|
||||||
}
|
if (Array.isArray(value) || value !== undefined && value !== null && value !== '') {
|
||||||
return isHiddenSysOrigin(props.value) ? undefined : props.value;
|
return;
|
||||||
});
|
}
|
||||||
|
if (!defaultValue) {
|
||||||
const selectAttrs = computed(() => {
|
return;
|
||||||
const nextAttrs = { ...attrs } as Record<string, any>;
|
}
|
||||||
delete nextAttrs.options;
|
emit('update:value', defaultValue);
|
||||||
delete nextAttrs.value;
|
emit('change', defaultValue, rawOptions.value[0] || null);
|
||||||
delete nextAttrs.onChange;
|
},
|
||||||
delete nextAttrs['onUpdate:value'];
|
{ immediate: true },
|
||||||
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>
|
||||||
<Select
|
<span class="sys-origin-select--hidden" aria-hidden="true" />
|
||||||
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__content {
|
.sys-origin-select--hidden {
|
||||||
align-items: center;
|
display: none !important;
|
||||||
display: inline-flex;
|
|
||||||
gap: 8px;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sys-origin-select__text {
|
|
||||||
line-height: 1;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,5 +1,13 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue';
|
import {
|
||||||
|
computed,
|
||||||
|
defineComponent,
|
||||||
|
nextTick,
|
||||||
|
onBeforeUnmount,
|
||||||
|
onMounted,
|
||||||
|
onUpdated,
|
||||||
|
ref,
|
||||||
|
} from 'vue';
|
||||||
|
|
||||||
import { Pagination, Table } from 'antdv-next';
|
import { Pagination, Table } from 'antdv-next';
|
||||||
|
|
||||||
@ -11,32 +19,59 @@ 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 };
|
||||||
@ -46,6 +81,19 @@ 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 '-';
|
||||||
@ -68,6 +116,10 @@ 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;
|
||||||
@ -85,9 +137,45 @@ 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 () => {
|
||||||
@ -104,10 +192,18 @@ 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);
|
||||||
});
|
});
|
||||||
@ -118,6 +214,10 @@ 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"
|
||||||
@ -126,22 +226,32 @@ 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">
|
||||||
<slot name="headerCell" v-bind="slotProps">
|
<RenderNode
|
||||||
|
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">
|
||||||
<slot name="bodyCell" v-bind="slotProps">
|
<RenderNode v-if="isVNodeLike(slotProps.text)" :node="slotProps.text" />
|
||||||
|
<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 ref="pagerRef" class="admin-config-table-pager">
|
<div v-if="infiniteStatusText" class="admin-config-table-load-status">
|
||||||
|
{{ 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"
|
||||||
@ -178,6 +288,15 @@ 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%;
|
||||||
@ -258,6 +377,16 @@ 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;
|
||||||
|
|||||||
@ -21,7 +21,10 @@ 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 { GIFT_CONFIG_TAB_OPTIONS } from '../constants';
|
import {
|
||||||
|
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';
|
||||||
@ -72,9 +75,12 @@ const emit = defineEmits<{
|
|||||||
success: [];
|
success: [];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const DEFAULT_GIFT_SPECIAL_LIST = ['MUSIC', 'ANIMATION'];
|
||||||
|
|
||||||
const batchForm = reactive({
|
const batchForm = reactive({
|
||||||
giftTab: 'ORDINARY',
|
giftTab: 'ORDINARY',
|
||||||
regionList: [] as Array<number | string>,
|
regionList: [] as Array<number | string>,
|
||||||
|
specialList: [...DEFAULT_GIFT_SPECIAL_LIST],
|
||||||
sysOrigin: '',
|
sysOrigin: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -97,6 +103,13 @@ 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 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 || '-'),
|
||||||
@ -176,6 +189,7 @@ function resetBatch() {
|
|||||||
String(props.initialSysOrigin || props.sysOriginOptions[0]?.value || 'LIKEI');
|
String(props.initialSysOrigin || props.sysOriginOptions[0]?.value || 'LIKEI');
|
||||||
batchForm.giftTab = 'ORDINARY';
|
batchForm.giftTab = 'ORDINARY';
|
||||||
batchForm.regionList = [];
|
batchForm.regionList = [];
|
||||||
|
batchForm.specialList = [...DEFAULT_GIFT_SPECIAL_LIST];
|
||||||
rows.value = [];
|
rows.value = [];
|
||||||
selectedRowKeys.value = [];
|
selectedRowKeys.value = [];
|
||||||
activeRowKey.value = '';
|
activeRowKey.value = '';
|
||||||
@ -669,7 +683,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: [],
|
specialList: [...batchForm.specialList],
|
||||||
standardId: '',
|
standardId: '',
|
||||||
sysOrigin: batchForm.sysOrigin,
|
sysOrigin: batchForm.sysOrigin,
|
||||||
type: 'GOLD',
|
type: 'GOLD',
|
||||||
@ -759,6 +773,7 @@ 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">
|
||||||
|
|||||||
@ -14,7 +14,6 @@ import {
|
|||||||
Space,
|
Space,
|
||||||
Spin,
|
Spin,
|
||||||
Switch,
|
Switch,
|
||||||
TextArea,
|
|
||||||
} from 'antdv-next';
|
} from 'antdv-next';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -313,151 +312,157 @@ async function handleSubmit() {
|
|||||||
:open="open"
|
:open="open"
|
||||||
:title="title"
|
:title="title"
|
||||||
destroy-on-close
|
destroy-on-close
|
||||||
width="980"
|
root-class="gift-form-drawer-root"
|
||||||
|
width="1120"
|
||||||
|
:styles="{
|
||||||
|
body: { padding: '0', overflow: 'hidden' },
|
||||||
|
footer: { padding: '10px 16px' },
|
||||||
|
}"
|
||||||
@close="emit('close')"
|
@close="emit('close')"
|
||||||
>
|
>
|
||||||
<Spin :spinning="loading">
|
<Spin :spinning="loading" wrapper-class-name="gift-editor-spin">
|
||||||
<Form layout="vertical">
|
<div class="gift-editor">
|
||||||
<div class="grid">
|
<aside class="media-panel">
|
||||||
<FormItem label="平台">
|
<div class="media-block">
|
||||||
<Input :value="form.sysOrigin" disabled />
|
<div class="media-head">
|
||||||
</FormItem>
|
<span>封面</span>
|
||||||
<FormItem label="区域">
|
<span class="required-mark">必填</span>
|
||||||
<Space direction="vertical" style="width: 100%">
|
</div>
|
||||||
<Button size="small" @click="toggleAllRegions">
|
<Image
|
||||||
{{ form.regionList.length === regions.length ? '取消全选' : '全选' }}
|
v-if="previewCover"
|
||||||
|
:src="previewCover"
|
||||||
|
class="preview-image"
|
||||||
|
/>
|
||||||
|
<div v-else class="preview-image preview-image--empty">未上传</div>
|
||||||
|
<div class="media-actions">
|
||||||
|
<Button :loading="coverUploading" size="small" @click="openCoverUpload">
|
||||||
|
上传封面
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
v-if="form.giftPhoto"
|
||||||
|
danger
|
||||||
|
size="small"
|
||||||
|
@click="form.giftPhoto = ''"
|
||||||
|
>
|
||||||
|
清空
|
||||||
</Button>
|
</Button>
|
||||||
<Select
|
|
||||||
v-model:value="form.regionList"
|
|
||||||
:options="regionOptions"
|
|
||||||
mode="multiple"
|
|
||||||
option-label-prop="label"
|
|
||||||
placeholder="请选择区域"
|
|
||||||
show-search
|
|
||||||
style="width: 100%"
|
|
||||||
/>
|
|
||||||
</Space>
|
|
||||||
</FormItem>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid">
|
|
||||||
<FormItem label="名称">
|
|
||||||
<Input v-model:value="form.giftName" />
|
|
||||||
</FormItem>
|
|
||||||
<FormItem label="Code">
|
|
||||||
<Input v-model:value="form.giftCode" />
|
|
||||||
</FormItem>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid">
|
|
||||||
<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="grid">
|
|
||||||
<FormItem label="礼物类型">
|
|
||||||
<Select
|
|
||||||
v-model:value="form.giftTab"
|
|
||||||
:options="giftTabOptions"
|
|
||||||
option-label-prop="label"
|
|
||||||
placeholder="请选择礼物类型"
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
<FormItem label="收费类型">
|
|
||||||
<Select
|
|
||||||
v-model:value="form.type"
|
|
||||||
:options="chargeTypeOptions"
|
|
||||||
option-label-prop="label"
|
|
||||||
placeholder="请选择收费类型"
|
|
||||||
/>
|
|
||||||
</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="排序">
|
|
||||||
<Input v-model:value="form.sort" />
|
|
||||||
</FormItem>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<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>
|
</div>
|
||||||
</FormItem>
|
</div>
|
||||||
<FormItem label="资源">
|
|
||||||
<div class="upload-field">
|
<div class="media-block">
|
||||||
<div class="source-value">{{ previewSource || '未上传' }}</div>
|
<div class="media-head">
|
||||||
<Space>
|
<span>资源</span>
|
||||||
<Button :loading="sourceUploading" @click="openSourceUpload">上传资源</Button>
|
<span class="optional-mark">可选</span>
|
||||||
<Button
|
|
||||||
v-if="form.giftSourceUrl"
|
|
||||||
danger
|
|
||||||
@click="form.giftSourceUrl = ''"
|
|
||||||
>
|
|
||||||
清空
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
</div>
|
</div>
|
||||||
</FormItem>
|
<div class="source-value">{{ previewSource || '未上传' }}</div>
|
||||||
</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>
|
||||||
|
|
||||||
<div class="grid">
|
<main class="editor-panel">
|
||||||
<FormItem label="有效期">
|
<Form class="editor-form" layout="vertical">
|
||||||
<DatePicker
|
<div class="section-title">基础信息</div>
|
||||||
v-model:value="form.expiredTime"
|
<div class="form-grid">
|
||||||
style="width: 100%"
|
<FormItem label="礼物类型">
|
||||||
value-format="x"
|
<Select
|
||||||
/>
|
v-model:value="form.giftTab"
|
||||||
</FormItem>
|
:options="giftTabOptions"
|
||||||
<FormItem v-if="form.giftTab === 'EXCLUSIVE'" label="归属人账号">
|
option-label-prop="label"
|
||||||
<Input v-model:value="form.account" />
|
placeholder="请选择礼物类型"
|
||||||
</FormItem>
|
/>
|
||||||
</div>
|
</FormItem>
|
||||||
|
<FormItem label="收费类型">
|
||||||
|
<Select
|
||||||
|
v-model:value="form.type"
|
||||||
|
:options="chargeTypeOptions"
|
||||||
|
option-label-prop="label"
|
||||||
|
placeholder="请选择收费类型"
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
<FormItem label="排序">
|
||||||
|
<Input v-model:value="form.sort" />
|
||||||
|
</FormItem>
|
||||||
|
<FormItem class="span-2" label="名称">
|
||||||
|
<Input v-model:value="form.giftName" />
|
||||||
|
</FormItem>
|
||||||
|
<FormItem label="Code">
|
||||||
|
<Input v-model:value="form.giftCode" />
|
||||||
|
</FormItem>
|
||||||
|
<FormItem label="有效期">
|
||||||
|
<DatePicker
|
||||||
|
v-model:value="form.expiredTime"
|
||||||
|
style="width: 100%"
|
||||||
|
value-format="x"
|
||||||
|
/>
|
||||||
|
</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="grid" v-if="isLuckyOrMagic">
|
<div class="section-title section-title--spaced">范围与效果</div>
|
||||||
<FormItem label="幸运规格">
|
<div class="form-grid">
|
||||||
<Select
|
<FormItem class="span-3" label="区域">
|
||||||
v-model:value="form.standardId"
|
<div class="inline-field">
|
||||||
:options="standardSelectOptions"
|
<Select
|
||||||
option-label-prop="label"
|
v-model:value="form.regionList"
|
||||||
placeholder="请选择幸运规格"
|
:max-tag-count="4"
|
||||||
/>
|
:options="regionOptions"
|
||||||
</FormItem>
|
class="compact-multiple-select"
|
||||||
</div>
|
mode="multiple"
|
||||||
|
option-label-prop="label"
|
||||||
<div class="grid" v-if="form.giftTab === 'CP'">
|
placeholder="请选择区域"
|
||||||
<FormItem label="是否告白礼物">
|
show-search
|
||||||
<Switch v-model:checked="form.explanationGift" />
|
/>
|
||||||
</FormItem>
|
<Button size="small" @click="toggleAllRegions">
|
||||||
</div>
|
{{ form.regionList.length === regions.length ? '取消全选' : '全选' }}
|
||||||
|
</Button>
|
||||||
<FormItem label="备注">
|
</div>
|
||||||
<TextArea :value="previewSource" :rows="2" disabled />
|
</FormItem>
|
||||||
</FormItem>
|
<FormItem class="span-3" label="特效">
|
||||||
</Form>
|
<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="归属人账号">
|
||||||
|
<Input v-model:value="form.account" />
|
||||||
|
</FormItem>
|
||||||
|
<FormItem v-if="isLuckyOrMagic" label="幸运规格">
|
||||||
|
<Select
|
||||||
|
v-model:value="form.standardId"
|
||||||
|
:options="standardSelectOptions"
|
||||||
|
option-label-prop="label"
|
||||||
|
placeholder="请选择幸运规格"
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
<FormItem v-if="form.giftTab === 'CP'" label="告白礼物">
|
||||||
|
<Switch v-model:checked="form.explanationGift" />
|
||||||
|
</FormItem>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
</Spin>
|
</Spin>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
@ -476,7 +481,7 @@ async function handleSubmit() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<Space>
|
<Space class="drawer-footer-actions">
|
||||||
<Button @click="emit('close')">取消</Button>
|
<Button @click="emit('close')">取消</Button>
|
||||||
<Button :loading="saving" type="primary" @click="handleSubmit">
|
<Button :loading="saving" type="primary" @click="handleSubmit">
|
||||||
保存
|
保存
|
||||||
@ -487,22 +492,72 @@ async function handleSubmit() {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.grid {
|
.gift-editor {
|
||||||
|
background: #f6f8fb;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 16px;
|
gap: 12px;
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: 260px minmax(0, 1fr);
|
||||||
|
height: calc(100vh - 106px);
|
||||||
|
min-height: 0;
|
||||||
|
padding: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.upload-field {
|
.media-panel,
|
||||||
|
.editor-panel {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 8px;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-panel {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 10px;
|
align-content: start;
|
||||||
|
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 {
|
||||||
border-radius: 16px;
|
background: #f8fafc;
|
||||||
height: 120px;
|
border: 1px solid #eef2f7;
|
||||||
|
border-radius: 8px;
|
||||||
|
height: 180px;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
width: 120px;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-image--empty,
|
.preview-image--empty,
|
||||||
@ -510,17 +565,104 @@ async function handleSubmit() {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
background: #f8fafc;
|
background: #f8fafc;
|
||||||
border: 1px dashed #cbd5e1;
|
border: 1px dashed #cbd5e1;
|
||||||
border-radius: 12px;
|
border-radius: 8px;
|
||||||
color: #94a3b8;
|
color: #94a3b8;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
font-size: 12px;
|
||||||
min-height: 72px;
|
min-height: 72px;
|
||||||
padding: 12px;
|
padding: 10px;
|
||||||
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) {
|
||||||
.grid {
|
.gift-editor {
|
||||||
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>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user