图片显示

This commit is contained in:
马梓彭 2026-04-07 14:13:13 +08:00
parent 13ee12a4f9
commit 3f71d2c19f
15 changed files with 607 additions and 91 deletions

View File

@ -0,0 +1,68 @@
<template>
<teleport to="body">
<el-image-viewer
v-if="visible"
:url-list="urlList"
:initial-index="initialIndex"
:z-index="9999"
@close="handleClose"
@switch="handleSwitch"
/>
</teleport>
</template>
<script setup lang="ts" name="ImageViewer">
import { ref, watch } from 'vue';
interface ImageViewerProps {
modelValue: boolean;
urlList: string[];
initialIndex?: number;
}
const props = withDefaults(defineProps<ImageViewerProps>(), {
initialIndex: 0,
});
const emit = defineEmits<{
(e: 'update:modelValue', value: boolean): void;
(e: 'close'): void;
(e: 'switch', index: number): void;
}>();
const visible = ref(props.modelValue);
watch(
() => props.modelValue,
(val) => {
visible.value = val;
}
);
watch(visible, (val) => {
emit('update:modelValue', val);
});
const handleClose = () => {
visible.value = false;
emit('close');
};
const handleSwitch = (index: number) => {
emit('switch', index);
};
</script>
<style scoped lang="scss">
:deep(.el-image-viewer__wrapper) {
z-index: 9999 !important;
}
:deep(.el-image-viewer__mask) {
z-index: 9999 !important;
}
:deep(.el-image-viewer__canvas) {
z-index: 10000 !important;
}
</style>

View File

@ -0,0 +1,149 @@
<template>
<div class="preview-image-container" :style="containerStyle">
<img
:src="src"
:style="imageStyle"
:alt="alt"
class="preview-image"
:class="{ 'clickable': !disabled && canPreview }"
@click="handleClick"
loading="lazy"
/>
<template v-if="!disabled && canPreview && showZoomIcon">
<div class="preview-overlay" @click.stop="handleClick">
<el-icon class="zoom-icon">
<ZoomIn />
</el-icon>
</div>
</template>
</div>
<ImageViewer v-model="viewerVisible" :url-list="urlList" :initial-index="currentIndex" />
</template>
<script setup lang="ts" name="PreviewImage">
import { ref, computed } from 'vue';
import { ZoomIn } from '@element-plus/icons-vue';
import ImageViewer from '../ImageViewer/index.vue';
interface PreviewImageProps {
src: string;
alt?: string;
width?: string | number;
height?: string | number;
fit?: 'contain' | 'cover' | 'fill' | 'none' | 'scale-down';
radius?: string;
disabled?: boolean;
urlList?: string[];
showZoomIcon?: boolean;
border?: boolean;
}
const props = withDefaults(defineProps<PreviewImageProps>(), {
alt: '',
width: '100%',
height: 'auto',
fit: 'cover',
radius: '0',
disabled: false,
urlList: () => [],
showZoomIcon: true,
border: false,
});
const viewerVisible = ref(false);
const currentIndex = ref(0);
const canPreview = computed(() => {
return props.urlList.length > 0 || props.src;
});
const urlList = computed(() => {
if (props.urlList.length > 0) {
return props.urlList;
}
return [props.src];
});
const containerStyle = computed(() => {
const style: Record<string, string | number> = {
width: typeof props.width === 'number' ? `${props.width}px` : props.width,
height: typeof props.height === 'number' ? `${props.height}px` : props.height,
borderRadius: props.radius,
};
if (props.border) {
style.border = '1px solid var(--el-border-color)';
}
return style;
});
const imageStyle = computed(() => {
return {
width: '100%',
height: '100%',
objectFit: props.fit,
borderRadius: props.radius,
};
});
const handleClick = () => {
if (props.disabled || !canPreview.value) return;
if (props.urlList.length > 0) {
currentIndex.value = props.urlList.indexOf(props.src);
if (currentIndex.value === -1) {
currentIndex.value = 0;
}
} else {
currentIndex.value = 0;
}
viewerVisible.value = true;
};
</script>
<style scoped lang="scss">
.preview-image-container {
position: relative;
display: inline-block;
overflow: hidden;
background-color: var(--el-fill-color-light);
}
.preview-image {
display: block;
transition: transform 0.3s ease;
}
.preview-image.clickable {
cursor: pointer;
}
.preview-image-container:hover .preview-image.clickable {
transform: scale(1.05);
}
.preview-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(0, 0, 0, 0.3);
opacity: 0;
transition: opacity 0.3s ease;
cursor: pointer;
z-index: 10;
.zoom-icon {
font-size: 32px;
color: white;
}
}
.preview-image-container:hover .preview-overlay {
opacity: 1;
}
</style>

View File

@ -2,12 +2,14 @@ import type { App } from 'vue';
import { authDirective } from '/@/directive/authDirective';
import { wavesDirective, dragDirective } from '/@/directive/customDirective';
import { debounceDirective } from './debounceDirective';
import { previewDirective } from './previewDirective';
/**
* 导出指令方法:v-xxx
* @methods authDirective 用户权限指令,用法:v-auth
* @methods wavesDirective 按钮波浪指令,用法:v-waves
* @methods dragDirective 自定义拖动指令,用法:v-drag
* @methods previewDirective 图片预览指令,用法:v-preview
*/
export function directive(app: App) {
// 用户权限指令
@ -19,4 +21,7 @@ export function directive(app: App) {
//防抖指令
debounceDirective(app);
// 图片预览指令
app.directive('preview', previewDirective);
}

View File

@ -0,0 +1,40 @@
import type { Directive, DirectiveBinding } from 'vue';
import { useImagePreview } from '../utils/imagePreview';
const { previewSingleImage } = useImagePreview();
export const previewDirective: Directive = {
mounted(el: HTMLElement, binding: DirectiveBinding) {
const imgEl = el.tagName === 'IMG' ? el : el.querySelector('img');
if (!imgEl) return;
const urlList = binding.value?.urlList || [];
const disabled = binding.value?.disabled || false;
if (!disabled) {
imgEl.style.cursor = 'pointer';
imgEl.addEventListener('click', (e) => {
e.stopPropagation();
const src = (imgEl as HTMLImageElement).src;
if (urlList.length > 0) {
const { showImagePreview } = useImagePreview();
const index = urlList.indexOf(src);
showImagePreview({
urlList,
initialIndex: index >= 0 ? index : 0,
});
} else {
previewSingleImage(src);
}
});
}
},
unmounted(el: HTMLElement) {
const imgEl = el.tagName === 'IMG' ? el : el.querySelector('img');
if (!imgEl) return;
imgEl.removeEventListener('click', () => {});
},
};
export default previewDirective;

71
src/utils/imagePreview.ts Normal file
View File

@ -0,0 +1,71 @@
import { createApp, markRaw, h, ref, type App } from 'vue';
import ImageViewer from '../components/ImageViewer/index.vue';
interface ImagePreviewOptions {
urlList: string[];
initialIndex?: number;
}
let imageViewerInstance: any = null;
export const useImagePreview = () => {
const showImagePreview = (options: ImagePreviewOptions) => {
const { urlList, initialIndex = 0 } = options;
if (!urlList || urlList.length === 0) {
return;
}
if (imageViewerInstance) {
imageViewerInstance.unmount();
imageViewerInstance = null;
}
const container = document.createElement('div');
document.body.appendChild(container);
const app = createApp({
components: { ImageViewer },
setup() {
const visible = ref(true);
const handleClose = () => {
visible.value = false;
setTimeout(() => {
app.unmount();
if (document.body.contains(container)) {
document.body.removeChild(container);
}
imageViewerInstance = null;
}, 300);
};
return () =>
h(ImageViewer, {
modelValue: visible.value,
'onUpdate:modelValue': (val: boolean) => {
visible.value = val;
if (!val) {
handleClose();
}
},
urlList,
initialIndex,
onClose: handleClose,
});
},
});
imageViewerInstance = app;
app.mount(container);
};
const previewSingleImage = (url: string) => {
showImagePreview({ urlList: [url] });
};
return {
showImagePreview,
previewSingleImage,
};
};

View File

@ -17,20 +17,16 @@
<el-row :gutter="15" v-if="state.tableData.data.length > 0" v-loading="state.loading">
<el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="4" class="mb15" v-for="v in state.tableData.data" :key="v.id">
<div class="img-box">
<el-image style="width: 250px; height: 180px" :src="v.url" fit="cover" lazy />
<PreviewImage
:src="v.url"
width="250px"
height="180px"
fit="cover"
radius="8px"
:url-list="imageUrlList"
:show-zoom-icon="true"
/>
<div class="operate">
<div
class="handle-icon"
@click="
() => {
imgViewVisible = true;
imageUrl = v.url!;
}
"
>
<el-icon><ZoomIn /></el-icon>
<span>查看</span>
</div>
<div class="handle-icon" @click="onDeleteImg(v.id!)">
<el-icon><Delete /></el-icon>
<span>删除</span>
@ -55,18 +51,17 @@
</el-pagination>
</template>
</el-card>
<el-image-viewer v-if="imgViewVisible" @close="imgViewVisible = false" :url-list="[imageUrl]" />
</div>
</template>
<script setup lang="ts" name="blogPictures">
import { reactive, onMounted, ref, nextTick } from 'vue';
import { reactive, onMounted, ref, computed, nextTick } from 'vue';
import PicturesApi from '/@/api/PicturesApi';
import { useRoute } from 'vue-router';
import type { PicturesPageOutput } from '/@/api/models';
// 查看图片
const imgViewVisible = ref(false);
const imageUrl = ref('');
import PreviewImage from '/@/components/PreviewImage/index.vue';
import { Delete } from '@element-plus/icons-vue';
// 定义变量内容
const route = useRoute();
const state = reactive({
@ -83,6 +78,10 @@ const state = reactive({
loading: false,
});
const imageUrlList = computed(() => {
return state.tableData.data.map((v) => v.url!).filter(Boolean);
});
// 分页点击
const onHandleSizeChange = (val: number) => {
state.tableData.param.pageSize = val;
@ -124,18 +123,7 @@ onMounted(async () => {
<style scoped lang="scss">
.img-box {
position: relative;
height: 180px;
width: 250px;
&:hover {
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.03);
opacity: 1;
box-sizing: border-box;
:deep(img) {
transition: all 0.3s ease;
transform: translateZ(0) scale(1.05);
}
.operate {
opacity: 1;
}
@ -147,28 +135,26 @@ onMounted(async () => {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
justify-content: flex-end;
width: 100%;
height: 100%;
cursor: pointer;
background: rgb(0 0 0 / 60%);
opacity: 0;
transition: var(--el-transition-duration-fast);
background: linear-gradient(to top, rgba(0, 0, 0, 0.5) 0%, transparent 50%);
.handle-icon {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 0 6%;
padding: 10px;
color: aliceblue;
.el-icon {
margin-bottom: 40%;
font-size: 130%;
line-height: 130%;
font-size: 20px;
}
span {
font-size: 85%;
line-height: 85%;
font-size: 12px;
margin-top: 4px;
}
}
}

View File

@ -12,14 +12,29 @@
</el-form-item>
<el-form-item label="营业执照:" prop="businessLicenseUrl">
<el-upload class="avatar-uploader" action="/api/file/upload" :show-file-list="false"
:on-success="handleUploadChange">
<div v-if="!formData.businessLicenseUrl" class="upload-placeholder">
<i class="el-icon-plus avatar-uploader-icon"></i>
<div>上传</div>
<div class="image-upload-container">
<div class="upload-area">
<el-upload class="avatar-uploader" action="/api/file/upload" :show-file-list="false"
:on-success="handleUploadChange">
<div class="upload-placeholder">
<i class="el-icon-plus avatar-uploader-icon"></i>
<div>上传</div>
</div>
</el-upload>
</div>
<el-image v-else :src="formData.businessLicenseUrl" alt="营业执照" style=" height: 200px;" />
</el-upload>
<div v-if="formData.businessLicenseUrl" class="preview-area">
<PreviewImage
:src="formData.businessLicenseUrl"
width="100%"
height="100%"
fit="contain"
:show-zoom-icon="true"
/>
</div>
<div v-else class="preview-area preview-empty">
<span class="preview-empty-text">预览区域</span>
</div>
</div>
</el-form-item>
<el-form-item label="地址:" prop="address">
@ -54,6 +69,7 @@ import { ref, reactive, onMounted, computed } from 'vue';
import { ElMessage } from 'element-plus';
import { merchantcertificationMyCertificationGet, merchantcertificationSubmitCertificationPost } from '/@/api/match/merchantcertification';
import { commonSmsSendVerificationCodePost } from '/@/api/match/common';
import PreviewImage from '/@/components/PreviewImage/index.vue';
const formRef = ref(null);
@ -108,7 +124,7 @@ const beforeUpload = () => {
};
const handleUploadChange = (response) => {
formData.businessLicenseUrl = response[0].url;
formData.businessLicenseUrl = response?.[0]?.url;
};
const loadData = async () => {
@ -190,6 +206,45 @@ onMounted(() => {
</script>
<style scoped>
.image-upload-container {
display: flex;
gap: 20px;
align-items: flex-start;
}
.upload-area {
flex-shrink: 0;
}
.preview-area {
width: 280px;
height: 200px;
border: 1px solid var(--el-border-color);
border-radius: 8px;
overflow: hidden;
background-color: var(--el-fill-color-light);
display: flex;
align-items: center;
justify-content: center;
}
.preview-empty {
border: 1px dashed var(--el-border-color);
}
.preview-empty-text {
color: var(--el-text-color-secondary);
font-size: 14px;
}
.avatar-uploader .avatar {
width: 178px;
height: 178px;
display: block;
}
</style>
<style>
.avatar-uploader .el-upload {
border: 1px dashed var(--el-border-color);
border-radius: 6px;
@ -197,23 +252,22 @@ onMounted(() => {
position: relative;
overflow: hidden;
transition: var(--el-transition-duration-fast);
width: 220px;
height: 160px;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
}
.avatar-uploader .el-upload:hover {
border-color: var(--el-color-primary);
}
.avatar-uploader .avatar-uploader-icon {
.el-icon.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 178px;
height: 178px;
text-align: center;
}
</style>
<style scoped>
.upload-placeholder {
color: #8c939d;
text-align: center;

View File

@ -7,15 +7,40 @@
<el-form-item label="封面" prop="coverImage">
<div v-if="readonly && localFormData.coverImage">
<img :src="localFormData.coverImage" class="avatar" />
<PreviewImage
:src="localFormData.coverImage"
width="178px"
height="178px"
fit="cover"
radius="8px"
:show-zoom-icon="true"
/>
</div>
<div v-else class="image-upload-container">
<div class="upload-area">
<el-upload class="avatar-uploader" action="/api/file/upload" :show-file-list="false"
:on-success="handleAvatarSuccess" :disabled="readonly">
<div class="upload-placeholder">
<el-icon class="avatar-uploader-icon">
<Plus />
</el-icon>
<div>上传</div>
</div>
</el-upload>
</div>
<div v-if="localFormData.coverImage" class="preview-area">
<PreviewImage
:src="localFormData.coverImage"
width="100%"
height="100%"
fit="cover"
:show-zoom-icon="true"
/>
</div>
<div v-else class="preview-area preview-empty">
<span class="preview-empty-text">预览区域</span>
</div>
</div>
<el-upload v-else class="avatar-uploader" action="/api/file/upload" :show-file-list="false"
:on-success="handleAvatarSuccess" :disabled="readonly">
<img v-if="localFormData.coverImage" :src="localFormData.coverImage" class="avatar" />
<el-icon v-else class="avatar-uploader-icon">
<Plus />
</el-icon>
</el-upload>
</el-form-item>
<el-form-item label="赛事类型" prop="eventType">
@ -94,6 +119,7 @@ import { ref, watch, onMounted, nextTick } from 'vue';
import { Plus } from '@element-plus/icons-vue';
import { pointsrulePageGet } from '/@/api/match/pointsrule';
import { regionProvincesGet, regionCitiesbyprovinceGet, regionDistrictsbycityGet } from '/@/api/match/area';
import PreviewImage from '/@/components/PreviewImage/index.vue';
const props = defineProps({
modelValue: { type: Object, required: true },
@ -339,6 +365,42 @@ defineExpose({ validate });
margin-top: 20px;
}
.image-upload-container {
display: flex;
gap: 20px;
align-items: flex-start;
}
.upload-area {
flex-shrink: 0;
}
.preview-area {
width: 178px;
height: 178px;
border: 1px solid var(--el-border-color);
border-radius: 8px;
overflow: hidden;
background-color: var(--el-fill-color-light);
display: flex;
align-items: center;
justify-content: center;
}
.preview-empty {
border: 1px dashed var(--el-border-color);
}
.preview-empty-text {
color: var(--el-text-color-secondary);
font-size: 14px;
}
.upload-placeholder {
color: #8c939d;
text-align: center;
}
.avatar-uploader .avatar {
width: 178px;
height: 178px;

View File

@ -43,7 +43,16 @@
</el-tag>
</template>
<template #paymentProofUrl="scope">
<el-image :src="scope.row.paymentProofUrl" style="height: 80px" fit="cover" />
<PreviewImage
v-if="scope.row.paymentProofUrl"
:src="scope.row.paymentProofUrl"
width="80px"
height="80px"
fit="cover"
radius="4px"
:show-zoom-icon="true"
/>
<span v-else>-</span>
</template>
<template #action="scope">
<el-popconfirm title="确认审核完毕吗?" @confirm="onSureOrder(scope.row.id)">
@ -163,6 +172,7 @@ import Step2Units from './Steps/Step2Units.vue';
import Step3ProjectConfig from './Steps/Step3ProjectConfig.vue';
import Step4FeeConfig from './Steps/Step4FeeConfig.vue';
import ProTable from '/@/components/ProTable/index.vue';
import PreviewImage from '/@/components/PreviewImage/index.vue';
const isShowDialog = ref(false);

View File

@ -99,7 +99,7 @@ const formRef = ref();
const buildEmptyForm = () => ({
id: undefined as number | undefined,
ruleName: '',
participationPoints: 0,
participationPoints: undefined as number | undefined,
remark: '',
details: [] as Array<{ id?: number; startRank: number; endRank: number; points: number }>,
});
@ -108,7 +108,6 @@ const formData = ref(buildEmptyForm());
const formRules = ref({
ruleName: [{ required: true, message: '请输入规则名称', trigger: 'blur' }],
participationPoints: [{ required: true, message: '请输入参与积分', trigger: 'change' }],
});
const columns = ref([
@ -139,7 +138,7 @@ const onOpenDialog = async (row?: any) => {
formData.value = {
id: res.data?.id ?? row.id,
ruleName: res.data?.ruleName ?? row.ruleName ?? '',
participationPoints: res.data?.participationPoints ?? row.participationPoints ?? 0,
participationPoints: res.data?.participationPoints ?? row.participationPoints,
remark: res.data?.remark ?? row.remark ?? '',
details: (res.data?.details ?? []).map((d: any) => ({
id: d.id,

View File

@ -22,7 +22,16 @@
</template>
<template #idCardImageUrl="scope">
<el-image style="width: 100px;" :src="scope.row.idCardImageUrl" fit="cover"></el-image>
<PreviewImage
v-if="scope.row.idCardImageUrl"
:src="scope.row.idCardImageUrl"
width="100px"
height="100px"
fit="cover"
radius="4px"
:show-zoom-icon="true"
/>
<span v-else>-</span>
</template>
<template #action="scope">
<el-button v-auth="'user:general:record'" size="small" text type="primary"
@ -76,6 +85,7 @@ import { auths } from '/@/utils/authFunction';
import ProTable from '/@/components/ProTable/index.vue';
import { appuserPageGet , appuserAuditPost} from '/@/api/match/appuser';
import { eventrecordPageGet , eventrecordCertificatesGet} from '/@/api/match/eventRecord';
import PreviewImage from '/@/components/PreviewImage/index.vue';
const initParam = ref({
refresh: true,

View File

@ -15,7 +15,16 @@
</el-tag>
</template>
<template #businessLicenseUrl="scope">
<el-image style="width: 100px;" :src="scope.row.businessLicenseUrl" fit="cover"></el-image>
<PreviewImage
v-if="scope.row.businessLicenseUrl"
:src="scope.row.businessLicenseUrl"
width="100px"
height="100px"
fit="cover"
radius="4px"
:show-zoom-icon="true"
/>
<span v-else>-</span>
</template>
<template #action="scope">
<el-button v-auth="'user:general:record'" size="small" text type="primary"
@ -89,6 +98,7 @@ import { auths } from '/@/utils/authFunction';
import ProTable from '/@/components/ProTable/index.vue';
import { eventrecordPageGet , eventrecordCertificatesGet} from '/@/api/match/eventRecord';
import { merchantcertificationPageGet, merchantcertificationAuditPost} from '/@/api/match/merchantcertification';
import PreviewImage from '/@/components/PreviewImage/index.vue';
const initParam = ref({
refresh: true,

View File

@ -67,12 +67,12 @@
</template>
<script setup name="sysMenu">
import { defineAsyncComponent, ref, reactive, onMounted } from 'vue';
import { defineAsyncComponent, ref, reactive, onMounted, computed } from 'vue';
import { ElMessage, ElForm } from 'element-plus';
import SysMenuApi from '/@/api/SysMenuApi';
import { auths } from '/@/utils/authFunction';
import ProTable from '/@/components/ProTable/index.vue';
import { sysuserPageGet, sysuserUserPost, sysuserUserPut, sysuserDelete } from '/@/api/match/account';
import { sysuserPageGet, sysuserUserPost, sysuserUserPut, sysuserDelete, sysuserDetailGet } from '/@/api/match/account';
import { sysroleSelectGet } from '/@/api/match/role';
// 定义表单引用类型
@ -99,20 +99,28 @@ const validateConfirmPassword = (rule, value, callback) => {
}
};
const formRules = ref({
name: [
{ required: true, message: '姓名', trigger: 'blur' }
],
account: [
{ required: true, message: '请输入账号', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, max: 32, message: '密码长度在 6 到 32 个字符', trigger: 'blur' }
],
confirmPassword: [
{ required: true, validator: validateConfirmPassword, trigger: 'blur' }
],
const formRules = computed(() => {
const rules = {
name: [
{ required: true, message: '姓名', trigger: 'blur' }
],
account: [
{ required: true, message: '请输入账号', trigger: 'blur' }
],
};
// 只有新增时才需要验证密码和确认密码
if (!isEdit.value) {
rules.password = [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, max: 32, message: '密码长度在 6 到 32 个字符', trigger: 'blur' }
];
rules.confirmPassword = [
{ required: true, validator: validateConfirmPassword, trigger: 'blur' }
];
}
return rules;
});
const columns = ref([
@ -156,12 +164,23 @@ const columns = ref([
const isShowDialog = ref(false);
const dialogTitle = ref('新增');
const isEdit = ref(false);
const onOpenMenu = async (row) => {
dialogTitle.value = row ? '编辑' : '新增';
isEdit.value = !!row;
if (row) {
formData.value = row;
// 调用详情接口获取完整的用户信息,包括角色
const { data } = await sysuserDetailGet({ id: row.id });
formData.value = {
id: data.id,
name: data.name,
account: data.account,
password: '',
confirmPassword: '',
roles: data.roles || []
};
} else {
formData.value = {
name: '',
@ -195,7 +214,9 @@ const onSubmit = () => {
try {
let res;
if (formData.value.id) {
res = await sysuserUserPut(formData.value);
// 编辑时,只发送需要更新的字段,不发送密码和确认密码
const { password, confirmPassword, ...updateData } = formData.value;
res = await sysuserUserPut(updateData);
} else {
res = await sysuserUserPost(formData.value);
}

View File

@ -60,7 +60,7 @@
<el-input v-model="teamFormData.name" placeholder="请输入团队名称" />
</el-form-item>
<el-form-item label="队员">
<el-form-item label="队员" prop="memberList">
<div>
<el-button type="primary" @click="openMemberDialog">添加队员</el-button>
<div style="margin-top:8px">
@ -319,8 +319,35 @@ const teamFormData = ref({
memberNames: '',
});
const validateMemberList = (rule, value, callback) => {
if (!teamFormData.value.memberList || teamFormData.value.memberList.length === 0) {
callback(new Error('请至少选择一名队员'));
} else {
callback();
}
};
const validateLeaderIds = (rule, value, callback) => {
if (!value || value.length === 0) {
callback(new Error('请选择领队'));
} else {
callback();
}
};
const validateCoachIds = (rule, value, callback) => {
if (!value || value.length === 0) {
callback(new Error('请选择教练'));
} else {
callback();
}
};
const teamFormRules = ref({
name: [{ required: true, message: '请输入团队名称', trigger: 'blur' }],
memberList: [{ required: true, validator: validateMemberList, trigger: ['change', 'blur'] }],
leaderIds: [{ required: true, validator: validateLeaderIds, trigger: ['change', 'blur'] }],
coachIds: [{ required: true, validator: validateCoachIds, trigger: ['change', 'blur'] }],
});
// 成员选择弹窗(使用 ProTable 的多选能力)
@ -472,10 +499,6 @@ const cancelMemberSelection = () => {
// 提交新增团队
const onTeamSubmit = () => {
if (!teamFormRef.value) return;
if(!teamFormData.value.name) return ElMessage.error('请输入团队名称');
if(!teamFormData.value.memberIds.length) return ElMessage.error('请选择队员');
if(!teamFormData.value.leaderIds.length) return ElMessage.error('请选择领队');
if(!teamFormData.value.coachIds.length) return ElMessage.error('请选择教练');
teamFormRef.value.validate(async (valid) => {
if (!valid) return;
try {

View File

@ -3,8 +3,15 @@
<ProTable ref="tableRef" :request-api="usereventregistrationListGet" :columns="columns" :pagination="true"
:tool-button="false" :init-param="initParam" row-key="id">
<template #coverImage="scope">
<el-image v-if="scope.row?.coverImage" style="height: 60px; width: 80px"
:src="scope.row.coverImage" fit="cover" />
<PreviewImage
v-if="scope.row?.coverImage"
:src="scope.row.coverImage"
width="80px"
height="60px"
fit="cover"
radius="4px"
:show-zoom-icon="true"
/>
<span v-else>-</span>
</template>
@ -444,6 +451,7 @@ import Step1BasicInfo from '/@/views/match/manage/Steps/Step1BasicInfo.vue';
import Step2Units from '/@/views/match/manage/Steps/Step2Units.vue';
import Step3ProjectConfig from '/@/views/match/manage/Steps/Step3ProjectConfig.vue';
import Step4FeeConfig from '/@/views/match/manage/Steps/Step4FeeConfig.vue';
import PreviewImage from '/@/components/PreviewImage/index.vue';
const tableRef = ref();
const initParam = ref({ refresh: true });