yumi-admin/apps/src/views/props/components/resource-config-modal.vue
2026-04-13 19:31:36 +08:00

401 lines
9.8 KiB
Vue

<script lang="ts" setup>
import { computed, reactive, ref, watch } from 'vue';
import {
OSS_FILE_BUCKETS,
getAccessImgUrl,
simpleUploadFile,
} from '#/api/legacy/oss';
import {
addPropsSource,
updatePropsSource,
} from '#/api/legacy/props';
import {
Button,
Form,
FormItem,
Image,
Input,
Modal,
Select,
Switch,
message,
} from 'antdv-next';
import {
NOBLE_VIP_OPTIONS,
PROPS_TYPES,
} from '../shared';
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 coverUploading = ref(false);
const sourceUploading = ref(false);
const expandUploading = ref(false);
const coverInputRef = ref<HTMLInputElement>();
const sourceInputRef = ref<HTMLInputElement>();
const expandInputRef = ref<HTMLInputElement>();
const form = reactive<Record<string, any>>({
adminFree: false,
amount: '',
code: '',
cover: '',
expand: '',
id: '',
name: '',
sourceUrl: '',
sysOrigin: '',
type: '',
});
const isUpdate = computed(() => Boolean(form.id));
const isImageSourceType = computed(() =>
['THEME', 'LAYOUT', 'CHAT_BUBBLE'].includes(form.type),
);
const isAmountRequired = computed(
() => !['CUSTOMIZE', 'FRAGMENTS'].includes(form.type),
);
const isSourceRequired = computed(
() => !['CUSTOMIZE', 'FRAGMENTS'].includes(form.type),
);
const showExpandUpload = computed(() => form.type === 'CHAT_BUBBLE');
const title = computed(() => (isUpdate.value ? '修改资源' : '新增资源'));
const propsTypeOptions = PROPS_TYPES.map((item) => ({
label: item.name,
value: item.value as any,
}));
const nobleVipNameOptions = NOBLE_VIP_OPTIONS.map((item) => ({
label: item.name,
value: item.value as any,
}));
function resetForm() {
form.adminFree = false;
form.amount = '';
form.code = '';
form.cover = '';
form.expand = '';
form.id = '';
form.name = '';
form.sourceUrl = '';
form.sysOrigin = props.sysOriginOptions[0]?.value ?? 'LIKEI';
form.type = PROPS_TYPES[0]?.value ?? '';
}
watch(
() => props.open,
(open) => {
if (!open) {
return;
}
const record = props.record;
if (!record) {
resetForm();
return;
}
form.adminFree = Boolean(record.adminFree);
form.amount = record.amount ?? '';
form.code = record.code ?? '';
form.cover = record.cover ?? '';
form.expand = record.expand ?? '';
form.id = record.id ?? '';
form.name = record.name ?? '';
form.sourceUrl = record.sourceUrl ?? '';
form.sysOrigin = record.sysOrigin || props.sysOriginOptions[0]?.value || 'LIKEI';
form.type = record.type ?? '';
},
{ immediate: true },
);
watch(
() => form.type,
(value) => {
if (!value) {
return;
}
if (value === 'NOBLE_VIP' && !NOBLE_VIP_OPTIONS.some((item) => item.value === form.name)) {
form.name = '';
}
if (['CUSTOMIZE', 'FRAGMENTS'].includes(value)) {
form.amount = 0;
form.sourceUrl = '';
form.expand = '';
}
if (value !== 'CHAT_BUBBLE') {
form.expand = '';
}
},
);
function openCoverPicker() {
coverInputRef.value?.click();
}
function openSourcePicker() {
sourceInputRef.value?.click();
}
function openExpandPicker() {
expandInputRef.value?.click();
}
function handleCoverInput(event: Event) {
void uploadFile(event, 'cover', coverUploading, true);
}
function handleSourceInput(event: Event) {
void uploadFile(event, 'sourceUrl', sourceUploading, isImageSourceType.value);
}
function handleExpandInput(event: Event) {
void uploadFile(event, 'expand', expandUploading, true);
}
async function uploadFile(
event: Event,
targetKey: 'cover' | 'expand' | 'sourceUrl',
loadingRef: typeof coverUploading,
imageBucket = false,
) {
const target = event.target as HTMLInputElement;
const file = target.files?.[0];
target.value = '';
if (!file) {
return;
}
loadingRef.value = true;
try {
const result = await simpleUploadFile(
file,
imageBucket ? OSS_FILE_BUCKETS.svgaCover : OSS_FILE_BUCKETS.svgasource,
);
form[targetKey] = getAccessImgUrl(result.name);
message.success('上传成功');
} finally {
loadingRef.value = false;
}
}
function validateForm() {
if (!form.sysOrigin) {
message.warning('请选择系统');
return false;
}
if (!form.type) {
message.warning('请选择类型');
return false;
}
if (!String(form.cover || '').trim()) {
message.warning('请上传封面');
return false;
}
if (isSourceRequired.value && !String(form.sourceUrl || '').trim()) {
message.warning('请上传资源');
return false;
}
if (showExpandUpload.value && !String(form.expand || '').trim()) {
message.warning('请上传 Android 资源');
return false;
}
if (!String(form.name || '').trim()) {
message.warning(form.type === 'NOBLE_VIP' ? '请选择贵族类型' : '请输入名称');
return false;
}
if (!isUpdate.value && !String(form.code || '').trim()) {
message.warning('请输入编码');
return false;
}
if (isAmountRequired.value && (form.amount === '' || Number.isNaN(Number(form.amount)))) {
message.warning('请输入底价');
return false;
}
return true;
}
async function handleSubmit() {
if (!validateForm()) {
return;
}
saving.value = true;
try {
const payload = {
...form,
amount: isAmountRequired.value ? Number(form.amount) : 0,
};
if (isUpdate.value) {
await updatePropsSource(payload);
} else {
await addPropsSource(payload);
}
message.success('保存成功');
emit('success');
emit('close');
} finally {
saving.value = false;
}
}
</script>
<template>
<Modal
:confirm-loading="saving"
:open="open"
destroy-on-close
:title="title"
width="720"
@cancel="emit('close')"
@ok="handleSubmit"
>
<Form layout="vertical">
<FormItem label="系统">
<SysOriginSelect v-model:value="form.sysOrigin"
:options="sysOriginOptions"
></SysOriginSelect>
</FormItem>
<FormItem label="类型">
<Select
option-label-prop="label"
v-model:value="form.type"
:options="propsTypeOptions"
/>
</FormItem>
<FormItem label="封面">
<input
ref="coverInputRef"
accept="image/*"
class="hidden-input"
type="file"
@change="handleCoverInput"
/>
<div class="upload-field">
<div v-if="form.cover" class="upload-preview">
<Image class="upload-preview__image" :src="form.cover" />
</div>
<div v-else class="upload-empty">请上传封面</div>
<Button :loading="coverUploading" @click="openCoverPicker">
{{ form.cover ? '重新上传' : '上传封面' }}
</Button>
</div>
</FormItem>
<FormItem v-if="isSourceRequired" :label="showExpandUpload ? 'iOS资源' : '资源'">
<input
ref="sourceInputRef"
:accept="isImageSourceType ? 'image/*' : '.svga,.pag,.mp4'"
class="hidden-input"
type="file"
@change="handleSourceInput"
/>
<div class="upload-field">
<div v-if="form.sourceUrl" class="upload-preview">
<Image
v-if="isImageSourceType"
class="upload-preview__image"
:src="form.sourceUrl"
/>
<div v-else class="upload-file">
{{ form.sourceUrl }}
</div>
</div>
<div v-else class="upload-empty">请上传资源</div>
<Button :loading="sourceUploading" @click="openSourcePicker">
{{ form.sourceUrl ? '重新上传' : '上传资源' }}
</Button>
</div>
</FormItem>
<FormItem v-if="showExpandUpload" label="Android资源">
<input
ref="expandInputRef"
accept="image/*"
class="hidden-input"
type="file"
@change="handleExpandInput"
/>
<div class="upload-field">
<div v-if="form.expand" class="upload-preview">
<Image class="upload-preview__image" :src="form.expand" />
</div>
<div v-else class="upload-empty">请上传 Android 资源</div>
<Button :loading="expandUploading" @click="openExpandPicker">
{{ form.expand ? '重新上传' : '上传资源' }}
</Button>
</div>
</FormItem>
<FormItem label="名称">
<Select
v-if="form.type === 'NOBLE_VIP'"
option-label-prop="label"
v-model:value="form.name"
:options="nobleVipNameOptions"
/>
<Input v-else v-model:value="form.name" />
</FormItem>
<FormItem v-if="!isUpdate" label="编码">
<Input v-model:value="form.code" placeholder="编码添加后不可修改" />
</FormItem>
<FormItem v-if="isAmountRequired" label="底价">
<Input v-model:value="form.amount" />
</FormItem>
<FormItem label="Admin Free">
<Switch
v-model:checked="form.adminFree"
checked-children=""
un-checked-children=""
/>
</FormItem>
</Form>
</Modal>
</template>
<style scoped>
.hidden-input {
display: none;
}
.upload-field {
display: grid;
gap: 12px;
}
.upload-preview,
.upload-empty {
align-items: center;
background: rgb(248 250 252);
border: 1px dashed rgb(148 163 184 / 35%);
border-radius: 16px;
display: flex;
justify-content: center;
min-height: 180px;
overflow: hidden;
padding: 12px;
}
.upload-empty {
color: rgb(148 163 184);
}
.upload-preview__image {
height: 180px;
object-fit: contain;
width: 100%;
}
.upload-file {
color: rgb(51 65 85);
line-height: 1.5;
word-break: break-all;
}
</style>