feat: feadback

This commit is contained in:
zzc
2026-01-26 11:18:24 +08:00
parent 87877d147d
commit 3226b35c4f
7 changed files with 411 additions and 5 deletions

9
api/mine.js Normal file
View 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,
});
};

View File

@@ -64,6 +64,14 @@
"enablePullDownRefresh": false, "enablePullDownRefresh": false,
"navigationStyle": "custom" "navigationStyle": "custom"
} }
},
{
"path": "pages/feedback/index",
"style": {
"navigationBarTitleText": "意见反馈",
"enablePullDownRefresh": false,
"navigationStyle": "custom"
}
} }
], ],
"globalStyle": { "globalStyle": {

BIN
pages/.DS_Store vendored Normal file

Binary file not shown.

348
pages/feedback/index.vue Normal file
View 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>

View File

@@ -67,7 +67,7 @@
</view> </view>
<!-- History --> <!-- History -->
<view class="section-title">历史记录</view> <!-- <view class="section-title">历史记录</view>
<view class="menu-group"> <view class="menu-group">
<view class="menu-item" @tap="navTo('history')"> <view class="menu-item" @tap="navTo('history')">
<view class="icon-box gray-bg"><text>🕒</text></view> <view class="icon-box gray-bg"><text>🕒</text></view>
@@ -79,7 +79,7 @@
<text class="menu-text">历年祝福存档</text> <text class="menu-text">历年祝福存档</text>
<text class="arrow"></text> <text class="arrow"></text>
</view> </view>
</view> </view> -->
<!-- Other Actions --> <!-- Other Actions -->
<view class="menu-group mt-30"> <view class="menu-group mt-30">
@@ -88,6 +88,11 @@
<text class="menu-text">分享给好友</text> <text class="menu-text">分享给好友</text>
<text class="arrow"></text> <text class="arrow"></text>
</button> </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="menu-item" @tap="navTo('help')">
<view class="icon-left"><text class="help-icon"></text></view> <view class="icon-left"><text class="help-icon"></text></view>
<text class="menu-text">使用说明 / 帮助</text> <text class="menu-text">使用说明 / 帮助</text>
@@ -161,6 +166,12 @@ const navTo = (page) => {
}); });
return; return;
} }
if (page === "feedback") {
uni.navigateTo({
url: "/pages/feedback/index",
});
return;
}
uni.showToast({ title: "功能开发中", icon: "none" }); uni.showToast({ title: "功能开发中", icon: "none" });
}; };
</script> </script>
@@ -364,7 +375,8 @@ const navTo = (page) => {
margin-left: 16rpx; margin-left: 16rpx;
} }
.share-icon, .share-icon,
.help-icon { .help-icon,
.feedback-icon {
font-size: 36rpx; font-size: 36rpx;
color: #666; color: #666;
} }

View File

@@ -4,3 +4,32 @@ export const generateObjectId = (
h = 16, h = 16,
s = (s) => m.floor(s).toString(h), s = (s) => m.floor(s).toString(h),
) => s(d.now() / 1000) + " ".repeat(h).replace(/./g, () => s(m.random() * 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);
},
});
});
};

View File

@@ -1,6 +1,6 @@
// const BASE_URL = 'https://apis.lihailezzc.com' // const BASE_URL = 'https://apis.lihailezzc.com'
const BASE_URL = 'http://127.0.0.1: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.1.3:3999";
// const BASE_URL = "http://192.168.31.253:3999"; // const BASE_URL = "http://192.168.31.253:3999";
import { useUserStore } from "@/stores/user"; import { useUserStore } from "@/stores/user";