feat: extend SMS signature and setting file support
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
title="选择图片"
|
||||
:title="props.acceptAll ? '选择文件' : '选择图片'"
|
||||
width="900px"
|
||||
append-to-body
|
||||
@close="handleClose"
|
||||
@@ -12,13 +12,13 @@
|
||||
<el-tab-pane label="文件库" name="fileLibrary">
|
||||
<div class="file-list-container">
|
||||
<div class="file-list-header">
|
||||
<h4>图片文件库</h4>
|
||||
<h4>{{ props.acceptAll ? '文件库' : '图片文件库' }}</h4>
|
||||
<div class="header-actions">
|
||||
<span v-if="props.multiple && selectedIds.size > 0" class="selected-count">
|
||||
已选 {{ selectedIds.size }} 个文件
|
||||
</span>
|
||||
<el-button type="primary" @click="switchToUpload" :icon="Upload">
|
||||
上传新图片
|
||||
{{ props.acceptAll ? '上传新文件' : '上传新图片' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -52,10 +52,15 @@
|
||||
</div>
|
||||
<div class="file-preview">
|
||||
<img
|
||||
v-if="isImageFile(file.realName, file.url)"
|
||||
:src="processImageUrl(file.url)"
|
||||
:alt="file.realName"
|
||||
@error="handleImageError"
|
||||
/>
|
||||
<div v-else class="file-type-icon">
|
||||
<el-icon :size="32"><Document /></el-icon>
|
||||
<span class="file-ext">{{ getFileExt(file.realName) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="file-info">
|
||||
<p class="file-name" :title="file.realName">{{ file.realName }}</p>
|
||||
@@ -63,7 +68,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-empty v-if="filteredFileList.length === 0 && !loading" description="暂无图片文件" />
|
||||
<el-empty v-if="filteredFileList.length === 0 && !loading" :description="props.acceptAll ? '暂无文件' : '暂无图片文件'" />
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container" v-if="total > 0">
|
||||
@@ -81,14 +86,14 @@
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 上传图片 -->
|
||||
<el-tab-pane label="上传图片" name="upload">
|
||||
<!-- 上传文件 -->
|
||||
<el-tab-pane :label="props.acceptAll ? '上传文件' : '上传图片'" name="upload">
|
||||
<div class="upload-section">
|
||||
<el-upload
|
||||
:auto-upload="false"
|
||||
:show-file-list="false"
|
||||
:on-change="handleFileChange"
|
||||
accept="image/*"
|
||||
:accept="props.acceptAll ? undefined : 'image/*'"
|
||||
multiple
|
||||
drag
|
||||
>
|
||||
@@ -98,7 +103,7 @@
|
||||
</div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">
|
||||
支持jpg、png、gif、webp等图片格式,单个文件不超过5MB
|
||||
{{ props.acceptAll ? '支持所有文件类型,单个文件不超过50MB' : '支持jpg、png、gif、webp等图片格式,单个文件不超过5MB' }}
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
@@ -111,7 +116,10 @@
|
||||
</div>
|
||||
<div class="pending-list">
|
||||
<div v-for="(file, index) in pendingFiles" :key="index" class="pending-item">
|
||||
<img :src="file.previewUrl" class="pending-preview" />
|
||||
<img v-if="file.isImage" :src="file.previewUrl" class="pending-preview" />
|
||||
<div v-else class="pending-preview pending-file-icon">
|
||||
<el-icon :size="20"><Document /></el-icon>
|
||||
</div>
|
||||
<span class="pending-name" :title="file.name">{{ file.name }}</span>
|
||||
<span class="pending-size">{{ formatFileSize(file.size) }}</span>
|
||||
<el-button type="danger" link size="small" @click="removePendingFile(index)">移除</el-button>
|
||||
@@ -149,7 +157,7 @@
|
||||
<script setup>
|
||||
import { ref, watch, computed } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Upload, UploadFilled, Search, Select, Delete } from '@element-plus/icons-vue'
|
||||
import { Upload, UploadFilled, Search, Select, Delete, Document } from '@element-plus/icons-vue'
|
||||
import { getFileList, getFileDetail, uploadFile } from '@/api/admin/file'
|
||||
|
||||
// Props
|
||||
@@ -165,6 +173,10 @@ const props = defineProps({
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
acceptAll: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
@@ -349,22 +361,37 @@ const selectFile = (file) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 判断是否为图片文件
|
||||
const isImageFile = (name, url) => {
|
||||
const imgExts = /\.(jpe?g|png|gif|webp|bmp|svg|ico|avif)$/i
|
||||
if (name && imgExts.test(name)) return true
|
||||
if (url && imgExts.test(url.split('?')[0])) return true
|
||||
return false
|
||||
}
|
||||
|
||||
const getFileExt = (name) => {
|
||||
if (!name) return ''
|
||||
const dotIndex = name.lastIndexOf('.')
|
||||
if (dotIndex === -1) return ''
|
||||
return name.slice(dotIndex + 1).toUpperCase()
|
||||
}
|
||||
|
||||
// 文件选择变化(收集待上传文件)
|
||||
const handleFileChange = (file) => {
|
||||
const rawFile = file.raw
|
||||
if (!rawFile) return
|
||||
|
||||
// 验证文件类型
|
||||
if (!props.acceptAll) {
|
||||
const isImage = rawFile.type.startsWith('image/')
|
||||
if (!isImage) {
|
||||
ElMessage.error(`${rawFile.name} 不是图片文件,已跳过`)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 验证文件大小
|
||||
const isLt5M = rawFile.size / 1024 / 1024 < 5
|
||||
if (!isLt5M) {
|
||||
ElMessage.error(`${rawFile.name} 超过 5MB,已跳过`)
|
||||
const sizeLimit = props.acceptAll ? 50 : 5
|
||||
if (rawFile.size / 1024 / 1024 > sizeLimit) {
|
||||
ElMessage.error(`${rawFile.name} 超过 ${sizeLimit}MB,已跳过`)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -372,12 +399,13 @@ const handleFileChange = (file) => {
|
||||
const exists = pendingFiles.value.some(f => f.name === rawFile.name && f.size === rawFile.size)
|
||||
if (exists) return
|
||||
|
||||
// 添加到待上传列表,生成本地预览URL
|
||||
const fileIsImage = rawFile.type.startsWith('image/')
|
||||
pendingFiles.value.push({
|
||||
raw: rawFile,
|
||||
name: rawFile.name,
|
||||
size: rawFile.size,
|
||||
previewUrl: URL.createObjectURL(rawFile)
|
||||
isImage: fileIsImage,
|
||||
previewUrl: fileIsImage ? URL.createObjectURL(rawFile) : ''
|
||||
})
|
||||
}
|
||||
|
||||
@@ -593,6 +621,25 @@ const handleConfirm = () => {
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.file-type-icon {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.file-ext {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: #606266;
|
||||
max-width: 60px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.file-info {
|
||||
text-align: center;
|
||||
}
|
||||
@@ -670,6 +717,14 @@ const handleConfirm = () => {
|
||||
border: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.pending-file-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #f5f7fa;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.pending-name {
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
|
||||
+256
-14
@@ -108,6 +108,7 @@
|
||||
</div>
|
||||
|
||||
<div class="sig-card-footer">
|
||||
<el-button size="small" @click="handleDetail(item)">详情</el-button>
|
||||
<el-button type="primary" size="small" @click="handleEdit(item)">编辑</el-button>
|
||||
<el-button v-if="item.status === 0" type="warning" size="small" @click="handleSubmit(item)">提交审核</el-button>
|
||||
<el-button v-if="item.status === 1" type="success" size="small" @click="handleApprove(item)">通过</el-button>
|
||||
@@ -159,18 +160,52 @@
|
||||
<el-form-item label="签名标题" prop="title">
|
||||
<el-input v-model="form.title" placeholder="如:某某公司" maxlength="30" show-word-limit />
|
||||
</el-form-item>
|
||||
|
||||
<el-divider content-position="left">申请人资料</el-divider>
|
||||
<el-form-item label="申请人姓名">
|
||||
<el-input v-model="form.applicant_name" placeholder="申请人真实姓名" />
|
||||
<el-input v-model="form.applicant_name" placeholder="申请人真实姓名(可选)" />
|
||||
</el-form-item>
|
||||
<el-form-item label="身份证号">
|
||||
<el-input v-model="form.applicant_id_card" placeholder="申请人身份证号" />
|
||||
<el-form-item label="申请人身份证号" prop="applicant_id_card">
|
||||
<el-input v-model="form.applicant_id_card" placeholder="申请人身份证号(可选)" />
|
||||
</el-form-item>
|
||||
<el-form-item label="公司名称">
|
||||
<el-input v-model="form.applicant_company" placeholder="申请人公司(可选)" />
|
||||
|
||||
<el-divider content-position="left">主体资料</el-divider>
|
||||
<el-form-item label="主体名称">
|
||||
<el-input v-model="form.applicant_company" placeholder="企业/主体名称(可选)" />
|
||||
</el-form-item>
|
||||
<el-form-item label="统一社会信用代码">
|
||||
<el-input v-model="form.credit_code" placeholder="统一社会信用代码(可选)" />
|
||||
</el-form-item>
|
||||
<el-form-item label="营业执照URL">
|
||||
<el-input v-model="form.license_url" placeholder="营业执照图片地址(可选)" />
|
||||
</el-form-item>
|
||||
|
||||
<el-divider content-position="left">法人资料</el-divider>
|
||||
<el-form-item label="法人姓名">
|
||||
<el-input v-model="form.legal_person_name" placeholder="法人姓名(可选)" />
|
||||
</el-form-item>
|
||||
<el-form-item label="法人手机号" prop="legal_person_phone">
|
||||
<el-input v-model="form.legal_person_phone" placeholder="法人手机号(可选)" />
|
||||
</el-form-item>
|
||||
<el-form-item label="法人身份证号" prop="legal_person_id_card">
|
||||
<el-input v-model="form.legal_person_id_card" placeholder="法人身份证号(可选)" />
|
||||
</el-form-item>
|
||||
|
||||
<el-divider content-position="left">经办人资料</el-divider>
|
||||
<el-form-item label="经办人手机号" prop="agent_phone">
|
||||
<el-input v-model="form.agent_phone" placeholder="经办人手机号(可选)" />
|
||||
</el-form-item>
|
||||
<el-form-item label="身份证正面URL">
|
||||
<el-input v-model="form.agent_id_card_front_url" placeholder="经办人身份证正面图片地址(可选)" />
|
||||
</el-form-item>
|
||||
<el-form-item label="身份证反面URL">
|
||||
<el-input v-model="form.agent_id_card_back_url" placeholder="经办人身份证反面图片地址(可选)" />
|
||||
</el-form-item>
|
||||
|
||||
<el-divider content-position="left">承诺函</el-divider>
|
||||
<el-form-item label="承诺函URL">
|
||||
<el-input v-model="form.commitment_letter_url" placeholder="签署盖章后的承诺函照片地址(可选)" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
@@ -191,6 +226,77 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 详情对话框 -->
|
||||
<el-dialog v-model="detailVisible" title="签名详情" width="640px" append-to-body destroy-on-close>
|
||||
<div v-if="detailData" class="detail-wrap">
|
||||
<el-descriptions title="基础信息" :column="2" border size="small">
|
||||
<el-descriptions-item label="签名标题">{{ detailData.title || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag :type="statusTagType(detailData.status)" size="small" effect="dark">{{ statusText(detailData.status) }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="用户">
|
||||
<el-link type="primary" :underline="false" @click="$router.push({ path: '/user/detail', query: { user_id: detailData.user_id } })">ID: {{ detailData.user_id }}</el-link>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">{{ formatDate(detailData.CreatedAt) }}</el-descriptions-item>
|
||||
<el-descriptions-item v-if="detailData.reject_reason" label="驳回原因" :span="2">
|
||||
<span class="reject-text">{{ detailData.reject_reason }}</span>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<el-descriptions title="申请人资料" :column="2" border size="small" class="detail-section">
|
||||
<el-descriptions-item label="申请人姓名">{{ detailData.applicant_name || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="申请人身份证号">{{ detailData.applicant_id_card || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<el-descriptions title="主体资料" :column="2" border size="small" class="detail-section">
|
||||
<el-descriptions-item label="主体名称">{{ detailData.applicant_company || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="统一社会信用代码">{{ detailData.credit_code || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<el-descriptions title="法人资料" :column="2" border size="small" class="detail-section">
|
||||
<el-descriptions-item label="法人姓名">{{ detailData.legal_person_name || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="法人手机号">{{ detailData.legal_person_phone || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="法人身份证号" :span="2">{{ detailData.legal_person_id_card || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<el-descriptions title="经办人资料" :column="2" border size="small" class="detail-section">
|
||||
<el-descriptions-item label="经办人手机号">{{ detailData.agent_phone || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<div class="detail-section detail-images-title">证件与文件</div>
|
||||
<div class="detail-images">
|
||||
<div class="detail-img-item">
|
||||
<span class="detail-img-label">营业执照</span>
|
||||
<el-image v-if="detailData.license_url" :src="detailData.license_url" :preview-src-list="[detailData.license_url]" fit="cover" class="detail-img" preview-teleported />
|
||||
<div v-else class="detail-img-empty">未上传</div>
|
||||
</div>
|
||||
<div class="detail-img-item">
|
||||
<span class="detail-img-label">身份证正面</span>
|
||||
<el-image v-if="detailData.agent_id_card_front_url" :src="detailData.agent_id_card_front_url" :preview-src-list="[detailData.agent_id_card_front_url]" fit="cover" class="detail-img" preview-teleported />
|
||||
<div v-else class="detail-img-empty">未上传</div>
|
||||
</div>
|
||||
<div class="detail-img-item">
|
||||
<span class="detail-img-label">身份证反面</span>
|
||||
<el-image v-if="detailData.agent_id_card_back_url" :src="detailData.agent_id_card_back_url" :preview-src-list="[detailData.agent_id_card_back_url]" fit="cover" class="detail-img" preview-teleported />
|
||||
<div v-else class="detail-img-empty">未上传</div>
|
||||
</div>
|
||||
<div class="detail-img-item">
|
||||
<span class="detail-img-label">承诺函</span>
|
||||
<el-image v-if="detailData.commitment_letter_url" :src="detailData.commitment_letter_url" :preview-src-list="[detailData.commitment_letter_url]" fit="cover" class="detail-img" preview-teleported />
|
||||
<div v-else class="detail-img-empty">未上传</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-file-row">
|
||||
<span class="detail-img-label">申请文件</span>
|
||||
<el-link v-if="detailData.application_file_url" type="primary" :href="detailData.application_file_url" target="_blank">查看申请文件</el-link>
|
||||
<span v-else class="detail-empty-text">未上传</span>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="detailVisible = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 表单用户选择器 -->
|
||||
<UserListSelector
|
||||
v-model="userSelectorVisible"
|
||||
@@ -229,17 +335,64 @@ const queryParams = reactive({ service_id: null, page: 1, count: 10, user_id: ''
|
||||
const dialogVisible = ref(false)
|
||||
const dialogType = ref('add')
|
||||
const formRef = ref(null)
|
||||
const form = reactive({ service_id: null, user_id: null, title: '', applicant_name: '', applicant_id_card: '', applicant_company: '', license_url: '' })
|
||||
|
||||
const createEmptyForm = () => ({
|
||||
service_id: null,
|
||||
user_id: null,
|
||||
title: '',
|
||||
// 主体资料
|
||||
applicant_company: '',
|
||||
credit_code: '',
|
||||
license_url: '',
|
||||
// 法人资料
|
||||
legal_person_name: '',
|
||||
legal_person_phone: '',
|
||||
legal_person_id_card: '',
|
||||
// 经办人资料
|
||||
applicant_name: '',
|
||||
agent_phone: '',
|
||||
applicant_id_card: '',
|
||||
agent_id_card_front_url: '',
|
||||
agent_id_card_back_url: '',
|
||||
commitment_letter_url: ''
|
||||
})
|
||||
|
||||
const form = reactive(createEmptyForm())
|
||||
const originalEditForm = ref(null)
|
||||
|
||||
// 非必填字段:仅当填写时才校验格式,接口未声明必填的字段不设为必填
|
||||
const validatePhone = (rule, value, callback) => {
|
||||
if (value && !/^1[3-9]\d{9}$/.test(value)) {
|
||||
callback(new Error('请输入正确的手机号'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
const validateIdCard = (rule, value, callback) => {
|
||||
if (value && !/(^\d{15}$)|(^\d{17}[\dXx]$)/.test(value)) {
|
||||
callback(new Error('请输入正确的身份证号'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
const formRules = {
|
||||
service_id: [{ required: true, message: '请选择服务', trigger: 'change' }],
|
||||
user_id: [{ required: true, message: '请输入用户ID', trigger: 'blur' }],
|
||||
title: [{ required: true, message: '请输入签名标题', trigger: 'blur' }]
|
||||
title: [{ required: true, message: '请输入签名标题', trigger: 'blur' }],
|
||||
legal_person_phone: [{ validator: validatePhone, trigger: 'blur' }],
|
||||
agent_phone: [{ validator: validatePhone, trigger: 'blur' }],
|
||||
legal_person_id_card: [{ validator: validateIdCard, trigger: 'blur' }],
|
||||
applicant_id_card: [{ validator: validateIdCard, trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const rejectDialogVisible = ref(false)
|
||||
const rejectReason = ref('')
|
||||
const currentRow = ref(null)
|
||||
|
||||
const detailVisible = ref(false)
|
||||
const detailData = ref(null)
|
||||
|
||||
const userSelectorVisible = ref(false)
|
||||
const selectedUserInfo = ref(null)
|
||||
const filterUserSelectorVisible = ref(false)
|
||||
@@ -308,26 +461,42 @@ const clearUser = () => {
|
||||
selectedUserInfo.value = null
|
||||
}
|
||||
|
||||
const handleDetail = (row) => {
|
||||
detailData.value = row
|
||||
detailVisible.value = true
|
||||
}
|
||||
|
||||
const handleAdd = () => {
|
||||
dialogType.value = 'add'
|
||||
selectedUserInfo.value = null
|
||||
Object.assign(form, { service_id: queryParams.service_id, user_id: null, title: '', applicant_name: '', applicant_id_card: '', applicant_company: '', license_url: '' })
|
||||
originalEditForm.value = null
|
||||
Object.assign(form, createEmptyForm(), { service_id: queryParams.service_id })
|
||||
delete form.signature_id
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleEdit = (row) => {
|
||||
dialogType.value = 'edit'
|
||||
selectedUserInfo.value = { user_id: row.user_id, user_name: `用户${row.user_id}` }
|
||||
Object.assign(form, {
|
||||
Object.assign(form, createEmptyForm(), {
|
||||
service_id: queryParams.service_id,
|
||||
signature_id: row.ID,
|
||||
user_id: row.user_id,
|
||||
title: row.title,
|
||||
applicant_name: row.applicant_name || '',
|
||||
applicant_id_card: row.applicant_id_card || '',
|
||||
applicant_company: row.applicant_company || '',
|
||||
license_url: row.license_url || ''
|
||||
credit_code: row.credit_code || '',
|
||||
license_url: row.license_url || '',
|
||||
legal_person_name: row.legal_person_name || '',
|
||||
legal_person_phone: row.legal_person_phone || '',
|
||||
legal_person_id_card: row.legal_person_id_card || '',
|
||||
applicant_name: row.applicant_name || '',
|
||||
agent_phone: row.agent_phone || '',
|
||||
applicant_id_card: row.applicant_id_card || '',
|
||||
agent_id_card_front_url: row.agent_id_card_front_url || '',
|
||||
agent_id_card_back_url: row.agent_id_card_back_url || '',
|
||||
commitment_letter_url: row.commitment_letter_url || ''
|
||||
})
|
||||
originalEditForm.value = { ...form }
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
@@ -336,11 +505,34 @@ const handleFormSubmit = () => {
|
||||
if (!valid) return
|
||||
submitting.value = true
|
||||
try {
|
||||
const data = { ...form }
|
||||
let res
|
||||
if (dialogType.value === 'add') {
|
||||
res = await createSmsSignature(data)
|
||||
res = await createSmsSignature({ ...form })
|
||||
} else {
|
||||
const data = {
|
||||
service_id: form.service_id,
|
||||
signature_id: form.signature_id
|
||||
}
|
||||
const editableFields = [
|
||||
'title',
|
||||
'applicant_name',
|
||||
'applicant_id_card',
|
||||
'applicant_company',
|
||||
'license_url',
|
||||
'credit_code',
|
||||
'legal_person_name',
|
||||
'legal_person_phone',
|
||||
'legal_person_id_card',
|
||||
'agent_phone',
|
||||
'agent_id_card_front_url',
|
||||
'agent_id_card_back_url',
|
||||
'commitment_letter_url'
|
||||
]
|
||||
editableFields.forEach((field) => {
|
||||
if (form[field] !== originalEditForm.value?.[field]) {
|
||||
data[field] = form[field]
|
||||
}
|
||||
})
|
||||
res = await updateSmsSignature(data)
|
||||
}
|
||||
if (res.data.code === 200) {
|
||||
@@ -546,4 +738,54 @@ onMounted(async () => {
|
||||
border-top: 1px solid #f0f2f5;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* 详情弹窗 */
|
||||
.detail-wrap { max-height: 60vh; overflow-y: auto; padding-right: 4px; }
|
||||
.detail-section { margin-top: 16px; }
|
||||
.detail-images-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.detail-images {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.detail-img-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
.detail-img-label {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
.detail-img {
|
||||
width: 140px;
|
||||
height: 90px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #ebeef5;
|
||||
cursor: pointer;
|
||||
}
|
||||
.detail-img-empty {
|
||||
width: 140px;
|
||||
height: 90px;
|
||||
border-radius: 6px;
|
||||
background: #f5f7fa;
|
||||
border: 1px dashed #dcdfe6;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #c0c4cc;
|
||||
font-size: 12px;
|
||||
}
|
||||
.detail-file-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
.detail-empty-text { color: #c0c4cc; font-size: 13px; }
|
||||
</style>
|
||||
|
||||
@@ -303,7 +303,7 @@
|
||||
<div v-else-if="settingForm.type === 'file'" class="file-upload-section">
|
||||
<div class="file-info-display" v-if="settingForm.value && fileInfo">
|
||||
<div class="file-item" :class="{ 'uploading': fileUploading && fileInfo.isLocal }">
|
||||
<div class="file-preview" v-if="fileInfo.url || fileInfo.localUrl" @click="openImageSelector" style="cursor: pointer;">
|
||||
<div class="file-preview" v-if="isImageFileName(fileInfo.realName || fileInfo.saveName) && (fileInfo.url || fileInfo.localUrl)" @click="openImageSelector" style="cursor: pointer;">
|
||||
<img :src="fileInfo.localUrl || processImageUrl(fileInfo.url)" :alt="fileInfo.realName" class="preview-image" @click.stop="previewImage(fileInfo.localUrl || fileInfo.url)" @error="handleImageError" />
|
||||
<div v-if="fileUploading && fileInfo.isLocal" class="upload-overlay">
|
||||
<el-icon class="is-loading"><Loading /></el-icon>
|
||||
@@ -313,6 +313,11 @@
|
||||
<div class="file-preview" v-else @click="openImageSelector" style="cursor: pointer;">
|
||||
<div class="file-placeholder">
|
||||
<el-icon><Document /></el-icon>
|
||||
<span v-if="fileInfo.realName" class="file-ext-label">{{ getFileExtLabel(fileInfo.realName) }}</span>
|
||||
</div>
|
||||
<div v-if="fileUploading && fileInfo.isLocal" class="upload-overlay">
|
||||
<el-icon class="is-loading"><Loading /></el-icon>
|
||||
<span>上传中...</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="file-details">
|
||||
@@ -321,8 +326,8 @@
|
||||
<span class="file-size">{{ formatFileSize(fileInfo.size) }}</span>
|
||||
</div>
|
||||
<div class="file-actions">
|
||||
<el-button type="primary" size="small" @click="openImageSelector" :icon="Picture">
|
||||
修改图片
|
||||
<el-button type="primary" size="small" @click="openImageSelector" :icon="FolderOpened">
|
||||
修改文件
|
||||
</el-button>
|
||||
<el-button type="danger" size="small" @click="clearFile">删除</el-button>
|
||||
</div>
|
||||
@@ -333,10 +338,10 @@
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="openImageSelector"
|
||||
:icon="Picture"
|
||||
:icon="FolderOpened"
|
||||
class="image-selector-btn"
|
||||
>
|
||||
从文件库选择图片
|
||||
从文件库选择文件
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -346,7 +351,7 @@
|
||||
<div class="file-list-header">
|
||||
<span class="list-title">文件列表 ({{ getEditableFormFileList().length }} 项)</span>
|
||||
<div class="header-actions">
|
||||
<el-button size="small" @click="openImageSelectorForList" :icon="Picture">
|
||||
<el-button size="small" @click="openImageSelectorForList" :icon="FolderOpened">
|
||||
从文件库选择
|
||||
</el-button>
|
||||
</div>
|
||||
@@ -366,7 +371,7 @@
|
||||
</div>
|
||||
<div class="file-preview">
|
||||
<img
|
||||
v-if="fileItem.url || fileItem.localUrl"
|
||||
v-if="isImageFileName(fileItem.realName || fileItem.saveName) && (fileItem.url || fileItem.localUrl)"
|
||||
:src="fileItem.localUrl || processImageUrl(fileItem.url)"
|
||||
:alt="fileItem.realName"
|
||||
class="preview-image"
|
||||
@@ -375,6 +380,7 @@
|
||||
/>
|
||||
<div v-else class="file-placeholder">
|
||||
<el-icon><Document /></el-icon>
|
||||
<span v-if="fileItem.realName || fileItem.saveName" class="file-ext-label">{{ getFileExtLabel(fileItem.realName || fileItem.saveName) }}</span>
|
||||
</div>
|
||||
<div v-if="fileItem.uploading" class="upload-overlay">
|
||||
<el-icon class="is-loading"><Loading /></el-icon>
|
||||
@@ -509,7 +515,7 @@
|
||||
<div class="file-thumb" @click="openStructFileSelector('struct', field.key)">
|
||||
<img v-if="structFileUrls[field.key]" :src="structFileUrls[field.key]" @error="(e) => e.target.style.display='none'" />
|
||||
<div v-else class="file-thumb-empty">
|
||||
<el-icon><Picture /></el-icon>
|
||||
<el-icon><Document /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<el-button v-if="structValue[field.key]" type="danger" link size="small" class="file-clear-btn" @click="structValue[field.key] = ''; delete structFileUrls[field.key]"><el-icon><Delete /></el-icon></el-button>
|
||||
@@ -525,7 +531,7 @@
|
||||
>
|
||||
<div class="file-thumb" @click="previewFile(fid)">
|
||||
<img v-if="structFileUrls[field.key + '_' + fi]" :src="structFileUrls[field.key + '_' + fi]" @error="(e) => e.target.style.display='none'" />
|
||||
<div v-else class="file-thumb-empty"><el-icon><Picture /></el-icon></div>
|
||||
<div v-else class="file-thumb-empty"><el-icon><Document /></el-icon></div>
|
||||
</div>
|
||||
<el-button class="thumb-del-btn" type="danger" link size="small" @click="removeInlineListItem('file_list', 'struct', field.key, null, -1, fi)"><el-icon><Delete /></el-icon></el-button>
|
||||
</div>
|
||||
@@ -591,7 +597,7 @@
|
||||
<div v-else-if="sub.type === 'file'" class="struct-file-picker">
|
||||
<div class="file-thumb" @click="openStructFileSelector('struct-nested', field.key, sub.key)">
|
||||
<img v-if="structFileUrls[field.key + '.' + sub.key]" :src="structFileUrls[field.key + '.' + sub.key]" @error="(e) => e.target.style.display='none'" />
|
||||
<div v-else class="file-thumb-empty"><el-icon><Picture /></el-icon></div>
|
||||
<div v-else class="file-thumb-empty"><el-icon><Document /></el-icon></div>
|
||||
</div>
|
||||
<el-button v-if="getNestedValue(field.key)[sub.key]" type="danger" link size="small" class="file-clear-btn" @click="getNestedValue(field.key)[sub.key] = ''; delete structFileUrls[field.key + '.' + sub.key]"><el-icon><Delete /></el-icon></el-button>
|
||||
</div>
|
||||
@@ -606,7 +612,7 @@
|
||||
>
|
||||
<div class="file-thumb" @click="previewFile(fid)">
|
||||
<img v-if="structFileUrls[field.key + '.' + sub.key + '_' + fi]" :src="structFileUrls[field.key + '.' + sub.key + '_' + fi]" @error="(e) => e.target.style.display='none'" />
|
||||
<div v-else class="file-thumb-empty"><el-icon><Picture /></el-icon></div>
|
||||
<div v-else class="file-thumb-empty"><el-icon><Document /></el-icon></div>
|
||||
</div>
|
||||
<el-button class="thumb-del-btn" type="danger" link size="small" @click="removeInlineListItem('file_list', 'struct-nested', field.key, sub.key, -1, fi)"><el-icon><Delete /></el-icon></el-button>
|
||||
</div>
|
||||
@@ -800,7 +806,7 @@
|
||||
<div v-else-if="field.type === 'file'" class="struct-file-picker">
|
||||
<div class="file-thumb small" @click="openStructFileSelector('struct-list', field.key, null, itemIndex)">
|
||||
<img v-if="structFileUrls['list_' + itemIndex + '_' + field.key]" :src="structFileUrls['list_' + itemIndex + '_' + field.key]" @error="(e) => e.target.style.display='none'" />
|
||||
<div v-else class="file-thumb-empty"><el-icon><Picture /></el-icon></div>
|
||||
<div v-else class="file-thumb-empty"><el-icon><Document /></el-icon></div>
|
||||
</div>
|
||||
<el-button v-if="item[field.key]" type="danger" link size="small" class="file-clear-btn" @click="item[field.key] = ''; delete structFileUrls['list_' + itemIndex + '_' + field.key]"><el-icon><Delete /></el-icon></el-button>
|
||||
</div>
|
||||
@@ -815,7 +821,7 @@
|
||||
>
|
||||
<div class="file-thumb small" @click="previewFile(fid)">
|
||||
<img v-if="structFileUrls['list_' + itemIndex + '_' + field.key + '_' + fi]" :src="structFileUrls['list_' + itemIndex + '_' + field.key + '_' + fi]" @error="(e) => e.target.style.display='none'" />
|
||||
<div v-else class="file-thumb-empty"><el-icon><Picture /></el-icon></div>
|
||||
<div v-else class="file-thumb-empty"><el-icon><Document /></el-icon></div>
|
||||
</div>
|
||||
<el-button class="thumb-del-btn" type="danger" link size="small" @click="removeInlineListItem('file_list', 'struct-list', field.key, null, itemIndex, fi)"><el-icon><Delete /></el-icon></el-button>
|
||||
</div>
|
||||
@@ -1056,11 +1062,12 @@
|
||||
<img :src="currentViewImage" style="max-width: 100%; max-height: 80vh;" />
|
||||
</el-dialog>
|
||||
|
||||
<!-- 图片选择器 -->
|
||||
<!-- 文件选择器 -->
|
||||
<ImageSelector
|
||||
v-model="imageSelectorVisible"
|
||||
:current-file-id="currentImageSelectorFileId"
|
||||
:multiple="imageSelectorMode === 'list' || imageSelectorMode === 'struct-file-list'"
|
||||
:accept-all="true"
|
||||
@confirm="handleImageSelectorConfirm"
|
||||
/>
|
||||
</div>
|
||||
@@ -1070,7 +1077,7 @@
|
||||
import { ref, reactive, onMounted, watch, nextTick, computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Search, Plus, Delete, UploadFilled, Folder, Document, Loading, Picture, Edit, Rank, FolderAdd, CopyDocument, CircleCheck, CircleClose, InfoFilled, Clock, Setting, EditPen, Connection, ArrowRight, View } from '@element-plus/icons-vue'
|
||||
import { Search, Plus, Delete, UploadFilled, Folder, FolderOpened, Document, Loading, Picture, Edit, Rank, FolderAdd, CopyDocument, CircleCheck, CircleClose, InfoFilled, Clock, Setting, EditPen, Connection, ArrowRight, View } from '@element-plus/icons-vue'
|
||||
import {
|
||||
getSettingGroupList,
|
||||
getSettingGroupInfo,
|
||||
@@ -2213,6 +2220,18 @@ const formatFileSize = (bytes) => {
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||||
}
|
||||
|
||||
const isImageFileName = (name) => {
|
||||
if (!name) return false
|
||||
return /\.(jpe?g|png|gif|webp|bmp|svg|ico|avif)$/i.test(name)
|
||||
}
|
||||
|
||||
const getFileExtLabel = (name) => {
|
||||
if (!name) return ''
|
||||
const dotIndex = name.lastIndexOf('.')
|
||||
if (dotIndex === -1) return ''
|
||||
return name.slice(dotIndex + 1).toUpperCase()
|
||||
}
|
||||
|
||||
const truncateFileName = (fileName, maxLength = 25) => {
|
||||
if (!fileName) return ''
|
||||
if (fileName.length <= maxLength) return fileName
|
||||
@@ -3485,13 +3504,22 @@ onMounted(() => {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
background: #f5f7fa;
|
||||
color: #909399;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.file-ext-label {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: #606266;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.file-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
Reference in New Issue
Block a user