Files
CosScene/clients/pages/spot/create.vue
T
2026-05-09 16:40:29 +08:00

652 lines
15 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.
<script setup>
import { ref, onMounted, onUnmounted } from "vue";
import { onShow } from "@dcloudio/uni-app";
import { createSpot, uploadImage } from "@/api/spot";
import { getTags } from "@/api/tag";
import { addTagToSpot } from "@/api/tag";
import { extractList } from "@/utils/request";
import { checkLogin } from "@/utils/auth";
onShow(() => { checkLogin(); });
const form = ref({
title: "",
city: "",
description: "",
transport: "",
best_time: "",
difficulty: "",
is_free: true,
price_type: "fixed",
price_min: "",
price_max: "",
longitude: "",
latitude: "",
});
const images = ref([]);
const submitting = ref(false);
const uploading = ref(false);
const uploadProgress = ref("");
const locationName = ref("");
const allTags = ref([]);
const selectedTagIds = ref([]);
const fetchTags = async () => {
try {
const res = await getTags();
allTags.value = extractList(res);
} catch (e) {
console.error(e);
}
};
fetchTags();
const toggleTag = (tagId) => {
const idx = selectedTagIds.value.indexOf(tagId);
if (idx >= 0) {
selectedTagIds.value.splice(idx, 1);
} else {
if (selectedTagIds.value.length >= 5) {
uni.showToast({ title: "最多选择5个标签", icon: "none" });
return;
}
selectedTagIds.value.push(tagId);
}
};
const onLocationPicked = (data) => {
form.value.longitude = data.longitude;
form.value.latitude = data.latitude;
form.value.city = data.city || "";
locationName.value = data.name || "已选择位置";
};
onMounted(() => {
uni.$on("locationPicked", onLocationPicked);
});
onUnmounted(() => {
uni.$off("locationPicked", onLocationPicked);
});
const chooseLocation = () => {
uni.navigateTo({ url: "/pages/spot/pick-location" });
};
const chooseImages = () => {
const remaining = 9 - images.value.length;
if (remaining <= 0) {
uni.showToast({ title: "最多添加9张图片", icon: "none" });
return;
}
uni.chooseImage({
count: remaining,
sizeType: ["compressed"],
sourceType: ["album", "camera"],
success: (res) => {
images.value.push(...res.tempFilePaths);
},
});
};
const removeImage = (idx) => {
images.value.splice(idx, 1);
};
const validate = () => {
if (!form.value.title.trim()) {
uni.showToast({ title: "请输入地点名称", icon: "none" });
return false;
}
if (!form.value.city) {
uni.showToast({ title: "请先通过定位获取所在城市", icon: "none" });
return false;
}
if (form.value.longitude === "" || form.value.longitude == null ||
form.value.latitude === "" || form.value.latitude == null) {
uni.showToast({ title: "请选择地图坐标位置", icon: "none" });
return false;
}
return true;
};
const handleSubmit = async () => {
if (!validate() || submitting.value) return;
submitting.value = true;
try {
const data = {
title: form.value.title.trim(),
city: form.value.city.trim(),
longitude: Number(form.value.longitude),
latitude: Number(form.value.latitude),
description: form.value.description.trim(),
transport: form.value.transport.trim(),
best_time: form.value.best_time.trim(),
difficulty: form.value.difficulty.trim(),
is_free: form.value.is_free,
price_min: form.value.is_free ? null : (parseFloat(form.value.price_min) || null),
price_max: form.value.is_free
? null
: form.value.price_type === "range"
? (parseFloat(form.value.price_max) || null)
: (parseFloat(form.value.price_min) || null),
tag_ids: selectedTagIds.value,
};
if (images.value.length > 0) {
uploading.value = true;
uploadProgress.value = "正在上传图片…";
const uploadedUrls = [];
for (let i = 0; i < images.value.length; i++) {
uploadProgress.value = `正在上传图片 ${i + 1}/${images.value.length}...`;
const result = await uploadImage(images.value[i]);
uploadedUrls.push(result.url);
}
data.image_urls = uploadedUrls;
uploading.value = false;
uploadProgress.value = "";
}
await createSpot(data);
uni.showToast({ title: "提交成功,待审核", icon: "success" });
setTimeout(() => {
uni.switchTab({ url: "/pages/index/index" });
}, 1000);
} catch (e) {
console.error(e);
} finally {
uploading.value = false;
submitting.value = false;
}
};
</script>
<template>
<view class="create-page">
<view class="form-card">
<view class="field">
<text class="label">地点名称 *</text>
<input
v-model="form.title"
class="input"
placeholder="请输入取景地名称"
placeholder-class="placeholder"
/>
</view>
<view class="field">
<text class="label">地点介绍</text>
<textarea
v-model="form.description"
class="textarea"
placeholder="描述这个取景地的特色..."
placeholder-class="placeholder"
maxlength="500"
:auto-height="false"
/>
</view>
<view class="field">
<text class="label">交通方式</text>
<textarea
v-model="form.transport"
class="textarea textarea-sm"
placeholder="如何到达这个地点?"
placeholder-class="placeholder"
maxlength="300"
:auto-height="false"
/>
</view>
<view class="field">
<text class="label">最佳拍摄时间</text>
<input
v-model="form.best_time"
class="input"
placeholder="例如:下午4-6点,黄金时段"
placeholder-class="placeholder"
/>
</view>
<view class="field">
<text class="label">路径难度说明</text>
<textarea
v-model="form.difficulty"
class="textarea textarea-sm"
placeholder="路况如何?是否适合携带大量器材?"
placeholder-class="placeholder"
maxlength="300"
:auto-height="false"
/>
</view>
<view class="field">
<text class="label">是否收费</text>
<view class="free-toggle">
<view
class="toggle-btn"
:class="{ active: form.is_free }"
@tap="form.is_free = true; form.price_min = ''; form.price_max = ''"
>
<text class="toggle-text">免费</text>
</view>
<view
class="toggle-btn"
:class="{ active: !form.is_free }"
@tap="form.is_free = false"
>
<text class="toggle-text">收费</text>
</view>
</view>
</view>
<view class="field" v-if="!form.is_free">
<text class="label">价格类型</text>
<view class="free-toggle">
<view
class="toggle-btn"
:class="{ active: form.price_type === 'fixed' }"
@tap="form.price_type = 'fixed'; form.price_max = ''"
>
<text class="toggle-text">固定价格</text>
</view>
<view
class="toggle-btn"
:class="{ active: form.price_type === 'range' }"
@tap="form.price_type = 'range'"
>
<text class="toggle-text">区间价格</text>
</view>
</view>
</view>
<view class="field" v-if="!form.is_free && form.price_type === 'fixed'">
<text class="label">价格</text>
<input
v-model="form.price_min"
class="input"
type="digit"
placeholder="请输入价格,例如:50"
placeholder-class="placeholder"
/>
</view>
<view class="field" v-if="!form.is_free && form.price_type === 'range'">
<text class="label">价格区间</text>
<view class="price-range">
<input
v-model="form.price_min"
class="input price-input"
type="digit"
placeholder="最低价"
placeholder-class="placeholder"
/>
<text class="price-sep">~</text>
<input
v-model="form.price_max"
class="input price-input"
type="digit"
placeholder="最高价"
placeholder-class="placeholder"
/>
</view>
</view>
<view class="field" v-if="allTags.length > 0">
<text class="label">标签最多5个</text>
<view class="tag-selector">
<view
v-for="tag in allTags"
:key="tag.id"
class="tag-chip"
:class="{ active: selectedTagIds.includes(tag.id) }"
@tap="toggleTag(tag.id)"
>
<text class="tag-chip-text">{{ tag.name }}</text>
</view>
</view>
</view>
<view class="field">
<text class="label">坐标位置</text>
<view class="location-picker" @tap="chooseLocation">
<uni-icons type="location" size="18" color="#6366f1" />
<text v-if="locationName" class="location-text">{{ locationName }}</text>
<text v-else class="location-placeholder">点击选择地图位置</text>
<uni-icons type="right" size="16" color="#cbd5e1" />
</view>
<view v-if="form.longitude && form.latitude" class="coord-display">
<text class="coord-text">经度: {{ form.longitude }} / 纬度: {{ form.latitude }}</text>
</view>
<view class="auto-city-display">
<text class="auto-city-label">所在城市</text>
<text v-if="form.city" class="auto-city-text">{{ form.city }}</text>
<text v-else class="auto-city-placeholder">选择地图位置后自动识别</text>
</view>
</view>
</view>
<view class="form-card">
<view class="field">
<text class="label">添加图片最多9张</text>
<view class="image-grid">
<view
v-for="(img, idx) in images"
:key="idx"
class="image-item"
>
<image class="preview-img" :src="img" mode="aspectFill" />
<view class="remove-btn" @tap="removeImage(idx)">
<text class="remove-icon"></text>
</view>
</view>
<view
v-if="images.length < 9"
class="image-item add-btn"
@tap="chooseImages"
>
<text class="add-icon"></text>
<text class="add-text">添加</text>
</view>
</view>
</view>
</view>
<view class="submit-area">
<button
class="submit-btn"
:loading="submitting"
:disabled="uploading || submitting"
@tap="handleSubmit"
>
{{ uploading ? uploadProgress : submitting ? "提交中..." : "提交审核" }}
</button>
</view>
</view>
</template>
<style scoped>
.create-page {
min-height: 100vh;
background: #f5f6fa;
padding: 24rpx 32rpx;
padding-bottom: 48rpx;
}
.form-card {
background: #ffffff;
border-radius: 16rpx;
padding: 32rpx;
margin-bottom: 24rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
}
.field {
margin-bottom: 32rpx;
}
.field:last-child {
margin-bottom: 0;
}
.label {
font-size: 28rpx;
font-weight: 600;
color: #1e293b;
display: block;
margin-bottom: 16rpx;
}
.input {
width: 100%;
height: 84rpx;
background: #f5f6fa;
border-radius: 12rpx;
padding: 0 24rpx;
font-size: 28rpx;
color: #1e293b;
box-sizing: border-box;
}
.textarea {
width: 100%;
height: 200rpx;
background: #f5f6fa;
border-radius: 12rpx;
padding: 20rpx 24rpx;
font-size: 28rpx;
color: #1e293b;
box-sizing: border-box;
line-height: 1.6;
}
.textarea-sm {
height: 140rpx;
}
.placeholder {
color: #94a3b8;
}
.location-picker {
display: flex;
align-items: center;
height: 84rpx;
background: #f5f6fa;
border-radius: 12rpx;
padding: 0 24rpx;
gap: 12rpx;
}
.location-text {
flex: 1;
font-size: 28rpx;
color: #1e293b;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.location-placeholder {
flex: 1;
font-size: 28rpx;
color: #94a3b8;
}
.coord-display {
margin-top: 12rpx;
padding: 12rpx 24rpx;
background: rgba(99, 102, 241, 0.06);
border-radius: 8rpx;
}
.coord-text {
font-size: 24rpx;
color: #6366f1;
}
.auto-city-display {
margin-top: 12rpx;
padding: 16rpx 24rpx;
background: #f8fafc;
border-radius: 8rpx;
}
.auto-city-label {
display: block;
font-size: 22rpx;
color: #94a3b8;
margin-bottom: 6rpx;
}
.auto-city-text {
display: block;
font-size: 28rpx;
color: #1e293b;
font-weight: 500;
}
.auto-city-placeholder {
display: block;
font-size: 26rpx;
color: #94a3b8;
}
.image-grid {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
}
.image-item {
width: 200rpx;
height: 200rpx;
border-radius: 12rpx;
overflow: hidden;
position: relative;
}
.preview-img {
width: 100%;
height: 100%;
}
.remove-btn {
position: absolute;
top: 8rpx;
right: 8rpx;
width: 40rpx;
height: 40rpx;
background: rgba(0, 0, 0, 0.5);
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: center;
}
.remove-icon {
color: #ffffff;
font-size: 22rpx;
}
.add-btn {
background: #f5f6fa;
border: 2rpx dashed #cbd5e1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.add-icon {
font-size: 48rpx;
color: #94a3b8;
}
.add-text {
font-size: 22rpx;
color: #94a3b8;
margin-top: 4rpx;
}
.free-toggle {
display: flex;
gap: 16rpx;
}
.toggle-btn {
flex: 1;
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
background: #f5f6fa;
border-radius: 12rpx;
border: 2rpx solid #e2e8f0;
}
.toggle-btn.active {
background: rgba(99, 102, 241, 0.12);
border-color: #6366f1;
}
.toggle-text {
font-size: 28rpx;
color: #64748b;
}
.toggle-btn.active .toggle-text {
color: #6366f1;
font-weight: 600;
}
.price-range {
display: flex;
align-items: center;
gap: 12rpx;
}
.price-input {
flex: 1;
}
.price-sep {
font-size: 28rpx;
color: #94a3b8;
}
.tag-selector {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
}
.tag-chip {
padding: 10rpx 24rpx;
background: #f5f6fa;
border-radius: 32rpx;
border: 2rpx solid #e2e8f0;
}
.tag-chip.active {
background: rgba(99, 102, 241, 0.12);
border-color: #6366f1;
}
.tag-chip-text {
font-size: 26rpx;
color: #64748b;
}
.tag-chip.active .tag-chip-text {
color: #6366f1;
font-weight: 500;
}
.submit-area {
margin-top: 16rpx;
}
.submit-btn {
width: 100%;
height: 88rpx;
line-height: 88rpx;
background: #6366f1;
color: #ffffff;
font-size: 32rpx;
font-weight: 600;
border-radius: 16rpx;
border: none;
}
.submit-btn[disabled] {
background: #a5b4fc;
}
.submit-btn::after {
border: none;
}
</style>