| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602 |
- <template>
- <view class="order-apply-page">
- <nav-bar title="下单预约"></nav-bar>
-
- <view class="apply-content">
- <!-- 01 服务类型 -->
- <text class="section-title">01 服务类型</text>
- <view class="card service-info-card">
- <view class="service-type-display">
- <view :class="['service-icon-box', activeService]">
- <!-- CSS 手绘服务图标 @Author: Antigravity -->
- <view class="pure-css-icon" :class="serviceIconClass"></view>
- </view>
- <view class="service-info-text">
- <text class="main-name">{{ currentServiceName }}</text>
- <text class="sub-desc">{{ serviceDesc }}</text>
- </view>
- </view>
- </view>
- <!-- 02 基础信息 -->
- <text class="section-title">02 基础信息</text>
- <view class="card basic-info-card">
- <view class="field-item" @click="showShopSelect = true">
- <text class="field-label require">服务门店</text>
- <text :class="['field-value', !formData.shopName ? 'placeholder' : '']">{{ formData.shopName || '请选择商户门店' }}</text>
- <view class="right-arrow"></view>
- </view>
- <view class="field-item" @click="showUserSelect = true">
- <text class="field-label require">宠主用户</text>
- <view class="field-value-wrap">
- <template v-if="selectedUser">
- <text class="selected-name">{{ selectedUser.name }}</text>
- <text class="selected-phone">{{ selectedUser.phone || selectedUser.phoneNumber }}</text>
- </template>
- <text v-else class="placeholder">点击搜索</text>
- </view>
- <view class="right-arrow"></view>
- </view>
- <view class="field-item" @click="openPetPicker">
- <text class="field-label require">选择宠物</text>
- <text :class="['field-value', !formData.petName ? 'placeholder' : '']">{{ formData.petName || '选择宠物档案' }}</text>
- <view class="right-arrow"></view>
- </view>
- </view>
- <!-- 03 业务表单 - 宠物接送 -->
- <template v-if="activeService === 'transport'">
- <text class="section-title">03 接送路线与时间</text>
- <view class="card transport-card">
- <view class="field-item">
- <text class="field-label">团购套餐</text>
- <input class="field-input" v-model="formData.packageName" placeholder="请输入套餐名称(选填)" />
- </view>
-
- <text class="form-item-label require">接送模式</text>
- <view class="mode-select">
- <view v-for="mode in transportModes" :key="mode.value"
- :class="['mode-btn', { active: formData.transportMode === mode.value }]"
- @click="formData.transportMode = mode.value">
- <text>{{ mode.label }}</text>
- </view>
- </view>
- <!-- 接宠路线 @Author: Antigravity -->
- <view class="route-box" v-if="formData.transportMode !== 'return_home'">
- <view class="route-icon pick">接</view>
- <view class="route-fields">
- <text class="addr-label require">起点 (用户家)</text>
- <view class="route-picker-trigger" @click="openRegionSelect('pick')">
- <text :class="['display-text', !formData.pickArea ? 'placeholder' : '']">{{ pickAreaLabel || '选择省/市/区' }}</text>
- <view class="right-arrow"></view>
- </view>
- <input class="route-input" v-model="formData.pickAddress" placeholder="详细地址" />
-
- <text class="addr-label require">终点 (门店)</text>
- <view class="route-picker-trigger" @click="openRegionSelect('pickEnd')">
- <text :class="['display-text', !formData.pickEndArea ? 'placeholder' : '']">{{ pickEndAreaLabel || '选择省/市/区' }}</text>
- <view class="right-arrow"></view>
- </view>
- <input class="route-input" v-model="formData.pickEndAddress" placeholder="详细地址" />
-
- <view class="contact-row">
- <input class="route-input half" v-model="formData.pickContact" placeholder="联系人" />
- <input class="route-input half" v-model="formData.pickPhone" placeholder="电话" type="tel" />
- </view>
- <view class="route-time-trigger" @click="openTimeModal('pick')">
- <text :class="!formData.pickTime ? 'placeholder' : ''">{{ pickTimeDisplay || '设置接宠时间' }}</text>
- </view>
- </view>
- </view>
- <!-- 送宠路线 @Author: Antigravity -->
- <view class="route-box" v-if="formData.transportMode !== 'pick_up'">
- <view class="route-icon send">送</view>
- <view class="route-fields">
- <text class="addr-label require">起点 (门店)</text>
- <view class="route-picker-trigger" @click="openRegionSelect('sendStart')">
- <text :class="['display-text', !formData.sendStartArea ? 'placeholder' : '']">{{ sendStartAreaLabel || '选择省/市/区' }}</text>
- <view class="right-arrow"></view>
- </view>
- <input class="route-input" v-model="formData.sendStartAddress" placeholder="详细地址" />
-
- <text class="addr-label require">终点 (用户家)</text>
- <view class="route-picker-trigger" @click="openRegionSelect('send')">
- <text :class="['display-text', !formData.sendArea ? 'placeholder' : '']">{{ sendAreaLabel || '选择省/市/区' }}</text>
- <view class="right-arrow"></view>
- </view>
- <input class="route-input" v-model="formData.sendAddress" placeholder="详细地址" />
-
- <view class="contact-row">
- <input class="route-input half" v-model="formData.sendContact" placeholder="联系人" />
- <input class="route-input half" v-model="formData.sendPhone" placeholder="电话" type="tel" />
- </view>
- <view class="route-time-trigger" @click="openTimeModal('send')">
- <text :class="!formData.sendTime ? 'placeholder' : ''">{{ sendTimeDisplay || '设置送宠时间' }}</text>
- </view>
- </view>
- </view>
- <!-- 接送备注 -->
- <text class="remarks-title">备注信息</text>
- <textarea class="remarks-textarea" v-model="formData.transportNote" placeholder="请添加接送备注 (如宠物性格、接送要求等)"></textarea>
- </view>
- </template>
- <!-- 03 业务表单 - 上门 -->
- <template v-else>
- <text class="section-title">03 服务细则</text>
- <view class="card feed-card">
- <view class="field-item">
- <text class="field-label">团购套餐</text>
- <input class="field-input" v-model="formData.packageName" placeholder="请输入套餐名称(选填)" />
- </view>
- <view class="route-box">
- <view class="route-icon service">服</view>
- <view class="route-fields">
- <text class="addr-label require">上门服务地址</text>
- <view class="route-picker-trigger" @click="openRegionSelect('service')">
- <text :class="['display-text', !formData.serviceArea ? 'placeholder' : '']">{{ serviceAreaLabel || '请选择省/市/区' }}</text>
- <view class="right-arrow"></view>
- </view>
- <input class="route-input" v-model="formData.serviceAddress" placeholder="详细地址 (街道/路名/门牌号)" />
- </view>
- </view>
- <view class="booking-section">
- <view class="booking-header">
- <text class="label require">预约服务时间</text>
- <view class="count-tag">共 {{ formData.feedTimes.length }} 次</view>
- </view>
- <view class="time-item-row" v-for="(time, index) in formData.feedTimes" :key="index">
- <text class="index">{{ index + 1 }}.</text>
- <view class="flex-time-box" @click="openTimeModal('feed', index, 'start')">
- <text :class="['time-text', !time.start ? 'placeholder' : '']">{{ truncateTime(time.start) || '开始' }}</text>
- </view>
- <text class="to-line">~</text>
- <view class="flex-time-box" @click="openTimeModal('feed', index, 'end')">
- <text :class="['time-text', !time.end ? 'placeholder' : '']">{{ truncateTime(time.end) || '结束' }}</text>
- </view>
- <view class="action-buttons">
- <view class="circle-btn add" v-if="index === formData.feedTimes.length - 1" @click="addFeedTime">+</view>
- <view class="circle-btn remove" v-if="formData.feedTimes.length > 1" @click="removeFeedTime(index)">-</view>
- </view>
- </view>
- </view>
- <text class="remarks-title">备注信息</text>
- <textarea class="remarks-textarea" v-model="formData.otherNote" placeholder="如有其他注意事项请备注"></textarea>
- </view>
- </template>
- <!-- 04 报价信息 -->
- <text class="section-title">04 报价信息</text>
- <view class="card quote-card">
- <view class="field-item">
- <text class="field-label require">报价金额</text>
- <input class="field-input quote-input" v-model="formData.quoteAmount" type="digit" placeholder="填入数字" />
- <text class="unit-text">元</text>
- </view>
- <text class="quote-tips">注:此价格将作为订单最终结算金额。</text>
- </view>
- </view>
- <!-- 底部操作栏 -->
- <view class="footer-bar safe-bottom">
- <view class="quotation-box">
- <text class="p-label">总计报价:</text>
- <text class="p-symbol">¥</text>
- <text class="p-amount">{{ totalFulfillmentCommission }}</text>
- </view>
- <button class="submit-btn" @click="onSubmit">立即下单</button>
- </view>
- <!-- 居中联动选择弹窗群 @Author: Antigravity -->
-
- <!-- 宠主搜索弹窗 -->
- <view class="center-modal-mask" v-if="showUserSelect" @click="showUserSelect = false">
- <view class="center-modal-content user-search-modal" @click.stop>
- <view class="modal-header">
- <view class="search-box">
- <view class="search-icon"></view>
- <input class="search-input" v-model="userSearchKey" placeholder="搜索宠主姓名/手机号" @confirm="fetchUsers" confirm-type="search" />
- <view class="search-btn" @click="fetchUsers">查询</view>
- </view>
- </view>
- <scroll-view scroll-y class="modal-list-scroll">
- <view class="list-item" v-for="user in userList" :key="user.id" @click="onUserSelect(user)">
- <view class="user-info">
- <text class="name">{{ user.name }}</text>
- <text class="phone">{{ user.phone || user.phoneNumber }}</text>
- </view>
- <view class="checkmark" v-if="formData.customerId === user.id"></view>
- </view>
- <view class="empty-tip" v-if="userList.length === 0">未找到相关宠主</view>
- </scroll-view>
- </view>
- </view>
-
- <!-- 区域选择器 (Cascader) @Author: Antigravity -->
- <view class="center-modal-mask" v-if="showRegionModal" @click="showRegionModal = false">
- <view class="center-modal-content region-modal" @click.stop>
- <view class="modal-header"><text class="modal-title">选择区域</text><view class="close-btn" @click="showRegionModal = false"></view></view>
- <view class="cascade-indicator">
- <text v-for="(node, idx) in regionPath" :key="idx" class="path-node" @click="backToLevel(idx)">{{ node.name }}</text>
- <text class="path-node active" v-if="regionPath.length < 3">请选择</text>
- </view>
- <scroll-view scroll-y class="modal-list-scroll">
- <view class="list-item" v-for="item in currentRegionList" :key="item.code" @click="onRegionStepSelect(item)">
- <text class="item-text">{{ item.name }}</text>
- <view class="checkmark" v-if="isRegionSelected(item)"></view>
- </view>
- </scroll-view>
- </view>
- </view>
- <!-- 门店选择 -->
- <center-select v-model="showShopSelect" title="选择服务门店" :options="shopList" labelKey="name" valueKey="id" :value="formData.merchantId" @select="onShopSelect" />
-
- <!-- 宠物选择 -->
- <center-select v-model="showPetPopup" title="选择指定宠物" :options="petOptions" labelKey="_label" valueKey="id" :value="formData.petId" @select="onPetSelect" />
- <!-- 日期时间选择弹窗 @Author: Antigravity -->
- <view class="center-modal-mask" v-if="showTimeModal" @click="showTimeModal = false">
- <view class="center-modal-content time-modal" @click.stop>
- <view class="modal-header"><text class="modal-title">选择预约时间</text></view>
- <view class="datetime-picker-body">
- <picker-view class="picker-view" :value="tempTimeIdx" @change="onTempTimeChange">
- <picker-view-column><view class="picker-item" v-for="d in timeRanges[0]" :key="d">{{ d }}</view></picker-view-column>
- <picker-view-column><view class="picker-item" v-for="h in timeRanges[1]" :key="h">{{ h }}时</view></picker-view-column>
- <picker-view-column><view class="picker-item" v-for="m in timeRanges[2]" :key="m">{{ m }}分</view></picker-view-column>
- </picker-view>
- </view>
- <view class="modal-footer">
- <view class="modal-cancel" @click="showTimeModal = false">取消</view>
- <view class="modal-confirm" @click="confirmTime">确定</view>
- </view>
- </view>
- </view>
- </view>
- </template>
- <script setup>
- /**
- * @Author: Antigravity
- */
- import { ref, reactive, computed, watch } from 'vue'
- import { onLoad } from '@dcloudio/uni-app'
- import navBar from '@/components/nav-bar/index.vue'
- import centerSelect from '@/components/center-select/index.vue'
- import { listStoreOnOrder } from '@/api/system/store'
- import { listCustomerOnOrder } from '@/api/archieves/customer'
- import { listPetByUser } from '@/api/archieves/pet'
- import { createOrder } from '@/api/order/order'
- import { listRegionTree } from '@/api/system/region'
- const activeService = ref('transport')
- const serviceInfo = ref(null)
- const shopList = ref([])
- const userList = ref([])
- const petList = ref([])
- const regionTree = ref([])
- // 弹窗控制
- const showShopSelect = ref(false)
- const showUserSelect = ref(false)
- const showPetPopup = ref(false)
- const showRegionModal = ref(false)
- const showTimeModal = ref(false)
- const userSearchKey = ref('')
- const selectedUser = ref(null)
- const selectedShop = ref(null)
- const pickAreaLabel = ref('')
- const pickEndAreaLabel = ref('')
- const sendStartAreaLabel = ref('')
- const sendAreaLabel = ref('')
- const serviceAreaLabel = ref('')
- const formData = reactive({
- merchantId: '', shopName: '', customerId: '', customerName: '', petId: '', petName: '',
- packageName: '', transportMode: 'round_trip',
- pickArea: '', pickAddress: '', pickEndArea: '', pickEndAddress: '', pickContact: '', pickPhone: '', pickTime: '',
- sendStartArea: '', sendStartAddress: '', sendArea: '', sendAddress: '', sendContact: '', sendPhone: '', sendTime: '',
- serviceArea: '', serviceAddress: '', feedTimes: [{ start: '', end: '' }],
- otherNote: '', transportNote: '', quoteAmount: ''
- })
- // 时间选择器逻辑 (5分钟一个间隔 @Author: Antigravity)
- const timeRanges = ref([[], [], []])
- const tempTimeIdx = ref([0, 0, 0])
- const timeCtx = reactive({ type: '', index: 0, field: '' })
- onLoad((options) => {
- if (options.service) activeService.value = options.service
- const stored = uni.getStorageSync('currentService')
- if (stored) serviceInfo.value = stored
- initTimeRanges()
- fetchShops(); fetchUsers(); fetchRegionTree()
- })
- const initTimeRanges = () => {
- const dates = []
- const now = new Date()
- for (let i = 0; i < 30; i++) {
- const d = new Date(now); d.setDate(d.getDate() + i)
- dates.push(`${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`)
- }
- timeRanges.value = [
- dates,
- Array.from({ length: 24 }, (_, i) => String(i).padStart(2, '0')),
- // 五分钟间隔生成 @Author: Antigravity
- Array.from({ length: 12 }, (_, i) => String(i * 5).padStart(2, '0'))
- ]
- }
- const openTimeModal = (type, index = 0, field = '') => {
- timeCtx.type = type; timeCtx.index = index; timeCtx.field = field
- tempTimeIdx.value = [0, 0, 0]
- showTimeModal.value = true
- }
- const onTempTimeChange = (e) => { tempTimeIdx.value = e.detail.value }
- const confirmTime = () => {
- const [di, hi, mi] = tempTimeIdx.value
- const val = `${new Date().getFullYear()}-${timeRanges.value[0][di]} ${timeRanges.value[1][hi]}:${timeRanges.value[2][mi]}:00`
- if (timeCtx.type === 'pick') formData.pickTime = val
- else if (timeCtx.type === 'send') formData.sendTime = val
- else if (timeCtx.type === 'feed') {
- if (timeCtx.field === 'start') formData.feedTimes[timeCtx.index].start = val
- else formData.feedTimes[timeCtx.index].end = val
- }
- showTimeModal.value = false
- }
- const truncateTime = (t) => t ? t.substring(5, 16) : ''
- const pickTimeDisplay = computed(() => truncateTime(formData.pickTime))
- const sendTimeDisplay = computed(() => truncateTime(formData.sendTime))
- // 区域选择逻辑
- const regionPath = ref([])
- const activeRegionType = ref('')
- const currentRegionList = computed(() => {
- let list = regionTree.value
- for (let node of regionPath.value) {
- const found = list.find(l => l.code === node.code)
- if (found && found.children) list = found.children
- else list = []
- }
- return list
- })
- const openRegionSelect = (type) => {
- activeRegionType.value = type
- regionPath.value = []
- showRegionModal.value = true
- }
- const backToLevel = (idx) => { regionPath.value = regionPath.value.slice(0, idx) }
- const onRegionStepSelect = (item) => {
- regionPath.value.push({ code: item.code, name: item.name })
- if (!item.children || item.children.length === 0 || regionPath.value.length >= 3) {
- const fullLabel = regionPath.value.map(p => p.name).join(' / ')
- const finalCode = item.code
- if (activeRegionType.value === 'pick') { formData.pickArea = finalCode; pickAreaLabel.value = fullLabel }
- else if (activeRegionType.value === 'pickEnd') { formData.pickEndArea = finalCode; pickEndAreaLabel.value = fullLabel }
- else if (activeRegionType.value === 'sendStart') { formData.sendStartArea = finalCode; sendStartAreaLabel.value = fullLabel }
- else if (activeRegionType.value === 'send') { formData.sendArea = finalCode; sendAreaLabel.value = fullLabel }
- else if (activeRegionType.value === 'service') { formData.serviceArea = finalCode; serviceAreaLabel.value = fullLabel }
- showRegionModal.value = false
- }
- }
- const isRegionSelected = (item) => {
- const level = regionPath.value.length
- return regionPath.value[level]?.code === item.code
- }
- // 核心回填逻辑修正 @Author: Antigravity
- watch([selectedShop, selectedUser, regionTree], ([shop, user, tree]) => {
- if (!shop && !user) return
-
- // 处理门店信息
- const storeAreaCode = (shop?.areaCode || '').replace(/,/g, '/')
- const storeLeaf = storeAreaCode.split('/').pop() || ''
- const storePath = findRegionLabel(storeAreaCode, tree)
- // 处理用户信息
- const userAreaCode = (user?.regionCode || '')
- const userLeaf = userAreaCode.split('/').pop() || ''
- const userPath = findRegionLabel(userAreaCode, tree)
-
- if (shop) {
- formData.merchantId = shop.id; formData.shopName = shop.name
- // 接送单终点 = 门店
- formData.pickEndArea = storeLeaf; formData.pickEndAddress = shop.address || ''
- pickEndAreaLabel.value = storePath
- // 接送单起点 = 门店
- formData.sendStartArea = storeLeaf; formData.sendStartAddress = shop.address || ''
- sendStartAreaLabel.value = storePath
- }
- if (user) {
- formData.customerId = user.id; formData.customerName = user.name
- // 接宠单起点 = 宠主家
- formData.pickArea = userLeaf; formData.pickAddress = user.address || ''
- pickAreaLabel.value = userPath
- // 送宠单终点 = 宠主家
- formData.sendArea = userLeaf; formData.sendAddress = user.address || ''
- sendAreaLabel.value = userPath
- // 服务单地址 = 宠主家
- formData.serviceArea = userLeaf; formData.serviceAddress = user.address || ''
- serviceAreaLabel.value = userPath
- formData.pickContact = user.name; formData.pickPhone = user.phoneNumber || user.phone || ''
- formData.sendContact = user.name; formData.sendPhone = formData.pickPhone
- }
- }, { deep: true })
- const findRegionLabel = (code, list) => {
- if (!code || !list || list.length === 0) return ''
- const target = code.split('/').pop()
- const find = (nodes, t) => {
- for (let n of nodes) {
- if (n.code === t) return n.name
- if (n.children) {
- const res = find(n.children, t)
- if (res) return n.name + ' / ' + res
- }
- }
- return null
- }
- return find(list, target) || ''
- }
- const fetchShops = () => listStoreOnOrder({ pageNum: 1, pageSize: 50, serviceId: serviceInfo.value?.id }).then(res => { shopList.value = res.rows || [] })
- const fetchUsers = () => listCustomerOnOrder({ pageNum: 1, pageSize: 20, content: userSearchKey.value }).then(res => { userList.value = res.rows || [] })
- const fetchPets = (uid) => listPetByUser(uid).then(res => { petList.value = Array.isArray(res) ? res : (res.rows || []) })
- const fetchRegionTree = () => listRegionTree().then(res => { regionTree.value = res || [] })
- const onShopSelect = (shop) => { selectedShop.value = shop; showShopSelect.value = false }
- const onUserSelect = (user) => {
- selectedUser.value = user; formData.customerId = user.id;
- formData.petId = ''; formData.petName = ''; petList.value = []; fetchPets(user.id)
- showUserSelect.value = false
- }
- const openPetPicker = () => { if (!formData.customerId) return uni.showToast({ title: '先选择宠主', icon: 'none' }); showPetPopup.value = true }
- const petOptions = computed(() => petList.value.map(p => ({ ...p, _label: `${p.name} (${p.breed || '未知'})` })))
- const onPetSelect = (pet) => { formData.petId = pet.id; formData.petName = pet.name; showPetPopup.value = false }
- const currentServiceName = computed(() => serviceInfo.value?.name || (activeService.value === 'transport' ? '宠物接送' : '上门喂遛'))
- const serviceIconClass = computed(() => activeService.value)
- const serviceDesc = computed(() => serviceInfo.value?.remark || '专人专项 · 贴心呵护')
- const transportModes = [{ label: '往返', value: 'round_trip' }, { label: '单程接', value: 'pick_up' }, { label: '单程送', value: 'return_home' }]
- const addFeedTime = () => formData.feedTimes.push({ start: '', end: '' })
- const removeFeedTime = (idx) => formData.feedTimes.splice(idx, 1)
- const totalFulfillmentCommission = computed(() => formData.quoteAmount ? parseFloat(formData.quoteAmount).toFixed(2) : '0.00')
- const onSubmit = async () => {
- if (!formData.merchantId || !formData.customerId || !formData.petId || !formData.quoteAmount) return uni.showToast({ title: '请完善红星必填项', icon: 'none' })
- uni.showLoading({ title: '提交中...', mask: true })
- try {
- const subOrders = []
- const baseMode = serviceInfo.value?.mode || 0
- const defC = selectedUser.value?.name; const defP = selectedUser.value?.phone || selectedUser.value?.phoneNumber
- if (activeService.value === 'transport') {
- if (formData.transportMode !== 'return_home') subOrders.push({ mode: baseMode, type: formData.transportMode === 'round_trip' ? 0 : 2, contact: formData.pickContact || defC, contactPhoneNumber: formData.pickPhone || defP, serviceTime: formData.pickTime, endServiceTime: formData.pickTime, fromCode: formData.pickArea, fromAddress: formData.pickAddress, toCode: formData.pickEndArea, toAddress: formData.pickEndAddress })
- if (formData.transportMode !== 'pick_up') subOrders.push({ mode: baseMode, type: formData.transportMode === 'round_trip' ? 1 : 3, contact: formData.sendContact || defC, contactPhoneNumber: formData.sendPhone || defP, serviceTime: formData.sendTime, endServiceTime: formData.sendTime, fromCode: formData.sendStartArea, fromAddress: formData.sendStartAddress, toCode: formData.sendArea, toAddress: formData.sendAddress })
- } else {
- formData.feedTimes.forEach(t => subOrders.push({ mode: baseMode, contact: defC, contactPhoneNumber: defP, serviceTime: t.start, endServiceTime: t.end || t.start, fromCode: formData.serviceArea, fromAddress: formData.serviceAddress, toCode: formData.serviceArea, toAddress: formData.serviceAddress }))
- }
- // 根据服务类型选择对应备注字段(与Web端逻辑对齐:transport用transportNote,其他用otherNote)
- const orderRemark = activeService.value === 'transport' ? formData.transportNote : formData.otherNote
- const payload = { store: formData.merchantId, storeSite: selectedShop.value?.site, customer: formData.customerId, pet: formData.petId, groupPurchasePackageName: formData.packageName, service: serviceInfo.value?.id, orderCommission: Math.round(Number(formData.quoteAmount) * 100), remark: orderRemark, tenantId: selectedShop.value?.tenantId, subOrders }
- await createOrder(payload)
- uni.showToast({ title: '成功', icon: 'success' })
- setTimeout(() => uni.reLaunch({ url: '/pages/order/list/index' }), 1000)
- } catch (e) { } finally { uni.hideLoading() }
- }
- </script>
- <style lang="scss" scoped>
- /* 统一页面字体栈 @Author: Antigravity */
- .order-apply-page {
- background: #f7f8fa;
- min-height: 100vh;
- padding-bottom: 220rpx;
- font-family: 'PingFang SC', 'Helvetica Neue', Helvetica, 'STHeitiSTXihei', 'Microsoft YaHei', Arial, sans-serif;
- }
- .apply-content { padding: 0 28rpx; }
- .section-title { display: flex; align-items: center; font-size: 28rpx; font-weight: bold; color: #333; margin: 32rpx 0 20rpx; &::before { content: ''; width: 8rpx; height: 26rpx; background: #f7ca3e; margin-right: 16rpx; border-radius: 4rpx; } }
- .card { background: #fff; border-radius: 24rpx; padding: 24rpx; margin-bottom: 24rpx; }
- .service-type-display { display: flex; align-items: center; gap: 24rpx; }
- .service-icon-box { width: 88rpx; height: 88rpx; border-radius: 20rpx; display: flex; align-items: center; justify-content: center; }
- .service-icon-box.transport { background: linear-gradient(135deg, #64b5f6, #2196f3); }
- .service-icon-box.feed { background: linear-gradient(135deg, #ffb74d, #ff9800); }
- .service-icon-box.wash { background: linear-gradient(135deg, #81c784, #4caf50); }
- .main-name {
- display: block;
- font-size: 32rpx;
- font-weight: bold;
- color: #333;
- }
- .sub-desc {
- display: block;
- font-size: 24rpx;
- color: #999;
- margin-top: 4rpx;
- }
- /* CSS 手绘图标 @Author: Antigravity */
- .pure-css-icon { width: 40rpx; height: 40rpx; border: 4rpx solid #fff; border-radius: 8rpx; position: relative; &::after { content: ''; position: absolute; top: 10rpx; left: 10rpx; width: 12rpx; height: 12rpx; background: #fff; border-radius: 50%; } }
- .field-item { display: flex; align-items: center; padding: 28rpx 0; border-bottom: 2rpx solid #f5f5f5; height: 44rpx; &:last-child { border-bottom: none; } }
- .field-label { width: 180rpx; font-size: 28rpx; color: #333; flex-shrink: 0; }
- .require::before { content: '*'; color: #f56c6c; margin-right: 4rpx; }
- .field-value { flex: 1; font-size: 28rpx; color: #333; text-align: right; margin-right: 16rpx; }
- .field-value.placeholder { color: #ccc; }
- .field-value-wrap { flex: 1; display: flex; flex-direction: column; align-items: flex-end; margin-right: 16rpx; .selected-name { font-size: 28rpx; font-weight: bold; color: #333; } .selected-phone { font-size: 22rpx; color: #999; } }
- .placeholder { color: #ccc; font-size: 28rpx; }
- .mode-select { display: flex; gap: 16rpx; margin: 20rpx 0 32rpx; }
- .mode-btn { flex: 1; height: 60rpx; display: flex; align-items: center; justify-content: center; border: 2rpx solid #f0f0f0; border-radius: 30rpx; font-size: 24rpx; color: #666; &.active { background: #fef8e5; border-color: #f7ca3e; color: #f7ca3e; font-weight: bold; } }
- .route-box { display: flex; gap: 20rpx; margin-bottom: 30rpx; }
- .route-icon { width: 44rpx; height: 44rpx; border-radius: 8rpx; color: #fff; display: flex; align-items: center; justify-content: center; font-size: 22rpx; font-weight: bold; flex-shrink: 0; margin-top: 10rpx; }
- .route-icon.pick { background: #5bb7ff; }
- .route-icon.send { background: #64cf5c; }
- .route-icon.service { background: #ff9500; }
- .route-fields { flex: 1; display: flex; flex-direction: column; gap: 6rpx; }
- .addr-label { font-size: 22rpx; color: #999; margin-top: 10rpx; }
- .route-picker-trigger { height: 64rpx; border-bottom: 2rpx solid #f5f5f5; display: flex; align-items: center; justify-content: space-between; .display-text { font-size: 26rpx; color: #333; &.placeholder { color: #ccc; } } }
- .route-input { height: 72rpx; font-size: 26rpx; border-bottom: 2rpx solid #f5f5f5; &.half { flex: 1; } }
- .contact-row { display: flex; gap: 16rpx; }
- .route-time-trigger { height: 72rpx; background: #f9f9f9; border-radius: 12rpx; display: flex; align-items: center; padding: 0 20rpx; font-size: 26rpx; color: #333; margin-top: 10rpx; .placeholder { color: #ccc; } }
- .address-title, .form-item-label, .booking-header .label, .remarks-title { display: block; font-size: 26rpx; color: #666; margin: 20rpx 0 10rpx; }
- .booking-section { margin-top: 24rpx; }
- .count-tag { font-size: 20rpx; color: #ff9500; background: #fff3e0; padding: 4rpx 12rpx; border-radius: 6rpx; }
- .time-item-row { display: flex; align-items: center; gap: 12rpx; margin-bottom: 16rpx; }
- .index { font-size: 24rpx; color: #999; width: 30rpx; }
- .flex-time-box { flex: 1; height: 64rpx; background: #fcfcfc; border: 2rpx solid #eee; border-radius: 10rpx; display: flex; align-items: center; justify-content: center; .time-text { font-size: 24rpx; color: #333; &.placeholder { color: #ccc; } } }
- .action-buttons { display: flex; gap: 12rpx; margin-left: 8rpx; }
- .circle-btn { width: 44rpx; height: 44rpx; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 32rpx; font-weight: bold; &.add { background: #e3f2fd; color: #2196f3; } &.remove { background: #fde2e2; color: #f56c6c; } }
- .remarks-textarea { width: 100%; height: 140rpx; font-size: 26rpx; background: #f9f9f9; border-radius: 16rpx; padding: 16rpx; box-sizing: border-box; }
- .quote-input { flex: 1; font-size: 36rpx; color: #f44336; font-weight: bold; text-align: right; }
- .unit-text { font-size: 28rpx; color: #333; margin-left: 8rpx; }
- .quote-tips { display: block; font-size: 22rpx; color: #999; margin-top: 20rpx; }
- .footer-bar { position: fixed; bottom: 0; left: 0; right: 0; background: #fff; padding: 24rpx 32rpx; display: flex; align-items: center; justify-content: space-between; box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.05); z-index: 100; }
- .quotation-box { display: flex; align-items: baseline; .p-label { font-size: 24rpx; color: #333; } .p-symbol { font-size: 28rpx; color: #f44336; font-weight: bold; margin-left: 8rpx; } .p-amount { font-size: 40rpx; font-weight: 900; color: #f44336; } }
- .submit-btn { width: 280rpx; height: 84rpx; background: linear-gradient(90deg, #ffd53f, #ff9500); color: #fff; border-radius: 42rpx; font-size: 28rpx; font-weight: bold; line-height: 84rpx; &::after { border: none; } }
- /* CSS Common Modal UI @Author: Antigravity */
- .center-modal-mask { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.6); z-index: 10000; display: flex; align-items: center; justify-content: center; backdrop-filter: blur(4rpx); }
- .center-modal-content { width: 620rpx; background: #fff; border-radius: 32rpx; display: flex; flex-direction: column; overflow: hidden; animation: popIn 0.3s ease-out; }
- @keyframes popIn { from { transform: scale(0.9); opacity: 0; } to { transform: scale(1); opacity: 1; } }
- .modal-header { padding: 32rpx; border-bottom: 2rpx solid #f2f2f2; position: relative; text-align: center; .modal-title { font-size: 30rpx; font-weight: bold; color: #333; } }
- .close-btn { position: absolute; right: 24rpx; top: 24rpx; width: 44rpx; height: 44rpx; &::before, &::after { content: ''; position: absolute; top: 20rpx; left: 8rpx; width: 28rpx; height: 4rpx; background: #999; transform: rotate(45deg); border-radius: 4rpx; } &::after { transform: rotate(-45deg); } }
- .search-box { display: flex; align-items: center; background: #f5f5f5; border-radius: 36rpx; padding: 0 24rpx; height: 72rpx; margin: 0 4rpx; .search-icon { width: 20rpx; height: 20rpx; border: 3rpx solid #999; border-radius: 50%; margin-right: 12rpx; position: relative; &::after { content: ''; width: 10rpx; height: 3rpx; background: #999; position: absolute; bottom: -4rpx; right: -4rpx; transform: rotate(45deg); } } .search-input { flex: 1; font-size: 26rpx; } .search-btn { font-size: 26rpx; color: #ff9500; font-weight: bold; margin-left: 20rpx; } }
- .modal-list-scroll { flex: 1; max-height: 55vh; padding: 0 32rpx; }
- .list-item { display: flex; align-items: center; justify-content: space-between; padding: 30rpx 0; border-bottom: 2rpx solid #f9f9f9; .user-info { display: flex; flex-direction: column; .name { font-size: 28rpx; font-weight: bold; color: #333; } .phone { font-size: 22rpx; color: #999; margin-top: 4rpx; } } }
- .checkmark { width: 12rpx; height: 22rpx; border-right: 4rpx solid #ff9500; border-bottom: 4rpx solid #ff9500; transform: rotate(45deg); }
- .cascade-indicator { display: flex; padding: 20rpx 32rpx; background: #fafafa; border-bottom: 2rpx solid #f2f2f2; flex-wrap: wrap; gap: 12rpx; .path-node { font-size: 24rpx; color: #666; &.active { color: #ff9500; font-weight: bold; } } }
- .datetime-picker-body { height: 400rpx; padding: 20rpx 0; }
- .picker-view { width: 100%; height: 100%; }
- .picker-item { line-height: 80rpx; text-align: center; font-size: 28rpx; }
- .modal-footer { display: flex; border-top: 2rpx solid #f2f2f2; .modal-cancel, .modal-confirm { flex: 1; height: 96rpx; line-height: 96rpx; text-align: center; font-size: 28rpx; } .modal-confirm { color: #ff9500; font-weight: bold; border-left: 2rpx solid #f2f2f2; } }
- .right-arrow { width: 12rpx; height: 12rpx; border-right: 3rpx solid #ccc; border-top: 3rpx solid #ccc; transform: rotate(45deg); flex-shrink: 0; }
- .empty-tip { padding: 80rpx 0; text-align: center; color: #ccc; font-size: 24rpx; }
- </style>
|