Files
rent/apps/miniapp/src/pages/seller/room-form.vue
T
2026-04-24 20:08:23 +08:00

411 lines
9.8 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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">面积()</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>