重构资产管理功能,优化虚拟资产和服务资产配置结构
This commit is contained in:
@@ -53,24 +53,32 @@ export function getAssetCategories() {
|
||||
});
|
||||
}
|
||||
|
||||
// 新增资产(支持文件上传)
|
||||
export function createAsset(data: FormData) {
|
||||
// 新增资产
|
||||
export function createAsset(data: any) {
|
||||
return newService({
|
||||
url: '/assets/asset/createAsset',
|
||||
method: 'post',
|
||||
data,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 修改资产(支持文件上传)
|
||||
export function updateAsset(data: FormData) {
|
||||
// 修改资产
|
||||
export function updateAsset(data: any) {
|
||||
return newService({
|
||||
url: '/assets/asset/updateAsset',
|
||||
method: 'put',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
// 上传资产图片
|
||||
export function uploadAssetImage(file: File) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
return newService({
|
||||
url: '/assets/asset/Uploadimage',
|
||||
method: 'post',
|
||||
data: formData,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
<el-form-item
|
||||
:label="getAttrLabel(attr)"
|
||||
:prop="'metadata.' + getAttrKey(attr)"
|
||||
:rules="[{ required: true, message: getAttrLabel(attr) + '不能为空', trigger: ['blur', 'change'] }]"
|
||||
:rules="[{ required: !!attr.required, message: getAttrLabel(attr) + '不能为空', trigger: ['blur', 'change'] }]"
|
||||
>
|
||||
<!-- 单选类型 -->
|
||||
<el-select
|
||||
@@ -229,79 +229,191 @@
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<!-- 服务资产配置 -->
|
||||
<template v-if="ruleForm.type === 'service'">
|
||||
<el-divider content-position="left">服务资产配置</el-divider>
|
||||
<!-- 虚拟资产配置 -->
|
||||
<template v-if="ruleForm.type === 'virtual'">
|
||||
<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-col :span="8">
|
||||
<el-form-item label="虚拟类型" prop="virtualAssetConfig.virtualType">
|
||||
<el-select v-model="ruleForm.virtualAssetConfig.virtualType" placeholder="请选择虚拟类型" class="w100">
|
||||
<el-option label="充值" value="recharge" />
|
||||
<el-option label="订阅" value="subscribe" />
|
||||
</el-select>
|
||||
</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-col :span="8" v-if="ruleForm.virtualAssetConfig.virtualType === 'subscribe'">
|
||||
<el-form-item label="合集价格" prop="virtualAssetConfig.collectionPrice">
|
||||
<el-input-number v-model="ruleForm.virtualAssetConfig.collectionPrice" :min="0" :precision="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-col :span="8">
|
||||
<el-form-item label="货币单位" prop="virtualAssetConfig.currency">
|
||||
<el-select v-model="ruleForm.virtualAssetConfig.currency" placeholder="请选择货币单位" class="w100">
|
||||
<el-option label="人民币 (CNY)" value="CNY" />
|
||||
<el-option label="美元 (USD)" value="USD" />
|
||||
</el-select>
|
||||
</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)" />
|
||||
<!-- API 配置 -->
|
||||
<el-divider content-position="left">API 配置</el-divider>
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="请求方式" prop="virtualAssetConfig.apiConfig.method">
|
||||
<el-select v-model="ruleForm.virtualAssetConfig.apiConfig.method" placeholder="请选择请求方式" class="w100">
|
||||
<el-option label="GET" value="GET" />
|
||||
<el-option label="POST" value="POST" />
|
||||
<el-option label="PUT" value="PUT" />
|
||||
</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>
|
||||
</el-col>
|
||||
<el-col :span="16">
|
||||
<el-form-item label="请求地址" prop="virtualAssetConfig.apiConfig.requestURL">
|
||||
<el-input v-model="ruleForm.virtualAssetConfig.apiConfig.requestURL" placeholder="请输入请求地址" class="w100" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="24">
|
||||
<el-form-item label="请求头">
|
||||
<div class="config-list-container">
|
||||
<div v-for="(item, index) in ruleForm.virtualAssetConfig.apiConfig.headers" :key="index" class="config-list-item">
|
||||
<el-input v-model="item.key" placeholder="Key" style="width: 200px" />
|
||||
<span class="separator">:</span>
|
||||
<el-input v-model="item.value" placeholder="Value" style="width: 200px" />
|
||||
<el-button type="danger" :icon="Delete" circle size="small" @click="removeKeyValuePair(ruleForm.virtualAssetConfig.apiConfig.headers, index)" />
|
||||
</div>
|
||||
<el-button type="primary" :icon="Plus" size="small" @click="addKeyValuePair(ruleForm.virtualAssetConfig.apiConfig.headers)">添加请求头</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="24">
|
||||
<el-form-item label="请求参数">
|
||||
<div class="config-list-container">
|
||||
<div v-for="(item, index) in ruleForm.virtualAssetConfig.apiConfig.params" :key="index" class="config-list-item">
|
||||
<el-input v-model="item.key" placeholder="Key" style="width: 200px" />
|
||||
<span class="separator">:</span>
|
||||
<el-input v-model="item.value" placeholder="Value" style="width: 200px" />
|
||||
<el-button type="danger" :icon="Delete" circle size="small" @click="removeKeyValuePair(ruleForm.virtualAssetConfig.apiConfig.params, index)" />
|
||||
</div>
|
||||
<el-button type="primary" :icon="Plus" size="small" @click="addKeyValuePair(ruleForm.virtualAssetConfig.apiConfig.params)">添加请求参数</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="认证方式" prop="virtualAssetConfig.apiConfig.authType">
|
||||
<el-select v-model="ruleForm.virtualAssetConfig.apiConfig.authType" placeholder="请选择认证方式" class="w100">
|
||||
<el-option label="无" value="none" />
|
||||
<el-option label="API Key" value="apikey" />
|
||||
<el-option label="Bearer Token" value="bearer" />
|
||||
<el-option label="OAuth" value="oauth" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="16" v-if="ruleForm.virtualAssetConfig.apiConfig.authType !== 'none'">
|
||||
<el-form-item label="认证配置" prop="virtualAssetConfig.apiConfig.authConfig">
|
||||
<el-input v-model="ruleForm.virtualAssetConfig.apiConfig.authConfig" type="textarea" :rows="2" placeholder="请输入认证配置" class="w100" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<!-- 例外日期配置 -->
|
||||
<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" />
|
||||
<!-- 服务资产配置 -->
|
||||
<template v-if="ruleForm.type === 'service'">
|
||||
<el-divider content-position="left">服务资产配置</el-divider>
|
||||
|
||||
<!-- 服务类型选择 -->
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="服务类型" prop="serviceAssetConfig.serviceAssetType">
|
||||
<el-select v-model="ruleForm.serviceAssetConfig.serviceAssetType" placeholder="请选择服务类型" class="w100">
|
||||
<el-option label="到店" value="arrival" />
|
||||
<el-option label="上门" value="visit" disabled />
|
||||
</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)" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 到店服务配置 -->
|
||||
<template v-if="ruleForm.serviceAssetConfig.serviceAssetType === 'arrival'">
|
||||
<!-- 预订配置 -->
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="6">
|
||||
<el-form-item label="最小提前" prop="serviceAssetConfig.serviceAssetArrivalConfig.booking.minAdvance">
|
||||
<el-input-number v-model="ruleForm.serviceAssetConfig.serviceAssetArrivalConfig.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.serviceAssetArrivalConfig.booking.minDuration">
|
||||
<el-input-number v-model="ruleForm.serviceAssetConfig.serviceAssetArrivalConfig.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.serviceAssetArrivalConfig.booking.cancelWindow">
|
||||
<el-input-number v-model="ruleForm.serviceAssetConfig.serviceAssetArrivalConfig.booking.cancelWindow" :min="0" class="w100" />
|
||||
<span class="unit-text">分钟</span>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 时间段配置 -->
|
||||
<el-form-item label="服务时间" prop="serviceAssetConfig.serviceAssetArrivalConfig.schedule.timeSlots">
|
||||
<div class="config-list-container">
|
||||
<div v-for="(slot, index) in ruleForm.serviceAssetConfig.serviceAssetArrivalConfig.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-button type="primary" :icon="Plus" size="small" @click="addException">添加休息时间</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 例外日期配置 -->
|
||||
<el-form-item label="休息时间">
|
||||
<div class="config-list-container">
|
||||
<div v-for="(exc, index) in ruleForm.serviceAssetConfig.serviceAssetArrivalConfig.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>
|
||||
</template>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
@@ -330,7 +442,7 @@ 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 { getAsset, createAsset, updateAsset, uploadAssetImage } 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';
|
||||
@@ -355,6 +467,12 @@ interface CategoryAttr {
|
||||
name: string;
|
||||
type: string;
|
||||
options?: { label: string; value: string }[];
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
interface KeyValuePair {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface RuleForm {
|
||||
@@ -372,23 +490,33 @@ interface RuleForm {
|
||||
};
|
||||
};
|
||||
virtualAssetConfig: {
|
||||
method: string;
|
||||
requestURL: string;
|
||||
authType: string;
|
||||
authConfig: string;
|
||||
virtualType: string;
|
||||
collectionPrice: number;
|
||||
currency: string;
|
||||
apiConfig: {
|
||||
method: string;
|
||||
requestURL: string;
|
||||
headers: KeyValuePair[];
|
||||
params: KeyValuePair[];
|
||||
authType: string;
|
||||
authConfig: string;
|
||||
};
|
||||
};
|
||||
serviceAssetConfig: {
|
||||
schedule: {
|
||||
timeSlots: TimeSlot[];
|
||||
exceptions: Exception[];
|
||||
};
|
||||
booking: {
|
||||
minAdvance: number;
|
||||
minDuration: number;
|
||||
cancelWindow: number;
|
||||
};
|
||||
capacity: {
|
||||
maxUsers: number;
|
||||
serviceAssetType: string;
|
||||
serviceAssetArrivalConfig: {
|
||||
schedule: {
|
||||
timeSlots: TimeSlot[];
|
||||
exceptions: Exception[];
|
||||
};
|
||||
booking: {
|
||||
minAdvance: number;
|
||||
minDuration: number;
|
||||
cancelWindow: number;
|
||||
};
|
||||
capacity: {
|
||||
maxUsers: number;
|
||||
};
|
||||
};
|
||||
};
|
||||
metadata: Record<string, any>;
|
||||
@@ -406,7 +534,7 @@ 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);
|
||||
const isTimeSlotLimitReached = computed(() => ruleForm.serviceAssetConfig.serviceAssetArrivalConfig.schedule.timeSlots.length >= MAX_TIME_SLOTS);
|
||||
|
||||
// 获取属性的key
|
||||
const getAttrKey = (attr: CategoryAttr): string => {
|
||||
@@ -464,23 +592,33 @@ const getInitialForm = (): RuleForm => ({
|
||||
},
|
||||
},
|
||||
virtualAssetConfig: {
|
||||
method: 'GET',
|
||||
requestURL: '',
|
||||
authType: 'none',
|
||||
authConfig: '',
|
||||
virtualType: 'recharge',
|
||||
collectionPrice: 0,
|
||||
currency: 'CNY',
|
||||
apiConfig: {
|
||||
method: 'GET',
|
||||
requestURL: '',
|
||||
headers: [],
|
||||
params: [],
|
||||
authType: 'none',
|
||||
authConfig: '',
|
||||
},
|
||||
},
|
||||
serviceAssetConfig: {
|
||||
schedule: {
|
||||
timeSlots: createDefaultTimeSlots(),
|
||||
exceptions: [],
|
||||
},
|
||||
booking: {
|
||||
minAdvance: 60,
|
||||
minDuration: 30,
|
||||
cancelWindow: 30,
|
||||
},
|
||||
capacity: {
|
||||
maxUsers: 0,
|
||||
serviceAssetType: 'arrival',
|
||||
serviceAssetArrivalConfig: {
|
||||
schedule: {
|
||||
timeSlots: createDefaultTimeSlots(),
|
||||
exceptions: [],
|
||||
},
|
||||
booking: {
|
||||
minAdvance: 60,
|
||||
minDuration: 30,
|
||||
cancelWindow: 30,
|
||||
},
|
||||
capacity: {
|
||||
maxUsers: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
metadata: {},
|
||||
@@ -528,11 +666,11 @@ const rules: FormRules = {
|
||||
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' }],
|
||||
mainImage: [{ required: false, message: '请上传主图', trigger: 'change' }],
|
||||
'serviceAssetConfig.serviceAssetArrivalConfig.booking.minAdvance': [{ required: true, message: '请输入最小提前时间', trigger: 'blur' }],
|
||||
'serviceAssetConfig.serviceAssetArrivalConfig.booking.minDuration': [{ required: true, message: '请输入最小时长', trigger: 'blur' }],
|
||||
'serviceAssetConfig.serviceAssetArrivalConfig.booking.cancelWindow': [{ required: true, message: '请输入取消提前时间', trigger: 'blur' }],
|
||||
'serviceAssetConfig.serviceAssetArrivalConfig.schedule.timeSlots': [{ validator: validateTimeSlots, trigger: 'change' }],
|
||||
};
|
||||
|
||||
// 主图上传处理
|
||||
@@ -605,35 +743,35 @@ const removeAttrImage = (attr: CategoryAttr) => {
|
||||
|
||||
// 时间段操作
|
||||
const addTimeSlot = () => {
|
||||
if (!ruleForm.serviceAssetConfig.schedule) {
|
||||
ruleForm.serviceAssetConfig.schedule = { timeSlots: [], exceptions: [] };
|
||||
if (!ruleForm.serviceAssetConfig.serviceAssetArrivalConfig.schedule) {
|
||||
ruleForm.serviceAssetConfig.serviceAssetArrivalConfig.schedule = { timeSlots: [], exceptions: [] };
|
||||
}
|
||||
if (!ruleForm.serviceAssetConfig.schedule.timeSlots) {
|
||||
ruleForm.serviceAssetConfig.schedule.timeSlots = [];
|
||||
if (!ruleForm.serviceAssetConfig.serviceAssetArrivalConfig.schedule.timeSlots) {
|
||||
ruleForm.serviceAssetConfig.serviceAssetArrivalConfig.schedule.timeSlots = [];
|
||||
}
|
||||
ruleForm.serviceAssetConfig.schedule.timeSlots.push({
|
||||
ruleForm.serviceAssetConfig.serviceAssetArrivalConfig.schedule.timeSlots.push({
|
||||
dayOfWeek: '1',
|
||||
startTime: '09:00',
|
||||
endTime: '18:00',
|
||||
capacity: 100,
|
||||
});
|
||||
formRef.value?.validateField('serviceAssetConfig.schedule.timeSlots');
|
||||
formRef.value?.validateField('serviceAssetConfig.serviceAssetArrivalConfig.schedule.timeSlots');
|
||||
};
|
||||
|
||||
const removeTimeSlot = (index: number) => {
|
||||
ruleForm.serviceAssetConfig.schedule.timeSlots.splice(index, 1);
|
||||
formRef.value?.validateField('serviceAssetConfig.schedule.timeSlots');
|
||||
ruleForm.serviceAssetConfig.serviceAssetArrivalConfig.schedule.timeSlots.splice(index, 1);
|
||||
formRef.value?.validateField('serviceAssetConfig.serviceAssetArrivalConfig.schedule.timeSlots');
|
||||
};
|
||||
|
||||
// 例外日期操作
|
||||
const addException = () => {
|
||||
if (!ruleForm.serviceAssetConfig.schedule) {
|
||||
ruleForm.serviceAssetConfig.schedule = { timeSlots: [], exceptions: [] };
|
||||
if (!ruleForm.serviceAssetConfig.serviceAssetArrivalConfig.schedule) {
|
||||
ruleForm.serviceAssetConfig.serviceAssetArrivalConfig.schedule = { timeSlots: [], exceptions: [] };
|
||||
}
|
||||
if (!ruleForm.serviceAssetConfig.schedule.exceptions) {
|
||||
ruleForm.serviceAssetConfig.schedule.exceptions = [];
|
||||
if (!ruleForm.serviceAssetConfig.serviceAssetArrivalConfig.schedule.exceptions) {
|
||||
ruleForm.serviceAssetConfig.serviceAssetArrivalConfig.schedule.exceptions = [];
|
||||
}
|
||||
ruleForm.serviceAssetConfig.schedule.exceptions.push({
|
||||
ruleForm.serviceAssetConfig.serviceAssetArrivalConfig.schedule.exceptions.push({
|
||||
exceptionType: 'date',
|
||||
date: '',
|
||||
status: 1,
|
||||
@@ -643,7 +781,7 @@ const addException = () => {
|
||||
};
|
||||
|
||||
const removeException = (index: number) => {
|
||||
ruleForm.serviceAssetConfig.schedule.exceptions.splice(index, 1);
|
||||
ruleForm.serviceAssetConfig.serviceAssetArrivalConfig.schedule.exceptions.splice(index, 1);
|
||||
};
|
||||
|
||||
// 例外类型切换时清空对应字段
|
||||
@@ -655,6 +793,15 @@ const onExceptionTypeChange = (exc: Exception) => {
|
||||
}
|
||||
};
|
||||
|
||||
// 键值对操作
|
||||
const addKeyValuePair = (list: KeyValuePair[]) => {
|
||||
list.push({ key: '', value: '' });
|
||||
};
|
||||
|
||||
const removeKeyValuePair = (list: KeyValuePair[], index: number) => {
|
||||
list.splice(index, 1);
|
||||
};
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
const initial = getInitialForm();
|
||||
@@ -744,20 +891,57 @@ const openDialog = (row?: any, edit?: boolean) => {
|
||||
Object.assign(ruleForm.physicalAssetConfig, data.physicalAssetConfig);
|
||||
}
|
||||
if (data.type === 'virtual' && data.virtualAssetConfig) {
|
||||
Object.assign(ruleForm.virtualAssetConfig, data.virtualAssetConfig);
|
||||
// 先处理 headers 和 params 为数组格式,再赋值
|
||||
const config = { ...data.virtualAssetConfig };
|
||||
|
||||
// 确保 apiConfig 存在
|
||||
if (!config.apiConfig) {
|
||||
config.apiConfig = {
|
||||
method: 'GET',
|
||||
requestURL: '',
|
||||
headers: [],
|
||||
params: [],
|
||||
authType: 'none',
|
||||
authConfig: '',
|
||||
};
|
||||
} else {
|
||||
// 处理 headers
|
||||
if (config.apiConfig.headers && typeof config.apiConfig.headers === 'object' && !Array.isArray(config.apiConfig.headers)) {
|
||||
config.apiConfig.headers = Object.keys(config.apiConfig.headers).map((key) => ({ key, value: config.apiConfig.headers[key] }));
|
||||
} else if (!Array.isArray(config.apiConfig.headers)) {
|
||||
config.apiConfig.headers = [];
|
||||
}
|
||||
|
||||
// 处理 params
|
||||
if (config.apiConfig.params && typeof config.apiConfig.params === 'object' && !Array.isArray(config.apiConfig.params)) {
|
||||
config.apiConfig.params = Object.keys(config.apiConfig.params).map((key) => ({ key, value: config.apiConfig.params[key] }));
|
||||
} else if (!Array.isArray(config.apiConfig.params)) {
|
||||
config.apiConfig.params = [];
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(ruleForm.virtualAssetConfig, config);
|
||||
}
|
||||
if (data.type === 'service' && data.serviceAssetConfig) {
|
||||
Object.assign(ruleForm.serviceAssetConfig, data.serviceAssetConfig);
|
||||
// 确保 serviceAssetArrivalConfig 对象存在
|
||||
if (!ruleForm.serviceAssetConfig.serviceAssetArrivalConfig) {
|
||||
ruleForm.serviceAssetConfig.serviceAssetArrivalConfig = {
|
||||
schedule: { timeSlots: [], exceptions: [] },
|
||||
booking: { minAdvance: 60, minDuration: 30, cancelWindow: 30 },
|
||||
capacity: { maxUsers: 0 },
|
||||
};
|
||||
}
|
||||
// 确保 schedule 对象存在
|
||||
if (!ruleForm.serviceAssetConfig.schedule) {
|
||||
ruleForm.serviceAssetConfig.schedule = { timeSlots: [], exceptions: [] };
|
||||
if (!ruleForm.serviceAssetConfig.serviceAssetArrivalConfig.schedule) {
|
||||
ruleForm.serviceAssetConfig.serviceAssetArrivalConfig.schedule = { timeSlots: [], exceptions: [] };
|
||||
}
|
||||
// 确保数组存在,防止后端返回 null 或 undefined 导致 push 报错
|
||||
if (!ruleForm.serviceAssetConfig.schedule.exceptions) {
|
||||
ruleForm.serviceAssetConfig.schedule.exceptions = [];
|
||||
if (!ruleForm.serviceAssetConfig.serviceAssetArrivalConfig.schedule.exceptions) {
|
||||
ruleForm.serviceAssetConfig.serviceAssetArrivalConfig.schedule.exceptions = [];
|
||||
} else {
|
||||
// 补充缺失的 exceptionType
|
||||
ruleForm.serviceAssetConfig.schedule.exceptions.forEach((exc) => {
|
||||
ruleForm.serviceAssetConfig.serviceAssetArrivalConfig.schedule.exceptions.forEach((exc: Exception) => {
|
||||
if (!exc.exceptionType) {
|
||||
if (exc.date) {
|
||||
exc.exceptionType = 'date';
|
||||
@@ -770,8 +954,8 @@ const openDialog = (row?: any, edit?: boolean) => {
|
||||
}
|
||||
});
|
||||
}
|
||||
if (!ruleForm.serviceAssetConfig.schedule.timeSlots) {
|
||||
ruleForm.serviceAssetConfig.schedule.timeSlots = [];
|
||||
if (!ruleForm.serviceAssetConfig.serviceAssetArrivalConfig.schedule.timeSlots) {
|
||||
ruleForm.serviceAssetConfig.serviceAssetArrivalConfig.schedule.timeSlots = [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -827,95 +1011,144 @@ const onCancel = () => {
|
||||
closeDialog();
|
||||
};
|
||||
|
||||
// 构建FormData
|
||||
const buildFormData = (): FormData => {
|
||||
const formData = new FormData();
|
||||
// 上传图片并返回URL
|
||||
const uploadImage = async (file: File): Promise<string> => {
|
||||
const res: any = await uploadAssetImage(file);
|
||||
return res.data?.url || res.data || '';
|
||||
};
|
||||
|
||||
// 基础字段
|
||||
// 构建请求体
|
||||
const buildRequestBody = async (): Promise<any> => {
|
||||
const body: any = {
|
||||
name: ruleForm.name,
|
||||
type: ruleForm.type,
|
||||
categoryId: ruleForm.categoryId,
|
||||
description: ruleForm.description || '',
|
||||
};
|
||||
|
||||
// 编辑模式添加 id
|
||||
if (isEdit.value && ruleForm.id) {
|
||||
formData.append('id', ruleForm.id);
|
||||
body.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);
|
||||
body.onlineTime = ruleForm.onlineTime;
|
||||
}
|
||||
if (ruleForm.offlineTime) {
|
||||
formData.append('offlineTime', ruleForm.offlineTime);
|
||||
body.offlineTime = ruleForm.offlineTime;
|
||||
}
|
||||
|
||||
// 主图
|
||||
// 主图上传
|
||||
if (mainImageFile.value) {
|
||||
formData.append('imageUrl', mainImageFile.value);
|
||||
body.imageUrl = await uploadImage(mainImageFile.value);
|
||||
} else if (ruleForm.mainImage && !ruleForm.mainImage.startsWith('blob:')) {
|
||||
body.imageUrl = ruleForm.mainImage;
|
||||
}
|
||||
|
||||
// 图片列表
|
||||
imageFileList.value.forEach((file) => {
|
||||
// 图片列表上传
|
||||
const imageUrls: string[] = [];
|
||||
for (const file of imageFileList.value) {
|
||||
if (file.raw) {
|
||||
formData.append('images', file.raw);
|
||||
const url = await uploadImage(file.raw);
|
||||
if (url) imageUrls.push(url);
|
||||
} else if (file.url && !file.url.startsWith('blob:')) {
|
||||
// 已有图片保留原URL
|
||||
imageUrls.push(file.url);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (imageUrls.length > 0) {
|
||||
body.images = imageUrls;
|
||||
}
|
||||
|
||||
// 根据类型添加配置
|
||||
if (ruleForm.type === 'physical') {
|
||||
formData.append('physicalAssetConfig', JSON.stringify(ruleForm.physicalAssetConfig));
|
||||
body.physicalAssetConfig = ruleForm.physicalAssetConfig;
|
||||
} else if (ruleForm.type === 'virtual') {
|
||||
formData.append('virtualAssetConfig', JSON.stringify(ruleForm.virtualAssetConfig));
|
||||
// 深拷贝 virtualAssetConfig 以避免修改原对象
|
||||
const virtualConfig = JSON.parse(JSON.stringify(ruleForm.virtualAssetConfig));
|
||||
|
||||
// 将数组转换为对象
|
||||
if (virtualConfig.apiConfig) {
|
||||
if (Array.isArray(virtualConfig.apiConfig.headers)) {
|
||||
const headersObj: Record<string, string> = {};
|
||||
virtualConfig.apiConfig.headers.forEach((item: KeyValuePair) => {
|
||||
if (item.key) headersObj[item.key] = item.value;
|
||||
});
|
||||
virtualConfig.apiConfig.headers = headersObj;
|
||||
}
|
||||
|
||||
if (Array.isArray(virtualConfig.apiConfig.params)) {
|
||||
const paramsObj: Record<string, string> = {};
|
||||
virtualConfig.apiConfig.params.forEach((item: KeyValuePair) => {
|
||||
if (item.key) paramsObj[item.key] = item.value;
|
||||
});
|
||||
virtualConfig.apiConfig.params = paramsObj;
|
||||
}
|
||||
}
|
||||
|
||||
body.virtualAssetConfig = virtualConfig;
|
||||
} else if (ruleForm.type === 'service') {
|
||||
formData.append('serviceAssetConfig', JSON.stringify(ruleForm.serviceAssetConfig));
|
||||
body.serviceAssetConfig = ruleForm.serviceAssetConfig;
|
||||
}
|
||||
|
||||
// 属性图片
|
||||
Object.keys(attrImageFiles.value).forEach((key) => {
|
||||
formData.append(key, attrImageFiles.value[key]);
|
||||
});
|
||||
|
||||
// 元数据(分类属性值)
|
||||
if (categoryAttrs.value.length > 0) {
|
||||
const metadataArray = categoryAttrs.value.map((attr) => {
|
||||
const metadataArray: any[] = [];
|
||||
for (const attr of categoryAttrs.value) {
|
||||
const key = getAttrKey(attr);
|
||||
let value = ruleForm.metadata[key];
|
||||
|
||||
// 过滤掉 blob url,避免传给后端
|
||||
if (typeof value === 'string' && value.startsWith('blob:')) {
|
||||
// 如果是图片类型且有文件需要上传
|
||||
if (attr.type === 'image' && attrImageFiles.value[key]) {
|
||||
value = await uploadImage(attrImageFiles.value[key]);
|
||||
} else if (typeof value === 'string' && value.startsWith('blob:')) {
|
||||
value = '';
|
||||
}
|
||||
|
||||
return {
|
||||
const metaItem: any = {
|
||||
name: attr.name,
|
||||
type: attr.type,
|
||||
value: value,
|
||||
options: attr.options || [],
|
||||
};
|
||||
});
|
||||
formData.append('metadata', JSON.stringify(metadataArray));
|
||||
|
||||
// 只有单选和多选类型才传递 options,且只传递选中的值对应的选项
|
||||
if ((attr.type === 'select' || attr.type === 'multi_select') && attr.options && value) {
|
||||
const selectedValues = Array.isArray(value) ? value : [value];
|
||||
metaItem.options = attr.options.filter((opt: { label: string; value: string }) =>
|
||||
selectedValues.includes(opt.value)
|
||||
);
|
||||
}
|
||||
|
||||
metadataArray.push(metaItem);
|
||||
}
|
||||
body.metadata = metadataArray;
|
||||
}
|
||||
|
||||
return formData;
|
||||
return body;
|
||||
};
|
||||
|
||||
// 提交
|
||||
const onSubmit = () => {
|
||||
const onSubmit = async () => {
|
||||
const form = formRef.value;
|
||||
if (!form) return;
|
||||
form.validate((valid: boolean) => {
|
||||
|
||||
form.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
submitLoading.value = true;
|
||||
const formData = buildFormData();
|
||||
try {
|
||||
const requestBody = await buildRequestBody();
|
||||
const request = isEdit.value ? updateAsset(requestBody) : createAsset(requestBody);
|
||||
|
||||
const request = isEdit.value ? updateAsset(formData) : createAsset(formData);
|
||||
|
||||
request
|
||||
.then(() => {
|
||||
ElMessage.success(isEdit.value ? '修改成功' : '添加成功');
|
||||
closeDialog();
|
||||
emit('getAssetList');
|
||||
})
|
||||
.finally(() => {
|
||||
submitLoading.value = false;
|
||||
});
|
||||
await request;
|
||||
ElMessage.success(isEdit.value ? '修改成功' : '添加成功');
|
||||
closeDialog();
|
||||
emit('getAssetList');
|
||||
} catch (error) {
|
||||
console.error('提交失败:', error);
|
||||
} finally {
|
||||
submitLoading.value = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
<div class="table-head">
|
||||
<span class="col col-name">属性名称</span>
|
||||
<span class="col col-type">属性类型</span>
|
||||
<span class="col col-required">必填</span>
|
||||
<span class="col col-dict">字典</span>
|
||||
<span class="col col-dict-value">字典值</span>
|
||||
<span class="col col-ops">操作</span>
|
||||
@@ -49,6 +50,9 @@
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="col col-required">
|
||||
<el-switch v-model="attr.required" />
|
||||
</div>
|
||||
<div class="col col-dict">
|
||||
<el-select
|
||||
v-model="attr.name"
|
||||
@@ -159,7 +163,6 @@ interface RuleForm {
|
||||
parentId: string;
|
||||
name: string;
|
||||
sort: number;
|
||||
status: string;
|
||||
attrs: CustomAttr[];
|
||||
}
|
||||
|
||||
@@ -181,7 +184,6 @@ const ruleForm = reactive<RuleForm>({
|
||||
parentId: '',
|
||||
name: '',
|
||||
sort: 0,
|
||||
status: 'enabled',
|
||||
attrs: [],
|
||||
});
|
||||
|
||||
@@ -252,6 +254,7 @@ const addAttr = () => {
|
||||
ruleForm.attrs.push({
|
||||
name: '',
|
||||
type: 'text',
|
||||
required: true,
|
||||
options: [],
|
||||
});
|
||||
};
|
||||
@@ -286,7 +289,6 @@ const resetForm = () => {
|
||||
ruleForm.parentId = '';
|
||||
ruleForm.name = '';
|
||||
ruleForm.sort = 0;
|
||||
ruleForm.status = 'enabled';
|
||||
ruleForm.attrs = [];
|
||||
};
|
||||
|
||||
@@ -309,7 +311,6 @@ const openDialog = (row?: CategoryRow | string, edit?: boolean) => {
|
||||
ruleForm.parentId = data.parentId || '';
|
||||
ruleForm.name = data.name || '';
|
||||
ruleForm.sort = data.sort || 0;
|
||||
ruleForm.status = data.status || 'enabled';
|
||||
// 处理 attrs 中的 options 字段
|
||||
ruleForm.attrs = (data.attrs || []).map((attr: any) => {
|
||||
let options = attr.options;
|
||||
@@ -453,7 +454,7 @@ defineExpose({
|
||||
.table-head,
|
||||
.table-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1.4fr 1fr 1fr 1.4fr 60px;
|
||||
grid-template-columns: 1.4fr 1fr 80px 1fr 1.4fr 60px;
|
||||
align-items: center;
|
||||
column-gap: 12px;
|
||||
padding: 12px 16px;
|
||||
|
||||
Reference in New Issue
Block a user