Files
cz-web/src/views/profile/cpns/FeedbackForm.vue
2026-10-10 09:57:56 +08:00

381 lines
7.8 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { reactive, ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
import type { FileInfo } from '@/types/api'
import type { Feedback } from '@/types/business/profile'
interface Props {
resetSignal?: number
}
const props = withDefaults(defineProps<Props>(), { resetSignal: 0 })
const emit = defineEmits<{
submit: [feedback: Feedback]
preview: [file: FileInfo]
}>()
const form = reactive({
title: '',
description: '',
})
const attachments = reactive<FileInfo[]>([])
const fileInputRef = ref<HTMLInputElement | null>(null)
function clear(): void {
form.title = ''
form.description = ''
attachments.splice(0, attachments.length)
}
watch(
() => props.resetSignal,
() => clear(),
)
function openFilePicker(): void {
fileInputRef.value?.click()
}
function onFileChange(event: Event): void {
const input = event.target as HTMLInputElement
const files = input.files
if (!files) return
for (const file of Array.from(files)) {
attachments.push({
name: file.name,
size: file.size,
uploadTime: new Date().toISOString().slice(0, 10),
url: '',
})
}
input.value = ''
}
function extOf(name: string): string {
const index = name.lastIndexOf('.')
return index >= 0 ? name.slice(index + 1, index + 4).toUpperCase() : 'FILE'
}
function sizeText(size: number): string {
if (size >= 1024 * 1024) return `${(size / 1024 / 1024).toFixed(1)}MB`
if (size >= 1024) return `${(size / 1024).toFixed(0)}KB`
return `${size}B`
}
function onDeleteFile(file: FileInfo): void {
const index = attachments.indexOf(file)
if (index >= 0) attachments.splice(index, 1)
}
function onPreview(file: FileInfo): void {
emit('preview', file)
}
function onSubmit(): void {
if (!form.title.trim()) {
ElMessage.warning('请填写反馈标题')
return
}
if (!form.description.trim()) {
ElMessage.warning('请填写详细描述')
return
}
emit('submit', {
title: form.title,
description: form.description,
attachments: [...attachments],
})
}
</script>
<template>
<div class="feedback-form">
<div class="feedback-form__field">
<label class="feedback-form__label">反馈标题<span class="feedback-form__required">*</span></label>
<input
v-model="form.title"
class="feedback-form__input"
type="text"
placeholder="请输入反馈标题"
maxlength="50"
>
</div>
<div class="feedback-form__field">
<label class="feedback-form__label">详细描述<span class="feedback-form__required">*</span></label>
<textarea
v-model="form.description"
class="feedback-form__textarea"
placeholder="请详细描述您的问题..."
maxlength="500"
/>
<span class="feedback-form__counter">{{ form.description.length }}/500</span>
</div>
<div class="feedback-form__attach-head">
<h3 class="feedback-form__attach-title">
附件上传
</h3>
<span class="feedback-form__attach-hint">支持图片/文档/压缩包,大小不超过1G</span>
<button class="feedback-form__upload-btn" @click="openFilePicker">
上传
</button>
<input
ref="fileInputRef"
class="feedback-form__file-input"
type="file"
multiple
@change="onFileChange"
>
</div>
<ul class="feedback-form__files">
<li v-for="file in attachments" :key="file.name" class="feedback-form__file">
<span class="feedback-form__file-icon" :class="`is-${extOf(file.name).toLowerCase()}`">
{{ extOf(file.name) }}
</span>
<div class="feedback-form__file-info">
<p class="feedback-form__file-name">
{{ file.name }}
</p>
<p class="feedback-form__file-meta">
{{ sizeText(file.size) }} · 上传于 {{ file.uploadTime }}
</p>
</div>
<div class="feedback-form__file-actions">
<button class="feedback-form__file-link" @click="onPreview(file)">
预览
</button>
<button class="feedback-form__file-link is-danger" @click="onDeleteFile(file)">
删除
</button>
</div>
</li>
</ul>
<div class="feedback-form__footer">
<button class="feedback-form__submit" @click="onSubmit">
提交反馈
</button>
</div>
</div>
</template>
<style lang="less" scoped>
.feedback-form {
.flex(column, flex-start);
gap: 20px;
width: 100%;
&__field {
.flex(column, flex-start);
gap: 10px;
width: 100%;
}
&__label {
font-size: 14px;
font-weight: 500;
color: @heading-color;
line-height: 20px;
}
&__required {
margin-left: 2px;
color: @danger-color;
}
&__input,
&__textarea {
width: 100%;
padding: 0 14px;
border: 1px solid @profile-input-border;
border-radius: @radius-base;
background: @white;
font-size: 13px;
color: @text-color;
outline: none;
transition: border-color 0.2s;
box-sizing: border-box;
&:focus {
border-color: @primary-strong;
}
&::placeholder {
color: @text-color-disabled;
}
}
&__input {
height: 40px;
}
&__textarea {
height: 150px;
padding: 12px 14px;
line-height: 1.7;
resize: vertical;
}
&__counter {
align-self: flex-end;
color: @text-color-disabled;
font-size: 12px;
line-height: 17px;
}
&__attach-head {
.flex(row, flex-start, center);
gap: 14px;
width: 100%;
margin-top: 6px;
}
&__attach-title {
font-size: 16px;
font-weight: 700;
color: @heading-color;
line-height: 24px;
}
&__attach-hint {
font-size: 12px;
color: @text-color-disabled;
}
&__upload-btn {
margin-left: auto;
height: 32px;
padding: 0 18px;
border: 1px solid @profile-input-border;
border-radius: @radius-base;
background: @white;
color: @text-color;
font-size: 13px;
transition: all 0.2s;
&:hover {
border-color: @primary-strong;
color: @primary-strong;
}
}
&__file-input {
display: none;
}
&__files {
.flex(column, flex-start);
gap: 12px;
width: 100%;
margin: 0;
padding: 0;
list-style: none;
}
&__file {
.flex(row, flex-start, center);
gap: 14px;
width: 100%;
padding: 13px 16px;
border-radius: @radius-base;
background: @service-input-bg;
box-sizing: border-box;
}
&__file-icon {
.flex(row, center);
flex-shrink: 0;
width: 40px;
height: 40px;
border-radius: @radius-base;
font-size: 11px;
font-weight: 700;
&.is-pdf {
background: @profile-icon-phone;
color: @danger-color;
}
&.is-doc,
&.is-docx {
background: @profile-page-active-bg;
color: @primary-strong;
}
&.is-xls,
&.is-xlsx {
background: @profile-tag-ok-bg;
color: @success-color;
}
}
&__file-info {
flex: 1;
min-width: 0;
}
&__file-name {
font-size: 14px;
font-weight: 500;
color: @heading-color;
line-height: 20px;
.ellipsis();
}
&__file-meta {
margin-top: 3px;
font-size: 12px;
color: @text-color-disabled;
line-height: 17px;
}
&__file-actions {
.flex(row, flex-end, center);
gap: 16px;
flex-shrink: 0;
}
&__file-link {
padding: 0;
border: none;
background: transparent;
color: @primary-strong;
font-size: 13px;
cursor: pointer;
&:hover {
color: @primary-hover;
}
&.is-danger {
color: @danger-color;
}
}
&__footer {
.flex(row, flex-start);
margin-top: 6px;
}
&__submit {
height: 40px;
padding: 0 32px;
border: none;
border-radius: @radius-base;
background: @primary-strong;
color: @white;
font-size: 14px;
font-weight: 500;
transition: background 0.2s;
&:hover {
background: @primary-hover;
}
}
}
</style>