巡检点上传图片修正

This commit is contained in:
Zy
2026-05-09 17:10:50 +08:00
parent e29e1c1346
commit b29e03ab4d
29 changed files with 2836 additions and 1189 deletions

View File

@@ -0,0 +1,261 @@
<template>
<view class="simple-uploader">
<!-- 图片列表 -->
<view class="image-list">
<!-- 已选图片 -->
<view v-for="(img, index) in innerList" :key="index" class="image-item">
<image :src="img.url || img" class="img" mode="aspectFill" @tap="preview(index)"></image>
<!-- 删除按钮 -->
<view class="delete-btn" @tap.stop="remove(index)">
<uni-icons type="closeempty" size="16" color="#fff"></uni-icons>
</view>
<!-- 上传状态 -->
<view v-if="img.uploading" class="uploading-mask">
<text>上传中...</text>
</view>
</view>
<!-- 添加按钮 -->
<view class="add-btn" @tap="choose">
<uni-icons color="#a6a6a6" type="plusempty" size="25"></uni-icons>
</view>
</view>
</view>
</template>
<script setup>
import { ref, watch } from 'vue'
// ======================
// Props(可配置)
// ======================
const props = defineProps({
// 双向绑定的图片列表
modelValue: {
type: Array,
default: () => []
},
/** 多选 最多上传数量 默认9*/
limit: {
type: Number,
default: 9
},
// 选图来源:album(相册)/ camera(拍照)
sourceType: {
type: Array,
default: () => ['album', 'camera']
},
// 图片大小:original(原图)/ compressed(压缩图)
sizeType: {
type: Array,
default: () => ['compressed']
},
// 后端上传接口地址(必填)
uploadUrl: {
type: String,
default: ''
},
// 后端接收的文件字段名
fileFieldName: {
type: String,
default: 'file'
},
// ✅ 新增:上传时携带的额外参数
extraData: {
type: Object,
default: () => ({})
}
})
// ======================
// Emits
// ======================
const emit = defineEmits(['update:modelValue', 'success', 'fail', 'remove'])
// ======================
// 内部数据
// ======================
const innerList = ref([])
// 监听外部数据变化
watch(
() => props.modelValue,
newVal => {
innerList.value = newVal.map(item => {
if (typeof item === 'string') {
return { url: item }
}
return item
})
},
{ immediate: true, deep: true }
)
// ======================
// 核心方法
// ======================
// 1. 选择图片
function choose() {
uni.chooseImage({
count: props.limit,
sourceType: props.sourceType,
sizeType: props.sizeType,
success: async res => {
const tempPaths = res.tempFilePaths
// 如果没传上传接口,只返回临时路径
if (!props.uploadUrl) {
const newItems = tempPaths.map(path => ({ url: path }))
innerList.value = [...innerList.value, ...newItems]
emit(
'update:modelValue',
innerList.value.map(i => i.url)
)
emit('success', { tempFilePaths: tempPaths })
return
}
// 有上传接口,自动上传
for (const path of tempPaths) {
await uploadSingle(path)
}
},
fail: err => {
console.log('选图失败', err)
emit('fail', err)
}
})
}
// 2. 上传单张图片
async function uploadSingle(filePath) {
// 先添加到列表,标记为上传中
const tempItem = { url: filePath, uploading: true }
innerList.value = [...innerList.value, tempItem]
return new Promise((resolve, reject) => {
uni.uploadFile({
url: props.uploadUrl,
filePath: filePath,
name: props.fileFieldName,
// ✅ 新增:携带额外参数
formData: props.extraData,
success: res => {
try {
const data = JSON.parse(res.data)
// 假设后端返回格式:{ code: 200, url: "https://xxx.com/xxx.png" }
// 如果你的后端返回格式不同,请在这里修改
if (data.code === 200 || data.url) {
const finalUrl = data.url || data.data?.url
// 更新列表,替换为网络 URL
const index = innerList.value.findIndex(item => item.url === filePath)
if (index !== -1) {
innerList.value[index] = { url: finalUrl, uploading: false }
}
emit(
'update:modelValue',
innerList.value.map(i => i.url)
)
emit('success', { url: finalUrl, tempPath: filePath })
resolve(finalUrl)
} else {
throw new Error(data.msg || '上传失败')
}
} catch (e) {
console.error('解析上传结果失败', e)
remove(innerList.value.findIndex(item => item.url === filePath))
emit('fail', e)
reject(e)
}
},
fail: err => {
console.error('上传失败', err)
remove(innerList.value.findIndex(item => item.url === filePath))
emit('fail', err)
reject(err)
}
})
})
}
// 3. 删除图片
function remove(index) {
const removed = innerList.value.splice(index, 1)[0]
emit(
'update:modelValue',
innerList.value.map(i => i.url)
)
emit('remove', { index, url: removed.url })
}
// 4. 预览图片
function preview(index) {
const urls = innerList.value.map(i => i.url)
uni.previewImage({
urls: urls,
current: index
})
}
</script>
<style scoped>
.simple-uploader {
width: 100%;
}
.image-list {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20rpx;
}
.image-item {
position: relative;
width: 160rpx;
height: 160rpx;
}
.img {
width: 100%;
height: 100%;
border-radius: 8rpx;
}
.delete-btn {
position: absolute;
top: 0rpx;
right: 0rpx;
width: 32rpx;
height: 32rpx;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
}
.uploading-mask {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
border-radius: 8rpx;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 24rpx;
}
.add-btn {
width: 160rpx;
height: 160rpx;
background-color: #f3f3f3;
display: flex;
align-items: center;
justify-content: center;
}
</style>

View File

@@ -27,7 +27,7 @@
<view class="nav-left" :style="{ width: leftWidth + 'px' }">
<slot name="left">
<view class="back-btn" @click="goBack" v-if="showBack">
<uni-icons type="left" size="20" color="inherit"></uni-icons>
<uni-icons type="left" size="22" color="inherit"></uni-icons>
<text v-if="backText" class="back-text">{{ backText }}</text>
</view>
</slot>
@@ -70,11 +70,12 @@ const props = withDefaults(defineProps<Props>(), {
backText: '',
placeholder: true // 默认开启占位,防止遮挡
})
const emits = defineEmits(['clickLeft'])
// --- 状态 ---
const statusBarHeight = ref(0)
const navBarHeight = ref(44)
const capsuleHeight = ref(0)
const gap = ref(0)
// --- 计算属性 ---
const totalNavHeight = computed(() => statusBarHeight.value + navBarHeight.value)
@@ -108,8 +109,8 @@ const initNavBar = () => {
// #ifdef MP-WEIXIN
const menuInfo = uni.getMenuButtonBoundingClientRect()
if (menuInfo.top && menuInfo.height) {
const gap = menuInfo.top - statusBarHeight.value
navBarHeight.value = (gap > 0 ? gap : 4) * 2 + menuInfo.height
gap.value = menuInfo.top - statusBarHeight.value
navBarHeight.value = (gap.value > 0 ? gap.value : 4) * 2 + menuInfo.height
capsuleHeight.value = menuInfo.height
} else {
navBarHeight.value = 44
@@ -123,7 +124,7 @@ const initNavBar = () => {
}
const goBack = () => {
uni.navigateBack()
emits('clickLeft')
}
</script>
@@ -159,14 +160,14 @@ const goBack = () => {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20rpx;
box-sizing: border-box;
padding: 0 20rpx;
padding-top: 10rpx;
}
.nav-left,
.nav-right {
display: flex;
align-items: center;
height: 100%;
}
@@ -180,16 +181,20 @@ const goBack = () => {
}
.nav-title {
font-size: 32rpx;
font-weight: 500;
font-size: 25rpx;
font-size: 28rpx;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: #000000;
line-height: 40rpx;
text-align: center;
}
.back-btn {
display: flex;
align-items: center;
font-weight: 600;
}
.back-text {

View File

@@ -0,0 +1,176 @@
<template>
<view class="steps-vertical">
<view class="step-item" v-for="(item, index) in steps" :key="index">
<!-- 左侧:圆点 + 竖线 -->
<view class="step-left">
<!-- 圆点 -->
<view class="step-circle" :class="getClass(index)">
<!-- 插槽:自定义圆点 -->
<slot v-if="$slots.circle" name="circle" :index="index" :status="getStatus(index)">
</slot>
<view v-else-if="getStatus(index) === 'finish'">
<uni-icons type="checkmarkempty" size="15" :color="props.color"></uni-icons>
</view>
<text v-else-if="getStatus(index) === 'process'">{{ index + 1 }}</text>
<text v-else-if="getStatus(index) === 'wait'">{{ index + 1 }}</text>
</view>
<!-- 竖线(最后一条不显示) -->
<view
class="step-line"
:class="{ finish: getStatus(index) === 'finish' }"
v-if="index < steps.length - 1"
></view>
</view>
<!-- 右侧:内容区域(支持插槽!) -->
<view class="step-right">
<!-- 默认标题 -->
<view
class="step-title"
:class="{ active: getStatus(index) !== 'wait' }"
v-if="!$slots.item"
>
{{ item.title }}
</view>
<!-- 默认描述 -->
<view class="step-desc" v-if="item.desc && !$slots.item">
{{ item.desc }}
</view>
<!-- 🔥 核心:自定义内容插槽 -->
<slot
name="item"
v-if="$slots.item"
:index="index"
:status="getStatus(index)"
:data="item"
></slot>
</view>
</view>
</view>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
// 步骤列表
steps: {
type: Array,
default: () => []
},
// 当前步骤(从 0 开始)
current: {
type: Number,
default: 0
},
// 主题色
color: {
type: String,
default: '#007aff'
}
})
// 获取状态:finish / process / wait
const getStatus = index => {
if (index < props.current) return 'finish'
if (index === props.current) return 'process'
return 'wait'
}
// 样式类
const getClass = index => {
return 'step-circle-' + getStatus(index)
}
</script>
<style scoped lang="scss">
.step-item {
display: flex;
position: relative;
}
/* 左侧 */
.step-left {
display: flex;
flex-direction: column;
align-items: center;
margin-right: 14rpx;
}
/* 圆点 */
.step-circle {
width: 48rpx;
height: 48rpx;
border-radius: 50%;
font-size: 32rpx;
line-height: 48rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
z-index: 10;
border: 3rpx solid transparent;
color: #999999;
border-color: #999999;
}
/* 已完成 */
.step-circle-finish {
background: transparent;
color: v-bind('props.color');
border-color: v-bind('props.color');
}
/* 进行中 */
.step-circle-process {
border-color: v-bind('props.color');
color: #fff;
background: v-bind('props.color');
}
/* 待完成 */
.step-circle-wait {
color: #999999;
border-color: #999999;
}
/* 竖线 */
.step-line {
width: 4rpx;
height: 100%;
background: #e5e5e5;
margin-top: 8rpx;
margin-bottom: 8rpx;
}
.step-line.finish {
background: v-bind('props.color');
}
/* 右侧内容 */
.step-right {
flex: 1;
}
.step-title {
font-size: 28rpx;
color: #666;
line-height: 1.4;
}
.step-title.active {
color: #333;
font-weight: 500;
}
.step-desc {
font-size: 24rpx;
color: #999;
margin-top: 8rpx;
line-height: 1.5;
}
</style>

View File

@@ -7,7 +7,7 @@
class="tabs_items"
:class="{
active: item.value === currentActiveValue,
line: showBottomLine
line: showBottomLine && item.value === currentActiveValue
}"
>
{{ item.label }}
@@ -62,7 +62,6 @@ const tabs = computed<TabItem[]>(() => {
// ======================
const currentActiveValue = ref<string | number>(props.activeValue ?? tabs.value[0]?.value ?? '')
console.log(currentActiveValue)
// 事件
const emit = defineEmits<{
update: [value: string | number]
@@ -90,22 +89,21 @@ const handleTabs = (item: TabItem) => {
margin-right: 60rpx;
font-size: 28rpx;
color: #222222;
position: relative;
&.active {
color: #4d5dfa;
position: relative;
}
&.line {
&::after {
content: '';
display: block;
width: 100%;
height: 8rpx;
background: linear-gradient(180deg, #4d5dfa 0%, #bac0fa 100%);
border-radius: 4rpx;
position: absolute;
left: 0;
bottom: 0;
&.line {
&::after {
content: '';
display: block;
width: 100%;
height: 8rpx;
background: linear-gradient(180deg, #4d5dfa 0%, #bac0fa 100%);
border-radius: 4rpx;
position: absolute;
left: 0;
bottom: 0;
}
}
}
}