前端文件上傳同名沖突檢測(cè)的詳細(xì)解決方案
在檔案管理系統(tǒng)中,用戶在同一目錄下上傳同名文件時(shí),系統(tǒng)沒有任何提示,新文件被靜默忽略。本文記錄這個(gè)文件重復(fù)校驗(yàn)問題的前后端協(xié)同解決方案。
一、問題背景
1.1 問題現(xiàn)象
| 操作步驟 | 預(yù)期結(jié)果 | 實(shí)際結(jié)果 |
|---|---|---|
| 1. 選擇三級(jí)目錄"規(guī)章制度" | - | - |
2. 上傳文件 合同.pdf | 上傳成功 | 上傳成功 |
| 3. 再次選擇同一目錄 | - | - |
4. 上傳另一個(gè) 合同.pdf | 提示"已存在同個(gè)名稱的文件" | 無(wú)任何提示,新文件被忽略 |
用戶困惑:明明上傳了新文件,但列表里只有第一次上傳的內(nèi)容。
1.2 問題影響
- 用戶誤以為上傳成功,實(shí)際新文件丟失
- 無(wú)法通過上傳覆蓋更新文件
- 用戶體驗(yàn)差,容易造成數(shù)據(jù)丟失
1.3 修復(fù)歷程
| 時(shí)間 | 操作 | 結(jié)果 |
|---|---|---|
| 12-11 | 創(chuàng)建問題 | - |
| 12-11 | AI分析并修復(fù) | 提交前端+后端代碼 |
| 12-12 | 合并代碼 | 驗(yàn)證通過 |
| 12-25 | 驗(yàn)收關(guān)閉 | 功能正常 |
激活次數(shù):0次(一次修復(fù)成功)
二、問題分析
2.1 業(yè)務(wù)場(chǎng)景
檔案管理采用三級(jí)目錄結(jié)構(gòu):
項(xiàng)目文檔
├── 規(guī)章制度
│ ├── 運(yùn)營(yíng)管理
│ │ ├── 合同.pdf ← 已存在
│ │ └── 規(guī)定.docx
│ └── 用戶協(xié)議
│ └── 協(xié)議.pdf
└── 會(huì)議記錄
└── ...
用戶上傳文件時(shí)需要:
- 選擇目標(biāo)分類(三級(jí)目錄)
- 輸入文件名稱
- 上傳文件
2.2 問題根因
問題1:前端去重邏輯缺陷
原有代碼:
// 提交時(shí)的去重邏輯
const submit = async () => {
const allFiles = [...categoryFiles, ...processedFiles]
// 問題:按 fileName 去重,但 fileName 可能不含擴(kuò)展名
const uniqueFiles = []
const fileNameSet = new Set()
for (const file of allFiles) {
const name = file.fileName || '' // 可能是 "合同" 而非 "合同.pdf"
if (name && !fileNameSet.has(name)) {
fileNameSet.add(name)
uniqueFiles.push(file)
}
}
// 問題:重復(fù)文件被靜默過濾,沒有任何提示!
}
問題分析:
fileName字段可能不含擴(kuò)展名,導(dǎo)致合同.pdf和合同.docx被視為同名- 去重時(shí)直接過濾,沒有給用戶任何反饋
- 用戶以為上傳成功,實(shí)際新文件被忽略
問題2:后端校驗(yàn)缺失
原有代碼:
public ResponseDTO<String> update(CommunityArchiveUpdateDTO dto) {
// 問題:只檢查 dto.getFileName(),但這個(gè)字段通常為空
// 實(shí)際的文件名在 files 數(shù)組的每個(gè)對(duì)象中
if (StringUtils.isNotBlank(dto.getFileName())) {
// 這個(gè)分支幾乎不會(huì)進(jìn)入
checkDuplicate(dto.getFileName());
}
// 直接保存,沒有校驗(yàn) files 中的重復(fù)文件
archiveMapper.update(dto);
return ResponseDTO.ok();
}
問題分析:
- 校驗(yàn)邏輯依賴
fileName字段,但該字段通常為空 - 真正的文件名在
files數(shù)組中,但沒有被校驗(yàn) - 后端作為最后防線,沒有起到應(yīng)有的攔截作用
2.3 問題傳導(dǎo)鏈
用戶選擇同名文件上傳
↓
前端:fileName 不含擴(kuò)展名,比對(duì)失敗
↓
前端:靜默去重,不提示用戶
↓
后端:fileName 為空,跳過校驗(yàn)
↓
數(shù)據(jù)庫(kù):只保留第一個(gè)文件
↓
用戶:以為上傳成功,實(shí)際新文件丟失
三、解決方案
3.1 前端修復(fù):提交前校驗(yàn)同名文件
核心思路:
- 獲取完整文件名(含擴(kuò)展名)
- 查找目標(biāo)分類下的現(xiàn)有文件
- 比對(duì)是否存在同名文件
- 存在則提示并阻斷提交
修復(fù)代碼:
const submit = async () => {
if (!isFormValid.value) return
const userFileName = fileName.value.trim()
// ========== 步驟1:獲取完整文件名(含擴(kuò)展名)==========
const getFullFileName = (item) => {
if (typeof item === 'string') {
return userFileName
}
// 優(yōu)先使用上傳時(shí)構(gòu)造的完整文件名
const directName = item.fileNameWithExt || item.originalName
if (directName && directName.includes('.')) {
return directName
}
// 兜底:從原始文件名中提取擴(kuò)展名后拼接
const original = item.originalFileName || item.name || ''
if (original.includes('.')) {
const ext = original.substring(original.lastIndexOf('.'))
return userFileName + ext
}
return item.fileName || userFileName
}
// 獲取所有新上傳文件的完整文件名
const newFullFileNames = urls.value.map(getFullFileName)
// ========== 步驟2:查找目標(biāo)分類下的現(xiàn)有文件 ==========
let categoryFiles = []
if (originalCategoryData.value && originalCategoryData.value.length > 0) {
let targetNode = null
// 遍歷三級(jí)目錄結(jié)構(gòu),找到目標(biāo)分類節(jié)點(diǎn)
for (const firstLevel of originalCategoryData.value) {
if (firstLevel.name === firstSelected.value) {
if (!secondSelected.value) {
targetNode = firstLevel
break
}
if (firstLevel.children) {
for (const secondLevel of firstLevel.children) {
if (secondLevel.name === secondSelected.value) {
if (!thirdSelected.value) {
targetNode = secondLevel
break
}
if (secondLevel.children) {
for (const thirdLevel of secondLevel.children) {
if (thirdLevel.name === thirdSelected.value) {
targetNode = thirdLevel
break
}
}
}
}
}
}
}
}
// 獲取該分類下的現(xiàn)有文件
if (targetNode && targetNode.files && Array.isArray(targetNode.files)) {
categoryFiles = targetNode.files
}
}
// ========== 步驟3:檢查是否存在同名文件 ==========
const normalizeName = (name) => (name || '').trim().toLowerCase()
// 構(gòu)建現(xiàn)有文件名集合(忽略大小寫)
const existingNameSet = new Set(
categoryFiles
.map((f) => normalizeName(f.fileName || f.name || f.originalName))
.filter((n) => n)
)
// 檢查新文件是否與現(xiàn)有文件重名
const newNameSet = new Set()
for (const fullName of newFullFileNames) {
const normalized = normalizeName(fullName)
if (!normalized) continue
// 與現(xiàn)有文件重名,或與本次上傳的其他文件重名
if (existingNameSet.has(normalized) || newNameSet.has(normalized)) {
uni.showToast({
title: '已存在同個(gè)名稱的文件,請(qǐng)重新上傳',
icon: 'none',
duration: 3000,
})
return // 阻斷提交
}
newNameSet.add(normalized)
}
// ========== 步驟4:通過校驗(yàn),繼續(xù)提交 ==========
// ... 后續(xù)提交邏輯
}
3.2 后端修復(fù):基于實(shí)際文件名校驗(yàn)
核心思路:
- 從
files數(shù)組中提取所有文件名 - 與數(shù)據(jù)庫(kù)中該分類下的現(xiàn)有文件比對(duì)
- 發(fā)現(xiàn)重復(fù)則返回錯(cuò)誤提示
修復(fù)代碼:
@Service
public class CommunityArchiveService {
@Autowired
private FileService fileService;
@Autowired
private CommunityArchiveMapper archiveMapper;
/**
* 更新檔案(添加文件)
*/
public ResponseDTO<String> update(CommunityArchiveUpdateDTO dto) {
// 獲取現(xiàn)有文件列表
List<String> existingFileKeys = archiveMapper.getFileKeysByCategoryId(dto.getId());
// 獲取新上傳的文件keys
List<String> newFileKeys = dto.getFiles().stream()
.map(FileDTO::getFileKey)
.filter(Objects::nonNull)
.collect(Collectors.toList());
// 計(jì)算真正新增的文件(差集)
List<String> addedFileKeys = newFileKeys.stream()
.filter(key -> !existingFileKeys.contains(key))
.collect(Collectors.toList());
if (!addedFileKeys.isEmpty()) {
// 獲取已有文件的真實(shí)文件名
Set<String> existingFileNames = fileService.getFileNamesByKeys(existingFileKeys)
.stream()
.map(String::toLowerCase)
.collect(Collectors.toSet());
// 獲取新增文件的真實(shí)文件名
List<String> newFileNames = fileService.getFileNamesByKeys(addedFileKeys);
// 檢查是否有重復(fù)文件名
for (String newName : newFileNames) {
if (existingFileNames.contains(newName.toLowerCase())) {
return ResponseDTO.userErrorParam("已存在同個(gè)名稱的文件,請(qǐng)重新上傳");
}
}
}
// 通過校驗(yàn),執(zhí)行更新
archiveMapper.update(dto);
return ResponseDTO.ok();
}
}
3.3 文件上傳時(shí)保存完整文件名
前端上傳成功后的處理:
// 上傳成功后,保存完整的文件信息
uni.uploadFile({
// ... 上傳配置
success: (uploadRes) => {
const res = JSON.parse(uploadRes.data)
if (res.code === 0 && res.data) {
// 關(guān)鍵:保存完整文件名(含擴(kuò)展名)
const fileData = {
...res.data,
fileName: userFileName, // 用戶輸入的名稱
originalFileName: originalFileName, // 原始文件名
fileNameWithExt: userFileName + fileExtension // 完整文件名(含擴(kuò)展名)
}
// 保存到文件列表
selectFileList.value = [{
name: originalFileName,
displayName: userFileName,
url: fileData.fileUrl,
resData: fileData
}]
urls.value = [fileData]
}
}
})
四、完整Demo代碼
4.1 前端:文件上傳組件
<template>
<view class="upload-page">
<!-- 分類選擇 -->
<view class="form-item" @click="showCategoryPicker">
<text class="label">分類</text>
<view class="value">
<text>{{ selectedCategory || '請(qǐng)選擇文件分類' }}</text>
<uni-icons type="right" size="16" color="#CCC"></uni-icons>
</view>
</view>
<!-- 文件名輸入 -->
<view class="form-item">
<text class="label">名稱</text>
<input
class="input"
v-model="fileName"
placeholder="請(qǐng)輸入文件名稱"
:maxlength="15"
/>
</view>
<!-- 上傳區(qū)域 -->
<view class="upload-area" v-if="!uploadedFile">
<view
class="upload-btn"
:class="{ disabled: !canUpload }"
@click="handleUpload"
>
<uni-icons type="upload" size="40" :color="canUpload ? '#40E0D0' : '#CCC'" />
<text class="upload-text">點(diǎn)擊上傳文件</text>
<text class="upload-desc">支持常用文件格式,最大5MB</text>
</view>
</view>
<!-- 已上傳文件展示 -->
<view class="uploaded-file" v-else>
<view class="file-info">
<uni-icons type="paperclip" size="24" color="#40E0D0" />
<text class="file-name">{{ uploadedFile.displayName }}</text>
<text class="file-status">上傳成功</text>
</view>
<view class="file-actions">
<text class="action-btn delete" @click="removeFile">刪除</text>
<text class="action-btn reupload" @click="reUpload">重新上傳</text>
</view>
</view>
<!-- 底部按鈕 -->
<view class="footer">
<button class="btn cancel" @click="cancel">取消</button>
<button class="btn submit" :disabled="!isFormValid" @click="submit">確定</button>
</view>
</view>
</template>
<script setup>
import { ref, computed } from 'vue'
// 表單數(shù)據(jù)
const fileName = ref('')
const selectedCategory = ref('')
const selectedCategoryId = ref(null)
const uploadedFile = ref(null)
const categoryTree = ref([]) // 分類樹數(shù)據(jù)
// 分類選擇狀態(tài)
const firstSelected = ref('')
const secondSelected = ref('')
const thirdSelected = ref('')
// 表單驗(yàn)證
const canUpload = computed(() => {
return selectedCategory.value && fileName.value.trim()
})
const isFormValid = computed(() => {
return canUpload.value && uploadedFile.value
})
/**
* 處理文件上傳
*/
const handleUpload = () => {
if (!canUpload.value) {
uni.showToast({ title: '請(qǐng)先選擇分類和輸入文件名稱', icon: 'none' })
return
}
const userFileName = fileName.value.trim()
wx.chooseMessageFile({
count: 1,
type: 'all',
success: (res) => {
const tempFile = res.tempFiles[0]
const originalFileName = tempFile.name
// 獲取文件擴(kuò)展名
const fileExtension = originalFileName.substring(originalFileName.lastIndexOf('.'))
const fullFileName = userFileName + fileExtension
// 檢查文件大小
if (tempFile.size > 5 * 1024 * 1024) {
uni.showToast({ title: '文件大小不能超過5MB', icon: 'none' })
return
}
uni.showLoading({ title: '上傳中...', mask: true })
// 上傳文件
uni.uploadFile({
url: '/api/upload',
filePath: tempFile.path,
name: 'file',
success: (uploadRes) => {
const data = JSON.parse(uploadRes.data)
if (data.code === 0) {
// 保存文件信息,關(guān)鍵是保存完整文件名
uploadedFile.value = {
...data.data,
fileName: userFileName,
originalFileName: originalFileName,
fileNameWithExt: fullFileName, // 完整文件名(含擴(kuò)展名)
displayName: originalFileName
}
uni.showToast({ title: '上傳成功', icon: 'success' })
} else {
uni.showToast({ title: data.msg || '上傳失敗', icon: 'none' })
}
},
fail: () => {
uni.showToast({ title: '網(wǎng)絡(luò)錯(cuò)誤', icon: 'none' })
},
complete: () => {
uni.hideLoading()
}
})
}
})
}
/**
* 獲取完整文件名(含擴(kuò)展名)
*/
const getFullFileName = (file) => {
// 優(yōu)先使用已保存的完整文件名
if (file.fileNameWithExt) {
return file.fileNameWithExt
}
// 從原始文件名提取擴(kuò)展名
const original = file.originalFileName || file.name || ''
if (original.includes('.')) {
const ext = original.substring(original.lastIndexOf('.'))
return fileName.value.trim() + ext
}
return file.fileName || fileName.value.trim()
}
/**
* 查找目標(biāo)分類下的現(xiàn)有文件
*/
const findCategoryFiles = () => {
if (!categoryTree.value || categoryTree.value.length === 0) {
return []
}
let targetNode = null
// 遍歷三級(jí)目錄結(jié)構(gòu)
for (const first of categoryTree.value) {
if (first.name !== firstSelected.value) continue
if (!secondSelected.value) {
targetNode = first
break
}
for (const second of first.children || []) {
if (second.name !== secondSelected.value) continue
if (!thirdSelected.value) {
targetNode = second
break
}
for (const third of second.children || []) {
if (third.name === thirdSelected.value) {
targetNode = third
break
}
}
}
}
return targetNode?.files || []
}
/**
* 提交表單
*/
const submit = async () => {
if (!isFormValid.value) return
// ========== 重點(diǎn):提交前檢查同名文件 ==========
const fullFileName = getFullFileName(uploadedFile.value)
const categoryFiles = findCategoryFiles()
// 文件名歸一化(忽略大小寫)
const normalizeName = (name) => (name || '').trim().toLowerCase()
// 構(gòu)建現(xiàn)有文件名集合
const existingNames = new Set(
categoryFiles
.map(f => normalizeName(f.fileName || f.name))
.filter(Boolean)
)
// 檢查是否存在同名文件
if (existingNames.has(normalizeName(fullFileName))) {
uni.showToast({
title: '已存在同個(gè)名稱的文件,請(qǐng)重新上傳',
icon: 'none',
duration: 3000
})
return // 阻斷提交
}
// 通過校驗(yàn),提交數(shù)據(jù)
try {
const data = {
id: selectedCategoryId.value,
files: [{
...uploadedFile.value,
fileName: fullFileName // 使用完整文件名
}]
}
await addArchiveFile(data)
uni.showToast({ title: '新增成功', icon: 'success' })
setTimeout(() => {
uni.navigateBack()
}, 1500)
} catch (error) {
uni.showToast({ title: '新增失敗', icon: 'none' })
}
}
/**
* 移除文件
*/
const removeFile = () => {
uploadedFile.value = null
}
/**
* 重新上傳
*/
const reUpload = () => {
uploadedFile.value = null
handleUpload()
}
const cancel = () => {
uni.navigateBack()
}
</script>
<style scoped lang="scss">
.upload-page {
min-height: 100vh;
background: #f5f5f5;
padding: 16px;
}
.form-item {
display: flex;
align-items: center;
padding: 16px;
background: #fff;
margin-bottom: 1px;
.label {
width: 60px;
color: #666;
font-size: 14px;
}
.value {
flex: 1;
display: flex;
justify-content: space-between;
align-items: center;
color: #333;
}
.input {
flex: 1;
font-size: 14px;
}
}
.upload-area {
margin-top: 16px;
background: #fff;
border-radius: 8px;
padding: 24px;
}
.upload-btn {
display: flex;
flex-direction: column;
align-items: center;
padding: 40px;
border: 2px dashed #e5e5e5;
border-radius: 8px;
&.disabled {
opacity: 0.5;
}
.upload-text {
margin-top: 12px;
font-size: 16px;
color: #333;
}
.upload-desc {
margin-top: 8px;
font-size: 12px;
color: #999;
}
}
.uploaded-file {
margin-top: 16px;
background: #fff;
border-radius: 8px;
padding: 16px;
.file-info {
display: flex;
align-items: center;
gap: 12px;
}
.file-name {
flex: 1;
font-size: 14px;
color: #333;
}
.file-status {
font-size: 12px;
color: #52c41a;
}
.file-actions {
display: flex;
gap: 24px;
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid #eee;
}
.action-btn {
font-size: 14px;
&.delete { color: #ff4d4f; }
&.reupload { color: #40E0D0; }
}
}
.footer {
position: fixed;
bottom: 0;
left: 0;
right: 0;
display: flex;
gap: 16px;
padding: 16px;
background: #fff;
box-shadow: 0 -2px 8px rgba(0,0,0,0.05);
.btn {
flex: 1;
height: 44px;
border-radius: 8px;
font-size: 16px;
&.cancel {
background: #f5f5f5;
color: #666;
}
&.submit {
background: #40E0D0;
color: #fff;
&:disabled {
background: #ccc;
}
}
}
}
</style>
4.2 后端:檔案服務(wù)
@Service
@Slf4j
public class ArchiveService {
@Autowired
private ArchiveMapper archiveMapper;
@Autowired
private FileService fileService;
/**
* 添加檔案文件
* @param dto 檔案更新DTO
* @return 操作結(jié)果
*/
public ResponseDTO<String> addArchiveFile(ArchiveUpdateDTO dto) {
// 獲取該分類下現(xiàn)有的文件keys
List<String> existingFileKeys = archiveMapper.getFileKeysByCategoryId(dto.getId());
// 提取新上傳的文件keys
List<String> newFileKeys = dto.getFiles().stream()
.map(FileDTO::getFileKey)
.filter(Objects::nonNull)
.collect(Collectors.toList());
// 計(jì)算真正新增的文件(排除已存在的)
List<String> addedFileKeys = newFileKeys.stream()
.filter(key -> !existingFileKeys.contains(key))
.collect(Collectors.toList());
if (!addedFileKeys.isEmpty()) {
// 獲取現(xiàn)有文件的真實(shí)文件名(含擴(kuò)展名)
Set<String> existingFileNames = fileService.getFileNamesByKeys(existingFileKeys)
.stream()
.map(String::toLowerCase) // 忽略大小寫
.collect(Collectors.toSet());
// 獲取新增文件的真實(shí)文件名
List<String> newFileNames = fileService.getFileNamesByKeys(addedFileKeys);
// 校驗(yàn)是否有重復(fù)文件名
for (String newName : newFileNames) {
String normalizedName = newName.toLowerCase();
if (existingFileNames.contains(normalizedName)) {
log.warn("文件名重復(fù): {}, 分類ID: {}", newName, dto.getId());
return ResponseDTO.userErrorParam("已存在同個(gè)名稱的文件,請(qǐng)重新上傳");
}
}
}
// 通過校驗(yàn),執(zhí)行更新
archiveMapper.addFiles(dto.getId(), dto.getFiles());
log.info("檔案文件添加成功, 分類ID: {}, 文件數(shù)量: {}", dto.getId(), dto.getFiles().size());
return ResponseDTO.ok();
}
}
4.3 API接口定義
// api/archive.js
/**
* 獲取檔案分類樹
* @param {string} communityId - 組織ID
*/
export function getArchiveTree(communityId) {
return request({
url: '/archive/tree',
method: 'get',
params: { communityId }
})
}
/**
* 添加檔案文件
* @param {Object} data - 檔案數(shù)據(jù)
* @param {number} data.id - 分類ID
* @param {Array} data.files - 文件列表
*/
export function addArchiveFile(data) {
return request({
url: '/archive/add',
method: 'post',
data
})
}
五、經(jīng)驗(yàn)總結(jié)
5.1 文件重復(fù)校驗(yàn)的要點(diǎn)
| 要點(diǎn) | 說明 |
|---|---|
| 使用完整文件名 | 必須包含擴(kuò)展名,如 合同.pdf 而非 合同 |
| 忽略大小寫 | File.PDF 和 file.pdf 應(yīng)視為相同 |
| 前端先校驗(yàn) | 提升用戶體驗(yàn),避免無(wú)效請(qǐng)求 |
| 后端兜底 | 作為最后防線,確保數(shù)據(jù)一致性 |
| 明確提示 | 告知用戶具體原因,而非靜默失敗 |
5.2 文件名處理的最佳實(shí)踐
// 歸一化文件名(用于比較)
const normalizeName = (name) => {
return (name || '')
.trim() // 去除首尾空格
.toLowerCase() // 統(tǒng)一小寫
}
// 獲取完整文件名(含擴(kuò)展名)
const getFullFileName = (userInput, originalFile) => {
// 從原始文件名提取擴(kuò)展名
const ext = originalFile.substring(originalFile.lastIndexOf('.'))
return userInput + ext
}
// 校驗(yàn)文件名是否重復(fù)
const isDuplicate = (newName, existingNames) => {
const normalized = normalizeName(newName)
return existingNames.some(name => normalizeName(name) === normalized)
}
5.3 為什么需要前后端雙重校驗(yàn)?
| 層級(jí) | 作用 | 優(yōu)勢(shì) |
|---|---|---|
| 前端校驗(yàn) | 即時(shí)反饋 | 用戶體驗(yàn)好,減少服務(wù)器壓力 |
| 后端校驗(yàn) | 數(shù)據(jù)安全 | 防止繞過前端,確保數(shù)據(jù)一致 |
雙重校驗(yàn)的必要性:
- 前端代碼可被繞過(直接調(diào)用API)
- 多端訪問(小程序、H5、PC)可能邏輯不一致
- 并發(fā)上傳時(shí)可能產(chǎn)生競(jìng)態(tài)條件
5.4 本問題的核心教訓(xùn)
| 問題 | 教訓(xùn) |
|---|---|
| 靜默去重 | 任何數(shù)據(jù)處理都應(yīng)給用戶明確反饋 |
| 文件名不完整 | 文件名必須包含擴(kuò)展名才能準(zhǔn)確比對(duì) |
| 后端校驗(yàn)缺失 | 關(guān)鍵業(yè)務(wù)邏輯必須有后端兜底 |
| 大小寫敏感 | 文件名比較應(yīng)忽略大小寫 |
一句話總結(jié):文件上傳場(chǎng)景中,重復(fù)校驗(yàn)必須使用完整文件名(含擴(kuò)展名),且前后端都要做校驗(yàn),避免靜默失敗導(dǎo)致用戶數(shù)據(jù)丟失。
這個(gè)案例說明:任何影響用戶數(shù)據(jù)的操作,都不應(yīng)該靜默失敗。即使是"去重"這樣看似友好的功能,如果沒有明確提示,也會(huì)讓用戶困惑。前后端協(xié)同校驗(yàn)是保證數(shù)據(jù)安全和用戶體驗(yàn)的關(guān)鍵。
到此這篇關(guān)于前端文件上傳同名沖突檢測(cè)的詳細(xì)解決方案的文章就介紹到這了,更多相關(guān)前端文件上傳同名沖突檢測(cè)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
JavaScript如何實(shí)現(xiàn)圖片懶加載(lazyload) 提高用戶體驗(yàn)(增強(qiáng)版)
這篇文章主要介紹了JavaScript如何實(shí)現(xiàn)圖片懶加載(lazyload) 提高用戶體驗(yàn)(增強(qiáng)版)的相關(guān)資料,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2016-11-11
微信小程序?qū)崿F(xiàn)動(dòng)態(tài)設(shè)置頁(yè)面標(biāo)題的方法【附源碼下載】
這篇文章主要介紹了微信小程序?qū)崿F(xiàn)動(dòng)態(tài)設(shè)置頁(yè)面標(biāo)題的方法,涉及微信小程序button組件事件綁定及頁(yè)面元素屬性動(dòng)態(tài)設(shè)置相關(guān)實(shí)現(xiàn)技巧,并附帶完整源碼供讀者下載參考,需要的朋友可以參考下2017-11-11
JavaScript類型系統(tǒng)之基本數(shù)據(jù)類型與包裝類型
javascript的數(shù)據(jù)類型可以分為兩種:原始類型和引用類型,在此文給大家提到。本文主要給大家介紹javascript類型系統(tǒng)之基本數(shù)據(jù)類型與包裝類型,涉及到j(luò)s 基本類型 包裝類相關(guān)知識(shí),本文介紹的詳細(xì),具有參考借鑒價(jià)值,對(duì)本文感興趣的朋友一起學(xué)習(xí)吧2016-01-01
js實(shí)現(xiàn)鼠標(biāo)點(diǎn)擊頁(yè)面彈出自定義文字效果
這篇文章主要為大家詳細(xì)介紹了js實(shí)現(xiàn)鼠標(biāo)點(diǎn)擊頁(yè)面彈出自定義文字效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-12-12
Next.js項(xiàng)目實(shí)戰(zhàn)踩坑指南(筆記)
這篇文章主要介紹了Next.js項(xiàng)目實(shí)戰(zhàn)踩坑指南(小結(jié)),小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來(lái)看看吧2018-11-11
JS 組件系列之BootstrapTable的treegrid功能
這篇文章主要介紹了JS 組件系列之BootstrapTable的treegrid功能,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2017-06-06
js實(shí)現(xiàn)的點(diǎn)擊超鏈顯示隱藏層
js實(shí)現(xiàn)的點(diǎn)擊超鏈顯示隱藏層...2007-05-05

