多选修改

This commit is contained in:
zhx 2026-05-19 22:23:14 +08:00
parent aa0988af6b
commit 045a8af46f
5 changed files with 1257 additions and 341 deletions

View File

@ -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>

View File

@ -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;

View File

@ -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">

View File

@ -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