2025-12-20 17:57:24 +08:00
|
|
|
|
<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>
|
2025-12-22 17:57:15 +08:00
|
|
|
|
<el-col :span="8">
|
2025-12-20 17:57:24 +08:00
|
|
|
|
<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"
|
2025-12-22 17:57:15 +08:00
|
|
|
|
:disabled="isEdit"
|
2025-12-20 17:57:24 +08:00
|
|
|
|
@change="onCategoryChange"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</el-col>
|
2025-12-22 17:57:15 +08:00
|
|
|
|
|
|
|
|
|
|
</el-row>
|
|
|
|
|
|
<el-row :gutter="24">
|
2025-12-20 17:57:24 +08:00
|
|
|
|
<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"
|
2025-12-23 13:10:26 +08:00
|
|
|
|
@change="onOnlineTimeChange"
|
2025-12-20 17:57:24 +08:00
|
|
|
|
/>
|
|
|
|
|
|
</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"
|
2025-12-23 13:10:26 +08:00
|
|
|
|
:disabled-date="disabledOfflineDate"
|
2025-12-20 17:57:24 +08:00
|
|
|
|
/>
|
|
|
|
|
|
</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">
|
2025-12-23 13:42:52 +08:00
|
|
|
|
<el-form-item
|
|
|
|
|
|
:label="getAttrLabel(attr)"
|
|
|
|
|
|
:prop="'metadata.' + getAttrKey(attr)"
|
2025-12-24 17:59:50 +08:00
|
|
|
|
:rules="[{ required: !!attr.required, message: getAttrLabel(attr) + '不能为空', trigger: ['blur', 'change'] }]"
|
2025-12-23 13:42:52 +08:00
|
|
|
|
>
|
2025-12-20 17:57:24 +08:00
|
|
|
|
<!-- 单选类型 -->
|
|
|
|
|
|
<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)]"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<!-- 图片类型 -->
|
2025-12-23 13:42:52 +08:00
|
|
|
|
<div v-else-if="attr.type === 'image'" class="w100">
|
|
|
|
|
|
<el-upload
|
|
|
|
|
|
class="attr-image-uploader"
|
|
|
|
|
|
:show-file-list="false"
|
2025-12-25 17:55:52 +08:00
|
|
|
|
:auto-upload="true"
|
2025-12-23 13:42:52 +08:00
|
|
|
|
accept="image/*"
|
2025-12-25 17:55:52 +08:00
|
|
|
|
:http-request="onAttrImageUpload(attr)"
|
2025-12-23 13:42:52 +08:00
|
|
|
|
>
|
|
|
|
|
|
<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>
|
2025-12-20 17:57:24 +08:00
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</el-col>
|
|
|
|
|
|
</el-row>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 图片上传 -->
|
|
|
|
|
|
<el-divider content-position="left">图片信息</el-divider>
|
|
|
|
|
|
<el-row :gutter="24">
|
|
|
|
|
|
<el-col :span="8">
|
2025-12-23 13:10:26 +08:00
|
|
|
|
<el-form-item label="主图" prop="mainImage">
|
2025-12-23 13:42:52 +08:00
|
|
|
|
<div class="w100">
|
|
|
|
|
|
<el-upload
|
|
|
|
|
|
class="avatar-uploader"
|
|
|
|
|
|
:show-file-list="false"
|
2025-12-25 17:55:52 +08:00
|
|
|
|
:auto-upload="true"
|
|
|
|
|
|
:http-request="handleMainImageUpload"
|
2025-12-23 13:42:52 +08:00
|
|
|
|
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>
|
2025-12-20 17:57:24 +08:00
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</el-col>
|
|
|
|
|
|
<el-col :span="16">
|
|
|
|
|
|
<el-form-item label="图片列表">
|
|
|
|
|
|
<el-upload
|
|
|
|
|
|
v-model:file-list="imageFileList"
|
|
|
|
|
|
list-type="picture-card"
|
2025-12-25 17:55:52 +08:00
|
|
|
|
:auto-upload="true"
|
|
|
|
|
|
:http-request="handleImageListUpload"
|
2025-12-20 17:57:24 +08:00
|
|
|
|
:on-preview="handlePictureCardPreview"
|
|
|
|
|
|
:on-remove="handleRemove"
|
|
|
|
|
|
accept="image/*"
|
|
|
|
|
|
multiple
|
|
|
|
|
|
>
|
|
|
|
|
|
<el-icon><Plus /></el-icon>
|
|
|
|
|
|
</el-upload>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</el-col>
|
|
|
|
|
|
</el-row>
|
|
|
|
|
|
|
2025-12-23 13:10:26 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 资产描述 -->
|
|
|
|
|
|
<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>
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-12-20 17:57:24 +08:00
|
|
|
|
<!-- 实物资产配置 -->
|
|
|
|
|
|
<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" />
|
2025-12-22 17:57:15 +08:00
|
|
|
|
<el-option label="同城配送" value="city_delivery" disabled />
|
2025-12-20 17:57:24 +08:00
|
|
|
|
</el-select>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</el-col>
|
|
|
|
|
|
</el-row>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
2025-12-24 17:59:50 +08:00
|
|
|
|
<!-- 虚拟资产配置 -->
|
|
|
|
|
|
<template v-if="ruleForm.type === 'virtual'">
|
|
|
|
|
|
<el-divider content-position="left">虚拟资产配置</el-divider>
|
2025-12-20 17:57:24 +08:00
|
|
|
|
|
2025-12-24 17:59:50 +08:00
|
|
|
|
<!-- 虚拟类型选择 -->
|
2025-12-20 17:57:24 +08:00
|
|
|
|
<el-row :gutter="24">
|
2025-12-24 17:59:50 +08:00
|
|
|
|
<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>
|
2025-12-20 17:57:24 +08:00
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</el-col>
|
2025-12-24 17:59:50 +08:00
|
|
|
|
<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>
|
2025-12-20 17:57:24 +08:00
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</el-col>
|
2025-12-24 17:59:50 +08:00
|
|
|
|
<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>
|
2025-12-20 17:57:24 +08:00
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</el-col>
|
|
|
|
|
|
</el-row>
|
|
|
|
|
|
|
2025-12-24 17:59:50 +08:00
|
|
|
|
<!-- 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" />
|
2025-12-22 14:08:17 +08:00
|
|
|
|
</el-select>
|
2025-12-24 17:59:50 +08:00
|
|
|
|
</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" />
|
2025-12-20 17:57:24 +08:00
|
|
|
|
</el-select>
|
2025-12-24 17:59:50 +08:00
|
|
|
|
</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>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 服务资产配置 -->
|
|
|
|
|
|
<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 />
|
2025-12-20 17:57:24 +08:00
|
|
|
|
</el-select>
|
2025-12-24 17:59:50 +08:00
|
|
|
|
</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>
|
2025-12-20 17:57:24 +08:00
|
|
|
|
</div>
|
2025-12-24 17:59:50 +08:00
|
|
|
|
</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>
|
2025-12-20 17:57:24 +08:00
|
|
|
|
</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">
|
2025-12-22 17:57:15 +08:00
|
|
|
|
import { ref, reactive, watch, computed } from 'vue';
|
2025-12-20 17:57:24 +08:00
|
|
|
|
import { ElMessage } from 'element-plus';
|
2025-12-22 17:57:15 +08:00
|
|
|
|
import type { FormInstance, FormRules } from 'element-plus';
|
2025-12-20 17:57:24 +08:00
|
|
|
|
import { Plus, Delete } from '@element-plus/icons-vue';
|
2025-12-24 17:59:50 +08:00
|
|
|
|
import { getAsset, createAsset, updateAsset, uploadAssetImage } from '/@/api/assets/asset';
|
2025-12-20 17:57:24 +08:00
|
|
|
|
import { getCategoryTree, getCategory } from '/@/api/assets/category';
|
|
|
|
|
|
import Editor from '/@/components/editor/index.vue';
|
2025-12-25 17:55:52 +08:00
|
|
|
|
import type { UploadFile, UploadUserFile, UploadRequestOptions } from 'element-plus';
|
2025-12-20 17:57:24 +08:00
|
|
|
|
|
|
|
|
|
|
// 类型定义
|
|
|
|
|
|
interface TimeSlot {
|
|
|
|
|
|
dayOfWeek: string;
|
|
|
|
|
|
startTime: string;
|
|
|
|
|
|
endTime: string;
|
|
|
|
|
|
capacity: number;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface Exception {
|
2025-12-22 14:08:17 +08:00
|
|
|
|
exceptionType: 'date' | 'dayOfWeek';
|
2025-12-20 17:57:24 +08:00
|
|
|
|
date: string;
|
|
|
|
|
|
status: number;
|
|
|
|
|
|
reason: string;
|
|
|
|
|
|
dayOfWeek: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface CategoryAttr {
|
|
|
|
|
|
name: string;
|
|
|
|
|
|
type: string;
|
|
|
|
|
|
options?: { label: string; value: string }[];
|
2025-12-24 17:59:50 +08:00
|
|
|
|
required?: boolean;
|
2026-01-12 15:59:05 +08:00
|
|
|
|
dictType?: string;
|
2025-12-24 17:59:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface KeyValuePair {
|
|
|
|
|
|
key: string;
|
|
|
|
|
|
value: string;
|
2025-12-20 17:57:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface RuleForm {
|
|
|
|
|
|
id: string;
|
|
|
|
|
|
name: string;
|
|
|
|
|
|
type: string;
|
|
|
|
|
|
categoryId: string;
|
|
|
|
|
|
description: string;
|
|
|
|
|
|
onlineTime: string;
|
|
|
|
|
|
offlineTime: string;
|
|
|
|
|
|
physicalAssetConfig: {
|
|
|
|
|
|
shipping: {
|
|
|
|
|
|
deliveryMethod: string;
|
|
|
|
|
|
deliveryTime: number;
|
|
|
|
|
|
};
|
|
|
|
|
|
};
|
|
|
|
|
|
virtualAssetConfig: {
|
2025-12-24 17:59:50 +08:00
|
|
|
|
virtualType: string;
|
|
|
|
|
|
collectionPrice: number;
|
|
|
|
|
|
currency: string;
|
|
|
|
|
|
apiConfig: {
|
|
|
|
|
|
method: string;
|
|
|
|
|
|
requestURL: string;
|
|
|
|
|
|
headers: KeyValuePair[];
|
|
|
|
|
|
params: KeyValuePair[];
|
|
|
|
|
|
authType: string;
|
|
|
|
|
|
authConfig: string;
|
|
|
|
|
|
};
|
2025-12-20 17:57:24 +08:00
|
|
|
|
};
|
|
|
|
|
|
serviceAssetConfig: {
|
2025-12-24 17:59:50 +08:00
|
|
|
|
serviceAssetType: string;
|
|
|
|
|
|
serviceAssetArrivalConfig: {
|
|
|
|
|
|
schedule: {
|
|
|
|
|
|
timeSlots: TimeSlot[];
|
|
|
|
|
|
exceptions: Exception[];
|
|
|
|
|
|
};
|
|
|
|
|
|
booking: {
|
|
|
|
|
|
minAdvance: number;
|
|
|
|
|
|
minDuration: number;
|
|
|
|
|
|
cancelWindow: number;
|
|
|
|
|
|
};
|
|
|
|
|
|
capacity: {
|
|
|
|
|
|
maxUsers: number;
|
|
|
|
|
|
};
|
2025-12-20 17:57:24 +08:00
|
|
|
|
};
|
|
|
|
|
|
};
|
|
|
|
|
|
metadata: Record<string, any>;
|
2025-12-23 13:10:26 +08:00
|
|
|
|
mainImage?: string;
|
2025-12-20 17:57:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const emit = defineEmits(['getAssetList']);
|
|
|
|
|
|
|
2025-12-22 17:57:15 +08:00
|
|
|
|
const formRef = ref<FormInstance>();
|
|
|
|
|
|
const editAssetRef = ref();
|
|
|
|
|
|
const MAX_TIME_SLOTS = 7;
|
2025-12-20 17:57:24 +08:00
|
|
|
|
const isShowDialog = ref(false);
|
|
|
|
|
|
const isEdit = ref(false);
|
|
|
|
|
|
const submitLoading = ref(false);
|
|
|
|
|
|
const formLoading = ref(false);
|
|
|
|
|
|
const categoryOptions = ref<any[]>([]);
|
|
|
|
|
|
const categoryAttrs = ref<CategoryAttr[]>([]);
|
2025-12-24 17:59:50 +08:00
|
|
|
|
const isTimeSlotLimitReached = computed(() => ruleForm.serviceAssetConfig.serviceAssetArrivalConfig.schedule.timeSlots.length >= MAX_TIME_SLOTS);
|
2025-12-20 17:57:24 +08:00
|
|
|
|
|
|
|
|
|
|
// 获取属性的key
|
|
|
|
|
|
const getAttrKey = (attr: CategoryAttr): string => {
|
2025-12-23 16:55:31 +08:00
|
|
|
|
return attr.name || `attr_${categoryAttrs.value.indexOf(attr)}`;
|
2025-12-20 17:57:24 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 获取属性的显示名称
|
|
|
|
|
|
const getAttrLabel = (attr: CategoryAttr): string => {
|
2025-12-23 16:55:31 +08:00
|
|
|
|
return attr.name || '属性';
|
2025-12-20 17:57:24 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 图片相关
|
|
|
|
|
|
const mainImagePreview = ref('');
|
|
|
|
|
|
const imageFileList = ref<UploadUserFile[]>([]);
|
|
|
|
|
|
const dialogVisible = ref(false);
|
|
|
|
|
|
const dialogImageUrl = ref('');
|
2025-12-22 17:57:15 +08:00
|
|
|
|
// 图片拼接
|
2025-12-26 10:08:04 +08:00
|
|
|
|
const fileAddressPrefix = ref('');
|
2025-12-22 17:57:15 +08:00
|
|
|
|
|
|
|
|
|
|
const formatImageUrl = (url?: string) => {
|
|
|
|
|
|
if (!url) return '';
|
|
|
|
|
|
if (/^https?:\/\//i.test(url)) return url;
|
2025-12-23 13:42:52 +08:00
|
|
|
|
if (/^blob:/i.test(url)) return url; // 支持本地预览地址
|
2025-12-26 10:08:04 +08:00
|
|
|
|
return `${fileAddressPrefix.value || ''}${url}`;
|
2025-12-22 17:57:15 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
};
|
2025-12-20 17:57:24 +08:00
|
|
|
|
|
|
|
|
|
|
// 初始表单数据
|
|
|
|
|
|
const getInitialForm = (): RuleForm => ({
|
|
|
|
|
|
id: '',
|
|
|
|
|
|
name: '',
|
|
|
|
|
|
type: 'physical',
|
|
|
|
|
|
categoryId: '',
|
|
|
|
|
|
description: '',
|
|
|
|
|
|
onlineTime: '',
|
|
|
|
|
|
offlineTime: '',
|
|
|
|
|
|
physicalAssetConfig: {
|
|
|
|
|
|
shipping: {
|
|
|
|
|
|
deliveryMethod: 'express',
|
|
|
|
|
|
deliveryTime: 24,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
virtualAssetConfig: {
|
2025-12-24 17:59:50 +08:00
|
|
|
|
virtualType: 'recharge',
|
|
|
|
|
|
collectionPrice: 0,
|
|
|
|
|
|
currency: 'CNY',
|
|
|
|
|
|
apiConfig: {
|
|
|
|
|
|
method: 'GET',
|
|
|
|
|
|
requestURL: '',
|
|
|
|
|
|
headers: [],
|
|
|
|
|
|
params: [],
|
|
|
|
|
|
authType: 'none',
|
|
|
|
|
|
authConfig: '',
|
|
|
|
|
|
},
|
2025-12-20 17:57:24 +08:00
|
|
|
|
},
|
|
|
|
|
|
serviceAssetConfig: {
|
2025-12-24 17:59:50 +08:00
|
|
|
|
serviceAssetType: 'arrival',
|
|
|
|
|
|
serviceAssetArrivalConfig: {
|
|
|
|
|
|
schedule: {
|
|
|
|
|
|
timeSlots: createDefaultTimeSlots(),
|
|
|
|
|
|
exceptions: [],
|
|
|
|
|
|
},
|
|
|
|
|
|
booking: {
|
|
|
|
|
|
minAdvance: 60,
|
|
|
|
|
|
minDuration: 30,
|
|
|
|
|
|
cancelWindow: 30,
|
|
|
|
|
|
},
|
|
|
|
|
|
capacity: {
|
|
|
|
|
|
maxUsers: 0,
|
|
|
|
|
|
},
|
2025-12-20 17:57:24 +08:00
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
metadata: {},
|
2025-12-23 13:10:26 +08:00
|
|
|
|
mainImage: '',
|
2025-12-20 17:57:24 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const ruleForm = reactive<RuleForm>(getInitialForm());
|
|
|
|
|
|
|
2025-12-22 17:57:15 +08:00
|
|
|
|
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();
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-12-23 13:10:26 +08:00
|
|
|
|
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');
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-12-22 17:57:15 +08:00
|
|
|
|
const rules: FormRules = {
|
2025-12-20 17:57:24 +08:00
|
|
|
|
name: [{ required: true, message: '资产名称不能为空', trigger: 'blur' }],
|
|
|
|
|
|
type: [{ required: true, message: '请选择资产类型', trigger: 'change' }],
|
|
|
|
|
|
categoryId: [{ required: true, message: '请选择资产分类', trigger: 'change' }],
|
2025-12-22 17:57:15 +08:00
|
|
|
|
offlineTime: [{ validator: validateOfflineTime, trigger: 'change' }],
|
2025-12-26 15:40:14 +08:00
|
|
|
|
mainImage: [{ required: true, message: '请上传主图', trigger: 'change' }],
|
2025-12-24 17:59:50 +08:00
|
|
|
|
'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' }],
|
2025-12-20 17:57:24 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 主图上传处理
|
2025-12-25 17:55:52 +08:00
|
|
|
|
const handleMainImageUpload = async (options: UploadRequestOptions) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const url = await uploadImage(options.file);
|
|
|
|
|
|
if (url) {
|
|
|
|
|
|
ruleForm.mainImage = url;
|
|
|
|
|
|
mainImagePreview.value = formatImageUrl(url);
|
|
|
|
|
|
formRef.value?.validateField('mainImage');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
ElMessage.error('主图上传失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 图片列表上传处理
|
|
|
|
|
|
const handleImageListUpload = async (options: UploadRequestOptions) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const url = await uploadImage(options.file);
|
|
|
|
|
|
if (url) {
|
|
|
|
|
|
const fileItem = imageFileList.value.find((f) => f.uid === options.file.uid);
|
|
|
|
|
|
if (fileItem) {
|
|
|
|
|
|
fileItem.url = formatImageUrl(url);
|
|
|
|
|
|
fileItem.status = 'success';
|
|
|
|
|
|
fileItem.response = url; // 存储原始 URL
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
ElMessage.error('上传失败');
|
|
|
|
|
|
const index = imageFileList.value.findIndex((f) => f.uid === options.file.uid);
|
|
|
|
|
|
if (index !== -1) {
|
|
|
|
|
|
imageFileList.value.splice(index, 1);
|
|
|
|
|
|
}
|
2025-12-20 17:57:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-12-25 17:55:52 +08:00
|
|
|
|
// 属性图片上传处理
|
|
|
|
|
|
const onAttrImageUpload = (attr: CategoryAttr) => {
|
|
|
|
|
|
return async (options: UploadRequestOptions) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const url = await uploadImage(options.file);
|
|
|
|
|
|
if (url) {
|
|
|
|
|
|
const key = getAttrKey(attr);
|
|
|
|
|
|
ruleForm.metadata[key] = url;
|
|
|
|
|
|
formRef.value?.validateField(`metadata.${key}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
ElMessage.error('图片上传失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-12-23 13:42:52 +08:00
|
|
|
|
// 主图预览
|
|
|
|
|
|
const previewMainImage = () => {
|
|
|
|
|
|
if (mainImagePreview.value) {
|
|
|
|
|
|
dialogImageUrl.value = mainImagePreview.value;
|
|
|
|
|
|
dialogVisible.value = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 主图删除
|
|
|
|
|
|
const removeMainImage = () => {
|
|
|
|
|
|
mainImagePreview.value = '';
|
|
|
|
|
|
ruleForm.mainImage = '';
|
|
|
|
|
|
formRef.value?.validateField('mainImage');
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-12-20 17:57:24 +08:00
|
|
|
|
// 图片列表预览
|
|
|
|
|
|
const handlePictureCardPreview = (file: UploadFile) => {
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-12-23 13:42:52 +08:00
|
|
|
|
// 移除属性图片
|
|
|
|
|
|
const removeAttrImage = (attr: CategoryAttr) => {
|
|
|
|
|
|
const key = getAttrKey(attr);
|
|
|
|
|
|
ruleForm.metadata[key] = '';
|
|
|
|
|
|
formRef.value?.validateField(`metadata.${key}`);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-12-20 17:57:24 +08:00
|
|
|
|
// 时间段操作
|
|
|
|
|
|
const addTimeSlot = () => {
|
2025-12-24 17:59:50 +08:00
|
|
|
|
if (!ruleForm.serviceAssetConfig.serviceAssetArrivalConfig.schedule) {
|
|
|
|
|
|
ruleForm.serviceAssetConfig.serviceAssetArrivalConfig.schedule = { timeSlots: [], exceptions: [] };
|
2025-12-23 13:10:26 +08:00
|
|
|
|
}
|
2025-12-24 17:59:50 +08:00
|
|
|
|
if (!ruleForm.serviceAssetConfig.serviceAssetArrivalConfig.schedule.timeSlots) {
|
|
|
|
|
|
ruleForm.serviceAssetConfig.serviceAssetArrivalConfig.schedule.timeSlots = [];
|
2025-12-23 13:10:26 +08:00
|
|
|
|
}
|
2025-12-24 17:59:50 +08:00
|
|
|
|
ruleForm.serviceAssetConfig.serviceAssetArrivalConfig.schedule.timeSlots.push({
|
2025-12-20 17:57:24 +08:00
|
|
|
|
dayOfWeek: '1',
|
|
|
|
|
|
startTime: '09:00',
|
|
|
|
|
|
endTime: '18:00',
|
|
|
|
|
|
capacity: 100,
|
|
|
|
|
|
});
|
2025-12-24 17:59:50 +08:00
|
|
|
|
formRef.value?.validateField('serviceAssetConfig.serviceAssetArrivalConfig.schedule.timeSlots');
|
2025-12-20 17:57:24 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const removeTimeSlot = (index: number) => {
|
2025-12-24 17:59:50 +08:00
|
|
|
|
ruleForm.serviceAssetConfig.serviceAssetArrivalConfig.schedule.timeSlots.splice(index, 1);
|
|
|
|
|
|
formRef.value?.validateField('serviceAssetConfig.serviceAssetArrivalConfig.schedule.timeSlots');
|
2025-12-20 17:57:24 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 例外日期操作
|
|
|
|
|
|
const addException = () => {
|
2025-12-24 17:59:50 +08:00
|
|
|
|
if (!ruleForm.serviceAssetConfig.serviceAssetArrivalConfig.schedule) {
|
|
|
|
|
|
ruleForm.serviceAssetConfig.serviceAssetArrivalConfig.schedule = { timeSlots: [], exceptions: [] };
|
2025-12-23 13:10:26 +08:00
|
|
|
|
}
|
2025-12-24 17:59:50 +08:00
|
|
|
|
if (!ruleForm.serviceAssetConfig.serviceAssetArrivalConfig.schedule.exceptions) {
|
|
|
|
|
|
ruleForm.serviceAssetConfig.serviceAssetArrivalConfig.schedule.exceptions = [];
|
2025-12-23 13:10:26 +08:00
|
|
|
|
}
|
2025-12-24 17:59:50 +08:00
|
|
|
|
ruleForm.serviceAssetConfig.serviceAssetArrivalConfig.schedule.exceptions.push({
|
2025-12-22 14:08:17 +08:00
|
|
|
|
exceptionType: 'date',
|
2025-12-20 17:57:24 +08:00
|
|
|
|
date: '',
|
|
|
|
|
|
status: 1,
|
|
|
|
|
|
reason: '',
|
|
|
|
|
|
dayOfWeek: '1',
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const removeException = (index: number) => {
|
2025-12-24 17:59:50 +08:00
|
|
|
|
ruleForm.serviceAssetConfig.serviceAssetArrivalConfig.schedule.exceptions.splice(index, 1);
|
2025-12-20 17:57:24 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2025-12-22 14:08:17 +08:00
|
|
|
|
// 例外类型切换时清空对应字段
|
|
|
|
|
|
const onExceptionTypeChange = (exc: Exception) => {
|
|
|
|
|
|
if (exc.exceptionType === 'date') {
|
|
|
|
|
|
exc.dayOfWeek = '';
|
|
|
|
|
|
} else {
|
|
|
|
|
|
exc.date = '';
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-12-24 17:59:50 +08:00
|
|
|
|
// 键值对操作
|
|
|
|
|
|
const addKeyValuePair = (list: KeyValuePair[]) => {
|
|
|
|
|
|
list.push({ key: '', value: '' });
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const removeKeyValuePair = (list: KeyValuePair[], index: number) => {
|
|
|
|
|
|
list.splice(index, 1);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-12-20 17:57:24 +08:00
|
|
|
|
// 重置表单
|
|
|
|
|
|
const resetForm = () => {
|
|
|
|
|
|
const initial = getInitialForm();
|
|
|
|
|
|
Object.assign(ruleForm, initial);
|
|
|
|
|
|
mainImagePreview.value = '';
|
|
|
|
|
|
imageFileList.value = [];
|
|
|
|
|
|
categoryAttrs.value = [];
|
2025-12-26 10:08:04 +08:00
|
|
|
|
fileAddressPrefix.value = '';
|
2025-12-20 17:57:24 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 获取分类数据
|
|
|
|
|
|
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;
|
2025-12-23 13:42:52 +08:00
|
|
|
|
// 初始化属性值,确保 boolean 类型默认为 false
|
2025-12-23 16:55:31 +08:00
|
|
|
|
categoryAttrs.value.forEach((attr: CategoryAttr) => {
|
2025-12-23 13:42:52 +08:00
|
|
|
|
const key = getAttrKey(attr);
|
|
|
|
|
|
if (attr.type === 'boolean') {
|
|
|
|
|
|
ruleForm.metadata[key] = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2025-12-20 17:57:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
.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;
|
2025-12-26 10:08:04 +08:00
|
|
|
|
// 支持 fileAddressPrefix 和 imgAddressPrefix
|
|
|
|
|
|
fileAddressPrefix.value = data.fileAddressPrefix || data.imgAddressPrefix || '';
|
2025-12-20 17:57:24 +08:00
|
|
|
|
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 || '';
|
|
|
|
|
|
|
2025-12-26 10:08:04 +08:00
|
|
|
|
// 主图预览 (支持 imageUrl 和 fileURL)
|
|
|
|
|
|
const mainImg = data.imageUrl || data.fileURL;
|
|
|
|
|
|
if (mainImg) {
|
|
|
|
|
|
mainImagePreview.value = formatImageUrl(mainImg);
|
|
|
|
|
|
ruleForm.mainImage = mainImg;
|
2025-12-20 17:57:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 图片列表
|
|
|
|
|
|
if (data.images && Array.isArray(data.images)) {
|
|
|
|
|
|
imageFileList.value = data.images.map((url: string, index: number) => ({
|
|
|
|
|
|
name: `image-${index}`,
|
2025-12-22 17:57:15 +08:00
|
|
|
|
url: formatImageUrl(url),
|
2025-12-26 10:08:04 +08:00
|
|
|
|
response: url, // 存储原始相对路径,供提交时使用
|
2025-12-20 17:57:24 +08:00
|
|
|
|
}));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 根据类型加载配置
|
|
|
|
|
|
if (data.type === 'physical' && data.physicalAssetConfig) {
|
|
|
|
|
|
Object.assign(ruleForm.physicalAssetConfig, data.physicalAssetConfig);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (data.type === 'virtual' && data.virtualAssetConfig) {
|
2025-12-24 17:59:50 +08:00
|
|
|
|
// 先处理 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);
|
2025-12-20 17:57:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
if (data.type === 'service' && data.serviceAssetConfig) {
|
|
|
|
|
|
Object.assign(ruleForm.serviceAssetConfig, data.serviceAssetConfig);
|
2025-12-24 17:59:50 +08:00
|
|
|
|
// 确保 serviceAssetArrivalConfig 对象存在
|
|
|
|
|
|
if (!ruleForm.serviceAssetConfig.serviceAssetArrivalConfig) {
|
|
|
|
|
|
ruleForm.serviceAssetConfig.serviceAssetArrivalConfig = {
|
|
|
|
|
|
schedule: { timeSlots: [], exceptions: [] },
|
|
|
|
|
|
booking: { minAdvance: 60, minDuration: 30, cancelWindow: 30 },
|
|
|
|
|
|
capacity: { maxUsers: 0 },
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
2025-12-23 13:10:26 +08:00
|
|
|
|
// 确保 schedule 对象存在
|
2025-12-24 17:59:50 +08:00
|
|
|
|
if (!ruleForm.serviceAssetConfig.serviceAssetArrivalConfig.schedule) {
|
|
|
|
|
|
ruleForm.serviceAssetConfig.serviceAssetArrivalConfig.schedule = { timeSlots: [], exceptions: [] };
|
2025-12-23 13:10:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
// 确保数组存在,防止后端返回 null 或 undefined 导致 push 报错
|
2025-12-24 17:59:50 +08:00
|
|
|
|
if (!ruleForm.serviceAssetConfig.serviceAssetArrivalConfig.schedule.exceptions) {
|
|
|
|
|
|
ruleForm.serviceAssetConfig.serviceAssetArrivalConfig.schedule.exceptions = [];
|
2025-12-23 13:10:26 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
// 补充缺失的 exceptionType
|
2025-12-24 17:59:50 +08:00
|
|
|
|
ruleForm.serviceAssetConfig.serviceAssetArrivalConfig.schedule.exceptions.forEach((exc: Exception) => {
|
2025-12-23 13:10:26 +08:00
|
|
|
|
if (!exc.exceptionType) {
|
|
|
|
|
|
if (exc.date) {
|
|
|
|
|
|
exc.exceptionType = 'date';
|
|
|
|
|
|
} else if (exc.dayOfWeek) {
|
|
|
|
|
|
exc.exceptionType = 'dayOfWeek';
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 默认值
|
|
|
|
|
|
exc.exceptionType = 'date';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2025-12-24 17:59:50 +08:00
|
|
|
|
if (!ruleForm.serviceAssetConfig.serviceAssetArrivalConfig.schedule.timeSlots) {
|
|
|
|
|
|
ruleForm.serviceAssetConfig.serviceAssetArrivalConfig.schedule.timeSlots = [];
|
2025-12-23 13:10:26 +08:00
|
|
|
|
}
|
2025-12-20 17:57:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 元数据
|
|
|
|
|
|
if (data.metadata) {
|
2025-12-23 17:07:32 +08:00
|
|
|
|
if (Array.isArray(data.metadata)) {
|
|
|
|
|
|
data.metadata.forEach((item: any) => {
|
|
|
|
|
|
if (item.name) {
|
2025-12-25 10:20:52 +08:00
|
|
|
|
let val = item.value;
|
|
|
|
|
|
// 修复回显问题:如果 value 不存在但 options 存在,尝试从 options 恢复值
|
|
|
|
|
|
if ((val === undefined || val === null) && item.options && item.options.length > 0) {
|
|
|
|
|
|
if (item.type === 'multi_select') {
|
|
|
|
|
|
val = item.options.map((opt: any) => opt.value);
|
|
|
|
|
|
} else if (item.type === 'select') {
|
|
|
|
|
|
val = item.options[0].value;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ruleForm.metadata[item.name] = val;
|
2025-12-23 17:07:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Object.assign(ruleForm.metadata, data.metadata);
|
|
|
|
|
|
}
|
2025-12-20 17:57:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 加载分类属性
|
|
|
|
|
|
if (data.categoryId) {
|
|
|
|
|
|
getCategory(data.categoryId)
|
|
|
|
|
|
.then((catRes: any) => {
|
|
|
|
|
|
const catData = catRes.data;
|
|
|
|
|
|
if (catData?.attrs && Array.isArray(catData.attrs)) {
|
|
|
|
|
|
categoryAttrs.value = catData.attrs;
|
2025-12-23 13:42:52 +08:00
|
|
|
|
// 初始化属性值,确保 boolean 类型默认为 false
|
2025-12-23 16:55:31 +08:00
|
|
|
|
categoryAttrs.value.forEach((attr: CategoryAttr) => {
|
2025-12-23 13:42:52 +08:00
|
|
|
|
const key = getAttrKey(attr);
|
|
|
|
|
|
if (attr.type === 'boolean' && ruleForm.metadata[key] === undefined) {
|
|
|
|
|
|
ruleForm.metadata[key] = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2025-12-20 17:57:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch(() => {
|
|
|
|
|
|
categoryAttrs.value = [];
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
.finally(() => {
|
|
|
|
|
|
formLoading.value = false;
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
isShowDialog.value = true;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 关闭弹窗
|
|
|
|
|
|
const closeDialog = () => {
|
|
|
|
|
|
isShowDialog.value = false;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 取消
|
|
|
|
|
|
const onCancel = () => {
|
|
|
|
|
|
closeDialog();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-12-24 17:59:50 +08:00
|
|
|
|
// 上传图片并返回URL
|
|
|
|
|
|
const uploadImage = async (file: File): Promise<string> => {
|
|
|
|
|
|
const res: any = await uploadAssetImage(file);
|
2025-12-26 10:08:04 +08:00
|
|
|
|
|
|
|
|
|
|
// 1. 尝试获取并设置 fileAddressPrefix
|
|
|
|
|
|
// 优先检查顶层,再检查 data 内部
|
|
|
|
|
|
if (res.fileAddressPrefix) {
|
|
|
|
|
|
fileAddressPrefix.value = res.fileAddressPrefix;
|
|
|
|
|
|
} else if (res.data && typeof res.data === 'object' && res.data.fileAddressPrefix) {
|
|
|
|
|
|
fileAddressPrefix.value = res.data.fileAddressPrefix;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 尝试获取 fileURL / url
|
|
|
|
|
|
// 优先检查顶层 fileURL
|
|
|
|
|
|
if (res.fileURL) return res.fileURL;
|
|
|
|
|
|
|
|
|
|
|
|
// 检查 data 对象中的 fileURL 或 url
|
|
|
|
|
|
if (res.data && typeof res.data === 'object') {
|
|
|
|
|
|
if (res.data.fileURL) return res.data.fileURL;
|
|
|
|
|
|
if (res.data.url) return res.data.url;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 兼容旧逻辑 (data 直接是 url 字符串)
|
|
|
|
|
|
if (typeof res.data === 'string') return res.data;
|
|
|
|
|
|
|
|
|
|
|
|
return '';
|
2025-12-24 17:59:50 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 构建请求体
|
|
|
|
|
|
const buildRequestBody = async (): Promise<any> => {
|
|
|
|
|
|
const body: any = {
|
|
|
|
|
|
name: ruleForm.name,
|
|
|
|
|
|
type: ruleForm.type,
|
|
|
|
|
|
categoryId: ruleForm.categoryId,
|
|
|
|
|
|
description: ruleForm.description || '',
|
|
|
|
|
|
};
|
2025-12-20 17:57:24 +08:00
|
|
|
|
|
2025-12-24 17:59:50 +08:00
|
|
|
|
// 编辑模式添加 id
|
2025-12-20 17:57:24 +08:00
|
|
|
|
if (isEdit.value && ruleForm.id) {
|
2025-12-24 17:59:50 +08:00
|
|
|
|
body.id = ruleForm.id;
|
2025-12-20 17:57:24 +08:00
|
|
|
|
}
|
2025-12-24 17:59:50 +08:00
|
|
|
|
|
|
|
|
|
|
// 上下线时间
|
2025-12-20 17:57:24 +08:00
|
|
|
|
if (ruleForm.onlineTime) {
|
2025-12-24 17:59:50 +08:00
|
|
|
|
body.onlineTime = ruleForm.onlineTime;
|
2025-12-20 17:57:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
if (ruleForm.offlineTime) {
|
2025-12-24 17:59:50 +08:00
|
|
|
|
body.offlineTime = ruleForm.offlineTime;
|
2025-12-20 17:57:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-25 17:55:52 +08:00
|
|
|
|
// 主图 (已在上传时直接赋值给 ruleForm.mainImage)
|
|
|
|
|
|
if (ruleForm.mainImage) {
|
2025-12-26 15:40:14 +08:00
|
|
|
|
body.imageURL = ruleForm.mainImage;
|
2025-12-20 17:57:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-25 17:55:52 +08:00
|
|
|
|
// 图片列表
|
2025-12-24 17:59:50 +08:00
|
|
|
|
const imageUrls: string[] = [];
|
|
|
|
|
|
for (const file of imageFileList.value) {
|
2025-12-26 15:40:14 +08:00
|
|
|
|
let url = (file.response as string) || '';
|
|
|
|
|
|
if (!url && file.url && !file.url.startsWith('blob:')) {
|
|
|
|
|
|
url = file.url;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (url) {
|
|
|
|
|
|
// 移除前缀,确保提交相对路径
|
|
|
|
|
|
if (fileAddressPrefix.value && url.startsWith(fileAddressPrefix.value)) {
|
|
|
|
|
|
url = url.substring(fileAddressPrefix.value.length);
|
|
|
|
|
|
}
|
|
|
|
|
|
imageUrls.push(url);
|
2025-12-20 17:57:24 +08:00
|
|
|
|
}
|
2025-12-24 17:59:50 +08:00
|
|
|
|
}
|
2025-12-26 15:40:14 +08:00
|
|
|
|
body.images = imageUrls;
|
2025-12-20 17:57:24 +08:00
|
|
|
|
|
|
|
|
|
|
// 根据类型添加配置
|
|
|
|
|
|
if (ruleForm.type === 'physical') {
|
2025-12-24 17:59:50 +08:00
|
|
|
|
body.physicalAssetConfig = ruleForm.physicalAssetConfig;
|
2025-12-20 17:57:24 +08:00
|
|
|
|
} else if (ruleForm.type === 'virtual') {
|
2025-12-24 17:59:50 +08:00
|
|
|
|
// 深拷贝 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;
|
2025-12-20 17:57:24 +08:00
|
|
|
|
} else if (ruleForm.type === 'service') {
|
2025-12-24 17:59:50 +08:00
|
|
|
|
body.serviceAssetConfig = ruleForm.serviceAssetConfig;
|
2025-12-20 17:57:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 元数据(分类属性值)
|
2025-12-23 17:07:32 +08:00
|
|
|
|
if (categoryAttrs.value.length > 0) {
|
2025-12-24 17:59:50 +08:00
|
|
|
|
const metadataArray: any[] = [];
|
|
|
|
|
|
for (const attr of categoryAttrs.value) {
|
2025-12-23 17:07:32 +08:00
|
|
|
|
const key = getAttrKey(attr);
|
|
|
|
|
|
let value = ruleForm.metadata[key];
|
|
|
|
|
|
|
2025-12-25 17:55:52 +08:00
|
|
|
|
// 如果是图片类型,且值是 blob 开头(未上传成功的情况),置为空
|
|
|
|
|
|
if (attr.type === 'image' && typeof value === 'string' && value.startsWith('blob:')) {
|
2025-12-23 17:07:32 +08:00
|
|
|
|
value = '';
|
2025-12-23 13:42:52 +08:00
|
|
|
|
}
|
2025-12-23 17:07:32 +08:00
|
|
|
|
|
2025-12-24 17:59:50 +08:00
|
|
|
|
const metaItem: any = {
|
2025-12-23 17:07:32 +08:00
|
|
|
|
name: attr.name,
|
|
|
|
|
|
type: attr.type,
|
|
|
|
|
|
value: value,
|
2026-01-12 15:59:05 +08:00
|
|
|
|
...(attr.dictType ? { dictType: attr.dictType } : {}),
|
2025-12-23 17:07:32 +08:00
|
|
|
|
};
|
2025-12-24 17:59:50 +08:00
|
|
|
|
|
|
|
|
|
|
// 只有单选和多选类型才传递 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;
|
2025-12-20 17:57:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-24 17:59:50 +08:00
|
|
|
|
return body;
|
2025-12-20 17:57:24 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 提交
|
2025-12-24 17:59:50 +08:00
|
|
|
|
const onSubmit = async () => {
|
2025-12-22 17:57:15 +08:00
|
|
|
|
const form = formRef.value;
|
|
|
|
|
|
if (!form) return;
|
2025-12-24 17:59:50 +08:00
|
|
|
|
|
|
|
|
|
|
form.validate(async (valid: boolean) => {
|
2025-12-20 17:57:24 +08:00
|
|
|
|
if (valid) {
|
|
|
|
|
|
submitLoading.value = true;
|
2025-12-24 17:59:50 +08:00
|
|
|
|
try {
|
|
|
|
|
|
const requestBody = await buildRequestBody();
|
|
|
|
|
|
const request = isEdit.value ? updateAsset(requestBody) : createAsset(requestBody);
|
|
|
|
|
|
|
|
|
|
|
|
await request;
|
|
|
|
|
|
ElMessage.success(isEdit.value ? '修改成功' : '添加成功');
|
|
|
|
|
|
closeDialog();
|
|
|
|
|
|
emit('getAssetList');
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('提交失败:', error);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
submitLoading.value = false;
|
|
|
|
|
|
}
|
2025-12-20 17:57:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 暴露方法
|
|
|
|
|
|
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>
|