巡检点上传图片修正
This commit is contained in:
261
wuye-manager-uniapp/components/filePicker/filePicker.vue
Normal file
261
wuye-manager-uniapp/components/filePicker/filePicker.vue
Normal 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>
|
||||
@@ -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 {
|
||||
|
||||
176
wuye-manager-uniapp/components/stops/stops.vue
Normal file
176
wuye-manager-uniapp/components/stops/stops.vue
Normal 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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user