|
- <template>
- <view class="wrap">
- <view v-if="tipText" class="tip-card">{{ tipText }}</view>
-
- <!-- 反馈类型 -->
- <view class="fb-card">
- <text class="fb-card-title"><text class="required">*</text>问题类型</text>
- <view class="type-grid">
- <view
- v-for="item in TYPES"
- :key="item.value"
- class="type-item"
- :class="{ active: form.type === item.value }"
- @click="form.type = item.value"
- >
- {{ item.label }}
- </view>
- </view>
- </view>
-
- <!-- 反馈内容 -->
- <view class="fb-card">
- <view class="fb-card-head">
- <text class="fb-card-title"><text class="required">*</text>反馈内容</text>
- <text class="fb-card-count">{{ form.content.length }}/500</text>
- </view>
- <textarea
- v-model="form.content"
- class="textarea"
- maxlength="500"
- placeholder="请描述你遇到的问题或建议,越具体我们越容易定位"
- />
- </view>
-
- <!-- 联系电话 -->
- <view class="fb-card">
- <text class="fb-card-title"><text class="required">*</text>联系电话</text>
- <input
- v-model="form.phone"
- class="input"
- type="number"
- maxlength="11"
- placeholder="方便我们回复你的处理结果"
- />
- </view>
-
- <!-- 截图 -->
- <view class="fb-card">
- <view class="fb-card-head">
- <text class="fb-card-title"><text class="required">*</text>问题截图</text>
- <text class="fb-card-count">{{ form.imgs.length }}/9</text>
- </view>
- <view class="img-grid">
- <view v-for="(url, index) in form.imgs" :key="url" class="img-item">
- <image class="img" :src="url" mode="aspectFill" @click="previewImg(index)" />
- <view class="img-del" @click.stop="removeImg(index)">×</view>
- </view>
- <view v-if="form.imgs.length < 9" class="img-add" @click="chooseImage">
- <text class="add-plus">+</text>
- <text class="add-text">上传截图</text>
- </view>
- </view>
- <text class="fb-card-desc">支持 PNG / JPG,单张不超过 10M</text>
- </view>
-
- <view class="bottom-bar">
- <view class="submit-btn" :class="{ disabled: submitting }" @click="submit">
- {{ submitting ? '提交中...' : '提交反馈' }}
- </view>
- </view>
- </view>
- </template>
-
- <script setup>
- import { reactive, ref } from 'vue'
- import { onLoad } from '@dcloudio/uni-app'
- import { commonApi } from '@/api/index.js'
- import { uploadImage } from '@/utils/upload.js'
- import { useUserStore } from '@/store/user.js'
- import { useApp } from '@/utils/useApp.js'
-
- const userStore = useUserStore()
- const { toast } = useApp()
-
- // 与后端 feedback.type 约定一致:1反馈使用问题 2提出产品意见 3其他咨询
- const TYPES = [
- { value: 1, label: '反馈使用问题' },
- { value: 2, label: '提出产品意见' },
- { value: 3, label: '其他咨询' }
- ]
- const CONFIG_KEY = 'feedback_tip_content'
-
- const form = reactive({ type: 1, content: '', phone: '', imgs: [] })
- const tipText = ref('')
- const submitting = ref(false)
-
- onLoad(() => {
- loadTip()
- // 注意:/user/getinfo 返回的手机号是脱敏的(138****1234),不能用来预填,让用户自己输
- })
-
- async function loadTip() {
- try {
- const res = await commonApi.getConfig({ keys: CONFIG_KEY })
- const rows = Array.isArray(res.list) ? res.list : []
- const item = rows.find((it) => it.key === CONFIG_KEY)
- if (item && item.value) tipText.value = item.value
- } catch (e) {
- // 配置缺失就不展示提示条
- }
- }
-
- function chooseImage() {
- if (!ensureLogin()) return
- const count = 9 - form.imgs.length
- if (count <= 0) return
- uni.chooseImage({
- count,
- sizeType: ['compressed'],
- sourceType: ['album', 'camera'],
- success: async (res) => {
- const files = res.tempFiles || []
- const paths = res.tempFilePaths || []
- uni.showLoading({ title: '上传中...', mask: true })
- try {
- for (let i = 0; i < paths.length; i++) {
- if (Number((files[i] || {}).size || 0) > 10 * 1024 * 1024) {
- toast('单张图片不能超过 10M')
- continue
- }
- const ret = await uploadImage(paths[i], files[i] || {})
- const url = (ret.data && ret.data.url) || ''
- if (url) form.imgs.push(url)
- }
- } catch (e) {
- toast((e && e.msg) || '图片上传失败')
- } finally {
- uni.hideLoading()
- }
- }
- })
- }
-
- function removeImg(index) {
- form.imgs.splice(index, 1)
- }
-
- function previewImg(index) {
- uni.previewImage({ urls: form.imgs, current: form.imgs[index] })
- }
-
- async function submit() {
- if (!ensureLogin()) return
- if (submitting.value) return
- if (!form.type) return toast('请选择问题类型')
- if (!form.content.trim()) return toast('请输入反馈内容')
- if (!/^1\d{10}$/.test(form.phone)) return toast('请输入正确的手机号')
- if (!form.imgs.length) return toast('请上传问题截图')
-
- submitting.value = true
- try {
- await commonApi.feedback({
- type: form.type,
- content: form.content.trim(),
- phone: form.phone,
- imgs: form.imgs.join(',')
- })
- toast('反馈成功,感谢你的建议')
- setTimeout(() => uni.navigateBack(), 1200)
- } catch (e) {
- toast((e && e.msg) || '反馈失败,请重试')
- } finally {
- submitting.value = false
- }
- }
-
- function ensureLogin() {
- if (userStore.isLogin) return true
- uni.navigateTo({ url: '/pages/login/login' })
- return false
- }
- </script>
-
- <style lang="scss" scoped>
- .wrap {
- min-height: 100vh;
- padding: 24rpx 28rpx calc(200rpx + env(safe-area-inset-bottom));
- background: #f3f7fb;
- box-sizing: border-box;
- }
-
- .tip-card {
- padding: 22rpx 24rpx;
- border-radius: 20rpx;
- color: #b7791f;
- background: #fff8e6;
- font-size: 24rpx;
- line-height: 1.6;
- }
-
- .fb-card {
- margin-top: 22rpx;
- padding: 28rpx;
- border-radius: 24rpx;
- background: #fff;
- }
- .fb-card-head { display: flex; align-items: baseline; }
- .fb-card-title { display: block; color: #172033; font-size: 30rpx; font-weight: 800; }
- .fb-card-count { margin-left: auto; color: #8a95a6; font-size: 23rpx; }
- .fb-card-desc { display: block; margin-top: 14rpx; color: #8a95a6; font-size: 22rpx; }
- .required { margin-right: 4rpx; color: #ef4444; }
-
- .type-grid {
- margin-top: 20rpx;
- display: grid;
- grid-template-columns: repeat(3, minmax(0, 1fr));
- gap: 14rpx;
- }
- .type-item {
- height: 76rpx;
- border-radius: 18rpx;
- color: #596579;
- background: #f7f9fd;
- border: 2rpx solid transparent;
- font-size: 24rpx;
- font-weight: 700;
- line-height: 76rpx;
- text-align: center;
- box-sizing: border-box;
- overflow: hidden;
- white-space: nowrap;
- text-overflow: ellipsis;
- }
- .type-item.active { color: #1f8cff; background: #eff8ff; border-color: #1f8cff; }
-
- .textarea {
- margin-top: 20rpx;
- width: 100%;
- min-height: 220rpx;
- padding: 22rpx;
- border-radius: 20rpx;
- color: #172033;
- background: #f7f9fd;
- font-size: 25rpx;
- line-height: 1.55;
- box-sizing: border-box;
- }
-
- .input {
- margin-top: 20rpx;
- width: 100%;
- height: 88rpx;
- padding: 0 22rpx;
- border-radius: 20rpx;
- color: #172033;
- background: #f7f9fd;
- font-size: 27rpx;
- box-sizing: border-box;
- }
-
- .img-grid {
- margin-top: 20rpx;
- display: grid;
- grid-template-columns: repeat(4, minmax(0, 1fr));
- gap: 14rpx;
- }
- .img-item, .img-add {
- position: relative;
- width: 100%;
- height: 150rpx;
- border-radius: 18rpx;
- overflow: hidden;
- }
- .img { width: 100%; height: 100%; }
- .img-del {
- position: absolute;
- right: 0;
- top: 0;
- width: 40rpx;
- height: 40rpx;
- border-radius: 0 0 0 16rpx;
- color: #fff;
- background: rgba(15, 23, 42, 0.55);
- font-size: 26rpx;
- line-height: 38rpx;
- text-align: center;
- }
- .img-add {
- border: 2rpx dashed rgba(31, 140, 255, 0.35);
- background: #f8fbff;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- box-sizing: border-box;
- }
- .add-plus { color: #1f8cff; font-size: 44rpx; line-height: 1; }
- .add-text { margin-top: 8rpx; color: #8a95a6; font-size: 21rpx; }
-
- .bottom-bar {
- position: fixed;
- left: 0;
- right: 0;
- bottom: 0;
- padding: 18rpx 28rpx calc(18rpx + env(safe-area-inset-bottom));
- background: rgba(255, 255, 255, 0.96);
- box-shadow: 0 -10rpx 30rpx rgba(32, 56, 92, 0.1);
- }
- .submit-btn {
- height: 88rpx;
- border-radius: 999rpx;
- color: #fff;
- background: linear-gradient(135deg, #1f8cff, #41b9ff);
- font-size: 30rpx;
- font-weight: 800;
- line-height: 88rpx;
- text-align: center;
- box-shadow: 0 12rpx 24rpx rgba(31, 140, 255, 0.28);
- }
- .submit-btn.disabled { opacity: 0.6; }
- </style>
|