Files
cheflinkuser/src/pages-user/pages/complaints/index.vue
T

332 lines
7.9 KiB
Vue

<script setup lang="ts">
import ChooseImage from "@/components/choose-image/choose-image.vue";
import { appFeedbackAddPost } from "@/service";
import { z } from 'zod';
const { t } = useI18n()
const form = ref({
content: '',
contactPhone: '',
images: '',
})
// 创建zod校验schema
const createValidationSchema = () => {
return z.object({
content: z.string()
.min(1, t('pages-user.complaints.validation.content-required'))
.min(10, t('pages-user.complaints.validation.content-min-length'))
.max(500, t('pages-user.complaints.validation.content-max-length')),
contactPhone: z.string()
.min(1, t('pages-user.complaints.validation.contact-phone-required'))
.regex(/^[\d\s\-\+\(\)]+$/, t('pages-user.complaints.validation.contact-phone-invalid'))
})
}
const chooseImageRef = ref()
const showNoticePopup = ref(true)
const confirmCountdown = ref(5)
let countdownTimer: ReturnType<typeof setInterval> | null = null
function startNoticeCountdown() {
if (countdownTimer) {
clearInterval(countdownTimer)
}
confirmCountdown.value = 5
countdownTimer = setInterval(() => {
if (confirmCountdown.value <= 1) {
confirmCountdown.value = 0
if (countdownTimer) {
clearInterval(countdownTimer)
countdownTimer = null
}
return
}
confirmCountdown.value -= 1
}, 1000)
}
function handleCloseNoticePopup() {
if (confirmCountdown.value > 0) {
return
}
showNoticePopup.value = false
}
// 校验单个字段(只校验 schema 中存在的字段)
const validateField = (field: 'content' | 'contactPhone') => {
try {
const schema = createValidationSchema()
const fieldSchema = schema.shape[field]
fieldSchema.parse(form.value[field])
return true
} catch (error) {
if (error instanceof z.ZodError) {
uni.showToast({
title: error.errors[0].message,
icon: 'none'
})
}
return false
}
}
// 校验整个表单
const validateForm = () => {
try {
const schema = createValidationSchema()
schema.parse(form.value)
return true
} catch (error) {
if (error instanceof z.ZodError) {
uni.showToast({
title: error.errors[0].message,
icon: 'none'
})
}
return false
}
}
const handleSubmit = () => {
if (!validateForm()) {
return
}
appFeedbackAddPost({
body: {
...form.value
}
}).then(res => {
uni.showToast({
title: t('toast.submitSuccess'),
icon: 'none'
})
form.value = {
content: '',
contactPhone: '',
images: '',
}
})
}
const handleChooseImage = () => {
chooseImageRef.value.init()
}
function onImageChange(files: string[]) {
form.value.images = files[0]
}
onLoad(() => {
startNoticeCountdown()
})
onUnload(() => {
if (countdownTimer) {
clearInterval(countdownTimer)
countdownTimer = null
}
})
</script>
<template>
<view class="complaints-root">
<navbar :title="t('pages-user.complaints.title')" />
<view class="complaints-page">
<view class="form-card">
<view class="field">
<view class="field__label">{{ t('pages-user.complaints.feedback-content') }}</view>
<view class="field__box field__box--textarea">
<wd-textarea v-model="form.content" :maxlength="500" custom-class="!bg-transparent"
custom-textarea-container-class="!bg-transparent" no-border auto-height
:placeholder="t('pages-user.complaints.feedback-content-placeholder')" @blur="validateField('content')" />
</view>
</view>
<view class="field field--mt">
<view class="field__label">{{ t('pages-user.complaints.image') }}</view>
<view class="upload" @click="handleChooseImage">
<image v-if="form.images" src="@img/chef/113.png" class="upload__remove"></image>
<image v-if="!form.images" src="@img/chef/112.png" class="upload__placeholder"></image>
<image v-else :src="form.images" class="upload__image" mode="aspectFill"></image>
</view>
</view>
<view class="field field--mt">
<view class="field__label">{{ t('pages-user.complaints.contact-information') }}</view>
<view class="field__box field__box--input">
<wd-input v-model.trim="form.contactPhone" no-border custom-class="!p-0 !bg-transparent"
custom-input-class="complaints-input" :placeholder="t('pages-user.complaints.contact-information-placeholder')" :maxlength="50"
@blur="validateField('contactPhone')" />
</view>
</view>
</view>
<view class="complaints-page__desc">
{{ t('pages-user.complaints.description') }}
</view>
<!-- 底部固定提交按钮 -->
<view class="bottom-actions">
<wd-button custom-class="submit-btn" block @click="handleSubmit">
{{ t('common.submit') }}
</wd-button>
</view>
<ChooseImage ref="chooseImageRef" @change="onImageChange" />
<wd-popup v-model="showNoticePopup" :close-on-click-modal="false" custom-class="!bg-transparent">
<view class="notice-dialog">
<view class="notice-dialog__title">{{ t('pages-user.complaints.title') }}</view>
<view class="notice-dialog__content">
{{ t('pages-user.complaints.contact-information-tip') }}
</view>
<wd-button
custom-class="notice-dialog__btn"
block
:disabled="confirmCountdown > 0"
@click="handleCloseNoticePopup"
>
{{ confirmCountdown > 0 ? `${t('common.gotIt')} (${confirmCountdown}s)` : t('common.gotIt') }}
</wd-button>
</view>
</wd-popup>
</view>
</view>
</template>
<style lang="scss" scoped>
.complaints-page {
padding: 32rpx 30rpx calc(160rpx + env(safe-area-inset-bottom));
background: #fff;
min-height: 100vh;
box-sizing: border-box;
}
.complaints-page__desc {
font-size: 22rpx;
line-height: 36rpx;
color: #333;
margin-bottom: 18rpx;
text-align: center;
}
.form-card {
background: #fff;
border-radius: 24rpx;
padding: 26rpx 24rpx 18rpx;
}
.field__label {
font-size: 26rpx;
line-height: 36rpx;
color: #333;
font-weight: 700;
margin-bottom: 16rpx;
}
.field--mt {
margin-top: 26rpx;
}
.field__box {
border: 1rpx solid #ececec;
border-radius: 18rpx;
background: #fff;
overflow: hidden;
}
.field__box--textarea {
padding: 14rpx 16rpx;
min-height: 240rpx;
box-sizing: border-box;
}
.field__box--input {
padding: 18rpx 16rpx;
box-sizing: border-box;
}
:deep(.complaints-input) {
text-align: left;
font-size: 26rpx;
line-height: 36rpx;
color: #333;
}
.upload {
width: 176rpx;
height: 176rpx;
border: 1rpx solid #ececec;
border-radius: 18rpx;
overflow: hidden;
position: relative;
background: #fff;
}
.upload__placeholder,
.upload__image {
width: 176rpx;
height: 176rpx;
}
.upload__remove {
position: absolute;
top: -10rpx;
right: -10rpx;
z-index: 2;
width: 36rpx;
height: 36rpx;
}
.bottom-actions {
position: fixed;
left: 30rpx;
right: 30rpx;
bottom: calc(36rpx + env(safe-area-inset-bottom));
z-index: 20;
}
:deep(.submit-btn) {
height: 108rpx !important;
border-radius: 999rpx !important;
background: #111 !important;
color: #fff !important;
font-size: 32rpx !important;
font-weight: 800 !important;
letter-spacing: 0.06em;
}
.notice-dialog {
width: 620rpx;
background: #fff;
border-radius: 24rpx;
padding: 36rpx 28rpx 28rpx;
box-sizing: border-box;
}
.notice-dialog__title {
text-align: center;
font-size: 30rpx;
line-height: 40rpx;
color: #222;
font-weight: 700;
}
.notice-dialog__content {
margin-top: 18rpx;
margin-bottom: 28rpx;
font-size: 26rpx;
line-height: 38rpx;
color: #666;
// text-align: center;
}
:deep(.notice-dialog__btn) {
height: 84rpx !important;
border-radius: 999rpx !important;
font-size: 28rpx !important;
font-weight: 700 !important;
}
</style>