Files
admin-ui/src/views/assets/asset/component/editAsset.vue

1009 lines
29 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="assets-edit-asset-container">
<el-dialog :title="isEdit ? '修改资产' : '新增资产'" v-model="isShowDialog" width="1000px" destroy-on-close>
<el-form ref="formRef" :model="ruleForm" :rules="rules" size="default" label-width="100px" v-loading="formLoading">
<!-- 基础信息 -->
<el-divider content-position="left">基础信息</el-divider>
<el-row :gutter="24">
<el-col :span="8">
<el-form-item label="资产名称" prop="name">
<el-input v-model="ruleForm.name" placeholder="请输入资产名称" clearable />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="资产类型" prop="type">
<el-select v-model="ruleForm.type" placeholder="请选择资产类型" class="w100" :disabled="isEdit">
<el-option label="实物" value="physical" />
<el-option label="虚拟" value="virtual" />
<el-option label="服务" value="service" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="资产分类" prop="categoryId">
<el-cascader
v-model="ruleForm.categoryId"
:options="categoryOptions"
:props="{ checkStrictly: true, emitPath: false, value: 'id', label: 'name', children: 'children' }"
placeholder="请选择资产分类"
clearable
class="w100"
:disabled="isEdit"
@change="onCategoryChange"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="24">
<el-col :span="8">
<el-form-item label="上线时间">
<el-date-picker
v-model="ruleForm.onlineTime"
type="datetime"
placeholder="请选择上线时间"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
class="w100"
@change="onOnlineTimeChange"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="下线时间">
<el-date-picker
v-model="ruleForm.offlineTime"
type="datetime"
placeholder="请选择下线时间"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
class="w100"
:disabled-date="disabledOfflineDate"
/>
</el-form-item>
</el-col>
</el-row>
<!-- 分类属性值选择 -->
<template v-if="categoryAttrs.length > 0">
<el-divider content-position="left">分类属性</el-divider>
<el-row :gutter="24">
<el-col :span="8" v-for="(attr, index) in categoryAttrs" :key="index">
<el-form-item
:label="getAttrLabel(attr)"
:prop="'metadata.' + getAttrKey(attr)"
:rules="[{ required: true, message: getAttrLabel(attr) + '不能为空', trigger: ['blur', 'change'] }]"
>
<!-- 单选类型 -->
<el-select
v-if="attr.type === 'select'"
v-model="ruleForm.metadata[getAttrKey(attr)]"
:placeholder="'请选择' + getAttrLabel(attr)"
class="w100"
clearable
>
<el-option
v-for="opt in attr.options"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
<!-- 多选类型 -->
<el-select
v-else-if="attr.type === 'multi_select'"
v-model="ruleForm.metadata[getAttrKey(attr)]"
:placeholder="'请选择' + getAttrLabel(attr)"
class="w100"
multiple
clearable
>
<el-option
v-for="opt in attr.options"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
<!-- 文本类型 -->
<el-input
v-else-if="attr.type === 'text'"
v-model="ruleForm.metadata[getAttrKey(attr)]"
:placeholder="'请输入' + getAttrLabel(attr)"
/>
<!-- 数字类型 -->
<el-input-number
v-else-if="attr.type === 'number'"
v-model="ruleForm.metadata[getAttrKey(attr)]"
class="w100"
/>
<!-- 日期类型 -->
<el-date-picker
v-else-if="attr.type === 'date'"
v-model="ruleForm.metadata[getAttrKey(attr)]"
type="date"
:placeholder="'请选择' + getAttrLabel(attr)"
class="w100"
/>
<!-- 布尔类型 -->
<el-switch
v-else-if="attr.type === 'boolean'"
v-model="ruleForm.metadata[getAttrKey(attr)]"
/>
<!-- 图片类型 -->
<div v-else-if="attr.type === 'image'" class="w100">
<el-upload
class="attr-image-uploader"
:show-file-list="false"
:auto-upload="false"
accept="image/*"
:on-change="onAttrImageChange(attr)"
>
<img
v-if="ruleForm.metadata[getAttrKey(attr)]"
:src="formatImageUrl(ruleForm.metadata[getAttrKey(attr)])"
class="avatar"
style="width: 80px; height: 80px"
/>
<el-icon v-else class="avatar-uploader-icon" style="width: 80px; height: 80px; line-height: 80px"><Plus /></el-icon>
</el-upload>
<el-button
v-if="ruleForm.metadata[getAttrKey(attr)]"
type="danger"
link
size="small"
@click.stop="removeAttrImage(attr)"
style="margin-top: 5px"
>
删除图片
</el-button>
</div>
</el-form-item>
</el-col>
</el-row>
</template>
<!-- 图片上传 -->
<el-divider content-position="left">图片信息</el-divider>
<el-row :gutter="24">
<el-col :span="8">
<el-form-item label="主图" prop="mainImage">
<div class="w100">
<el-upload
class="avatar-uploader"
:show-file-list="false"
:auto-upload="false"
:on-change="handleMainImageChange"
accept="image/*"
>
<img v-if="mainImagePreview" :src="mainImagePreview" class="avatar" />
<el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
</el-upload>
<div v-if="mainImagePreview" style="margin-top: 5px">
<el-button type="primary" link size="small" @click.stop="previewMainImage">预览</el-button>
<el-button type="danger" link size="small" @click.stop="removeMainImage">删除</el-button>
</div>
</div>
</el-form-item>
</el-col>
<el-col :span="16">
<el-form-item label="图片列表">
<el-upload
v-model:file-list="imageFileList"
list-type="picture-card"
:auto-upload="false"
:on-preview="handlePictureCardPreview"
:on-remove="handleRemove"
accept="image/*"
multiple
>
<el-icon><Plus /></el-icon>
</el-upload>
</el-form-item>
</el-col>
</el-row>
<!-- 资产描述 -->
<el-divider content-position="left">资产描述</el-divider>
<el-form-item label="描述内容" label-width="100px">
<div class="editor-wrapper">
<Editor v-model="ruleForm.description" height="200px" placeholder="请输入资产描述" />
</div>
</el-form-item>
<!-- 实物资产配置 -->
<template v-if="ruleForm.type === 'physical'">
<el-divider content-position="left">实物资产配置</el-divider>
<el-row :gutter="24">
<el-col :span="8">
<el-form-item label="配送方式">
<el-select v-model="ruleForm.physicalAssetConfig.shipping.deliveryMethod" placeholder="请选择配送方式" class="w100">
<el-option label="快递" value="express" />
<el-option label="自提" value="self_pickup" />
<el-option label="同城配送" value="city_delivery" disabled />
</el-select>
</el-form-item>
</el-col>
</el-row>
</template>
<!-- 服务资产配置 -->
<template v-if="ruleForm.type === 'service'">
<el-divider content-position="left">服务资产配置</el-divider>
<!-- 预订配置 -->
<el-row :gutter="24">
<el-col :span="6">
<el-form-item label="最小提前" prop="serviceAssetConfig.booking.minAdvance">
<el-input-number v-model="ruleForm.serviceAssetConfig.booking.minAdvance" :min="0" class="w100" />
<span class="unit-text">分钟</span>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="最小时长" prop="serviceAssetConfig.booking.minDuration">
<el-input-number v-model="ruleForm.serviceAssetConfig.booking.minDuration" :min="0" class="w100" />
<span class="unit-text">分钟</span>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="取消提前" prop="serviceAssetConfig.booking.cancelWindow">
<el-input-number v-model="ruleForm.serviceAssetConfig.booking.cancelWindow" :min="0" class="w100" />
<span class="unit-text">分钟</span>
</el-form-item>
</el-col>
</el-row>
<!-- 时间段配置 -->
<el-form-item label="服务时间" prop="serviceAssetConfig.schedule.timeSlots">
<div class="config-list-container">
<div v-for="(slot, index) in ruleForm.serviceAssetConfig.schedule.timeSlots" :key="index" class="config-list-item">
<el-select v-model="slot.dayOfWeek" placeholder="星期" style="width: 100px">
<el-option v-for="d in 7" :key="d" :label="'周' + ['一','二','三','四','五','六','日'][d-1]" :value="String(d)" />
</el-select>
<el-time-picker v-model="slot.startTime" format="HH:mm" value-format="HH:mm" placeholder="开始" style="width: 100px" />
<span class="separator">-</span>
<el-time-picker v-model="slot.endTime" format="HH:mm" value-format="HH:mm" placeholder="结束" style="width: 100px" />
<el-input-number v-model="slot.capacity" :min="1" placeholder="容量" style="width: 100px" controls-position="right" />
<el-button type="danger" :icon="Delete" circle size="small" @click="removeTimeSlot(index)" />
</div>
<el-button
type="primary"
:icon="Plus"
size="small"
@click="addTimeSlot"
:disabled="isTimeSlotLimitReached"
>
添加时间段
</el-button>
</div>
</el-form-item>
<!-- 例外日期配置 -->
<el-form-item label="休息时间">
<div class="config-list-container">
<div v-for="(exc, index) in ruleForm.serviceAssetConfig.schedule.exceptions" :key="index" class="config-list-item">
<el-select v-model="exc.exceptionType" placeholder="类型" style="width: 100px" @change="onExceptionTypeChange(exc)">
<el-option label="指定日期" value="date" />
<el-option label="指定星期" value="dayOfWeek" />
</el-select>
<el-date-picker v-if="exc.exceptionType === 'date'" v-model="exc.date" type="date" format="YYYY-MM-DD" value-format="YYYY-MM-DD" placeholder="日期" style="width: 130px" />
<el-select v-if="exc.exceptionType === 'dayOfWeek'" v-model="exc.dayOfWeek" placeholder="星期" style="width: 100px">
<el-option v-for="d in 7" :key="d" :label="'周' + ['一','二','三','四','五','六','日'][d-1]" :value="String(d)" />
</el-select>
<el-select v-model="exc.status" placeholder="状态" style="width: 90px">
<el-option label="可用" :value="1" />
<el-option label="不可用" :value="0" />
</el-select>
<el-input v-model="exc.reason" placeholder="原因" style="width: 120px" />
<el-button type="danger" :icon="Delete" circle size="small" @click="removeException(index)" />
</div>
<el-button type="primary" :icon="Plus" size="small" @click="addException">添加休息时间</el-button>
</div>
</el-form-item>
</template>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="onCancel" size="default"> </el-button>
<el-button type="primary" @click="onSubmit" size="default" :loading="submitLoading">{{ isEdit ? '修 改' : '添 加' }}</el-button>
</span>
</template>
</el-dialog>
<!-- 图片预览 -->
<el-dialog v-model="dialogVisible" title="图片预览">
<img :src="dialogImageUrl" style="width: 100%" />
</el-dialog>
</div>
</template>
<script lang="ts">
export default {
name: 'assetsEditAsset',
};
</script>
<script setup lang="ts">
import { ref, reactive, watch, computed } from 'vue';
import { ElMessage } from 'element-plus';
import type { FormInstance, FormRules } from 'element-plus';
import { Plus, Delete } from '@element-plus/icons-vue';
import { getAsset, createAsset, updateAsset } from '/@/api/assets/asset';
import { getCategoryTree, getCategory } from '/@/api/assets/category';
import Editor from '/@/components/editor/index.vue';
import type { UploadFile, UploadUserFile } from 'element-plus';
// 类型定义
interface TimeSlot {
dayOfWeek: string;
startTime: string;
endTime: string;
capacity: number;
}
interface Exception {
exceptionType: 'date' | 'dayOfWeek';
date: string;
status: number;
reason: string;
dayOfWeek: string;
}
interface CategoryAttr {
name: string;
type: string;
options?: { label: string; value: string }[];
}
interface RuleForm {
id: string;
name: string;
type: string;
categoryId: string;
description: string;
onlineTime: string;
offlineTime: string;
physicalAssetConfig: {
shipping: {
deliveryMethod: string;
deliveryTime: number;
};
};
virtualAssetConfig: {
method: string;
requestURL: string;
authType: string;
authConfig: string;
};
serviceAssetConfig: {
schedule: {
timeSlots: TimeSlot[];
exceptions: Exception[];
};
booking: {
minAdvance: number;
minDuration: number;
cancelWindow: number;
};
capacity: {
maxUsers: number;
};
};
metadata: Record<string, any>;
mainImage?: string;
}
const emit = defineEmits(['getAssetList']);
const formRef = ref<FormInstance>();
const editAssetRef = ref();
const MAX_TIME_SLOTS = 7;
const isShowDialog = ref(false);
const isEdit = ref(false);
const submitLoading = ref(false);
const formLoading = ref(false);
const categoryOptions = ref<any[]>([]);
const categoryAttrs = ref<CategoryAttr[]>([]);
const isTimeSlotLimitReached = computed(() => ruleForm.serviceAssetConfig.schedule.timeSlots.length >= MAX_TIME_SLOTS);
// 获取属性的key
const getAttrKey = (attr: CategoryAttr): string => {
return attr.name || `attr_${categoryAttrs.value.indexOf(attr)}`;
};
// 获取属性的显示名称
const getAttrLabel = (attr: CategoryAttr): string => {
return attr.name || '属性';
};
// 图片相关
const mainImageFile = ref<File | null>(null);
const mainImagePreview = ref('');
const imageFileList = ref<UploadUserFile[]>([]);
const attrImageFiles = ref<Record<string, File>>({}); // 暂存属性图片文件
const dialogVisible = ref(false);
const dialogImageUrl = ref('');
// 图片拼接
const imgAddressPrefix = ref('');
const formatImageUrl = (url?: string) => {
if (!url) return '';
if (/^https?:\/\//i.test(url)) return url;
if (/^blob:/i.test(url)) return url; // 支持本地预览地址
return `${imgAddressPrefix.value || ''}${url}`;
};
const createDefaultTimeSlots = (): TimeSlot[] => {
const slots: TimeSlot[] = [];
for (let i = 1; i <= MAX_TIME_SLOTS; i++) {
slots.push({
dayOfWeek: String(i),
startTime: '09:00',
endTime: '18:00',
capacity: 100,
});
}
return slots;
};
// 初始表单数据
const getInitialForm = (): RuleForm => ({
id: '',
name: '',
type: 'physical',
categoryId: '',
description: '',
onlineTime: '',
offlineTime: '',
physicalAssetConfig: {
shipping: {
deliveryMethod: 'express',
deliveryTime: 24,
},
},
virtualAssetConfig: {
method: 'GET',
requestURL: '',
authType: 'none',
authConfig: '',
},
serviceAssetConfig: {
schedule: {
timeSlots: createDefaultTimeSlots(),
exceptions: [],
},
booking: {
minAdvance: 60,
minDuration: 30,
cancelWindow: 30,
},
capacity: {
maxUsers: 0,
},
},
metadata: {},
mainImage: '',
});
const ruleForm = reactive<RuleForm>(getInitialForm());
const validateOfflineTime = (_rule: any, value: string, callback: Function) => {
if (value && ruleForm.onlineTime && new Date(value).getTime() < new Date(ruleForm.onlineTime).getTime()) {
callback(new Error('下线时间不能早于上线时间'));
} else {
callback();
}
};
const disabledOfflineDate = (time: Date) => {
if (!ruleForm.onlineTime) return false;
return time.getTime() < new Date(ruleForm.onlineTime).setHours(0, 0, 0, 0);
};
const validateTimeSlots = (_rule: any, value: TimeSlot[], callback: Function) => {
if (!value || value.length === 0) {
callback(new Error('请至少添加一个服务时间段'));
return;
}
for (let i = 0; i < value.length; i++) {
const slot = value[i];
if (!slot.dayOfWeek || !slot.startTime || !slot.endTime || !slot.capacity) {
callback(new Error(`${i + 1} 行服务时间配置不完整`));
return;
}
}
callback();
};
const onOnlineTimeChange = () => {
if (ruleForm.offlineTime) {
formRef.value?.validateField('offlineTime');
}
};
const rules: FormRules = {
name: [{ required: true, message: '资产名称不能为空', trigger: 'blur' }],
type: [{ required: true, message: '请选择资产类型', trigger: 'change' }],
categoryId: [{ required: true, message: '请选择资产分类', trigger: 'change' }],
offlineTime: [{ validator: validateOfflineTime, trigger: 'change' }],
mainImage: [{ required: true, message: '请上传主图', trigger: 'change' }],
'serviceAssetConfig.booking.minAdvance': [{ required: true, message: '请输入最小提前时间', trigger: 'blur' }],
'serviceAssetConfig.booking.minDuration': [{ required: true, message: '请输入最小时长', trigger: 'blur' }],
'serviceAssetConfig.booking.cancelWindow': [{ required: true, message: '请输入取消提前时间', trigger: 'blur' }],
'serviceAssetConfig.schedule.timeSlots': [{ validator: validateTimeSlots, trigger: 'change' }],
};
// 主图上传处理
const handleMainImageChange = (file: UploadFile) => {
if (file.raw) {
mainImageFile.value = file.raw;
mainImagePreview.value = URL.createObjectURL(file.raw);
ruleForm.mainImage = 'set'; // 标记已上传
formRef.value?.validateField('mainImage');
}
};
// 主图预览
const previewMainImage = () => {
if (mainImagePreview.value) {
dialogImageUrl.value = mainImagePreview.value;
dialogVisible.value = true;
}
};
// 主图删除
const removeMainImage = () => {
mainImageFile.value = null;
mainImagePreview.value = '';
ruleForm.mainImage = '';
formRef.value?.validateField('mainImage');
};
// 图片列表预览
const handlePictureCardPreview = (file: UploadFile) => {
console.log(file,'111');
dialogImageUrl.value = file.url || '';
dialogVisible.value = true;
};
// 图片列表移除
const handleRemove = (file: UploadFile) => {
const index = imageFileList.value.findIndex((f) => f.uid === file.uid);
if (index > -1) {
imageFileList.value.splice(index, 1);
}
};
// 属性图片上传处理
const handleAttrImageChange = (file: UploadFile, attr: CategoryAttr) => {
if (file.raw) {
const key = getAttrKey(attr);
const url = URL.createObjectURL(file.raw);
ruleForm.metadata[key] = url;
attrImageFiles.value[key] = file.raw;
formRef.value?.validateField(`metadata.${key}`);
}
};
// 生成属性图片上传回调
const onAttrImageChange = (attr: CategoryAttr) => {
return (file: UploadFile) => handleAttrImageChange(file, attr);
};
// 移除属性图片
const removeAttrImage = (attr: CategoryAttr) => {
const key = getAttrKey(attr);
ruleForm.metadata[key] = '';
if (attrImageFiles.value[key]) {
delete attrImageFiles.value[key];
}
formRef.value?.validateField(`metadata.${key}`);
};
// 时间段操作
const addTimeSlot = () => {
if (!ruleForm.serviceAssetConfig.schedule) {
ruleForm.serviceAssetConfig.schedule = { timeSlots: [], exceptions: [] };
}
if (!ruleForm.serviceAssetConfig.schedule.timeSlots) {
ruleForm.serviceAssetConfig.schedule.timeSlots = [];
}
ruleForm.serviceAssetConfig.schedule.timeSlots.push({
dayOfWeek: '1',
startTime: '09:00',
endTime: '18:00',
capacity: 100,
});
formRef.value?.validateField('serviceAssetConfig.schedule.timeSlots');
};
const removeTimeSlot = (index: number) => {
ruleForm.serviceAssetConfig.schedule.timeSlots.splice(index, 1);
formRef.value?.validateField('serviceAssetConfig.schedule.timeSlots');
};
// 例外日期操作
const addException = () => {
if (!ruleForm.serviceAssetConfig.schedule) {
ruleForm.serviceAssetConfig.schedule = { timeSlots: [], exceptions: [] };
}
if (!ruleForm.serviceAssetConfig.schedule.exceptions) {
ruleForm.serviceAssetConfig.schedule.exceptions = [];
}
ruleForm.serviceAssetConfig.schedule.exceptions.push({
exceptionType: 'date',
date: '',
status: 1,
reason: '',
dayOfWeek: '1',
});
};
const removeException = (index: number) => {
ruleForm.serviceAssetConfig.schedule.exceptions.splice(index, 1);
};
// 例外类型切换时清空对应字段
const onExceptionTypeChange = (exc: Exception) => {
if (exc.exceptionType === 'date') {
exc.dayOfWeek = '';
} else {
exc.date = '';
}
};
// 重置表单
const resetForm = () => {
const initial = getInitialForm();
Object.assign(ruleForm, initial);
mainImageFile.value = null;
mainImagePreview.value = '';
imageFileList.value = [];
attrImageFiles.value = {};
categoryAttrs.value = [];
imgAddressPrefix.value = '';
};
// 获取分类数据
const fetchCategories = () => {
getCategoryTree()
.then((res: any) => {
const tree = res.data?.tree ?? [];
categoryOptions.value = tree.length > 0 && tree[0].children ? tree[0].children : tree;
})
.catch(() => {
categoryOptions.value = [];
});
};
// 分类变更时获取分类属性
const onCategoryChange = (categoryId: string) => {
categoryAttrs.value = [];
ruleForm.metadata = {};
if (!categoryId) return;
getCategory(categoryId)
.then((res: any) => {
const data = res.data;
if (data?.attrs && Array.isArray(data.attrs)) {
categoryAttrs.value = data.attrs;
// 初始化属性值,确保 boolean 类型默认为 false
categoryAttrs.value.forEach((attr: CategoryAttr) => {
const key = getAttrKey(attr);
if (attr.type === 'boolean') {
ruleForm.metadata[key] = false;
}
});
}
})
.catch(() => {
categoryAttrs.value = [];
});
};
// 打开弹窗
const openDialog = (row?: any, edit?: boolean) => {
resetForm();
isEdit.value = edit || false;
fetchCategories();
if (row && edit) {
// 修改模式:获取详情
formLoading.value = true;
getAsset(row.id)
.then((res: any) => {
const data = res.data;
imgAddressPrefix.value = data.imgAddressPrefix || '';
ruleForm.id = data.id || '';
ruleForm.name = data.name || '';
ruleForm.type = data.type || 'physical';
ruleForm.categoryId = data.categoryId || '';
ruleForm.description = data.description || '';
ruleForm.onlineTime = data.onlineTime || '';
ruleForm.offlineTime = data.offlineTime || '';
// 主图预览
if (data.imageUrl) {
mainImagePreview.value = formatImageUrl(data.imageUrl);
ruleForm.mainImage = data.imageUrl;
}
// 图片列表
if (data.images && Array.isArray(data.images)) {
imageFileList.value = data.images.map((url: string, index: number) => ({
name: `image-${index}`,
url: formatImageUrl(url),
}));
}
// 根据类型加载配置
if (data.type === 'physical' && data.physicalAssetConfig) {
Object.assign(ruleForm.physicalAssetConfig, data.physicalAssetConfig);
}
if (data.type === 'virtual' && data.virtualAssetConfig) {
Object.assign(ruleForm.virtualAssetConfig, data.virtualAssetConfig);
}
if (data.type === 'service' && data.serviceAssetConfig) {
Object.assign(ruleForm.serviceAssetConfig, data.serviceAssetConfig);
// 确保 schedule 对象存在
if (!ruleForm.serviceAssetConfig.schedule) {
ruleForm.serviceAssetConfig.schedule = { timeSlots: [], exceptions: [] };
}
// 确保数组存在,防止后端返回 null 或 undefined 导致 push 报错
if (!ruleForm.serviceAssetConfig.schedule.exceptions) {
ruleForm.serviceAssetConfig.schedule.exceptions = [];
} else {
// 补充缺失的 exceptionType
ruleForm.serviceAssetConfig.schedule.exceptions.forEach((exc) => {
if (!exc.exceptionType) {
if (exc.date) {
exc.exceptionType = 'date';
} else if (exc.dayOfWeek) {
exc.exceptionType = 'dayOfWeek';
} else {
// 默认值
exc.exceptionType = 'date';
}
}
});
}
if (!ruleForm.serviceAssetConfig.schedule.timeSlots) {
ruleForm.serviceAssetConfig.schedule.timeSlots = [];
}
}
// 元数据
if (data.metadata) {
Object.assign(ruleForm.metadata, data.metadata);
}
// 加载分类属性
if (data.categoryId) {
getCategory(data.categoryId)
.then((catRes: any) => {
const catData = catRes.data;
if (catData?.attrs && Array.isArray(catData.attrs)) {
categoryAttrs.value = catData.attrs;
// 初始化属性值,确保 boolean 类型默认为 false
categoryAttrs.value.forEach((attr: CategoryAttr) => {
const key = getAttrKey(attr);
if (attr.type === 'boolean' && ruleForm.metadata[key] === undefined) {
ruleForm.metadata[key] = false;
}
});
}
})
.catch(() => {
categoryAttrs.value = [];
});
}
})
.finally(() => {
formLoading.value = false;
});
}
isShowDialog.value = true;
};
// 关闭弹窗
const closeDialog = () => {
isShowDialog.value = false;
};
// 取消
const onCancel = () => {
closeDialog();
};
// 构建FormData
const buildFormData = (): FormData => {
const formData = new FormData();
// 基础字段
if (isEdit.value && ruleForm.id) {
formData.append('id', ruleForm.id);
}
formData.append('name', ruleForm.name);
formData.append('type', ruleForm.type);
formData.append('categoryId', ruleForm.categoryId);
formData.append('description', ruleForm.description || '');
if (ruleForm.onlineTime) {
formData.append('onlineTime', ruleForm.onlineTime);
}
if (ruleForm.offlineTime) {
formData.append('offlineTime', ruleForm.offlineTime);
}
// 主图
if (mainImageFile.value) {
formData.append('imageUrl', mainImageFile.value);
}
// 图片列表
imageFileList.value.forEach((file) => {
if (file.raw) {
formData.append('images', file.raw);
}
});
// 根据类型添加配置
if (ruleForm.type === 'physical') {
formData.append('physicalAssetConfig', JSON.stringify(ruleForm.physicalAssetConfig));
} else if (ruleForm.type === 'virtual') {
formData.append('virtualAssetConfig', JSON.stringify(ruleForm.virtualAssetConfig));
} else if (ruleForm.type === 'service') {
formData.append('serviceAssetConfig', JSON.stringify(ruleForm.serviceAssetConfig));
}
// 属性图片
Object.keys(attrImageFiles.value).forEach((key) => {
formData.append(key, attrImageFiles.value[key]);
});
// 元数据(分类属性值)
if (Object.keys(ruleForm.metadata).length > 0) {
// 过滤掉 blob url避免传给后端
const metadataToSend = { ...ruleForm.metadata };
Object.keys(metadataToSend).forEach((key) => {
if (typeof metadataToSend[key] === 'string' && metadataToSend[key].startsWith('blob:')) {
// 如果有对应的文件在 attrImageFiles 中,则置空或不传,这里选择置空
metadataToSend[key] = '';
}
});
formData.append('metadata', JSON.stringify(metadataToSend));
}
return formData;
};
// 提交
const onSubmit = () => {
const form = formRef.value;
if (!form) return;
form.validate((valid: boolean) => {
if (valid) {
submitLoading.value = true;
const formData = buildFormData();
const request = isEdit.value ? updateAsset(formData) : createAsset(formData);
request
.then(() => {
ElMessage.success(isEdit.value ? '修改成功' : '添加成功');
closeDialog();
emit('getAssetList');
})
.finally(() => {
submitLoading.value = false;
});
}
});
};
// 暴露方法
defineExpose({
openDialog,
});
</script>
<style scoped lang="scss">
.w100 {
width: 100%;
}
.ml10 {
margin-left: 10px;
}
.mx5 {
margin: 0 5px;
}
.text-muted {
color: #909399;
font-size: 12px;
}
.avatar-uploader {
:deep(.el-upload) {
border: 1px dashed var(--el-border-color);
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
transition: var(--el-transition-duration-fast);
&:hover {
border-color: var(--el-color-primary);
}
}
}
.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 100px;
height: 100px;
text-align: center;
line-height: 100px;
}
.avatar {
width: 100px;
height: 100px;
display: block;
object-fit: cover;
}
.config-list-container {
width: 100%;
.config-list-item {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
flex-wrap: wrap;
.separator {
color: #909399;
}
.el-button.is-circle {
:deep(.el-icon) {
margin-right: 0 !important;
}
}
}
}
.unit-text {
margin-left: 8px;
color: #909399;
font-size: 12px;
white-space: nowrap;
}
.editor-wrapper {
width: 100%;
border: 1px solid #dcdfe6;
border-radius: 4px;
:deep(.editor-toolbar) {
border-bottom: 1px solid #dcdfe6;
}
}
.attr-image-uploader {
:deep(.el-upload) {
border: 1px dashed var(--el-border-color);
border-radius: 6px;
cursor: pointer;
}
}
</style>