411 lines
9.8 KiB
Vue
411 lines
9.8 KiB
Vue
<template>
|
||
<view class="page-room-form">
|
||
<view class="form-section">
|
||
<!-- 房源名称 -->
|
||
<view class="form-item">
|
||
<text class="label required">房源名称</text>
|
||
<input class="input" type="text" v-model="form.name" placeholder="请输入房源名称" maxlength="100" />
|
||
</view>
|
||
|
||
<!-- 类型 + 价格 -->
|
||
<view class="form-row">
|
||
<view class="form-item half">
|
||
<text class="label required">类型</text>
|
||
<picker :range="typeOptions" range-key="label" :value="typeIndex" @change="onTypeChange">
|
||
<view class="picker-value">{{ typeOptions[typeIndex].label }}</view>
|
||
</picker>
|
||
</view>
|
||
<view class="form-item half">
|
||
<text class="label required">价格/晚</text>
|
||
<input class="input" type="digit" v-model="form.price" placeholder="0.00" />
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 床型 + 面积 + 人数 -->
|
||
<view class="form-row">
|
||
<view class="form-item third">
|
||
<text class="label">床型</text>
|
||
<input class="input" type="text" v-model="form.bedType" placeholder="大床/双床" />
|
||
</view>
|
||
<view class="form-item third">
|
||
<text class="label">面积(m²)</text>
|
||
<input class="input" type="digit" v-model="form.area" placeholder="0" />
|
||
</view>
|
||
<view class="form-item third">
|
||
<text class="label">最多入住</text>
|
||
<input class="input" type="number" v-model="form.maxGuests" placeholder="1" />
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 取消政策 -->
|
||
<view class="form-item">
|
||
<text class="label">取消政策</text>
|
||
<picker :range="policyOptions" range-key="label" :value="policyIndex" @change="onPolicyChange">
|
||
<view class="picker-value">{{ policyOptions[policyIndex].label }}</view>
|
||
</picker>
|
||
</view>
|
||
|
||
<!-- 图片上传 -->
|
||
<view class="form-item">
|
||
<text class="label">房源图片</text>
|
||
<view class="image-list">
|
||
<view v-for="(img, i) in imageList" :key="i" class="image-item">
|
||
<image class="image-preview" :src="img" mode="aspectFill" />
|
||
<view class="image-delete" @tap="removeImage(i)">×</view>
|
||
</view>
|
||
<view v-if="imageList.length < 20" class="image-add" @tap="chooseImage">
|
||
<text class="add-icon">+</text>
|
||
</view>
|
||
</view>
|
||
<text class="hint">第一张为封面,最多20张</text>
|
||
</view>
|
||
|
||
<!-- 设施选择 -->
|
||
<view class="form-item">
|
||
<text class="label">设施服务</text>
|
||
<view class="facility-list">
|
||
<view
|
||
v-for="f in facilityOptions"
|
||
:key="f"
|
||
:class="['facility-tag', { active: form.facilities.includes(f) }]"
|
||
@tap="toggleFacility(f)"
|
||
>
|
||
{{ f }}
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 描述 -->
|
||
<view class="form-item">
|
||
<text class="label">房源描述</text>
|
||
<textarea class="textarea" v-model="form.description" placeholder="请描述房源特色" maxlength="1000" />
|
||
</view>
|
||
</view>
|
||
|
||
<button class="submit-btn" :disabled="loading" @tap="handleSubmit">
|
||
{{ loading ? '保存中...' : '保存' }}
|
||
</button>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, onMounted } from 'vue';
|
||
import { createMerchantRoom, updateMerchantRoom, getMerchantRoom } from '@/api/room';
|
||
import { chooseAndUpload } from '@/utils/upload';
|
||
|
||
const loading = ref(false);
|
||
const roomId = ref<number | null>(null);
|
||
const imageList = ref<string[]>([]);
|
||
|
||
const typeOptions = [
|
||
{ label: '酒店', value: 'hotel' },
|
||
{ label: '民宿', value: 'homestay' },
|
||
{ label: '公寓', value: 'apartment' },
|
||
{ label: '青旅', value: 'hostel' },
|
||
];
|
||
const typeIndex = ref(0);
|
||
|
||
const policyOptions = [
|
||
{ label: '灵活取消', value: 'flexible' },
|
||
{ label: '免费取消', value: 'free' },
|
||
{ label: '严格取消', value: 'strict' },
|
||
];
|
||
const policyIndex = ref(0);
|
||
|
||
const facilityOptions = ['WiFi', '空调', '热水', '电视', '冰箱', '洗衣机', '停车场', '电梯', '厨房', '阳台', '泳池', '健身房'];
|
||
|
||
const form = ref({
|
||
name: '',
|
||
type: 'hotel',
|
||
price: '',
|
||
bedType: '',
|
||
area: '',
|
||
maxGuests: '',
|
||
cancelPolicy: 'flexible',
|
||
facilities: [] as string[],
|
||
description: '',
|
||
});
|
||
|
||
onMounted(() => {
|
||
const pages = getCurrentPages();
|
||
const page = pages[pages.length - 1] as any;
|
||
const id = page.options?.id || page.$page?.options?.id;
|
||
if (id) {
|
||
roomId.value = Number(id);
|
||
uni.setNavigationBarTitle({ title: '编辑房源' });
|
||
loadRoom(Number(id));
|
||
}
|
||
});
|
||
|
||
async function loadRoom(id: number) {
|
||
try {
|
||
const res = await getMerchantRoom(id);
|
||
const room = res.data;
|
||
form.value = {
|
||
name: room.name || '',
|
||
type: room.type || 'hotel',
|
||
price: room.price?.toString() || '',
|
||
bedType: room.bedType || '',
|
||
area: room.area?.toString() || '',
|
||
maxGuests: room.maxGuests?.toString() || '',
|
||
cancelPolicy: room.cancelPolicy || 'flexible',
|
||
facilities: room.facilities || [],
|
||
description: room.description || '',
|
||
};
|
||
imageList.value = room.images || [];
|
||
typeIndex.value = typeOptions.findIndex((t) => t.value === room.type);
|
||
policyIndex.value = policyOptions.findIndex((p) => p.value === room.cancelPolicy);
|
||
} catch (e) {
|
||
console.error(e);
|
||
}
|
||
}
|
||
|
||
function onTypeChange(e: any) {
|
||
typeIndex.value = e.detail.value;
|
||
form.value.type = typeOptions[e.detail.value].value;
|
||
}
|
||
|
||
function onPolicyChange(e: any) {
|
||
policyIndex.value = e.detail.value;
|
||
form.value.cancelPolicy = policyOptions[e.detail.value].value;
|
||
}
|
||
|
||
function toggleFacility(f: string) {
|
||
const idx = form.value.facilities.indexOf(f);
|
||
if (idx > -1) {
|
||
form.value.facilities.splice(idx, 1);
|
||
} else {
|
||
form.value.facilities.push(f);
|
||
}
|
||
}
|
||
|
||
async function chooseImage() {
|
||
const remaining = 20 - imageList.value.length;
|
||
if (remaining <= 0) {
|
||
uni.showToast({ title: '最多上传20张图片', icon: 'none' });
|
||
return;
|
||
}
|
||
try {
|
||
uni.showLoading({ title: '上传中...' });
|
||
const urls = await chooseAndUpload({ count: remaining, useSellerToken: true });
|
||
imageList.value.push(...urls);
|
||
} catch (err: any) {
|
||
uni.showToast({ title: err.message || '上传失败', icon: 'none' });
|
||
} finally {
|
||
uni.hideLoading();
|
||
}
|
||
}
|
||
|
||
function removeImage(index: number) {
|
||
imageList.value.splice(index, 1);
|
||
}
|
||
|
||
function validate(): boolean {
|
||
if (!form.value.name) {
|
||
uni.showToast({ title: '请输入房源名称', icon: 'none' });
|
||
return false;
|
||
}
|
||
if (!form.value.price || Number(form.value.price) <= 0) {
|
||
uni.showToast({ title: '请输入有效价格', icon: 'none' });
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
async function handleSubmit() {
|
||
if (!validate()) return;
|
||
loading.value = true;
|
||
|
||
const data = {
|
||
name: form.value.name,
|
||
type: form.value.type,
|
||
price: Number(form.value.price),
|
||
bedType: form.value.bedType || undefined,
|
||
area: form.value.area ? Number(form.value.area) : undefined,
|
||
maxGuests: form.value.maxGuests ? Number(form.value.maxGuests) : undefined,
|
||
cancelPolicy: form.value.cancelPolicy,
|
||
facilities: form.value.facilities,
|
||
images: imageList.value,
|
||
coverImage: imageList.value[0] || '',
|
||
description: form.value.description || undefined,
|
||
};
|
||
|
||
try {
|
||
if (roomId.value) {
|
||
await updateMerchantRoom(roomId.value, data);
|
||
} else {
|
||
await createMerchantRoom(data);
|
||
}
|
||
uni.redirectTo({ url: '/pages/seller/rooms' });
|
||
} catch (e: any) {
|
||
uni.showToast({ title: e.message || '操作失败', icon: 'none' });
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.page-room-form {
|
||
min-height: 100vh;
|
||
background: #f5f5f5;
|
||
padding: 24rpx;
|
||
padding-bottom: 160rpx;
|
||
}
|
||
|
||
.form-section {
|
||
background: #fff;
|
||
border-radius: 16rpx;
|
||
padding: 24rpx;
|
||
}
|
||
|
||
.form-item {
|
||
margin-bottom: 28rpx;
|
||
|
||
&.half {
|
||
flex: 1;
|
||
}
|
||
&.third {
|
||
flex: 1;
|
||
}
|
||
}
|
||
|
||
.form-row {
|
||
display: flex;
|
||
gap: 16rpx;
|
||
}
|
||
|
||
.label {
|
||
font-size: 28rpx;
|
||
color: #333;
|
||
margin-bottom: 12rpx;
|
||
display: block;
|
||
|
||
&.required::before {
|
||
content: '*';
|
||
color: #ff4d4f;
|
||
margin-right: 8rpx;
|
||
}
|
||
}
|
||
|
||
.input {
|
||
width: 100%;
|
||
height: 80rpx;
|
||
background: #f5f5f5;
|
||
border-radius: 12rpx;
|
||
padding: 0 20rpx;
|
||
font-size: 28rpx;
|
||
}
|
||
|
||
.picker-value {
|
||
height: 80rpx;
|
||
background: #f5f5f5;
|
||
border-radius: 12rpx;
|
||
padding: 0 20rpx;
|
||
font-size: 28rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.textarea {
|
||
width: 100%;
|
||
height: 160rpx;
|
||
background: #f5f5f5;
|
||
border-radius: 12rpx;
|
||
padding: 20rpx;
|
||
font-size: 28rpx;
|
||
}
|
||
|
||
.hint {
|
||
font-size: 22rpx;
|
||
color: #999;
|
||
margin-top: 8rpx;
|
||
}
|
||
|
||
.image-list {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 16rpx;
|
||
}
|
||
|
||
.image-item {
|
||
width: 160rpx;
|
||
height: 160rpx;
|
||
position: relative;
|
||
}
|
||
|
||
.image-preview {
|
||
width: 100%;
|
||
height: 100%;
|
||
border-radius: 12rpx;
|
||
}
|
||
|
||
.image-delete {
|
||
position: absolute;
|
||
top: -12rpx;
|
||
right: -12rpx;
|
||
width: 36rpx;
|
||
height: 36rpx;
|
||
background: #ff4d4f;
|
||
border-radius: 50%;
|
||
color: #fff;
|
||
font-size: 24rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.image-add {
|
||
width: 160rpx;
|
||
height: 160rpx;
|
||
background: #f5f5f5;
|
||
border-radius: 12rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.add-icon {
|
||
font-size: 48rpx;
|
||
color: #999;
|
||
}
|
||
|
||
.facility-list {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 12rpx;
|
||
}
|
||
|
||
.facility-tag {
|
||
padding: 10rpx 24rpx;
|
||
background: #f5f5f5;
|
||
border-radius: 24rpx;
|
||
font-size: 24rpx;
|
||
color: #666;
|
||
|
||
&.active {
|
||
background: #fff1eb;
|
||
color: #FF6B35;
|
||
border: 1rpx solid #FF6B35;
|
||
}
|
||
}
|
||
|
||
.submit-btn {
|
||
position: fixed;
|
||
bottom: 0;
|
||
left: 0;
|
||
right: 0;
|
||
height: 100rpx;
|
||
background: linear-gradient(135deg, #FF6B35, #ff8a5c);
|
||
color: #fff;
|
||
border-radius: 0;
|
||
font-size: 32rpx;
|
||
font-weight: 600;
|
||
border: none;
|
||
|
||
&[disabled] {
|
||
opacity: 0.6;
|
||
}
|
||
}
|
||
</style>
|