Initial project commit
This commit is contained in:
@@ -0,0 +1,651 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user