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,
|
"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
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>
|
</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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user