feat: feadback
This commit is contained in:
9
api/mine.js
Normal file
9
api/mine.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import { request } from "@/utils/request.js";
|
||||
|
||||
export const sendFeedback = async (data) => {
|
||||
return request({
|
||||
url: "/api/common/feedback",
|
||||
method: "POST",
|
||||
data,
|
||||
});
|
||||
};
|
||||
@@ -64,6 +64,14 @@
|
||||
"enablePullDownRefresh": false,
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/feedback/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "意见反馈",
|
||||
"enablePullDownRefresh": false,
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
}
|
||||
],
|
||||
"globalStyle": {
|
||||
|
||||
BIN
pages/.DS_Store
vendored
Normal file
BIN
pages/.DS_Store
vendored
Normal file
Binary file not shown.
348
pages/feedback/index.vue
Normal file
348
pages/feedback/index.vue
Normal file
@@ -0,0 +1,348 @@
|
||||
<template>
|
||||
<view class="feedback-page" :style="{ paddingTop: getBavBarHeight() + 'px' }">
|
||||
<!-- Custom Navbar -->
|
||||
<view class="nav-bar">
|
||||
<view class="back" @tap="goBack">‹</view>
|
||||
<text class="nav-title">意见反馈</text>
|
||||
</view>
|
||||
|
||||
<view class="content-wrap">
|
||||
<!-- Type Selector -->
|
||||
<view class="form-item">
|
||||
<view class="label">反馈类型</view>
|
||||
<view class="type-list">
|
||||
<view
|
||||
v-for="(item, index) in feedbackTypes"
|
||||
:key="index"
|
||||
class="type-chip"
|
||||
:class="{ active: formData.type === item.value }"
|
||||
@tap="formData.type = item.value"
|
||||
>
|
||||
{{ item.label }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Content Input -->
|
||||
<view class="form-item">
|
||||
<view class="label">反馈内容 <text class="required">*</text></view>
|
||||
<view class="textarea-box">
|
||||
<textarea
|
||||
v-model="formData.content"
|
||||
class="textarea"
|
||||
placeholder="请输入您的反馈意见,我们将为您不断改进..."
|
||||
placeholder-class="placeholder"
|
||||
maxlength="200"
|
||||
/>
|
||||
<text class="counter">{{ formData.content.length }}/200</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Image Upload -->
|
||||
<view class="form-item">
|
||||
<view class="label"
|
||||
>图片上传 <text class="optional">(选填,最多3张)</text></view
|
||||
>
|
||||
<view class="image-grid">
|
||||
<view
|
||||
v-for="(img, index) in formData.images"
|
||||
:key="index"
|
||||
class="image-item"
|
||||
>
|
||||
<image
|
||||
:src="img"
|
||||
mode="aspectFill"
|
||||
class="thumb"
|
||||
@tap="previewImage(index)"
|
||||
/>
|
||||
<view class="delete-btn" @tap.stop="deleteImage(index)">×</view>
|
||||
</view>
|
||||
<view
|
||||
v-if="formData.images.length < 3"
|
||||
class="upload-btn"
|
||||
@tap="chooseImage"
|
||||
>
|
||||
<text class="plus">+</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<view class="submit-wrap">
|
||||
<button
|
||||
class="submit-btn"
|
||||
:class="{ disabled: !isValid }"
|
||||
:disabled="!isValid"
|
||||
@tap="submitFeedback"
|
||||
>
|
||||
提交反馈
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from "vue";
|
||||
import { getBavBarHeight } from "@/utils/system";
|
||||
import { sendFeedback } from "@/api/mine.js";
|
||||
import { uploadImage } from "@/utils/common.js";
|
||||
|
||||
const feedbackTypes = [
|
||||
{ label: "其他", value: 0 },
|
||||
{ label: "功能建议", value: 1 },
|
||||
{ label: "问题反馈", value: 2 },
|
||||
{ label: "投诉", value: 3 },
|
||||
];
|
||||
|
||||
const formData = ref({
|
||||
type: 0,
|
||||
content: "",
|
||||
images: [],
|
||||
});
|
||||
|
||||
const isValid = computed(() => {
|
||||
return formData.value.content.trim().length > 0;
|
||||
});
|
||||
|
||||
const goBack = () => {
|
||||
uni.navigateBack();
|
||||
};
|
||||
|
||||
const chooseImage = () => {
|
||||
uni.chooseImage({
|
||||
count: 3 - formData.value.images.length,
|
||||
sizeType: ["compressed"],
|
||||
sourceType: ["album", "camera"],
|
||||
success: (res) => {
|
||||
formData.value.images = [...formData.value.images, ...res.tempFilePaths];
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const deleteImage = (index) => {
|
||||
formData.value.images.splice(index, 1);
|
||||
};
|
||||
|
||||
const previewImage = (index) => {
|
||||
uni.previewImage({
|
||||
urls: formData.value.images,
|
||||
current: index,
|
||||
});
|
||||
};
|
||||
|
||||
const submitFeedback = async () => {
|
||||
if (!isValid.value) return;
|
||||
|
||||
uni.showLoading({ title: "提交中..." });
|
||||
|
||||
try {
|
||||
const uploadedImages = [];
|
||||
// Upload all images first
|
||||
for (const img of formData.value.images) {
|
||||
const url = await uploadImage(img);
|
||||
uploadedImages.push(url);
|
||||
}
|
||||
|
||||
// Submit with real URLs
|
||||
await sendFeedback({
|
||||
...formData.value,
|
||||
images: uploadedImages,
|
||||
});
|
||||
|
||||
uni.hideLoading();
|
||||
uni.showToast({
|
||||
title: "感谢您的反馈",
|
||||
icon: "success",
|
||||
duration: 2000,
|
||||
});
|
||||
setTimeout(() => {
|
||||
uni.navigateBack();
|
||||
}, 1000);
|
||||
} catch (err) {
|
||||
uni.hideLoading();
|
||||
uni.showToast({ title: "提交失败", icon: "none" });
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.feedback-page {
|
||||
min-height: 100vh;
|
||||
background: #f9f9f9;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.nav-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16rpx 24rpx;
|
||||
background: #fff;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
.back {
|
||||
font-size: 40rpx;
|
||||
margin-right: 24rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
.nav-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.content-wrap {
|
||||
padding: 32rpx;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
margin-bottom: 48rpx;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 20rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.required {
|
||||
color: #ff3b30;
|
||||
margin-left: 8rpx;
|
||||
}
|
||||
.optional {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
font-weight: normal;
|
||||
margin-left: 8rpx;
|
||||
}
|
||||
|
||||
/* Type Selector */
|
||||
.type-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20rpx;
|
||||
}
|
||||
.type-chip {
|
||||
padding: 12rpx 32rpx;
|
||||
background: #fff;
|
||||
border-radius: 999rpx;
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
border: 2rpx solid transparent;
|
||||
transition: all 0.2s;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
.type-chip.active {
|
||||
background: #fff0f0;
|
||||
color: #ff3b30;
|
||||
border-color: #ff3b30;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Textarea */
|
||||
.textarea-box {
|
||||
background: #fff;
|
||||
border-radius: 24rpx;
|
||||
padding: 24rpx;
|
||||
position: relative;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
.textarea {
|
||||
width: 100%;
|
||||
height: 240rpx;
|
||||
font-size: 28rpx;
|
||||
line-height: 1.5;
|
||||
color: #333;
|
||||
}
|
||||
.placeholder {
|
||||
color: #ccc;
|
||||
}
|
||||
.counter {
|
||||
position: absolute;
|
||||
bottom: 16rpx;
|
||||
right: 24rpx;
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* Image Grid */
|
||||
.image-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 24rpx;
|
||||
}
|
||||
.image-item {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
position: relative;
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
.thumb {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
.delete-btn {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-bottom-left-radius: 12rpx;
|
||||
font-size: 32rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
.upload-btn {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 2rpx dashed #ddd;
|
||||
}
|
||||
.plus {
|
||||
font-size: 60rpx;
|
||||
color: #ddd;
|
||||
font-weight: 300;
|
||||
margin-top: -8rpx;
|
||||
}
|
||||
|
||||
/* Submit Button */
|
||||
.submit-wrap {
|
||||
margin-top: 60rpx;
|
||||
}
|
||||
.submit-btn {
|
||||
background: #ff3b30;
|
||||
color: #fff;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
height: 88rpx;
|
||||
border-radius: 999rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 10rpx 20rpx rgba(255, 59, 48, 0.2);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.submit-btn.disabled {
|
||||
background: #ffccc7;
|
||||
box-shadow: none;
|
||||
opacity: 0.8;
|
||||
}
|
||||
.submit-btn::after {
|
||||
border: none;
|
||||
}
|
||||
</style>
|
||||
@@ -67,7 +67,7 @@
|
||||
</view>
|
||||
|
||||
<!-- History -->
|
||||
<view class="section-title">历史记录</view>
|
||||
<!-- <view class="section-title">历史记录</view>
|
||||
<view class="menu-group">
|
||||
<view class="menu-item" @tap="navTo('history')">
|
||||
<view class="icon-box gray-bg"><text>🕒</text></view>
|
||||
@@ -79,7 +79,7 @@
|
||||
<text class="menu-text">历年祝福存档</text>
|
||||
<text class="arrow">›</text>
|
||||
</view>
|
||||
</view>
|
||||
</view> -->
|
||||
|
||||
<!-- Other Actions -->
|
||||
<view class="menu-group mt-30">
|
||||
@@ -88,6 +88,11 @@
|
||||
<text class="menu-text">分享给好友</text>
|
||||
<text class="arrow">›</text>
|
||||
</button>
|
||||
<view class="menu-item" @tap="navTo('feedback')">
|
||||
<view class="icon-left"><text class="feedback-icon">📝</text></view>
|
||||
<text class="menu-text">意见反馈</text>
|
||||
<text class="arrow">›</text>
|
||||
</view>
|
||||
<view class="menu-item" @tap="navTo('help')">
|
||||
<view class="icon-left"><text class="help-icon">❓</text></view>
|
||||
<text class="menu-text">使用说明 / 帮助</text>
|
||||
@@ -161,6 +166,12 @@ const navTo = (page) => {
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (page === "feedback") {
|
||||
uni.navigateTo({
|
||||
url: "/pages/feedback/index",
|
||||
});
|
||||
return;
|
||||
}
|
||||
uni.showToast({ title: "功能开发中", icon: "none" });
|
||||
};
|
||||
</script>
|
||||
@@ -364,7 +375,8 @@ const navTo = (page) => {
|
||||
margin-left: 16rpx;
|
||||
}
|
||||
.share-icon,
|
||||
.help-icon {
|
||||
.help-icon,
|
||||
.feedback-icon {
|
||||
font-size: 36rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
@@ -4,3 +4,32 @@ export const generateObjectId = (
|
||||
h = 16,
|
||||
s = (s) => m.floor(s).toString(h),
|
||||
) => s(d.now() / 1000) + " ".repeat(h).replace(/./g, () => s(m.random() * h));
|
||||
|
||||
export const uploadImage = (filePath) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.uploadFile({
|
||||
url: "https://api.ai-meng.com/api/common/upload",
|
||||
filePath: filePath,
|
||||
name: "file",
|
||||
header: {
|
||||
"x-app-id": "69665538a49b8ae3be50fe5d",
|
||||
},
|
||||
success: (res) => {
|
||||
if (res.statusCode < 400) {
|
||||
try {
|
||||
const keyJson = JSON.parse(res.data);
|
||||
const url = `https://file.lihailezzc.com/${keyJson?.data.key}`;
|
||||
resolve(url);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
} else {
|
||||
reject(new Error("Upload failed"));
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err);
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// const BASE_URL = 'https://apis.lihailezzc.com'
|
||||
const BASE_URL = 'http://127.0.0.1:3999'
|
||||
// const BASE_URL = "http://192.168.1.3:3999";
|
||||
// const BASE_URL = 'http://127.0.0.1:3999'
|
||||
const BASE_URL = "http://192.168.1.3:3999";
|
||||
// const BASE_URL = "http://192.168.31.253:3999";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user