yumi-admin/apps/src/views/props/components/store-edit-modal.vue
2026-05-18 18:55:40 +08:00

557 lines
16 KiB
Vue

<script lang="ts" setup>
import { computed, reactive, ref, watch } from 'vue';
import {
Form,
FormItem,
Input,
message,
Modal,
Select,
Switch,
} from 'antdv-next';
import {
addOrUpdatePropsStoreLegacy,
listSysOriginTypeList,
} from '#/api/legacy/props';
import {
NOBLE_VIP_OPTIONS,
PROPS_CURRENCY_TYPES,
PROPS_STORE_TYPES,
PROPS_VALID_DAYS,
} from '../shared';
import RewardIcon from './reward-icon.vue';
const props = defineProps<{
open: boolean;
record: null | Record<string, any>;
sysOriginOptions: Array<{ label: string; value: string }>;
}>();
const emit = defineEmits<{
close: [];
success: [];
}>();
const saving = ref(false);
const optionsLoading = ref(false);
const sourceOptionsMap = reactive<
Record<string, { list: Array<Record<string, any>>; loaded: boolean; loading: boolean }>
>({});
const mainSource = ref<null | Record<string, any>>(null);
const rideSource = ref<null | Record<string, any>>(null);
const avatarSource = ref<null | Record<string, any>>(null);
const dataCardSource = ref<null | Record<string, any>>(null);
const chatBubbleSource = ref<null | Record<string, any>>(null);
const form = reactive<Record<string, any>>({
currencyTypes: '',
del: false,
discount: 0,
id: '',
label: '',
propsAbility: {
adminNumber: '',
avatarFrameId: '',
carId: '',
chatBubbleId: '',
dataCardId: '',
loginRewardsAmount: '',
roomMaxMember: '',
vipLevel: '',
vipType: '',
},
propsType: '',
shelfStatus: false,
sort: 0,
sourceId: '',
sysOrigin: '',
tmpCurrencyTypes: [] as Array<string>,
tmpValidDays: [] as Array<number | string>,
validDays: '',
});
const title = computed(() => (form.id ? '修改商品' : '新增商品'));
const isNobleVip = computed(() => form.propsType === 'NOBLE_VIP');
const propsStoreTypeOptions = PROPS_STORE_TYPES.map((item) => ({
label: item.name,
value: item.value as any,
}));
const currencyTypeOptions = computed(() =>
PROPS_CURRENCY_TYPES.map((item) => ({
disabled: isNobleVip.value && item.value !== 'GOLD',
label: item.name,
value: item.value as any,
})),
);
const validDayOptions = PROPS_VALID_DAYS.map((item) => ({
label: item.name,
value: item.value as any,
}));
const nobleVipOptions = NOBLE_VIP_OPTIONS.map((item) => ({
label: item.name,
value: item.value as any,
}));
function resetSourceOptionsMap() {
Object.keys(sourceOptionsMap).forEach((key) => {
delete sourceOptionsMap[key];
});
}
function createTypeStore(type: string) {
if (!sourceOptionsMap[type]) {
sourceOptionsMap[type] = { list: [], loaded: false, loading: false };
}
return sourceOptionsMap[type];
}
async function loadSourceOptions(type: string) {
if (!type || !form.sysOrigin) {
return;
}
const store = createTypeStore(type);
if (store.loaded || store.loading) {
return;
}
store.loading = true;
optionsLoading.value = true;
try {
store.list = await listSysOriginTypeList(form.sysOrigin, type);
store.loaded = true;
} finally {
store.loading = false;
optionsLoading.value = false;
}
}
function resetSelections() {
mainSource.value = null;
rideSource.value = null;
avatarSource.value = null;
dataCardSource.value = null;
chatBubbleSource.value = null;
}
async function ensureSelections() {
await loadSourceOptions(form.propsType);
if (isNobleVip.value) {
await Promise.all([
loadSourceOptions('RIDE'),
loadSourceOptions('AVATAR_FRAME'),
loadSourceOptions('DATA_CARD'),
loadSourceOptions('CHAT_BUBBLE'),
]);
}
}
watch(
() => props.open,
async (open) => {
if (!open) {
return;
}
resetSourceOptionsMap();
resetSelections();
const record = props.record || {};
const commodity = record.commodity || record;
const propsAbility = record.propsAbility || {};
const nobleVipAbility = propsAbility.nobleVipAbility || {};
form.currencyTypes = commodity.currencyTypes || '';
form.del = commodity.del ?? false;
form.discount = commodity.discount || 0;
form.id = commodity.id || '';
form.label = commodity.label || '';
form.propsType = commodity.propsType || '';
form.shelfStatus = Boolean(commodity.shelfStatus);
form.sort = commodity.sort ?? 0;
form.sourceId = commodity.sourceId || '';
form.sysOrigin = commodity.sysOrigin || props.sysOriginOptions[0]?.value || 'LIKEI';
form.tmpCurrencyTypes = commodity.currencyTypes
? String(commodity.currencyTypes)
.split(',')
.filter(Boolean)
: [];
form.tmpValidDays = commodity.validDays
? String(commodity.validDays)
.split(',')
.filter(Boolean)
: [];
form.validDays = commodity.validDays || '';
form.propsAbility = {
adminNumber: nobleVipAbility.adminNumber ?? '',
avatarFrameId: nobleVipAbility.avatarFrameId ?? '',
carId: nobleVipAbility.carId ?? '',
chatBubbleId: nobleVipAbility.chatBubbleId ?? '',
dataCardId: nobleVipAbility.dataCardId ?? '',
loginRewardsAmount: nobleVipAbility.loginRewardsAmount ?? '',
roomMaxMember: nobleVipAbility.roomMaxMember ?? '',
vipLevel: nobleVipAbility.vipLevel ?? '',
vipType: nobleVipAbility.vipType ?? '',
};
await ensureSelections();
mainSource.value = record.propsSource || null;
rideSource.value = propsAbility.car || null;
avatarSource.value = propsAbility.avatarFrame || null;
dataCardSource.value = propsAbility.dataCard || null;
chatBubbleSource.value = propsAbility.chatBubble || null;
},
{ immediate: true },
);
function updateSelectedSource(type: string, id: number | string) {
const item = sourceOptionsMap[type]?.list.find(
(option) => String(option.id) === String(id),
);
if (type === form.propsType) {
mainSource.value = item || null;
form.sourceId = id;
return;
}
if (type === 'RIDE') {
rideSource.value = item || null;
form.propsAbility.carId = id;
return;
}
if (type === 'AVATAR_FRAME') {
avatarSource.value = item || null;
form.propsAbility.avatarFrameId = id;
return;
}
if (type === 'DATA_CARD') {
dataCardSource.value = item || null;
form.propsAbility.dataCardId = id;
return;
}
if (type === 'CHAT_BUBBLE') {
chatBubbleSource.value = item || null;
form.propsAbility.chatBubbleId = id;
}
}
function getSourceSelectOptions(type: string) {
return (sourceOptionsMap[type]?.list || []).map((item) => ({
label: `${item.id} / ${item.name}`,
value: item.id as any,
}));
}
function validateForm() {
if (!form.sysOrigin) {
message.warning('请选择系统');
return false;
}
if (!form.propsType) {
message.warning('请选择道具类型');
return false;
}
if (!form.sourceId) {
message.warning('请选择道具资源');
return false;
}
if (form.tmpCurrencyTypes.length === 0) {
message.warning('请选择付费类型');
return false;
}
if (form.tmpValidDays.length === 0) {
message.warning('请选择有效天数');
return false;
}
if (form.sort === '' || Number.isNaN(Number(form.sort))) {
message.warning('请输入序号');
return false;
}
if (form.discount === '' || Number.isNaN(Number(form.discount))) {
message.warning('请输入折扣率');
return false;
}
if (isNobleVip.value) {
const ability = form.propsAbility;
if (
!ability.vipType ||
ability.loginRewardsAmount === '' ||
ability.vipLevel === '' ||
ability.adminNumber === '' ||
ability.roomMaxMember === '' ||
!ability.avatarFrameId ||
!ability.carId ||
!ability.chatBubbleId ||
!ability.dataCardId
) {
message.warning('请完整填写贵族能力信息');
return false;
}
}
return true;
}
async function handleSubmit() {
if (!validateForm()) {
return;
}
saving.value = true;
try {
await addOrUpdatePropsStoreLegacy({
...form,
currencyTypes: form.tmpCurrencyTypes.join(','),
discount: Number(form.discount) >= 0 && Number(form.discount) <= 1 ? Number(form.discount) : 1,
sort: Number(form.sort),
validDays: form.tmpValidDays.join(','),
});
message.success('保存成功');
emit('success');
emit('close');
} finally {
saving.value = false;
}
}
</script>
<template>
<Modal
:confirm-loading="saving"
:open="open"
destroy-on-close
:title="title"
width="960"
@cancel="emit('close')"
@ok="handleSubmit"
>
<Form layout="vertical">
<div class="form-grid">
<FormItem label="归属系统">
<SysOriginSelect
v-model:value="form.sysOrigin"
disabled
:options="sysOriginOptions"
/>
</FormItem>
<FormItem label="道具类型">
<Select
option-label-prop="label"
v-model:value="form.propsType"
:options="propsStoreTypeOptions"
disabled
/>
</FormItem>
</div>
<FormItem label="道具资源">
<Select
option-label-prop="label"
v-model:value="form.sourceId"
:loading="sourceOptionsMap[form.propsType]?.loading"
:options="getSourceSelectOptions(form.propsType)"
option-filter-prop="label"
show-search
@change="(value: string) => updateSelectedSource(form.propsType, value)"
/>
<div v-if="mainSource" class="source-preview">
<RewardIcon :item="mainSource" :size="56" />
<div class="source-preview__meta">
<div>{{ mainSource.name || '-' }}</div>
<div class="source-preview__id">ID: {{ mainSource.id || '-' }}</div>
</div>
</div>
</FormItem>
<div class="form-grid">
<FormItem label="付费类型">
<Select
option-label-prop="label"
v-model:value="form.tmpCurrencyTypes"
:options="currencyTypeOptions"
mode="multiple"
/>
</FormItem>
<FormItem label="有效天数">
<Select
option-label-prop="label"
v-model:value="form.tmpValidDays"
:options="validDayOptions"
mode="multiple"
/>
</FormItem>
</div>
<div class="form-grid">
<FormItem label="标签">
<Input v-model:value="form.label" placeholder="请输入标签" />
</FormItem>
<FormItem label="折扣率">
<Input v-model:value="form.discount" />
</FormItem>
</div>
<div class="form-grid">
<FormItem label="序号">
<Input v-model:value="form.sort" placeholder="序号越大越靠前" />
</FormItem>
<FormItem label="上架状态">
<Switch
v-model:checked="form.shelfStatus"
checked-children="上架"
un-checked-children="下架"
/>
</FormItem>
</div>
<template v-if="isNobleVip">
<div class="section-title">贵族能力</div>
<div class="form-grid">
<FormItem label="VIP类型">
<Select
option-label-prop="label"
v-model:value="form.propsAbility.vipType"
:options="nobleVipOptions"
/>
</FormItem>
<FormItem label="VIP等级">
<Input v-model:value="form.propsAbility.vipLevel" />
</FormItem>
</div>
<div class="form-grid">
<FormItem label="登录奖励">
<Input v-model:value="form.propsAbility.loginRewardsAmount" />
</FormItem>
<FormItem label="管理员数">
<Input v-model:value="form.propsAbility.adminNumber" />
</FormItem>
</div>
<FormItem label="房间成员数">
<Input v-model:value="form.propsAbility.roomMaxMember" />
</FormItem>
<div class="ability-grid">
<div class="ability-item">
<div class="ability-item__label">车辆</div>
<Select
option-label-prop="label"
v-model:value="form.propsAbility.carId"
:loading="sourceOptionsMap.RIDE?.loading"
:options="getSourceSelectOptions('RIDE')"
option-filter-prop="label"
show-search
@change="(value: string) => updateSelectedSource('RIDE', value)"
/>
<div v-if="rideSource" class="source-preview">
<RewardIcon :item="rideSource" :size="48" />
<div class="source-preview__meta">
<div>{{ rideSource.name }}</div>
</div>
</div>
</div>
<div class="ability-item">
<div class="ability-item__label">头像框</div>
<Select
option-label-prop="label"
v-model:value="form.propsAbility.avatarFrameId"
:loading="sourceOptionsMap.AVATAR_FRAME?.loading"
:options="getSourceSelectOptions('AVATAR_FRAME')"
option-filter-prop="label"
show-search
@change="(value: string) => updateSelectedSource('AVATAR_FRAME', value)"
/>
<div v-if="avatarSource" class="source-preview">
<RewardIcon :item="avatarSource" :size="48" />
<div class="source-preview__meta">
<div>{{ avatarSource.name }}</div>
</div>
</div>
</div>
<div class="ability-item">
<div class="ability-item__label">资料卡</div>
<Select
option-label-prop="label"
v-model:value="form.propsAbility.dataCardId"
:loading="sourceOptionsMap.DATA_CARD?.loading"
:options="getSourceSelectOptions('DATA_CARD')"
option-filter-prop="label"
show-search
@change="(value: string) => updateSelectedSource('DATA_CARD', value)"
/>
<div v-if="dataCardSource" class="source-preview">
<RewardIcon :item="dataCardSource" :size="48" />
<div class="source-preview__meta">
<div>{{ dataCardSource.name }}</div>
</div>
</div>
</div>
<div class="ability-item">
<div class="ability-item__label">聊天气泡</div>
<Select
option-label-prop="label"
v-model:value="form.propsAbility.chatBubbleId"
:loading="sourceOptionsMap.CHAT_BUBBLE?.loading"
:options="getSourceSelectOptions('CHAT_BUBBLE')"
option-filter-prop="label"
show-search
@change="(value: string) => updateSelectedSource('CHAT_BUBBLE', value)"
/>
<div v-if="chatBubbleSource" class="source-preview">
<RewardIcon :item="chatBubbleSource" :size="48" />
<div class="source-preview__meta">
<div>{{ chatBubbleSource.name }}</div>
</div>
</div>
</div>
</div>
</template>
</Form>
</Modal>
</template>
<style scoped>
.ability-grid,
.form-grid {
display: grid;
gap: 16px;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.ability-item {
background: rgb(248 250 252);
border: 1px solid rgb(226 232 240);
border-radius: 16px;
padding: 12px;
}
.ability-item__label {
color: rgb(15 23 42);
font-size: 13px;
font-weight: 600;
margin-bottom: 8px;
}
.section-title {
color: rgb(15 23 42);
font-size: 16px;
font-weight: 600;
margin: 8px 0 16px;
}
.source-preview {
align-items: center;
display: flex;
gap: 10px;
margin-top: 10px;
}
.source-preview__id {
color: rgb(100 116 139);
font-size: 12px;
}
</style>