| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148 |
- <template>
- <div class="page-container">
- <div class="create-layout">
- <!-- 左侧:下单填写区 -->
- <div class="form-container">
- <!-- 1. 服务类型选择 -->
- <div class="type-selection">
- <div
- v-for="item in serviceList"
- :key="item.type"
- class="type-card"
- :class="[item.type, { active: form.type === item.type }]"
- @click="handleTypeChange(item.type)"
- >
- <div class="icon-box"><el-icon><component :is="item.icon" /></el-icon></div>
- <div class="text">
- <div class="type-name">{{ item.name }}</div>
- <div class="type-desc">{{ item.desc }}</div>
- </div>
- </div>
- </div>
- <!-- 2. 基础信息:门店与宠主 -->
- <el-card shadow="never" class="section-card">
- <template #header>
- <div class="card-title">
- <span class="step-num">02</span> 基础信息
- </div>
- </template>
- <div class="card-body">
- <el-form label-position="top" class="base-form">
- <el-row :gutter="20">
- <el-col :span="12">
- <el-form-item>
- <template #label>
- <div style="display:flex; align-items:center; height: 24px;">
- <span>服务门店 (平台代下单)</span>
- </div>
- </template>
- <el-select v-model="form.merchantId" placeholder="请选择商户门店" size="large" style="width: 100%" filterable>
- <el-option v-for="m in merchants" :key="m.id" :label="m.name" :value="m.id" />
- </el-select>
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item>
- <template #label>
- <div style="display:flex; justify-content:space-between; align-items:center; width:100%; height: 24px;">
- <span>宠主用户</span>
- <el-button type="primary" plain size="small" @click="openAddUser" icon="Plus" style="margin-left: 15px;">添加用户</el-button>
- </div>
- </template>
- <el-select
- v-model="form.userId"
- placeholder="搜索手机号/姓名"
- size="large"
- style="width: 100%"
- filterable
- remote
- :remote-method="searchUser"
- :loading="userLoading"
- @change="handleUserChange"
- >
- <el-option v-for="u in userOptions" :key="u.id" :label="u.name + ' - ' + u.phone" :value="u.id" />
- </el-select>
- </el-form-item>
- </el-col>
- </el-row>
- <el-form-item label="选择宠物" v-if="form.userId">
- <div class="pet-select-row">
- <div
- v-for="p in currentPets"
- :key="p.id"
- class="pet-card"
- :class="{ active: form.petId === p.id }"
- @click="form.petId = p.id"
- >
- <el-avatar :size="48" :src="p.avatar" shape="square" style="border-radius: 6px;">{{ p.name.charAt(0) }}</el-avatar>
- <div class="pet-info">
- <div class="name">{{ p.name }}</div>
- <div class="sub">{{ p.breed }}</div>
- </div>
- <div class="check-mark" v-if="form.petId === p.id"><el-icon><Check /></el-icon></div>
- </div>
- <!-- Add Button Card (Last Item in Grid) -->
- <div class="pet-card add-card" @click="openAddPet">
- <el-icon :size="24"><Plus /></el-icon>
- <span style="font-size: 15px; font-weight: bold;">新增宠物</span>
- </div>
- </div>
- </el-form-item>
- </el-form>
- </div>
- </el-card>
- <!-- 3. 业务详情表单 -->
- <el-card shadow="never" class="section-card form-card" v-if="form.type">
- <template #header>
- <div class="card-title">
- <span class="step-num">03</span>
- {{ getStepTitle(form.type) }}
- </div>
- </template>
- <div class="card-body">
- <!-- 服务套餐信息 -->
- <el-form-item label="团购套餐">
- <el-input v-model="form.groupBuyPackage" placeholder="请输入团购套餐名称 (选填)" clearable />
- </el-form-item>
- <div class="divider"></div>
- <!-- A. 宠物接送表单 -->
- <div v-show="form.type === 'transport'" class="business-form">
- <el-form-item label="接送模式">
- <el-radio-group v-model="form.transport.subType" size="large" @change="calcPrice('transport')">
- <el-radio-button label="round">往返接送</el-radio-button>
- <el-radio-button label="pick">单程接 (到店)</el-radio-button>
- <el-radio-button label="drop">单程送 (回家)</el-radio-button>
- </el-radio-group>
- </el-form-item>
- <div class="route-box">
- <!-- 接宠段 -->
- <div class="route-segment" v-if="['round', 'pick'].includes(form.transport.subType)">
- <div class="seg-badge start">接</div>
- <div class="seg-content">
- <el-row :gutter="10">
- <el-col :span="8">
- <el-cascader v-model="form.transport.pickRegion" :options="pcaOptions" placeholder="省/市/区" style="width: 100%" />
- </el-col>
- <el-col :span="16">
- <el-input v-model="form.transport.pickDetail" placeholder="详细地址 (街道/门牌号)" prefix-icon="Location" />
- </el-col>
- </el-row>
- <el-row :gutter="10">
- <el-col :span="12"><el-input v-model="form.transport.pickContact" placeholder="联系人" /></el-col>
- <el-col :span="12"><el-input v-model="form.transport.pickPhone" placeholder="电话" /></el-col>
- </el-row>
- <el-row :gutter="10">
- <el-col :span="24">
- <el-date-picker v-model="form.transport.pickTime" type="datetime" placeholder="选择接宠时间" style="width: 100%" />
- </el-col>
- </el-row>
- </div>
- </div>
- <!-- 门店中转标识 -->
- <div class="route-connector">
- <div class="line"></div>
- <div class="store-node"><el-icon><Shop /></el-icon> 服务门店</div>
- <div class="line"></div>
- </div>
- <!-- 送回段 -->
- <div class="route-segment" v-if="['round', 'drop'].includes(form.transport.subType)">
- <div class="seg-badge end">送</div>
- <div class="seg-content">
- <el-row :gutter="10">
- <el-col :span="8">
- <el-cascader v-model="form.transport.dropRegion" :options="pcaOptions" placeholder="省/市/区" style="width: 100%" />
- </el-col>
- <el-col :span="16">
- <el-input v-model="form.transport.dropDetail" placeholder="详细地址" prefix-icon="Location" />
- </el-col>
- </el-row>
- <el-row :gutter="10">
- <el-col :span="12"><el-input v-model="form.transport.dropContact" placeholder="联系人" /></el-col>
- <el-col :span="12"><el-input v-model="form.transport.dropPhone" placeholder="电话" /></el-col>
- </el-row>
- <el-row :gutter="10">
- <el-col :span="24">
- <el-date-picker v-model="form.transport.dropTime" type="datetime" placeholder="预计送回时间 (可选)" style="width: 100%" />
- </el-col>
- </el-row>
- </div>
- </div>
- </div>
- </div>
- <!-- B. 上门喂遛表单 -->
- <div v-show="form.type === 'feeding'" class="business-form">
- <div style="margin-bottom: 20px;">
- <div class="section-label">上门服务地址</div>
- <el-row :gutter="10">
- <el-col :span="8">
- <el-cascader v-model="form.feeding.region" :options="pcaOptions" placeholder="省/市/区" style="width: 100%" />
- </el-col>
- <el-col :span="16">
- <el-input v-model="form.feeding.addressDetail" placeholder="详细地址 (街道/门牌号)" prefix-icon="Location" />
- </el-col>
- </el-row>
- </div>
- <div style="margin-bottom: 20px;">
- <div class="section-label" style="display:flex; align-items:center; margin-bottom:10px;">
- 预约服务时间
- <el-tag type="info" size="small" style="margin-left:10px;">共 {{ form.feeding.appointments.length }} 次</el-tag>
- </div>
- <div v-for="(item, index) in form.feeding.appointments" :key="index" style="display:flex; align-items:center; margin-bottom:10px;">
- <span style="width:30px; color:#999; font-size:12px; font-weight:bold;">{{ index + 1 }}.</span>
- <el-date-picker
- v-model="item.startTime"
- type="datetime"
- placeholder="开始时间"
- style="width: 200px; margin-right: 5px;"
- format="YYYY-MM-DD HH:mm"
- />
- <span style="margin:0 5px; color:#999;">~</span>
- <el-date-picker
- v-model="item.endTime"
- type="datetime"
- placeholder="结束时间 (可选)"
- style="width: 200px; margin-right: 15px;"
- format="YYYY-MM-DD HH:mm"
- />
- <div style="display:flex; gap:8px; margin-left:5px;">
- <el-button v-if="index === form.feeding.appointments.length - 1" type="primary" circle size="small" icon="Plus" @click="addAppointment('feeding')" />
- <el-button v-if="form.feeding.appointments.length > 1" type="danger" circle size="small" icon="Minus" @click="removeAppointment('feeding', index)" plain />
- </div>
- </div>
- </div>
- <div class="remark-section">
- <div class="section-label">家庭服务及宠物档案备注</div>
- <el-row :gutter="15">
- <el-col :span="12"><el-input v-model="form.feeding.area" placeholder="宠物活动区域" /></el-col>
- <el-col :span="12"><el-input v-model="form.feeding.itemLoc" placeholder="物品存放位置" /></el-col>
- <el-col :span="12" style="margin-top:10px"><el-input v-model="form.feeding.cleanLoc" placeholder="清洗位置" /></el-col>
- <el-col :span="12" style="margin-top:10px"><el-input v-model="form.feeding.foodAmount" placeholder="喂食量标准" /></el-col>
- <el-col :span="24" style="margin-top:10px"><el-input v-model="form.feeding.other" type="textarea" :rows="2" placeholder="其他注意事项" /></el-col>
- </el-row>
- </div>
- </div>
- <!-- C. 上门洗护表单 -->
- <div v-show="form.type === 'washing'" class="business-form">
- <div style="margin-bottom: 20px;">
- <div class="section-label">上门服务地址</div>
- <el-row :gutter="10">
- <el-col :span="8">
- <el-cascader v-model="form.washing.region" :options="pcaOptions" placeholder="省/市/区" style="width: 100%" />
- </el-col>
- <el-col :span="16">
- <el-input v-model="form.washing.addressDetail" placeholder="详细地址 (街道/门牌号)" prefix-icon="Location" />
- </el-col>
- </el-row>
- </div>
- <div style="margin-bottom: 20px;">
- <div class="section-label" style="display:flex; align-items:center; margin-bottom:10px;">
- 预约服务时间
- <el-tag type="info" size="small" style="margin-left:10px;">共 {{ form.washing.appointments.length }} 次</el-tag>
- </div>
- <div v-for="(item, index) in form.washing.appointments" :key="index" style="display:flex; align-items:center; margin-bottom:10px;">
- <span style="width:30px; color:#999; font-size:12px; font-weight:bold;">{{ index + 1 }}.</span>
- <el-date-picker
- v-model="item.startTime"
- type="datetime"
- placeholder="开始时间"
- style="width: 200px; margin-right: 5px;"
- format="YYYY-MM-DD HH:mm"
- />
- <span style="margin:0 5px; color:#999;">~</span>
- <el-date-picker
- v-model="item.endTime"
- type="datetime"
- placeholder="结束时间 (可选)"
- style="width: 200px; margin-right: 15px;"
- format="YYYY-MM-DD HH:mm"
- />
- <div style="display:flex; gap:8px; margin-left:5px;">
- <el-button v-if="index === form.washing.appointments.length - 1" type="primary" circle size="small" icon="Plus" @click="addAppointment('washing')" />
- <el-button v-if="form.washing.appointments.length > 1" type="danger" circle size="small" icon="Minus" @click="removeAppointment('washing', index)" plain />
- </div>
- </div>
- </div>
- <div class="remark-section">
- <div class="section-label">服务备注及宠物状态</div>
- <el-row :gutter="15">
- <el-col :span="8">
- <el-select v-model="form.washing.petStatus" placeholder="宠物应激状态" style="width:100%">
- <el-option label="性格温顺" value="calm" />
- <!-- ... options ... -->
- <el-option label="胆小怕人" value="shy" />
- <el-option label="容易应激" value="stress" />
- <el-option label="有攻击性" value="aggressive" />
- </el-select>
- </el-col>
- <el-col :span="8"><el-input v-model="form.washing.cleanLoc" placeholder="清洗位置" /></el-col>
- <el-col :span="8"><el-input v-model="form.washing.toolLoc" placeholder="工具/水源位置" /></el-col>
- <el-col :span="24" style="margin-top:10px"><el-input v-model="form.washing.other" type="textarea" :rows="2" placeholder="其他注意事项" /></el-col>
- </el-row>
- </div>
- </div>
- </div>
- </el-card>
- </div>
- <!-- 右侧:收银台概览 -->
- <div class="summary-sidebar">
- <div class="summary-panel">
- <div class="summary-header">订单概览</div>
- <div class="summary-content">
- <div class="row" v-if="selectedMerchantName">
- <span class="label">服务门店</span>
- <span class="value">{{ selectedMerchantName }}</span>
- </div>
- <div class="row" v-if="selectedUserName">
- <span class="label">客户</span>
- <span class="value">{{ selectedUserName }}</span>
- </div>
- <div class="row" v-if="selectedPetName">
- <span class="label">服务对象</span>
- <span class="value action-text">{{ selectedPetName }} ({{ selectedPetBreed }})</span>
- </div>
- <div class="divider"></div>
- <div class="service-preview" v-if="form.type">
- <div class="preview-title">{{ getTypeName(form.type) }}</div>
- <!-- 套餐显示 -->
- <div class="preview-detail" v-if="selectedPkgName">
- <div style="font-weight:bold; color:#409eff">{{ selectedPkgName }}</div>
- </div>
- <div class="preview-detail" v-else>
- <div style="color:#e6a23c">非服务套餐 (单次)</div>
- </div>
- <!-- 接送预览 -->
- <div v-if="form.type === 'transport'" class="preview-detail">
- <div>{{ form.transport.subType === 'round' ? '往返接送' : (form.transport.subType === 'pick' ? '单程接' : '单程送') }}</div>
- <div class="minor">接: {{ form.transport.pickTime ? formatTime(form.transport.pickTime) : '未选时间' }}</div>
- <div class="minor" v-if="form.transport.subType !== 'pick'">送: {{ form.transport.dropTime ? formatTime(form.transport.dropTime) : '未选' }}</div>
- </div>
- </div>
- </div>
- <div class="summary-footer">
- <el-button type="primary" size="large" class="submit-btn" :disabled="!canSubmit" @click="handleSubmit">
- 立即下单
- </el-button>
- </div>
- </div>
- </div>
- </div>
- <!-- Dialogs -->
- <!-- Add User Dialog -->
- <el-dialog v-model="userDialogVisible" title="新增用户" width="700px" destroy-on-close append-to-body class="add-user-dialog">
- <el-form :model="userForm" label-width="90px" class="user-form">
- <div style="display: flex; justify-content: center; align-items: center; gap: 20px; margin-bottom: 30px;">
- <el-upload action="#" :show-file-list="false" :auto-upload="false" :on-change="handleUserAvatarChange">
- <el-avatar :size="80" :src="userForm.avatar || 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png'" style="cursor: pointer; border: 2px solid #e4e7ed;" />
- </el-upload>
- <el-button type="primary" link @click="">点击修改头像</el-button>
- </div>
- <div class="form-section-header">基本资料</div>
- <el-row :gutter="30">
- <el-col :span="12">
- <el-form-item label="录入来源">
- <el-select v-model="userForm.source" style="width: 100%" filterable allow-create default-first-option>
- <el-option label="平台录入" value="平台录入" />
- <el-option label="萌它宠物连锁录入" value="萌它宠物连锁录入" />
- </el-select>
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="所属区域">
- <el-select v-model="userForm.area" style="width: 100%" filterable allow-create default-first-option placeholder="请选择或输入">
- <el-option label="朝阳区" value="朝阳区" />
- <el-option label="海淀区" value="海淀区" />
- </el-select>
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="姓名" required><el-input v-model="userForm.name" placeholder="请输入姓名" /></el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="电话" required><el-input v-model="userForm.phone" placeholder="请输入电话" /></el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="性别">
- <el-radio-group v-model="userForm.gender">
- <el-radio label="男">男</el-radio>
- <el-radio label="女">女</el-radio>
- </el-radio-group>
- </el-form-item>
- </el-col>
- </el-row>
- <div class="form-section-header">居住信息</div>
- <el-row :gutter="30">
- <el-col :span="24">
- <el-form-item label="所在地区">
- <el-cascader v-model="userForm.region" :options="pcaOptions" placeholder="请选择省/市/区" style="width: 100%" />
- </el-form-item>
- </el-col>
- <el-col :span="24">
- <el-form-item label="详细住址"><el-input v-model="userForm.detailAddress" placeholder="请输入街道/门牌号" /></el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="房屋类型">
- <el-radio-group v-model="userForm.houseType">
- <el-radio label="stairs">楼梯</el-radio>
- <el-radio label="elevator">电梯</el-radio>
- </el-radio-group>
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="入门方式">
- <el-radio-group v-model="userForm.entryMethod">
- <el-radio label="password">密码开门</el-radio>
- <el-radio label="key">钥匙开门</el-radio>
- </el-radio-group>
- </el-form-item>
- </el-col>
- <el-col :span="12" v-if="userForm.entryMethod === 'password'">
- <el-form-item label="开门密码">
- <el-input v-model="userForm.entryPassword" placeholder="请输入密码" />
- </el-form-item>
- </el-col>
- <el-col :span="12" v-if="userForm.entryMethod === 'key'">
- <el-form-item label="钥匙位置">
- <el-input v-model="userForm.keyLocation" placeholder="如:地毯下" />
- </el-form-item>
- </el-col>
- </el-row>
- <div class="form-section-header">其他</div>
- <el-row :gutter="30">
- <el-col :span="24">
- <el-form-item label="用户标签">
- <el-select v-model="userSelectedTagIds" multiple placeholder="选择标签" style="width: 100%">
- <el-option v-for="tag in allUserTags" :key="tag.id" :label="tag.name" :value="tag.id">
- <el-tag :type="tag.type" effect="light" size="small">{{ tag.name }}</el-tag>
- </el-option>
- </el-select>
- </el-form-item>
- </el-col>
- <el-col :span="24">
- <el-form-item label="备注说明"><el-input type="textarea" v-model="userForm.remark" rows="3" /></el-form-item>
- </el-col>
- </el-row>
- </el-form>
- <template #footer>
- <div style="text-align: center; margin-top: 20px;">
- <el-button @click="userDialogVisible = false" size="large" style="width: 120px;">取消</el-button>
- <el-button type="primary" @click="submitUser" size="large" style="width: 120px;">保存</el-button>
- </div>
- </template>
- </el-dialog>
- <el-dialog v-model="petDialogVisible" title="宠物档案详情" width="800px" top="10vh" class="pet-profile-dialog">
- <el-tabs v-model="activePetTab" class="pet-tabs">
- <el-tab-pane label="基本信息" name="basic">
- <div class="pet-form-content">
- <!-- Avatar Upload -->
- <div class="avatar-col">
- <el-upload
- class="avatar-uploader"
- action="#"
- :show-file-list="false"
- :auto-upload="false"
- :on-change="handleAvatarChange"
- >
- <img v-if="petForm.avatar" :src="petForm.avatar" class="avatar" />
- <el-icon v-else class="avatar-uploader-icon" :size="28" color="#8c939d"><Plus /></el-icon>
- </el-upload>
- <div style="font-size:12px; color:#999; margin-top:8px; text-align:center">点击上传头像</div>
- </div>
- <!-- Form Fields -->
- <el-form :model="petForm" label-width="80px" class="inner-form">
- <el-row :gutter="20">
- <el-col :span="12">
- <el-form-item label="宠物姓名" required>
- <el-input v-model="petForm.name" placeholder="请输入" />
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="所属主人" required>
- <el-select v-model="form.userId" disabled placeholder="选择主人" style="width:100%">
- <el-option v-for="u in userOptions" :key="u.id" :label="u.name" :value="u.id" />
- </el-select>
- </el-form-item>
- </el-col>
- </el-row>
- <el-row :gutter="20">
- <el-col :span="12">
- <el-form-item label="性别">
- <el-radio-group v-model="petForm.gender">
- <el-radio label="MM">公</el-radio>
- <el-radio label="GG">母</el-radio>
- </el-radio-group>
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="品种">
- <el-select v-model="petForm.breed" placeholder="请选择品种" style="width:100%">
- <el-option label="金毛" value="金毛" />
- <el-option label="布偶" value="布偶" />
- <el-option label="边牧" value="边牧" />
- </el-select>
- </el-form-item>
- </el-col>
- </el-row>
- <el-row :gutter="20">
- <el-col :span="12">
- <el-form-item label="体型">
- <el-select v-model="petForm.bodyType" placeholder="选择体型" style="width:100%">
- <el-option label="小型" value="small" />
- <el-option label="中型" value="medium" />
- <el-option label="大型" value="large" />
- </el-select>
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="体重(kg)">
- <el-row :gutter="10">
- <el-col :span="12"><el-input-number v-model="petForm.weight" :min="0" :step="0.1" controls="false" style="width:100%" /></el-col>
- <el-col :span="12"></el-col>
- </el-row>
- </el-form-item>
- </el-col>
- </el-row>
- <el-row :gutter="20">
- <el-col :span="12">
- <el-form-item label="年龄(岁)">
- <el-input-number v-model="petForm.age" :min="0" style="width:100%" />
- </el-form-item>
- </el-col>
- </el-row>
- <el-form-item label="性格关键词">
- <el-input v-model="petForm.keywords" placeholder="如:活泼、粘人" />
- </el-form-item>
- <el-form-item label="萌宠性格">
- <el-input v-model="petForm.desc" type="textarea" placeholder="详细描述" :rows="2" />
- </el-form-item>
- <el-form-item label="宠物标签">
- <el-select v-model="petForm.tags" multiple placeholder="选择标签" style="width:100%">
- <el-option label="绝育" value="1" />
- <el-option label="疫苗齐全" value="2" />
- </el-select>
- </el-form-item>
- </el-form>
- </div>
- </el-tab-pane>
- <el-tab-pane label="家庭信息" name="family">
- <el-form :model="petForm" label-width="120px">
- <el-form-item label="新来家庭时间">
- <el-date-picker v-model="petForm.arrivalTime" type="date" placeholder="选择日期" style="width: 100%" />
- </el-form-item>
- <el-form-item label="家庭房屋类型">
- <el-radio-group v-model="petForm.houseType">
- <el-radio label="stairs">楼梯</el-radio>
- <el-radio label="elevator">电梯</el-radio>
- </el-radio-group>
- </el-form-item>
- <el-form-item label="入门方式">
- <el-radio-group v-model="petForm.entryMethod">
- <el-radio label="password">密码开门</el-radio>
- <el-radio label="key">钥匙开门</el-radio>
- </el-radio-group>
- </el-form-item>
- <el-form-item label="密码" v-if="petForm.entryMethod === 'password'">
- <el-input v-model="petForm.entryPassword" placeholder="请输入门锁密码" />
- </el-form-item>
- <el-form-item label="钥匙位置" v-if="petForm.entryMethod === 'key'">
- <el-input v-model="petForm.keyLocation" placeholder="请输入钥匙存放位置" />
- </el-form-item>
- </el-form>
- </el-tab-pane>
- <el-tab-pane label="健康状况" name="health">
- <el-form :model="petForm" label-width="120px">
- <el-form-item label="健康状态">
- <el-radio-group v-model="petForm.healthStatus">
- <el-radio label="健康">健康</el-radio>
- <el-radio label="亚健康">亚健康</el-radio>
- <el-radio label="疾病">疾病</el-radio>
- </el-radio-group>
- </el-form-item>
- <el-form-item label="是否有攻击倾向">
- <el-switch v-model="petForm.aggression" active-text="是" inactive-text="否" />
- </el-form-item>
- <el-form-item label="疫苗情况">
- <el-input v-model="petForm.vaccine" type="textarea" placeholder="记录疫苗接种情况" />
- </el-form-item>
- <el-form-item label="既往病史">
- <el-input v-model="petForm.medicalHistory" type="textarea" placeholder="如有病史请记录" />
- </el-form-item>
- <el-form-item label="过敏史">
- <el-input v-model="petForm.allergies" type="textarea" placeholder="如有过敏源请记录" />
- </el-form-item>
- </el-form>
- </el-tab-pane>
- </el-tabs>
- <template #footer>
- <el-button @click="petDialogVisible = false">取消</el-button>
- <el-button type="primary" @click="submitPet">保存</el-button>
- </template>
- </el-dialog>
- </div>
- </template>
- <script setup>
- import { ref, reactive, computed, onMounted, watch } from 'vue'
- import { ElMessage } from 'element-plus'
- // --- Mock Data ---
- const merchants = ref([
- { id: 1, name: '萌它宠物三里屯店' },
- { id: 2, name: '宠爱国际动物医院' }
- ])
- const userOptions = ref([
- { id: 101, name: '张三', phone: '13812345678' },
- { id: 102, name: '李四', phone: '13987654321' }
- ])
- const mockPets = {
- 101: [
- { id: 1, name: '旺财', breed: '金毛', avatar: '', region: ['北京市', '市辖区', '朝阳区'], address: '三里屯SOHO A座 1001' },
- { id: 2, name: '咪咪', breed: '布偶', avatar: '', region: ['北京市', '市辖区', '朝阳区'], address: '三里屯SOHO A座 1001' }
- ],
- 102: [
- { id: 3, name: '奥利奥', breed: '边牧', avatar: '', region: ['上海市', '市辖区', '浦东新区'], address: '陆家嘴一号院 5-502' }
- ]
- }
- const serviceList = [
- { type: 'transport', name: '宠物接送', icon: 'Van', desc: '专车接送 · 全程监护', basePrice: 35 },
- { type: 'feeding', name: '上门喂遛', icon: 'Food', desc: '喂食添水 · 陪玩遛狗', basePrice: 68 },
- { type: 'washing', name: '上门洗护', icon: 'Soap', desc: '专业设备 · 深度清洁', basePrice: 88 }
- ]
- const allPackages = [
- { id: 10, type: 'transport', name: '包月接送套餐', price: 0 },
- { id: 11, type: 'feeding', name: '基础喂猫套餐', price: 0 },
- { id: 12, type: 'feeding', name: '深度陪玩套餐', price: 0 },
- { id: 13, type: 'washing', name: '精致洗护+美容', price: 0 },
- { id: 14, type: 'washing', name: '除菌药浴套餐', price: 0 },
- ]
- // --- State ---
- const userLoading = ref(false)
- const currentPets = ref([])
- const form = reactive({
- merchantId: '',
- userId: '',
- petId: '',
- type: 'transport',
- groupBuyPackage: '',
- // Sub Forms Data
- transport: {
- pkgId: '',
- price: 0,
- pickPrice: 35,
- dropPrice: 35,
- subType: 'round',
- pickRegion: [], pickDetail: '', pickContact: '', pickPhone: '', pickTime: '',
- dropRegion: [], dropDetail: '', dropContact: '', dropPhone: '', dropTime: ''
- },
- feeding: {
- pkgId: '', price: 68,
- appointments: [{ startTime: '', endTime: '' }],
- region: [], addressDetail: '',
- count: 1, dates: [], area: '', itemLoc: '', cleanLoc: '', foodAmount: '', other: ''
- },
- washing: {
- pkgId: '', price: 88,
- appointments: [{ startTime: '', endTime: '' }],
- region: [], addressDetail: '',
- time: '', petStatus: '', cleanLoc: '', toolLoc: '', other: ''
- }
- })
- // Address Autofill Watcher
- watch(() => form.petId, (newId) => {
- if (!newId) return
- const pet = currentPets.value.find(p => p.id === newId)
- if (!pet) return
- const user = userOptions.value.find(u => u.id === form.userId)
- // Fill Transport
- form.transport.pickRegion = pet.region || []
- form.transport.pickDetail = pet.address || ''
- form.transport.pickContact = user?.name || ''
- form.transport.pickPhone = user?.phone || ''
- form.transport.dropRegion = pet.region || []
- form.transport.dropDetail = pet.address || ''
- form.transport.dropContact = user?.name || ''
- form.transport.dropPhone = user?.phone || ''
- // Fill Feeding
- form.feeding.region = pet.region || []
- form.feeding.addressDetail = pet.address || ''
- // Fill Washing
- form.washing.region = pet.region || []
- form.washing.addressDetail = pet.address || ''
- })
- // Current Active Data Helper
- const activeData = computed(() => {
- return form[form.type]
- })
- // --- Logic ---
- const handleTypeChange = (type) => {
- form.type = type
- calcPrice(type)
- }
- const currentPackages = computed(() => {
- return allPackages.filter(p => p.type === form.type)
- })
- const handlePkgSelect = (id) => {
- activeData.value.pkgId = id
- // Price calculation should remain same (base price), just payable changes
- calcPrice(form.type)
- }
- const calcPrice = (type) => {
- const data = form[type]
- const base = serviceList.find(s => s.type === type)?.basePrice || 0
- // Always use Base Logic for "Order Value", regardless of package
- if (type === 'transport') {
- if(data.subType === 'round') {
- data.pickPrice = base
- data.dropPrice = base
- } else if (data.subType === 'pick') {
- data.pickPrice = base
- data.dropPrice = 0
- } else if (data.subType === 'drop') {
- data.pickPrice = 0
- data.dropPrice = base
- }
- } else if (type === 'feeding') {
- data.price = base * data.count
- } else if (type === 'washing') {
- data.price = base
- }
- }
- // Appointment Logic
- const addAppointment = (type) => {
- form[type].appointments.push({ startTime: '', endTime: '' })
- if(type === 'feeding') {
- form.feeding.count = form.feeding.appointments.length
- calcPrice('feeding')
- }
- }
- const removeAppointment = (type, index) => {
- if (form[type].appointments.length <= 1) return
- form[type].appointments.splice(index, 1)
- if(type === 'feeding') {
- form.feeding.count = form.feeding.appointments.length
- calcPrice('feeding')
- }
- }
- // Add User Logic
- const userDialogVisible = ref(false)
- const userSelectedTagIds = ref([])
- const allUserTags = [
- { id: 1, name: '优质客户', type: 'success' },
- { id: 2, name: '潜在流失', type: 'warning' },
- { id: 3, name: '黑名单', type: 'danger' }
- ]
- const pcaOptions = [
- {
- value: '北京市', label: '北京市',
- children: [
- { value: '市辖区', label: '市辖区', children: [ { value: '朝阳区', label: '朝阳区' }, { value: '海淀区', label: '海淀区' } ] }
- ]
- },
- {
- value: '上海市', label: '上海市',
- children: [
- { value: '市辖区', label: '市辖区', children: [ { value: '浦东新区', label: '浦东新区' }, { value: '徐汇区', label: '徐汇区' } ] }
- ]
- }
- ]
- const userForm = reactive({
- id: null, avatar: '', name: '', phone: '', gender: '男', address: '', detailAddress: '', region: [], remark: '',
- houseType: 'elevator', entryMethod: 'password', entryPassword: '', keyLocation: '',
- source: '平台录入', area: ''
- })
- const openAddUser = () => {
- userSelectedTagIds.value = []
- Object.assign(userForm, {
- id: null, avatar: '', name: '', phone: '', gender: '男', address: '', detailAddress: '', region: [], remark: '',
- houseType: 'elevator', entryMethod: 'password', entryPassword: '', keyLocation: '',
- source: '平台录入', area: ''
- })
- userDialogVisible.value = true
- }
- const handleUserAvatarChange = (uploadFile) => {
- userForm.avatar = URL.createObjectURL(uploadFile.raw)
- }
- const submitUser = () => {
- if(!userForm.name || !userForm.phone) {
- ElMessage.warning('请补全用户必填信息')
- return
- }
- const newUser = {
- id: Date.now(),
- name: userForm.name,
- phone: userForm.phone
- }
- userOptions.value.push(newUser)
- form.userId = newUser.id
- // Clear pets for new user
- currentPets.value = []
- form.petId = ''
- userDialogVisible.value = false
- ElMessage.success('用户添加成功并已选中')
- }
- // Add Pet Logic
- const petDialogVisible = ref(false)
- const activePetTab = ref('basic')
- const petForm = reactive({
- name: '', breed: '', gender: 'MM', avatar: '',
- bodyType: 'small', weight: 0, age: 0, keywords: '', desc: '', tags: [],
- // Family
- arrivalTime: '', houseType: 'stairs', entryMethod: 'key', entryPassword: '', keyLocation: '',
- // Health
- healthStatus: '健康', aggression: false, vaccine: '', medicalHistory: '', allergies: ''
- })
- const openAddPet = () => {
- activePetTab.value = 'basic'
- Object.assign(petForm, {
- name: '', breed: '', gender: 'MM', avatar: '',
- bodyType: 'small', weight: 0, age: 0, keywords: '', desc: '', tags: [],
- arrivalTime: '', houseType: 'stairs', entryMethod: 'key', entryPassword: '', keyLocation: '',
- healthStatus: '健康', aggression: false, vaccine: '', medicalHistory: '', allergies: ''
- })
- petDialogVisible.value = true
- }
- const handleAvatarChange = (uploadFile) => {
- // Mock upload: create local URL
- petForm.avatar = URL.createObjectURL(uploadFile.raw)
- }
- const submitPet = () => {
- if(!petForm.name || !petForm.breed) {
- ElMessage.warning('请补全宠物必填信息')
- return
- }
- const newPet = {
- id: Date.now(),
- name: petForm.name,
- breed: petForm.breed,
- avatar: petForm.avatar
- }
- if(!currentPets.value) currentPets.value = []
- currentPets.value.push(newPet)
- form.petId = newPet.id
- petDialogVisible.value = false
- ElMessage.success('宠物添加成功')
- }
- // --- Computed Helpers ---
- const selectedMerchantName = computed(() => merchants.value.find(m => m.id === form.merchantId)?.name)
- const selectedUserName = computed(() => userOptions.value.find(u => u.id === form.userId)?.name)
- const selectedPet = computed(() => currentPets.value.find(p => p.id === form.petId))
- const selectedPetName = computed(() => selectedPet.value?.name)
- const selectedPetBreed = computed(() => selectedPet.value?.breed)
- const selectedPkgName = computed(() => {
- const pkgId = activeData.value.pkgId
- return allPackages.find(p => p.id === pkgId)?.name || ''
- })
- const canSubmit = computed(() => {
- if(!form.merchantId || !form.userId || !form.petId) return false
- return true
- })
- // --- Methods ---
- const searchUser = (query) => { /* Mock */ }
- const handleUserChange = (val) => {
- currentPets.value = mockPets[val] || []
- form.petId = ''
- }
- const getStepTitle = (type) => {
- const map = { transport: '填写接送路线与时间', feeding: '选择套餐与服务的细则', washing: '选择套餐与服务的细则' }
- return map[type]
- }
- const getTypeName = (type) => {
- const map = { transport: '宠物接送', feeding: '上门喂遛', washing: '上门洗护' }
- return map[type]
- }
- const formatTime = (time) => {
- if(!time) return ''
- const d = new Date(time)
- return `${d.getMonth()+1}-${d.getDate()} ${d.getHours()}:${d.getMinutes() < 10 ? '0'+d.getMinutes() : d.getMinutes()}`
- }
- const handleSubmit = () => {
- ElMessage.success('下单成功!订单号:ORD20248888')
- }
- // Initialize
- onMounted(() => {
- calcPrice('transport')
- })
- </script>
- <style scoped>
- .page-container { padding: 20px; background-color: #f0f2f5; min-height: 100vh; }
- .create-layout { display: flex; gap: 20px; align-items: flex-start; max-width: 1400px; margin: 0 auto; }
- /* Left Content */
- .form-container { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 20px; }
- .section-card { border-radius: 8px; border: none; }
- .card-title { font-size: 16px; font-weight: bold; color: #303133; display: flex; align-items: center; gap: 10px; }
- .step-num {
- background: #e6f7ff; color: #1890ff; width: 28px; height: 28px; border-radius: 50%;
- text-align: center; line-height: 28px; font-family: Impact, sans-serif;
- }
- .base-form .el-form-item { margin-bottom: 18px; }
- /* Pet Selection */
- /* Pet Selection */
- .pet-select-row {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
- gap: 15px;
- width: 100%;
- }
- .pet-card {
- border: 1px solid #8D9095;
- border-radius: 8px;
- padding: 12px 15px;
- cursor: pointer;
- display: flex;
- align-items: center;
- gap: 12px;
- position: relative;
- transition: all 0.2s ease-in-out;
- background: #fff;
- min-height: 70px;
- }
- .pet-card:hover {
- border-color: #303133;
- transform: translateY(-2px);
- box-shadow: 0 4px 12px rgba(0,0,0,0.08);
- }
- .pet-card.active {
- border-color: #409eff;
- background-color: #fff;
- box-shadow: 0 0 0 1px #409eff inset;
- }
- .check-mark {
- position: absolute;
- right: 0;
- top: 0;
- background: #409eff;
- color: white;
- width: 28px;
- height: 18px;
- border-radius: 0 8px 0 12px;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 12px;
- }
- .pet-info .name { font-weight: bold; font-size: 15px; color: #303133; margin-bottom: 2px; }
- .pet-info .sub { font-size: 12px; color: #606266; line-height: 1.2; }
- .pet-card.add-card {
- border: 1px solid #8D9095;
- justify-content: center;
- align-items: center;
- color: #303133;
- flex-direction: row;
- gap: 8px;
- background: #fff;
- box-shadow: none;
- height: auto;
- min-height: 70px;
- }
- .pet-card.add-card:hover {
- border-color: #303133;
- color: #303133;
- background: #f9f9f9;
- transform: translateY(-2px);
- }
- /* Dialog Styles */
- .pet-form-content { display: flex; gap: 20px; }
- .avatar-col { width: 120px; display: flex; flex-direction: column; align-items: center; padding-top: 10px; }
- .avatar-uploader {
- display: inline-block;
- }
- .avatar-uploader .el-upload {
- border: 1px dashed #d9d9d9;
- border-radius: 6px;
- cursor: pointer;
- position: relative;
- overflow: hidden;
- transition: var(--el-transition-duration-fast);
- }
- .avatar-uploader .el-upload:hover {
- border-color: var(--el-color-primary);
- }
- .avatar-uploader-icon {
- font-size: 28px;
- color: #8c939d;
- width: 100px;
- height: 100px;
- text-align: center;
- border: 1px dashed #d9d9d9;
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- }
- .avatar {
- width: 100px;
- height: 100px;
- display: block;
- border-radius: 50%;
- object-fit: cover;
- }
- .inner-form { flex: 1; }
- /* Type Selection */
- .type-selection { display: grid; grid-template-columns: repeat(3, 1fr); gap: 15px; }
- .type-card {
- background: white; border-radius: 8px; padding: 20px; cursor: pointer; position: relative;
- display: flex; align-items: center; gap: 15px; transition: all 0.2s;
- border: 2px solid transparent; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
- }
- .type-card:hover { transform: translateY(-2px); box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.1); }
- .type-card.active { border-color: #409eff; background-color: #f0f9ff; }
- .type-card .icon-box {
- width: 48px; height: 48px; border-radius: 12px; background: #f2f3f5;
- display: flex; align-items: center; justify-content: center; font-size: 24px; color: #606266;
- }
- .type-card.active .icon-box { background: #409eff; color: white; }
- /* Colors */
- .type-card.transport.active .icon-box { background: #409eff; }
- .type-card.transport.active { border-color: #409eff; background-color: #f0f9ff; }
- .type-card.feeding.active .icon-box { background: #e6a23c; }
- .type-card.feeding.active { border-color: #e6a23c; background-color: #fdf6ec; }
- .type-card.washing.active .icon-box { background: #67c23a; }
- .type-card.washing.active { border-color: #67c23a; background-color: #f0f9eb; }
- .type-name { font-weight: bold; font-size: 16px; color: #303133; margin-bottom: 4px; }
- .type-desc { font-size: 12px; color: #909399; margin-bottom: 4px; }
- .type-price { font-size: 14px; color: #f56c6c; font-weight: bold; }
- /* Package Selection Grid */
- .form-section-title { font-weight: bold; margin-bottom: 12px; font-size: 14px; }
- .package-selection-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 20px; }
- .pkg-select-card {
- border: 1px solid #dcdfe6; border-radius: 8px; padding: 10px 15px; cursor: pointer; position: relative;
- background: #fff; transition: all 0.2s; min-height: 56px; display: flex; flex-direction: column; justify-content: center;
- }
- .pkg-select-card:hover { border-color: #409eff; }
- .pkg-select-card.active { border-color: #409eff; background-color: #ecf5ff; }
- .pkg-select-card .pkg-name { font-weight: bold; font-size: 14px; color: #303133; }
- .pkg-select-card .pkg-desc { font-size: 12px; color: #909399; margin-top: 2px; }
- /* Business Form */
- .business-form { padding-top: 5px; }
- .route-box { background: #f9f9f9; padding: 20px; border-radius: 8px; border: 1px solid #EBEEF5; }
- .route-segment { display: flex; gap: 15px; }
- .seg-badge {
- width: 32px; height: 32px; background: #409eff; color: white; border-radius: 8px;
- text-align: center; line-height: 32px; font-weight: bold; flex-shrink: 0;
- }
- .seg-badge.end { background: #67c23a; }
- .seg-content { flex: 1; display: flex; flex-direction: column; gap: 10px; }
- .route-connector { display: flex; align-items: center; justify-content: center; margin: 15px 0; gap: 10px; color: #909399; font-size: 12px; }
- .route-connector .line { height: 1px; width: 80px; background: #dcdfe6; }
- .route-connector .store-node { background: white; padding: 4px 12px; border-radius: 20px; border: 1px solid #dcdfe6; display: flex; align-items: center; gap: 5px; }
- .divider { height: 1px; background: #EBEEF5; margin: 15px 0; }
- .remark-section { background: #fdfdfd; border: 1px dashed #dcdfe6; padding: 15px; border-radius: 6px; margin-top: 20px; }
- .section-label { font-size: 13px; font-weight: bold; color: #606266; margin-bottom: 12px; }
- .tip { font-size: 12px; color: #e6a23c; margin-top: 4px; }
- /* Sidebar */
- .summary-sidebar { width: 320px; flex-shrink: 0; }
- .summary-panel { background: white; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.05); position: sticky; top: 20px; }
- .summary-header { background: #304156; color: white; padding: 15px 20px; font-weight: bold; font-size: 16px; border-radius: 8px 8px 0 0; }
- .summary-content { padding: 20px; }
- .row { display: flex; justify-content: space-between; margin-bottom: 12px; font-size: 14px; }
- .row .label { color: #909399; }
- .row .value { color: #303133; font-weight: 500; }
- .preview-title { font-weight: bold; margin-bottom: 8px; color: #333; }
- .preview-detail { background: #f8f9fa; padding: 10px; border-radius: 4px; font-size: 13px; margin-bottom: 8px; }
- .preview-detail .minor { color: #999; font-size: 12px; margin-top: 2px; }
- .placeholder { color: #C0C4CC; text-align: center; padding: 20px 0; font-size: 13px; font-style: italic; }
- .summary-footer { background: #f9f9fc; padding: 15px 20px; border-top: 1px solid #ebeef5; text-align: center; border-radius: 0 0 8px 8px; }
- .submit-btn { width: 100%; font-weight: bold; border-radius: 22px; }
- </style>
|